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_state_mapper.cpp
|
||||||
src/wallet/lite_wallet_lifecycle_ui_adapter.cpp
|
src/wallet/lite_wallet_lifecycle_ui_adapter.cpp
|
||||||
src/wallet/lite_wallet_server_selection_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/wallet/lite_wallet_lifecycle_service.cpp
|
||||||
src/data/wallet_state.cpp
|
src/data/wallet_state.cpp
|
||||||
src/data/transaction_history_cache.cpp
|
src/data/transaction_history_cache.cpp
|
||||||
@@ -475,10 +474,8 @@ set(APP_SOURCES
|
|||||||
src/ui/windows/shield_dialog.cpp
|
src/ui/windows/shield_dialog.cpp
|
||||||
src/ui/windows/request_payment_dialog.cpp
|
src/ui/windows/request_payment_dialog.cpp
|
||||||
src/ui/windows/block_info_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_all_keys_dialog.cpp
|
||||||
src/ui/windows/export_transactions_dialog.cpp
|
src/ui/windows/export_transactions_dialog.cpp
|
||||||
src/ui/windows/backup_wallet_dialog.cpp
|
|
||||||
src/ui/widgets/qr_code.cpp
|
src/ui/widgets/qr_code.cpp
|
||||||
src/rpc/rpc_client.cpp
|
src/rpc/rpc_client.cpp
|
||||||
src/rpc/rpc_worker.cpp
|
src/rpc/rpc_worker.cpp
|
||||||
@@ -506,6 +503,7 @@ set(APP_SOURCES
|
|||||||
src/util/lite_server_probe.cpp
|
src/util/lite_server_probe.cpp
|
||||||
src/util/pool_registry_core.cpp
|
src/util/pool_registry_core.cpp
|
||||||
src/util/pool_stats_service.cpp
|
src/util/pool_stats_service.cpp
|
||||||
|
src/util/http_download.cpp
|
||||||
src/util/xmrig_updater.cpp
|
src/util/xmrig_updater.cpp
|
||||||
src/util/xmrig_updater_core.cpp
|
src/util/xmrig_updater_core.cpp
|
||||||
src/util/daemon_updater.cpp
|
src/util/daemon_updater.cpp
|
||||||
@@ -557,7 +555,6 @@ set(APP_HEADERS
|
|||||||
src/wallet/lite_wallet_state_mapper.h
|
src/wallet/lite_wallet_state_mapper.h
|
||||||
src/wallet/lite_wallet_lifecycle_ui_adapter.h
|
src/wallet/lite_wallet_lifecycle_ui_adapter.h
|
||||||
src/wallet/lite_wallet_server_selection_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/wallet/lite_wallet_lifecycle_service.h
|
||||||
src/chat/chat_protocol.h
|
src/chat/chat_protocol.h
|
||||||
src/config/version.h
|
src/config/version.h
|
||||||
@@ -601,10 +598,8 @@ set(APP_HEADERS
|
|||||||
src/ui/windows/shield_dialog.h
|
src/ui/windows/shield_dialog.h
|
||||||
src/ui/windows/request_payment_dialog.h
|
src/ui/windows/request_payment_dialog.h
|
||||||
src/ui/windows/block_info_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_all_keys_dialog.h
|
||||||
src/ui/windows/export_transactions_dialog.h
|
src/ui/windows/export_transactions_dialog.h
|
||||||
src/ui/windows/backup_wallet_dialog.h
|
|
||||||
src/ui/widgets/qr_code.h
|
src/ui/widgets/qr_code.h
|
||||||
src/rpc/rpc_client.h
|
src/rpc/rpc_client.h
|
||||||
src/rpc/rpc_worker.h
|
src/rpc/rpc_worker.h
|
||||||
@@ -825,6 +820,7 @@ endif()
|
|||||||
add_executable(HushChatFixtureCheck
|
add_executable(HushChatFixtureCheck
|
||||||
tools/hushchat_fixture_check.cpp
|
tools/hushchat_fixture_check.cpp
|
||||||
src/chat/chat_protocol.cpp
|
src/chat/chat_protocol.cpp
|
||||||
|
src/chat/chat_fixture_tooling.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
target_include_directories(HushChatFixtureCheck PRIVATE
|
target_include_directories(HushChatFixtureCheck PRIVATE
|
||||||
@@ -1020,7 +1016,6 @@ if(BUILD_TESTING)
|
|||||||
src/wallet/lite_wallet_state_mapper.cpp
|
src/wallet/lite_wallet_state_mapper.cpp
|
||||||
src/wallet/lite_wallet_lifecycle_ui_adapter.cpp
|
src/wallet/lite_wallet_lifecycle_ui_adapter.cpp
|
||||||
src/wallet/lite_wallet_server_selection_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/wallet/lite_wallet_lifecycle_service.cpp
|
||||||
src/ui/explorer/explorer_block_cache.cpp
|
src/ui/explorer/explorer_block_cache.cpp
|
||||||
src/ui/windows/balance_address_list.cpp
|
src/ui/windows/balance_address_list.cpp
|
||||||
@@ -1051,6 +1046,7 @@ if(BUILD_TESTING)
|
|||||||
src/util/logger.cpp
|
src/util/logger.cpp
|
||||||
src/util/lite_server_probe.cpp
|
src/util/lite_server_probe.cpp
|
||||||
src/util/pool_registry_core.cpp
|
src/util/pool_registry_core.cpp
|
||||||
|
src/util/http_download.cpp
|
||||||
src/util/xmrig_updater.cpp
|
src/util/xmrig_updater.cpp
|
||||||
src/util/xmrig_updater_core.cpp
|
src/util/xmrig_updater_core.cpp
|
||||||
src/util/daemon_updater.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-secondary = "#FFFFFF"
|
||||||
--on-background = "#E8E6F0"
|
--on-background = "#E8E6F0"
|
||||||
--on-surface = "#E8E6F0"
|
--on-surface = "#E8E6F0"
|
||||||
--on-surface-medium = "rgba(232,230,240,0.72)"
|
--on-surface-medium = "rgba(232,230,240,0.85)"
|
||||||
--on-surface-disabled = "rgba(232,230,240,0.40)"
|
--on-surface-disabled = "rgba(232,230,240,0.58)"
|
||||||
--error = "#FF5C72"
|
--error = "#FF5C72"
|
||||||
--on-error = "#000000"
|
--on-error = "#000000"
|
||||||
--success = "#3DE8A0"
|
--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-badge = "rgba(232,230,240,1.0)"
|
||||||
--sidebar-divider = "rgba(200,190,240,0.05)"
|
--sidebar-divider = "rgba(200,190,240,0.05)"
|
||||||
--chart-line = "rgba(124,108,255,0.12)"
|
--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-control-hover = "rgba(124,108,255,0.12)"
|
||||||
--window-close-hover = "rgba(255,92,114,0.75)"
|
--window-close-hover = "rgba(255,92,114,0.75)"
|
||||||
--spinner-track = "rgba(200,190,240,0.08)"
|
--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-secondary = "#FFFFFF"
|
||||||
--on-background = "#1E1E2A"
|
--on-background = "#1E1E2A"
|
||||||
--on-surface = "#1E1E2A"
|
--on-surface = "#1E1E2A"
|
||||||
--on-surface-medium = "rgba(30,30,42,0.72)"
|
--on-surface-medium = "rgba(30,30,42,0.86)"
|
||||||
--on-surface-disabled = "rgba(30,30,42,0.38)"
|
--on-surface-disabled = "rgba(30,30,42,0.62)"
|
||||||
--error = "#E0304A"
|
--error = "#E0304A"
|
||||||
--on-error = "#FFFFFF"
|
--on-error = "#FFFFFF"
|
||||||
--success = "#18A860"
|
--success = "#18A860"
|
||||||
--on-success = "#FFFFFF"
|
--on-success = "#FFFFFF"
|
||||||
--warning = "#E09020"
|
--warning = "#E09020"
|
||||||
--on-warning = "#000000"
|
--on-warning = "#000000"
|
||||||
--divider = "rgba(30,30,60,0.12)"
|
--divider = "rgba(30,30,60,0.20)"
|
||||||
--outline = "rgba(30,30,60,0.15)"
|
--outline = "rgba(30,30,60,0.24)"
|
||||||
--scrim = "rgba(0,0,0,0.45)"
|
--scrim = "rgba(0,0,0,0.45)"
|
||||||
--surface-hover = "rgba(96,64,224,0.05)"
|
--surface-hover = "rgba(96,64,224,0.05)"
|
||||||
--surface-alt = "rgba(96,64,224,0.02)"
|
--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)"
|
--chart-hover-ring = "rgba(96,64,224,0.30)"
|
||||||
--tooltip-bg = "rgba(36,34,52,0.94)"
|
--tooltip-bg = "rgba(36,34,52,0.94)"
|
||||||
--tooltip-border = "rgba(96,64,224,0.16)"
|
--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-border = "rgba(30,30,60,0.10)"
|
||||||
--glass-noise-tint = "rgba(96,64,224,0.02)"
|
--glass-noise-tint = "rgba(96,64,224,0.02)"
|
||||||
--tactile-top = "rgba(255,255,255,0.40)"
|
--tactile-top = "rgba(255,255,255,0.40)"
|
||||||
--tactile-bottom = "rgba(255,255,255,0.05)"
|
--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)"
|
--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)"
|
--status-divider = "rgba(30,30,60,0.08)"
|
||||||
--sidebar-hover = "rgba(96,64,224,0.07)"
|
--sidebar-hover = "rgba(96,64,224,0.07)"
|
||||||
--sidebar-icon = "rgba(30,30,42,0.50)"
|
--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-secondary = "#000000"
|
||||||
--on-background = "#D0D0D4"
|
--on-background = "#D0D0D4"
|
||||||
--on-surface = "#D0D0D4"
|
--on-surface = "#D0D0D4"
|
||||||
--on-surface-medium = "rgba(208,208,212,0.75)"
|
--on-surface-medium = "rgba(208,208,212,0.85)"
|
||||||
--on-surface-disabled = "rgba(208,208,212,0.45)"
|
--on-surface-disabled = "rgba(208,208,212,0.58)"
|
||||||
--error = "#B07080"
|
--error = "#B07080"
|
||||||
--on-error = "#000000"
|
--on-error = "#000000"
|
||||||
--success = "#7AAE7C"
|
--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-badge = "rgba(208,208,212,1.0)"
|
||||||
--sidebar-divider = "rgba(220,220,225,0.05)"
|
--sidebar-divider = "rgba(220,220,225,0.05)"
|
||||||
--chart-line = "rgba(220,220,225,0.08)"
|
--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-control-hover = "rgba(220,220,225,0.10)"
|
||||||
--window-close-hover = "rgba(200,50,60,0.70)"
|
--window-close-hover = "rgba(200,50,60,0.70)"
|
||||||
--spinner-track = "rgba(220,220,225,0.08)"
|
--spinner-track = "rgba(220,220,225,0.08)"
|
||||||
|
|||||||
@@ -20,16 +20,16 @@ elevation = { --elevation-0 = "#FDF8F0", --elevation-1 = "#F5EDE0", --elevation-
|
|||||||
--on-secondary = "#FFFFFF"
|
--on-secondary = "#FFFFFF"
|
||||||
--on-background = "#3A2E22"
|
--on-background = "#3A2E22"
|
||||||
--on-surface = "#3A2E22"
|
--on-surface = "#3A2E22"
|
||||||
--on-surface-medium = "rgba(58,46,34,0.68)"
|
--on-surface-medium = "rgba(58,46,34,0.86)"
|
||||||
--on-surface-disabled = "rgba(58,46,34,0.38)"
|
--on-surface-disabled = "rgba(58,46,34,0.62)"
|
||||||
--error = "#A0524A"
|
--error = "#A0524A"
|
||||||
--on-error = "#FFFFFF"
|
--on-error = "#FFFFFF"
|
||||||
--success = "#6A8A5C"
|
--success = "#4E8A42"
|
||||||
--on-success = "#FFFFFF"
|
--on-success = "#FFFFFF"
|
||||||
--warning = "#C08840"
|
--warning = "#C08840"
|
||||||
--on-warning = "#000000"
|
--on-warning = "#000000"
|
||||||
--divider = "rgba(140,110,70,0.14)"
|
--divider = "rgba(140,110,70,0.20)"
|
||||||
--outline = "rgba(140,110,70,0.16)"
|
--outline = "rgba(140,110,70,0.24)"
|
||||||
--scrim = "rgba(30,20,10,0.45)"
|
--scrim = "rgba(30,20,10,0.45)"
|
||||||
--surface-hover = "rgba(176,120,64,0.06)"
|
--surface-hover = "rgba(176,120,64,0.06)"
|
||||||
--surface-alt = "rgba(176,120,64,0.03)"
|
--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)"
|
--chart-hover-ring = "rgba(176,120,64,0.28)"
|
||||||
--tooltip-bg = "rgba(50,38,24,0.94)"
|
--tooltip-bg = "rgba(50,38,24,0.94)"
|
||||||
--tooltip-border = "rgba(176,120,64,0.12)"
|
--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-border = "rgba(176,120,64,0.14)"
|
||||||
--glass-noise-tint = "rgba(180,140,80,0.03)"
|
--glass-noise-tint = "rgba(180,140,80,0.03)"
|
||||||
--tactile-top = "rgba(255,255,248,0.50)"
|
--tactile-top = "rgba(255,255,248,0.50)"
|
||||||
--tactile-bottom = "rgba(255,255,248,0.08)"
|
--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)"
|
--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)"
|
--status-divider = "rgba(176,120,64,0.10)"
|
||||||
--sidebar-hover = "rgba(176,120,64,0.08)"
|
--sidebar-hover = "rgba(176,120,64,0.08)"
|
||||||
--sidebar-icon = "rgba(58,46,34,0.50)"
|
--sidebar-icon = "rgba(58,46,34,0.50)"
|
||||||
|
|||||||
@@ -20,16 +20,16 @@ elevation = { --elevation-0 = "#FDFBFF", --elevation-1 = "#F5F0FA", --elevation-
|
|||||||
--on-secondary = "#FFFFFF"
|
--on-secondary = "#FFFFFF"
|
||||||
--on-background = "#1C1525"
|
--on-background = "#1C1525"
|
||||||
--on-surface = "#1C1525"
|
--on-surface = "#1C1525"
|
||||||
--on-surface-medium = "rgba(28,21,37,0.72)"
|
--on-surface-medium = "rgba(28,21,37,0.86)"
|
||||||
--on-surface-disabled = "rgba(28,21,37,0.40)"
|
--on-surface-disabled = "rgba(28,21,37,0.62)"
|
||||||
--error = "#C62828"
|
--error = "#C62828"
|
||||||
--on-error = "#FFFFFF"
|
--on-error = "#FFFFFF"
|
||||||
--success = "#2E7D32"
|
--success = "#2E7D32"
|
||||||
--on-success = "#FFFFFF"
|
--on-success = "#FFFFFF"
|
||||||
--warning = "#E65100"
|
--warning = "#E65100"
|
||||||
--on-warning = "#000000"
|
--on-warning = "#000000"
|
||||||
--divider = "rgba(120,80,160,0.12)"
|
--divider = "rgba(120,80,160,0.20)"
|
||||||
--outline = "rgba(120,80,160,0.14)"
|
--outline = "rgba(120,80,160,0.24)"
|
||||||
--scrim = "rgba(20,10,30,0.45)"
|
--scrim = "rgba(20,10,30,0.45)"
|
||||||
--surface-hover = "rgba(140,107,175,0.06)"
|
--surface-hover = "rgba(140,107,175,0.06)"
|
||||||
--surface-alt = "rgba(140,107,175,0.03)"
|
--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)"
|
--chart-hover-ring = "rgba(140,107,175,0.28)"
|
||||||
--tooltip-bg = "rgba(32,24,48,0.94)"
|
--tooltip-bg = "rgba(32,24,48,0.94)"
|
||||||
--tooltip-border = "rgba(140,107,175,0.12)"
|
--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-border = "rgba(140,107,175,0.14)"
|
||||||
--glass-noise-tint = "rgba(180,140,220,0.03)"
|
--glass-noise-tint = "rgba(180,140,220,0.03)"
|
||||||
--tactile-top = "rgba(255,255,255,0.50)"
|
--tactile-top = "rgba(255,255,255,0.50)"
|
||||||
--tactile-bottom = "rgba(255,255,255,0.08)"
|
--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)"
|
--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)"
|
--status-divider = "rgba(140,107,175,0.10)"
|
||||||
--sidebar-hover = "rgba(140,107,175,0.08)"
|
--sidebar-hover = "rgba(140,107,175,0.08)"
|
||||||
--sidebar-icon = "rgba(28,21,37,0.50)"
|
--sidebar-icon = "rgba(28,21,37,0.50)"
|
||||||
|
|||||||
@@ -19,16 +19,16 @@ elevation = { --elevation-0 = "#FAFAFA", --elevation-1 = "#F2F3F5", --elevation-
|
|||||||
--on-secondary = "#FFFFFF"
|
--on-secondary = "#FFFFFF"
|
||||||
--on-background = "#2A2C30"
|
--on-background = "#2A2C30"
|
||||||
--on-surface = "#2A2C30"
|
--on-surface = "#2A2C30"
|
||||||
--on-surface-medium = "rgba(42,44,48,0.68)"
|
--on-surface-medium = "rgba(42,44,48,0.86)"
|
||||||
--on-surface-disabled = "rgba(42,44,48,0.38)"
|
--on-surface-disabled = "rgba(42,44,48,0.62)"
|
||||||
--error = "#8C5A62"
|
--error = "#8C5A62"
|
||||||
--on-error = "#FFFFFF"
|
--on-error = "#FFFFFF"
|
||||||
--success = "#5A7E5C"
|
--success = "#3D7A42"
|
||||||
--on-success = "#FFFFFF"
|
--on-success = "#FFFFFF"
|
||||||
--warning = "#8A7A52"
|
--warning = "#9A7A2E"
|
||||||
--on-warning = "#000000"
|
--on-warning = "#000000"
|
||||||
--divider = "rgba(42,44,48,0.12)"
|
--divider = "rgba(42,44,48,0.20)"
|
||||||
--outline = "rgba(42,44,48,0.14)"
|
--outline = "rgba(42,44,48,0.24)"
|
||||||
--scrim = "rgba(0,0,0,0.42)"
|
--scrim = "rgba(0,0,0,0.42)"
|
||||||
--surface-hover = "rgba(42,44,48,0.04)"
|
--surface-hover = "rgba(42,44,48,0.04)"
|
||||||
--surface-alt = "rgba(42,44,48,0.02)"
|
--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)"
|
--chart-hover-ring = "rgba(42,44,48,0.24)"
|
||||||
--tooltip-bg = "rgba(50,52,58,0.92)"
|
--tooltip-bg = "rgba(50,52,58,0.92)"
|
||||||
--tooltip-border = "rgba(42,44,48,0.10)"
|
--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-border = "rgba(42,44,48,0.10)"
|
||||||
--glass-noise-tint = "rgba(42,44,48,0.015)"
|
--glass-noise-tint = "rgba(42,44,48,0.015)"
|
||||||
--tactile-top = "rgba(255,255,255,0.35)"
|
--tactile-top = "rgba(255,255,255,0.35)"
|
||||||
--tactile-bottom = "rgba(255,255,255,0.04)"
|
--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)"
|
--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)"
|
--status-divider = "rgba(42,44,48,0.08)"
|
||||||
--sidebar-hover = "rgba(42,44,48,0.05)"
|
--sidebar-hover = "rgba(42,44,48,0.05)"
|
||||||
--sidebar-icon = "rgba(42,44,48,0.45)"
|
--sidebar-icon = "rgba(42,44,48,0.45)"
|
||||||
|
|||||||
@@ -20,16 +20,16 @@ elevation = { --elevation-0 = "#FAFAF8", --elevation-1 = "#F0EEEC", --elevation-
|
|||||||
--on-secondary = "#FFFFFF"
|
--on-secondary = "#FFFFFF"
|
||||||
--on-background = "#2C2A28"
|
--on-background = "#2C2A28"
|
||||||
--on-surface = "#2C2A28"
|
--on-surface = "#2C2A28"
|
||||||
--on-surface-medium = "rgba(44,42,40,0.68)"
|
--on-surface-medium = "rgba(44,42,40,0.86)"
|
||||||
--on-surface-disabled = "rgba(44,42,40,0.38)"
|
--on-surface-disabled = "rgba(44,42,40,0.62)"
|
||||||
--error = "#8C5250"
|
--error = "#8C5250"
|
||||||
--on-error = "#FFFFFF"
|
--on-error = "#FFFFFF"
|
||||||
--success = "#5C7A62"
|
--success = "#3F7A48"
|
||||||
--on-success = "#FFFFFF"
|
--on-success = "#FFFFFF"
|
||||||
--warning = "#8A7A4C"
|
--warning = "#9A7A2E"
|
||||||
--on-warning = "#000000"
|
--on-warning = "#000000"
|
||||||
--divider = "rgba(80,75,68,0.12)"
|
--divider = "rgba(80,75,68,0.20)"
|
||||||
--outline = "rgba(80,75,68,0.14)"
|
--outline = "rgba(80,75,68,0.24)"
|
||||||
--scrim = "rgba(20,18,16,0.42)"
|
--scrim = "rgba(20,18,16,0.42)"
|
||||||
--surface-hover = "rgba(110,117,128,0.05)"
|
--surface-hover = "rgba(110,117,128,0.05)"
|
||||||
--surface-alt = "rgba(110,117,128,0.025)"
|
--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)"
|
--chart-hover-ring = "rgba(110,117,128,0.24)"
|
||||||
--tooltip-bg = "rgba(44,42,40,0.94)"
|
--tooltip-bg = "rgba(44,42,40,0.94)"
|
||||||
--tooltip-border = "rgba(110,117,128,0.10)"
|
--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-border = "rgba(110,117,128,0.10)"
|
||||||
--glass-noise-tint = "rgba(80,75,68,0.02)"
|
--glass-noise-tint = "rgba(80,75,68,0.02)"
|
||||||
--tactile-top = "rgba(255,255,255,0.45)"
|
--tactile-top = "rgba(255,255,255,0.45)"
|
||||||
--tactile-bottom = "rgba(255,255,255,0.06)"
|
--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)"
|
--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)"
|
--status-divider = "rgba(110,117,128,0.08)"
|
||||||
--sidebar-hover = "rgba(110,117,128,0.06)"
|
--sidebar-hover = "rgba(110,117,128,0.06)"
|
||||||
--sidebar-icon = "rgba(44,42,40,0.48)"
|
--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-secondary = "#000000"
|
||||||
--on-background = "#E8E0F0"
|
--on-background = "#E8E0F0"
|
||||||
--on-surface = "#E8E0F0"
|
--on-surface = "#E8E0F0"
|
||||||
--on-surface-medium = "rgba(232,224,240,0.75)"
|
--on-surface-medium = "rgba(232,224,240,0.85)"
|
||||||
--on-surface-disabled = "rgba(232,224,240,0.45)"
|
--on-surface-disabled = "rgba(232,224,240,0.58)"
|
||||||
--error = "#CF6679"
|
--error = "#CF6679"
|
||||||
--on-error = "#000000"
|
--on-error = "#000000"
|
||||||
--success = "#81C784"
|
--success = "#81C784"
|
||||||
|
|||||||
+6
-4
@@ -37,8 +37,8 @@ images = { background_image = "backgrounds/texture/drgx_bg.png", logo = "logos/l
|
|||||||
--on-secondary = "#000000"
|
--on-secondary = "#000000"
|
||||||
--on-background = "#F0E0D8"
|
--on-background = "#F0E0D8"
|
||||||
--on-surface = "#F0E0D8"
|
--on-surface = "#F0E0D8"
|
||||||
--on-surface-medium = "rgba(240,224,216,0.7)"
|
--on-surface-medium = "rgba(240,224,216,0.85)"
|
||||||
--on-surface-disabled = "rgba(240,224,216,0.44)"
|
--on-surface-disabled = "rgba(240,224,216,0.58)"
|
||||||
--error = "#FF5252"
|
--error = "#FF5252"
|
||||||
--on-error = "#000000"
|
--on-error = "#000000"
|
||||||
--success = "#81C784"
|
--success = "#81C784"
|
||||||
@@ -874,7 +874,7 @@ accent-stripe-inset-ratio = { size = 0.0 }
|
|||||||
accent-stripe-left-offset = { size = 0.0 }
|
accent-stripe-left-offset = { size = 0.0 }
|
||||||
accent-stripe-width = { size = 4.0 }
|
accent-stripe-width = { size = 4.0 }
|
||||||
accent-stripe-rounding = { size = 1.5 }
|
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-y-axis-padding = { size = 70.0 }
|
||||||
chart-dot-min-radius = { size = 1.5 }
|
chart-dot-min-radius = { size = 1.5 }
|
||||||
chart-dot-radius = { size = 2.0 }
|
chart-dot-radius = { size = 2.0 }
|
||||||
@@ -1219,6 +1219,9 @@ notification-progress = { color = "var(--primary)", height = 4, position = 18 }
|
|||||||
fill-alpha = { size = 12.0 }
|
fill-alpha = { size = 12.0 }
|
||||||
noise-alpha = { size = 14.0 }
|
noise-alpha = { size = 14.0 }
|
||||||
|
|
||||||
|
[components.overlay-dialog]
|
||||||
|
confirm-btn-height = { size = 40.0 }
|
||||||
|
|
||||||
[components.qr-code]
|
[components.qr-code]
|
||||||
module-scale = { size = 4 }
|
module-scale = { size = 4 }
|
||||||
border-modules = { size = 2 }
|
border-modules = { size = 2 }
|
||||||
@@ -1525,7 +1528,6 @@ title = { font = "h5" }
|
|||||||
input = { width = 320.0, height = 40.0 }
|
input = { width = 320.0, height = 40.0 }
|
||||||
unlock-button = { width = 320.0, height = 44.0, font = "subtitle1" }
|
unlock-button = { width = 320.0, height = 44.0, font = "subtitle1" }
|
||||||
error-text = { font = "caption" }
|
error-text = { font = "caption" }
|
||||||
backdrop-alpha = { opacity = 0.0 }
|
|
||||||
mode-toggle = { font = "caption" }
|
mode-toggle = { font = "caption" }
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
+87
-277
@@ -66,6 +66,7 @@
|
|||||||
#include "ui/notifications.h"
|
#include "ui/notifications.h"
|
||||||
#include "util/i18n.h"
|
#include "util/i18n.h"
|
||||||
#include "util/platform.h"
|
#include "util/platform.h"
|
||||||
|
#include "util/text_format.h"
|
||||||
#include "util/payment_uri.h"
|
#include "util/payment_uri.h"
|
||||||
#include "util/texture_loader.h"
|
#include "util/texture_loader.h"
|
||||||
#include "util/bootstrap.h"
|
#include "util/bootstrap.h"
|
||||||
@@ -777,17 +778,8 @@ void App::update()
|
|||||||
// poll (which hits the still-running pre-restart daemon, rescanning=false)
|
// poll (which hits the still-running pre-restart daemon, rescanning=false)
|
||||||
// would fire a false "complete" the instant rescan was clicked.
|
// would fire a false "complete" the instant rescan was clicked.
|
||||||
ui::Notifications::instance().success("Blockchain rescan complete");
|
ui::Notifications::instance().success("Blockchain rescan complete");
|
||||||
state_.sync.rescanning = false;
|
resetWitnessRescanProgress();
|
||||||
rescan_confirmed_active_ = false;
|
|
||||||
state_.sync.rescan_progress = 1.0f;
|
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.
|
// 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
|
// 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
|
// 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.
|
// rescan/witness progress froze. Inline, progress updates every frame regardless.
|
||||||
const std::string output = std::move(newOutput);
|
const auto scan = services::NetworkRefreshService::parseDaemonRescanOutput(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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply results directly — we are already on the main thread.
|
// Apply results directly — we are already on the main thread.
|
||||||
const std::string& status = lastStatus;
|
const std::string& status = scan.lastStatus;
|
||||||
if (finished) {
|
if (scan.finished) {
|
||||||
if (state_.sync.rescanning) {
|
if (state_.sync.rescanning) {
|
||||||
ui::Notifications::instance().success("Blockchain rescan complete");
|
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.
|
// Witness rebuild finishes with the rescan it's part of.
|
||||||
state_.sync.building_witnesses = false;
|
resetWitnessRescanProgress();
|
||||||
state_.sync.witness_phase = 0;
|
state_.sync.rescan_progress = 1.0f;
|
||||||
state_.sync.witness_progress = 0.0f;
|
} else if (scan.foundWitness) {
|
||||||
state_.sync.witness_remaining = 0;
|
|
||||||
witness_rebuild_total_blocks_ = 0;
|
|
||||||
witness_seen_txids_.clear();
|
|
||||||
witness_total_txs_ = 0;
|
|
||||||
} else if (foundWitness) {
|
|
||||||
// Witness rebuild is the tail phase of a rescan — keep rescanning set so
|
// Witness rebuild is the tail phase of a rescan — keep rescanning set so
|
||||||
// the broader gating holds, but surface the witness-specific progress.
|
// the broader gating holds, but surface the witness-specific progress.
|
||||||
state_.sync.rescanning = true;
|
state_.sync.rescanning = true;
|
||||||
@@ -1074,8 +880,8 @@ void App::update()
|
|||||||
// never drops back to the initial pass. Otherwise an interleaved initial
|
// never drops back to the initial pass. Otherwise an interleaved initial
|
||||||
// line would flip the phase back and reset the bar every batch (thrash).
|
// line would flip the phase back and reset the bar every batch (thrash).
|
||||||
int phase = state_.sync.witness_phase;
|
int phase = state_.sync.witness_phase;
|
||||||
if (witnessRemaining >= 0 || witnessRebuilt) phase = 2;
|
if (scan.witnessRemaining >= 0 || scan.witnessRebuilt) phase = 2;
|
||||||
else if (!witnessTxids.empty() && phase < 2) phase = 1;
|
else if (!scan.witnessTxids.empty() && phase < 2) phase = 1;
|
||||||
|
|
||||||
// Entering a higher sub-phase → reset its progress + accumulators so the
|
// Entering a higher sub-phase → reset its progress + accumulators so the
|
||||||
// bar restarts from 0 (the two phases have different scales). Upgrade-only,
|
// bar restarts from 0 (the two phases have different scales). Upgrade-only,
|
||||||
@@ -1090,24 +896,24 @@ void App::update()
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (phase == 2) {
|
if (phase == 2) {
|
||||||
state_.sync.witness_remaining = witnessRemaining;
|
state_.sync.witness_remaining = scan.witnessRemaining;
|
||||||
float p = -1.0f;
|
float p = -1.0f;
|
||||||
if (witnessRebuilt) {
|
if (scan.witnessRebuilt) {
|
||||||
// Parallel rebuild finished (it logs no progress) — snap to 100%.
|
// Parallel rebuild finished (it logs no progress) — snap to 100%.
|
||||||
p = 1.0f;
|
p = 1.0f;
|
||||||
state_.sync.witness_remaining = 0;
|
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
|
// New daemon: exact done/total. Use the reported total as the
|
||||||
// denominator directly (no peak anchoring) for a smooth 0..1 sweep.
|
// denominator directly (no peak anchoring) for a smooth 0..1 sweep.
|
||||||
int done = witnessReadTotal - (witnessRemaining >= 0 ? witnessRemaining : 0);
|
int done = scan.witnessReadTotal - (scan.witnessRemaining >= 0 ? scan.witnessRemaining : 0);
|
||||||
p = static_cast<float>(done) / static_cast<float>(witnessReadTotal);
|
p = static_cast<float>(done) / static_cast<float>(scan.witnessReadTotal);
|
||||||
} else if (witnessRemaining >= 0) {
|
} else if (scan.witnessRemaining >= 0) {
|
||||||
// Older daemon: bare "<n> remaining" with no total — anchor the % to
|
// Older daemon: bare "<n> remaining" with no total — anchor the % to
|
||||||
// the largest remaining seen (its first/largest value ≈ the full span).
|
// the largest remaining seen (its first/largest value ≈ the full span).
|
||||||
if (witnessRemaining > witness_rebuild_total_blocks_)
|
if (scan.witnessRemaining > witness_rebuild_total_blocks_)
|
||||||
witness_rebuild_total_blocks_ = witnessRemaining;
|
witness_rebuild_total_blocks_ = scan.witnessRemaining;
|
||||||
if (witness_rebuild_total_blocks_ > 0)
|
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_);
|
static_cast<float>(witness_rebuild_total_blocks_);
|
||||||
}
|
}
|
||||||
if (p >= 0.0f) {
|
if (p >= 0.0f) {
|
||||||
@@ -1119,8 +925,8 @@ void App::update()
|
|||||||
// Initial pass: count DISTINCT witnessed txs / total. The set only
|
// Initial pass: count DISTINCT witnessed txs / total. The set only
|
||||||
// grows (dedups the daemon's double-prints), so it's monotonic; the
|
// 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.
|
// raw "<i>" can't be used — it's an unordered-map slot that bounces.
|
||||||
if (witnessTotalTxs > witness_total_txs_) witness_total_txs_ = witnessTotalTxs;
|
if (scan.witnessTotalTxs > witness_total_txs_) witness_total_txs_ = scan.witnessTotalTxs;
|
||||||
for (const auto& txid : witnessTxids) witness_seen_txids_.insert(txid);
|
for (const auto& txid : scan.witnessTxids) witness_seen_txids_.insert(txid);
|
||||||
if (witness_total_txs_ > 0 && !witness_seen_txids_.empty()) {
|
if (witness_total_txs_ > 0 && !witness_seen_txids_.empty()) {
|
||||||
float p = static_cast<float>(witness_seen_txids_.size()) /
|
float p = static_cast<float>(witness_seen_txids_.size()) /
|
||||||
static_cast<float>(witness_total_txs_);
|
static_cast<float>(witness_total_txs_);
|
||||||
@@ -1132,14 +938,14 @@ void App::update()
|
|||||||
if (!status.empty()) {
|
if (!status.empty()) {
|
||||||
state_.sync.rescan_status = status;
|
state_.sync.rescan_status = status;
|
||||||
}
|
}
|
||||||
} else if (foundRescan) {
|
} else if (scan.foundRescan) {
|
||||||
state_.sync.rescanning = true;
|
state_.sync.rescanning = true;
|
||||||
// Reading "Still rescanning" straight from the daemon log is hard proof the
|
// Reading "Still rescanning" straight from the daemon log is hard proof the
|
||||||
// rescan is genuinely running — confirm it so the getrescaninfo poll's
|
// rescan is genuinely running — confirm it so the getrescaninfo poll's
|
||||||
// completion check can fire even if it never caught a warmup error.
|
// completion check can fire even if it never caught a warmup error.
|
||||||
rescan_confirmed_active_ = true;
|
rescan_confirmed_active_ = true;
|
||||||
if (rescanPct > 0.0f) {
|
if (scan.rescanPct > 0.0f) {
|
||||||
state_.sync.rescan_progress = rescanPct / 100.0f;
|
state_.sync.rescan_progress = scan.rescanPct / 100.0f;
|
||||||
}
|
}
|
||||||
if (!status.empty()) {
|
if (!status.empty()) {
|
||||||
state_.sync.rescan_status = status;
|
state_.sync.rescan_status = status;
|
||||||
@@ -1305,9 +1111,17 @@ void App::update()
|
|||||||
if (network_refresh_.consumeDue(RefreshTimer::Price)) {
|
if (network_refresh_.consumeDue(RefreshTimer::Price)) {
|
||||||
if (settings_->getFetchPrices()) {
|
if (settings_->getFetchPrices()) {
|
||||||
refreshPrice();
|
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
|
// Keyboard shortcut: Ctrl+, to open Settings page
|
||||||
if (io.KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_Comma)) {
|
if (io.KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_Comma)) {
|
||||||
setCurrentPage(ui::NavPage::Settings);
|
setCurrentPage(ui::NavPage::Settings);
|
||||||
@@ -1429,6 +1243,30 @@ void App::render()
|
|||||||
// Process deferred encryption from wizard (runs in background)
|
// Process deferred encryption from wizard (runs in background)
|
||||||
processDeferredEncryption();
|
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)
|
// Main content area - use full window (no menu bar)
|
||||||
ImGuiViewport* viewport = ImGui::GetMainViewport();
|
ImGuiViewport* viewport = ImGui::GetMainViewport();
|
||||||
ImGui::SetNextWindowPos(viewport->WorkPos);
|
ImGui::SetNextWindowPos(viewport->WorkPos);
|
||||||
@@ -2080,6 +1918,10 @@ void App::render()
|
|||||||
|
|
||||||
// Render notifications (toast messages)
|
// Render notifications (toast messages)
|
||||||
ui::Notifications::instance().render();
|
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()
|
void App::renderStatusBar()
|
||||||
@@ -2503,7 +2345,7 @@ void App::renderLiteFirstRunPrompt()
|
|||||||
creating = false; // back to the buttons so the user can retry
|
creating = false; // back to the buttons so the user can retry
|
||||||
}
|
}
|
||||||
} else {
|
} 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
|
// 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.
|
// server falls through to the next). On success the wizard reveals the seed.
|
||||||
if (lite_wallet_->beginCreateWallet()) {
|
if (lite_wallet_->beginCreateWallet()) {
|
||||||
@@ -2513,11 +2355,11 @@ void App::renderLiteFirstRunPrompt()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
ImGui::SameLine();
|
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)
|
step = 3; // inline seed-entry restore (see step 3 below)
|
||||||
}
|
}
|
||||||
ImGui::Spacing();
|
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))) {
|
ImVec2(btnW * 2 + ImGui::GetStyle().ItemSpacing.x, 0))) {
|
||||||
finish();
|
finish();
|
||||||
}
|
}
|
||||||
@@ -2552,7 +2394,7 @@ void App::renderLiteFirstRunPrompt()
|
|||||||
ImGui::PopStyleColor();
|
ImGui::PopStyleColor();
|
||||||
ImGui::Spacing(); ImGui::Spacing();
|
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();
|
chips.clear();
|
||||||
for (const auto& w : words) chips.emplace_back(w, false);
|
for (const auto& w : words) chips.emplace_back(w, false);
|
||||||
std::mt19937 rng{std::random_device{}()};
|
std::mt19937 rng{std::random_device{}()};
|
||||||
@@ -2561,9 +2403,9 @@ void App::renderLiteFirstRunPrompt()
|
|||||||
step = 2;
|
step = 2;
|
||||||
}
|
}
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
if (ImGui::Button("Copy", ImVec2(80, 0))) copySecretToClipboard(seed);
|
if (ui::material::TactileButton("Copy", ImVec2(80, 0))) copySecretToClipboard(seed);
|
||||||
ImGui::SameLine();
|
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);
|
ui::Notifications::instance().success(TR("lite_welcome_created"), 6.0f);
|
||||||
finish();
|
finish();
|
||||||
}
|
}
|
||||||
@@ -2591,9 +2433,9 @@ void App::renderLiteFirstRunPrompt()
|
|||||||
ImGui::PushID((int)i);
|
ImGui::PushID((int)i);
|
||||||
if (chips[i].second) {
|
if (chips[i].second) {
|
||||||
ImGui::BeginDisabled();
|
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();
|
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]) {
|
if (progress < (int)words.size() && chips[i].first == words[progress]) {
|
||||||
chips[i].second = true; // correct next word
|
chips[i].second = true; // correct next word
|
||||||
++progress;
|
++progress;
|
||||||
@@ -2608,15 +2450,15 @@ void App::renderLiteFirstRunPrompt()
|
|||||||
|
|
||||||
const bool verified = progress == (int)words.size();
|
const bool verified = progress == (int)words.size();
|
||||||
if (!verified) ImGui::BeginDisabled();
|
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);
|
ui::Notifications::instance().success("Wallet created and backed up.", 6.0f);
|
||||||
finish();
|
finish();
|
||||||
}
|
}
|
||||||
if (!verified) ImGui::EndDisabled();
|
if (!verified) ImGui::EndDisabled();
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
if (ImGui::Button("Back", ImVec2(80, 0))) step = 1;
|
if (ui::material::TactileButton("Back", ImVec2(80, 0))) step = 1;
|
||||||
ImGui::SameLine();
|
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);
|
ui::Notifications::instance().success(TR("lite_welcome_created"), 6.0f);
|
||||||
finish();
|
finish();
|
||||||
}
|
}
|
||||||
@@ -2668,7 +2510,7 @@ void App::renderLiteFirstRunPrompt()
|
|||||||
while (!seedTrim.empty() && std::isspace((unsigned char)seedTrim.back())) seedTrim.pop_back();
|
while (!seedTrim.empty() && std::isspace((unsigned char)seedTrim.back())) seedTrim.pop_back();
|
||||||
|
|
||||||
ImGui::BeginDisabled(seedTrim.empty());
|
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;
|
wallet::LiteWalletRestoreRequest req;
|
||||||
req.seedPhrase = seedTrim;
|
req.seedPhrase = seedTrim;
|
||||||
req.birthday = static_cast<unsigned long long>(std::max(0, restoreBirthday));
|
req.birthday = static_cast<unsigned long long>(std::max(0, restoreBirthday));
|
||||||
@@ -2684,7 +2526,7 @@ void App::renderLiteFirstRunPrompt()
|
|||||||
}
|
}
|
||||||
ImGui::EndDisabled();
|
ImGui::EndDisabled();
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
if (ImGui::Button("Back", ImVec2(80, 0))) {
|
if (ui::material::TactileButton("Back", ImVec2(80, 0))) {
|
||||||
sodium_memzero(restoreSeed, sizeof(restoreSeed));
|
sodium_memzero(restoreSeed, sizeof(restoreSeed));
|
||||||
restoreErr.clear();
|
restoreErr.clear();
|
||||||
step = 0;
|
step = 0;
|
||||||
@@ -2719,7 +2561,7 @@ void App::renderLiteUnlockPrompt()
|
|||||||
ImGuiInputTextFlags_Password | ImGuiInputTextFlags_EnterReturnsTrue);
|
ImGuiInputTextFlags_Password | ImGuiInputTextFlags_EnterReturnsTrue);
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
const float btnW = 130.0f;
|
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) {
|
if (doUnlock) {
|
||||||
const bool ok = lite_wallet_->unlockWallet(pass);
|
const bool ok = lite_wallet_->unlockWallet(pass);
|
||||||
sodium_memzero(pass, sizeof(pass));
|
sodium_memzero(pass, sizeof(pass));
|
||||||
@@ -2732,7 +2574,7 @@ void App::renderLiteUnlockPrompt()
|
|||||||
ImGui::CloseCurrentPopup();
|
ImGui::CloseCurrentPopup();
|
||||||
}
|
}
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
if (ImGui::Button(TR("cancel"), ImVec2(btnW, 0))) {
|
if (ui::material::TactileButton(TR("cancel"), ImVec2(btnW, 0))) {
|
||||||
sodium_memzero(pass, sizeof(pass));
|
sodium_memzero(pass, sizeof(pass));
|
||||||
lite_unlock_prompt_ = false;
|
lite_unlock_prompt_ = false;
|
||||||
ImGui::CloseCurrentPopup();
|
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()
|
void App::renderImportKeyDialog()
|
||||||
{
|
{
|
||||||
@@ -2910,7 +2717,7 @@ void App::renderExportKeyDialog()
|
|||||||
if (ImGui::BeginCombo("##exportaddr", export_address_.empty() ? "Select address..." : export_address_.c_str())) {
|
if (ImGui::BeginCombo("##exportaddr", export_address_.empty() ? "Select address..." : export_address_.c_str())) {
|
||||||
for (const auto& addr : all_addrs) {
|
for (const auto& addr : all_addrs) {
|
||||||
bool selected = (export_address_ == addr);
|
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)) {
|
if (ImGui::Selectable(display.c_str(), selected)) {
|
||||||
export_address_ = addr;
|
export_address_ = addr;
|
||||||
export_result_.clear();
|
export_result_.clear();
|
||||||
@@ -3622,7 +3429,9 @@ void App::renderShutdownScreen()
|
|||||||
ImGui::SetNextWindowPos(vp_pos);
|
ImGui::SetNextWindowPos(vp_pos);
|
||||||
ImGui::SetNextWindowSize(vp_size);
|
ImGui::SetNextWindowSize(vp_size);
|
||||||
ImGui::SetNextWindowFocus();
|
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_WindowRounding, 0.0f);
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
|
||||||
ImGuiWindowFlags shutdownFlags =
|
ImGuiWindowFlags shutdownFlags =
|
||||||
@@ -3661,7 +3470,7 @@ void App::renderShutdownScreen()
|
|||||||
ImGui::PushFont(Type().h5());
|
ImGui::PushFont(Type().h5());
|
||||||
ImVec2 ts = ImGui::CalcTextSize(title);
|
ImVec2 ts = ImGui::CalcTextSize(title);
|
||||||
ImGui::SetCursorPosX(cx - ts.x * 0.5f);
|
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::TextUnformatted(title);
|
||||||
ImGui::PopStyleColor();
|
ImGui::PopStyleColor();
|
||||||
ImGui::PopFont();
|
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_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_ButtonHovered, ImVec4(0.75f, 0.2f, 0.2f, 1.0f));
|
||||||
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.5f, 0.1f, 0.1f, 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;
|
force_quit_confirm_ = true;
|
||||||
}
|
}
|
||||||
if (ImGui::IsItemHovered()) {
|
if (ImGui::IsItemHovered()) {
|
||||||
@@ -3771,14 +3580,15 @@ void App::renderShutdownScreen()
|
|||||||
float totalW = btnW * 2 + ImGui::GetStyle().ItemSpacing.x;
|
float totalW = btnW * 2 + ImGui::GetStyle().ItemSpacing.x;
|
||||||
ImGui::SetCursorPosX((ImGui::GetWindowWidth() - totalW) * 0.5f);
|
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::CloseCurrentPopup();
|
||||||
}
|
}
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.6f, 0.15f, 0.15f, 0.9f));
|
// Destructive action — theme error color (alpha-varied for hover/active) instead of hardcoded red.
|
||||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.75f, 0.2f, 0.2f, 1.0f));
|
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(WithAlpha(Error(), 210)));
|
||||||
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.5f, 0.1f, 0.1f, 1.0f));
|
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(Error()));
|
||||||
if (ImGui::Button(TR("force_quit_yes"), ImVec2(btnW, 0))) {
|
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_);
|
DEBUG_LOGF("Force quit confirmed by user after %.0fs\n", shutdown_timer_);
|
||||||
shutdown_complete_ = true;
|
shutdown_complete_ = true;
|
||||||
ImGui::CloseCurrentPopup();
|
ImGui::CloseCurrentPopup();
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
#include <unordered_set>
|
#include <unordered_set>
|
||||||
|
#include <nlohmann/json_fwd.hpp>
|
||||||
#include "data/transaction_history_cache.h"
|
#include "data/transaction_history_cache.h"
|
||||||
#include "data/wallet_state.h"
|
#include "data/wallet_state.h"
|
||||||
#include "rpc/connection.h"
|
#include "rpc/connection.h"
|
||||||
@@ -294,6 +295,9 @@ public:
|
|||||||
// Fetch the live exchange/pair list from CoinGecko once per session (venues are
|
// Fetch the live exchange/pair list from CoinGecko once per session (venues are
|
||||||
// near-static); populates state.market.exchanges. Safe to call every frame.
|
// near-static); populates state.market.exchanges. Safe to call every frame.
|
||||||
void refreshExchanges();
|
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
|
/// @brief Per-category refresh intervals, adjusted by active tab
|
||||||
using RefreshIntervals = services::NetworkRefreshService::Intervals;
|
using RefreshIntervals = services::NetworkRefreshService::Intervals;
|
||||||
@@ -305,6 +309,16 @@ public:
|
|||||||
void setCurrentPage(ui::NavPage page);
|
void setCurrentPage(ui::NavPage page);
|
||||||
ui::NavPage getCurrentPage() const { return current_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)
|
// Dialog triggers (used by settings page to open modal dialogs)
|
||||||
void showImportKeyDialog() { show_import_key_ = true; }
|
void showImportKeyDialog() { show_import_key_ = true; }
|
||||||
void showExportKeyDialog() { show_export_key_ = true; }
|
void showExportKeyDialog() { show_export_key_ = true; }
|
||||||
@@ -394,6 +408,12 @@ public:
|
|||||||
|
|
||||||
// Wallet encryption helpers
|
// Wallet encryption helpers
|
||||||
void encryptWalletWithPassphrase(const std::string& passphrase);
|
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 unlockWallet(const std::string& passphrase, int timeout);
|
||||||
void lockWallet();
|
void lockWallet();
|
||||||
void changePassphrase(const std::string& oldPass, const std::string& newPass);
|
void changePassphrase(const std::string& oldPass, const std::string& newPass);
|
||||||
@@ -443,8 +463,16 @@ private:
|
|||||||
friend class AppDaemonLifecycleRuntime;
|
friend class AppDaemonLifecycleRuntime;
|
||||||
friend class AppDaemonLifecycleTaskContext;
|
friend class AppDaemonLifecycleTaskContext;
|
||||||
|
|
||||||
|
// Global keyboard-shortcut handling, dispatched once per frame from update().
|
||||||
|
void handleGlobalShortcuts();
|
||||||
|
|
||||||
bool sendStopCommandSafely(rpc::RPCClient& client, const char* context);
|
bool sendStopCommandSafely(rpc::RPCClient& client, const char* context);
|
||||||
void maybeFinishTransactionSendProgress();
|
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,
|
void upsertPendingSendTransaction(const std::string& opid,
|
||||||
const std::string& from,
|
const std::string& from,
|
||||||
const std::string& to,
|
const std::string& to,
|
||||||
@@ -461,10 +489,28 @@ private:
|
|||||||
void resendWithFeeGapWorkaround(const std::string& from, const std::string& to,
|
void resendWithFeeGapWorkaround(const std::string& from, const std::string& to,
|
||||||
double amount, double fee, const std::string& memo,
|
double amount, double fee, const std::string& memo,
|
||||||
std::function<void(bool, const std::string&)> callback);
|
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,
|
void markPendingSendTransactionSucceeded(const std::string& opid,
|
||||||
const std::string& txid);
|
const std::string& txid);
|
||||||
void removePendingSendTransactions(const std::vector<std::string>& opids,
|
void removePendingSendTransactions(const std::vector<std::string>& opids,
|
||||||
bool restoreBalances);
|
bool restoreBalances);
|
||||||
|
// 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
|
// 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).
|
// reaches a terminal status. Returns true if a callback was registered (and fired).
|
||||||
bool invokeSendResultCallback(const std::string& opid, bool ok,
|
bool invokeSendResultCallback(const std::string& opid, bool ok,
|
||||||
@@ -473,6 +519,14 @@ private:
|
|||||||
std::string transactionHistoryCacheWalletIdentity() const;
|
std::string transactionHistoryCacheWalletIdentity() const;
|
||||||
bool ensureTransactionHistoryCacheUnlockedFor(const std::string& walletIdentity);
|
bool ensureTransactionHistoryCacheUnlockedFor(const std::string& walletIdentity);
|
||||||
void unlockTransactionHistoryCacheWithPassphrase(const std::string& passphrase);
|
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 loadTransactionHistoryCacheIfAvailable();
|
||||||
void storeTransactionHistoryCacheIfAvailable();
|
void storeTransactionHistoryCacheIfAvailable();
|
||||||
void wipePendingTransactionHistoryCachePassphrase();
|
void wipePendingTransactionHistoryCachePassphrase();
|
||||||
@@ -526,6 +580,7 @@ private:
|
|||||||
bool balance_refresh_pending_ = false; // UI asked for an immediate refresh
|
bool balance_refresh_pending_ = false; // UI asked for an immediate refresh
|
||||||
bool balance_snapshot_seen_ = false; // the current in-flight snapshot was applied
|
bool balance_snapshot_seen_ = false; // the current in-flight snapshot was applied
|
||||||
bool exchanges_fetch_started_ = false; // once-per-session CoinGecko tickers fetch
|
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_;
|
util::AsyncTaskManager async_tasks_;
|
||||||
bool pending_antivirus_dialog_ = false; // Show Windows Defender help dialog
|
bool pending_antivirus_dialog_ = false; // Show Windows Defender help dialog
|
||||||
|
|
||||||
@@ -593,6 +648,21 @@ private:
|
|||||||
ui::NavPage prev_page_ = ui::NavPage::Overview;
|
ui::NavPage prev_page_ = ui::NavPage::Overview;
|
||||||
float page_alpha_ = 1.0f; // 0→1 fade on page switch
|
float page_alpha_ = 1.0f; // 0→1 fade on page switch
|
||||||
bool sidebar_collapsed_ = false; // true = icon-only mode
|
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
|
bool sidebar_user_toggled_ = false; // user manually toggled — suppress auto-collapse
|
||||||
float sidebar_width_anim_ = 0.0f; // animated width (0 = uninitialized)
|
float sidebar_width_anim_ = 0.0f; // animated width (0 = uninitialized)
|
||||||
float prev_dpi_scale_ = 0.0f; // detect DPI changes to snap sidebar width
|
float prev_dpi_scale_ = 0.0f; // detect DPI changes to snap sidebar width
|
||||||
@@ -794,7 +864,6 @@ private:
|
|||||||
|
|
||||||
// Private methods - rendering
|
// Private methods - rendering
|
||||||
void renderStatusBar();
|
void renderStatusBar();
|
||||||
void renderAboutDialog();
|
|
||||||
void renderLiteFirstRunPrompt(); // lite-only welcome modal when no wallet exists yet
|
void renderLiteFirstRunPrompt(); // lite-only welcome modal when no wallet exists yet
|
||||||
void renderLiteUnlockPrompt(); // lite-only send-time unlock modal
|
void renderLiteUnlockPrompt(); // lite-only send-time unlock modal
|
||||||
void renderImportKeyDialog();
|
void renderImportKeyDialog();
|
||||||
|
|||||||
+238
-165
@@ -40,8 +40,10 @@
|
|||||||
#include "daemon/xmrig_manager.h"
|
#include "daemon/xmrig_manager.h"
|
||||||
#include "util/pool_registry.h"
|
#include "util/pool_registry.h"
|
||||||
#include "ui/notifications.h"
|
#include "ui/notifications.h"
|
||||||
|
#include "ui/schema/skin_manager.h"
|
||||||
#include "default_banlist_embedded.h"
|
#include "default_banlist_embedded.h"
|
||||||
#include "util/amount_format.h"
|
#include "util/amount_format.h"
|
||||||
|
#include "util/http_download.h"
|
||||||
#include "util/platform.h"
|
#include "util/platform.h"
|
||||||
#include "util/perf_log.h"
|
#include "util/perf_log.h"
|
||||||
#include "util/i18n.h"
|
#include "util/i18n.h"
|
||||||
@@ -52,6 +54,7 @@
|
|||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <ctime>
|
#include <ctime>
|
||||||
|
#include <filesystem>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <utility>
|
#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
|
bool App::shouldRefreshTransactions() const
|
||||||
{
|
{
|
||||||
// NOTE: this is block-height / dirty driven, NOT interval-gated. It returns true only when a new
|
// 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));
|
state_.transactions.insert(state_.transactions.begin(), std::move(pending));
|
||||||
}
|
}
|
||||||
if (newPending) {
|
if (newPending) {
|
||||||
auto applyDelta = [&](std::vector<AddressInfo>& addresses) {
|
applyPendingSendDelta(pendingInfo.from, -pendingInfo.amount, /*includeAggregates*/ true);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
state_.last_tx_update = std::time(nullptr);
|
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) {
|
for (const auto& opid : opidSet) {
|
||||||
auto pending = pending_send_info_.find(opid);
|
auto pending = pending_send_info_.find(opid);
|
||||||
if (pending == pending_send_info_.end()) continue;
|
if (pending == pending_send_info_.end()) continue;
|
||||||
auto restoreBalance = [&](std::vector<AddressInfo>& addresses) {
|
applyPendingSendDelta(pending->second.from, pending->second.amount,
|
||||||
for (auto& address : addresses) {
|
/*includeAggregates*/ true);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
state_.transactions.erase(
|
state_.transactions.erase(
|
||||||
@@ -878,6 +954,32 @@ void App::removePendingSendTransactions(const std::vector<std::string>& opids,
|
|||||||
state_.last_tx_update = std::time(nullptr);
|
state_.last_tx_update = std::time(nullptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void App::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)
|
void App::trackOperation(const std::string& opid)
|
||||||
{
|
{
|
||||||
if (opid.empty()) return;
|
if (opid.empty()) return;
|
||||||
@@ -903,27 +1005,28 @@ void App::applyPendingSendBalanceDeltas(bool includeAggregateBalances)
|
|||||||
{
|
{
|
||||||
for (const auto& [opid, pending] : pending_send_info_) {
|
for (const auto& [opid, pending] : pending_send_info_) {
|
||||||
(void)opid;
|
(void)opid;
|
||||||
auto applyDelta = [&](std::vector<AddressInfo>& addresses) {
|
applyPendingSendDelta(pending.from, -pending.amount, includeAggregateBalances);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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::string App::transactionHistoryCacheWalletIdentity() const
|
||||||
{
|
{
|
||||||
std::vector<std::string> shieldedAddresses;
|
std::vector<std::string> shieldedAddresses;
|
||||||
@@ -1156,17 +1259,8 @@ void App::refreshCoreData()
|
|||||||
// rescan_confirmed_active_ ensures we actually observed this rescan running (set
|
// rescan_confirmed_active_ ensures we actually observed this rescan running (set
|
||||||
// by the getrescaninfo / daemon-log pollers) before declaring it done.
|
// by the getrescaninfo / daemon-log pollers) before declaring it done.
|
||||||
if (state_.sync.rescanning && rescan_confirmed_active_) {
|
if (state_.sync.rescanning && rescan_confirmed_active_) {
|
||||||
state_.sync.rescanning = false;
|
resetWitnessRescanProgress();
|
||||||
rescan_confirmed_active_ = false;
|
|
||||||
state_.sync.rescan_progress = 1.0f;
|
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.
|
// Notes/witnesses were rebuilt — force a fresh history + balance pull.
|
||||||
transactions_dirty_ = true;
|
transactions_dirty_ = true;
|
||||||
last_tx_block_height_ = -1;
|
last_tx_block_height_ = -1;
|
||||||
@@ -1632,6 +1726,46 @@ void App::refreshMarketData()
|
|||||||
{
|
{
|
||||||
refreshPrice();
|
refreshPrice();
|
||||||
refreshExchanges();
|
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()
|
void App::refreshExchanges()
|
||||||
@@ -1944,64 +2078,29 @@ void App::applyDefaultBanlist()
|
|||||||
|
|
||||||
void App::createNewZAddress(std::function<void(const std::string&)> callback)
|
void App::createNewZAddress(std::function<void(const std::string&)> callback)
|
||||||
{
|
{
|
||||||
// Lite build: derive locally via the controller (fast, no network). The backend auto-saves
|
createNewAddress(/*shielded*/ true, std::move(callback));
|
||||||
// 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);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void App::createNewTAddress(std::function<void(const std::string&)> callback)
|
void App::createNewTAddress(std::function<void(const std::string&)> callback)
|
||||||
{
|
{
|
||||||
// Lite build: derive locally via the controller (see createNewZAddress).
|
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_) {
|
if (lite_wallet_) {
|
||||||
const auto result = lite_wallet_->newAddress(/*shielded*/ false);
|
const auto result = lite_wallet_->newAddress(shielded);
|
||||||
if (result.ok) {
|
if (result.ok) {
|
||||||
AddressInfo info;
|
AddressInfo info;
|
||||||
info.address = result.address;
|
info.address = result.address;
|
||||||
info.type = "transparent";
|
info.type = typeStr;
|
||||||
info.balance = 0.0;
|
info.balance = 0.0;
|
||||||
state_.t_addresses.push_back(info);
|
targetList.push_back(info);
|
||||||
state_.addresses.push_back(info);
|
state_.addresses.push_back(info);
|
||||||
address_list_dirty_ = true;
|
address_list_dirty_ = true;
|
||||||
}
|
}
|
||||||
@@ -2011,23 +2110,26 @@ void App::createNewTAddress(std::function<void(const std::string&)> callback)
|
|||||||
|
|
||||||
if (!state_.connected || !rpc_ || !worker_) return;
|
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;
|
std::string addr;
|
||||||
try {
|
try {
|
||||||
rpc::RPCClient::TraceScope trace("Receive tab / New transparent address");
|
rpc::RPCClient::TraceScope trace(traceLabel);
|
||||||
json result = rpc_->call("getnewaddress");
|
json result = rpc_->call(rpcMethod);
|
||||||
addr = result.get<std::string>();
|
addr = result.get<std::string>();
|
||||||
} catch (const std::exception& e) {
|
} 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()) {
|
if (!addr.empty()) {
|
||||||
// Inject immediately so UI can select the address next frame
|
// Inject immediately so UI can select the address next frame
|
||||||
AddressInfo info;
|
AddressInfo info;
|
||||||
info.address = addr;
|
info.address = addr;
|
||||||
info.type = "transparent";
|
info.type = typeStr;
|
||||||
info.balance = 0.0;
|
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
|
state_.addresses.push_back(info); // keep combined view in sync immediately
|
||||||
address_list_dirty_ = true;
|
address_list_dirty_ = true;
|
||||||
// Also trigger full refresh to get proper balances
|
// Also trigger full refresh to get proper balances
|
||||||
@@ -2405,26 +2507,36 @@ void App::sendTransaction(const std::string& from, const std::string& to,
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
send_progress_active_ = true;
|
|
||||||
++send_submissions_in_flight_;
|
|
||||||
// z_sendmany signature is (fromaddress, amounts, minconf, fee). The fee MUST be a JSON number:
|
// 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
|
// 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
|
// 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.
|
// "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
|
// (The recipient "amount" above is intentionally a fixed-decimal STRING — that field is parsed
|
||||||
// with ParseFixedPoint, which a scientific-notation double would break.)
|
// 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;
|
bool ok = false;
|
||||||
std::string result_str;
|
std::string result_str;
|
||||||
try {
|
try {
|
||||||
rpc::RPCClient::TraceScope trace("Send tab / Submit transaction");
|
rpc::RPCClient::TraceScope trace(traceLabel);
|
||||||
auto result = rpc_->call("z_sendmany", {from, recipients, 1, fee});
|
auto result = rpc_->call("z_sendmany", {from, recipients, 1, fee});
|
||||||
result_str = result.get<std::string>();
|
result_str = result.get<std::string>();
|
||||||
ok = true;
|
ok = true;
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
result_str = e.what();
|
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 (send_submissions_in_flight_ > 0) --send_submissions_in_flight_;
|
||||||
if (ok) {
|
if (ok) {
|
||||||
// A send changes address balances — refresh on next cycle
|
// 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.
|
// "sent successfully" for an operation that may still fail.
|
||||||
if (!result_str.empty()) {
|
if (!result_str.empty()) {
|
||||||
pending_opids_.push_back(result_str);
|
pending_opids_.push_back(result_str);
|
||||||
|
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);
|
upsertPendingSendTransaction(result_str, from, to, amount, memo, fee);
|
||||||
if (callback) pending_send_callbacks_[result_str] = callback;
|
if (callback) pending_send_callbacks_[result_str] = callback;
|
||||||
} else if (callback) {
|
} else if (callback) {
|
||||||
@@ -2528,40 +2642,8 @@ void App::resendWithFeeGapWorkaround(const std::string& from, const std::string&
|
|||||||
selfOut["amount"] = util::formatAmountFixed(fee);
|
selfOut["amount"] = util::formatAmountFixed(fee);
|
||||||
recipients.push_back(selfOut);
|
recipients.push_back(selfOut);
|
||||||
|
|
||||||
send_progress_active_ = true;
|
submitZSendMany(from, to, amount, fee, memo, recipients, "Send tab / Fee-gap retry",
|
||||||
++send_submissions_in_flight_;
|
/*markFeeGapRetry*/ true, std::move(callback));
|
||||||
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);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --------------------------------------------------------------------------------------------
|
// --------------------------------------------------------------------------------------------
|
||||||
@@ -2652,16 +2734,7 @@ void App::runtimeRescan(int startHeight)
|
|||||||
}
|
}
|
||||||
return [this, ok, err]() {
|
return [this, ok, err]() {
|
||||||
runtime_rescan_active_ = false;
|
runtime_rescan_active_ = false;
|
||||||
rescan_confirmed_active_ = false;
|
resetWitnessRescanProgress();
|
||||||
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;
|
|
||||||
if (ok) {
|
if (ok) {
|
||||||
state_.sync.rescan_progress = 1.0f;
|
state_.sync.rescan_progress = 1.0f;
|
||||||
transactions_dirty_ = true;
|
transactions_dirty_ = true;
|
||||||
|
|||||||
+157
-105
@@ -230,6 +230,34 @@ private:
|
|||||||
// Wallet encryption helpers
|
// 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) {
|
void App::encryptWalletWithPassphrase(const std::string& passphrase) {
|
||||||
if (!rpc_ || !rpc_->isConnected()) return;
|
if (!rpc_ || !rpc_->isConnected()) return;
|
||||||
encrypt_in_progress_ = true;
|
encrypt_in_progress_ = true;
|
||||||
@@ -267,23 +295,8 @@ void App::encryptWalletWithPassphrase(const std::string& passphrase) {
|
|||||||
// The daemon shuts itself down after encryptwallet.
|
// The daemon shuts itself down after encryptwallet.
|
||||||
// Update connection_status_ so the loading overlay
|
// Update connection_status_ so the loading overlay
|
||||||
// explains why the daemon is restarting.
|
// explains why the daemon is restarting.
|
||||||
if (isUsingEmbeddedDaemon()) {
|
restartDaemonAfterEncryption("encrypt-daemon-restart",
|
||||||
connection_status_ = TR("restarting_after_encryption");
|
/*announceRestartStatus=*/true);
|
||||||
// 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.");
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
std::string err = result.error;
|
std::string err = result.error;
|
||||||
@@ -366,20 +379,8 @@ void App::processDeferredEncryption() {
|
|||||||
wallet_security_.clearDeferredEncryption();
|
wallet_security_.clearDeferredEncryption();
|
||||||
|
|
||||||
// Restart daemon (it shuts itself down after encryptwallet)
|
// Restart daemon (it shuts itself down after encryptwallet)
|
||||||
if (isUsingEmbeddedDaemon()) {
|
restartDaemonAfterEncryption("deferred-encrypt-daemon-restart",
|
||||||
async_tasks_.submit("deferred-encrypt-daemon-restart", [this](const util::AsyncTaskManager::Token& token) {
|
/*announceRestartStatus=*/false);
|
||||||
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.");
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
std::string err = result.error;
|
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) {
|
void App::unlockWallet(const std::string& passphrase, int timeout) {
|
||||||
if (!rpc_ || !rpc_->isConnected() || !worker_) return;
|
if (!rpc_ || !rpc_->isConnected() || !worker_) return;
|
||||||
lock_unlock_in_progress_ = true;
|
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());
|
util::SecureVault::secureZero(passphrase.data(), passphrase.size());
|
||||||
|
|
||||||
return [this, ok, err_msg, timeout, passphrase = std::move(cachePassphrase)]() mutable {
|
return [this, ok, err_msg, timeout, passphrase = std::move(cachePassphrase)]() mutable {
|
||||||
lock_unlock_in_progress_ = false;
|
|
||||||
if (ok) {
|
if (ok) {
|
||||||
lock_error_msg_.clear();
|
applyUnlockSuccess(passphrase, timeout);
|
||||||
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);
|
|
||||||
} else {
|
} else {
|
||||||
lock_attempts_++;
|
DEBUG_LOGF("[App] Passphrase unlock RPC error: %s\n", err_msg.c_str());
|
||||||
lock_error_msg_ = TR("incorrect_passphrase");
|
applyUnlockFailure(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());
|
|
||||||
}
|
}
|
||||||
util::SecureVault::secureZero(passphrase.data(), passphrase.size());
|
util::SecureVault::secureZero(passphrase.data(), passphrase.size());
|
||||||
};
|
};
|
||||||
@@ -753,15 +767,55 @@ void App::checkIdleMining() {
|
|||||||
void App::renderLockScreen() {
|
void App::renderLockScreen() {
|
||||||
using namespace ui::material;
|
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();
|
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||||
ImVec2 winPos = ImGui::GetWindowPos();
|
// Consume input to everything behind (sidebar included) while locked. AllowOverlap so the lock
|
||||||
ImVec2 winSize = ImGui::GetWindowSize();
|
// 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)
|
// Live-blur backdrop over the wallet content while locked — matches the app's modal look AND
|
||||||
float backdropAlpha = ui::schema::UI().drawElement("screens.lock-screen", "backdrop-alpha").opacity;
|
// obscures the wallet for privacy (the old backdrop defaulted to 0 = fully visible). Capture-once
|
||||||
if (backdropAlpha > 0.0f) {
|
// on lock-open + resize; the opaque base inside DrawFullWindowBlurBackdrop guarantees the wallet
|
||||||
ImU32 backdropCol = IM_COL32(0, 0, 0, (int)(255 * backdropAlpha));
|
// is obscured even when the blur is unavailable (low-spec / acrylic-off / first frame / capture
|
||||||
dl->AddRectFilled(winPos, ImVec2(winPos.x + winSize.x, winPos.y + winSize.y), backdropCol);
|
// 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
|
// Card
|
||||||
@@ -776,8 +830,14 @@ void App::renderLockScreen() {
|
|||||||
ImVec2 cardMin(cardX, cardY);
|
ImVec2 cardMin(cardX, cardY);
|
||||||
ImVec2 cardMax(cardX + cardW, cardY + cardH);
|
ImVec2 cardMax(cardX + cardW, cardY + cardH);
|
||||||
|
|
||||||
ImU32 cardBg = ui::material::SurfaceVariant();
|
// Match the dialog cards: the same glass panel BeginOverlayDialog draws (renders opaque-dark on
|
||||||
dl->AddRectFilled(cardMin, cardMax, cardBg, 16.0f);
|
// 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;
|
float cy = cardY + 24.0f;
|
||||||
|
|
||||||
@@ -895,6 +955,12 @@ void App::renderLockScreen() {
|
|||||||
ImGui::PopStyleVar();
|
ImGui::PopStyleVar();
|
||||||
ImGui::PopItemWidth();
|
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;
|
cy += 40.0f + 12.0f;
|
||||||
|
|
||||||
// Focus the input when the lock screen first appears.
|
// Focus the input when the lock screen first appears.
|
||||||
@@ -922,10 +988,8 @@ void App::renderLockScreen() {
|
|||||||
float rowH = captionFont->LegacySize + 8.0f;
|
float rowH = captionFont->LegacySize + 8.0f;
|
||||||
if (lock_unlock_in_progress_) {
|
if (lock_unlock_in_progress_) {
|
||||||
// Animated spinner dots
|
// Animated spinner dots
|
||||||
int dots = ((int)(ImGui::GetTime() * 3.0f)) % 4;
|
|
||||||
const char* dotStr[] = {"", ".", "..", "..."};
|
|
||||||
char msg[64];
|
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);
|
ImVec2 ms = captionFont->CalcTextSizeA(captionFont->LegacySize, FLT_MAX, 0, msg);
|
||||||
dl->AddText(captionFont, captionFont->LegacySize,
|
dl->AddText(captionFont, captionFont->LegacySize,
|
||||||
ImVec2(cardX + (cardW - ms.x) * 0.5f, cy),
|
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::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnPrimary()));
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f);
|
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f);
|
||||||
ImGui::BeginDisabled(!canSubmit);
|
ImGui::BeginDisabled(!canSubmit);
|
||||||
bool btnClicked = ImGui::Button("Unlock", ImVec2(unlockW, unlockH));
|
bool btnClicked = ui::material::TactileButton("Unlock", ImVec2(unlockW, unlockH));
|
||||||
ImGui::EndDisabled();
|
ImGui::EndDisabled();
|
||||||
ImGui::PopStyleVar();
|
ImGui::PopStyleVar();
|
||||||
ImGui::PopStyleColor(3);
|
ImGui::PopStyleColor(3);
|
||||||
@@ -980,21 +1044,15 @@ void App::renderLockScreen() {
|
|||||||
if (!vaultOk) {
|
if (!vaultOk) {
|
||||||
bool noVault = !vault_ || !vault_->hasVault();
|
bool noVault = !vault_ || !vault_->hasVault();
|
||||||
return [this, noVault]() {
|
return [this, noVault]() {
|
||||||
lock_unlock_in_progress_ = false;
|
|
||||||
if (noVault) {
|
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_error_msg_ = TR("pin_not_set");
|
||||||
lock_use_pin_ = false;
|
lock_use_pin_ = false;
|
||||||
|
lock_error_timer_ = 3.0f;
|
||||||
} else {
|
} else {
|
||||||
lock_attempts_++;
|
applyUnlockFailure(TR("incorrect_pin"));
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -1015,25 +1073,15 @@ void App::renderLockScreen() {
|
|||||||
|
|
||||||
if (rpcOk) {
|
if (rpcOk) {
|
||||||
return [this, timeout, passphrase = std::move(passphrase)]() mutable {
|
return [this, timeout, passphrase = std::move(passphrase)]() mutable {
|
||||||
lock_unlock_in_progress_ = false;
|
applyUnlockSuccess(passphrase, timeout);
|
||||||
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);
|
|
||||||
util::SecureVault::secureZero(passphrase.data(), passphrase.size());
|
util::SecureVault::secureZero(passphrase.data(), passphrase.size());
|
||||||
};
|
};
|
||||||
} else {
|
} 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 {
|
return [this, rpcErr, passphrase = std::move(passphrase)]() mutable {
|
||||||
lock_unlock_in_progress_ = false;
|
applyUnlockFailure("Unlock failed: " + rpcErr);
|
||||||
lock_attempts_++;
|
|
||||||
lock_error_msg_ = "Unlock failed: " + rpcErr;
|
|
||||||
lock_error_timer_ = 3.0f;
|
|
||||||
util::SecureVault::secureZero(passphrase.data(), passphrase.size());
|
util::SecureVault::secureZero(passphrase.data(), passphrase.size());
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -1044,6 +1092,10 @@ void App::renderLockScreen() {
|
|||||||
unlockWallet(std::string(lock_passphrase_buf_), timeout);
|
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;
|
float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
|
||||||
ImGui::BeginDisabled(!valid || encrypt_in_progress_);
|
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_);
|
std::string pass(encrypt_pass_buf_);
|
||||||
enc_dlg_saved_passphrase_ = pass;
|
enc_dlg_saved_passphrase_ = pass;
|
||||||
memset(encrypt_pass_buf_, 0, sizeof(encrypt_pass_buf_));
|
memset(encrypt_pass_buf_, 0, sizeof(encrypt_pass_buf_));
|
||||||
@@ -1136,7 +1188,7 @@ void App::renderEncryptWalletDialog() {
|
|||||||
ImGui::EndDisabled();
|
ImGui::EndDisabled();
|
||||||
|
|
||||||
ImGui::SameLine();
|
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_pass_buf_, 0, sizeof(encrypt_pass_buf_));
|
||||||
memset(encrypt_confirm_buf_, 0, sizeof(encrypt_confirm_buf_));
|
memset(encrypt_confirm_buf_, 0, sizeof(encrypt_confirm_buf_));
|
||||||
show_encrypt_dialog_ = false;
|
show_encrypt_dialog_ = false;
|
||||||
@@ -1218,7 +1270,7 @@ void App::renderEncryptWalletDialog() {
|
|||||||
float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
|
float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
|
||||||
|
|
||||||
ImGui::BeginDisabled(!pinValid || !hasPassphrase || pin_in_progress_);
|
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;
|
pin_in_progress_ = true;
|
||||||
enc_dlg_pin_status_.clear();
|
enc_dlg_pin_status_.clear();
|
||||||
std::string savedPass = enc_dlg_saved_passphrase_;
|
std::string savedPass = enc_dlg_saved_passphrase_;
|
||||||
@@ -1255,7 +1307,7 @@ void App::renderEncryptWalletDialog() {
|
|||||||
ImGui::EndDisabled();
|
ImGui::EndDisabled();
|
||||||
|
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
if (ImGui::Button("Skip", ImVec2(btnW, 40))) {
|
if (ui::material::TactileButton("Skip", ImVec2(btnW, 40))) {
|
||||||
if (!enc_dlg_saved_passphrase_.empty()) {
|
if (!enc_dlg_saved_passphrase_.empty()) {
|
||||||
util::SecureVault::secureZero(&enc_dlg_saved_passphrase_[0],
|
util::SecureVault::secureZero(&enc_dlg_saved_passphrase_[0],
|
||||||
enc_dlg_saved_passphrase_.size());
|
enc_dlg_saved_passphrase_.size());
|
||||||
@@ -1310,7 +1362,7 @@ void App::renderEncryptWalletDialog() {
|
|||||||
strlen(change_new_pass_buf_) >= 8 &&
|
strlen(change_new_pass_buf_) >= 8 &&
|
||||||
strcmp(change_new_pass_buf_, change_confirm_buf_) == 0;
|
strcmp(change_new_pass_buf_, change_confirm_buf_) == 0;
|
||||||
ImGui::BeginDisabled(!valid || encrypt_in_progress_);
|
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_),
|
changePassphrase(std::string(change_old_pass_buf_),
|
||||||
std::string(change_new_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;
|
float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
|
||||||
ImGui::BeginDisabled(!valid || decryptState.inProgress);
|
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_);
|
std::string passphrase(decrypt_pass_buf_);
|
||||||
memset(decrypt_pass_buf_, 0, sizeof(decrypt_pass_buf_));
|
memset(decrypt_pass_buf_, 0, sizeof(decrypt_pass_buf_));
|
||||||
wallet_security_workflow_.start(std::chrono::steady_clock::now());
|
wallet_security_workflow_.start(std::chrono::steady_clock::now());
|
||||||
@@ -1567,7 +1619,7 @@ void App::renderDecryptWalletDialog() {
|
|||||||
ImGui::EndDisabled();
|
ImGui::EndDisabled();
|
||||||
|
|
||||||
ImGui::SameLine();
|
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_));
|
memset(decrypt_pass_buf_, 0, sizeof(decrypt_pass_buf_));
|
||||||
show_decrypt_dialog_ = false;
|
show_decrypt_dialog_ = false;
|
||||||
}
|
}
|
||||||
@@ -1685,7 +1737,7 @@ void App::renderDecryptWalletDialog() {
|
|||||||
"was saved as wallet.dat.encrypted.bak in your data directory.");
|
"was saved as wallet.dat.encrypted.bak in your data directory.");
|
||||||
|
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
if (ImGui::Button("Close", ImVec2(-1, 40))) {
|
if (ui::material::TactileButton("Close", ImVec2(-1, 40))) {
|
||||||
show_decrypt_dialog_ = false;
|
show_decrypt_dialog_ = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1702,11 +1754,11 @@ void App::renderDecryptWalletDialog() {
|
|||||||
|
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
|
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();
|
wallet_security_workflow_.reset();
|
||||||
}
|
}
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
if (ImGui::Button("Close", ImVec2(btnW, 40))) {
|
if (ui::material::TactileButton("Close", ImVec2(btnW, 40))) {
|
||||||
show_decrypt_dialog_ = false;
|
show_decrypt_dialog_ = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1762,7 +1814,7 @@ void App::renderPinDialogs() {
|
|||||||
strcmp(pin_buf_, pin_confirm_buf_) == 0;
|
strcmp(pin_buf_, pin_confirm_buf_) == 0;
|
||||||
|
|
||||||
ImGui::BeginDisabled(!valid || pin_in_progress_);
|
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_in_progress_ = true;
|
||||||
pin_status_ = "Verifying passphrase...";
|
pin_status_ = "Verifying passphrase...";
|
||||||
|
|
||||||
@@ -1858,7 +1910,7 @@ void App::renderPinDialogs() {
|
|||||||
strcmp(pin_buf_, pin_confirm_buf_) == 0;
|
strcmp(pin_buf_, pin_confirm_buf_) == 0;
|
||||||
|
|
||||||
ImGui::BeginDisabled(!valid || pin_in_progress_);
|
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_in_progress_ = true;
|
||||||
pin_status_ = "Changing PIN...";
|
pin_status_ = "Changing PIN...";
|
||||||
std::string oldPin(pin_old_buf_);
|
std::string oldPin(pin_old_buf_);
|
||||||
@@ -1917,7 +1969,7 @@ void App::renderPinDialogs() {
|
|||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
bool valid = strlen(pin_old_buf_) >= 4;
|
bool valid = strlen(pin_old_buf_) >= 4;
|
||||||
ImGui::BeginDisabled(!valid || pin_in_progress_);
|
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_in_progress_ = true;
|
||||||
pin_status_ = "Verifying PIN...";
|
pin_status_ = "Verifying PIN...";
|
||||||
std::string oldPin(pin_old_buf_);
|
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_ButtonHovered, ImGui::ColorConvertU32ToFloat4(ui::material::PrimaryVariant()));
|
||||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnPrimary()));
|
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnPrimary()));
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
||||||
if (ImGui::Button("Continue##app", ImVec2(btnW, btnH))) {
|
if (ui::material::TactileButton("Continue##app", ImVec2(btnW, btnH))) {
|
||||||
// Save appearance choices, advance to Bootstrap
|
// Save appearance choices, advance to Bootstrap
|
||||||
settings_->setAcrylicEnabled(wiz_blur_amount > 0.001f);
|
settings_->setAcrylicEnabled(wiz_blur_amount > 0.001f);
|
||||||
settings_->setAcrylicQuality(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;
|
float cancelBX = rightX + (colW - cancelW) * 0.5f;
|
||||||
ImGui::SetCursorScreenPos(ImVec2(cancelBX, cy));
|
ImGui::SetCursorScreenPos(ImVec2(cancelBX, cy));
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
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();
|
bootstrap_->cancel();
|
||||||
}
|
}
|
||||||
ImGui::PopStyleVar();
|
ImGui::PopStyleVar();
|
||||||
@@ -812,7 +812,7 @@ void App::renderFirstRunWizard() {
|
|||||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnPrimary()));
|
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnPrimary()));
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
||||||
ImGui::BeginDisabled(!supportsFullNodeLifecycleActions());
|
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
|
// Stop embedded daemon before bootstrap to avoid chain data corruption
|
||||||
stopDaemonForBootstrap();
|
stopDaemonForBootstrap();
|
||||||
bootstrap_ = std::make_unique<util::Bootstrap>();
|
bootstrap_ = std::make_unique<util::Bootstrap>();
|
||||||
@@ -826,7 +826,7 @@ void App::renderFirstRunWizard() {
|
|||||||
|
|
||||||
ImGui::SetCursorScreenPos(ImVec2(bx + btnW2 + 12.0f * dp, cy));
|
ImGui::SetCursorScreenPos(ImVec2(bx + btnW2 + 12.0f * dp, cy));
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
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;
|
wizard_phase_ = WizardPhase::EncryptOffer;
|
||||||
}
|
}
|
||||||
ImGui::PopStyleVar();
|
ImGui::PopStyleVar();
|
||||||
@@ -896,7 +896,7 @@ void App::renderFirstRunWizard() {
|
|||||||
IM_COL32(220, 60, 60, 255)));
|
IM_COL32(220, 60, 60, 255)));
|
||||||
ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(255, 255, 255, 255));
|
ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(255, 255, 255, 255));
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
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_stopping_external_ = true;
|
||||||
wizard_stop_status_ = "Sending stop command...";
|
wizard_stop_status_ = "Sending stop command...";
|
||||||
async_tasks_.submit("wizard-stop-external-daemon", [this](const util::AsyncTaskManager::Token& token) {
|
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::SetCursorScreenPos(ImVec2(bx + stopW + 12.0f * dp, cy));
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
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;
|
wizard_phase_ = WizardPhase::EncryptOffer;
|
||||||
}
|
}
|
||||||
ImGui::PopStyleVar();
|
ImGui::PopStyleVar();
|
||||||
@@ -995,7 +995,7 @@ void App::renderFirstRunWizard() {
|
|||||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnPrimary()));
|
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnPrimary()));
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
||||||
ImGui::BeginDisabled(!supportsFullNodeLifecycleActions());
|
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
|
// Stop embedded daemon before bootstrap to avoid chain data corruption
|
||||||
stopDaemonForBootstrap();
|
stopDaemonForBootstrap();
|
||||||
bootstrap_ = std::make_unique<util::Bootstrap>();
|
bootstrap_ = std::make_unique<util::Bootstrap>();
|
||||||
@@ -1014,7 +1014,7 @@ void App::renderFirstRunWizard() {
|
|||||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnSurface()));
|
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnSurface()));
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
||||||
ImGui::BeginDisabled(!supportsFullNodeLifecycleActions());
|
ImGui::BeginDisabled(!supportsFullNodeLifecycleActions());
|
||||||
if (ImGui::Button("Mirror##bs_mirror", ImVec2(mirrorW, btnH2))) {
|
if (ui::material::TactileButton("Mirror##bs_mirror", ImVec2(mirrorW, btnH2))) {
|
||||||
stopDaemonForBootstrap();
|
stopDaemonForBootstrap();
|
||||||
bootstrap_ = std::make_unique<util::Bootstrap>();
|
bootstrap_ = std::make_unique<util::Bootstrap>();
|
||||||
std::string dataDir = util::Platform::getDragonXDataDir();
|
std::string dataDir = util::Platform::getDragonXDataDir();
|
||||||
@@ -1032,7 +1032,7 @@ void App::renderFirstRunWizard() {
|
|||||||
// --- Skip button ---
|
// --- Skip button ---
|
||||||
ImGui::SetCursorScreenPos(ImVec2(bx + dlBtnW + 8.0f * dp + mirrorW + 8.0f * dp, cy));
|
ImGui::SetCursorScreenPos(ImVec2(bx + dlBtnW + 8.0f * dp + mirrorW + 8.0f * dp, cy));
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
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;
|
wizard_phase_ = WizardPhase::EncryptOffer;
|
||||||
}
|
}
|
||||||
ImGui::PopStyleVar();
|
ImGui::PopStyleVar();
|
||||||
@@ -1127,7 +1127,7 @@ void App::renderFirstRunWizard() {
|
|||||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(ui::material::PrimaryVariant()));
|
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(ui::material::PrimaryVariant()));
|
||||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnPrimary()));
|
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnPrimary()));
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
||||||
if (ImGui::Button("Continue##encok", ImVec2(btnW2, btnH2))) {
|
if (ui::material::TactileButton("Continue##encok", ImVec2(btnW2, btnH2))) {
|
||||||
wizard_phase_ = WizardPhase::Done;
|
wizard_phase_ = WizardPhase::Done;
|
||||||
settings_->setWizardCompleted(true);
|
settings_->setWizardCompleted(true);
|
||||||
settings_->save();
|
settings_->save();
|
||||||
@@ -1310,7 +1310,7 @@ void App::renderFirstRunWizard() {
|
|||||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnPrimary()));
|
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnPrimary()));
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
||||||
ImGui::BeginDisabled(!canEncrypt);
|
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
|
// Save passphrase + optional PIN for background processing
|
||||||
wallet_security_.beginDeferredEncryption(
|
wallet_security_.beginDeferredEncryption(
|
||||||
std::string(encrypt_pass_buf_),
|
std::string(encrypt_pass_buf_),
|
||||||
@@ -1341,7 +1341,7 @@ void App::renderFirstRunWizard() {
|
|||||||
|
|
||||||
ImGui::SetCursorScreenPos(ImVec2(bx + encBtnW + 12.0f * dp, cy));
|
ImGui::SetCursorScreenPos(ImVec2(bx + encBtnW + 12.0f * dp, cy));
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
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;
|
wizard_phase_ = WizardPhase::Done;
|
||||||
settings_->setWizardCompleted(true);
|
settings_->setWizardCompleted(true);
|
||||||
settings_->save();
|
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
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -84,438 +84,10 @@ struct HushChatTransactionExtractionResult {
|
|||||||
std::size_t ignored_memo_count = 0;
|
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 int kHushChatSupportedVersion = 0;
|
||||||
constexpr std::size_t kHushChatMemoByteLimit = 512;
|
constexpr std::size_t kHushChatMemoByteLimit = 512;
|
||||||
constexpr std::size_t kHushChatPublicKeyHexLength = 64;
|
constexpr std::size_t kHushChatPublicKeyHexLength = 64;
|
||||||
constexpr std::size_t kHushChatSecretstreamHeaderHexLength = 48;
|
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()
|
constexpr bool hushChatFeatureEnabledAtBuild()
|
||||||
{
|
{
|
||||||
@@ -527,60 +99,7 @@ HushChatMemoGroupingResult groupHushChatMemoOutputs(const std::vector<HushChatMe
|
|||||||
HushChatTransactionExtractionResult extractHushChatTransactionMetadata(
|
HushChatTransactionExtractionResult extractHushChatTransactionMetadata(
|
||||||
const HushChatTransactionInput& transaction,
|
const HushChatTransactionInput& transaction,
|
||||||
bool featureEnabled = hushChatFeatureEnabledAtBuild());
|
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* hushChatHeaderTypeName(HushChatHeaderType type);
|
||||||
const char* hushChatMemoGroupingIssueName(HushChatMemoGroupingIssue issue);
|
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 <fstream>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <ctime>
|
#include <ctime>
|
||||||
|
#include <algorithm>
|
||||||
|
#include <type_traits>
|
||||||
|
|
||||||
#include "../util/logger.h"
|
#include "../util/logger.h"
|
||||||
#include "../util/platform.h"
|
#include "../util/platform.h"
|
||||||
@@ -76,37 +78,43 @@ const char* poolSelectModeName(Settings::PoolSelectMode mode)
|
|||||||
return "manual";
|
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
|
} // namespace
|
||||||
|
|
||||||
std::string Settings::getDefaultPath()
|
std::string Settings::getDefaultPath()
|
||||||
{
|
{
|
||||||
#ifdef _WIN32
|
// Single per-platform, per-variant config dir (util::Platform::getConfigDir handles the
|
||||||
char path[MAX_PATH];
|
// _WIN32 / __APPLE__ / XDG split and the DRAGONX_APP_NAME variant suffix in one place).
|
||||||
if (SUCCEEDED(SHGetFolderPathA(NULL, CSIDL_APPDATA, NULL, 0, path))) {
|
const std::string dir = util::Platform::getConfigDir();
|
||||||
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;
|
|
||||||
fs::create_directories(dir);
|
fs::create_directories(dir);
|
||||||
return dir + "/settings.json";
|
return (fs::path(dir) / "settings.json").string();
|
||||||
#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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Settings::load()
|
bool Settings::load()
|
||||||
@@ -127,29 +135,29 @@ bool Settings::load(const std::string& path)
|
|||||||
json j;
|
json j;
|
||||||
file >> j;
|
file >> j;
|
||||||
|
|
||||||
if (j.contains("theme")) theme_ = j["theme"].get<std::string>();
|
loadScalar(j, "theme", theme_);
|
||||||
if (j.contains("save_ztxs")) save_ztxs_ = j["save_ztxs"].get<bool>();
|
loadScalar(j, "save_ztxs", save_ztxs_);
|
||||||
if (j.contains("auto_shield")) auto_shield_ = j["auto_shield"].get<bool>();
|
loadScalar(j, "auto_shield", auto_shield_);
|
||||||
if (j.contains("use_tor")) use_tor_ = j["use_tor"].get<bool>();
|
loadScalar(j, "use_tor", use_tor_);
|
||||||
if (j.contains("allow_custom_fees")) allow_custom_fees_ = j["allow_custom_fees"].get<bool>();
|
loadScalar(j, "allow_custom_fees", allow_custom_fees_);
|
||||||
if (j.contains("default_fee")) default_fee_ = j["default_fee"].get<double>();
|
loadScalar(j, "default_fee", default_fee_);
|
||||||
if (j.contains("fetch_prices")) fetch_prices_ = j["fetch_prices"].get<bool>();
|
loadScalar(j, "fetch_prices", fetch_prices_);
|
||||||
if (j.contains("tx_explorer_url")) tx_explorer_url_ = j["tx_explorer_url"].get<std::string>();
|
loadScalar(j, "tx_explorer_url", tx_explorer_url_);
|
||||||
if (j.contains("address_explorer_url")) address_explorer_url_ = j["address_explorer_url"].get<std::string>();
|
loadScalar(j, "address_explorer_url", address_explorer_url_);
|
||||||
if (j.contains("language")) language_ = j["language"].get<std::string>();
|
loadScalar(j, "language", language_);
|
||||||
if (j.contains("skin_id")) skin_id_ = j["skin_id"].get<std::string>();
|
loadScalar(j, "skin_id", skin_id_);
|
||||||
if (j.contains("acrylic_enabled")) acrylic_enabled_ = j["acrylic_enabled"].get<bool>();
|
loadScalar(j, "acrylic_enabled", acrylic_enabled_);
|
||||||
if (j.contains("acrylic_quality")) acrylic_quality_ = j["acrylic_quality"].get<int>();
|
loadScalar(j, "acrylic_quality", acrylic_quality_);
|
||||||
if (j.contains("blur_multiplier")) blur_multiplier_ = j["blur_multiplier"].get<float>();
|
loadScalar(j, "blur_multiplier", blur_multiplier_);
|
||||||
if (j.contains("noise_opacity")) noise_opacity_ = j["noise_opacity"].get<float>();
|
loadScalar(j, "noise_opacity", noise_opacity_);
|
||||||
if (j.contains("gradient_background")) gradient_background_ = j["gradient_background"].get<bool>();
|
loadScalar(j, "gradient_background", gradient_background_);
|
||||||
// Migrate legacy reduced_transparency bool -> ui_opacity float
|
// Migrate legacy reduced_transparency bool -> ui_opacity float
|
||||||
if (j.contains("ui_opacity")) {
|
if (j.contains("ui_opacity")) {
|
||||||
ui_opacity_ = j["ui_opacity"].get<float>();
|
ui_opacity_ = j["ui_opacity"].get<float>();
|
||||||
} else if (j.contains("reduced_transparency") && j["reduced_transparency"].get<bool>()) {
|
} else if (j.contains("reduced_transparency") && j["reduced_transparency"].get<bool>()) {
|
||||||
ui_opacity_ = 1.0f; // legacy: reduced = fully opaque
|
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.contains("balance_layout")) {
|
||||||
if (j["balance_layout"].is_string())
|
if (j["balance_layout"].is_string())
|
||||||
balance_layout_ = j["balance_layout"].get<std::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 (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()) {
|
if (j.contains("hidden_addresses") && j["hidden_addresses"].is_array()) {
|
||||||
hidden_addresses_.clear();
|
hidden_addresses_.clear();
|
||||||
for (const auto& a : j["hidden_addresses"])
|
for (const auto& a : j["hidden_addresses"])
|
||||||
@@ -189,13 +197,13 @@ bool Settings::load(const std::string& path)
|
|||||||
address_meta_[addr] = m;
|
address_meta_[addr] = m;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (j.contains("wizard_completed")) wizard_completed_ = j["wizard_completed"].get<bool>();
|
loadScalar(j, "wizard_completed", wizard_completed_);
|
||||||
if (j.contains("auto_lock_timeout")) auto_lock_timeout_ = j["auto_lock_timeout"].get<int>();
|
loadScalar(j, "auto_lock_timeout", auto_lock_timeout_);
|
||||||
if (j.contains("unlock_duration")) unlock_duration_ = j["unlock_duration"].get<int>();
|
loadScalar(j, "unlock_duration", unlock_duration_);
|
||||||
if (j.contains("pin_enabled")) pin_enabled_ = j["pin_enabled"].get<bool>();
|
loadScalar(j, "pin_enabled", pin_enabled_);
|
||||||
if (j.contains("keep_daemon_running")) keep_daemon_running_ = j["keep_daemon_running"].get<bool>();
|
loadScalar(j, "keep_daemon_running", keep_daemon_running_);
|
||||||
if (j.contains("stop_external_daemon")) stop_external_daemon_ = j["stop_external_daemon"].get<bool>();
|
loadScalar(j, "stop_external_daemon", stop_external_daemon_);
|
||||||
if (j.contains("max_connections")) max_connections_ = j["max_connections"].get<int>();
|
loadScalar(j, "max_connections", max_connections_);
|
||||||
if (j.contains("lite_wallet") && j["lite_wallet"].is_object()) {
|
if (j.contains("lite_wallet") && j["lite_wallet"].is_object()) {
|
||||||
const auto& lite = j["lite_wallet"];
|
const auto& lite = j["lite_wallet"];
|
||||||
if (lite.contains("server_selection_mode")) {
|
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 (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()) {
|
if (j.contains("debug_categories") && j["debug_categories"].is_array()) {
|
||||||
debug_categories_.clear();
|
debug_categories_.clear();
|
||||||
for (const auto& c : j["debug_categories"])
|
for (const auto& c : j["debug_categories"])
|
||||||
if (c.is_string()) debug_categories_.insert(c.get<std::string>());
|
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>();
|
loadScalar(j, "theme_effects_enabled", theme_effects_enabled_);
|
||||||
if (j.contains("low_spec_mode")) low_spec_mode_ = j["low_spec_mode"].get<bool>();
|
loadScalar(j, "low_spec_mode", low_spec_mode_);
|
||||||
if (j.contains("reduce_motion")) reduce_motion_ = j["reduce_motion"].get<bool>();
|
loadScalar(j, "reduce_motion", reduce_motion_);
|
||||||
if (j.contains("selected_exchange")) selected_exchange_ = j["selected_exchange"].get<std::string>();
|
loadScalar(j, "selected_exchange", selected_exchange_);
|
||||||
if (j.contains("selected_pair")) selected_pair_ = j["selected_pair"].get<std::string>();
|
loadScalar(j, "selected_pair", selected_pair_);
|
||||||
if (j.contains("pool_url")) pool_url_ = j["pool_url"].get<std::string>();
|
loadScalar(j, "pool_url", pool_url_);
|
||||||
// Migrate old default pool URL that was missing the stratum port
|
// Migrate old default pool URL that was missing the stratum port
|
||||||
if (pool_url_ == "pool.dragonx.is") pool_url_ = "pool.dragonx.is:3433";
|
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>();
|
loadScalar(j, "pool_algo", pool_algo_);
|
||||||
if (j.contains("pool_worker")) pool_worker_ = j["pool_worker"].get<std::string>();
|
loadScalar(j, "pool_worker", pool_worker_);
|
||||||
if (j.contains("pool_threads")) pool_threads_ = j["pool_threads"].get<int>();
|
loadScalar(j, "pool_threads", pool_threads_);
|
||||||
if (j.contains("pool_tls")) pool_tls_ = j["pool_tls"].get<bool>();
|
loadScalar(j, "pool_tls", pool_tls_);
|
||||||
if (j.contains("pool_hugepages")) pool_hugepages_ = j["pool_hugepages"].get<bool>();
|
loadScalar(j, "pool_hugepages", pool_hugepages_);
|
||||||
if (j.contains("pool_mode")) pool_mode_ = j["pool_mode"].get<bool>();
|
loadScalar(j, "pool_mode", pool_mode_);
|
||||||
if (j.contains("pool_select_mode")) pool_select_mode_ = parsePoolSelectMode(j["pool_select_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>();
|
loadScalar(j, "mine_when_idle", mine_when_idle_);
|
||||||
if (j.contains("xmrig_version")) xmrig_version_ = j["xmrig_version"].get<std::string>();
|
loadScalar(j, "xmrig_version", xmrig_version_);
|
||||||
if (j.contains("mine_idle_delay")) mine_idle_delay_= std::max(30, j["mine_idle_delay"].get<int>());
|
// Lower-bounded only (min 30s), matching setMineIdleDelay; now type-guarded too.
|
||||||
if (j.contains("idle_thread_scaling")) idle_thread_scaling_ = j["idle_thread_scaling"].get<bool>();
|
if (j.contains("mine_idle_delay") && j["mine_idle_delay"].is_number_integer())
|
||||||
if (j.contains("idle_threads_active")) idle_threads_active_ = j["idle_threads_active"].get<int>();
|
mine_idle_delay_ = std::max(30, j["mine_idle_delay"].get<int>());
|
||||||
if (j.contains("idle_threads_idle")) idle_threads_idle_ = j["idle_threads_idle"].get<int>();
|
loadScalar(j, "idle_thread_scaling", idle_thread_scaling_);
|
||||||
if (j.contains("idle_gpu_aware")) idle_gpu_aware_ = j["idle_gpu_aware"].get<bool>();
|
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()) {
|
if (j.contains("saved_pool_urls") && j["saved_pool_urls"].is_array()) {
|
||||||
saved_pool_urls_.clear();
|
saved_pool_urls_.clear();
|
||||||
for (const auto& u : j["saved_pool_urls"])
|
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())
|
if (e.contains("addresses") && e["addresses"].is_array())
|
||||||
for (const auto& a : e["addresses"])
|
for (const auto& a : e["addresses"])
|
||||||
if (a.is_string()) entry.addresses.push_back(a.get<std::string>());
|
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 (!entry.label.empty()) portfolio_entries_.push_back(std::move(entry));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (j.contains("font_scale") && j["font_scale"].is_number())
|
loadClamped(j, "font_scale", font_scale_, 1.0f, 1.5f);
|
||||||
font_scale_ = std::max(1.0f, std::min(1.5f, j["font_scale"].get<float>()));
|
loadScalar(j, "window_width", window_width_);
|
||||||
if (j.contains("window_width") && j["window_width"].is_number_integer())
|
loadScalar(j, "window_height", window_height_);
|
||||||
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>();
|
|
||||||
|
|
||||||
// Version tracking — detect upgrades so we can re-save with new defaults
|
// 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) {
|
if (settings_version_ != DRAGONX_VERSION) {
|
||||||
DEBUG_LOGF("Settings version %s differs from wallet %s — will re-save\n",
|
DEBUG_LOGF("Settings version %s differs from wallet %s — will re-save\n",
|
||||||
settings_version_.empty() ? "(none)" : settings_version_.c_str(),
|
settings_version_.empty() ? "(none)" : settings_version_.c_str(),
|
||||||
@@ -456,6 +493,21 @@ bool Settings::save(const std::string& path)
|
|||||||
entry["label"] = e.label;
|
entry["label"] = e.label;
|
||||||
entry["addresses"] = json::array();
|
entry["addresses"] = json::array();
|
||||||
for (const auto& a : e.addresses) entry["addresses"].push_back(a);
|
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["portfolio_entries"].push_back(std::move(entry));
|
||||||
}
|
}
|
||||||
j["font_scale"] = font_scale_;
|
j["font_scale"] = font_scale_;
|
||||||
|
|||||||
@@ -78,6 +78,26 @@ public:
|
|||||||
struct PortfolioEntry {
|
struct PortfolioEntry {
|
||||||
std::string label;
|
std::string label;
|
||||||
std::vector<std::string> addresses;
|
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
|
// Theme
|
||||||
|
|||||||
@@ -11,13 +11,6 @@
|
|||||||
#include "../util/logger.h"
|
#include "../util/logger.h"
|
||||||
#include "../util/platform.h"
|
#include "../util/platform.h"
|
||||||
|
|
||||||
#ifdef _WIN32
|
|
||||||
#include <shlobj.h>
|
|
||||||
#else
|
|
||||||
#include <pwd.h>
|
|
||||||
#include <unistd.h>
|
|
||||||
#endif
|
|
||||||
|
|
||||||
namespace fs = std::filesystem;
|
namespace fs = std::filesystem;
|
||||||
using json = nlohmann::json;
|
using json = nlohmann::json;
|
||||||
|
|
||||||
@@ -29,33 +22,11 @@ AddressBook::~AddressBook() = default;
|
|||||||
|
|
||||||
std::string AddressBook::getDefaultPath()
|
std::string AddressBook::getDefaultPath()
|
||||||
{
|
{
|
||||||
#ifdef _WIN32
|
// Co-locate with settings.json in the per-variant config dir (Lite -> ObsidianDragonLite/).
|
||||||
char path[MAX_PATH];
|
// util::Platform::getConfigDir() owns the per-platform + per-variant path in one place.
|
||||||
if (SUCCEEDED(SHGetFolderPathA(NULL, CSIDL_APPDATA, NULL, 0, path))) {
|
const std::string dir = util::Platform::getConfigDir();
|
||||||
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";
|
|
||||||
fs::create_directories(dir);
|
fs::create_directories(dir);
|
||||||
return dir + "/addressbook.json";
|
return (fs::path(dir) / "addressbook.json").string();
|
||||||
#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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool AddressBook::load()
|
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 <vector>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
|
#include <ctime>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
#include "exchange_info.h"
|
#include "exchange_info.h"
|
||||||
|
|
||||||
@@ -159,9 +161,19 @@ struct MarketInfo {
|
|||||||
bool price_loading = false;
|
bool price_loading = false;
|
||||||
std::string price_error;
|
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;
|
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
|
// Exchanges/pairs fetched live from CoinGecko (empty until fetched; the Market tab
|
||||||
// falls back to data::getExchangeRegistry() while empty).
|
// 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 <cstdio>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
|
#include <cstdint>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
|
#include <miniz.h>
|
||||||
#include "util/logger.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
|
#ifdef _WIN32
|
||||||
// Windows crash handler — writes a minidump and log entry before exit
|
// Windows crash handler — writes a minidump and log entry before exit
|
||||||
static LONG WINAPI CrashHandler(EXCEPTION_POINTERS* ep)
|
static LONG WINAPI CrashHandler(EXCEPTION_POINTERS* ep)
|
||||||
@@ -382,6 +441,184 @@ static std::string findPaymentURI(int argc, char* argv[])
|
|||||||
return "";
|
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[])
|
int main(int argc, char* argv[])
|
||||||
{
|
{
|
||||||
// Ensure ObsidianDragon config directory exists early (before any file I/O)
|
// 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();
|
ImDrawList* bgDL = ImGui::GetBackgroundDrawList();
|
||||||
ImVec2 p0 = vp->Pos;
|
ImVec2 p0 = vp->Pos;
|
||||||
ImVec2 p1 = ImVec2(vp->Pos.x + vp->Size.x, vp->Pos.y + vp->Size.y);
|
ImVec2 p1 = ImVec2(vp->Pos.x + vp->Size.x, vp->Pos.y + vp->Size.y);
|
||||||
ImTextureID curGradTex2 = ctx->app->getGradientTexture();
|
// Resize path historically always sampled the texture and always
|
||||||
float winOp2 = ctx->app->settings() ? ctx->app->settings()->getWindowOpacity() : 1.0f;
|
// added the capture callback — pass lowSpec = false to preserve
|
||||||
|
// that behavior exactly.
|
||||||
// Scale only background layer alpha by window opacity so the
|
drawWindowBackdrop(*ctx->app, bgDL, p0, p1, ctx->backdrop_active, /*lowSpec=*/false);
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGuiIO& io = ImGui::GetIO();
|
ImGuiIO& io = ImGui::GetIO();
|
||||||
@@ -1143,76 +1331,9 @@ int main(int argc, char* argv[])
|
|||||||
#ifdef __APPLE__
|
#ifdef __APPLE__
|
||||||
newScale = 1.0f; // macOS handles Retina via DisplayFramebufferScale
|
newScale = 1.0f; // macOS handles Retina via DisplayFramebufferScale
|
||||||
#endif
|
#endif
|
||||||
auto& typo = dragonx::ui::material::Typography::instance();
|
handleDisplayScaleChange(window, newScale, savedSizeForScale,
|
||||||
float oldScale = typo.getDpiScale();
|
lastKnownW, lastKnownH, dpiTargetW, dpiTargetH,
|
||||||
if (std::abs(newScale - oldScale) > 0.01f) {
|
dpiResizeRetries, dpiResizePending);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
needsRedraw = true; // Got an event — redraw
|
needsRedraw = true; // Got an event — redraw
|
||||||
}
|
}
|
||||||
@@ -1323,84 +1444,9 @@ int main(int argc, char* argv[])
|
|||||||
#ifdef __APPLE__
|
#ifdef __APPLE__
|
||||||
newScale = 1.0f; // macOS handles Retina via DisplayFramebufferScale
|
newScale = 1.0f; // macOS handles Retina via DisplayFramebufferScale
|
||||||
#endif
|
#endif
|
||||||
auto& typo = dragonx::ui::material::Typography::instance();
|
handleDisplayScaleChange(window, newScale, savedSizeForScale,
|
||||||
float oldScale = typo.getDpiScale();
|
lastKnownW, lastKnownH, dpiTargetW, dpiTargetH,
|
||||||
if (std::abs(newScale - oldScale) > 0.01f) {
|
dpiResizeRetries, dpiResizePending);
|
||||||
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");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1503,82 +1549,8 @@ int main(int argc, char* argv[])
|
|||||||
ImDrawList* bgDL = ImGui::GetBackgroundDrawList();
|
ImDrawList* bgDL = ImGui::GetBackgroundDrawList();
|
||||||
ImVec2 p0 = vp->Pos;
|
ImVec2 p0 = vp->Pos;
|
||||||
ImVec2 p1 = ImVec2(vp->Pos.x + vp->Size.x, vp->Pos.y + vp->Size.y);
|
ImVec2 p1 = ImVec2(vp->Pos.x + vp->Size.x, vp->Pos.y + vp->Size.y);
|
||||||
|
drawWindowBackdrop(app, bgDL, p0, p1, backdrop_active,
|
||||||
// Read the current gradient texture from the app each frame
|
dragonx::ui::effects::isLowSpecMode());
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
PERF_END("Backdrop", _perfBackdrop);
|
PERF_END("Backdrop", _perfBackdrop);
|
||||||
@@ -1719,6 +1691,12 @@ int main(int argc, char* argv[])
|
|||||||
}
|
}
|
||||||
ImGui_ImplDX11_RenderDrawData(ImGui::GetDrawData());
|
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)
|
// Update and Render additional Platform Windows (multi-viewport)
|
||||||
if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable)
|
if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable)
|
||||||
{
|
{
|
||||||
@@ -1742,6 +1720,12 @@ int main(int argc, char* argv[])
|
|||||||
glClear(GL_COLOR_BUFFER_BIT);
|
glClear(GL_COLOR_BUFFER_BIT);
|
||||||
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
|
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)
|
// Update and Render additional Platform Windows (multi-viewport)
|
||||||
if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable)
|
if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable)
|
||||||
{
|
{
|
||||||
|
|||||||
+50
-108
@@ -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);
|
emitRpcTrace(method);
|
||||||
|
|
||||||
json payload = makePayload(method, params);
|
json payload = makePayload(method, params);
|
||||||
@@ -316,16 +311,21 @@ json RPCClient::call(const std::string& method, const json& params)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check HTTP response code
|
// Check HTTP response code
|
||||||
long http_code = 0;
|
httpCodeOut = 0;
|
||||||
curl_easy_getinfo(impl_->curl, CURLINFO_RESPONSE_CODE, &http_code);
|
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
|
// Bitcoin/Hush RPC returns HTTP 500 for application-level errors
|
||||||
// (insufficient funds, bad params, etc.) with a valid JSON body.
|
// (insufficient funds, bad params, etc.) with a valid JSON body.
|
||||||
// Parse the body first to extract the real error message.
|
// Parse the body first to extract the real error message.
|
||||||
if (http_code != 200) {
|
if (httpCode != 200) {
|
||||||
int errCode = 0;
|
int errCode = 0;
|
||||||
try {
|
try {
|
||||||
json response = json::parse(response_data);
|
json response = json::parse(body);
|
||||||
if (response.contains("error") && response["error"].is_object()) {
|
if (response.contains("error") && response["error"].is_object()) {
|
||||||
if (response["error"].contains("code") && response["error"]["code"].is_number_integer())
|
if (response["error"].contains("code") && response["error"]["code"].is_number_integer())
|
||||||
errCode = response["error"]["code"].get<int>();
|
errCode = response["error"]["code"].get<int>();
|
||||||
@@ -337,10 +337,10 @@ json RPCClient::call(const std::string& method, const json& params)
|
|||||||
} catch (const json::exception&) {
|
} catch (const json::exception&) {
|
||||||
// Body wasn't valid JSON — fall through to generic HTTP error
|
// Body wasn't valid JSON — fall through to generic HTTP error
|
||||||
}
|
}
|
||||||
throw 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()) {
|
if (response.contains("error") && !response["error"].is_null()) {
|
||||||
int errCode = 0;
|
int errCode = 0;
|
||||||
@@ -358,6 +358,18 @@ json RPCClient::call(const std::string& method, const json& params)
|
|||||||
return response["result"];
|
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)
|
json RPCClient::call(const std::string& method, const json& params, long timeoutSec)
|
||||||
{
|
{
|
||||||
std::lock_guard<std::recursive_mutex> lk(curl_mutex_);
|
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");
|
throw std::runtime_error("Not connected");
|
||||||
}
|
}
|
||||||
|
|
||||||
emitRpcTrace(method);
|
|
||||||
|
|
||||||
// Temporarily override timeout
|
// Temporarily override timeout
|
||||||
long prevTimeout = 30L;
|
long prevTimeout = 30L;
|
||||||
curl_easy_setopt(impl_->curl, CURLOPT_TIMEOUT, timeoutSec);
|
curl_easy_setopt(impl_->curl, CURLOPT_TIMEOUT, timeoutSec);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Unlock before calling to avoid recursive lock issues — but we already hold it,
|
long http_code = 0;
|
||||||
// and call() also locks with recursive_mutex, so just delegate to the body directly.
|
std::string response_data = performCall(method, params, http_code);
|
||||||
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);
|
|
||||||
|
|
||||||
// Restore original timeout
|
// Restore original timeout
|
||||||
curl_easy_setopt(impl_->curl, CURLOPT_TIMEOUT, prevTimeout);
|
curl_easy_setopt(impl_->curl, CURLOPT_TIMEOUT, prevTimeout);
|
||||||
|
return parseRpcResult(http_code, response_data);
|
||||||
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"];
|
|
||||||
} catch (...) {
|
} catch (...) {
|
||||||
// Ensure timeout is always restored
|
// Ensure timeout is always restored
|
||||||
curl_easy_setopt(impl_->curl, CURLOPT_TIMEOUT, prevTimeout);
|
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);
|
doRPC("z_listreceivedbyaddress", {address, minconf}, cb, err);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unified callback versions
|
// Adapts a UnifiedCallback into the (Callback, ErrorCallback) pair doRPC expects: the
|
||||||
void RPCClient::getInfo(UnifiedCallback cb)
|
// 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) {
|
[cb](const json& result) {
|
||||||
if (cb) cb(result, "");
|
if (cb) cb(result, "");
|
||||||
},
|
},
|
||||||
[cb](const std::string& error) {
|
[cb](const std::string& error) {
|
||||||
if (cb) cb(json{}, 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)
|
void RPCClient::rescanBlockchain(int startHeight, UnifiedCallback cb)
|
||||||
{
|
{
|
||||||
// hush/komodo daemons expose this as "rescan <height>", not bitcoin's "rescanblockchain".
|
// hush/komodo daemons expose this as "rescan <height>", not bitcoin's "rescanblockchain".
|
||||||
doRPC("rescan", {startHeight},
|
auto handlers = splitUnified(std::move(cb));
|
||||||
[cb](const json& result) {
|
doRPC("rescan", {startHeight}, handlers.first, handlers.second);
|
||||||
if (cb) cb(result, "");
|
|
||||||
},
|
|
||||||
[cb](const std::string& error) {
|
|
||||||
if (cb) cb(json{}, error);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void RPCClient::z_shieldCoinbase(const std::string& fromAddr, const std::string& toAddr,
|
void RPCClient::z_shieldCoinbase(const std::string& fromAddr, const std::string& toAddr,
|
||||||
double fee, int limit, UnifiedCallback cb)
|
double fee, int limit, UnifiedCallback cb)
|
||||||
{
|
{
|
||||||
doRPC("z_shieldcoinbase", {fromAddr, toAddr, fee, limit},
|
auto handlers = splitUnified(std::move(cb));
|
||||||
[cb](const json& result) {
|
doRPC("z_shieldcoinbase", {fromAddr, toAddr, fee, limit}, handlers.first, handlers.second);
|
||||||
if (cb) cb(result, "");
|
|
||||||
},
|
|
||||||
[cb](const std::string& error) {
|
|
||||||
if (cb) cb(json{}, error);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void RPCClient::z_mergeToAddress(const std::vector<std::string>& fromAddrs, const std::string& toAddr,
|
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) {
|
for (const auto& addr : fromAddrs) {
|
||||||
addrs.push_back(addr);
|
addrs.push_back(addr);
|
||||||
}
|
}
|
||||||
doRPC("z_mergetoaddress", {addrs, toAddr, fee, 0, limit},
|
auto handlers = splitUnified(std::move(cb));
|
||||||
[cb](const json& result) {
|
doRPC("z_mergetoaddress", {addrs, toAddr, fee, 0, limit}, handlers.first, handlers.second);
|
||||||
if (cb) cb(result, "");
|
|
||||||
},
|
|
||||||
[cb](const std::string& error) {
|
|
||||||
if (cb) cb(json{}, error);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void RPCClient::z_getOperationStatus(const std::vector<std::string>& opids, UnifiedCallback cb)
|
void RPCClient::z_getOperationStatus(const std::vector<std::string>& opids, UnifiedCallback cb)
|
||||||
@@ -824,14 +778,8 @@ void RPCClient::z_getOperationStatus(const std::vector<std::string>& opids, Unif
|
|||||||
for (const auto& id : opids) {
|
for (const auto& id : opids) {
|
||||||
ids.push_back(id);
|
ids.push_back(id);
|
||||||
}
|
}
|
||||||
doRPC("z_getoperationstatus", {ids},
|
auto handlers = splitUnified(std::move(cb));
|
||||||
[cb](const json& result) {
|
doRPC("z_getoperationstatus", {ids}, handlers.first, handlers.second);
|
||||||
if (cb) cb(result, "");
|
|
||||||
},
|
|
||||||
[cb](const std::string& error) {
|
|
||||||
if (cb) cb(json{}, error);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void RPCClient::getBlock(int height, UnifiedCallback cb)
|
void RPCClient::getBlock(int height, UnifiedCallback cb)
|
||||||
@@ -840,14 +788,8 @@ void RPCClient::getBlock(int height, UnifiedCallback cb)
|
|||||||
getBlockHash(height,
|
getBlockHash(height,
|
||||||
[this, cb](const json& hashResult) {
|
[this, cb](const json& hashResult) {
|
||||||
std::string hash = hashResult.get<std::string>();
|
std::string hash = hashResult.get<std::string>();
|
||||||
getBlock(hash,
|
auto handlers = splitUnified(cb);
|
||||||
[cb](const json& blockResult) {
|
getBlock(hash, handlers.first, handlers.second);
|
||||||
if (cb) cb(blockResult, "");
|
|
||||||
},
|
|
||||||
[cb](const std::string& error) {
|
|
||||||
if (cb) cb(json{}, error);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
[cb](const std::string& error) {
|
[cb](const std::string& error) {
|
||||||
if (cb) cb(json{}, error);
|
if (cb) cb(json{}, error);
|
||||||
|
|||||||
@@ -245,6 +245,17 @@ private:
|
|||||||
json makePayload(const std::string& method, const json& params = json::array());
|
json makePayload(const std::string& method, const json& params = json::array());
|
||||||
void doRPC(const std::string& method, const json& params, Callback cb, ErrorCallback err);
|
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 host_;
|
||||||
std::string port_;
|
std::string port_;
|
||||||
std::string auth_; // Base64 encoded "user:password"
|
std::string auth_; // Base64 encoded "user:password"
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
#include "../util/logger.h"
|
#include "../util/logger.h"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <cctype>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <map>
|
#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(
|
NetworkRefreshService::PriceHttpResult NetworkRefreshService::parsePriceHttpResponse(
|
||||||
const PriceHttpResponse& response,
|
const PriceHttpResponse& response,
|
||||||
std::time_t fetchedAt)
|
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(
|
NetworkRefreshService::DispatchTicket NetworkRefreshService::beginDispatch(
|
||||||
Job job, std::size_t queuedWork, std::size_t maxQueuedWork)
|
Job job, std::size_t queuedWork, std::size_t maxQueuedWork)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -250,6 +250,10 @@ public:
|
|||||||
std::time_t fetchedAt);
|
std::time_t fetchedAt);
|
||||||
static PriceHttpResult parsePriceHttpResponse(const PriceHttpResponse& response,
|
static PriceHttpResult parsePriceHttpResponse(const PriceHttpResponse& response,
|
||||||
std::time_t fetchedAt);
|
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,
|
static AddressInfo buildShieldedAddressInfo(const std::string& address,
|
||||||
const nlohmann::json& validation,
|
const nlohmann::json& validation,
|
||||||
bool validationSucceeded);
|
bool validationSucceeded);
|
||||||
@@ -293,6 +297,23 @@ public:
|
|||||||
static OperationStatusPollResult parseOperationStatusPoll(const nlohmann::json& result,
|
static OperationStatusPollResult parseOperationStatusPoll(const nlohmann::json& result,
|
||||||
const std::vector<std::string>& requestedOpids);
|
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 applyConnectionInfoResult(WalletState& state, const ConnectionInfoResult& result);
|
||||||
static void applyWalletEncryptionResult(WalletState& state, const WalletEncryptionResult& result);
|
static void applyWalletEncryptionResult(WalletState& state, const WalletEncryptionResult& result);
|
||||||
static void applyConnectionInitResult(WalletState& state, const ConnectionInitResult& result);
|
static void applyConnectionInitResult(WalletState& state, const ConnectionInitResult& result);
|
||||||
|
|||||||
@@ -355,6 +355,20 @@ void AcrylicMaterial::captureBackgroundDirect()
|
|||||||
blurCacheValid_ = false; // force re-blur with fresh capture
|
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
|
bool AcrylicMaterial::shouldSkipEffect() const
|
||||||
{
|
{
|
||||||
return !settings_.enabled ||
|
return !settings_.enabled ||
|
||||||
@@ -1238,6 +1252,26 @@ void AcrylicMaterial::captureBackgroundDirect()
|
|||||||
blurCacheValid_ = false;
|
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)
|
void AcrylicMaterial::applyBlur(float radius)
|
||||||
{
|
{
|
||||||
if (!dx_blurSRV_[0] || !dx_blurPS_ || !dx_context_) return;
|
if (!dx_blurSRV_[0] || !dx_blurPS_ || !dx_context_) return;
|
||||||
@@ -1666,6 +1700,7 @@ void AcrylicMaterial::shutdown() {}
|
|||||||
void AcrylicMaterial::resize(int, int) {}
|
void AcrylicMaterial::resize(int, int) {}
|
||||||
void AcrylicMaterial::captureBackground() {}
|
void AcrylicMaterial::captureBackground() {}
|
||||||
void AcrylicMaterial::captureBackgroundDirect() {}
|
void AcrylicMaterial::captureBackgroundDirect() {}
|
||||||
|
void AcrylicMaterial::captureLiveFramebuffer() {}
|
||||||
void AcrylicMaterial::applyBlur(float) {}
|
void AcrylicMaterial::applyBlur(float) {}
|
||||||
ImTextureID AcrylicMaterial::getBlurredTexture() const { return 0; }
|
ImTextureID AcrylicMaterial::getBlurredTexture() const { return 0; }
|
||||||
ImTextureID AcrylicMaterial::getNoiseTexture() const { return 0; }
|
ImTextureID AcrylicMaterial::getNoiseTexture() const { return 0; }
|
||||||
|
|||||||
@@ -166,6 +166,16 @@ public:
|
|||||||
*/
|
*/
|
||||||
void captureBackgroundDirect();
|
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.
|
* @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
|
} } } } // 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 dragonx {
|
||||||
namespace ui {
|
namespace ui {
|
||||||
@@ -233,6 +236,17 @@ ImDrawCallback GetBackgroundCaptureCallback()
|
|||||||
return BackgroundCaptureCallback;
|
return BackgroundCaptureCallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void LiveCaptureCallback(const ImDrawList*, const ImDrawCmd*)
|
||||||
|
{
|
||||||
|
if (!s_initialized) return;
|
||||||
|
getAcrylicMaterial().captureLiveFramebuffer();
|
||||||
|
}
|
||||||
|
|
||||||
|
ImDrawCallback GetLiveCaptureCallback()
|
||||||
|
{
|
||||||
|
return LiveCaptureCallback;
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Drawing Functions
|
// Drawing Functions
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -638,408 +652,6 @@ void ApplyBlurAmount(float blur)
|
|||||||
} // namespace ui
|
} // namespace ui
|
||||||
} // namespace dragonx
|
} // 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
|
#else // neither GLAD nor DX11
|
||||||
|
|
||||||
namespace dragonx {
|
namespace dragonx {
|
||||||
@@ -1056,6 +668,7 @@ void BeginFrame(int, int) {}
|
|||||||
void CaptureBackground() {}
|
void CaptureBackground() {}
|
||||||
void InvalidateCapture() {}
|
void InvalidateCapture() {}
|
||||||
ImDrawCallback GetBackgroundCaptureCallback() { return nullptr; }
|
ImDrawCallback GetBackgroundCaptureCallback() { return nullptr; }
|
||||||
|
ImDrawCallback GetLiveCaptureCallback() { return nullptr; }
|
||||||
|
|
||||||
void DrawAcrylicRect(ImDrawList* drawList,
|
void DrawAcrylicRect(ImDrawList* drawList,
|
||||||
const ImVec2& pMin, const ImVec2& pMax,
|
const ImVec2& pMin, const ImVec2& pMax,
|
||||||
@@ -1173,6 +786,5 @@ void ApplyBlurAmount(float) {}
|
|||||||
} // namespace ui
|
} // namespace ui
|
||||||
} // namespace dragonx
|
} // 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();
|
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
|
// Drawing Functions
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|||||||
@@ -15,6 +15,23 @@ namespace dragonx {
|
|||||||
namespace ui {
|
namespace ui {
|
||||||
namespace effects {
|
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
|
// Singleton
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -327,16 +344,13 @@ void ThemeEffects::drawShimmer(ImDrawList* dl, ImVec2 pMin, ImVec2 pMax,
|
|||||||
if (clLeft >= clRight) return;
|
if (clLeft >= clRight) return;
|
||||||
|
|
||||||
float mid = (clLeft + clRight) * 0.5f;
|
float mid = (clLeft + clRight) * 0.5f;
|
||||||
int peakA = (int)(shimmer_.alpha * bgOpacity_ * 255.0f);
|
int peakA = scaledAlpha(shimmer_.alpha, bgOpacity_);
|
||||||
peakA = std::clamp(peakA, 0, 255);
|
|
||||||
|
|
||||||
// Extract shimmer color RGB (ignore alpha, use shimmer_.alpha)
|
// Extract shimmer color RGB (ignore alpha, use shimmer_.alpha)
|
||||||
int sR = shimmer_.color & 0xFF;
|
RGB s = unpackRGB(shimmer_.color);
|
||||||
int sG = (shimmer_.color >> 8) & 0xFF;
|
|
||||||
int sB = (shimmer_.color >> 16) & 0xFF;
|
|
||||||
|
|
||||||
ImU32 clear = IM_COL32(sR, sG, sB, 0);
|
ImU32 clear = IM_COL32(s.r, s.g, s.b, 0);
|
||||||
ImU32 peak = IM_COL32(sR, sG, sB, peakA);
|
ImU32 peak = IM_COL32(s.r, s.g, s.b, peakA);
|
||||||
|
|
||||||
// Inset clip rect by corner rounding to prevent shimmer bleeding into rounded corners
|
// 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;
|
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 minDim = std::min(w, h);
|
||||||
float glareR = minDim * specular_glare_.radius;
|
float glareR = minDim * specular_glare_.radius;
|
||||||
|
|
||||||
int cR = specular_glare_.color & 0xFF;
|
RGB c = unpackRGB(specular_glare_.color);
|
||||||
int cG = (specular_glare_.color >> 8) & 0xFF;
|
|
||||||
int cB = (specular_glare_.color >> 16) & 0xFF;
|
|
||||||
|
|
||||||
// Per-panel seed from position — unique drift per panel
|
// Per-panel seed from position — unique drift per panel
|
||||||
float seed = pMin.x * 0.0073f + pMin.y * 0.0137f;
|
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;
|
if (a <= 0) continue;
|
||||||
|
|
||||||
dl->AddCircleFilled(ImVec2(cx, cy), ringRadius,
|
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 s = positional_hue_.strength;
|
||||||
float inv = 1.0f - s;
|
float inv = 1.0f - s;
|
||||||
|
|
||||||
int bR = baseColor & 0xFF;
|
RGB base = unpackRGB(baseColor);
|
||||||
int bG = (baseColor >> 8) & 0xFF;
|
|
||||||
int bB = (baseColor >> 16) & 0xFF;
|
|
||||||
int bA = (baseColor >> 24) & 0xFF;
|
int bA = (baseColor >> 24) & 0xFF;
|
||||||
|
|
||||||
int tR = tint & 0xFF;
|
RGB t = unpackRGB(tint);
|
||||||
int tG = (tint >> 8) & 0xFF;
|
|
||||||
int tB = (tint >> 16) & 0xFF;
|
|
||||||
|
|
||||||
int r = (int)(bR * inv + tR * s);
|
int r = (int)(base.r * inv + t.r * s);
|
||||||
int g = (int)(bG * inv + tG * s);
|
int g = (int)(base.g * inv + t.g * s);
|
||||||
int b = (int)(bB * inv + tB * s);
|
int b = (int)(base.b * inv + t.b * s);
|
||||||
|
|
||||||
return IM_COL32(r, g, b, bA);
|
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);
|
float alpha = glow_pulse_.minAlpha + phase * (glow_pulse_.maxAlpha - glow_pulse_.minAlpha);
|
||||||
if (alpha <= 0.001f) return;
|
if (alpha <= 0.001f) return;
|
||||||
|
|
||||||
int a = std::clamp((int)(alpha * bgOpacity_ * 255.0f), 0, 255);
|
int a = scaledAlpha(alpha, bgOpacity_);
|
||||||
int cR = glow_pulse_.color & 0xFF;
|
RGB c = unpackRGB(glow_pulse_.color);
|
||||||
int cG = (glow_pulse_.color >> 8) & 0xFF;
|
|
||||||
int cB = (glow_pulse_.color >> 16) & 0xFF;
|
|
||||||
|
|
||||||
// Border-only glow: concentric outlines with smooth Gaussian falloff.
|
// Border-only glow: concentric outlines with smooth Gaussian falloff.
|
||||||
// Use many fine sub-rings (4× the pixel radius) to avoid visible
|
// 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(
|
dl->AddRect(
|
||||||
ImVec2(pMin.x - expand, pMin.y - expand),
|
ImVec2(pMin.x - expand, pMin.y - expand),
|
||||||
ImVec2(pMax.x + expand, pMax.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);
|
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;
|
float phase = std::sin(time_ * gradient_border_.speed * 2.0f * 3.14159265f) * 0.5f + 0.5f;
|
||||||
|
|
||||||
// Extract RGBA from both colors and lerp
|
// Extract RGBA from both colors and lerp
|
||||||
int aR = gradient_border_.colorA & 0xFF;
|
RGB ca = unpackRGB(gradient_border_.colorA);
|
||||||
int aG = (gradient_border_.colorA >> 8) & 0xFF;
|
RGB cb = unpackRGB(gradient_border_.colorB);
|
||||||
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;
|
|
||||||
|
|
||||||
int r = aR + (int)((bR - aR) * phase);
|
int r = ca.r + (int)((cb.r - ca.r) * phase);
|
||||||
int g = aG + (int)((bG - aG) * phase);
|
int g = ca.g + (int)((cb.g - ca.g) * phase);
|
||||||
int b = aB + (int)((bB - aB) * phase);
|
int b = ca.b + (int)((cb.b - ca.b) * phase);
|
||||||
int a = std::clamp((int)(gradient_border_.alpha * bgOpacity_ * 255.0f), 0, 255);
|
int a = scaledAlpha(gradient_border_.alpha, bgOpacity_);
|
||||||
|
|
||||||
// Draw the shifting border
|
// Draw the shifting border
|
||||||
dl->AddRect(pMin, pMax, IM_COL32(r, g, b, a),
|
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 headPos = std::fmod(time_ * edge_trace_.speed, 1.0f);
|
||||||
float traceLen = edge_trace_.length;
|
float traceLen = edge_trace_.length;
|
||||||
|
|
||||||
int cR = edge_trace_.color & 0xFF;
|
RGB c = unpackRGB(edge_trace_.color);
|
||||||
int cG = (edge_trace_.color >> 8) & 0xFF;
|
|
||||||
int cB = (edge_trace_.color >> 16) & 0xFF;
|
|
||||||
|
|
||||||
// Draw the trace as connected line segments with fading alpha.
|
// Draw the trace as connected line segments with fading alpha.
|
||||||
// Use enough segments to make curved corners smooth.
|
// 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
|
aFrac = aFrac * aFrac; // quadratic falloff for natural fade
|
||||||
int a = (int)(edge_trace_.alpha * aFrac * bgOpacity_ * 255.0f);
|
int a = (int)(edge_trace_.alpha * aFrac * bgOpacity_ * 255.0f);
|
||||||
if (a > 0) {
|
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;
|
prev = pt;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bright dot at the head
|
// 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);
|
ImVec2 headPt = perimeterPoint(pMin, pMax, headPos, rounding);
|
||||||
dl->AddCircleFilled(headPt, edge_trace_.thickness * 1.5f,
|
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;
|
float h = pMax.y - pMin.y;
|
||||||
if (w <= 0 || h <= 0) return;
|
if (w <= 0 || h <= 0) return;
|
||||||
|
|
||||||
int cR = ember_rise_.color & 0xFF;
|
RGB c = unpackRGB(ember_rise_.color);
|
||||||
int cG = (ember_rise_.color >> 8) & 0xFF;
|
|
||||||
int cB = (ember_rise_.color >> 16) & 0xFF;
|
|
||||||
|
|
||||||
for (int i = 0; i < ember_rise_.count; i++) {
|
for (int i = 0; i < ember_rise_.count; i++) {
|
||||||
// Golden-ratio spacing gives evenly distributed phases
|
// Golden-ratio spacing gives evenly distributed phases
|
||||||
@@ -723,11 +721,11 @@ void ThemeEffects::drawEmberRise(ImDrawList* dl, ImVec2 pMin, ImVec2 pMax) const
|
|||||||
|
|
||||||
// Warm core
|
// Warm core
|
||||||
dl->AddCircleFilled(ImVec2(x, y), size,
|
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
|
// Softer outer glow
|
||||||
if (a > 30) {
|
if (a > 30) {
|
||||||
dl->AddCircleFilled(ImVec2(x, y), size * 2.0f,
|
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;
|
float vpY = vp->WorkPos.y;
|
||||||
if (vpW <= 0 || vpH <= 0) return;
|
if (vpW <= 0 || vpH <= 0) return;
|
||||||
|
|
||||||
int cR = ember_rise_.color & 0xFF;
|
RGB c = unpackRGB(ember_rise_.color);
|
||||||
int cG = (ember_rise_.color >> 8) & 0xFF;
|
|
||||||
int cB = (ember_rise_.color >> 16) & 0xFF;
|
|
||||||
|
|
||||||
// Viewport embers: more particles, spread across the whole screen
|
// Viewport embers: more particles, spread across the whole screen
|
||||||
int vpCount = ember_rise_.count * 3;
|
int vpCount = ember_rise_.count * 3;
|
||||||
@@ -777,10 +773,10 @@ void ThemeEffects::drawViewportEmbers(ImDrawList* dl) const {
|
|||||||
if (a <= 0) continue;
|
if (a <= 0) continue;
|
||||||
|
|
||||||
dl->AddCircleFilled(ImVec2(x, y), size,
|
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) {
|
if (a > 20) {
|
||||||
dl->AddCircleFilled(ImVec2(x, y), size * 2.5f,
|
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;
|
float vpY = vp->WorkPos.y;
|
||||||
if (vpW <= 0 || vpH <= 0) return;
|
if (vpW <= 0 || vpH <= 0) return;
|
||||||
|
|
||||||
int cR = sandstorm_.color & 0xFF;
|
RGB c = unpackRGB(sandstorm_.color);
|
||||||
int cG = (sandstorm_.color >> 8) & 0xFF;
|
|
||||||
int cB = (sandstorm_.color >> 16) & 0xFF;
|
|
||||||
|
|
||||||
// Wind direction vector from angle (degrees from horizontal)
|
// Wind direction vector from angle (degrees from horizontal)
|
||||||
float windRad = sandstorm_.windAngle * 3.14159265f / 180.0f;
|
float windRad = sandstorm_.windAngle * 3.14159265f / 180.0f;
|
||||||
@@ -875,21 +869,21 @@ void ThemeEffects::drawSandstorm(ImDrawList* dl) const {
|
|||||||
dl->AddLine(
|
dl->AddLine(
|
||||||
ImVec2(x, y),
|
ImVec2(x, y),
|
||||||
ImVec2(x + sx, y + sy),
|
ImVec2(x + sx, y + sy),
|
||||||
IM_COL32(cR, cG, cB, a),
|
IM_COL32(c.r, c.g, c.b, a),
|
||||||
size * 0.7f);
|
size * 0.7f);
|
||||||
// Bright head dot
|
// Bright head dot
|
||||||
dl->AddCircleFilled(ImVec2(x, y), size * 0.5f,
|
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 {
|
} else {
|
||||||
// Small/slow particles: simple circle
|
// Small/slow particles: simple circle
|
||||||
dl->AddCircleFilled(ImVec2(x, y), size,
|
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)
|
// Occasional larger dust puff (every ~8th particle, double size, half alpha)
|
||||||
if (i % 8 == 0 && a > 15) {
|
if (i % 8 == 0 && a > 15) {
|
||||||
dl->AddCircleFilled(ImVec2(x, y), size * 2.5f,
|
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 headPos = std::fmod(time_ * edge_trace_.speed + posKey, 1.0f);
|
||||||
float traceLen = edge_trace_.length;
|
float traceLen = edge_trace_.length;
|
||||||
|
|
||||||
int cR = edge_trace_.color & 0xFF;
|
RGB c = unpackRGB(edge_trace_.color);
|
||||||
int cG = (edge_trace_.color >> 8) & 0xFF;
|
|
||||||
int cB = (edge_trace_.color >> 16) & 0xFF;
|
|
||||||
|
|
||||||
// Panel edge trace is subtler than sidebar — 60% alpha
|
// Panel edge trace is subtler than sidebar — 60% alpha
|
||||||
float panelAlpha = edge_trace_.alpha * 0.6f * bgOpacity_;
|
float panelAlpha = edge_trace_.alpha * 0.6f * bgOpacity_;
|
||||||
@@ -935,7 +927,7 @@ void ThemeEffects::drawPanelEffects(ImDrawList* dl, ImVec2 pMin, ImVec2 pMax,
|
|||||||
aFrac = aFrac * aFrac;
|
aFrac = aFrac * aFrac;
|
||||||
int a = (int)(panelAlpha * aFrac * 255.0f);
|
int a = (int)(panelAlpha * aFrac * 255.0f);
|
||||||
if (a > 0) {
|
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);
|
edge_trace_.thickness * 0.8f);
|
||||||
}
|
}
|
||||||
prev = pt;
|
prev = pt;
|
||||||
@@ -945,7 +937,7 @@ void ThemeEffects::drawPanelEffects(ImDrawList* dl, ImVec2 pMin, ImVec2 pMax,
|
|||||||
int headA = (int)(panelAlpha * 255.0f);
|
int headA = (int)(panelAlpha * 255.0f);
|
||||||
ImVec2 headPt = perimeterPoint(pMin, pMax, headPos, rounding);
|
ImVec2 headPt = perimeterPoint(pMin, pMax, headPos, rounding);
|
||||||
dl->AddCircleFilled(headPt, edge_trace_.thickness * 1.2f,
|
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);
|
int panelCount = std::max(2, ember_rise_.count / 3);
|
||||||
float panelAlpha = ember_rise_.alpha * 0.5f * bgOpacity_;
|
float panelAlpha = ember_rise_.alpha * 0.5f * bgOpacity_;
|
||||||
|
|
||||||
int cR = ember_rise_.color & 0xFF;
|
RGB c = unpackRGB(ember_rise_.color);
|
||||||
int cG = (ember_rise_.color >> 8) & 0xFF;
|
|
||||||
int cB = (ember_rise_.color >> 16) & 0xFF;
|
|
||||||
|
|
||||||
// Use panel position as seed for unique particle distribution
|
// Use panel position as seed for unique particle distribution
|
||||||
float seed = pMin.x * 0.013f + pMin.y * 0.031f;
|
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;
|
if (a <= 0) continue;
|
||||||
|
|
||||||
dl->AddCircleFilled(ImVec2(x, y), size,
|
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();
|
dl->PopClipRect();
|
||||||
@@ -1074,14 +1064,12 @@ void ThemeEffects::drawViewportOverlay(ImDrawList* dl) const {
|
|||||||
// === Vignette: edge darkening/tinting (4 gradient strips) ===
|
// === Vignette: edge darkening/tinting (4 gradient strips) ===
|
||||||
// Overlapping strips naturally darken corners more than edges — cinematic look
|
// Overlapping strips naturally darken corners more than edges — cinematic look
|
||||||
if (viewport_overlay_.vignetteEnabled) {
|
if (viewport_overlay_.vignetteEnabled) {
|
||||||
int cR = viewport_overlay_.vignetteColor & 0xFF;
|
RGB c = unpackRGB(viewport_overlay_.vignetteColor);
|
||||||
int cG = (viewport_overlay_.vignetteColor >> 8) & 0xFF;
|
int maxA = scaledAlpha(viewport_overlay_.vignetteAlpha, bgOpacity_);
|
||||||
int cB = (viewport_overlay_.vignetteColor >> 16) & 0xFF;
|
|
||||||
int maxA = std::clamp((int)(viewport_overlay_.vignetteAlpha * bgOpacity_ * 255.0f), 0, 255);
|
|
||||||
|
|
||||||
if (maxA > 0) {
|
if (maxA > 0) {
|
||||||
ImU32 edgeCol = IM_COL32(cR, cG, cB, maxA);
|
ImU32 edgeCol = IM_COL32(c.r, c.g, c.b, maxA);
|
||||||
ImU32 clearCol = IM_COL32(cR, cG, cB, 0);
|
ImU32 clearCol = IM_COL32(c.r, c.g, c.b, 0);
|
||||||
|
|
||||||
float fadeW = vpSize.x * viewport_overlay_.vignetteRadius;
|
float fadeW = vpSize.x * viewport_overlay_.vignetteRadius;
|
||||||
float fadeH = vpSize.y * 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_Border] = ImVec4(1.0f, 1.0f, 1.0f, 0.15f);
|
||||||
colors[ImGuiCol_BorderShadow] = ImVec4(0, 0, 0, 0.08f);
|
colors[ImGuiCol_BorderShadow] = ImVec4(0, 0, 0, 0.08f);
|
||||||
} else {
|
} 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);
|
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_FrameBgHovered] = ImVec4(1.0f, 1.0f, 1.0f, 0.10f);
|
||||||
colors[ImGuiCol_FrameBgActive] = ImVec4(1.0f, 1.0f, 1.0f, 0.15f);
|
colors[ImGuiCol_FrameBgActive] = ImVec4(1.0f, 1.0f, 1.0f, 0.15f);
|
||||||
} else {
|
} else {
|
||||||
colors[ImGuiCol_FrameBg] = ImVec4(0, 0, 0, 0.04f);
|
// Light themes: the old 4/7/10% black fills were nearly invisible on a white surface (input
|
||||||
colors[ImGuiCol_FrameBgHovered] = ImVec4(0, 0, 0, 0.07f);
|
// fields blended in). Darker fills + a 1px frame border (below) so inputs/dropdowns read as
|
||||||
colors[ImGuiCol_FrameBgActive] = ImVec4(0, 0, 0, 0.10f);
|
// 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
|
// 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_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);
|
colors[ImGuiCol_ButtonActive] = ImVec4(1.0f, 1.0f, 1.0f, 45.0f / 255.0f);
|
||||||
} else {
|
} else {
|
||||||
colors[ImGuiCol_Button] = ImVec4(0, 0, 0, 0.05f);
|
// Darker overlays on light themes — the old 5/8/12% were barely-there grey and washed out;
|
||||||
colors[ImGuiCol_ButtonHovered] = ImVec4(0, 0, 0, 0.08f);
|
// bumped again so the small/secondary buttons read as clearly as the main ones.
|
||||||
colors[ImGuiCol_ButtonActive] = ImVec4(0, 0, 0, 0.12f);
|
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)
|
// 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.WindowBorderSize = bwElem.extraFloats.count("window") ? bwElem.extraFloats.at("window") : 1.0f;
|
||||||
style.ChildBorderSize = bwElem.extraFloats.count("child") ? bwElem.extraFloats.at("child") : 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.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.TabBorderSize = 0.0f;
|
||||||
|
|
||||||
style.AntiAliasedLines = true;
|
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
|
|
||||||
+594
-110
@@ -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);
|
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
|
// Text Drop Shadow
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -107,7 +123,9 @@ inline bool IsCurrentWindowOverlayDialog()
|
|||||||
{
|
{
|
||||||
ImGuiWindow* window = ImGui::GetCurrentWindow();
|
ImGuiWindow* window = ImGui::GetCurrentWindow();
|
||||||
for (ImGuiWindow* node = window; node; node = node->ParentWindow) {
|
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 true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@@ -210,15 +228,35 @@ inline void DrawTactileOverlay(ImDrawList* dl, const ImVec2& bMin,
|
|||||||
dl->PopClipRect();
|
dl->PopClipRect();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convenience: call right after any ImGui::Button() / SmallButton() call
|
// Glass-card button surface: frosted-glass hover/idle fill (skipped while
|
||||||
// to add tactile depth. Uses the last item rect automatically.
|
// pressed) + rim light + tactile depth overlay. Shared by the tactile
|
||||||
inline void ApplyTactile(ImDrawList* dl = nullptr)
|
// 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();
|
// Light themes: flat buttons (like the Manage-portfolio modal's plain buttons). Skip the glass
|
||||||
ImVec2 bMin = ImGui::GetItemRectMin();
|
// sheen + tactile 3D — ImGui::Button already draws the fill/hover/active — and add just a subtle
|
||||||
ImVec2 bMax = ImGui::GetItemRectMax();
|
// defining edge, which reads cleaner on light surfaces than the raised glass look.
|
||||||
float rounding = ImGui::GetStyle().FrameRounding;
|
if (IsLightTheme()) {
|
||||||
bool active = ImGui::IsItemActive();
|
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);
|
DrawTactileOverlay(dl, bMin, bMax, rounding, active);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -241,6 +279,159 @@ inline ImFont* resolveButtonFont(int perButton, int sectionDefault)
|
|||||||
return resolveButtonFont(perButton >= 0 ? perButton : 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 ────────────────────────────────────────────────────
|
// ── Tactile wrappers ────────────────────────────────────────────────────
|
||||||
// Drop-in replacements for ImGui::Button / SmallButton that automatically
|
// Drop-in replacements for ImGui::Button / SmallButton that automatically
|
||||||
// add the tactile highlight overlay after rendering.
|
// 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;
|
float rounding = ImGui::GetStyle().FrameRounding;
|
||||||
bool active = ImGui::IsItemActive();
|
bool active = ImGui::IsItemActive();
|
||||||
bool hovered = ImGui::IsItemHovered();
|
bool hovered = ImGui::IsItemHovered();
|
||||||
// Frosted glass highlight — subtle fill on top
|
DrawButtonGlassOverlay(dl, bMin, bMax, rounding, active, hovered);
|
||||||
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);
|
|
||||||
return pressed;
|
return pressed;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -306,20 +484,44 @@ inline bool TactileSmallButton(const char* label, ImFont* font = nullptr)
|
|||||||
float rounding = ImGui::GetStyle().FrameRounding;
|
float rounding = ImGui::GetStyle().FrameRounding;
|
||||||
bool active = ImGui::IsItemActive();
|
bool active = ImGui::IsItemActive();
|
||||||
bool hovered = ImGui::IsItemHovered();
|
bool hovered = ImGui::IsItemHovered();
|
||||||
if (!active) {
|
DrawButtonGlassOverlay(dl, bMin, bMax, rounding, active, hovered);
|
||||||
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);
|
|
||||||
return pressed;
|
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)
|
// Glass Panel (glassmorphism card)
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -335,11 +537,19 @@ struct GlassPanelSpec {
|
|||||||
float borderWidth = 1.0f;
|
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,
|
inline void DrawGlassPanel(ImDrawList* dl, const ImVec2& pMin,
|
||||||
const ImVec2& pMax,
|
const ImVec2& pMax,
|
||||||
const GlassPanelSpec& spec = GlassPanelSpec())
|
const GlassPanelSpec& spec = GlassPanelSpec())
|
||||||
{
|
{
|
||||||
if (IsBackdropActive() && !dragonx::ui::effects::isLowSpecMode()) {
|
if (IsBackdropActive() && !dragonx::ui::effects::isLowSpecMode() && !IsFullWindowBlurOverlayActive()) {
|
||||||
// --- Cached color lookups (invalidated on theme change) ---
|
// --- Cached color lookups (invalidated on theme change) ---
|
||||||
// These 3 resolveColor() calls do string parsing + map lookup
|
// These 3 resolveColor() calls do string parsing + map lookup
|
||||||
// each time. Cache them per-frame using the schema generation
|
// 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
|
// 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;
|
float rounding = ImGui::GetStyle().FrameRounding;
|
||||||
bool active = ImGui::IsItemActive();
|
bool active = ImGui::IsItemActive();
|
||||||
bool hovered = ImGui::IsItemHovered();
|
bool hovered = ImGui::IsItemHovered();
|
||||||
if (!active) {
|
DrawButtonGlassOverlay(dl, bMin, bMax, rounding, active, hovered);
|
||||||
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);
|
|
||||||
|
|
||||||
ImGui::PopStyleVar(styleCount);
|
ImGui::PopStyleVar(styleCount);
|
||||||
ImGui::PopStyleColor(colorCount);
|
ImGui::PopStyleColor(colorCount);
|
||||||
@@ -911,6 +1189,94 @@ inline bool DrawDialogTitleBar(const char* title, bool* p_open, ImU32 accent_col
|
|||||||
return closeClicked;
|
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
|
// 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::unordered_map<std::string, float> g_overlayCardHeights;
|
||||||
inline std::string g_overlayCurrentKey;
|
inline std::string g_overlayCurrentKey;
|
||||||
|
|
||||||
inline bool BeginOverlayDialog(const char* title, bool* p_open, float cardWidth = 460.0f, float scrimOpacity = 0.92f,
|
// The dark base color for any full-window overlay backdrop (dialog scrims + the shutdown screen),
|
||||||
float cardBottomViewportRatio = 0.85f, const char* idSuffix = nullptr)
|
// 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();
|
MarkOverlayDialogActive();
|
||||||
|
|
||||||
ImGuiViewport* vp = ImGui::GetMainViewport();
|
ImGuiViewport* vp = ImGui::GetMainViewport();
|
||||||
ImVec2 vp_pos = vp->Pos;
|
ImVec2 vp_pos = vp->Pos, vp_size = vp->Size;
|
||||||
ImVec2 vp_size = vp->Size;
|
|
||||||
|
|
||||||
// Dialog widths are authored as raw pixels, but the fonts/spacing inside scale with
|
// Cards are authored in raw px but scale with dpiScale (font-size setting); clamp to viewport.
|
||||||
// Layout::dpiScale() (which includes the user's font-size setting). Scale the card by the same
|
float cardWidth = spec.cardWidth * dp;
|
||||||
// factor so the content doesn't outgrow a fixed card and overflow/misalign at non-default
|
|
||||||
// scales. No-op at the default scale (dpiScale() == 1). Clamped to the viewport so a large scale
|
|
||||||
// can't push the card off-screen.
|
|
||||||
cardWidth *= Layout::dpiScale();
|
|
||||||
cardWidth = std::min(cardWidth, vp_size.x - 32.0f);
|
cardWidth = std::min(cardWidth, vp_size.x - 32.0f);
|
||||||
|
|
||||||
// Fullscreen scrim overlay
|
|
||||||
ImGui::SetNextWindowPos(vp_pos);
|
ImGui::SetNextWindowPos(vp_pos);
|
||||||
ImGui::SetNextWindowSize(vp_size);
|
ImGui::SetNextWindowSize(vp_size);
|
||||||
ImGui::SetNextWindowFocus();
|
// Focus the scrim so the dialog owns input — but not while a popup (combo/color picker) opened
|
||||||
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.04f, 0.04f, 0.06f, scrimOpacity));
|
// 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_WindowRounding, 0.0f);
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0));
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0));
|
||||||
|
|
||||||
std::string scrimId = "##OverlayScrim";
|
std::string scrimId = "##OverlayScrim";
|
||||||
std::string childId = "##OverlayDialogContent";
|
std::string childId = "##OverlayDialogContent";
|
||||||
if (idSuffix && idSuffix[0] != '\0') {
|
if (spec.idSuffix && spec.idSuffix[0] != '\0') {
|
||||||
scrimId += "_";
|
scrimId += "_"; scrimId += spec.idSuffix;
|
||||||
scrimId += idSuffix;
|
childId += "_"; childId += spec.idSuffix;
|
||||||
childId += "_";
|
|
||||||
childId += idSuffix;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool opened = ImGui::Begin(scrimId.c_str(), nullptr,
|
bool opened = ImGui::Begin(scrimId.c_str(), nullptr,
|
||||||
@@ -973,69 +1364,119 @@ inline bool BeginOverlayDialog(const char* title, bool* p_open, float cardWidth
|
|||||||
|
|
||||||
ImDrawList* dl = ImGui::GetWindowDrawList();
|
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||||
|
|
||||||
// Consume pointer input on the scrim so the overlay owns clicks and wheel
|
// Live-blur backdrop with capture-once: (re)capture the content drawn BELOW this overlay for a
|
||||||
// events even when the click lands outside the card content.
|
// 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::SetCursorScreenPos(vp_pos);
|
||||||
ImGui::InvisibleButton("##OverlayInputBlocker", vp_size,
|
ImGui::InvisibleButton("##OverlayInputBlocker", vp_size,
|
||||||
ImGuiButtonFlags_MouseButtonLeft |
|
ImGuiButtonFlags_MouseButtonLeft |
|
||||||
ImGuiButtonFlags_MouseButtonRight |
|
ImGuiButtonFlags_MouseButtonRight |
|
||||||
ImGuiButtonFlags_MouseButtonMiddle);
|
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 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
|
// Card background — a glass panel unless the content floats directly on the backdrop.
|
||||||
// drawn to a fixed viewport ratio left a tall band of empty glass under short dialogs. Reuse the
|
if (!floating) {
|
||||||
// height the child reported LAST frame (content is stable frame-to-frame, so no visible lag) and
|
GlassPanelSpec cardGlass;
|
||||||
// fall back to the ratio on the first frame. Still capped at the ratio so a very tall dialog can't
|
cardGlass.rounding = 16.0f; cardGlass.fillAlpha = 35; cardGlass.borderAlpha = 50; cardGlass.borderWidth = 1.0f;
|
||||||
// run off-screen (its content spills/scrolls as before).
|
DrawGlassPanel(dl, cardMin, cardMax, cardGlass);
|
||||||
g_overlayCurrentKey = childId;
|
|
||||||
float ratioMaxY = vp_pos.y + vp_size.y * cardBottomViewportRatio;
|
|
||||||
auto prevHeightIt = g_overlayCardHeights.find(childId);
|
|
||||||
float cardBottomY = (prevHeightIt != g_overlayCardHeights.end() && prevHeightIt->second > 0.0f)
|
|
||||||
? std::min(cardY + prevHeightIt->second, ratioMaxY)
|
|
||||||
: ratioMaxY;
|
|
||||||
|
|
||||||
// Draw glass card background
|
|
||||||
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) &&
|
|
||||||
!ImGui::IsMouseHoveringRect(cardMin, cardMax, false)) {
|
|
||||||
*p_open = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set up child region for card content
|
// 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)) {
|
||||||
|
*spec.p_open = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Content child.
|
||||||
ImGui::SetCursorScreenPos(ImVec2(cardX, cardY));
|
ImGui::SetCursorScreenPos(ImVec2(cardX, cardY));
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 16.0f);
|
ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, floating ? 20.0f : 16.0f);
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(28, 24));
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, floating ? ImVec2(28, 20) : ImVec2(28, 24));
|
||||||
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0, 0, 0, 0)); // Transparent - glass already drawn
|
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(),
|
bool childVisible = ImGui::BeginChild(childId.c_str(),
|
||||||
ImVec2(cardWidth, 0), // 0 height = auto-size
|
ImVec2(cardWidth, fixedHeight ? (cardBottomY - cardY) : 0.0f),
|
||||||
ImGuiChildFlags_AutoResizeY | ImGuiChildFlags_AlwaysUseWindowPadding,
|
cflags, ImGuiWindowFlags_NoScrollbar);
|
||||||
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));
|
||||||
|
}
|
||||||
|
|
||||||
// Draw title bar with close button
|
if (childVisible && spec.title) {
|
||||||
if (childVisible && title) {
|
if (plainHd) {
|
||||||
DrawDialogTitleBar(title, p_open);
|
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;
|
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()
|
inline void EndOverlayDialog()
|
||||||
{
|
{
|
||||||
ImGui::EndChild();
|
ImGui::EndChild();
|
||||||
@@ -1069,6 +1510,49 @@ inline void BeginOverlayDialogFooter(float totalActionWidth, bool drawSeparator
|
|||||||
PlaceOverlayDialogActions(totalActionWidth);
|
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 material
|
||||||
} // namespace ui
|
} // namespace ui
|
||||||
} // namespace dragonx
|
} // 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
|
||||||
+170
-342
@@ -170,6 +170,51 @@ struct SettingsPageState {
|
|||||||
|
|
||||||
static SettingsPageState s_settingsState;
|
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.
|
// Count whitespace-separated words in a (seed) buffer — used to validate/guide restore input.
|
||||||
static int liteSeedWordCount(const char* s) {
|
static int liteSeedWordCount(const char* s) {
|
||||||
int words = 0;
|
int words = 0;
|
||||||
@@ -274,7 +319,7 @@ static void evaluateLiteLifecycleRequestFromPageState(App* app) {
|
|||||||
}
|
}
|
||||||
if (started) {
|
if (started) {
|
||||||
s_settingsState.lite_lifecycle_pending = true;
|
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();
|
s_settingsState.lite_lifecycle_summary.clear();
|
||||||
} else {
|
} else {
|
||||||
// Rejected before any thread launched (wallet already open, an attempt in flight, or no
|
// Rejected before any thread launched (wallet already open, an attempt in flight, or no
|
||||||
@@ -287,16 +332,12 @@ static void evaluateLiteLifecycleRequestFromPageState(App* app) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto result = wallet::executeLiteWalletLifecycleUiRequest(*app->settings(), input);
|
// No linked lite backend to execute against (full-node build, or the lite backend
|
||||||
s_settingsState.lite_lifecycle_summary = result.requestSummaryRedacted;
|
// failed to load / rollout-disabled). The live path above returns when a backend is
|
||||||
if (result.ok) {
|
// present, so reaching here means there is nothing to run.
|
||||||
s_settingsState.lite_lifecycle_status = "Ready";
|
s_settingsState.lite_lifecycle_summary.clear();
|
||||||
} else {
|
s_settingsState.lite_lifecycle_status = "Lite wallet backend unavailable";
|
||||||
s_settingsState.lite_lifecycle_status = result.error.empty()
|
Notifications::instance().warning(s_settingsState.lite_lifecycle_status);
|
||||||
? wallet::liteWalletLifecycleUiExecutionStatusName(result.status)
|
|
||||||
: result.error;
|
|
||||||
Notifications::instance().warning(s_settingsState.lite_lifecycle_status);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// (APPEARANCE card now uses ChannelsSplit like all other cards)
|
// (APPEARANCE card now uses ChannelsSplit like all other cards)
|
||||||
@@ -432,28 +473,10 @@ void RenderSettingsPage(App* app) {
|
|||||||
if (s_settingsState.low_spec_mode != runtimeLowSpec) {
|
if (s_settingsState.low_spec_mode != runtimeLowSpec) {
|
||||||
if (runtimeLowSpec) {
|
if (runtimeLowSpec) {
|
||||||
// Hotkey turned low-spec ON — save snapshot, override statics
|
// Hotkey turned low-spec ON — save snapshot, override statics
|
||||||
s_settingsState.low_spec_snapshot.valid = true;
|
enterLowSpec(false);
|
||||||
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;
|
|
||||||
} else if (s_settingsState.low_spec_snapshot.valid) {
|
} else if (s_settingsState.low_spec_snapshot.valid) {
|
||||||
// Hotkey turned low-spec OFF — restore snapshot
|
// Hotkey turned low-spec OFF — restore snapshot
|
||||||
s_settingsState.blur_amount = s_settingsState.low_spec_snapshot.blur_amount;
|
exitLowSpec(false);
|
||||||
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;
|
|
||||||
} else if (app->settings()) {
|
} else if (app->settings()) {
|
||||||
// No snapshot — read prefs from settings file
|
// No snapshot — read prefs from settings file
|
||||||
s_settingsState.blur_amount = app->settings()->getBlurMultiplier();
|
s_settingsState.blur_amount = app->settings()->getBlurMultiplier();
|
||||||
@@ -565,12 +588,7 @@ void RenderSettingsPage(App* app) {
|
|||||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("theme_language"));
|
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("theme_language"));
|
||||||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||||
|
|
||||||
ImVec2 cardMin = ImGui::GetCursorScreenPos();
|
material::GlassCardScope card(dl, availWidth, pad, bottomPad, glassSpec);
|
||||||
dl->ChannelsSplit(2);
|
|
||||||
dl->ChannelsSetCurrent(1);
|
|
||||||
|
|
||||||
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMin.y + pad));
|
|
||||||
ImGui::Indent(pad);
|
|
||||||
|
|
||||||
float contentW = availWidth - pad * 2;
|
float contentW = availWidth - pad * 2;
|
||||||
float comboGap = S.drawElement("components.settings-page", "combo-row-gap").size;
|
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()));
|
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||||
|
|
||||||
// --- Collapsible: Advanced Effects... ---
|
// --- Collapsible: Advanced Effects... ---
|
||||||
{
|
material::CollapsibleHeader(dl, "##EffectsToggle", TR("advanced_effects"),
|
||||||
const char* arrow = s_settingsState.effects_expanded ? ICON_MD_EXPAND_LESS : ICON_MD_EXPAND_MORE;
|
s_settingsState.effects_expanded, contentW,
|
||||||
ImGui::PushFont(body2);
|
body2, OnSurfaceMedium());
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (s_settingsState.effects_expanded) {
|
if (s_settingsState.effects_expanded) {
|
||||||
ImGui::PushFont(body2);
|
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)) {
|
if (ImGui::Checkbox(TrId("low_spec_mode", "low_spec").c_str(), &s_settingsState.low_spec_mode)) {
|
||||||
effects::setLowSpecMode(s_settingsState.low_spec_mode);
|
effects::setLowSpecMode(s_settingsState.low_spec_mode);
|
||||||
if (s_settingsState.low_spec_mode) {
|
if (s_settingsState.low_spec_mode) {
|
||||||
s_settingsState.low_spec_snapshot.valid = true;
|
enterLowSpec(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;
|
|
||||||
} else if (s_settingsState.low_spec_snapshot.valid) {
|
} else if (s_settingsState.low_spec_snapshot.valid) {
|
||||||
s_settingsState.blur_amount = s_settingsState.low_spec_snapshot.blur_amount;
|
exitLowSpec(true);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
saveSettingsPageState(app->settings());
|
saveSettingsPageState(app->settings());
|
||||||
}
|
}
|
||||||
@@ -1069,26 +1043,10 @@ void RenderSettingsPage(App* app) {
|
|||||||
|
|
||||||
// --- Collapsible: Advanced Effects... ---
|
// --- Collapsible: Advanced Effects... ---
|
||||||
{
|
{
|
||||||
const char* arrow = s_settingsState.effects_expanded ? ICON_MD_EXPAND_LESS : ICON_MD_EXPAND_MORE;
|
float narrowContentW = availWidth - pad * 2;
|
||||||
ImGui::PushFont(body2);
|
material::CollapsibleHeader(dl, "##EffectsToggleN", TR("advanced_effects"),
|
||||||
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0,0,0,0));
|
s_settingsState.effects_expanded, narrowContentW,
|
||||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(1,1,1,0.05f));
|
body2, OnSurfaceMedium());
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (s_settingsState.effects_expanded) {
|
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)) {
|
if (ImGui::Checkbox(TrId("low_spec_mode", "low_spec").c_str(), &s_settingsState.low_spec_mode)) {
|
||||||
effects::setLowSpecMode(s_settingsState.low_spec_mode);
|
effects::setLowSpecMode(s_settingsState.low_spec_mode);
|
||||||
if (s_settingsState.low_spec_mode) {
|
if (s_settingsState.low_spec_mode) {
|
||||||
s_settingsState.low_spec_snapshot.valid = true;
|
enterLowSpec(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;
|
|
||||||
} else if (s_settingsState.low_spec_snapshot.valid) {
|
} else if (s_settingsState.low_spec_snapshot.valid) {
|
||||||
s_settingsState.blur_amount = s_settingsState.low_spec_snapshot.blur_amount;
|
exitLowSpec(true);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
saveSettingsPageState(app->settings());
|
saveSettingsPageState(app->settings());
|
||||||
}
|
}
|
||||||
@@ -1223,19 +1155,6 @@ void RenderSettingsPage(App* app) {
|
|||||||
ImGui::PopFont();
|
ImGui::PopFont();
|
||||||
} // s_settingsState.effects_expanded
|
} // 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));
|
ImGui::Dummy(ImVec2(0, gap));
|
||||||
@@ -1247,11 +1166,8 @@ void RenderSettingsPage(App* app) {
|
|||||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("wallet"));
|
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("wallet"));
|
||||||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||||
|
|
||||||
ImVec2 cardMin = ImGui::GetCursorScreenPos();
|
material::GlassCardScope card(dl, availWidth, pad, bottomPad, glassSpec);
|
||||||
dl->ChannelsSplit(2);
|
const ImVec2& cardMin = card.cardMin;
|
||||||
dl->ChannelsSetCurrent(1);
|
|
||||||
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMin.y + pad));
|
|
||||||
ImGui::Indent(pad);
|
|
||||||
|
|
||||||
float contentW = availWidth - pad * 2;
|
float contentW = availWidth - pad * 2;
|
||||||
|
|
||||||
@@ -1311,27 +1227,9 @@ void RenderSettingsPage(App* app) {
|
|||||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||||
|
|
||||||
// --- Collapsible: Tools & Actions... ---
|
// --- Collapsible: Tools & Actions... ---
|
||||||
{
|
material::CollapsibleHeader(dl, "##ToolsToggle", TR("tools_actions"),
|
||||||
const char* arrow = s_settingsState.tools_expanded ? ICON_MD_EXPAND_LESS : ICON_MD_EXPAND_MORE;
|
s_settingsState.tools_expanded, contentW,
|
||||||
ImGui::PushFont(body2);
|
body2, OnSurfaceMedium());
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (s_settingsState.tools_expanded) {
|
if (s_settingsState.tools_expanded) {
|
||||||
float btnSpacing = Layout::spacingMd();
|
float btnSpacing = Layout::spacingMd();
|
||||||
@@ -1439,17 +1337,6 @@ void RenderSettingsPage(App* app) {
|
|||||||
|
|
||||||
if (scale < 1.0f) ImGui::SetWindowFontScale(1.0f);
|
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));
|
ImGui::Dummy(ImVec2(0, gap));
|
||||||
@@ -1462,11 +1349,7 @@ void RenderSettingsPage(App* app) {
|
|||||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("node_security"));
|
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("node_security"));
|
||||||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||||
|
|
||||||
ImVec2 cardMin = ImGui::GetCursorScreenPos();
|
material::GlassCardScope card(dl, availWidth, pad, bottomPad, glassSpec);
|
||||||
dl->ChannelsSplit(2);
|
|
||||||
dl->ChannelsSetCurrent(1);
|
|
||||||
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMin.y + pad));
|
|
||||||
ImGui::Indent(pad);
|
|
||||||
|
|
||||||
float contentW = availWidth - pad * 2;
|
float contentW = availWidth - pad * 2;
|
||||||
float minBtnW = S.drawElement("components.settings-page", "wallet-btn-min-width").sizeOr(130.0f);
|
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.
|
// Lite-server selection lives in the dedicated Network tab now.
|
||||||
Type().textColored(TypeStyle::Body2, OnSurfaceMedium(),
|
Type().textColored(TypeStyle::Body2, OnSurfaceMedium(),
|
||||||
"Lite servers are managed in the Network tab.");
|
TR("lite_servers_network_tab"));
|
||||||
|
|
||||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
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;
|
s_settingsState.lite_lifecycle_expanded = !s_settingsState.lite_lifecycle_expanded;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (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::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
||||||
ImGui::AlignTextToFramePadding();
|
ImGui::AlignTextToFramePadding();
|
||||||
ImGui::TextUnformatted("Action");
|
ImGui::TextUnformatted(TR("lite_action"));
|
||||||
ImGui::SameLine(leftX - sectionOrigin.x + liteLabelW);
|
ImGui::SameLine(leftX - sectionOrigin.x + liteLabelW);
|
||||||
ImGui::SetNextItemWidth(liteInputW);
|
ImGui::SetNextItemWidth(liteInputW);
|
||||||
if (ImGui::BeginCombo("##LiteLifecycleOperation",
|
if (ImGui::BeginCombo("##LiteLifecycleOperation",
|
||||||
@@ -1654,7 +1537,7 @@ void RenderSettingsPage(App* app) {
|
|||||||
s_settingsState.lite_lifecycle_operation == 2) {
|
s_settingsState.lite_lifecycle_operation == 2) {
|
||||||
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
||||||
ImGui::AlignTextToFramePadding();
|
ImGui::AlignTextToFramePadding();
|
||||||
ImGui::TextUnformatted("Wallet");
|
ImGui::TextUnformatted(TR("lite_wallet_label"));
|
||||||
ImGui::SameLine(leftX - sectionOrigin.x + liteLabelW);
|
ImGui::SameLine(leftX - sectionOrigin.x + liteLabelW);
|
||||||
ImGui::SetNextItemWidth(liteInputW);
|
ImGui::SetNextItemWidth(liteInputW);
|
||||||
ImGui::InputText("##LiteWalletPath", s_settingsState.lite_wallet_path,
|
ImGui::InputText("##LiteWalletPath", s_settingsState.lite_wallet_path,
|
||||||
@@ -1664,7 +1547,7 @@ void RenderSettingsPage(App* app) {
|
|||||||
if (s_settingsState.lite_lifecycle_operation == 2) {
|
if (s_settingsState.lite_lifecycle_operation == 2) {
|
||||||
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
||||||
ImGui::AlignTextToFramePadding();
|
ImGui::AlignTextToFramePadding();
|
||||||
ImGui::TextUnformatted("Seed");
|
ImGui::TextUnformatted(TR("lite_seed_label"));
|
||||||
ImGui::SameLine(leftX - sectionOrigin.x + liteLabelW);
|
ImGui::SameLine(leftX - sectionOrigin.x + liteLabelW);
|
||||||
ImGui::SetNextItemWidth(liteInputW);
|
ImGui::SetNextItemWidth(liteInputW);
|
||||||
ImGui::InputText("##LiteRestoreSeed", s_settingsState.lite_restore_seed,
|
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);
|
const int wc = liteSeedWordCount(s_settingsState.lite_restore_seed);
|
||||||
char wbuf[32];
|
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));
|
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
||||||
Type().textColored(TypeStyle::Caption, wc == 24 ? Success() : Warning(), wbuf);
|
Type().textColored(TypeStyle::Caption, wc == 24 ? Success() : Warning(), wbuf);
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
||||||
ImGui::AlignTextToFramePadding();
|
ImGui::AlignTextToFramePadding();
|
||||||
ImGui::TextUnformatted("Birthday");
|
ImGui::TextUnformatted(TR("lite_birthday_label"));
|
||||||
ImGui::SameLine(leftX - sectionOrigin.x + liteLabelW);
|
ImGui::SameLine(leftX - sectionOrigin.x + liteLabelW);
|
||||||
ImGui::SetNextItemWidth(std::min(160.0f, liteInputW));
|
ImGui::SetNextItemWidth(std::min(160.0f, liteInputW));
|
||||||
ImGui::InputInt("##LiteRestoreBirthday", &s_settingsState.lite_restore_birthday);
|
ImGui::InputInt("##LiteRestoreBirthday", &s_settingsState.lite_restore_birthday);
|
||||||
if (s_settingsState.lite_restore_birthday < 0) s_settingsState.lite_restore_birthday = 0;
|
if (s_settingsState.lite_restore_birthday < 0) s_settingsState.lite_restore_birthday = 0;
|
||||||
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
||||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(),
|
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::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
||||||
ImGui::AlignTextToFramePadding();
|
ImGui::AlignTextToFramePadding();
|
||||||
ImGui::TextUnformatted("Account");
|
ImGui::TextUnformatted(TR("lite_account_label"));
|
||||||
ImGui::SameLine(leftX - sectionOrigin.x + liteLabelW);
|
ImGui::SameLine(leftX - sectionOrigin.x + liteLabelW);
|
||||||
ImGui::SetNextItemWidth(std::min(160.0f, liteInputW));
|
ImGui::SetNextItemWidth(std::min(160.0f, liteInputW));
|
||||||
ImGui::InputInt("##LiteRestoreAccount", &s_settingsState.lite_restore_account);
|
ImGui::InputInt("##LiteRestoreAccount", &s_settingsState.lite_restore_account);
|
||||||
if (s_settingsState.lite_restore_account < 0) s_settingsState.lite_restore_account = 0;
|
if (s_settingsState.lite_restore_account < 0) s_settingsState.lite_restore_account = 0;
|
||||||
|
|
||||||
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
||||||
ImGui::Checkbox("Overwrite##LiteRestoreOverwrite",
|
ImGui::Checkbox(TrId("lite_overwrite", "LiteRestoreOverwrite").c_str(),
|
||||||
&s_settingsState.lite_restore_overwrite);
|
&s_settingsState.lite_restore_overwrite);
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
||||||
ImGui::AlignTextToFramePadding();
|
ImGui::AlignTextToFramePadding();
|
||||||
ImGui::TextUnformatted("Passphrase");
|
ImGui::TextUnformatted(TR("lite_passphrase_label"));
|
||||||
ImGui::SameLine(leftX - sectionOrigin.x + liteLabelW);
|
ImGui::SameLine(leftX - sectionOrigin.x + liteLabelW);
|
||||||
ImGui::SetNextItemWidth(liteInputW);
|
ImGui::SetNextItemWidth(liteInputW);
|
||||||
ImGui::InputText("##LiteLifecyclePassphrase",
|
ImGui::InputText("##LiteLifecyclePassphrase",
|
||||||
@@ -1723,7 +1606,7 @@ void RenderSettingsPage(App* app) {
|
|||||||
const auto& result = lite->lastLifecycleResult();
|
const auto& result = lite->lastLifecycleResult();
|
||||||
s_settingsState.lite_lifecycle_summary = result.bridgeResponseRedacted;
|
s_settingsState.lite_lifecycle_summary = result.bridgeResponseRedacted;
|
||||||
if (result.walletReady) {
|
if (result.walletReady) {
|
||||||
s_settingsState.lite_lifecycle_status = "Wallet ready";
|
s_settingsState.lite_lifecycle_status = TR("lite_wallet_ready");
|
||||||
} else {
|
} else {
|
||||||
s_settingsState.lite_lifecycle_status = result.error.empty()
|
s_settingsState.lite_lifecycle_status = result.error.empty()
|
||||||
? result.status.message
|
? result.status.message
|
||||||
@@ -1739,7 +1622,7 @@ void RenderSettingsPage(App* app) {
|
|||||||
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
||||||
const bool liteLifecycleBusy = s_settingsState.lite_lifecycle_pending;
|
const bool liteLifecycleBusy = s_settingsState.lite_lifecycle_pending;
|
||||||
if (liteLifecycleBusy) ImGui::BeginDisabled();
|
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);
|
evaluateLiteLifecycleRequestFromPageState(app);
|
||||||
}
|
}
|
||||||
if (liteLifecycleBusy) ImGui::EndDisabled();
|
if (liteLifecycleBusy) ImGui::EndDisabled();
|
||||||
@@ -1769,17 +1652,15 @@ void RenderSettingsPage(App* app) {
|
|||||||
if (app->liteWallet() && app->liteWallet()->walletOpen()) {
|
if (app->liteWallet() && app->liteWallet()->walletOpen()) {
|
||||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||||
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
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));
|
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();
|
auto r = app->liteWallet()->exportSeed();
|
||||||
wallet::secureWipeLiteSecret(s_settingsState.lite_export_secret);
|
wallet::secureWipeLiteSecret(s_settingsState.lite_export_secret);
|
||||||
if (r.ok) {
|
if (r.ok) {
|
||||||
s_settingsState.lite_export_secret = r.seedPhrase;
|
s_settingsState.lite_export_secret = r.seedPhrase;
|
||||||
s_settingsState.lite_export_label =
|
s_settingsState.lite_export_label = TR("lite_seed_warning");
|
||||||
"Seed phrase — the ONLY way to restore your wallet. "
|
|
||||||
"Write it down, store it offline, never share it.";
|
|
||||||
s_settingsState.lite_export_is_seed = true;
|
s_settingsState.lite_export_is_seed = true;
|
||||||
s_settingsState.lite_export_birthday = r.birthday;
|
s_settingsState.lite_export_birthday = r.birthday;
|
||||||
s_settingsState.lite_backup_status.clear();
|
s_settingsState.lite_backup_status.clear();
|
||||||
@@ -1791,13 +1672,13 @@ void RenderSettingsPage(App* app) {
|
|||||||
wallet::secureWipeLiteSecret(r.seedPhrase); // wipe the result copy
|
wallet::secureWipeLiteSecret(r.seedPhrase); // wipe the result copy
|
||||||
}
|
}
|
||||||
ImGui::SameLine(0, Layout::spacingSm());
|
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();
|
auto r = app->liteWallet()->exportPrivateKeys();
|
||||||
wallet::secureWipeLiteSecret(s_settingsState.lite_export_secret);
|
wallet::secureWipeLiteSecret(s_settingsState.lite_export_secret);
|
||||||
s_settingsState.lite_export_is_seed = false; // keys, not a seed
|
s_settingsState.lite_export_is_seed = false; // keys, not a seed
|
||||||
if (r.ok) {
|
if (r.ok) {
|
||||||
s_settingsState.lite_export_secret = r.privateKeysJson;
|
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();
|
s_settingsState.lite_backup_status.clear();
|
||||||
} else {
|
} else {
|
||||||
s_settingsState.lite_export_label.clear();
|
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.
|
// The seed's birthday is needed to restore quickly — show + back it up too.
|
||||||
if (s_settingsState.lite_export_is_seed) {
|
if (s_settingsState.lite_export_is_seed) {
|
||||||
char bday[64];
|
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);
|
s_settingsState.lite_export_birthday);
|
||||||
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
||||||
Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), bday);
|
Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), bday);
|
||||||
}
|
}
|
||||||
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
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);
|
app->copySecretToClipboard(s_settingsState.lite_export_secret);
|
||||||
}
|
}
|
||||||
ImGui::SameLine(0, Layout::spacingSm());
|
ImGui::SameLine(0, Layout::spacingSm());
|
||||||
// Save the seed (+ birthday) to an owner-only (0600) file in the config dir.
|
// Save the seed (+ birthday) to an owner-only (0600) file in the config dir.
|
||||||
if (s_settingsState.lite_export_is_seed &&
|
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 path = util::Platform::getConfigDir() + "/lite-seed-backup.txt";
|
||||||
std::string content = s_settingsState.lite_export_secret + "\nBirthday: " +
|
std::string content = s_settingsState.lite_export_secret + "\nBirthday: " +
|
||||||
std::to_string(s_settingsState.lite_export_birthday) + "\n";
|
std::to_string(s_settingsState.lite_export_birthday) + "\n";
|
||||||
@@ -1838,11 +1719,11 @@ void RenderSettingsPage(App* app) {
|
|||||||
path, content, /*restrictPermissions=*/true);
|
path, content, /*restrictPermissions=*/true);
|
||||||
wallet::secureWipeLiteSecret(content);
|
wallet::secureWipeLiteSecret(content);
|
||||||
s_settingsState.lite_backup_status = ok
|
s_settingsState.lite_backup_status = ok
|
||||||
? ("Saved (plaintext, owner-only) to " + path)
|
? (std::string(TR("lite_saved_to")) + path)
|
||||||
: ("Could not write " + path);
|
: (std::string(TR("lite_could_not_write")) + path);
|
||||||
}
|
}
|
||||||
if (s_settingsState.lite_export_is_seed) ImGui::SameLine(0, Layout::spacingSm());
|
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);
|
wallet::secureWipeLiteSecret(s_settingsState.lite_export_secret);
|
||||||
s_settingsState.lite_export_label.clear();
|
s_settingsState.lite_export_label.clear();
|
||||||
s_settingsState.lite_export_is_seed = false;
|
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).
|
// Import a spending/viewing key (history appears after the next sync).
|
||||||
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
||||||
ImGui::AlignTextToFramePadding();
|
ImGui::AlignTextToFramePadding();
|
||||||
ImGui::TextUnformatted("Import key");
|
ImGui::TextUnformatted(TR("lite_import_key_label"));
|
||||||
ImGui::SameLine(leftX - sectionOrigin.x + liteLabelW);
|
ImGui::SameLine(leftX - sectionOrigin.x + liteLabelW);
|
||||||
ImGui::SetNextItemWidth(liteInputW);
|
ImGui::SetNextItemWidth(liteInputW);
|
||||||
ImGui::InputText("##LiteImportKey", s_settingsState.lite_import_key,
|
ImGui::InputText("##LiteImportKey", s_settingsState.lite_import_key,
|
||||||
sizeof(s_settingsState.lite_import_key),
|
sizeof(s_settingsState.lite_import_key),
|
||||||
ImGuiInputTextFlags_Password);
|
ImGuiInputTextFlags_Password);
|
||||||
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
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);
|
const auto r = app->liteWallet()->importKey(s_settingsState.lite_import_key);
|
||||||
sodium_memzero(s_settingsState.lite_import_key, sizeof(s_settingsState.lite_import_key));
|
sodium_memzero(s_settingsState.lite_import_key, sizeof(s_settingsState.lite_import_key));
|
||||||
s_settingsState.lite_backup_status =
|
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()) {
|
if (!s_settingsState.lite_backup_status.empty()) {
|
||||||
@@ -1875,57 +1756,57 @@ void RenderSettingsPage(App* app) {
|
|||||||
// ---- Security: passphrase encryption (encrypt / unlock / lock / decrypt) ----
|
// ---- Security: passphrase encryption (encrypt / unlock / lock / decrypt) ----
|
||||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||||
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
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 auto& wstate = app->getWalletState();
|
||||||
const float encLabelX = leftX - sectionOrigin.x + liteLabelW;
|
const float encLabelX = leftX - sectionOrigin.x + liteLabelW;
|
||||||
if (!wstate.isEncrypted()) {
|
if (!wstate.isEncrypted()) {
|
||||||
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
||||||
ImGui::AlignTextToFramePadding();
|
ImGui::AlignTextToFramePadding();
|
||||||
ImGui::TextUnformatted("Passphrase");
|
ImGui::TextUnformatted(TR("lite_passphrase_label"));
|
||||||
ImGui::SameLine(encLabelX);
|
ImGui::SameLine(encLabelX);
|
||||||
ImGui::SetNextItemWidth(liteInputW);
|
ImGui::SetNextItemWidth(liteInputW);
|
||||||
ImGui::InputText("##LiteEncryptPass", s_settingsState.lite_enc_pass,
|
ImGui::InputText("##LiteEncryptPass", s_settingsState.lite_enc_pass,
|
||||||
sizeof(s_settingsState.lite_enc_pass), ImGuiInputTextFlags_Password);
|
sizeof(s_settingsState.lite_enc_pass), ImGuiInputTextFlags_Password);
|
||||||
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
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);
|
const auto r = app->liteWallet()->encryptWallet(s_settingsState.lite_enc_pass);
|
||||||
sodium_memzero(s_settingsState.lite_enc_pass, sizeof(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 {
|
} else {
|
||||||
if (wstate.isLocked()) {
|
if (wstate.isLocked()) {
|
||||||
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
||||||
ImGui::AlignTextToFramePadding();
|
ImGui::AlignTextToFramePadding();
|
||||||
ImGui::TextUnformatted("Unlock");
|
ImGui::TextUnformatted(TR("lite_unlock"));
|
||||||
ImGui::SameLine(encLabelX);
|
ImGui::SameLine(encLabelX);
|
||||||
ImGui::SetNextItemWidth(liteInputW);
|
ImGui::SetNextItemWidth(liteInputW);
|
||||||
ImGui::InputText("##LiteUnlockPass", s_settingsState.lite_enc_pass,
|
ImGui::InputText("##LiteUnlockPass", s_settingsState.lite_enc_pass,
|
||||||
sizeof(s_settingsState.lite_enc_pass), ImGuiInputTextFlags_Password);
|
sizeof(s_settingsState.lite_enc_pass), ImGuiInputTextFlags_Password);
|
||||||
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
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);
|
const bool ok = app->liteWallet()->unlockWallet(s_settingsState.lite_enc_pass);
|
||||||
sodium_memzero(s_settingsState.lite_enc_pass, sizeof(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 {
|
} else {
|
||||||
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
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 =
|
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::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
||||||
ImGui::AlignTextToFramePadding();
|
ImGui::AlignTextToFramePadding();
|
||||||
ImGui::TextUnformatted("Passphrase");
|
ImGui::TextUnformatted(TR("lite_passphrase_label"));
|
||||||
ImGui::SameLine(encLabelX);
|
ImGui::SameLine(encLabelX);
|
||||||
ImGui::SetNextItemWidth(liteInputW);
|
ImGui::SetNextItemWidth(liteInputW);
|
||||||
ImGui::InputText("##LiteDecryptPass", s_settingsState.lite_dec_pass,
|
ImGui::InputText("##LiteDecryptPass", s_settingsState.lite_dec_pass,
|
||||||
sizeof(s_settingsState.lite_dec_pass), ImGuiInputTextFlags_Password);
|
sizeof(s_settingsState.lite_dec_pass), ImGuiInputTextFlags_Password);
|
||||||
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
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);
|
const auto r = app->liteWallet()->decryptWallet(s_settingsState.lite_dec_pass);
|
||||||
sodium_memzero(s_settingsState.lite_dec_pass, sizeof(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()) {
|
if (!s_settingsState.lite_encryption_status.empty()) {
|
||||||
@@ -2351,17 +2232,6 @@ void RenderSettingsPage(App* app) {
|
|||||||
ImGui::SetCursorScreenPos(ImVec2(sectionOrigin.x, ImGui::GetCursorScreenPos().y));
|
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));
|
ImGui::Dummy(ImVec2(0, gap));
|
||||||
@@ -2373,11 +2243,7 @@ void RenderSettingsPage(App* app) {
|
|||||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("explorer_section"));
|
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("explorer_section"));
|
||||||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||||
|
|
||||||
ImVec2 cardMin = ImGui::GetCursorScreenPos();
|
material::GlassCardScope card(dl, availWidth, pad, bottomPad, glassSpec);
|
||||||
dl->ChannelsSplit(2);
|
|
||||||
dl->ChannelsSetCurrent(1);
|
|
||||||
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMin.y + pad));
|
|
||||||
ImGui::Indent(pad);
|
|
||||||
|
|
||||||
float contentW = availWidth - pad * 2;
|
float contentW = availWidth - pad * 2;
|
||||||
ImGui::PushFont(body2);
|
ImGui::PushFont(body2);
|
||||||
@@ -2417,17 +2283,6 @@ void RenderSettingsPage(App* app) {
|
|||||||
util::Platform::openUrl("https://explorer.dragonx.is");
|
util::Platform::openUrl("https://explorer.dragonx.is");
|
||||||
}
|
}
|
||||||
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_block_explorer"));
|
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));
|
ImGui::Dummy(ImVec2(0, gap));
|
||||||
@@ -2570,7 +2425,7 @@ void RenderSettingsPage(App* app) {
|
|||||||
ImGui::Dummy(ImVec2(0, gap));
|
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)
|
// daemon debug= categories written to DRAGONX.conf; lite has no daemon)
|
||||||
// ====================================================================
|
// ====================================================================
|
||||||
if (app->supportsFullNodeLifecycleActions()) {
|
if (app->supportsFullNodeLifecycleActions()) {
|
||||||
@@ -2601,11 +2456,19 @@ void RenderSettingsPage(App* app) {
|
|||||||
if (s_settingsState.debug_expanded) {
|
if (s_settingsState.debug_expanded) {
|
||||||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||||
|
|
||||||
ImVec2 cardMin = ImGui::GetCursorScreenPos();
|
material::GlassCardScope card(dl, availWidth, pad, bottomPad, glassSpec);
|
||||||
dl->ChannelsSplit(2);
|
|
||||||
dl->ChannelsSetCurrent(1);
|
// Screenshot sweep — capture every theme x every tab (above the daemon debug categories).
|
||||||
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMin.y + pad));
|
ImGui::TextWrapped("%s", TR("screenshot_sweep_desc"));
|
||||||
ImGui::Indent(pad);
|
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.5f), "%s", TR("settings_debug_select"));
|
||||||
ImGui::TextColored(ImVec4(1,1,1,0.35f), "%s", TR("settings_debug_restart_note"));
|
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"));
|
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,11 +2595,7 @@ void RenderSettingsPage(App* app) {
|
|||||||
// Confirmation dialog for clearing z-tx history
|
// Confirmation dialog for clearing z-tx history
|
||||||
if (s_settingsState.confirm_clear_ztx) {
|
if (s_settingsState.confirm_clear_ztx) {
|
||||||
if (BeginOverlayDialog(TR("confirm_clear_ztx_title"), &s_settingsState.confirm_clear_ztx, 480.0f, 0.94f)) {
|
if (BeginOverlayDialog(TR("confirm_clear_ztx_title"), &s_settingsState.confirm_clear_ztx, 480.0f, 0.94f)) {
|
||||||
ImGui::PushFont(Type().iconLarge());
|
material::DialogWarningHeader(TR("warning"), ImVec4(1.0f, 0.6f, 0.0f, 1.0f));
|
||||||
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"));
|
|
||||||
|
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
ImGui::TextWrapped("%s", TR("confirm_clear_ztx_warning1"));
|
ImGui::TextWrapped("%s", TR("confirm_clear_ztx_warning1"));
|
||||||
@@ -2757,14 +2605,13 @@ void RenderSettingsPage(App* app) {
|
|||||||
ImGui::Separator();
|
ImGui::Separator();
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
|
|
||||||
float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
|
bool doCancel = false, doConfirm = false;
|
||||||
if (ImGui::Button(TR("cancel"), ImVec2(btnW, 40))) {
|
material::DialogConfirmFooter(TR("cancel"), TrId("clear_anyway", "clear_ztx_btn").c_str(),
|
||||||
|
true, doCancel, doConfirm);
|
||||||
|
if (doCancel) {
|
||||||
s_settingsState.confirm_clear_ztx = false;
|
s_settingsState.confirm_clear_ztx = false;
|
||||||
}
|
}
|
||||||
ImGui::SameLine();
|
if (doConfirm) {
|
||||||
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))) {
|
|
||||||
std::string ztx_file = util::Platform::getDragonXDataDir() + "ztx_history.json";
|
std::string ztx_file = util::Platform::getDragonXDataDir() + "ztx_history.json";
|
||||||
if (util::Platform::deleteFile(ztx_file)) {
|
if (util::Platform::deleteFile(ztx_file)) {
|
||||||
Notifications::instance().success("Z-transaction history cleared");
|
Notifications::instance().success("Z-transaction history cleared");
|
||||||
@@ -2773,7 +2620,6 @@ void RenderSettingsPage(App* app) {
|
|||||||
}
|
}
|
||||||
s_settingsState.confirm_clear_ztx = false;
|
s_settingsState.confirm_clear_ztx = false;
|
||||||
}
|
}
|
||||||
ImGui::PopStyleColor(2);
|
|
||||||
EndOverlayDialog();
|
EndOverlayDialog();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2781,11 +2627,7 @@ void RenderSettingsPage(App* app) {
|
|||||||
// Confirmation dialog for deleting blockchain data
|
// Confirmation dialog for deleting blockchain data
|
||||||
if (s_settingsState.confirm_delete_blockchain) {
|
if (s_settingsState.confirm_delete_blockchain) {
|
||||||
if (BeginOverlayDialog(TR("confirm_delete_blockchain_title"), &s_settingsState.confirm_delete_blockchain, 500.0f, 0.94f)) {
|
if (BeginOverlayDialog(TR("confirm_delete_blockchain_title"), &s_settingsState.confirm_delete_blockchain, 500.0f, 0.94f)) {
|
||||||
ImGui::PushFont(Type().iconLarge());
|
material::DialogWarningHeader(TR("warning"), ImVec4(1.0f, 0.3f, 0.3f, 1.0f));
|
||||||
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"));
|
|
||||||
|
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
ImGui::TextWrapped("%s", TR("confirm_delete_blockchain_msg"));
|
ImGui::TextWrapped("%s", TR("confirm_delete_blockchain_msg"));
|
||||||
@@ -2795,19 +2637,17 @@ void RenderSettingsPage(App* app) {
|
|||||||
ImGui::Separator();
|
ImGui::Separator();
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
|
|
||||||
float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
|
bool doCancel = false, doConfirm = false;
|
||||||
if (ImGui::Button(TR("cancel"), ImVec2(btnW, 40))) {
|
material::DialogConfirmFooter(TR("cancel"), TrId("delete_blockchain_confirm", "del_bc_btn").c_str(),
|
||||||
|
true, doCancel, doConfirm);
|
||||||
|
if (doCancel) {
|
||||||
s_settingsState.confirm_delete_blockchain = false;
|
s_settingsState.confirm_delete_blockchain = false;
|
||||||
}
|
}
|
||||||
ImGui::SameLine();
|
if (doConfirm) {
|
||||||
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 (app->supportsFullNodeLifecycleActions())
|
if (app->supportsFullNodeLifecycleActions())
|
||||||
app->deleteBlockchainData();
|
app->deleteBlockchainData();
|
||||||
s_settingsState.confirm_delete_blockchain = false;
|
s_settingsState.confirm_delete_blockchain = false;
|
||||||
}
|
}
|
||||||
ImGui::PopStyleColor(2);
|
|
||||||
EndOverlayDialog();
|
EndOverlayDialog();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2829,11 +2669,7 @@ void RenderSettingsPage(App* app) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (BeginOverlayDialog(TR("confirm_rescan_title"), &s_settingsState.confirm_rescan, 500.0f, 0.94f)) {
|
if (BeginOverlayDialog(TR("confirm_rescan_title"), &s_settingsState.confirm_rescan, 500.0f, 0.94f)) {
|
||||||
ImGui::PushFont(Type().iconLarge());
|
material::DialogWarningHeader(TR("warning"), ImVec4(1.0f, 0.8f, 0.2f, 1.0f));
|
||||||
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), ICON_MD_WARNING);
|
|
||||||
ImGui::PopFont();
|
|
||||||
ImGui::SameLine();
|
|
||||||
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), "%s", TR("warning"));
|
|
||||||
|
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
|
|
||||||
@@ -2860,12 +2696,12 @@ void RenderSettingsPage(App* app) {
|
|||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
|
|
||||||
float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
|
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;
|
s_settingsState.confirm_rescan = false;
|
||||||
}
|
}
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
ImGui::BeginDisabled(detecting);
|
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) {
|
if (bootstrapped) {
|
||||||
app->runtimeRescan(s_settingsState.rescan_start_height);
|
app->runtimeRescan(s_settingsState.rescan_start_height);
|
||||||
} else {
|
} else {
|
||||||
@@ -2881,11 +2717,7 @@ void RenderSettingsPage(App* app) {
|
|||||||
// Confirm: repair wallet (-zapwallettxes=2 — wipe & rebuild wallet tx records, then rescan)
|
// Confirm: repair wallet (-zapwallettxes=2 — wipe & rebuild wallet tx records, then rescan)
|
||||||
if (s_settingsState.confirm_repair_wallet) {
|
if (s_settingsState.confirm_repair_wallet) {
|
||||||
if (BeginOverlayDialog(TR("confirm_repair_wallet_title"), &s_settingsState.confirm_repair_wallet, 500.0f, 0.94f)) {
|
if (BeginOverlayDialog(TR("confirm_repair_wallet_title"), &s_settingsState.confirm_repair_wallet, 500.0f, 0.94f)) {
|
||||||
ImGui::PushFont(Type().iconLarge());
|
material::DialogWarningHeader(TR("warning"), ImVec4(1.0f, 0.8f, 0.2f, 1.0f));
|
||||||
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), ICON_MD_WARNING);
|
|
||||||
ImGui::PopFont();
|
|
||||||
ImGui::SameLine();
|
|
||||||
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), "%s", TR("warning"));
|
|
||||||
|
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
ImGui::TextWrapped("%s", TR("confirm_repair_wallet_msg"));
|
ImGui::TextWrapped("%s", TR("confirm_repair_wallet_msg"));
|
||||||
@@ -2895,12 +2727,14 @@ void RenderSettingsPage(App* app) {
|
|||||||
ImGui::Separator();
|
ImGui::Separator();
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
|
|
||||||
float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
|
bool doCancel = false, doConfirm = false;
|
||||||
if (ImGui::Button(TrId("cancel", "repair_wallet_cancel").c_str(), ImVec2(btnW, 40))) {
|
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;
|
s_settingsState.confirm_repair_wallet = false;
|
||||||
}
|
}
|
||||||
ImGui::SameLine();
|
if (doConfirm) {
|
||||||
if (ImGui::Button(TrId("repair_wallet", "repair_wallet_confirm").c_str(), ImVec2(btnW, 40))) {
|
|
||||||
app->repairWallet();
|
app->repairWallet();
|
||||||
s_settingsState.confirm_repair_wallet = false;
|
s_settingsState.confirm_repair_wallet = false;
|
||||||
}
|
}
|
||||||
@@ -2911,11 +2745,7 @@ void RenderSettingsPage(App* app) {
|
|||||||
// Confirm: reinstall the bundled daemon binary (stop → overwrite → restart)
|
// Confirm: reinstall the bundled daemon binary (stop → overwrite → restart)
|
||||||
if (s_settingsState.confirm_reinstall_daemon) {
|
if (s_settingsState.confirm_reinstall_daemon) {
|
||||||
if (BeginOverlayDialog(TR("confirm_reinstall_daemon_title"), &s_settingsState.confirm_reinstall_daemon, 500.0f, 0.94f)) {
|
if (BeginOverlayDialog(TR("confirm_reinstall_daemon_title"), &s_settingsState.confirm_reinstall_daemon, 500.0f, 0.94f)) {
|
||||||
ImGui::PushFont(Type().iconLarge());
|
material::DialogWarningHeader(TR("warning"), ImVec4(1.0f, 0.8f, 0.2f, 1.0f));
|
||||||
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), ICON_MD_WARNING);
|
|
||||||
ImGui::PopFont();
|
|
||||||
ImGui::SameLine();
|
|
||||||
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), "%s", TR("warning"));
|
|
||||||
|
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
ImGui::TextWrapped("%s", TR("confirm_reinstall_daemon_msg"));
|
ImGui::TextWrapped("%s", TR("confirm_reinstall_daemon_msg"));
|
||||||
@@ -2925,12 +2755,14 @@ void RenderSettingsPage(App* app) {
|
|||||||
ImGui::Separator();
|
ImGui::Separator();
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
|
|
||||||
float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
|
bool doCancel = false, doConfirm = false;
|
||||||
if (ImGui::Button(TrId("cancel", "reinstall_daemon_cancel").c_str(), ImVec2(btnW, 40))) {
|
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;
|
s_settingsState.confirm_reinstall_daemon = false;
|
||||||
}
|
}
|
||||||
ImGui::SameLine();
|
if (doConfirm) {
|
||||||
if (ImGui::Button(TrId("daemon_install_bundled", "reinstall_daemon_confirm").c_str(), ImVec2(btnW, 40))) {
|
|
||||||
app->reinstallBundledDaemon();
|
app->reinstallBundledDaemon();
|
||||||
s_settingsState.daemon_info_loaded = false; // refresh the panel after the swap
|
s_settingsState.daemon_info_loaded = false; // refresh the panel after the swap
|
||||||
s_settingsState.confirm_reinstall_daemon = false;
|
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)
|
// Confirm: restart daemon (briefly drops the connection to apply changed options)
|
||||||
if (s_settingsState.confirm_restart_daemon) {
|
if (s_settingsState.confirm_restart_daemon) {
|
||||||
if (BeginOverlayDialog(TR("confirm_restart_daemon_title"), &s_settingsState.confirm_restart_daemon, 500.0f, 0.94f)) {
|
if (BeginOverlayDialog(TR("confirm_restart_daemon_title"), &s_settingsState.confirm_restart_daemon, 500.0f, 0.94f)) {
|
||||||
ImGui::PushFont(Type().iconLarge());
|
material::DialogWarningHeader(TR("warning"), ImVec4(1.0f, 0.8f, 0.2f, 1.0f));
|
||||||
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), ICON_MD_WARNING);
|
|
||||||
ImGui::PopFont();
|
|
||||||
ImGui::SameLine();
|
|
||||||
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), "%s", TR("warning"));
|
|
||||||
|
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
ImGui::TextWrapped("%s", TR("confirm_restart_daemon_msg"));
|
ImGui::TextWrapped("%s", TR("confirm_restart_daemon_msg"));
|
||||||
@@ -2954,12 +2782,14 @@ void RenderSettingsPage(App* app) {
|
|||||||
ImGui::Separator();
|
ImGui::Separator();
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
|
|
||||||
float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
|
bool doCancel = false, doConfirm = false;
|
||||||
if (ImGui::Button(TrId("cancel", "restartd_cancel").c_str(), ImVec2(btnW, 40))) {
|
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;
|
s_settingsState.confirm_restart_daemon = false;
|
||||||
}
|
}
|
||||||
ImGui::SameLine();
|
if (doConfirm) {
|
||||||
if (ImGui::Button(TrId("settings_restart_daemon", "restartd_confirm").c_str(), ImVec2(btnW, 40))) {
|
|
||||||
s_settingsState.debug_cats_dirty = false;
|
s_settingsState.debug_cats_dirty = false;
|
||||||
app->restartDaemon();
|
app->restartDaemon();
|
||||||
s_settingsState.confirm_restart_daemon = false;
|
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)
|
// Confirm: lite wallet re-download blocks (rescan from the lite server — long but safe)
|
||||||
if (s_settingsState.confirm_lite_redownload) {
|
if (s_settingsState.confirm_lite_redownload) {
|
||||||
if (BeginOverlayDialog(TR("confirm_lite_redownload_title"), &s_settingsState.confirm_lite_redownload, 500.0f, 0.94f)) {
|
if (BeginOverlayDialog(TR("confirm_lite_redownload_title"), &s_settingsState.confirm_lite_redownload, 500.0f, 0.94f)) {
|
||||||
ImGui::PushFont(Type().iconLarge());
|
material::DialogWarningHeader(TR("warning"), ImVec4(1.0f, 0.8f, 0.2f, 1.0f));
|
||||||
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), ICON_MD_WARNING);
|
|
||||||
ImGui::PopFont();
|
|
||||||
ImGui::SameLine();
|
|
||||||
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), "%s", TR("warning"));
|
|
||||||
|
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
ImGui::TextWrapped("%s", TR("confirm_lite_redownload_msg"));
|
ImGui::TextWrapped("%s", TR("confirm_lite_redownload_msg"));
|
||||||
@@ -2985,12 +2811,14 @@ void RenderSettingsPage(App* app) {
|
|||||||
ImGui::Separator();
|
ImGui::Separator();
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
|
|
||||||
float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
|
bool doCancel = false, doConfirm = false;
|
||||||
if (ImGui::Button(TrId("cancel", "lite_redl_cancel").c_str(), ImVec2(btnW, 40))) {
|
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;
|
s_settingsState.confirm_lite_redownload = false;
|
||||||
}
|
}
|
||||||
ImGui::SameLine();
|
if (doConfirm) {
|
||||||
if (ImGui::Button(TrId("lite_redownload_blocks", "lite_redl_confirm").c_str(), ImVec2(btnW, 40))) {
|
|
||||||
if (auto* lite = app->liteWallet()) lite->startRescan();
|
if (auto* lite = app->liteWallet()) lite->startRescan();
|
||||||
s_settingsState.confirm_lite_redownload = false;
|
s_settingsState.confirm_lite_redownload = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
#include "../../app.h"
|
#include "../../app.h"
|
||||||
#include "../../data/address_book.h"
|
#include "../../data/address_book.h"
|
||||||
#include "../../util/i18n.h"
|
#include "../../util/i18n.h"
|
||||||
|
#include "../../util/text_format.h"
|
||||||
#include "../notifications.h"
|
#include "../notifications.h"
|
||||||
#include "../schema/ui_schema.h"
|
#include "../schema/ui_schema.h"
|
||||||
#include "../material/draw_helpers.h"
|
#include "../material/draw_helpers.h"
|
||||||
@@ -100,15 +101,13 @@ void AddressBookDialog::render(App* app)
|
|||||||
|
|
||||||
if (material::BeginOverlayDialog(title, open, dialogW, 0.94f,
|
if (material::BeginOverlayDialog(title, open, dialogW, 0.94f,
|
||||||
Layout::kDialogCompactBottomRatio(), id)) {
|
Layout::kDialogCompactBottomRatio(), id)) {
|
||||||
ImGui::Text("%s", TR("label"));
|
material::LabeledInput(TR("label"), isEdit ? "##EditLabel" : "##AddLabel",
|
||||||
ImGui::SetNextItemWidth(formW);
|
s_edit_label, sizeof(s_edit_label), formW);
|
||||||
ImGui::InputText(isEdit ? "##EditLabel" : "##AddLabel", s_edit_label, sizeof(s_edit_label));
|
|
||||||
|
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
|
|
||||||
ImGui::Text("%s", TR("address_label"));
|
material::LabeledInput(TR("address_label"), isEdit ? "##EditAddress" : "##AddAddress",
|
||||||
ImGui::SetNextItemWidth(formW);
|
s_edit_address, sizeof(s_edit_address), formW);
|
||||||
ImGui::InputText(isEdit ? "##EditAddress" : "##AddAddress", s_edit_address, sizeof(s_edit_address));
|
|
||||||
if (!isEdit) {
|
if (!isEdit) {
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
if (material::StyledButton(TR("paste"), ImVec2(0,0), S.resolveFont(actionBtn.font))) {
|
if (material::StyledButton(TR("paste"), ImVec2(0,0), S.resolveFont(actionBtn.font))) {
|
||||||
@@ -119,10 +118,8 @@ void AddressBookDialog::render(App* app)
|
|||||||
|
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
|
|
||||||
ImGui::Text("%s", TR("notes_optional"));
|
material::LabeledInputMultiline(TR("notes_optional"), isEdit ? "##EditNotes" : "##AddNotes",
|
||||||
ImGui::SetNextItemWidth(formW);
|
s_edit_notes, sizeof(s_edit_notes), ImVec2(formW, notesH));
|
||||||
ImGui::InputTextMultiline(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;
|
bool canSubmit = std::strlen(s_edit_label) > 0 && std::strlen(s_edit_address) > 0;
|
||||||
float totalActionsW = actionW * 2.0f + actionGap;
|
float totalActionsW = actionW * 2.0f + actionGap;
|
||||||
@@ -253,8 +250,7 @@ void AddressBookDialog::render(App* app)
|
|||||||
std::string addr_display = entry.address;
|
std::string addr_display = entry.address;
|
||||||
int addrTruncLen = (addrTable.columns.count("address") && addrTable.columns.at("address").truncate > 0) ? addrTable.columns.at("address").truncate : 40;
|
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)) {
|
if (addr_display.length() > static_cast<size_t>(addrTruncLen)) {
|
||||||
addr_display = addr_display.substr(0, addrFrontLbl.truncate) + "..." +
|
addr_display = util::truncateMiddle(addr_display, addrFrontLbl.truncate, addrBackLbl.truncate);
|
||||||
addr_display.substr(addr_display.length() - addrBackLbl.truncate);
|
|
||||||
}
|
}
|
||||||
ImGui::TextDisabled("%s", addr_display.c_str());
|
ImGui::TextDisabled("%s", addr_display.c_str());
|
||||||
if (ImGui::IsItemHovered()) {
|
if (ImGui::IsItemHovered()) {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
#include "../../app.h"
|
#include "../../app.h"
|
||||||
#include "../../util/i18n.h"
|
#include "../../util/i18n.h"
|
||||||
|
#include "../../util/text_format.h"
|
||||||
#include "../material/draw_helpers.h"
|
#include "../material/draw_helpers.h"
|
||||||
#include "../material/project_icons.h"
|
#include "../material/project_icons.h"
|
||||||
#include "../theme.h"
|
#include "../theme.h"
|
||||||
@@ -69,7 +70,7 @@ public:
|
|||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
{
|
{
|
||||||
std::string display = s_address;
|
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());
|
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), display.c_str());
|
||||||
}
|
}
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
@@ -188,7 +189,7 @@ public:
|
|||||||
// "No icon" option
|
// "No icon" option
|
||||||
if (showClearIcon) {
|
if (showClearIcon) {
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
if (ImGui::SmallButton(TR("clear_icon"))) {
|
if (material::TactileSmallButton(TR("clear_icon"))) {
|
||||||
s_selectedIcon = -1;
|
s_selectedIcon = -1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ public:
|
|||||||
|
|
||||||
// Max button
|
// Max button
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
if (ImGui::SmallButton(TR("max"))) {
|
if (material::TactileSmallButton(TR("max"))) {
|
||||||
snprintf(s_amount, sizeof(s_amount), "%.8f",
|
snprintf(s_amount, sizeof(s_amount), "%.8f",
|
||||||
maxSendableAmount(s_info.fromBalance, s_fee));
|
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();
|
bool sharedAddrSyncing = state.sync.syncing && !state.sync.isSynced();
|
||||||
ImGui::BeginDisabled(sharedAddrSyncing);
|
ImGui::BeginDisabled(sharedAddrSyncing);
|
||||||
if (s_generating_z_address) {
|
if (s_generating_z_address) {
|
||||||
int dots = ((int)(ImGui::GetTime() * 3.0f)) % 4;
|
|
||||||
const char* dotStr[] = {"", ".", "..", "..."};
|
|
||||||
char genLabel[64];
|
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),
|
TactileButton(genLabel, ImVec2(buttonWidth, 0),
|
||||||
S.resolveFont(addrBtn.font.empty() ? "button" : addrBtn.font));
|
S.resolveFont(addrBtn.font.empty() ? "button" : addrBtn.font));
|
||||||
} else if (TactileButton(TR("new_z_address"), ImVec2(buttonWidth, 0),
|
} 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();
|
const auto& state = app->state();
|
||||||
|
|
||||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "RECENT TRANSACTIONS");
|
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("recent_transactions"));
|
||||||
ImGui::SameLine();
|
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);
|
app->setCurrentPage(NavPage::History);
|
||||||
}
|
}
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
@@ -791,7 +789,7 @@ void RenderSharedRecentTx(App* app, float recentH, float availW, float hs, float
|
|||||||
int count = (int)txs.size();
|
int count = (int)txs.size();
|
||||||
|
|
||||||
if (count == 0) {
|
if (count == 0) {
|
||||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), "No transactions yet");
|
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("no_transactions_yet"));
|
||||||
} else {
|
} else {
|
||||||
ImDrawList* dl = ImGui::GetWindowDrawList();
|
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||||
ImFont* capFont = Type().caption();
|
ImFont* capFont = Type().caption();
|
||||||
|
|||||||
@@ -10,13 +10,6 @@ namespace dragonx {
|
|||||||
namespace ui {
|
namespace ui {
|
||||||
|
|
||||||
namespace {
|
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)
|
std::string formatRecentAmount(const std::string& type, double amount)
|
||||||
{
|
{
|
||||||
char buffer[32];
|
char buffer[32];
|
||||||
@@ -36,7 +29,7 @@ RecentTxDisplay buildRecentTxDisplay(const TransactionInfo& tx, int addressMaxLe
|
|||||||
{
|
{
|
||||||
return {
|
return {
|
||||||
tx.getTypeDisplay(),
|
tx.getTypeDisplay(),
|
||||||
truncateRecentAddress(tx.address, addressMaxLen),
|
util::truncateMiddle(tx.address, addressMaxLen),
|
||||||
formatRecentAmount(tx.type, tx.amount),
|
formatRecentAmount(tx.type, tx.amount),
|
||||||
recentTimeAgo(tx.timestamp)
|
recentTimeAgo(tx.timestamp)
|
||||||
};
|
};
|
||||||
|
|||||||
+16
-575
@@ -218,17 +218,11 @@ static void RenderBalanceClassic(App* app)
|
|||||||
{
|
{
|
||||||
using namespace material;
|
using namespace material;
|
||||||
const auto& S = schema::UISchema::instance();
|
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");
|
const auto syncBar = S.drawElement("tabs.balance", "sync-bar");
|
||||||
|
|
||||||
// Read layout properties from schema
|
// Read layout properties from schema
|
||||||
const float kBalanceLerpSpeed = S.drawElement("tabs.balance", "balance-lerp-speed").sizeOr(8.0f);
|
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 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();
|
const auto& state = app->state();
|
||||||
|
|
||||||
@@ -241,17 +235,6 @@ static void RenderBalanceClassic(App* app)
|
|||||||
const float cGap = Layout::cardGap();
|
const float cGap = Layout::cardGap();
|
||||||
const float dp = Layout::dpiScale();
|
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
|
// Lerp displayed balances toward actual values
|
||||||
{
|
{
|
||||||
float dt = ImGui::GetIO().DeltaTime;
|
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
|
float recentReserve = contentAvail.y * S.drawElement("tabs.balance", "recent-tx-reserve-ratio").sizeOr(0.18f);
|
||||||
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 classicAddrH = S.drawElement("tabs.balance.classic", "address-table-height").size;
|
float classicAddrH = S.drawElement("tabs.balance.classic", "address-table-height").size;
|
||||||
float addrListH;
|
float addrH = (classicAddrH >= 0.0f) ? classicAddrH * dp
|
||||||
if (classicAddrH >= 0.0f) {
|
: ImGui::GetContentRegionAvail().y - recentReserve
|
||||||
addrListH = classicAddrH * dp; // scaled for DPI + font scale
|
- Layout::spacingXl() - Type().h6()->LegacySize - Layout::spacingMd();
|
||||||
} else {
|
RenderSharedAddressList(app, addrH, contentAvail.x, glassRound, hs, vs);
|
||||||
addrListH = ImGui::GetContentRegionAvail().y - recentTxReserve;
|
RenderSharedRecentTx(app, recentReserve, contentAvail.x, hs, vs);
|
||||||
}
|
|
||||||
// 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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2189,9 +1630,9 @@ static void RenderBalanceShield(App* app) {
|
|||||||
// Label below gauge
|
// Label below gauge
|
||||||
ImFont* capFont = Type().caption();
|
ImFont* capFont = Type().caption();
|
||||||
const char* statusMsg;
|
const char* statusMsg;
|
||||||
if (privPct >= 80.0f) statusMsg = "Great privacy!";
|
if (privPct >= 80.0f) statusMsg = TR("privacy_great");
|
||||||
else if (privPct >= 50.0f) statusMsg = "Consider shielding more";
|
else if (privPct >= 50.0f) statusMsg = TR("privacy_medium");
|
||||||
else statusMsg = "Low privacy — shield funds";
|
else statusMsg = TR("privacy_low");
|
||||||
ImVec2 msgSz = capFont->CalcTextSizeA(capFont->LegacySize, 1000, 0, statusMsg);
|
ImVec2 msgSz = capFont->CalcTextSizeA(capFont->LegacySize, 1000, 0, statusMsg);
|
||||||
dl->AddText(capFont, capFont->LegacySize,
|
dl->AddText(capFont, capFont->LegacySize,
|
||||||
ImVec2(gaugeCx - msgSz.x * 0.5f, gaugeCy + 4 * dp),
|
ImVec2(gaugeCx - msgSz.x * 0.5f, gaugeCy + 4 * dp),
|
||||||
@@ -2307,7 +1748,7 @@ static void RenderBalanceTimeline(App* app) {
|
|||||||
DrawGlassPanel(dl, chartMin, chartMax, spec);
|
DrawGlassPanel(dl, chartMin, chartMax, spec);
|
||||||
|
|
||||||
ImFont* capFont = Type().caption();
|
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);
|
ImVec2 msgSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, msg);
|
||||||
dl->AddText(capFont, capFont->LegacySize,
|
dl->AddText(capFont, capFont->LegacySize,
|
||||||
ImVec2(chartMin.x + (availW - msgSz.x) * 0.5f,
|
ImVec2(chartMin.x + (availW - msgSz.x) * 0.5f,
|
||||||
@@ -2629,10 +2070,10 @@ static void RenderBalanceMinimal(App* app) {
|
|||||||
ImGui::Dummy(heroSz);
|
ImGui::Dummy(heroSz);
|
||||||
|
|
||||||
// Line 2: Shielded + Transparent
|
// 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);
|
Type().textColored(TypeStyle::Caption, Success(), buf);
|
||||||
ImGui::SameLine(0, Layout::spacingLg());
|
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);
|
Type().textColored(TypeStyle::Caption, Warning(), buf);
|
||||||
|
|
||||||
// USD value
|
// USD value
|
||||||
|
|||||||
@@ -36,9 +36,7 @@ std::string timeAgo(int64_t timestamp)
|
|||||||
|
|
||||||
std::string truncateAddress(const std::string& address, int maxLen)
|
std::string truncateAddress(const std::string& address, int maxLen)
|
||||||
{
|
{
|
||||||
if (maxLen <= 3 || address.length() <= static_cast<size_t>(maxLen)) return address;
|
return util::truncateMiddle(address, maxLen);
|
||||||
int half = (maxLen - 3) / 2;
|
|
||||||
return address.substr(0, half) + "..." + address.substr(address.length() - half);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ImU32 recentTxIconColor(const std::string& type)
|
ImU32 recentTxIconColor(const std::string& type)
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
#include "../../rpc/rpc_client.h"
|
#include "../../rpc/rpc_client.h"
|
||||||
#include "../../rpc/rpc_worker.h"
|
#include "../../rpc/rpc_worker.h"
|
||||||
#include "../../util/i18n.h"
|
#include "../../util/i18n.h"
|
||||||
|
#include "../../util/text_format.h"
|
||||||
#include "../notifications.h"
|
#include "../notifications.h"
|
||||||
#include "../schema/ui_schema.h"
|
#include "../schema/ui_schema.h"
|
||||||
#include "../material/draw_helpers.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 hashBackLbl = S.label("dialogs.block-info", "hash-back-label");
|
||||||
auto closeBtn = S.button("dialogs.block-info", "close-button");
|
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();
|
auto* rpc = app->rpc();
|
||||||
const auto& state = app->getWalletState();
|
const auto& state = app->getWalletState();
|
||||||
|
|
||||||
@@ -251,7 +257,7 @@ void BlockInfoDialog::render(App* app)
|
|||||||
// Truncate for display
|
// Truncate for display
|
||||||
std::string prev_short = s_prev_hash;
|
std::string prev_short = s_prev_hash;
|
||||||
if (prev_short.length() > static_cast<size_t>(hashLbl.truncate)) {
|
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::Text("%s", prev_short.c_str());
|
||||||
ImGui::PopStyleColor();
|
ImGui::PopStyleColor();
|
||||||
@@ -273,7 +279,7 @@ void BlockInfoDialog::render(App* app)
|
|||||||
// Truncate for display
|
// Truncate for display
|
||||||
std::string next_short = s_next_hash;
|
std::string next_short = s_next_hash;
|
||||||
if (next_short.length() > static_cast<size_t>(hashLbl.truncate)) {
|
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::Text("%s", next_short.c_str());
|
||||||
ImGui::PopStyleColor();
|
ImGui::PopStyleColor();
|
||||||
|
|||||||
@@ -53,7 +53,10 @@ public:
|
|||||||
using namespace material;
|
using namespace material;
|
||||||
const float dp = Layout::dpiScale();
|
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) {
|
if (s_state == State::Confirm) {
|
||||||
renderConfirm(dp);
|
renderConfirm(dp);
|
||||||
@@ -159,18 +162,7 @@ private:
|
|||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
|
|
||||||
// Progress bar
|
// Progress bar
|
||||||
float barH = 8.0f * dp;
|
material::ProgressBar(ImGui::GetContentRegionAvail().x, 8.0f * dp, prog.percent / 100.0f);
|
||||||
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));
|
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
|
|
||||||
// Percent + status text
|
// 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 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);
|
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;
|
// The schema console colors are authored for the dark terminal (light-on-dark); honor them only
|
||||||
COLOR_RESULT = !res.color.empty() ? S.resolveColor(res.color, defRes) : defRes;
|
// in dark themes. Light themes always use the dark-text defaults so log text stays legible on the
|
||||||
COLOR_ERROR = !err.color.empty() ? S.resolveColor(err.color, defErr) : defErr;
|
// now near-white console surface (otherwise the schema's light-blue text sat pale-on-white ~1.2:1).
|
||||||
COLOR_DAEMON = !dmn.color.empty() ? S.resolveColor(dmn.color, defDmn) : defDmn;
|
COLOR_COMMAND = (dark && !cmd.color.empty()) ? S.resolveColor(cmd.color, defCmd) : defCmd;
|
||||||
COLOR_INFO = !inf.color.empty() ? S.resolveColor(inf.color, defInf) : defInf;
|
COLOR_RESULT = (dark && !res.color.empty()) ? S.resolveColor(res.color, defRes) : defRes;
|
||||||
COLOR_RPC = !rpc.color.empty() ? S.resolveColor(rpc.color, defRpc) : defRpc;
|
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 {
|
} else {
|
||||||
// No schema — use hardcoded defaults per theme
|
// No schema — use hardcoded defaults per theme
|
||||||
COLOR_COMMAND = dark ? IM_COL32(191, 209, 229, 255) : IM_COL32(21, 101, 192, 255);
|
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::JsonKey: return WithAlpha(Secondary(), 255);
|
||||||
case ConsoleChannel::JsonString: return WithAlpha(Success(), 255);
|
case ConsoleChannel::JsonString: return WithAlpha(Success(), 255);
|
||||||
case ConsoleChannel::JsonNumber: return WithAlpha(Warning(), 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:
|
case ConsoleChannel::None:
|
||||||
default: return COLOR_RESULT;
|
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.
|
// 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));
|
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;
|
int consoleParentVtx = dlOut->VtxBuffer.Size;
|
||||||
@@ -1341,7 +1347,11 @@ void ConsoleTab::renderCommandsPopup()
|
|||||||
using namespace material;
|
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);
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1463,7 +1473,7 @@ void ConsoleTab::renderCommandsPopup()
|
|||||||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||||
|
|
||||||
// Close button
|
// Close button
|
||||||
if (ImGui::Button(TR("console_close"), ImVec2(-1, 0))) {
|
if (material::TactileButton(TR("console_close"), ImVec2(-1, 0))) {
|
||||||
cmdFilter[0] = '\0';
|
cmdFilter[0] = '\0';
|
||||||
show_commands_popup_ = false;
|
show_commands_popup_ = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,7 +62,10 @@ public:
|
|||||||
}
|
}
|
||||||
using namespace material;
|
using namespace material;
|
||||||
const float dp = Layout::dpiScale();
|
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();
|
const auto p = s_updater->getProgress();
|
||||||
using St = util::DaemonUpdater::State;
|
using St = util::DaemonUpdater::State;
|
||||||
switch (p.state) {
|
switch (p.state) {
|
||||||
@@ -186,16 +189,7 @@ private:
|
|||||||
: TR("daemon_update_installing");
|
: TR("daemon_update_installing");
|
||||||
Type().text(TypeStyle::Subtitle2, title);
|
Type().text(TypeStyle::Subtitle2, title);
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
const float barH = 8.0f * dp;
|
material::ProgressBar(fullW(), 8.0f * dp, p.percent / 100.0f);
|
||||||
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));
|
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
ImGui::Text("%s", p.status_text.c_str());
|
ImGui::Text("%s", p.status_text.c_str());
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
|
|||||||
@@ -8,6 +8,8 @@
|
|||||||
#include "../../rpc/rpc_worker.h"
|
#include "../../rpc/rpc_worker.h"
|
||||||
#include "../../data/wallet_state.h"
|
#include "../../data/wallet_state.h"
|
||||||
#include "../../util/i18n.h"
|
#include "../../util/i18n.h"
|
||||||
|
#include "../../util/text_format.h"
|
||||||
|
#include "mining_tab_helpers.h"
|
||||||
#include "../explorer/explorer_block_cache.h"
|
#include "../explorer/explorer_block_cache.h"
|
||||||
#include "../schema/ui_schema.h"
|
#include "../schema/ui_schema.h"
|
||||||
#include "../material/type.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) {
|
static std::string truncateHash(const std::string& hash, int front, int back) {
|
||||||
if (hash.length() <= static_cast<size_t>(front + back + 3))
|
return util::truncateMiddle(hash, front, back);
|
||||||
return hash;
|
|
||||||
return hash.substr(0, front) + "..." + hash.substr(hash.length() - back);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Adaptive hash truncation based on available pixel width
|
// Adaptive hash truncation based on available pixel width
|
||||||
@@ -176,21 +176,6 @@ static std::string formatSize(int bytes) {
|
|||||||
return b;
|
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
|
// 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) {
|
static void copyButton(const char* id, const std::string& text, float x, float y) {
|
||||||
ImFont* iconFont = Type().iconSmall();
|
ImFont* iconFont = Type().iconSmall();
|
||||||
@@ -615,17 +600,11 @@ static void renderChainStats(App* app, float availWidth) {
|
|||||||
|
|
||||||
float maxTextW = cardW * 0.34f;
|
float maxTextW = cardW * 0.34f;
|
||||||
statusText = truncateHashToFit(statusText, capFont, maxTextW);
|
statusText = truncateHashToFit(statusText, capFont, maxTextW);
|
||||||
ImVec2 textSz = capFont->CalcTextSizeA(capFont->LegacySize, 1000.0f, 0.0f, statusText.c_str());
|
ImVec2 pillPad(Layout::spacingSm(), Layout::spacingXs());
|
||||||
float pillPadX = Layout::spacingSm();
|
ImVec2 pillSz = material::PillSize(statusText.c_str(), capFont, pillPad);
|
||||||
float pillH = capFont->LegacySize + Layout::spacingXs() * 2.0f;
|
ImVec2 pillMin(cardMin.x + cardW - pad - pillSz.x, cardMin.y + pad * 0.5f);
|
||||||
float pillW = textSz.x + pillPadX * 2.0f;
|
material::DrawPill(dl, pillMin, statusText.c_str(), capFont, pillCol,
|
||||||
ImVec2 pillMin(cardMin.x + cardW - pad - pillW, cardMin.y + pad * 0.5f);
|
WithAlpha(pillCol, 32), WithAlpha(pillCol, 110), pillPad);
|
||||||
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());
|
|
||||||
};
|
};
|
||||||
|
|
||||||
auto drawChainTip = [&](const ImVec2& cardMin, float cardW) {
|
auto drawChainTip = [&](const ImVec2& cardMin, float cardW) {
|
||||||
@@ -698,7 +677,7 @@ static void renderChainStats(App* app, float availWidth) {
|
|||||||
|
|
||||||
MetricSpec metrics[4] = {
|
MetricSpec metrics[4] = {
|
||||||
{ ICON_MD_SPEED, TR("difficulty"), difficulty, "", Warning() },
|
{ 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_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() },
|
{ 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
|
// Show a loading modal while fetching
|
||||||
if (BeginOverlayDialog(TR("loading"), &s_detail_loading, 300.0f, 0.85f)) {
|
if (BeginOverlayDialog(TR("loading"), &s_detail_loading, 300.0f, 0.85f)) {
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
int dots = ((int)(ImGui::GetTime() * 3.0f)) % 4;
|
|
||||||
const char* dotStr[] = {"", ".", "..", "..."};
|
|
||||||
char loadBuf[64];
|
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::TextDisabled("%s", loadBuf);
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
EndOverlayDialog();
|
EndOverlayDialog();
|
||||||
@@ -1041,7 +1018,11 @@ static void renderBlockDetailModal(App* app) {
|
|||||||
return;
|
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;
|
return;
|
||||||
|
|
||||||
float contentW = ImGui::GetContentRegionAvail().x;
|
float contentW = ImGui::GetContentRegionAvail().x;
|
||||||
@@ -1062,7 +1043,7 @@ static void renderBlockDetailModal(App* app) {
|
|||||||
if (s_detail_height > 1) {
|
if (s_detail_height > 1) {
|
||||||
ImGui::PushFont(Type().iconMed());
|
ImGui::PushFont(Type().iconMed());
|
||||||
ImGui::PushID("prevBlock");
|
ImGui::PushID("prevBlock");
|
||||||
if (ImGui::SmallButton(ICON_MD_CHEVRON_LEFT)) {
|
if (material::TactileSmallButton(ICON_MD_CHEVRON_LEFT)) {
|
||||||
if (app->rpc() && app->rpc()->isConnected())
|
if (app->rpc() && app->rpc()->isConnected())
|
||||||
fetchBlockDetail(app, s_detail_height - 1);
|
fetchBlockDetail(app, s_detail_height - 1);
|
||||||
}
|
}
|
||||||
@@ -1076,7 +1057,7 @@ static void renderBlockDetailModal(App* app) {
|
|||||||
if (!s_detail_next_hash.empty()) {
|
if (!s_detail_next_hash.empty()) {
|
||||||
ImGui::PushFont(Type().iconMed());
|
ImGui::PushFont(Type().iconMed());
|
||||||
ImGui::PushID("nextBlock");
|
ImGui::PushID("nextBlock");
|
||||||
if (ImGui::SmallButton(ICON_MD_CHEVRON_RIGHT)) {
|
if (material::TactileSmallButton(ICON_MD_CHEVRON_RIGHT)) {
|
||||||
if (app->rpc() && app->rpc()->isConnected())
|
if (app->rpc() && app->rpc()->isConnected())
|
||||||
fetchBlockDetail(app, s_detail_height + 1);
|
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
|
// Show loading state inside the modal when navigating between blocks
|
||||||
if (s_detail_loading) {
|
if (s_detail_loading) {
|
||||||
int dots = ((int)(ImGui::GetTime() * 3.0f)) % 4;
|
snprintf(buf, sizeof(buf), "%s%s", TR("loading"), material::LoadingDots());
|
||||||
const char* dotStr[] = {"", ".", "..", "..."};
|
|
||||||
snprintf(buf, sizeof(buf), "%s%s", TR("loading"), dotStr[dots]);
|
|
||||||
ImGui::TextDisabled("%s", buf);
|
ImGui::TextDisabled("%s", buf);
|
||||||
EndOverlayDialog();
|
EndOverlayDialog();
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -69,7 +69,11 @@ void ExportAllKeysDialog::render(App* app)
|
|||||||
auto exportBtn = S.button("dialogs.export-all-keys", "export-button");
|
auto exportBtn = S.button("dialogs.export-all-keys", "export-button");
|
||||||
auto closeBtn = S.button("dialogs.export-all-keys", "close-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
|
// Warning
|
||||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.4f, 0.4f, 1.0f));
|
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.4f, 0.4f, 1.0f));
|
||||||
ImGui::PushFont(material::Type().iconSmall());
|
ImGui::PushFont(material::Type().iconSmall());
|
||||||
@@ -95,9 +99,7 @@ void ExportAllKeysDialog::render(App* app)
|
|||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
|
|
||||||
// Filename
|
// Filename
|
||||||
ImGui::Text("%s", TR("output_filename"));
|
material::LabeledInput(TR("output_filename"), "##Filename", s_filename, sizeof(s_filename));
|
||||||
ImGui::SetNextItemWidth(-1);
|
|
||||||
ImGui::InputText("##Filename", s_filename, sizeof(s_filename));
|
|
||||||
|
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
ImGui::TextDisabled("%s", TR("file_save_location"));
|
ImGui::TextDisabled("%s", TR("file_save_location"));
|
||||||
|
|||||||
@@ -82,9 +82,7 @@ void ExportTransactionsDialog::render(App* app)
|
|||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
|
|
||||||
// Filename
|
// Filename
|
||||||
ImGui::Text("%s", TR("output_filename"));
|
material::LabeledInput(TR("output_filename"), "##Filename", s_filename, sizeof(s_filename));
|
||||||
ImGui::SetNextItemWidth(-1);
|
|
||||||
ImGui::InputText("##Filename", s_filename, sizeof(s_filename));
|
|
||||||
|
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
ImGui::TextDisabled("%s", TR("file_save_location"));
|
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);
|
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 ui
|
||||||
} // namespace dragonx
|
} // namespace dragonx
|
||||||
|
|||||||
@@ -16,7 +16,6 @@
|
|||||||
#include "../material/type.h"
|
#include "../material/type.h"
|
||||||
#include "../material/draw_helpers.h"
|
#include "../material/draw_helpers.h"
|
||||||
#include "../material/colors.h"
|
#include "../material/colors.h"
|
||||||
#include "../material/components/buttons.h"
|
|
||||||
#include "../layout.h"
|
#include "../layout.h"
|
||||||
#include "../notifications.h"
|
#include "../notifications.h"
|
||||||
#include "../../embedded/IconsMaterialDesign.h"
|
#include "../../embedded/IconsMaterialDesign.h"
|
||||||
@@ -125,31 +124,17 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo&
|
|||||||
float btnX = idleRightEdge - btnSz;
|
float btnX = idleRightEdge - btnSz;
|
||||||
float btnY = curY + (headerH - btnSz) * 0.5f;
|
float btnY = curY + (headerH - btnSz) * 0.5f;
|
||||||
|
|
||||||
// Pill background when active
|
// Idle toggle: active pill + state-colored glyph (cursor saved/restored at block end).
|
||||||
if (idleOn) {
|
material::IconButtonStyle st;
|
||||||
dl->AddRectFilled(ImVec2(btnX, btnY), ImVec2(btnX + btnSz, btnY + btnSz),
|
st.color = idleOn ? Primary() : OnSurfaceDisabled();
|
||||||
WithAlpha(Primary(), 60), btnSz * 0.5f);
|
st.restBg = idleOn ? WithAlpha(Primary(), 60) : 0;
|
||||||
}
|
st.tooltip = idleOn ? TR("mining_idle_on_tooltip") : TR("mining_idle_off_tooltip");
|
||||||
|
|
||||||
// Icon centered in button
|
|
||||||
ImVec2 icoSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, idleIcon);
|
|
||||||
ImU32 icoCol = idleOn ? Primary() : OnSurfaceDisabled();
|
|
||||||
dl->AddText(icoFont, icoFont->LegacySize,
|
|
||||||
ImVec2(btnX + (btnSz - icoSz.x) * 0.5f, btnY + (btnSz - icoSz.y) * 0.5f),
|
|
||||||
icoCol, idleIcon);
|
|
||||||
|
|
||||||
// Click target (save/restore cursor so layout is unaffected)
|
|
||||||
ImVec2 savedCur = ImGui::GetCursorScreenPos();
|
ImVec2 savedCur = ImGui::GetCursorScreenPos();
|
||||||
ImGui::SetCursorScreenPos(ImVec2(btnX, btnY));
|
ImGui::SetCursorScreenPos(ImVec2(btnX, btnY));
|
||||||
ImGui::InvisibleButton("##IdleMining", ImVec2(btnSz, btnSz));
|
if (material::IconButton("##IdleMining", idleIcon, icoFont, ImVec2(btnSz, btnSz), st)) {
|
||||||
if (ImGui::IsItemClicked()) {
|
|
||||||
app->settings()->setMineWhenIdle(!idleOn);
|
app->settings()->setMineWhenIdle(!idleOn);
|
||||||
app->settings()->save();
|
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;
|
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;
|
const char* scaleIcon = threadScaling ? ICON_MD_TUNE : ICON_MD_POWER_SETTINGS_NEW;
|
||||||
float sBtnX = idleRightEdge - btnSz;
|
float sBtnX = idleRightEdge - btnSz;
|
||||||
float sBtnY = btnY;
|
float sBtnY = btnY;
|
||||||
|
material::IconButtonStyle st;
|
||||||
if (threadScaling) {
|
st.color = threadScaling ? Primary() : OnSurfaceMedium();
|
||||||
dl->AddRectFilled(ImVec2(sBtnX, sBtnY), ImVec2(sBtnX + btnSz, sBtnY + btnSz),
|
st.restBg = threadScaling ? WithAlpha(Primary(), 40) : 0;
|
||||||
WithAlpha(Primary(), 40), btnSz * 0.5f);
|
st.tooltip = threadScaling ? TR("mining_idle_scale_on_tooltip")
|
||||||
}
|
: TR("mining_idle_scale_off_tooltip");
|
||||||
|
|
||||||
ImVec2 sIcoSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, scaleIcon);
|
|
||||||
ImU32 sIcoCol = threadScaling ? Primary() : OnSurfaceMedium();
|
|
||||||
dl->AddText(icoFont, icoFont->LegacySize,
|
|
||||||
ImVec2(sBtnX + (btnSz - sIcoSz.x) * 0.5f, sBtnY + (btnSz - sIcoSz.y) * 0.5f),
|
|
||||||
sIcoCol, scaleIcon);
|
|
||||||
|
|
||||||
ImGui::SetCursorScreenPos(ImVec2(sBtnX, sBtnY));
|
ImGui::SetCursorScreenPos(ImVec2(sBtnX, sBtnY));
|
||||||
ImGui::InvisibleButton("##IdleScaleMode", ImVec2(btnSz, btnSz));
|
if (material::IconButton("##IdleScaleMode", scaleIcon, icoFont, ImVec2(btnSz, btnSz), st)) {
|
||||||
if (ImGui::IsItemClicked()) {
|
|
||||||
app->settings()->setIdleThreadScaling(!threadScaling);
|
app->settings()->setIdleThreadScaling(!threadScaling);
|
||||||
app->settings()->save();
|
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;
|
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
|
// When OFF: unrestricted mode, only keyboard/mouse input matters
|
||||||
if (idleOn) {
|
if (idleOn) {
|
||||||
bool gpuAware = app->settings()->getIdleGpuAware();
|
bool gpuAware = app->settings()->getIdleGpuAware();
|
||||||
const char* gpuIcon = gpuAware ? ICON_MD_MONITOR : ICON_MD_MONITOR;
|
|
||||||
float gBtnX = idleRightEdge - btnSz;
|
float gBtnX = idleRightEdge - btnSz;
|
||||||
float gBtnY = btnY;
|
float gBtnY = btnY;
|
||||||
|
material::IconButtonStyle st;
|
||||||
if (gpuAware) {
|
st.color = gpuAware ? Primary() : OnSurfaceDisabled();
|
||||||
dl->AddRectFilled(ImVec2(gBtnX, gBtnY), ImVec2(gBtnX + btnSz, gBtnY + btnSz),
|
st.restBg = gpuAware ? WithAlpha(Primary(), 40) : 0;
|
||||||
WithAlpha(Primary(), 40), btnSz * 0.5f);
|
st.tooltip = gpuAware ? TR("mining_idle_gpu_on_tooltip")
|
||||||
}
|
: TR("mining_idle_gpu_off_tooltip");
|
||||||
|
|
||||||
ImVec2 gIcoSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, gpuIcon);
|
|
||||||
ImU32 gIcoCol = gpuAware ? Primary() : OnSurfaceDisabled();
|
|
||||||
dl->AddText(icoFont, icoFont->LegacySize,
|
|
||||||
ImVec2(gBtnX + (btnSz - gIcoSz.x) * 0.5f, gBtnY + (btnSz - gIcoSz.y) * 0.5f),
|
|
||||||
gIcoCol, gpuIcon);
|
|
||||||
|
|
||||||
ImGui::SetCursorScreenPos(ImVec2(gBtnX, gBtnY));
|
ImGui::SetCursorScreenPos(ImVec2(gBtnX, gBtnY));
|
||||||
ImGui::InvisibleButton("##IdleGpuAware", ImVec2(btnSz, btnSz));
|
if (material::IconButton("##IdleGpuAware", ICON_MD_MONITOR, icoFont, ImVec2(btnSz, btnSz), st)) {
|
||||||
if (ImGui::IsItemClicked()) {
|
|
||||||
app->settings()->setIdleGpuAware(!gpuAware);
|
app->settings()->setIdleGpuAware(!gpuAware);
|
||||||
app->settings()->save();
|
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;
|
idleRightEdge = gBtnX - 4.0f * dp;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -488,27 +444,12 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo&
|
|||||||
float btnX = idleRightEdge - btnSz;
|
float btnX = idleRightEdge - btnSz;
|
||||||
float btnY = curY + (headerH - btnSz) * 0.5f;
|
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::SetCursorScreenPos(ImVec2(btnX, btnY));
|
||||||
ImGui::InvisibleButton("##BenchStart", ImVec2(btnSz, btnSz));
|
if (material::IconButton("##BenchStart", ICON_MD_SPEED, icoFont, ImVec2(btnSz, btnSz), st)) {
|
||||||
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) {
|
|
||||||
// Require a wallet address for pool mining
|
// Require a wallet address for pool mining
|
||||||
std::string worker(s_pool_worker);
|
std::string worker(s_pool_worker);
|
||||||
if (!worker.empty()) {
|
if (!worker.empty()) {
|
||||||
|
|||||||
@@ -283,17 +283,9 @@ void RenderMiningEarnings(App* app, const WalletState& state, const MiningInfo&
|
|||||||
snprintf(buf, sizeof(buf), "%.4f", mining.difficulty);
|
snprintf(buf, sizeof(buf), "%.4f", mining.difficulty);
|
||||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(col1X + valOffX, cy), OnSurface(), buf);
|
dl->AddText(capFont, capFont->LegacySize, ImVec2(col1X + valOffX, cy), OnSurface(), buf);
|
||||||
ImVec2 diffSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, buf);
|
ImVec2 diffSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, buf);
|
||||||
ImGui::SetCursorScreenPos(ImVec2(col1X + valOffX, cy));
|
if (material::CopyableTextOverlay("##DiffCopy", ImVec2(col1X + valOffX, cy), diffSz,
|
||||||
ImGui::InvisibleButton("##DiffCopy", ImVec2(diffSz.x + Layout::spacingMd(), capFont->LegacySize + 4 * dp));
|
capFont->LegacySize, buf, TR("mining_click_copy_difficulty"),
|
||||||
if (ImGui::IsItemHovered()) {
|
ImVec2(Layout::spacingMd(), 4 * dp))) {
|
||||||
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);
|
|
||||||
Notifications::instance().info(TR("mining_difficulty_copied"));
|
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);
|
snprintf(buf, sizeof(buf), "%d", mining.blocks);
|
||||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(col2X + valOffX, cy), OnSurface(), buf);
|
dl->AddText(capFont, capFont->LegacySize, ImVec2(col2X + valOffX, cy), OnSurface(), buf);
|
||||||
ImVec2 blkSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, buf);
|
ImVec2 blkSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, buf);
|
||||||
ImGui::SetCursorScreenPos(ImVec2(col2X + valOffX, cy));
|
if (material::CopyableTextOverlay("##BlockCopy", ImVec2(col2X + valOffX, cy), blkSz,
|
||||||
ImGui::InvisibleButton("##BlockCopy", ImVec2(blkSz.x + Layout::spacingMd(), capFont->LegacySize + 4 * dp));
|
capFont->LegacySize, buf, TR("mining_click_copy_block"),
|
||||||
if (ImGui::IsItemHovered()) {
|
ImVec2(Layout::spacingMd(), 4 * dp))) {
|
||||||
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);
|
|
||||||
Notifications::instance().info(TR("mining_block_copied"));
|
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());
|
dl->AddText(capFont, capFont->LegacySize, ImVec2(col3X + valOffX, cy), OnSurface(), truncAddr.c_str());
|
||||||
|
|
||||||
ImVec2 addrTextSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, truncAddr.c_str());
|
ImVec2 addrTextSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, truncAddr.c_str());
|
||||||
ImGui::SetCursorScreenPos(ImVec2(col3X + valOffX, cy));
|
if (material::CopyableTextOverlay("##MiningAddrCopy", ImVec2(col3X + valOffX, cy), addrTextSz,
|
||||||
ImGui::InvisibleButton("##MiningAddrCopy", ImVec2(addrTextSz.x + Layout::spacingMd(), capFont->LegacySize + 4 * dp));
|
capFont->LegacySize, mining_address.c_str(),
|
||||||
if (ImGui::IsItemHovered()) {
|
TR("mining_click_copy_address"),
|
||||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
ImVec2(Layout::spacingMd(), 4 * dp))) {
|
||||||
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());
|
|
||||||
Notifications::instance().info(TR("mining_address_copied"));
|
Notifications::instance().info(TR("mining_address_copied"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,6 @@
|
|||||||
#include "../material/type.h"
|
#include "../material/type.h"
|
||||||
#include "../material/draw_helpers.h"
|
#include "../material/draw_helpers.h"
|
||||||
#include "../material/colors.h"
|
#include "../material/colors.h"
|
||||||
#include "../material/components/buttons.h"
|
|
||||||
#include "../layout.h"
|
#include "../layout.h"
|
||||||
#include "../notifications.h"
|
#include "../notifications.h"
|
||||||
#include "../../embedded/IconsMaterialDesign.h"
|
#include "../../embedded/IconsMaterialDesign.h"
|
||||||
@@ -191,26 +190,13 @@ void RenderMiningModeToggle(App* app, const WalletState& state, const MiningInfo
|
|||||||
// --- URL: Dropdown arrow button ---
|
// --- URL: Dropdown arrow button ---
|
||||||
ImGui::SameLine(0, 0);
|
ImGui::SameLine(0, 0);
|
||||||
{
|
{
|
||||||
ImVec2 btnPos = ImGui::GetCursorScreenPos();
|
material::IconButtonStyle st;
|
||||||
ImVec2 btnSize(iconBtnW, inputFrameH2);
|
st.color = OnSurfaceMedium();
|
||||||
ImGui::InvisibleButton("##PoolDropdown", btnSize);
|
st.hoverBg = StateHover();
|
||||||
bool btnHov = ImGui::IsItemHovered();
|
st.bgRounding = 4.0f * dp;
|
||||||
bool btnClk = ImGui::IsItemClicked();
|
st.tooltip = TR("mining_saved_pools");
|
||||||
ImDrawList* dl2 = ImGui::GetWindowDrawList();
|
if (material::IconButton("##PoolDropdown", ICON_MD_ARROW_DROP_DOWN, Type().iconSmall(),
|
||||||
ImVec2 btnCenter(btnPos.x + btnSize.x * 0.5f, btnPos.y + btnSize.y * 0.5f);
|
ImVec2(iconBtnW, inputFrameH2), st)) {
|
||||||
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) {
|
|
||||||
ImGui::OpenPopup("##SavedPoolsPopup");
|
ImGui::OpenPopup("##SavedPoolsPopup");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -218,28 +204,16 @@ void RenderMiningModeToggle(App* app, const WalletState& state, const MiningInfo
|
|||||||
// --- URL: Bookmark button ---
|
// --- URL: Bookmark button ---
|
||||||
ImGui::SameLine(0, 0);
|
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);
|
std::string currentUrl(s_pool_url);
|
||||||
bool alreadySaved = miningValueAlreadySaved(app->settings()->getSavedPoolUrls(), currentUrl);
|
bool alreadySaved = miningValueAlreadySaved(app->settings()->getSavedPoolUrls(), currentUrl);
|
||||||
if (btnHov) {
|
material::IconButtonStyle st;
|
||||||
dl2->AddRectFilled(btnPos, ImVec2(btnPos.x + btnSize.x, btnPos.y + btnSize.y),
|
st.color = alreadySaved ? Primary() : OnSurfaceMedium();
|
||||||
StateHover(), 4.0f * dp);
|
st.hoverBg = StateHover();
|
||||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
st.bgRounding = 4.0f * dp;
|
||||||
material::Tooltip("%s", alreadySaved ? TR("mining_already_saved") : TR("mining_save_pool_url"));
|
st.tooltip = alreadySaved ? TR("mining_already_saved") : TR("mining_save_pool_url");
|
||||||
}
|
if (material::IconButton("##SavePoolUrl", alreadySaved ? ICON_MD_BOOKMARK : ICON_MD_BOOKMARK_BORDER,
|
||||||
ImFont* icoFont = Type().iconSmall();
|
Type().iconSmall(), ImVec2(iconBtnW, inputFrameH2), st)
|
||||||
const char* saveIcon = alreadySaved ? ICON_MD_BOOKMARK : ICON_MD_BOOKMARK_BORDER;
|
&& !currentUrl.empty() && !alreadySaved) {
|
||||||
ImVec2 icoSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, saveIcon);
|
|
||||||
dl2->AddText(icoFont, icoFont->LegacySize,
|
|
||||||
ImVec2(btnCenter.x - icoSz.x * 0.5f, btnCenter.y - icoSz.y * 0.5f),
|
|
||||||
alreadySaved ? Primary() : OnSurfaceMedium(), saveIcon);
|
|
||||||
if (btnClk && !currentUrl.empty() && !alreadySaved) {
|
|
||||||
app->settings()->addSavedPoolUrl(currentUrl);
|
app->settings()->addSavedPoolUrl(currentUrl);
|
||||||
app->settings()->save();
|
app->settings()->save();
|
||||||
}
|
}
|
||||||
@@ -413,26 +387,13 @@ void RenderMiningModeToggle(App* app, const WalletState& state, const MiningInfo
|
|||||||
// --- Worker: Dropdown arrow button ---
|
// --- Worker: Dropdown arrow button ---
|
||||||
ImGui::SameLine(0, 0);
|
ImGui::SameLine(0, 0);
|
||||||
{
|
{
|
||||||
ImVec2 btnPos = ImGui::GetCursorScreenPos();
|
material::IconButtonStyle st;
|
||||||
ImVec2 btnSize(iconBtnW, inputFrameH2);
|
st.color = OnSurfaceMedium();
|
||||||
ImGui::InvisibleButton("##WorkerDropdown", btnSize);
|
st.hoverBg = StateHover();
|
||||||
bool btnHov = ImGui::IsItemHovered();
|
st.bgRounding = 4.0f * dp;
|
||||||
bool btnClk = ImGui::IsItemClicked();
|
st.tooltip = TR("mining_saved_addresses");
|
||||||
ImDrawList* dl2 = ImGui::GetWindowDrawList();
|
if (material::IconButton("##WorkerDropdown", ICON_MD_ARROW_DROP_DOWN, Type().iconSmall(),
|
||||||
ImVec2 btnCenter(btnPos.x + btnSize.x * 0.5f, btnPos.y + btnSize.y * 0.5f);
|
ImVec2(iconBtnW, inputFrameH2), st)) {
|
||||||
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) {
|
|
||||||
ImGui::OpenPopup("##SavedWorkersPopup");
|
ImGui::OpenPopup("##SavedWorkersPopup");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -440,28 +401,16 @@ void RenderMiningModeToggle(App* app, const WalletState& state, const MiningInfo
|
|||||||
// --- Worker: Bookmark button ---
|
// --- Worker: Bookmark button ---
|
||||||
ImGui::SameLine(0, 0);
|
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);
|
std::string currentWorker(s_pool_worker);
|
||||||
bool alreadySaved = miningValueAlreadySaved(app->settings()->getSavedPoolWorkers(), currentWorker);
|
bool alreadySaved = miningValueAlreadySaved(app->settings()->getSavedPoolWorkers(), currentWorker);
|
||||||
if (btnHov) {
|
material::IconButtonStyle st;
|
||||||
dl2->AddRectFilled(btnPos, ImVec2(btnPos.x + btnSize.x, btnPos.y + btnSize.y),
|
st.color = alreadySaved ? Primary() : OnSurfaceMedium();
|
||||||
StateHover(), 4.0f * dp);
|
st.hoverBg = StateHover();
|
||||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
st.bgRounding = 4.0f * dp;
|
||||||
material::Tooltip("%s", alreadySaved ? TR("mining_already_saved") : TR("mining_save_payout_address"));
|
st.tooltip = alreadySaved ? TR("mining_already_saved") : TR("mining_save_payout_address");
|
||||||
}
|
if (material::IconButton("##SaveWorkerAddr", alreadySaved ? ICON_MD_BOOKMARK : ICON_MD_BOOKMARK_BORDER,
|
||||||
ImFont* icoFont = Type().iconSmall();
|
Type().iconSmall(), ImVec2(iconBtnW, inputFrameH2), st)
|
||||||
const char* saveIcon = alreadySaved ? ICON_MD_BOOKMARK : ICON_MD_BOOKMARK_BORDER;
|
&& !currentWorker.empty() && currentWorker != "x" && !alreadySaved) {
|
||||||
ImVec2 icoSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, saveIcon);
|
|
||||||
dl2->AddText(icoFont, icoFont->LegacySize,
|
|
||||||
ImVec2(btnCenter.x - icoSz.x * 0.5f, btnCenter.y - icoSz.y * 0.5f),
|
|
||||||
alreadySaved ? Primary() : OnSurfaceMedium(), saveIcon);
|
|
||||||
if (btnClk && !currentWorker.empty() && currentWorker != "x" && !alreadySaved) {
|
|
||||||
app->settings()->addSavedPoolWorker(currentWorker);
|
app->settings()->addSavedPoolWorker(currentWorker);
|
||||||
app->settings()->save();
|
app->settings()->save();
|
||||||
}
|
}
|
||||||
@@ -580,32 +529,13 @@ void RenderMiningModeToggle(App* app, const WalletState& state, const MiningInfo
|
|||||||
// === Reset to defaults button ===
|
// === Reset to defaults button ===
|
||||||
ImGui::SameLine(0, Layout::spacingSm());
|
ImGui::SameLine(0, Layout::spacingSm());
|
||||||
{
|
{
|
||||||
ImVec2 btnPos = ImGui::GetCursorScreenPos();
|
material::IconButtonStyle st;
|
||||||
ImVec2 btnSize(inputFrameH2, inputFrameH2);
|
st.color = OnSurfaceMedium();
|
||||||
ImGui::InvisibleButton("##ResetPoolDefaults", btnSize);
|
st.hoverBg = StateHover();
|
||||||
bool btnHov = ImGui::IsItemHovered();
|
st.bgRounding = 4.0f * dp;
|
||||||
bool btnClk = ImGui::IsItemClicked();
|
st.tooltip = TR("mining_reset_defaults");
|
||||||
|
if (material::IconButton("##ResetPoolDefaults", ICON_MD_REFRESH, Type().iconSmall(),
|
||||||
ImDrawList* dl2 = ImGui::GetWindowDrawList();
|
ImVec2(inputFrameH2, inputFrameH2), st)) {
|
||||||
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) {
|
|
||||||
strncpy(s_pool_url, defaultPoolUrl(), sizeof(s_pool_url) - 1);
|
strncpy(s_pool_url, defaultPoolUrl(), sizeof(s_pool_url) - 1);
|
||||||
// Default to user's first shielded (z) address for pool payouts.
|
// Default to user's first shielded (z) address for pool payouts.
|
||||||
// Leave blank if no z-address exists yet.
|
// 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());
|
(int)util::knownPools().size());
|
||||||
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(x, y), OnSurfaceMedium(), hdr);
|
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;
|
float btnS = ovFont->LegacySize + 6 * dp;
|
||||||
ImVec2 rbMin(x + w - btnS, y - 2 * 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::SetCursorScreenPos(rbMin);
|
||||||
ImGui::InvisibleButton("##PoolRefresh", ImVec2(btnS, btnS));
|
if (material::IconButton("##PoolRefresh", ICON_MD_REFRESH, Type().iconSmall(), ImVec2(btnS, btnS), st))
|
||||||
bool rhov = ImGui::IsItemHovered();
|
app->requestPoolBalanceRefresh();
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
y += ovFont->LegacySize + 4 * dp;
|
y += ovFont->LegacySize + 4 * dp;
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,6 @@
|
|||||||
#include "../material/type.h"
|
#include "../material/type.h"
|
||||||
#include "../material/draw_helpers.h"
|
#include "../material/draw_helpers.h"
|
||||||
#include "../material/colors.h"
|
#include "../material/colors.h"
|
||||||
#include "../material/components/buttons.h"
|
|
||||||
#include "../effects/low_spec.h"
|
#include "../effects/low_spec.h"
|
||||||
#include "../layout.h"
|
#include "../layout.h"
|
||||||
#include "../notifications.h"
|
#include "../notifications.h"
|
||||||
|
|||||||
@@ -285,9 +285,8 @@ void RenderLiteNetworkTab(App* app)
|
|||||||
ImVec2 tagSz = cap->CalcTextSizeA(cap->LegacySize, FLT_MAX, 0, tag);
|
ImVec2 tagSz = cap->CalcTextSizeA(cap->LegacySize, FLT_MAX, 0, tag);
|
||||||
float tagX = rx - tagSz.x;
|
float tagX = rx - tagSz.x;
|
||||||
float tagY = cardMin.y + 9.0f * dp;
|
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),
|
material::DrawPill(dl, ImVec2(tagX - 6 * dp, tagY - 1 * dp), tag, cap, tagFg, tagBg, 0,
|
||||||
tagBg, 4.0f * dp);
|
ImVec2(6 * dp, 1 * dp), 4.0f * dp);
|
||||||
dl->AddText(cap, cap->LegacySize, ImVec2(tagX, tagY), tagFg, tag);
|
|
||||||
if (selected) {
|
if (selected) {
|
||||||
const char* inUse = TR("lite_net_in_use");
|
const char* inUse = TR("lite_net_in_use");
|
||||||
ImVec2 uSz = cap->CalcTextSizeA(cap->LegacySize, FLT_MAX, 0, inUse);
|
ImVec2 uSz = cap->CalcTextSizeA(cap->LegacySize, FLT_MAX, 0, inUse);
|
||||||
@@ -295,25 +294,18 @@ void RenderLiteNetworkTab(App* app)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Hide / unhide button (far-right strip).
|
// Hide / unhide button (far-right strip).
|
||||||
ImVec2 hideMin(cardMin.x + cardBtnW, cardMin.y);
|
ImGui::SetCursorScreenPos(ImVec2(cardMin.x + cardBtnW, cardMin.y));
|
||||||
ImGui::SetCursorScreenPos(hideMin);
|
material::IconButtonStyle hideSt;
|
||||||
ImGui::InvisibleButton(("##litehide_" + sv.url).c_str(), ImVec2(hideW, cardH));
|
hideSt.color = OnSurfaceDisabled();
|
||||||
bool hideHov = ImGui::IsItemHovered();
|
hideSt.hoverColor = OnSurface();
|
||||||
if (hideHov) {
|
hideSt.tooltip = hiddenList ? TR("lite_net_unhide") : TR("lite_net_hide");
|
||||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
if (material::IconButton(("##litehide_" + sv.url).c_str(),
|
||||||
material::Tooltip("%s", hiddenList ? TR("lite_net_unhide") : TR("lite_net_hide"));
|
hiddenList ? ICON_MD_VISIBILITY : ICON_MD_VISIBILITY_OFF,
|
||||||
}
|
Type().iconSmall(), ImVec2(hideW, cardH), hideSt)) {
|
||||||
if (ImGui::IsItemClicked()) {
|
|
||||||
if (hiddenList) st->unhideLiteServer(sv.url);
|
if (hiddenList) st->unhideLiteServer(sv.url);
|
||||||
else st->hideLiteServer(sv.url);
|
else st->hideLiteServer(sv.url);
|
||||||
st->save();
|
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
|
// 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.
|
// content height actually grows — ImGui won't extend bounds from a bare SetCursorScreenPos.
|
||||||
|
|||||||
+90
-159
@@ -6,6 +6,7 @@
|
|||||||
#include "../../app.h"
|
#include "../../app.h"
|
||||||
#include "../../data/wallet_state.h"
|
#include "../../data/wallet_state.h"
|
||||||
#include "../../util/i18n.h"
|
#include "../../util/i18n.h"
|
||||||
|
#include "mining_tab_helpers.h"
|
||||||
#include "../theme.h"
|
#include "../theme.h"
|
||||||
#include "../effects/imgui_acrylic.h"
|
#include "../effects/imgui_acrylic.h"
|
||||||
#include "../effects/low_spec.h"
|
#include "../effects/low_spec.h"
|
||||||
@@ -63,6 +64,30 @@ static bool IsSeedNode(const std::string& addr) {
|
|||||||
return seeds.count(ExtractIP(addr)) > 0;
|
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)
|
void RenderPeersTab(App* app)
|
||||||
{
|
{
|
||||||
auto& S = schema::UI();
|
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"));
|
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 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;
|
float ry = cardMin.y + pad * 0.5f + headerH;
|
||||||
|
|
||||||
// Helper to draw a subtle horizontal divider
|
// Advance ry past a row and draw a divider before the next one. Keeps the row
|
||||||
auto drawDivider = [&](float y) {
|
// stepping + divider in one place (was a per-row copy + a duplicate divider lambda).
|
||||||
float rnd = glassSpec.rounding;
|
auto stepRow = [&]() {
|
||||||
dl->AddLine(ImVec2(cardMin.x + rnd * 0.5f, y),
|
ry += rowH + Layout::spacingSm() * 0.5f;
|
||||||
ImVec2(cardMax.x - rnd * 0.5f, y),
|
drawCardDivider(dl, cardMin.x, cardMax.x, ry, glassSpec.rounding, dp);
|
||||||
WithAlpha(OnSurface(), 15), 1.0f * dp);
|
ry += Layout::spacingSm() * 0.5f + dividerH;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Row 1: Blocks | Longest Chain
|
// Row 1: Blocks | Longest Chain
|
||||||
{
|
{
|
||||||
// Blocks
|
// Blocks — bespoke: block number + "(X left)" in a second color
|
||||||
float cx = cardMin.x + pad;
|
float cx = leftX;
|
||||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("peers_blocks"));
|
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("peers_blocks"));
|
||||||
int blocks = state.sync.blocks;
|
int blocks = state.sync.blocks;
|
||||||
if (blocks > 0) {
|
if (blocks > 0) {
|
||||||
@@ -173,7 +200,6 @@ void RenderPeersTab(App* app)
|
|||||||
int blocksLeft = chainTip - blocks;
|
int blocksLeft = chainTip - blocks;
|
||||||
if (blocksLeft < 0) blocksLeft = 0;
|
if (blocksLeft < 0) blocksLeft = 0;
|
||||||
if (blocksLeft > 0) {
|
if (blocksLeft > 0) {
|
||||||
snprintf(buf, sizeof(buf), "%d (%d left)", blocks, blocksLeft);
|
|
||||||
float valY = ry + capFont->LegacySize + Layout::spacingXs();
|
float valY = ry + capFont->LegacySize + Layout::spacingXs();
|
||||||
// Draw block number in normal color
|
// Draw block number in normal color
|
||||||
char blockStr[32];
|
char blockStr[32];
|
||||||
@@ -195,145 +221,85 @@ void RenderPeersTab(App* app)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Longest Chain
|
// Longest Chain
|
||||||
cx = cardMin.x + pad + colW;
|
|
||||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("peers_longest_chain"));
|
|
||||||
if (state.longestchain > 0) {
|
if (state.longestchain > 0) {
|
||||||
snprintf(buf, sizeof(buf), "%d", state.longestchain);
|
snprintf(buf, sizeof(buf), "%d", state.longestchain);
|
||||||
int localHeight = mining.blocks > 0 ? mining.blocks : state.sync.blocks;
|
int localHeight = mining.blocks > 0 ? mining.blocks : state.sync.blocks;
|
||||||
ImU32 chainCol = (localHeight >= state.longestchain) ? Success() : Warning();
|
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 {
|
} 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;
|
stepRow();
|
||||||
drawDivider(ry);
|
|
||||||
ry += Layout::spacingSm() * 0.5f + dividerH;
|
|
||||||
|
|
||||||
// Row 2: Hashrate | Difficulty
|
// Row 2: Hashrate | Difficulty
|
||||||
{
|
{
|
||||||
// Hashrate
|
std::string hr = mining.networkHashrate > 0 ? FormatHashrate(mining.networkHashrate) : std::string();
|
||||||
float cx = cardMin.x + pad;
|
drawStatCell(dl, leftX, ry, capFont, sub1, TR("peers_hashrate"),
|
||||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("peers_hashrate"));
|
hr.empty() ? nullptr : hr.c_str(), Success());
|
||||||
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");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Difficulty
|
if (mining.difficulty > 0) snprintf(buf, sizeof(buf), "%.4f", mining.difficulty);
|
||||||
cx = cardMin.x + pad + colW;
|
drawStatCell(dl, rightX, ry, capFont, sub1, TR("difficulty"),
|
||||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("difficulty"));
|
mining.difficulty > 0 ? buf : nullptr, OnSurface());
|
||||||
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");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ry += rowH + Layout::spacingSm() * 0.5f;
|
stepRow();
|
||||||
drawDivider(ry);
|
|
||||||
ry += Layout::spacingSm() * 0.5f + dividerH;
|
|
||||||
|
|
||||||
// Row 3: Notarized | Protocol
|
// Row 3: Notarized | Protocol
|
||||||
{
|
{
|
||||||
// Notarized
|
if (state.notarized > 0) snprintf(buf, sizeof(buf), "%d", state.notarized);
|
||||||
float cx = cardMin.x + pad;
|
drawStatCell(dl, leftX, ry, capFont, sub1, TR("peers_notarized"),
|
||||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("peers_notarized"));
|
state.notarized > 0 ? buf : nullptr, OnSurface());
|
||||||
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");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Protocol
|
if (state.protocol_version > 0) snprintf(buf, sizeof(buf), "%d", state.protocol_version);
|
||||||
cx = cardMin.x + pad + colW;
|
drawStatCell(dl, rightX, ry, capFont, sub1, TR("peers_protocol"),
|
||||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("peers_protocol"));
|
state.protocol_version > 0 ? buf : nullptr, OnSurface());
|
||||||
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");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ry += rowH + Layout::spacingSm() * 0.5f;
|
stepRow();
|
||||||
drawDivider(ry);
|
|
||||||
ry += Layout::spacingSm() * 0.5f + dividerH;
|
|
||||||
|
|
||||||
// Row 4: Version | Memory
|
// 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) {
|
if (state.daemon_version > 0) {
|
||||||
int major = state.daemon_version / 1000000;
|
int major = state.daemon_version / 1000000;
|
||||||
int minor = (state.daemon_version / 10000) % 100;
|
int minor = (state.daemon_version / 10000) % 100;
|
||||||
int patch = (state.daemon_version / 100) % 100;
|
int patch = (state.daemon_version / 100) % 100;
|
||||||
snprintf(buf, sizeof(buf), "%d.%d.%d", major, minor, patch);
|
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;
|
double memMb = state.mining.daemon_memory_mb;
|
||||||
if (memMb > 0) {
|
if (memMb > 0) {
|
||||||
if (memMb >= 1024.0)
|
if (memMb >= 1024.0)
|
||||||
snprintf(buf, sizeof(buf), "%.1f GB", memMb / 1024.0);
|
snprintf(buf, sizeof(buf), "%.1f GB", memMb / 1024.0);
|
||||||
else
|
else
|
||||||
snprintf(buf, sizeof(buf), "%.0f MB", memMb);
|
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;
|
stepRow();
|
||||||
drawDivider(ry);
|
|
||||||
ry += Layout::spacingSm() * 0.5f + dividerH;
|
|
||||||
|
|
||||||
// Row 5: Longest Chain | Best Block
|
// Row 5: Longest Chain | Best Block
|
||||||
{
|
{
|
||||||
// Longest Chain
|
// 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) {
|
if (state.longestchain > 0) {
|
||||||
snprintf(buf, sizeof(buf), "%d", state.longestchain);
|
snprintf(buf, sizeof(buf), "%d", state.longestchain);
|
||||||
// Color green if local matches longest, warning if behind
|
// Color green if local matches longest, warning if behind
|
||||||
int localHeight = mining.blocks > 0 ? mining.blocks : state.sync.blocks;
|
int localHeight = mining.blocks > 0 ? mining.blocks : state.sync.blocks;
|
||||||
ImU32 chainCol = (localHeight >= state.longestchain) ? Success() : Warning();
|
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 {
|
} 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)
|
// Best Block — bespoke: truncated hash with click-to-copy + hover underline
|
||||||
cx = cardMin.x + pad + colW;
|
float cx = rightX;
|
||||||
|
float valY = ry + capFont->LegacySize + Layout::spacingXs();
|
||||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("peers_best_block"));
|
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()) {
|
if (!state.sync.best_blockhash.empty()) {
|
||||||
// Truncate hash to fit: first 6 + "..." + last 6
|
// Truncate hash to fit: first 6 + "..." + last 6
|
||||||
std::string hash = state.sync.best_blockhash;
|
std::string hash = state.sync.best_blockhash;
|
||||||
@@ -348,17 +314,10 @@ void RenderPeersTab(App* app)
|
|||||||
// Click to copy full hash
|
// Click to copy full hash
|
||||||
ImVec2 hashSz = sub1->CalcTextSizeA(sub1->LegacySize, FLT_MAX, 0, truncHash.c_str());
|
ImVec2 hashSz = sub1->CalcTextSizeA(sub1->LegacySize, FLT_MAX, 0, truncHash.c_str());
|
||||||
ImVec2 savedCursor = ImGui::GetCursorScreenPos();
|
ImVec2 savedCursor = ImGui::GetCursorScreenPos();
|
||||||
ImGui::SetCursorScreenPos(ImVec2(cx, valY));
|
std::string copyTip = std::string(TR("peers_click_copy")) + " " + hash;
|
||||||
ImGui::InvisibleButton("##BestBlockCopy", ImVec2(hashSz.x + Layout::spacingSm(), sub1->LegacySize + 2 * dp));
|
if (material::CopyableTextOverlay("##BestBlockCopy", ImVec2(cx, valY), hashSz,
|
||||||
if (ImGui::IsItemHovered()) {
|
sub1->LegacySize, hash.c_str(), copyTip.c_str(),
|
||||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
ImVec2(Layout::spacingSm(), 2 * dp))) {
|
||||||
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());
|
|
||||||
ui::Notifications::instance().info(TR("peers_hash_copied"));
|
ui::Notifications::instance().info(TR("peers_hash_copied"));
|
||||||
}
|
}
|
||||||
ImGui::SetCursorScreenPos(savedCursor);
|
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"));
|
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 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;
|
float ry = cardMin.y + pad * 0.5f + headerH;
|
||||||
|
|
||||||
// Helper to draw a subtle horizontal divider
|
// Advance ry past a row and draw a divider before the next one (see blockchain card).
|
||||||
auto drawPeerDivider = [&](float y) {
|
auto stepRow = [&]() {
|
||||||
float rnd = glassSpec.rounding;
|
ry += rowH + Layout::spacingSm() * 0.5f;
|
||||||
dl->AddLine(ImVec2(cardMin.x + rnd * 0.5f, y),
|
drawCardDivider(dl, cardMin.x, cardMax.x, ry, glassSpec.rounding, dp);
|
||||||
ImVec2(cardMax.x - rnd * 0.5f, y),
|
ry += Layout::spacingSm() * 0.5f + dividerH;
|
||||||
WithAlpha(OnSurface(), 15), 1.0f * dp);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Row 1: Connected | In/Out
|
// 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);
|
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);
|
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;
|
stepRow();
|
||||||
drawPeerDivider(ry);
|
|
||||||
ry += Layout::spacingSm() * 0.5f + dividerH;
|
|
||||||
|
|
||||||
// Row 2: TLS | Avg Ping
|
// Row 2: TLS | Avg Ping
|
||||||
{
|
{
|
||||||
// TLS
|
// TLS — bespoke: appends a check icon when every peer is TLS
|
||||||
float cx = cardMin.x + pad;
|
float cx = leftX;
|
||||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), "TLS");
|
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), "TLS");
|
||||||
float valY = ry + capFont->LegacySize + Layout::spacingXs();
|
float valY = ry + capFont->LegacySize + Layout::spacingXs();
|
||||||
if (totalPeers > 0) {
|
if (totalPeers > 0) {
|
||||||
@@ -428,60 +380,39 @@ void RenderPeersTab(App* app)
|
|||||||
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), OnSurfaceDisabled(), "\xE2\x80\x94");
|
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), OnSurfaceDisabled(), "\xE2\x80\x94");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Avg Ping
|
// Avg Ping — color by latency
|
||||||
cx = cardMin.x + pad + colW;
|
|
||||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("peers_avg_ping"));
|
|
||||||
ImU32 pingCol;
|
ImU32 pingCol;
|
||||||
if (avgPing < 100) pingCol = Success();
|
if (avgPing < 100) pingCol = Success();
|
||||||
else if (avgPing < 500) pingCol = Warning();
|
else if (avgPing < 500) pingCol = Warning();
|
||||||
else pingCol = Error();
|
else pingCol = Error();
|
||||||
snprintf(buf, sizeof(buf), "%.0f ms", avgPing);
|
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;
|
stepRow();
|
||||||
drawPeerDivider(ry);
|
|
||||||
ry += Layout::spacingSm() * 0.5f + dividerH;
|
|
||||||
|
|
||||||
// Row 3: Received | Sent
|
// 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);
|
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);
|
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;
|
stepRow();
|
||||||
drawPeerDivider(ry);
|
|
||||||
ry += Layout::spacingSm() * 0.5f + dividerH;
|
|
||||||
|
|
||||||
// Row 4: P2P Port | Banned
|
// Row 4: P2P Port | Banned
|
||||||
{
|
{
|
||||||
// P2P Port
|
if (state.p2p_port > 0) snprintf(buf, sizeof(buf), "%d", state.p2p_port);
|
||||||
float cx = cardMin.x + pad;
|
drawStatCell(dl, leftX, ry, capFont, sub1, TR("peers_p2p_port"),
|
||||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("peers_p2p_port"));
|
state.p2p_port > 0 ? buf : nullptr, OnSurface());
|
||||||
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();
|
|
||||||
size_t bannedCount = state.bannedPeers.size();
|
size_t bannedCount = state.bannedPeers.size();
|
||||||
snprintf(buf, sizeof(buf), "%zu", bannedCount);
|
snprintf(buf, sizeof(buf), "%zu", bannedCount);
|
||||||
ImU32 bannedCol = (bannedCount > 0) ? Warning() : OnSurface();
|
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);
|
ImGui::SetCursorScreenPos(basePos);
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
#include "../../config/settings.h"
|
#include "../../config/settings.h"
|
||||||
#include "../../util/i18n.h"
|
#include "../../util/i18n.h"
|
||||||
#include "../../util/platform.h"
|
#include "../../util/platform.h"
|
||||||
|
#include "../../util/text_format.h"
|
||||||
#include "../../config/version.h"
|
#include "../../config/version.h"
|
||||||
#include "../../data/wallet_state.h"
|
#include "../../data/wallet_state.h"
|
||||||
#include "../../ui/widgets/qr_code.h"
|
#include "../../ui/widgets/qr_code.h"
|
||||||
@@ -71,12 +72,6 @@ static bool s_generating_address = false;
|
|||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Helpers
|
// 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) {
|
static void OpenExplorerURL(App* app, const std::string& address) {
|
||||||
std::string url = app->settings()->getAddressExplorerUrl() + address;
|
std::string url = app->settings()->getAddressExplorerUrl() + address;
|
||||||
dragonx::util::Platform::openUrl(url);
|
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 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 toggleGap = schema::UI().drawElement("tabs.receive", "toggle-gap").size;
|
||||||
float toggleTotalW = toggleBtnW * 3 + toggleGap * 2;
|
float toggleTotalW = toggleBtnW * 3 + toggleGap * 2;
|
||||||
|
float segH = ImGui::GetFrameHeight();
|
||||||
ImGui::SameLine(width - toggleTotalW);
|
ImGui::SameLine(width - toggleTotalW);
|
||||||
|
ImVec2 segOrigin = ImGui::GetCursorScreenPos();
|
||||||
const char* filterLabels[] = { TR("all_filter"), "Z", "T" };
|
const char* filterLabels[] = { TR("all_filter"), "Z", "T" };
|
||||||
for (int i = 0; i < 3; i++) {
|
int segClk = material::SegmentedControl(ImGui::GetWindowDrawList(), segOrigin, toggleTotalW, segH,
|
||||||
bool isActive = (s_addr_type_filter == i);
|
filterLabels, 3, s_addr_type_filter, Type().caption(),
|
||||||
if (isActive) {
|
"##addrFilter", Layout::dpiScale());
|
||||||
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(PrimaryVariant()));
|
if (segClk >= 0) s_addr_type_filter = segClk;
|
||||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1, 1, 1, 1));
|
ImGui::Dummy(ImVec2(toggleTotalW, segH));
|
||||||
} 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
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];
|
const auto& addr = state.addresses[s_selected_address_idx];
|
||||||
bool isZ = addr.type == "shielded";
|
bool isZ = addr.type == "shielded";
|
||||||
const char* tag = isZ ? "[Z]" : "[T]";
|
const char* tag = isZ ? "[Z]" : "[T]";
|
||||||
std::string trunc = TruncateAddress(addr.address,
|
std::string trunc = util::truncateMiddle(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)));
|
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",
|
snprintf(buf, sizeof(buf), "%s %s \xe2\x80\x94 %.8f %s",
|
||||||
tag, trunc.c_str(), addr.balance, DRAGONX_TICKER);
|
tag, trunc.c_str(), addr.balance, DRAGONX_TICKER);
|
||||||
s_source_preview = buf;
|
s_source_preview = buf;
|
||||||
@@ -256,7 +239,7 @@ static void RenderAddressDropdown(App* app, float width) {
|
|||||||
auto lblIt = s_address_labels.find(addr.address);
|
auto lblIt = s_address_labels.find(addr.address);
|
||||||
bool hasLabel = (lblIt != s_address_labels.end() && !lblIt->second.empty());
|
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) {
|
if (hasLabel) {
|
||||||
snprintf(buf, sizeof(buf), "%s %s (%s) \xe2\x80\x94 %.8f %s%s",
|
snprintf(buf, sizeof(buf), "%s %s (%s) \xe2\x80\x94 %.8f %s%s",
|
||||||
tag, lblIt->second.c_str(), trunc.c_str(),
|
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::SameLine(0, Layout::spacingSm());
|
||||||
ImGui::BeginDisabled(!app->isConnected() || s_generating_address);
|
ImGui::BeginDisabled(!app->isConnected() || s_generating_address);
|
||||||
if (s_generating_address) {
|
if (s_generating_address) {
|
||||||
int dots = ((int)(ImGui::GetTime() * 3.0f)) % 4;
|
|
||||||
const char* dotStr[] = {"", ".", "..", "..."};
|
|
||||||
char genLabel[64];
|
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"));
|
TactileButton(genLabel, ImVec2(newBtnW, 0), schema::UI().resolveFont("button"));
|
||||||
} else if (TactileButton(TrId("new", "recv").c_str(), 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;
|
s_generating_address = true;
|
||||||
@@ -416,8 +397,8 @@ static void RenderRecentReceived(const AddressInfo& /* addr */,
|
|||||||
|
|
||||||
// Address (second line)
|
// Address (second line)
|
||||||
float addrX = txX + S.drawElement("tabs.balance", "recent-tx-addr-offset").sizeOr(65.0f);
|
float addrX = txX + S.drawElement("tabs.balance", "recent-tx-addr-offset").sizeOr(65.0f);
|
||||||
std::string addrDisplay = TruncateAddress(tx.address,
|
std::string addrDisplay = util::truncateMiddle(tx.address,
|
||||||
(size_t)S.drawElement("tabs.balance", "recent-tx-addr-trunc").sizeOr(20.0f));
|
(int)S.drawElement("tabs.balance", "recent-tx-addr-trunc").sizeOr(20.0f));
|
||||||
rowDL->AddText(capFont, capFont->LegacySize,
|
rowDL->AddText(capFont, capFont->LegacySize,
|
||||||
ImVec2(addrX, rowPos.y + 2.0f * dp), OnSurfaceDisabled(), addrDisplay.c_str());
|
ImVec2(addrX, rowPos.y + 2.0f * dp), OnSurfaceDisabled(), addrDisplay.c_str());
|
||||||
|
|
||||||
|
|||||||
@@ -197,9 +197,7 @@ void RequestPaymentDialog::render(App* app)
|
|||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
|
|
||||||
// Label (optional)
|
// Label (optional)
|
||||||
ImGui::Text("%s", TR("request_label"));
|
if (material::LabeledInput(TR("request_label"), "##Label", s_label, sizeof(s_label))) {
|
||||||
ImGui::SetNextItemWidth(-1);
|
|
||||||
if (ImGui::InputText("##Label", s_label, sizeof(s_label))) {
|
|
||||||
s_uri_dirty = true;
|
s_uri_dirty = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -208,9 +206,8 @@ void RequestPaymentDialog::render(App* app)
|
|||||||
// Memo (optional, only for z-addr)
|
// Memo (optional, only for z-addr)
|
||||||
bool is_zaddr = (s_address[0] == 'z');
|
bool is_zaddr = (s_address[0] == 'z');
|
||||||
if (is_zaddr) {
|
if (is_zaddr) {
|
||||||
ImGui::Text("%s", TR("request_memo"));
|
if (material::LabeledInputMultiline(TR("request_memo"), "##Memo", s_memo, sizeof(s_memo),
|
||||||
ImGui::SetNextItemWidth(-1);
|
ImVec2(-1, memoInput.height > 0 ? memoInput.height : 60))) {
|
||||||
if (ImGui::InputTextMultiline("##Memo", s_memo, sizeof(s_memo), ImVec2(-1, memoInput.height > 0 ? memoInput.height : 60))) {
|
|
||||||
s_uri_dirty = true;
|
s_uri_dirty = true;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -242,13 +239,11 @@ void RequestPaymentDialog::render(App* app)
|
|||||||
|
|
||||||
// Payment URI display
|
// Payment URI display
|
||||||
if (!s_payment_uri.empty()) {
|
if (!s_payment_uri.empty()) {
|
||||||
ImGui::Text("%s", TR("request_payment_uri"));
|
|
||||||
ImGui::SetNextItemWidth(-1);
|
|
||||||
|
|
||||||
// Use a selectable text area for the URI
|
// Use a selectable text area for the URI
|
||||||
char uri_buf[1024];
|
char uri_buf[1024];
|
||||||
strncpy(uri_buf, s_payment_uri.c_str(), sizeof(uri_buf) - 1);
|
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();
|
ImGui::Spacing();
|
||||||
|
|
||||||
|
|||||||
+14
-16
@@ -147,12 +147,6 @@ static double GetAvailableBalance(App* app) {
|
|||||||
return 0.0;
|
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
|
// 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".
|
// 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.
|
// 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];
|
const auto& addr = state.addresses[s_selected_from_idx];
|
||||||
bool isZ = addr.type == "shielded";
|
bool isZ = addr.type == "shielded";
|
||||||
const char* tag = isZ ? "[Z]" : "[T]";
|
const char* tag = isZ ? "[Z]" : "[T]";
|
||||||
std::string trunc = TruncateAddress(addr.address,
|
std::string trunc = util::truncateMiddle(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)));
|
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",
|
snprintf(buf, sizeof(buf), "%s %s — %.8f %s",
|
||||||
tag, trunc.c_str(), addr.balance, DRAGONX_TICKER);
|
tag, trunc.c_str(), addr.balance, DRAGONX_TICKER);
|
||||||
s_source_preview = buf;
|
s_source_preview = buf;
|
||||||
@@ -285,7 +279,7 @@ static void RenderSourceDropdown(App* app, float width) {
|
|||||||
bool isZ = addr.type == "shielded";
|
bool isZ = addr.type == "shielded";
|
||||||
const char* tag = isZ ? "[Z]" : "[T]";
|
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",
|
snprintf(buf, sizeof(buf), "%s %s — %.8f %s",
|
||||||
tag, trunc.c_str(), addr.balance, DRAGONX_TICKER);
|
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);
|
ImGui::BeginChild(childId, ImVec2(width, sugH), true);
|
||||||
for (size_t si = 0; si < suggestions.size(); si++) {
|
for (size_t si = 0; si < suggestions.size(); si++) {
|
||||||
int sugTrunc = (int)schema::UI().drawElement("tabs.send", "suggestion-trunc-len").size;
|
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);
|
ImGui::PushID((int)si);
|
||||||
if (ImGui::Selectable(dispSug.c_str())) {
|
if (ImGui::Selectable(dispSug.c_str())) {
|
||||||
snprintf(s_to_address, sizeof(s_to_address), "%s", suggestions[si].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);
|
DrawGlassPanel(popDl, cMin, cMax, gs);
|
||||||
popDl->AddText(body2, body2->LegacySize,
|
popDl->AddText(body2, body2->LegacySize,
|
||||||
ImVec2(cMin.x + Layout::spacingMd(), cMin.y + Layout::spacingSm()),
|
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(popW, addrCardH));
|
||||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||||
}
|
}
|
||||||
@@ -722,7 +716,7 @@ void RenderSendConfirmPopup(App* app) {
|
|||||||
DrawGlassPanel(popDl, cMin, cMax, gs);
|
DrawGlassPanel(popDl, cMin, cMax, gs);
|
||||||
popDl->AddText(body2, body2->LegacySize,
|
popDl->AddText(body2, body2->LegacySize,
|
||||||
ImVec2(cMin.x + Layout::spacingMd(), cMin.y + Layout::spacingSm()),
|
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(popW, addrCardH));
|
||||||
ImGui::Dummy(ImVec2(0, Layout::spacingMd()));
|
ImGui::Dummy(ImVec2(0, Layout::spacingMd()));
|
||||||
}
|
}
|
||||||
@@ -910,8 +904,12 @@ static void RenderActionButtons(App* app, float width, float vScale,
|
|||||||
|
|
||||||
// ---- Send button ----
|
// ---- Send button ----
|
||||||
if (!can_send) {
|
if (!can_send) {
|
||||||
ImGui::PushStyleColor(ImGuiCol_Button, 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
|
||||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(IM_COL32(255, 255, 255, (int)S.drawElement("tabs.send", "disabled-btn-bg-alpha").size)));
|
// 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()));
|
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(OnSurfaceDisabled()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1058,8 +1056,8 @@ static void RenderRecentSends(const WalletState& state, float width, ImFont* cap
|
|||||||
|
|
||||||
// Address (second line)
|
// Address (second line)
|
||||||
float addrX = txX + S.drawElement("tabs.balance", "recent-tx-addr-offset").sizeOr(65.0f);
|
float addrX = txX + S.drawElement("tabs.balance", "recent-tx-addr-offset").sizeOr(65.0f);
|
||||||
std::string addrDisplay = TruncateAddress(tx.address,
|
std::string addrDisplay = util::truncateMiddle(tx.address,
|
||||||
(size_t)S.drawElement("tabs.balance", "recent-tx-addr-trunc").sizeOr(20.0f));
|
(int)S.drawElement("tabs.balance", "recent-tx-addr-trunc").sizeOr(20.0f));
|
||||||
rowDL->AddText(capFont, capFont->LegacySize,
|
rowDL->AddText(capFont, capFont->LegacySize,
|
||||||
ImVec2(addrX, rowPos.y + 2.0f * dp), OnSurfaceDisabled(), addrDisplay.c_str());
|
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::Separator();
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
|
|
||||||
ImGui::Text("%s", TR("rpc_host"));
|
material::LabeledInputRow(TR("rpc_host"), connLbl.position, "##RPCHost",
|
||||||
ImGui::SameLine(connLbl.position);
|
s_rpc_host, sizeof(s_rpc_host), cmb.width);
|
||||||
ImGui::SetNextItemWidth(cmb.width);
|
|
||||||
ImGui::InputText("##RPCHost", s_rpc_host, sizeof(s_rpc_host));
|
|
||||||
|
|
||||||
ImGui::Text("%s", TR("rpc_port"));
|
material::LabeledInputRow(TR("rpc_port"), connLbl.position, "##RPCPort",
|
||||||
ImGui::SameLine(connLbl.position);
|
s_rpc_port, sizeof(s_rpc_port), portInput.width);
|
||||||
ImGui::SetNextItemWidth(portInput.width);
|
|
||||||
ImGui::InputText("##RPCPort", s_rpc_port, sizeof(s_rpc_port));
|
|
||||||
|
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
|
|
||||||
ImGui::Text("%s", TR("rpc_user"));
|
material::LabeledInputRow(TR("rpc_user"), connLbl.position, "##RPCUser",
|
||||||
ImGui::SameLine(connLbl.position);
|
s_rpc_user, sizeof(s_rpc_user), cmb.width);
|
||||||
ImGui::SetNextItemWidth(cmb.width);
|
|
||||||
ImGui::InputText("##RPCUser", s_rpc_user, sizeof(s_rpc_user));
|
|
||||||
|
|
||||||
ImGui::Text("%s", TR("rpc_pass"));
|
material::LabeledInputRow(TR("rpc_pass"), connLbl.position, "##RPCPassword",
|
||||||
ImGui::SameLine(connLbl.position);
|
s_rpc_password, sizeof(s_rpc_password), cmb.width, nullptr,
|
||||||
ImGui::SetNextItemWidth(cmb.width);
|
ImGuiInputTextFlags_Password);
|
||||||
ImGui::InputText("##RPCPassword", s_rpc_password, sizeof(s_rpc_password),
|
|
||||||
ImGuiInputTextFlags_Password);
|
|
||||||
|
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
ImGui::Separator();
|
ImGui::Separator();
|
||||||
@@ -487,11 +479,7 @@ void RenderSettingsWindow(App* app, bool* p_open)
|
|||||||
// Confirmation dialog
|
// Confirmation dialog
|
||||||
if (s_confirm_clear_ztx) {
|
if (s_confirm_clear_ztx) {
|
||||||
if (material::BeginOverlayDialog(TR("confirm_clear_ztx_title"), &s_confirm_clear_ztx, 480.0f, 0.94f)) {
|
if (material::BeginOverlayDialog(TR("confirm_clear_ztx_title"), &s_confirm_clear_ztx, 480.0f, 0.94f)) {
|
||||||
ImGui::PushFont(material::Type().iconLarge());
|
material::DialogWarningHeader(TR("warning"), ImVec4(1.0f, 0.6f, 0.0f, 1.0f));
|
||||||
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"));
|
|
||||||
|
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
ImGui::TextWrapped("%s", TR("confirm_clear_ztx_warning1"));
|
ImGui::TextWrapped("%s", TR("confirm_clear_ztx_warning1"));
|
||||||
@@ -501,14 +489,13 @@ void RenderSettingsWindow(App* app, bool* p_open)
|
|||||||
ImGui::Separator();
|
ImGui::Separator();
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
|
|
||||||
float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
|
bool doCancel = false, doConfirm = false;
|
||||||
if (ImGui::Button(TR("cancel"), ImVec2(btnW, 40))) {
|
material::DialogConfirmFooter(TR("cancel"), TrId("clear_anyway", "clear_ztx_w").c_str(),
|
||||||
|
true, doCancel, doConfirm);
|
||||||
|
if (doCancel) {
|
||||||
s_confirm_clear_ztx = false;
|
s_confirm_clear_ztx = false;
|
||||||
}
|
}
|
||||||
ImGui::SameLine();
|
if (doConfirm) {
|
||||||
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))) {
|
|
||||||
std::string ztx_file = util::Platform::getDragonXDataDir() + "ztx_history.json";
|
std::string ztx_file = util::Platform::getDragonXDataDir() + "ztx_history.json";
|
||||||
if (util::Platform::deleteFile(ztx_file)) {
|
if (util::Platform::deleteFile(ztx_file)) {
|
||||||
Notifications::instance().success("Z-transaction history cleared");
|
Notifications::instance().success("Z-transaction history cleared");
|
||||||
@@ -517,7 +504,6 @@ void RenderSettingsWindow(App* app, bool* p_open)
|
|||||||
}
|
}
|
||||||
s_confirm_clear_ztx = false;
|
s_confirm_clear_ztx = false;
|
||||||
}
|
}
|
||||||
ImGui::PopStyleColor(2);
|
|
||||||
material::EndOverlayDialog();
|
material::EndOverlayDialog();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -95,9 +95,7 @@ void ShieldDialog::render(App* app)
|
|||||||
|
|
||||||
// From address (for shield coinbase)
|
// From address (for shield coinbase)
|
||||||
if (s_mode == Mode::ShieldCoinbase) {
|
if (s_mode == Mode::ShieldCoinbase) {
|
||||||
ImGui::Text("%s", TR("shield_from_address"));
|
material::LabeledInput(TR("shield_from_address"), "##FromAddr", s_from_address, sizeof(s_from_address));
|
||||||
ImGui::SetNextItemWidth(-1);
|
|
||||||
ImGui::InputText("##FromAddr", s_from_address, sizeof(s_from_address));
|
|
||||||
ImGui::TextDisabled("%s", TR("shield_wildcard_hint"));
|
ImGui::TextDisabled("%s", TR("shield_wildcard_hint"));
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,7 +45,12 @@ void TransactionDetailsDialog::render(App* app)
|
|||||||
auto memoInput = S.input("dialogs.transaction-details", "memo-input");
|
auto memoInput = S.input("dialogs.transaction-details", "memo-input");
|
||||||
auto bottomBtn = S.button("dialogs.transaction-details", "bottom-button");
|
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;
|
const auto& tx = s_transaction;
|
||||||
|
|
||||||
// Type indicator with color
|
// Type indicator with color
|
||||||
|
|||||||
@@ -37,13 +37,6 @@ static std::string TrId(const char* key, const char* id) {
|
|||||||
return std::string(TR(key)) + "##" + 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
|
// Case-insensitive string search
|
||||||
static bool containsIgnoreCase(const std::string& str, const std::string& search) {
|
static bool containsIgnoreCase(const std::string& str, const std::string& search) {
|
||||||
return dragonx::util::containsIgnoreCase(str, 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);
|
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)
|
void RenderTransactionsTab(App* app)
|
||||||
{
|
{
|
||||||
auto& S = schema::UISchema::instance();
|
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.
|
// Sort mode: 0 = newest first, 1 = oldest first, 2 = largest amount, 3 = smallest amount.
|
||||||
static int s_sort_mode = 0;
|
static int s_sort_mode = 0;
|
||||||
|
|
||||||
// --- Received card ---
|
// --- Received / Sent / Mined cards ---
|
||||||
{
|
// filter values: Received = 2, Sent = 1, Mined = 3 (see idx_map for the accent bar below)
|
||||||
ImVec2 cMin = origin;
|
drawSummaryCard(dl, origin, cardW, cardH, innerPad, iconSz, glassSpec,
|
||||||
ImVec2 cMax(cMin.x + cardW, cMin.y + cardH);
|
ovFont, capFont, body2, "receive", greenCol, "received_upper",
|
||||||
DrawGlassPanel(dl, cMin, cMax, glassSpec);
|
recvCount, recvTotal, "+", 2, type_filter);
|
||||||
|
drawSummaryCard(dl, ImVec2(origin.x + (cardW + cardGap), origin.y), cardW, cardH,
|
||||||
float cx = cMin.x + innerPad;
|
innerPad, iconSz, glassSpec, ovFont, capFont, body2, "send", redCol, "sent_upper",
|
||||||
float cy = cMin.y + Layout::spacingMd();
|
sendCount, sendTotal, "-", 1, type_filter);
|
||||||
|
drawSummaryCard(dl, ImVec2(origin.x + 2 * (cardW + cardGap), origin.y), cardW, cardH,
|
||||||
// Icon
|
innerPad, iconSz, glassSpec, ovFont, capFont, body2, "mined", goldCol, "mined_upper",
|
||||||
DrawTxIcon(dl, "receive", cx + iconSz, cy + iconSz * 1.33f, iconSz, greenCol);
|
minedCount, minedTotal, "+", 3, type_filter);
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Selected card accent
|
// Selected card accent
|
||||||
if (type_filter > 0) {
|
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::TextColored(ImVec4(0.6f, 0.8f, 1.0f, pulse), ICON_MD_HOURGLASS_EMPTY);
|
||||||
ImGui::PopFont();
|
ImGui::PopFont();
|
||||||
ImGui::SameLine(0, Layout::spacingXs());
|
ImGui::SameLine(0, Layout::spacingXs());
|
||||||
int dots = ((int)(ImGui::GetTime() * 3.0f)) % 4;
|
ImGui::TextColored(ImVec4(0.6f, 0.8f, 1.0f, 1.0f), "%s%s", txLoadingText.c_str(), material::LoadingDots());
|
||||||
const char* dotStr[] = {"", ".", "..", "..."};
|
|
||||||
ImGui::TextColored(ImVec4(0.6f, 0.8f, 1.0f, 1.0f), "%s%s", txLoadingText.c_str(), dotStr[dots]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm() + Layout::spacingXs()));
|
ImGui::Dummy(ImVec2(0, Layout::spacingSm() + Layout::spacingXs()));
|
||||||
@@ -679,9 +630,7 @@ void RenderTransactionsTab(App* app)
|
|||||||
} else if (state.transactions.empty()) {
|
} else if (state.transactions.empty()) {
|
||||||
ImGui::Dummy(ImVec2(0, 20));
|
ImGui::Dummy(ImVec2(0, 20));
|
||||||
if (txLoading) {
|
if (txLoading) {
|
||||||
int dots = ((int)(ImGui::GetTime() * 3.0f)) % 4;
|
snprintf(buf, sizeof(buf), "%s%s", txLoadingText.c_str(), material::LoadingDots());
|
||||||
const char* dotStr[] = {"", ".", "..", "..."};
|
|
||||||
snprintf(buf, sizeof(buf), "%s%s", txLoadingText.c_str(), dotStr[dots]);
|
|
||||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), buf);
|
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), buf);
|
||||||
} else {
|
} else {
|
||||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("no_transactions"));
|
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("no_transactions"));
|
||||||
@@ -756,7 +705,7 @@ void RenderTransactionsTab(App* app)
|
|||||||
OnSurfaceDisabled(), ago.c_str());
|
OnSurfaceDisabled(), ago.c_str());
|
||||||
|
|
||||||
// Address (second line, left side)
|
// 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()),
|
dl->AddText(capFont, capFont->LegacySize, ImVec2(labelX, cy + body2->LegacySize + Layout::spacingXs()),
|
||||||
OnSurfaceMedium(), addr_display.c_str());
|
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")) {
|
} else if (tx.confirmations >= 100 && (tx.display_type == "generate" || tx.display_type == "mined")) {
|
||||||
statusStr = TR("mature"); statusCol = greenCol;
|
statusStr = TR("mature"); statusCol = greenCol;
|
||||||
} else {
|
} else {
|
||||||
statusStr = TR("confirmed"); statusCol = WithAlpha(Success(), 140);
|
statusStr = TR("confirmed"); statusCol = Success();
|
||||||
}
|
}
|
||||||
// Position status badge in the middle-right area
|
// Position status badge in the middle-right area
|
||||||
ImVec2 sSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, statusStr);
|
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
|
float minStatusX = cx + innerW * 0.25f; // don't overlap address
|
||||||
if (statusX < minStatusX) statusX = minStatusX;
|
if (statusX < minStatusX) statusX = minStatusX;
|
||||||
float statusTextX = statusX + (stackW - sSz.x) * 0.5f;
|
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) {
|
if (shieldedDisplay) {
|
||||||
float shieldX = statusX + (stackW - shieldSz.x) * 0.5f;
|
float shieldX = statusX + (stackW - shieldSz.x) * 0.5f;
|
||||||
ImU32 shieldCol = Primary();
|
ImU32 shieldCol = Primary();
|
||||||
ImU32 shieldBg = (shieldCol & 0x00FFFFFFu) | (static_cast<ImU32>(30) << 24);
|
|
||||||
ImVec2 shieldPillMin(shieldX - Layout::spacingSm(), cy - 1.0f);
|
ImVec2 shieldPillMin(shieldX - Layout::spacingSm(), cy - 1.0f);
|
||||||
ImVec2 shieldPillMax(shieldX + shieldSz.x + Layout::spacingSm(),
|
ImVec2 shieldPillMax(shieldX + shieldSz.x + Layout::spacingSm(),
|
||||||
shieldPillMin.y + capFont->LegacySize + Layout::spacingXs());
|
shieldPillMin.y + capFont->LegacySize + Layout::spacingXs());
|
||||||
dl->AddRectFilled(shieldPillMin, shieldPillMax, shieldBg,
|
dl->AddRectFilled(shieldPillMin, shieldPillMax,
|
||||||
schema::UI().drawElement("tabs.transactions", "status-pill-rounding").size);
|
(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,
|
dl->AddText(capFont, capFont->LegacySize,
|
||||||
ImVec2(shieldX, cy), shieldCol, shieldedStr);
|
ImVec2(shieldX, cy), shieldCol, shieldedStr);
|
||||||
}
|
}
|
||||||
// Background pill
|
// Background pill
|
||||||
ImU32 pillBg = (statusCol & 0x00FFFFFFu) | (static_cast<ImU32>(30) << 24);
|
|
||||||
ImVec2 pillMin(statusTextX - Layout::spacingSm(), cy + body2->LegacySize + 1);
|
ImVec2 pillMin(statusTextX - Layout::spacingSm(), cy + body2->LegacySize + 1);
|
||||||
ImVec2 pillMax(statusTextX + sSz.x + Layout::spacingSm(), pillMin.y + capFont->LegacySize + Layout::spacingXs());
|
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,
|
dl->AddText(capFont, capFont->LegacySize,
|
||||||
ImVec2(statusTextX, cy + body2->LegacySize + Layout::spacingXs()), statusCol, statusStr);
|
ImVec2(statusTextX, cy + body2->LegacySize + Layout::spacingXs()), statusCol, statusStr);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,10 +61,9 @@ void ValidateAddressDialog::render(App* app)
|
|||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
|
|
||||||
// Address input
|
// Address input
|
||||||
ImGui::Text("%s", TR("address_label"));
|
bool enter_pressed = material::LabeledInput(TR("address_label"), "##ValidateAddr", s_address_input,
|
||||||
ImGui::SetNextItemWidth(-1);
|
sizeof(s_address_input), -1.0f, nullptr,
|
||||||
bool enter_pressed = ImGui::InputText("##ValidateAddr", s_address_input, sizeof(s_address_input),
|
ImGuiInputTextFlags_EnterReturnsTrue);
|
||||||
ImGuiInputTextFlags_EnterReturnsTrue);
|
|
||||||
|
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
|
|
||||||
|
|||||||
@@ -51,7 +51,10 @@ public:
|
|||||||
}
|
}
|
||||||
using namespace material;
|
using namespace material;
|
||||||
const float dp = Layout::dpiScale();
|
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();
|
const auto p = s_updater->getProgress();
|
||||||
using St = util::XmrigUpdater::State;
|
using St = util::XmrigUpdater::State;
|
||||||
switch (p.state) {
|
switch (p.state) {
|
||||||
@@ -179,16 +182,7 @@ private:
|
|||||||
: TR("xmrig_installing");
|
: TR("xmrig_installing");
|
||||||
Type().text(TypeStyle::Subtitle2, title);
|
Type().text(TypeStyle::Subtitle2, title);
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
const float barH = 8.0f * dp;
|
material::ProgressBar(fullW(), 8.0f * dp, p.percent / 100.0f);
|
||||||
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));
|
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
ImGui::Text("%s", p.status_text.c_str());
|
ImGui::Text("%s", p.status_text.c_str());
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
#include "daemon_updater.h"
|
#include "daemon_updater.h"
|
||||||
#include "xmrig_updater.h" // util::sha256Hex, util::verifyXmrigSignature (shared crypto)
|
#include "xmrig_updater.h" // util::sha256Hex, util::verifyXmrigSignature (shared crypto)
|
||||||
|
|
||||||
|
#include "http_download.h"
|
||||||
#include "logger.h"
|
#include "logger.h"
|
||||||
|
|
||||||
#include <curl/curl.h>
|
#include <curl/curl.h>
|
||||||
@@ -35,24 +36,6 @@ namespace {
|
|||||||
constexpr curl_off_t kMaxArchiveBytes = 256LL * 1024 * 1024; // 256 MiB
|
constexpr curl_off_t kMaxArchiveBytes = 256LL * 1024 * 1024; // 256 MiB
|
||||||
constexpr std::size_t kMaxMemberBytes = 128u * 1024 * 1024; // 128 MiB per extracted file
|
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)
|
std::string baseName(const std::string& path)
|
||||||
{
|
{
|
||||||
const auto slash = path.find_last_of("/\\");
|
const auto slash = path.find_last_of("/\\");
|
||||||
@@ -120,64 +103,13 @@ void DaemonUpdater::cancel()
|
|||||||
|
|
||||||
std::string DaemonUpdater::httpGet(const std::string& url)
|
std::string DaemonUpdater::httpGet(const std::string& url)
|
||||||
{
|
{
|
||||||
CURL* curl = curl_easy_init();
|
return httpGetString(url, "[daemon-updater]");
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool DaemonUpdater::downloadToFile(const std::string& url, const std::string& destPath)
|
bool DaemonUpdater::downloadToFile(const std::string& url, const std::string& destPath)
|
||||||
{
|
{
|
||||||
FILE* fp = std::fopen(destPath.c_str(), "wb");
|
return httpDownloadToFile(url, destPath, "[daemon-updater]", kMaxArchiveBytes,
|
||||||
if (!fp) return false;
|
[this](double downloaded, double total) { return onDownloadProgress(downloaded, total); });
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void DaemonUpdater::startCheck(const std::string& installedVersion)
|
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_["keep_daemon"] = "Keep daemon running";
|
||||||
strings_["stop_external"] = "Stop external daemon";
|
strings_["stop_external"] = "Stop external daemon";
|
||||||
strings_["verbose_logging"] = "Verbose logging";
|
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_["mine_when_idle"] = "Mine when idle";
|
||||||
strings_["setup_wizard"] = "Run Setup Wizard...";
|
strings_["setup_wizard"] = "Run Setup Wizard...";
|
||||||
|
|
||||||
@@ -452,6 +455,49 @@ void I18n::loadBuiltinEnglish()
|
|||||||
strings_["confirm_lite_redownload_title"] = "Redownload Blocks";
|
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_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.";
|
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_encrypt"] = "Encrypt wallet.dat with a passphrase";
|
||||||
strings_["tt_change_pass"] = "Change the wallet encryption passphrase";
|
strings_["tt_change_pass"] = "Change the wallet encryption passphrase";
|
||||||
strings_["tt_lock"] = "Lock the wallet immediately";
|
strings_["tt_lock"] = "Lock the wallet immediately";
|
||||||
@@ -483,7 +529,7 @@ void I18n::loadBuiltinEnglish()
|
|||||||
// Settings: about / debug
|
// 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_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_["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_select"] = "Select categories to enable daemon debug logging (-debug= flags).";
|
||||||
strings_["settings_debug_restart_note"] = "Changes take effect after restarting the daemon.";
|
strings_["settings_debug_restart_note"] = "Changes take effect after restarting the daemon.";
|
||||||
|
|
||||||
@@ -652,6 +698,15 @@ void I18n::loadBuiltinEnglish()
|
|||||||
strings_["transparent"] = "Transparent";
|
strings_["transparent"] = "Transparent";
|
||||||
strings_["total"] = "Total";
|
strings_["total"] = "Total";
|
||||||
strings_["unconfirmed"] = "Unconfirmed";
|
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_["your_addresses"] = "Your Addresses";
|
||||||
strings_["z_addresses"] = "Z-Addresses";
|
strings_["z_addresses"] = "Z-Addresses";
|
||||||
strings_["t_addresses"] = "T-Addresses";
|
strings_["t_addresses"] = "T-Addresses";
|
||||||
@@ -1059,15 +1114,53 @@ void I18n::loadBuiltinEnglish()
|
|||||||
strings_["market_24h"] = "24h";
|
strings_["market_24h"] = "24h";
|
||||||
strings_["market_24h_volume"] = "24H VOLUME";
|
strings_["market_24h_volume"] = "24H VOLUME";
|
||||||
strings_["market_6h"] = "6h";
|
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_attribution"] = "Price data from CoinGecko";
|
||||||
strings_["market_btc_price"] = "BTC PRICE";
|
strings_["market_btc_price"] = "BTC PRICE";
|
||||||
strings_["market_no_history"] = "No price history available";
|
strings_["market_no_history"] = "No price history available";
|
||||||
strings_["market_no_price"] = "No price data";
|
strings_["market_no_price"] = "No price data";
|
||||||
strings_["market_now"] = "Now";
|
strings_["market_now"] = "Now";
|
||||||
strings_["market_pct_shielded"] = "%.0f%% Shielded";
|
strings_["market_pct_shielded"] = "%.0f%% Shielded";
|
||||||
strings_["market_portfolio"] = "MY DRGX";
|
strings_["market_portfolio"] = "PORTFOLIO";
|
||||||
strings_["portfolio_all_funds"] = "All funds";
|
strings_["portfolio_all_funds"] = "All funds";
|
||||||
strings_["portfolio_manage"] = "Manage\xE2\x80\xA6";
|
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_manage_title"] = "Manage portfolio";
|
||||||
strings_["portfolio_add_entry"] = "Add entry";
|
strings_["portfolio_add_entry"] = "Add entry";
|
||||||
strings_["portfolio_new_entry"] = "New entry";
|
strings_["portfolio_new_entry"] = "New entry";
|
||||||
@@ -1081,6 +1174,10 @@ void I18n::loadBuiltinEnglish()
|
|||||||
strings_["portfolio_delete"] = "Delete";
|
strings_["portfolio_delete"] = "Delete";
|
||||||
strings_["portfolio_save"] = "Save";
|
strings_["portfolio_save"] = "Save";
|
||||||
strings_["portfolio_cancel"] = "Cancel";
|
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_add_to"] = "Add to portfolio";
|
||||||
strings_["portfolio_remove_from"] = "Remove from portfolio";
|
strings_["portfolio_remove_from"] = "Remove from portfolio";
|
||||||
strings_["market_24h_change"] = "24h";
|
strings_["market_24h_change"] = "24h";
|
||||||
|
|||||||
@@ -245,6 +245,9 @@ std::string Platform::getConfigDir()
|
|||||||
return std::string(path) + "\\" DRAGONX_APP_NAME "\\";
|
return std::string(path) + "\\" DRAGONX_APP_NAME "\\";
|
||||||
}
|
}
|
||||||
return getHomeDir() + "\\AppData\\Roaming\\" 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
|
#else
|
||||||
return getHomeDir() + "/.config/" DRAGONX_APP_NAME "/";
|
return getHomeDir() + "/.config/" DRAGONX_APP_NAME "/";
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -45,6 +45,21 @@ std::string formatTimeAgoShort(std::int64_t timestamp)
|
|||||||
return buf;
|
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)
|
bool containsIgnoreCase(std::string_view haystack, std::string_view needle)
|
||||||
{
|
{
|
||||||
if (needle.empty()) return true;
|
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.
|
// An empty needle matches everything. Safe for per-frame filtering of large lists.
|
||||||
bool containsIgnoreCase(std::string_view haystack, std::string_view needle);
|
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 util
|
||||||
} // namespace dragonx
|
} // namespace dragonx
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
#include "xmrig_updater.h"
|
#include "xmrig_updater.h"
|
||||||
|
|
||||||
|
#include "http_download.h"
|
||||||
#include "logger.h"
|
#include "logger.h"
|
||||||
|
|
||||||
#include <curl/curl.h>
|
#include <curl/curl.h>
|
||||||
@@ -31,24 +32,6 @@ namespace {
|
|||||||
constexpr curl_off_t kMaxArchiveBytes = 64LL * 1024 * 1024; // 64 MiB
|
constexpr curl_off_t kMaxArchiveBytes = 64LL * 1024 * 1024; // 64 MiB
|
||||||
constexpr std::size_t kMaxMemberBytes = 64u * 1024 * 1024; // 64 MiB per extracted file
|
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)
|
std::string baseName(const std::string& path)
|
||||||
{
|
{
|
||||||
const auto slash = path.find_last_of("/\\");
|
const auto slash = path.find_last_of("/\\");
|
||||||
@@ -109,64 +92,13 @@ void XmrigUpdater::cancel()
|
|||||||
|
|
||||||
std::string XmrigUpdater::httpGet(const std::string& url)
|
std::string XmrigUpdater::httpGet(const std::string& url)
|
||||||
{
|
{
|
||||||
CURL* curl = curl_easy_init();
|
return httpGetString(url, "[xmrig-updater]");
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool XmrigUpdater::downloadToFile(const std::string& url, const std::string& destPath)
|
bool XmrigUpdater::downloadToFile(const std::string& url, const std::string& destPath)
|
||||||
{
|
{
|
||||||
FILE* fp = std::fopen(destPath.c_str(), "wb");
|
return httpDownloadToFile(url, destPath, "[xmrig-updater]", kMaxArchiveBytes,
|
||||||
if (!fp) return false;
|
[this](double downloaded, double total) { return onDownloadProgress(downloaded, total); });
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void XmrigUpdater::startCheck(const std::string& installedTag)
|
void XmrigUpdater::startCheck(const std::string& installedTag)
|
||||||
|
|||||||
@@ -13,17 +13,6 @@ namespace wallet {
|
|||||||
|
|
||||||
namespace {
|
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)
|
bool startsWith(const std::string& value, const char* prefix)
|
||||||
{
|
{
|
||||||
const std::string prefixValue(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 trimmedEndpoint(const LiteServerEndpoint& endpoint)
|
||||||
{
|
{
|
||||||
LiteServerEndpoint normalized = endpoint;
|
LiteServerEndpoint normalized = endpoint;
|
||||||
normalized.url = trimCopy(normalized.url);
|
normalized.url = liteTrimCopy(normalized.url);
|
||||||
return normalized;
|
return normalized;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,12 +41,23 @@ std::vector<std::pair<std::size_t, LiteServerEndpoint>> usableConfiguredServers(
|
|||||||
|
|
||||||
std::string normalizedChainName(const std::string& chainName)
|
std::string normalizedChainName(const std::string& chainName)
|
||||||
{
|
{
|
||||||
const std::string normalized = trimCopy(chainName);
|
const std::string normalized = liteTrimCopy(chainName);
|
||||||
return normalized.empty() ? kDragonXLiteChainName : normalized;
|
return normalized.empty() ? kDragonXLiteChainName : normalized;
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace
|
} // 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()
|
std::vector<LiteServerEndpoint> defaultDragonXLiteServers()
|
||||||
{
|
{
|
||||||
return {
|
return {
|
||||||
@@ -83,13 +83,13 @@ LiteConnectionSettings defaultLiteConnectionSettings()
|
|||||||
|
|
||||||
bool isLiteServerUrlUsable(const std::string& serverUrl)
|
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://");
|
return startsWith(normalized, "https://") || startsWith(normalized, "http://");
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isOfficialLiteServer(const std::string& serverUrl)
|
bool isOfficialLiteServer(const std::string& serverUrl)
|
||||||
{
|
{
|
||||||
const std::string url = trimCopy(serverUrl);
|
const std::string url = liteTrimCopy(serverUrl);
|
||||||
for (const auto& s : defaultDragonXLiteServers())
|
for (const auto& s : defaultDragonXLiteServers())
|
||||||
if (s.url == url) return true;
|
if (s.url == url) return true;
|
||||||
return false;
|
return false;
|
||||||
@@ -126,7 +126,7 @@ const char* liteConnectionAvailabilityName(LiteConnectionAvailability availabili
|
|||||||
LiteServerSelectionResult selectLiteServer(const LiteConnectionSettings& settings)
|
LiteServerSelectionResult selectLiteServer(const LiteConnectionSettings& settings)
|
||||||
{
|
{
|
||||||
auto usableServers = usableConfiguredServers(settings);
|
auto usableServers = usableConfiguredServers(settings);
|
||||||
const std::string stickyServerUrl = trimCopy(settings.stickyServerUrl);
|
const std::string stickyServerUrl = liteTrimCopy(settings.stickyServerUrl);
|
||||||
|
|
||||||
if (settings.selectionMode == LiteServerSelectionMode::Sticky &&
|
if (settings.selectionMode == LiteServerSelectionMode::Sticky &&
|
||||||
isLiteServerUrlUsable(stickyServerUrl)) {
|
isLiteServerUrlUsable(stickyServerUrl)) {
|
||||||
|
|||||||
@@ -84,6 +84,8 @@ struct LiteServerHealthCheckResult {
|
|||||||
std::string error;
|
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();
|
std::vector<LiteServerEndpoint> defaultDragonXLiteServers();
|
||||||
LiteConnectionSettings defaultLiteConnectionSettings();
|
LiteConnectionSettings defaultLiteConnectionSettings();
|
||||||
bool isLiteServerUrlUsable(const std::string& serverUrl);
|
bool isLiteServerUrlUsable(const std::string& serverUrl);
|
||||||
|
|||||||
@@ -1,24 +1,12 @@
|
|||||||
#include "wallet/lite_sync_service.h"
|
#include "wallet/lite_sync_service.h"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cctype>
|
|
||||||
#include <limits>
|
#include <limits>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
namespace dragonx::wallet {
|
namespace dragonx::wallet {
|
||||||
namespace {
|
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)
|
std::uint64_t effectiveStagnantThreshold(std::uint64_t threshold)
|
||||||
{
|
{
|
||||||
return threshold == 0 ? 1 : threshold;
|
return threshold == 0 ? 1 : threshold;
|
||||||
@@ -283,7 +271,7 @@ LiteSyncStatusResult LiteSyncService::pollSyncStatus(const LiteSyncStatusRequest
|
|||||||
|
|
||||||
LiteServerSelectionResult LiteSyncService::selectServerForRequest(const std::string& serverUrl) const
|
LiteServerSelectionResult LiteSyncService::selectServerForRequest(const std::string& serverUrl) const
|
||||||
{
|
{
|
||||||
const auto overrideUrl = trimSyncCopy(serverUrl);
|
const auto overrideUrl = liteTrimCopy(serverUrl);
|
||||||
if (!overrideUrl.empty()) {
|
if (!overrideUrl.empty()) {
|
||||||
if (!isLiteServerUrlUsable(overrideUrl)) {
|
if (!isLiteServerUrlUsable(overrideUrl)) {
|
||||||
return LiteServerSelectionResult{false, {}, 0, false, "lite sync server URL is not usable"};
|
return LiteServerSelectionResult{false, {}, 0, false, "lite sync server URL is not usable"};
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
#include "lite_wallet_controller.h"
|
#include "lite_wallet_controller.h"
|
||||||
|
|
||||||
#include "lite_diagnostics.h"
|
#include "lite_diagnostics.h"
|
||||||
|
#include "lite_result_parsers.h"
|
||||||
#include "../data/wallet_state.h"
|
#include "../data/wallet_state.h"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
@@ -695,19 +696,14 @@ std::optional<LiteWalletAppRefreshModel> LiteWalletController::refreshModel()
|
|||||||
if (bridge_) {
|
if (bridge_) {
|
||||||
const auto h = bridge_->execute("height", "");
|
const auto h = bridge_->execute("height", "");
|
||||||
if (h.ok) {
|
if (h.ok) {
|
||||||
try {
|
const auto parsed = parseLiteHeightResponse(h.value);
|
||||||
const auto j = nlohmann::json::parse(h.value);
|
if (parsed.ok && parsed.height.height > 0) {
|
||||||
if (j.is_object() && j.contains("height") && j["height"].is_number_unsigned()) {
|
model.hasSyncStatus = true;
|
||||||
const unsigned long long height = j["height"].get<unsigned long long>();
|
model.sync.walletHeight = parsed.height.height;
|
||||||
if (height > 0) {
|
model.sync.chainHeight = parsed.height.height; // synced: wallet height == chain tip
|
||||||
model.hasSyncStatus = true;
|
model.sync.complete = true;
|
||||||
model.sync.walletHeight = height;
|
model.sync.progress = 1.0;
|
||||||
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 */ }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return model;
|
return model;
|
||||||
|
|||||||
@@ -1,22 +1,10 @@
|
|||||||
#include "wallet/lite_wallet_gateway.h"
|
#include "wallet/lite_wallet_gateway.h"
|
||||||
|
|
||||||
#include <cctype>
|
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
namespace dragonx::wallet {
|
namespace dragonx::wallet {
|
||||||
namespace {
|
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 makeGatewayStatus(WalletBackendState state, std::string message)
|
||||||
{
|
{
|
||||||
WalletBackendStatus status;
|
WalletBackendStatus status;
|
||||||
@@ -262,7 +250,7 @@ LiteWalletRefreshResult LiteWalletGateway::refresh(const LiteWalletRefreshReques
|
|||||||
|
|
||||||
LiteServerSelectionResult LiteWalletGateway::selectServerForRequest(const std::string& serverUrl) const
|
LiteServerSelectionResult LiteWalletGateway::selectServerForRequest(const std::string& serverUrl) const
|
||||||
{
|
{
|
||||||
const auto overrideUrl = trimGatewayCopy(serverUrl);
|
const auto overrideUrl = liteTrimCopy(serverUrl);
|
||||||
if (!overrideUrl.empty()) {
|
if (!overrideUrl.empty()) {
|
||||||
if (!isLiteServerUrlUsable(overrideUrl)) {
|
if (!isLiteServerUrlUsable(overrideUrl)) {
|
||||||
return LiteServerSelectionResult{false, {}, 0, false, "lite gateway server URL is not usable"};
|
return LiteServerSelectionResult{false, {}, 0, false, "lite gateway server URL is not usable"};
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
|
|
||||||
#include "lite_wallet_lifecycle_service.h"
|
#include "lite_wallet_lifecycle_service.h"
|
||||||
|
|
||||||
#include <cctype>
|
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
namespace dragonx {
|
namespace dragonx {
|
||||||
@@ -12,17 +11,6 @@ namespace wallet {
|
|||||||
|
|
||||||
namespace {
|
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)
|
LiteRedactedPrivateData redactedField(LitePrivateDataKind kind, const std::string& value)
|
||||||
{
|
{
|
||||||
return LiteRedactedPrivateData{kind, !value.empty(), redactLitePrivateDataValue(value)};
|
return LiteRedactedPrivateData{kind, !value.empty(), redactLitePrivateDataValue(value)};
|
||||||
@@ -172,7 +160,7 @@ LiteWalletLifecyclePlan LiteWalletLifecycleService::planOpenWallet(
|
|||||||
LiteWalletLifecyclePlan LiteWalletLifecycleService::planRestoreWallet(
|
LiteWalletLifecyclePlan LiteWalletLifecycleService::planRestoreWallet(
|
||||||
const LiteWalletRestoreRequest& request) const
|
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"
|
? "restore seed phrase is required"
|
||||||
: std::string();
|
: std::string();
|
||||||
return makePlan(LiteWalletLifecycleOperation::RestoreFromSeed,
|
return makePlan(LiteWalletLifecycleOperation::RestoreFromSeed,
|
||||||
@@ -220,7 +208,7 @@ LiteWalletLifecycleResult LiteWalletLifecycleService::restoreWallet(
|
|||||||
LiteServerSelectionResult LiteWalletLifecycleService::selectServerForRequest(
|
LiteServerSelectionResult LiteWalletLifecycleService::selectServerForRequest(
|
||||||
const std::string& serverUrl) const
|
const std::string& serverUrl) const
|
||||||
{
|
{
|
||||||
const std::string overrideUrl = trimLifecycleCopy(serverUrl);
|
const std::string overrideUrl = liteTrimCopy(serverUrl);
|
||||||
if (!overrideUrl.empty()) {
|
if (!overrideUrl.empty()) {
|
||||||
if (!isLiteServerUrlUsable(overrideUrl)) {
|
if (!isLiteServerUrlUsable(overrideUrl)) {
|
||||||
return LiteServerSelectionResult{false, {}, 0, false, "lite lifecycle server URL is not usable"};
|
return LiteServerSelectionResult{false, {}, 0, false, "lite lifecycle server URL is not usable"};
|
||||||
|
|||||||
@@ -1,428 +1,7 @@
|
|||||||
#include "lite_wallet_lifecycle_ui_adapter.h"
|
#include "lite_wallet_lifecycle_ui_adapter.h"
|
||||||
|
|
||||||
#include <sstream>
|
// The lite-wallet lifecycle UI adapter is now header-only: it declares the live
|
||||||
#include <utility>
|
// LiteWalletLifecycleUiExecutionInput DTO (and its request-intent / prerequisites
|
||||||
|
// sub-structs) that Settings drives the App-owned controller with. The former
|
||||||
namespace dragonx {
|
// dry-run "readiness" executor and its status/issue reporting were removed as dead
|
||||||
namespace wallet {
|
// scaffolding, leaving no out-of-line definitions in this translation unit.
|
||||||
|
|
||||||
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
|
|
||||||
|
|||||||
@@ -1,53 +1,21 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "lite_wallet_server_selection_adapter.h"
|
#include "lite_wallet_server_selection_adapter.h"
|
||||||
|
#include "lite_wallet_lifecycle_service.h"
|
||||||
|
#include "wallet_capabilities.h"
|
||||||
|
|
||||||
#include "../config/settings.h"
|
#include "../config/settings.h"
|
||||||
|
|
||||||
#include <string>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
namespace dragonx {
|
namespace dragonx {
|
||||||
namespace wallet {
|
namespace wallet {
|
||||||
|
|
||||||
enum class LiteWalletLifecycleUiExecutionStatus {
|
struct LiteWalletLifecycleUiPrerequisites {
|
||||||
ReadyForFutureLifecycleRequest,
|
bool selectedServerDisplayReady = false;
|
||||||
WaitingForLiteBuild,
|
bool lifecycleUiOwnerReady = false;
|
||||||
WaitingForBackendCapability,
|
bool operationConfirmed = false;
|
||||||
WaitingForSettings,
|
bool privateDataRedactionReady = false;
|
||||||
WaitingForPersistedServerSelection,
|
bool syncPlannerFeedReady = false;
|
||||||
WaitingForServerSelection,
|
bool realLifecycleExecutionRequested = false;
|
||||||
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 LiteWalletLifecycleUiRequestIntent {
|
struct LiteWalletLifecycleUiRequestIntent {
|
||||||
@@ -77,59 +45,5 @@ struct LiteWalletLifecycleUiExecutionInput {
|
|||||||
bool sendImportExportExecutionRequested = false;
|
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 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 "lite_wallet_server_selection_adapter.h"
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
#include <cctype>
|
|
||||||
#include <sstream>
|
|
||||||
#include <utility>
|
|
||||||
|
|
||||||
namespace dragonx {
|
namespace dragonx {
|
||||||
namespace wallet {
|
namespace wallet {
|
||||||
|
|
||||||
namespace {
|
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(
|
LiteServerSelectionMode walletModeFromSettings(
|
||||||
config::Settings::LiteServerSelectionPreferenceMode mode)
|
config::Settings::LiteServerSelectionPreferenceMode mode)
|
||||||
{
|
{
|
||||||
@@ -46,146 +29,6 @@ config::Settings::LiteServerSelectionPreferenceMode settingsModeFromWallet(
|
|||||||
return config::Settings::LiteServerSelectionPreferenceMode::Sticky;
|
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
|
} // namespace
|
||||||
|
|
||||||
LiteConnectionSettings liteConnectionSettingsFromAppSettings(const config::Settings& settings)
|
LiteConnectionSettings liteConnectionSettingsFromAppSettings(const config::Settings& settings)
|
||||||
@@ -194,14 +37,14 @@ LiteConnectionSettings liteConnectionSettingsFromAppSettings(const config::Setti
|
|||||||
connectionSettings.servers.clear();
|
connectionSettings.servers.clear();
|
||||||
for (const auto& server : settings.getLiteServers()) {
|
for (const auto& server : settings.getLiteServers()) {
|
||||||
connectionSettings.servers.push_back(LiteServerEndpoint{
|
connectionSettings.servers.push_back(LiteServerEndpoint{
|
||||||
trimCopy(server.url),
|
liteTrimCopy(server.url),
|
||||||
server.label,
|
server.label,
|
||||||
server.enabled
|
server.enabled
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
connectionSettings.selectionMode = walletModeFromSettings(settings.getLiteServerSelectionMode());
|
connectionSettings.selectionMode = walletModeFromSettings(settings.getLiteServerSelectionMode());
|
||||||
connectionSettings.stickyServerUrl = trimCopy(settings.getLiteStickyServerUrl());
|
connectionSettings.stickyServerUrl = liteTrimCopy(settings.getLiteStickyServerUrl());
|
||||||
connectionSettings.chainName = trimCopy(settings.getLiteChainName());
|
connectionSettings.chainName = liteTrimCopy(settings.getLiteChainName());
|
||||||
if (connectionSettings.chainName.empty()) connectionSettings.chainName = kDragonXLiteChainName;
|
if (connectionSettings.chainName.empty()) connectionSettings.chainName = kDragonXLiteChainName;
|
||||||
connectionSettings.randomSelectionSeed = settings.getLiteRandomSelectionSeed();
|
connectionSettings.randomSelectionSeed = settings.getLiteRandomSelectionSeed();
|
||||||
return connectionSettings;
|
return connectionSettings;
|
||||||
@@ -214,238 +57,29 @@ void applyLiteConnectionSettingsToAppSettings(config::Settings& settings,
|
|||||||
servers.reserve(connectionSettings.servers.size());
|
servers.reserve(connectionSettings.servers.size());
|
||||||
for (const auto& server : connectionSettings.servers) {
|
for (const auto& server : connectionSettings.servers) {
|
||||||
servers.push_back(config::Settings::LiteServerPreference{
|
servers.push_back(config::Settings::LiteServerPreference{
|
||||||
trimCopy(server.url),
|
liteTrimCopy(server.url),
|
||||||
server.label,
|
server.label,
|
||||||
server.enabled
|
server.enabled
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
settings.setLiteServerSelectionMode(settingsModeFromWallet(connectionSettings.selectionMode));
|
settings.setLiteServerSelectionMode(settingsModeFromWallet(connectionSettings.selectionMode));
|
||||||
settings.setLiteStickyServerUrl(trimCopy(connectionSettings.stickyServerUrl));
|
settings.setLiteStickyServerUrl(liteTrimCopy(connectionSettings.stickyServerUrl));
|
||||||
settings.setLiteChainName(trimCopy(connectionSettings.chainName).empty()
|
settings.setLiteChainName(liteTrimCopy(connectionSettings.chainName).empty()
|
||||||
? kDragonXLiteChainName
|
? kDragonXLiteChainName
|
||||||
: trimCopy(connectionSettings.chainName));
|
: liteTrimCopy(connectionSettings.chainName));
|
||||||
settings.setLiteRandomSelectionSeed(connectionSettings.randomSelectionSeed);
|
settings.setLiteRandomSelectionSeed(connectionSettings.randomSelectionSeed);
|
||||||
settings.setLiteServers(servers);
|
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)
|
std::string redactLiteServerSelectionValue(const std::string& value)
|
||||||
{
|
{
|
||||||
const std::string trimmed = trimCopy(value);
|
const std::string trimmed = liteTrimCopy(value);
|
||||||
if (trimmed.empty()) return "<empty>";
|
if (trimmed.empty()) return "<empty>";
|
||||||
const auto scheme = trimmed.find("://");
|
const auto scheme = trimmed.find("://");
|
||||||
if (scheme == std::string::npos) return "<redacted>";
|
if (scheme == std::string::npos) return "<redacted>";
|
||||||
return trimmed.substr(0, scheme + 3) + "<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 wallet
|
||||||
} // namespace dragonx
|
} // namespace dragonx
|
||||||
@@ -1,150 +1,19 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "lite_connection_service.h"
|
#include "lite_connection_service.h"
|
||||||
#include "lite_wallet_server_lifecycle_readiness.h"
|
|
||||||
#include "wallet_capabilities.h"
|
#include "wallet_capabilities.h"
|
||||||
|
|
||||||
#include "../config/settings.h"
|
#include "../config/settings.h"
|
||||||
|
|
||||||
#include <cstddef>
|
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
namespace dragonx {
|
namespace dragonx {
|
||||||
namespace wallet {
|
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);
|
LiteConnectionSettings liteConnectionSettingsFromAppSettings(const config::Settings& settings);
|
||||||
void applyLiteConnectionSettingsToAppSettings(config::Settings& settings,
|
void applyLiteConnectionSettingsToAppSettings(config::Settings& settings,
|
||||||
const LiteConnectionSettings& connectionSettings);
|
const LiteConnectionSettings& connectionSettings);
|
||||||
const char* liteWalletServerSelectionUiExecutionStatusName(
|
|
||||||
LiteWalletServerSelectionUiExecutionStatus status);
|
|
||||||
const char* liteWalletServerSelectionUiExecutionIssueName(
|
|
||||||
LiteWalletServerSelectionUiExecutionIssue issue);
|
|
||||||
std::string redactLiteServerSelectionValue(const std::string& value);
|
std::string redactLiteServerSelectionValue(const std::string& value);
|
||||||
|
|
||||||
LiteWalletServerSelectionUiExecutionResult executeLiteWalletServerSelectionUi(
|
|
||||||
config::Settings& settings,
|
|
||||||
const LiteWalletServerSelectionUiExecutionInput& input);
|
|
||||||
|
|
||||||
} // namespace wallet
|
} // namespace wallet
|
||||||
} // namespace dragonx
|
} // namespace dragonx
|
||||||
@@ -40,6 +40,7 @@
|
|||||||
#include "config/settings.h"
|
#include "config/settings.h"
|
||||||
#include "data/wallet_state.h"
|
#include "data/wallet_state.h"
|
||||||
#include "data/portfolio.h"
|
#include "data/portfolio.h"
|
||||||
|
#include "data/market_series.h"
|
||||||
#include "fake_lite_backend.h"
|
#include "fake_lite_backend.h"
|
||||||
|
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
@@ -1051,6 +1052,99 @@ void testNetworkRefreshSnapshotHelpers()
|
|||||||
EXPECT_EQ(snapshot.sendTxids.count("pending-send"), static_cast<size_t>(1));
|
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()
|
void testNetworkRefreshRpcCollectors()
|
||||||
{
|
{
|
||||||
using Refresh = dragonx::services::NetworkRefreshService;
|
using Refresh = dragonx::services::NetworkRefreshService;
|
||||||
@@ -1803,6 +1897,26 @@ void testNetworkRefreshResultModels()
|
|||||||
EXPECT_FALSE(priceHttpParseResult.price.has_value());
|
EXPECT_FALSE(priceHttpParseResult.price.has_value());
|
||||||
EXPECT_EQ(priceHttpParseResult.errorMessage, std::string("Price fetch returned an unrecognized response"));
|
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;
|
Refresh::AddressRefreshResult addresses;
|
||||||
dragonx::AddressInfo zAddr;
|
dragonx::AddressInfo zAddr;
|
||||||
zAddr.address = "zs-refresh";
|
zAddr.address = "zs-refresh";
|
||||||
@@ -2622,6 +2736,74 @@ void testPortfolioHelpers()
|
|||||||
EXPECT_EQ(group[0], std::string("R-t2"));
|
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
|
// Regression: the Market-tab portfolio (and other consumers) read the combined
|
||||||
// WalletState::addresses view, which the full-node refresh path builds from the
|
// WalletState::addresses view, which the full-node refresh path builds from the
|
||||||
// authoritative z/t lists via rebuildAddressList(). Before the fix that rebuild
|
// authoritative z/t lists via rebuildAddressList(). Before the fix that rebuild
|
||||||
@@ -5381,6 +5563,7 @@ int main()
|
|||||||
testRefreshScheduler();
|
testRefreshScheduler();
|
||||||
testNetworkRefreshService();
|
testNetworkRefreshService();
|
||||||
testNetworkRefreshSnapshotHelpers();
|
testNetworkRefreshSnapshotHelpers();
|
||||||
|
testParseDaemonRescanOutput();
|
||||||
testNetworkRefreshRpcCollectors();
|
testNetworkRefreshRpcCollectors();
|
||||||
testNetworkRefreshResultModels();
|
testNetworkRefreshResultModels();
|
||||||
testOperationStatusPollParsing();
|
testOperationStatusPollParsing();
|
||||||
@@ -5393,6 +5576,7 @@ int main()
|
|||||||
testConsoleTextLayout();
|
testConsoleTextLayout();
|
||||||
testConsoleModel();
|
testConsoleModel();
|
||||||
testPortfolioHelpers();
|
testPortfolioHelpers();
|
||||||
|
testMarketSeries();
|
||||||
testWalletStateAddressListRebuild();
|
testWalletStateAddressListRebuild();
|
||||||
testConsoleFoldSpans();
|
testConsoleFoldSpans();
|
||||||
testConsoleScrollController();
|
testConsoleScrollController();
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
#include "chat/chat_protocol.h"
|
#include "chat/chat_fixture_tooling.h"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
|
|||||||
Reference in New Issue
Block a user