362 Commits

Author SHA1 Message Date
ardocrat ee8841590a ci: recursive submodules 2026-06-23 15:42:43 +03:00
ardocrat 20db758bc2 wallet: update to last version, removing separate node module, added ability to finalize tx over tor 2026-06-23 15:35:17 +03:00
ardocrat 3981ebe3ed fix: repost pay tx over tor, do not try to send if tor is starting 2026-06-22 20:05:51 +03:00
ardocrat 161faba9a0 Merge remote-tracking branch 'refs/remotes/jwinterm/feat/auto-tor-reply-on-invoice-scan' 2026-06-22 18:44:01 +03:00
jwinterm 53bc6d3e3e wallet: auto-reply over Tor when scanning an Invoice1 slatepack
Mirrors the existing Standard1 + Send-task flow for the invoice
direction. When a customer scans an Invoice1 slatepack QR with an
embedded sender address (now standard for slatepacks produced by
grin-wallet's issue_invoice_tx), the patched pay() forwards the
sender address through create_slatepack_message, and the
OpenMessage handler — if the wallet's Tor service is running or
starting — pushes the signed Invoice2 to the sender's foreign-api
finalize_tx over Tor. The merchant's wallet finalizes + posts on
their side, so no local finalize/post is needed (cf. the existing
send_tor closure for Standard1 which does need that).

Backward-compatible: if the slatepack has no embedded sender
address (older clients) or the Tor service isn't up, the existing
write-slatepack-to-disk-for-paste-back fallback runs unchanged.
No protocol change, no new dependencies, no new failure modes —
the response slatepack file is always written before the Tor send
is attempted, so a Tor failure mid-flight is recoverable.

Closes the mobile-UX gap that required the customer to manually
copy the response slatepack from the wallet and paste it back to
the merchant's web/storefront interface. With this patch and a
foreign-api listener on the merchant side, scanning a Grin invoice
QR is now a single tap: scan → confirm → done.

send_tor() gains a `finalize: bool` parameter that selects between
the existing receive_tx body (for Standard1 sends) and a new
finalize_tx body (for the invoice-flow case). The same Tor SOCKS
plumbing handles both.

Real-world validation: end-to-end working today on a production
BTCPay deployment (Such Software's btcpayserver-plugin-grin v1.3.5)
— invoice QR scan with a patched build settles a merchant invoice
in ~1 confirmation window with zero customer interaction beyond
the scan.
2026-06-22 11:29:48 -04:00
ardocrat 8524084c47 wallet: ability to specify address for invoice to encrypt slatepack message 2026-06-20 15:12:43 +03:00
ardocrat a91d9016a8 node: ability to launch API, P2P and Stratum at all interfaces with IPv6 support 2026-06-19 14:46:49 +03:00
ardocrat 726a51bd0e tor: update to arti 0.43, do not store secret key, use new hyper to send requests 2026-06-15 14:53:52 +03:00
ardocrat 60d8dc7555 node + wallet: update to latest versions 2026-06-11 10:53:48 +03:00
ardocrat b51a46b943 build: update node and wallet to latest versions 2026-06-04 18:06:32 +03:00
ardocrat 3d1a721f29 node: optimize iterator 2026-05-31 15:57:22 +03:00
ardocrat 176df6f93e wallet: fix tx repost, delete txs with 0 amount, trim message to parse 2026-05-25 16:44:51 +03:00
ardocrat f7287bd9ad tor: create runtime only once 2026-05-25 16:32:13 +03:00
ardocrat 4c4b6cd5dc ui: better check for wallet data emptiness 2026-05-23 18:21:04 +03:00
ardocrat 4aeda9c9dc build: v0.3.6, format code 2026-05-21 00:56:28 +03:00
ardocrat a4eadebef2 wallet: handle iter result 2026-05-21 00:54:02 +03:00
ardocrat 15d1aa1a21 build: git format hook 2026-05-21 00:04:22 +03:00
ardocrat 242a3b9434 node: optimize lmdb iterator, pibd peers fix to use .zip fallback 2026-05-20 20:15:46 +03:00
ardocrat edc1a09b2c tor: remove delay after connection, immediately show service as started after bootstrap, remove unused features 2026-05-20 18:14:06 +03:00
ardocrat f31953f455 fix: recreate send/receive/message modals content on open 2026-05-20 17:43:46 +03:00
ardocrat d573ddedca tor: update to 0.42, add arti to logger 2026-05-18 22:53:11 +03:00
ardocrat 3be6925ff8 node: store blocked peers at memory, rust edition 2021 2026-05-14 23:01:37 +03:00
ardocrat c7abd9cbfa wallet: bigger scan window, include last height into scan batch 2026-05-14 21:40:57 +03:00
ardocrat 512d216fee log: filter, debug by default 2026-05-14 21:39:48 +03:00
ardocrat eaefc58c5a wallet: fix scan 2026-05-13 17:38:07 +03:00
ardocrat 2519e68dd5 ci: build checkout submodules 2026-05-13 17:37:45 +03:00
ardocrat 73c0884f95 ci: macos runner 2026-05-08 00:10:14 +03:00
ardocrat f7b2150228 wallet: parse slatepack message at background thread 2026-05-04 14:10:52 +03:00
ardocrat 03924b5300 build: remove unused cpp flags for android 2026-05-04 01:31:35 +03:00
ardocrat a479189135 tx: show message input after copy/share if finalization is needed 2026-05-03 23:34:45 +03:00
ardocrat e691a7b02d ci: fix changelog, update wix upgradecode on every build 2026-05-03 23:05:26 +03:00
ardocrat f2b79cd70d build: add rustfmt hook and config 2026-05-03 10:05:03 +03:00
ardocrat f20d1ee2c2 node: use git hash for user agent 2026-05-02 23:34:42 +03:00
ardocrat 534c4cc86a node: update user-agent, include last fixes for peers from PRs 2026-05-01 12:50:50 +03:00
ardocrat 558ac034b2 txs: fix save with new lmdb, sort to show new on top 2026-05-01 11:42:35 +03:00
ardocrat 13bf8e830c node: reset data from settings 2026-05-01 02:18:47 +03:00
ardocrat 57f319edfc android: include application mime type 2026-04-30 20:36:42 +03:00
ardocrat 748aebffb6 ci: release runner for version and forgejo release 2026-04-30 20:16:12 +03:00
ardocrat b32085a423 node + wallet: update lmdb 2026-04-30 18:27:27 +03:00
ardocrat a9c65546e3 node: update default dns seeds, setup seeds for testnet on launch 2026-04-30 14:14:51 +03:00
ardocrat b94241b82a wallet: select external connection by default on creation if integrated node is not running 2026-04-30 13:53:44 +03:00
ardocrat 8a1a69b739 node: update seeds 2026-04-24 01:14:16 +03:00
ardocrat 01d17e25ee tor: update webtunnel list 2026-04-24 00:12:22 +03:00
ardocrat cab38097fa build: v0.3.5 2026-04-21 13:13:28 +03:00
ardocrat 0026fc3717 build: fix android no_mangle attributes for rust 2024 2026-04-11 23:12:56 +03:00
ardocrat 0fd04f14a4 wallet: save last scanned block info to save progress on scan interruption 2026-04-11 22:52:43 +03:00
ardocrat 3338f51de5 tor: update to arti 0.41 2026-04-11 00:33:41 +03:00
ardocrat 0fa8963bd2 fix: wallet txs selection, wait starting tor service on send 2026-04-10 15:50:58 +03:00
ardocrat 70bba5d7ce pull_to_refresh: refresh when dragged far enough without release 2026-04-10 15:38:43 +03:00
ardocrat 0bb43e1e5d ui: show loader when fee is calculating 2026-04-10 15:18:23 +03:00
ardocrat fd52757549 build: version 0.3.4 2026-04-10 15:09:41 +03:00
ardocrat 6835bb1909 fix: do not send over tor when service not launched 2026-04-10 00:28:27 +03:00
ardocrat 31bc74529c build: update grin node 2026-04-09 20:56:45 +03:00
ardocrat 8d6943975b gui: glow renderer by default 2026-04-09 02:44:06 +03:00
ardocrat 4c5d8abe7b wix: update uuid 2026-04-09 02:44:01 +03:00
ardocrat 4dc42bce4a tor: fix multiline bridge connection 2026-03-30 01:37:36 +03:00
ardocrat e2d5d92f18 build: version 0.3.3 2026-03-30 01:36:45 +03:00
ardocrat b001eb4712 node: update to last git version (fix pibd stuck) 2026-03-29 21:18:36 +03:00
ardocrat f14bd902ea build: update win package guid 2026-03-24 14:52:20 +03:00
ardocrat 33ab11933a build: update wallet branch 2026-03-24 14:51:12 +03:00
ardocrat 6b05a2177e log: info level into file, crash report for android 2026-03-24 13:18:04 +03:00
ardocrat 7bbe637414 ui: do not show username at ext conn settings 2026-03-24 02:32:53 +03:00
ardocrat 9b6252de3a tor: fix connection with multiple bridges 2026-03-24 02:13:08 +03:00
ardocrat 26debcf51c ui: camera paddings, focus on password at wallet creation modal 2026-03-24 02:06:00 +03:00
ardocrat 497b967fd0 node: scan and share connection with qr code 2026-03-23 04:46:29 +03:00
ardocrat 05e18cf6c4 fix: show tx modal after message parse, cancel tx when slate not found 2026-03-23 02:49:23 +03:00
ardocrat 6e50b2b38a ui: make list items clickable, ability to delete tx 2026-03-23 01:21:09 +03:00
ardocrat 9bc96de398 ci: optimize release upload
- separate job telegram upload to avoid forgejo release upload repeat if failed
- upload artifacts to forgejo from another runner

Reviewed-on: https://code.gri.mw/GUI/grim/pulls/55
2026-03-22 22:14:04 +00:00
ardocrat 5a525c50e1 img: update cover 2026-03-19 10:37:45 +03:00
ardocrat ba0af0968d tor: multiline bridges input, optimize tor connection check, add multiple default webtunnel bridges, fix tx cancel on finalization error 2026-03-18 15:44:32 +03:00
ardocrat a0947aa47c ci: fix pre-release check 2026-03-15 21:18:42 +00:00
ardocrat 06c6b8b4f5 android: fix text input on some devices 2026-03-15 23:26:57 +03:00
ardocrat b19335d0bc build: version 0.3.2 2026-03-15 23:25:46 +03:00
ardocrat 40eb30fb75 macos: fix version 2026-03-15 23:25:42 +03:00
ardocrat 8223e52570 build: version script 2026-03-15 23:25:08 +03:00
ardocrat 875bd11bdb ci: macos universal release name 2026-03-10 02:02:15 +03:00
ardocrat 19e4cb664d ci: fix pre-release check 2026-03-10 01:58:28 +03:00
ardocrat 18bc327a99 build: update msi and android version 2026-03-10 01:43:15 +03:00
ardocrat 88e2fb0715 node: update 5.4.0 release 2026-03-10 01:09:53 +03:00
ardocrat feb38dc7cf ui: show scan and wallet actions before getting data from node 2026-03-10 00:49:18 +03:00
ardocrat 28ecb5b1f4 fix: camera image square crop and animate qr code scanning progress 2026-03-10 00:39:42 +03:00
ardocrat 024a9d0098 ui: make qr codes background lighter for better scanning 2026-03-10 00:06:57 +03:00
ardocrat 59cf46e1cb feat: open modal to send amount on address scan 2026-03-09 21:33:11 +03:00
ardocrat 22255e0f2a fix: close wallet panels when settings are open 2026-03-09 20:55:11 +03:00
ardocrat 7fdb8d272b fix: hide account list panel on wallet change, do not store account list at content 2026-03-09 20:48:00 +03:00
ardocrat d043562058 build: bump version 2026-03-09 20:10:01 +03:00
ardocrat 096788c899 android: open only text files 2026-03-09 11:49:07 +03:00
ardocrat ba914903eb ci: fix git tag check 2026-03-08 23:45:25 +03:00
ardocrat 162c5f88eb fix: android build for status text 2026-03-08 22:53:24 +03:00
ardocrat ae0ff12935 feat: check app updates
Check update using API endpoint: https://code.gri.mw/api/v1/repos/gui/grim/releases/latest

Reviewed-on: https://code.gri.mw/GUI/grim/pulls/54
2026-03-08 19:28:28 +00:00
ardocrat af203b8f9b ui: update i18n lib 2026-03-06 23:51:44 +03:00
ardocrat 1bd57cd88d fix: check transport settings change to restart services 2026-03-06 23:09:17 +03:00
ardocrat 8eea776111 ui: optimize paddings for mobile 2026-03-06 22:26:58 +03:00
ardocrat f5f6141881 ui: txs limit and sort, wallet deletion from the list, fix tor conn on accounts and settings change
- Limit loading at tix list
- Sort txs by confirmation status to show txs waiting for an action at top
- Ability to delete wallet from the list without opening
- Optimize Tor connection on account switch

Reviewed-on: https://code.gri.mw/GUI/grim/pulls/53
2026-03-05 11:48:23 +00:00
ardocrat beb1a80c6a Payment proofs (#52)
- Ability to export and verify payment proofs

Some fixes:
- Migrated tx heights store from lmdb (also changed heights key from local id to slate_id to avoid conflict between wallets)
- Close address panel on wallet change

Reviewed-on: https://code.gri.mw/GUI/grim/pulls/52
2026-03-03 19:54:46 +00:00
ardocrat 2a41689231 fix: wallet data directory creation 2026-03-03 11:31:32 +03:00
ardocrat dda3be7f86 fix: check external connection url format 2026-03-03 09:07:30 +03:00
ardocrat 65e9546f81 tor: reduce service check delay 2026-02-27 23:42:38 +03:00
ardocrat 8f1175ff1a tor: optimize service check 2026-02-27 22:09:47 +03:00
ardocrat 0ca2c7f372 fix: button rounding 2026-02-27 16:20:15 +03:00
ardocrat ee4752a95f Ability to change data location (#50)
Closes https://code.gri.mw/GUI/grim/issues/9
- Change node chain data directory
- Change wallet data directory
- Check for valid bridge Tor binary

Reviewed-on: https://code.gri.mw/GUI/grim/pulls/50
2026-02-27 00:29:55 +00:00
ardocrat 366bbaeac6 gui: glow renderer only for macos 2026-02-27 01:24:44 +03:00
ardocrat 4e1ada3188 github: add windows msi to release 2026-02-26 15:12:28 +00:00
ardocrat a499c91619 fix: do not hang on tor service launch at ui
- Retrieve secret key on wallet sync to prevent lock at UI

Reviewed-on: https://code.gri.mw/GUI/grim/pulls/51
2026-02-26 14:12:39 +00:00
ardocrat 9f2ad32031 wix: update version 2026-02-25 17:18:44 +00:00
ardocrat 431cda358f github: download win msi 2026-02-25 17:14:38 +00:00
ardocrat 149555cc0a ci: windows msi build
- Separate runner for Windows build
- Create .msi release format

Reviewed-on: https://code.gri.mw/GUI/grim/pulls/49
2026-02-25 17:13:32 +00:00
ardocrat 72de1d5c05 conn: add grinffindor.org to wallet connections and seed list 2026-02-21 19:32:26 +03:00
ardocrat b4c64dae6b fix: fonts setup on first draw 2026-02-20 16:53:17 +03:00
ardocrat e334386fe2 fix: disable modal closing on qr scan at messages 2026-02-20 16:53:17 +03:00
ardocrat a03758d383 fix: webtunnel build
- Fix build command execution on non-Windows

Reviewed-on: https://code.gri.mw/GUI/grim/pulls/48
2026-02-20 00:01:28 +00:00
ardocrat 7d75fc2ae0 gui: fixes
- Wgpu renderer by default for Windows
- Fix items borders sizes
- Start camera at  messages on scan press
- Do not show pull-to-refresh on empty tx list
- Do not close non-closeable modal on Back/Esc key press
- Cut long connection name at wallet list
- Fix setting of tx receiver address

Reviewed-on: https://code.gri.mw/GUI/grim/pulls/47
2026-02-19 23:15:24 +00:00
ardocrat a8df3a20ba fix: build on windows
- Added Windows batch file
- Fixed check for empty file on build

Reviewed-on: https://code.gri.mw/GUI/grim/pulls/45
2026-02-19 19:27:08 +00:00
ardocrat 67514b8609 tor: webtunnel support
- Add webtunnel bridge
- Build from https://code.gri.mw/ardocrat/webtunnel to include binary into the build
- Build and run webtunnel for Android

Reviewed-on: https://code.gri.mw/GUI/grim/pulls/44
2026-02-18 13:38:11 +00:00
ardocrat 3a23438e17 fix: check wallet state from node, build: update common deps, tor: optimize running service check, p2p: async peer saving, update seeds
- Update common dependencies
- Optimize check of running Tor service
- Async peer saving
- Add mainnet and testnet seeds
- Remove grinnode.live from default external connections

Reviewed-on: https://code.gri.mw/GUI/grim/pulls/43
2026-02-10 11:54:59 +00:00
ardocrat 86d4fde77d tx: parse message on input change 2026-02-09 13:50:46 +00:00
ardocrat b751aa256e txs: text slatepack format
Reviewed-on: https://code.gri.mw/GUI/grim/pulls/42
2026-02-09 13:14:08 +00:00
ardocrat b2ef91e67d ci: cache for every platform
Reviewed-on: https://code.gri.mw/GUI/grim/pulls/41
2026-02-08 16:24:43 +00:00
ardocrat b54fd3251f feat: calculate fee and maximum amount on send
Reviewed-on: https://code.gri.mw/GUI/grim/pulls/32
2026-02-07 13:11:23 +00:00
ardocrat 9bb5f1d66a camera: update nokhwa to 0.10.10
Update camera lib, also to avoid git dependency for macos.

Reviewed-on: https://code.gri.mw/GUI/grim/pulls/40
2026-02-07 09:10:20 +00:00
ardocrat e56058ff33 ci: fix cargo registry 2026-02-06 16:59:30 +00:00
ardocrat 45473ded7e ci: optimize build
- Optimize jobs dependencies
- Optimize Android and Linux x86 cache
- Telegram link to all releases

Reviewed-on: https://code.gri.mw/GUI/grim/pulls/39
2026-02-06 16:11:36 +00:00
ardocrat 6eec01bad6 ci: linux x86 build, cargo registry
- Build Linux x86 on separate runner (fix Appimage on x86 platform)
- Use Cargo Nexus registry
- Build at single file
- Fix previous release check for tag

Reviewed-on: https://code.gri.mw/GUI/grim/pulls/38
2026-02-06 07:52:15 +00:00
ardocrat 35dbc3eca9 ci: fix workflow vars, artifacts repo name path 2026-01-30 15:34:25 +03:00
ardocrat 94bae256af ci: cache, local maven for android build and artifacts, fix telegram upload
Reviewed-on: https://code.gri.mw/GUI/grim/pulls/37
2026-01-30 11:42:19 +00:00
ardocrat dae59744b3 fix: github release 2026-01-28 18:30:51 +01:00
ardocrat 7d28a31e18 ci: macos universal build 2026-01-26 08:51:39 +03:00
ardocrat 6d5445f72f ci: telegram notify url 2026-01-26 08:38:14 +03:00
ardocrat 04417f1f53 ci: fix telegram upload 2026-01-26 08:30:16 +03:00
ardocrat 97239ba0f5 android: fix manifest permissions 2026-01-26 01:30:30 +03:00
ardocrat 51898404db ci: tg files upload 2026-01-26 01:29:58 +03:00
ardocrat 2ca7c03999 build: fix nokhwa dep path 2026-01-26 01:29:15 +03:00
ardocrat 86187e4e59 ci: github release fix, do not build on master fork, notify group on pr
- Do not build on master branch fork
- Fix Github release build
- Notify group and channel on PR

Reviewed-on: https://code.gri.mw/GUI/grim/pulls/35
2026-01-23 14:00:34 +01:00
ardocrat 7ebfaaf477 ci: separate android runner, github release download, telegram notifications and release upload
Reviewed-on: https://code.gri.mw/GUI/grim/pulls/33
2026-01-23 11:20:15 +01:00
ardocrat 00f8eb7d18 gui: eframe default features, add wgpu dep, try wgpu on windows error
Reviewed-on: https://code.gri.mw/GUI/grim/pulls/34
2026-01-19 21:52:33 +01:00
ardocrat 0713ba0213 gui: wgpu fallback renderer for desktop (#31)
Reviewed-on: https://code.gri.mw/GUI/grim/pulls/31
2026-01-19 21:21:13 +01:00
ardocrat ec81ba2cee readme: update build instruction 2026-01-09 23:54:39 +00:00
ardocrat cc5831358a ci: checkout submodules
Update checkout actions.

Reviewed-on: https://code.gri.mw/GUI/grim/pulls/30
2026-01-09 23:38:08 +00:00
ardocrat 961e65be4c build: grin submodules
Use node and wallet submodules to avoid dependency conflicts inside grin-wallet on grin repo update.

Reviewed-on: https://code.gri.mw/GUI/grim/pulls/29
2026-01-09 23:08:34 +00:00
ardocrat 12b6626624 readme: fix images links
To correctly show on Github.

Reviewed-on: https://code.gri.mw/GUI/grim/pulls/28
2026-01-09 11:07:37 +00:00
ardocrat 03fbb0914e wallet: optimize proxy url parse to use localhost or dns record 2025-11-25 23:49:05 +03:00
ardocrat ed2dc880aa android: migrate back to gameactivity, update to API 36, fix back navigation at network panel 2025-11-21 01:34:51 +03:00
ardocrat 646a7c5e04 fix: tx ui on repost 2025-11-21 01:33:53 +03:00
ardocrat 11a5a73775 sync: increase header size 2025-11-11 00:38:00 +03:00
ardocrat 48ec553e0a build: android version naming 2025-11-11 00:36:49 +03:00
ardocrat a567243716 ci: fix version naming and override 2025-11-11 00:35:26 +03:00
ardocrat 4773fdb8d5 fix: render on linux wayland 2025-11-10 15:59:22 +03:00
ardocrat 7f65471ba1 ui: fix wallet tabs state 2025-11-06 14:01:40 +03:00
ardocrat fabd0a90df build: remove tag symbol from android version 2025-11-06 13:21:16 +03:00
ardocrat 6093d2bddb build: fix android version naming 2025-11-06 13:19:11 +03:00
ardocrat 42bcda621a ui: update egui, fix android back button 2025-11-05 18:11:07 +03:00
ardocrat 215b5d3f27 ci: jobs deps 2025-11-05 18:09:38 +03:00
ardocrat 97d8b86d39 ci: forgejo 2025-11-05 13:26:05 +03:00
ardocrat 8ba11daf31 build: fix lifetime warning 2025-11-03 13:48:34 +03:00
ardocrat fe2f79ecad tor: update arti to 0.36 2025-11-03 13:48:18 +03:00
ardocrat 606072ca3a build: working android release by default 2025-10-27 17:21:37 +03:00
ardocrat 2eef58e23a Update images links 2025-10-22 23:55:17 +03:00
ardocrat cf4f0789a3 build: update egui to last github version 2025-06-25 13:13:31 +03:00
ardocrat 1b78118f51 fix: action repeat tor tx, no resend for tor 2025-06-25 12:51:29 +03:00
ardocrat a89a9bcaed fix: message opening 2025-06-25 11:50:49 +03:00
ardocrat 8528c33be5 fix: message opening when slate with previous state exists 2025-06-25 11:45:26 +03:00
ardocrat d1502e26b1 wallet: cleanup broadcasting delay on repost 2025-06-25 11:32:26 +03:00
ardocrat 2f56defffa wallet: broadcasting delay, repeat tx action 2025-06-25 11:08:57 +03:00
ardocrat 01af084568 build: update grin and tor deps 2025-06-19 09:34:20 +03:00
ardocrat cd0e3485c5 txs: async tasks for wallet 2025-06-19 09:18:20 +03:00
ardocrat b540fcbf19 tor: do not start already starting service 2025-06-11 15:16:33 +03:00
ardocrat 7d29b2af6d tx: qr padding, info buttons positions 2025-06-10 22:25:10 +03:00
ardocrat ad030fe811 fix: tx finalizing status setup 2025-06-10 22:16:51 +03:00
ardocrat fae1364f10 wallet: tx response flag to show sharing controls 2025-06-10 22:03:49 +03:00
ardocrat 93297b5401 tx: do not show sharing content when can not finalize 2025-06-10 21:20:27 +03:00
ardocrat 511611f994 wallet: show only txs with slate id 2025-06-10 20:40:50 +03:00
ardocrat e9e2a0a8e7 ui: fix tx description 2025-06-10 20:25:23 +03:00
ardocrat 1222399926 tx: remove manual slatepack input, scan outputs after wallet db deletion 2025-06-10 20:09:24 +03:00
ardocrat 845c1dc0ea i18n: file 2025-06-10 19:34:35 +03:00
ardocrat 3a21e60e19 ui: do not copy form animated qr 2025-06-10 19:30:36 +03:00
ardocrat 9622429180 build: remove unused module 2025-06-10 19:04:56 +03:00
ardocrat d04b7a4e6a build: update version name 2025-06-09 12:51:07 +03:00
ardocrat 8b369b6049 ui: refactoring of wallet screen, fix colors 2025-06-09 12:34:07 +03:00
ardocrat b54a573f61 tor: proxy settings 2025-06-09 12:27:36 +03:00
ardocrat 184326bfde wallet: open slatepack 2025-06-09 12:23:01 +03:00
ardocrat b1f3c7d42b fix: mnemonic input 2025-06-06 14:29:29 +03:00
ardocrat 53a96e567d wallet: sort accounts to show current first 2025-06-04 15:33:34 +03:00
ardocrat 20daa7b465 network: fix external connections check 2025-06-03 16:11:08 +03:00
ardocrat 0fa2ef4283 qr: smaller text 2025-06-02 21:54:13 +03:00
ardocrat e067a0a900 qr: add max size support, ui copy button 2025-06-02 21:03:49 +03:00
ardocrat 31d8e2f012 eframe: glow renderer 2025-06-02 12:10:41 +03:00
ardocrat 84d385ef1a macos: glow renderer 2025-06-01 23:11:57 +03:00
ardocrat fabef9492e proxy: tls support 2025-06-01 00:05:48 +03:00
ardocrat 92f8386264 http: client, wallet to node communication with proxy 2025-05-31 23:34:51 +03:00
ardocrat 1ef62a806b fix: show word list on wallet creation 2025-05-31 22:34:12 +03:00
ardocrat f8da3d0754 fix: hyper client import 2025-05-31 17:44:37 +03:00
ardocrat 8165fab326 tor: update arti-client to 0.30.0 2025-05-31 17:08:39 +03:00
ardocrat 918c5b4355 build: imports 2025-05-31 15:47:03 +03:00
ardocrat f930cd4ade config: node db path 2025-05-31 15:45:36 +03:00
ardocrat 3f3940e752 ui: remove storage settings 2025-05-31 15:45:15 +03:00
ardocrat 4ef5dd839d platform: pick folder 2025-05-31 15:44:24 +03:00
ardocrat fd14700eae settings: network proxy 2025-05-31 14:12:31 +03:00
ardocrat e5548eb6f1 fix: current locale check at modal 2025-05-31 09:20:46 +03:00
ardocrat a364daf52e ui: network and storage settings modules, language selection modal 2025-05-31 09:11:07 +03:00
ardocrat 7089e6e1b2 ui: update app title 2025-05-31 09:09:01 +03:00
ardocrat 0621154902 ui: remove on_back callback from content container 2025-05-30 22:11:16 +03:00
ardocrat acfb5fec1a ui: wallet content container, accounts panel 2025-05-30 21:25:29 +03:00
ardocrat 1a3df4619e ui: accounts module 2025-05-30 16:13:27 +03:00
ardocrat 8994775be2 fix: keyboard focus 2025-05-30 15:06:47 +03:00
ardocrat 81365dbe6a ui: reset keyboard window state on opening and inputs focus change 2025-05-30 14:48:49 +03:00
ardocrat 7ae63b2b66 fix: modal window focus 2025-05-30 14:14:58 +03:00
ardocrat b8dd5911d4 ui: animate wallet list panels 2025-05-30 13:01:12 +03:00
ardocrat 3fc4ffa179 fix: wallet and mnemonic modals for container 2025-05-30 12:50:33 +03:00
ardocrat b84f6480e7 ui: content container 2025-05-30 12:33:13 +03:00
ardocrat 5dd8de7950 modal: move focus setup to the root content 2025-05-29 23:50:26 +03:00
ardocrat 78baaca4a3 fix: keyboard modal focus 2025-05-29 15:37:00 +03:00
ardocrat e597ac7e4b ui: ability to not show soft keyboard for input, move modal on top only at first draw 2025-05-29 13:32:33 +03:00
ardocrat 4d5cc93a38 ui: settings content at separate panel 2025-05-29 12:56:49 +03:00
ardocrat ed50132d5e keyboard: long press clear 2025-05-29 01:31:12 +03:00
ardocrat fbb084f636 wallet: do not scan outputs for new wallet 2025-05-29 00:38:45 +03:00
ardocrat d42ef102b2 keyboard: layouts for languages 2025-05-28 20:16:23 +03:00
ardocrat 9673c7d719 keyboard: show refactoring 2025-05-28 13:46:44 +03:00
ardocrat 9b4623c558 keyboard: state refactoring 2025-05-28 12:58:57 +03:00
ardocrat b7563e63c1 ui: esc key handling for keyboard without modal 2025-05-28 11:11:22 +03:00
ardocrat 4d4b5eb007 keyboard: optimize buttons 2025-05-28 10:23:17 +03:00
ardocrat 6c04eec026 modal: close refactoring 2025-05-27 22:14:43 +03:00
ardocrat 1ff2b27edc android: release build script 2025-05-27 22:04:55 +03:00
ardocrat 6bce9ec071 android: switch to nativeactivity, fix clicks 2025-05-27 21:01:00 +03:00
ardocrat 98619cc362 ui: update to egui 0.31 2025-05-27 16:10:29 +03:00
ardocrat 1987d0553c ui: numeric keyboard input 2025-05-27 14:28:23 +03:00
ardocrat 3f78095fe3 ui: keyboard language switch 2025-05-27 13:08:32 +03:00
ardocrat 245766e1b5 fix: text width inside input content 2025-05-26 22:37:03 +03:00
ardocrat 2591653f66 ui: input refactoring 2025-05-26 20:48:29 +03:00
ardocrat d11e90226b feat: software keyboard (without language switch) 2025-05-23 19:20:42 +03:00
ardocrat fb159c17a0 i18n: chinese 2025-04-27 21:22:08 +03:00
ardocrat f7eb6580cc tor: trim address on send 2025-04-27 19:51:06 +03:00
ardocrat 43720b34ba fix: external connection deletion 2025-04-23 15:10:48 +03:00
ardocrat f1f0f002ce fix: content redraw at connections 2025-04-23 13:09:31 +03:00
ardocrat 86afa21a60 node: do not remove lock file on cleanup 2025-04-23 12:38:23 +03:00
ardocrat 0169acba81 build: use zig linker for macos and linux for arm on x86 2025-04-02 22:30:59 +03:00
ardocrat 073d950d41 github: disable release build 2025-04-02 21:10:45 +03:00
ardocrat 4eaaebd739 release: v0.2.4 2025-04-02 20:48:58 +03:00
ardocrat a9e2106fda git: ignore cargo parse result file 2025-04-02 20:48:11 +03:00
ardocrat 8b427989c5 github: disable release build 2025-04-02 20:37:47 +03:00
ardocrat f16ce3c69b fix: transparent background on desktop 2025-04-02 20:37:23 +03:00
ardocrat a1b3330e5e async: use tokio for thread block calls 2025-04-02 19:15:20 +03:00
ardocrat 3da8f5420b build: update tor arti 0.29.0 2025-04-02 17:05:20 +03:00
ardocrat 109e896506 tor: clean error after start 2025-04-02 16:47:07 +03:00
ardocrat 8ad38f381e ui: change values on enter press at node settings modals 2025-04-02 15:49:07 +03:00
ardocrat 1e32315346 win: use system window frame 2025-04-02 15:22:15 +03:00
ardocrat ef8c645a6a win: allow downgrade install 2025-04-02 14:32:00 +03:00
ardocrat 15ecdf1e57 build: update guid for win installer 2025-04-02 13:31:04 +03:00
ardocrat 587b00c93a build: version for windows 2025-04-01 00:26:59 +03:00
ardocrat aba2bead27 build: update package info, other dependencies 2025-03-31 21:21:51 +03:00
ardocrat 85ce58f69c fix: parse result from scan on top panel 2025-03-31 20:46:23 +03:00
ardocrat bb7e00b0eb fix: initial color theme setup 2025-03-29 21:52:10 +03:00
ardocrat d60b35ebef Merge pull request 'macos: use nokhwa camera dependency' (#16) from macos_camera_fix into master
Reviewed-on: https://gri.mw/code/code/GUI/grim/pulls/16
2025-03-29 21:36:25 +03:00
ardocrat eb60c52224 macos: use nokhwa camera dependency 2025-03-29 21:18:53 +03:00
ardocrat 61828ea2db build: update tor lib 2025-03-15 20:41:30 +03:00
ardocrat 7e819e14d1 node: fix peers config saving 2025-03-15 20:35:10 +03:00
ardocrat 1d9b7d9698 wallet: do not lock whole balance on send 2025-01-14 17:55:50 +03:00
ardocrat 82c05588bc readme: update title 2025-01-13 21:59:22 +03:00
ardocrat 1cddd05bc0 readme: update img tag 2025-01-13 21:58:29 +03:00
ardocrat 8ad0d1c461 readme: update images 2025-01-13 21:56:48 +03:00
ardocrat a22a75913c img: add grin logo 2025-01-13 21:55:55 +03:00
ardocrat e797da0ed8 img: add cover 2025-01-13 21:26:00 +03:00
ardocrat 6936c14ed2 tor: remove macos tls fix 2025-01-13 21:06:34 +03:00
ardocrat c626ed5a48 tor: clear data on launch, update arti to 0.26.0 2025-01-13 19:40:09 +03:00
ardocrat d79d05ef5a android: debug build without keystore 2025-01-13 16:54:27 +03:00
ardocrat 094a5b8969 release: v0.2.3 2024-10-27 20:12:12 +03:00
ardocrat 12a75f8370 macos: future version update 2024-10-27 19:45:00 +03:00
ardocrat 1c14b9aa93 tx: fix confirmation status for new block, do not show Slatepack message after finalization 2024-10-27 19:02:17 +03:00
ardocrat 8ea388554a github: macos target 11.0 2024-10-27 18:07:22 +03:00
ardocrat 1531c201bb github: macos 12 2024-10-27 00:46:53 +03:00
ardocrat ed522c56ae github: macos zig linker 2024-10-27 00:40:58 +03:00
ardocrat 4b454ab2f3 github: macos last os 2024-10-27 00:29:27 +03:00
ardocrat f6fbf7226e fix: window size saving 2024-10-26 23:54:47 +03:00
ardocrat ebd09ab1c8 camera: update nokhwa, eye for macos, ability to switch camera when another camera not loaded 2024-10-26 23:25:55 +03:00
ardocrat 75cf7edc96 fix: modal padding and window border on desktop 2024-10-26 23:23:39 +03:00
ardocrat 5c8b9c40be build: provide version for android release 2024-10-26 02:17:28 +03:00
ardocrat dcaf9945c8 ui: wgpu renderer for macos, desktop content background fix, do not show left line for camera content at dual panel mode 2024-10-26 02:16:47 +03:00
ardocrat f9426287d5 macos: release on darwin without zig, info.plist camera usage description and version update 2024-10-25 20:03:57 +03:00
ardocrat 77281e3ab9 github: fix macos arm sdk 2024-10-23 00:37:41 +03:00
ardocrat 64439ad3d3 github: fix macos deployment target 2024-10-23 00:11:45 +03:00
ardocrat 9494c1292e github: macos coreutils 2024-10-22 23:47:19 +03:00
ardocrat accf123d49 github: macos build 2024-10-22 23:24:02 +03:00
ardocrat d77598c259 github: fix macos sdk env 2024-10-22 04:39:08 +03:00
ardocrat 4e6dff52fe github: macos install zig 2024-10-22 04:14:21 +03:00
ardocrat 92d0aac250 github: fix macos sdk unzip 2024-10-22 04:08:26 +03:00
ardocrat 5ef310558a release: v0.2.2 2024-10-22 03:51:01 +03:00
ardocrat 683821b667 build: fix version script 2024-10-22 03:50:48 +03:00
ardocrat da4cf71fac github: build macos on linux with SDK 10.15 2024-10-22 03:19:34 +03:00
ardocrat f81ceae940 txs: new block confirmation time 2024-10-22 02:11:25 +03:00
ardocrat fa6301a1db stratum: fix wallet name after selection, do not panic after stop 2024-10-22 00:12:13 +03:00
ardocrat 442fc425f7 ui: update to egui 0.29.1, wallet qr scan content, panels strokes and colors refactoring, check closeable modal at desktop title, fix app socket name 2024-10-21 12:03:09 +03:00
ardocrat ea61588ede build: check android lib result 2024-10-12 19:58:14 +03:00
ardocrat 7f67aa134a build: increment on android development 2024-10-12 15:33:23 +03:00
ardocrat d7d1c53c52 build: incremental release on desktop development 2024-10-12 15:26:01 +03:00
ardocrat 18f52f877a node: remove delay after server start 2024-10-12 15:24:15 +03:00
ardocrat c13195bd61 stratum: prevent crash at connections thread 2024-10-10 21:51:28 +03:00
ardocrat e40d5b6474 node: single function to get api secrets 2024-10-10 21:11:50 +03:00
ardocrat 92e5d38755 build: update grin 5.3.3, arti 0.23.0 (fork arti-hyper crate) and non-egui dependencies 2024-10-09 12:58:59 +03:00
ardocrat ec7e795ba9 build: camera features 2024-10-09 10:13:55 +03:00
ardocrat af220b2a09 camera: remove eye-rs to fix build for mac, horizontally flip image 2024-10-08 23:23:04 +03:00
ardocrat 846e30cb38 app: better panic handling, macos single app instance 2024-10-08 17:11:45 +03:00
ardocrat d371d4368b wallet: disable tor listener by default 2024-10-08 14:59:51 +03:00
ardocrat 85fc8101e4 ui: show tx modal on error if exists 2024-10-08 02:37:51 +03:00
ardocrat e2f58a8938 android: update gradle 2024-10-07 20:55:23 +03:00
ardocrat 7e6954afd9 fix: opened file data providing 2024-10-07 19:45:29 +03:00
ardocrat bed041a1c3 git: ignore android release artifacts 2024-09-21 00:33:46 +03:00
ardocrat f955f720d2 release: v0.2.1 2024-09-20 23:33:08 +03:00
ardocrat b627ac1ca6 fix: mnemonic import 2024-09-20 23:30:41 +03:00
ardocrat ac0b218376 fix: connection selection 2024-09-20 23:12:44 +03:00
ardocrat 04bf5a5349 github: coreutils for macos 2024-09-20 21:46:17 +03:00
ardocrat 9cce52a7d9 github: fix sha256sum 2024-09-20 20:38:05 +03:00
ardocrat 51e0d87d27 github: fix release 2024-09-20 15:17:41 +03:00
ardocrat d6f7e2e976 github: release sha256sum 2024-09-20 15:15:18 +03:00
ardocrat 0bbf395a62 build: android warning fix 2024-09-20 15:03:56 +03:00
ardocrat 609d7ceb7a build: remove panic message dependency 2024-09-20 14:45:40 +03:00
ardocrat b91605864d github: fix macos release 2024-09-20 14:42:37 +03:00
ardocrat 7857b708c9 release: v0.2.0 2024-09-20 14:17:03 +03:00
ardocrat a0f85538e9 ui: tx modal height 2024-09-20 14:09:53 +03:00
ardocrat c52da4f479 wallet: accounts balance calculating optimization, payment proof support on send, selection_strategy_is_use_all 2024-09-20 13:56:25 +03:00
ardocrat af597df7b1 i18n: move confirmation word 2024-09-20 13:49:31 +03:00
ardocrat 2adb29f4ee ui: external connection check and ui repaint fix, tab button callback argument 2024-09-20 13:42:45 +03:00
ardocrat 2b83944f34 ui: show node error status on connection item 2024-09-20 11:10:05 +03:00
ardocrat 71e80f6df7 ui: reset node config from ui on error 2024-09-20 10:58:52 +03:00
ardocrat 0ead11ec6c tx: receiver address 2024-09-20 02:39:06 +03:00
ardocrat 3e249c5314 android: share file type 2024-09-20 00:16:12 +03:00
ardocrat bacc87945c messages: qr scan modal 2024-09-20 00:09:08 +03:00
ardocrat 2cfd428c4c ui: do not clear qr state 2024-09-19 21:39:59 +03:00
ardocrat c155deedb5 wallet: qr scan modal, connections content and default list, wallet creation and list refactoring, tx height 2024-09-19 15:56:53 +03:00
Ardocrat 3bc8c407b4 Merge pull request #13 from ardocrat/slatepack_ext_file
Open .slatepack file with the app
2024-09-16 16:08:27 +00:00
ardocrat c3fae38d5c desktop: open camera check 2024-09-15 15:54:07 +03:00
ardocrat d6ec4213ab ui: ability to finalize tx only when wallet is loaded 2024-09-14 21:21:03 +03:00
ardocrat 150a0de1c4 android: always build with release-apk profile 2024-09-14 21:17:43 +03:00
ardocrat 7cedebc70e ui: qr scan and accounts modals module, parsing messages fix 2024-09-14 21:11:52 +03:00
ardocrat fe5aca6f0e build: remove debug from release profile 2024-09-14 16:08:40 +03:00
ardocrat 5d83710fed ui: dark colors fix 2024-09-14 16:02:20 +03:00
ardocrat 1431e307ee ui: separate wallet accounts modal 2024-09-14 15:21:08 +03:00
ardocrat 1934dc3377 desktop: args text 2024-09-14 15:04:11 +03:00
ardocrat 8af06d8860 build: android fix 2024-09-14 13:07:48 +03:00
ardocrat 9ea0da95b7 build: release sha256sum 2024-09-14 12:12:50 +03:00
ardocrat d39e2ec21e build: android signed release 2024-09-14 02:06:35 +03:00
ardocrat 68c9c9df04 build: local android release 2024-09-14 01:47:06 +03:00
ardocrat 6f7156ef17 github: android secrets 2024-09-13 22:31:28 +03:00
ardocrat 50638ff54e github: android keystore 2024-09-13 22:00:59 +03:00
ardocrat 8594279b98 android: java call result fixes 2024-09-13 21:08:14 +03:00
ardocrat 0205e01b3c build: macos fix 2024-09-13 19:51:33 +03:00
ardocrat 17545c1b7c macos: platform build 2024-09-13 18:57:09 +03:00
ardocrat bcf821c06a macos: initial file type association 2024-09-13 15:21:43 +03:00
ardocrat 34376d3490 build: fix macos 2024-09-13 14:56:04 +03:00
ardocrat 8ed2308340 macos: build, warn fix 2024-09-13 14:53:22 +03:00
ardocrat c73cd58eed platform: android file opening, better exit 2024-09-13 14:22:15 +03:00
ardocrat d78ec570b0 platform: passed data at lib, desktop user attention, check existing file on share at android 2024-09-12 21:27:37 +03:00
ardocrat dd45f7ce38 desktop: platform socket fix, file extension association for windows 2024-09-12 18:02:02 +03:00
ardocrat fb7312cb80 desktop: request window focus on data 2024-09-11 21:13:52 +03:00
ardocrat dbc28205e8 desktop: parse file content from argument on launch, single app instance, wallets selection and opening modals refactoring 2024-09-11 17:01:05 +03:00
ardocrat a3ed3bd234 build: linux release 2024-09-07 12:45:05 +03:00
ardocrat 21ecf200b8 wallet + ui: optimize sync after tx actions, remove tx repost, share message as file from tx modal, show tx info after tor sending and message creation or finalization, messages and transport modules refactoring, qr code text optimization, wallet dandelion setting, recovery phrase modal next step on enter 2024-09-07 00:11:17 +03:00
ardocrat c8bca08bdc txs: share message as file from modal, module refactoring 2024-08-15 23:09:42 +03:00
ardocrat 68bd2b81ec peers: fix config edit and load, default mainnet dnsseed 2024-08-13 02:31:38 +03:00
ardocrat 09cfb84b94 fix: ellipsized sync status text at connections 2024-08-12 18:30:10 +03:00
ardocrat 5c1ffb5636 build: push version 2024-08-10 12:15:40 +03:00
ardocrat 7f79cc0708 release: v0.1.3 2024-08-10 12:08:20 +03:00
ardocrat b0b4f9068a build: version release 2024-08-10 11:59:12 +03:00
ardocrat cb9e86750c mnemonic: words import and errors check refactoring 2024-08-10 02:35:42 +03:00
163 changed files with 35417 additions and 24543 deletions
+71
View File
@@ -0,0 +1,71 @@
name: Test build
on:
push:
tags-ignore:
- "*"
branches-ignore:
- master
- ci
jobs:
build:
runs-on: ubuntu
steps:
- uses: actions/checkout@v6
- name: Checkout submodules
run: |
sed -i 's#https://code\.gri\.mw#${{ secrets.REPO_HOST }}#g' .gitmodules
git submodule update --init --recursive --remote
- name: Check commit
id: check
run: |
git fetch && git checkout master
sha=$(git rev-parse HEAD)
[[ "${{ github.sha }}" == "${sha}" ]] && test=false || test=true
echo "test=${test}" >> "$FORGEJO_OUTPUT"
- name: Restore cargo cache
id: cache-cargo-restore
uses: actions/cache/restore@v5
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ runner.os }}-test-cargo-${{ hashFiles('**/Cargo.lock') }}
- name: Setup registry
run: |
echo -e '[registries.nexus]\nindex = "sparse+${{ secrets.MAVEN_HOST }}/repository/cargo/"\n[registry]\ndefault = "nexus"\n[source.crates-io]\nreplace-with = "nexus"\n[source.nexus]\nregistry = "sparse+${{ secrets.MAVEN_HOST }}/repository/cargo/"' > ~/.cargo/config.toml
- name: Build
if: ${{ steps.check.outputs.test == 'true' }}
run: cargo build --release
- name: Save cargo cache
uses: actions/cache/save@v5
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ steps.cache-cargo-restore.outputs.cache-primary-key }}
- name: Telegram Notify Channel
if: ${{ steps.check.outputs.test == 'true' && (success() || failure()) }}
uses: actions/telegram-notifier@main
with:
api_url: ${{ secrets.TELEGRAM_API_URL }}
token: ${{ secrets.TELEGRAM_BOT_TOKEN }}
chat_id: ${{ secrets.TELEGRAM_CHANNEL_ID }}
status: ${{ job.status }}
notify_fields: "actor,repository,workflow,branch,commit"
- name: Telegram Notify Group
if: ${{ steps.check.outputs.test == 'true' && (success() || failure()) }}
uses: actions/telegram-notifier@main
with:
api_url: ${{ secrets.TELEGRAM_API_URL }}
token: ${{ secrets.TELEGRAM_BOT_TOKEN }}
chat_id: ${{ secrets.TELEGRAM_GROUP_ID }}
status: ${{ job.status }}
notify_fields: "actor,repository,workflow,branch,commit"
+29
View File
@@ -0,0 +1,29 @@
name: Pull Request
on:
pull_request:
types:
- closed
- opened
jobs:
notify:
runs-on: debian-release
steps:
- name: Telegram Notify Channel
uses: actions/telegram-notifier@main
with:
api_url: ${{ secrets.TELEGRAM_API_URL }}
token: ${{ secrets.TELEGRAM_BOT_TOKEN }}
chat_id: ${{ secrets.TELEGRAM_CHANNEL_ID }}
status: ${{ job.status }}
notify_fields: "actor,repository"
- name: Telegram Notify Group
uses: actions/telegram-notifier@main
with:
api_url: ${{ secrets.TELEGRAM_API_URL }}
token: ${{ secrets.TELEGRAM_BOT_TOKEN }}
chat_id: ${{ secrets.TELEGRAM_GROUP_ID }}
status: ${{ job.status }}
notify_fields: "actor,repository"
+474
View File
@@ -0,0 +1,474 @@
name: Release build
on:
push:
branches:
- master
- ci
tags-ignore:
- "*-dev*"
jobs:
version:
runs-on: debian-release
outputs:
v: ${{ steps.version.outputs.v }}
pre: ${{ steps.version.outputs.pre }}
exists: ${{ steps.check.outputs.exists }}
last_tag: ${{ steps.check_prev.outputs.last_tag }}
steps:
- uses: actions/checkout@v6
- name: Checkout submodules
run: |
sed -i 's#https://code\.gri\.mw#${{ secrets.REPO_HOST }}#g' .gitmodules
git submodule update --init --recursive --remote
- name: Get version
id: version
run: |
ver="$(grep -m 1 -Po 'version = "\K[^"]*' Cargo.toml)"
[[ $ver == *"-dev"* ]] && ver=${ver} || ver=${ver}-dev
[[ ${{ forgejo.ref_type }} == 'tag' ]] && app_ver=${{ forgejo.ref_name }} || app_ver=v${ver}
echo "v=${app_ver}" >> "$FORGEJO_OUTPUT"
echo $app_ver
[[ ${{ forgejo.ref_type }} == 'tag' ]] && pre='false' || pre='true'
echo "pre=${pre}" >> "$FORGEJO_OUTPUT"
echo "pre-release: ${pre}"
- name: Check existing release
if: ${{ forgejo.ref_type == 'tag' }}
id: check
run: |
git fetch --tags
dev_sha=$(git rev-parse refs/tags/${{ forgejo.ref_name }}-dev) || :
[[ "$(git show-ref)" == *"${dev_sha}"* ]] && exists='true' || exists='false'
echo "exists=${exists}" >> "$FORGEJO_OUTPUT"
echo ${exists}
mkdir release
- uses: actions/forgejo-release@v2.11.3
if: ${{ forgejo.ref_type == 'tag' && steps.check.outputs.exists == 'true' }}
with:
direction: download
token: ${{ secrets.RELEASE_TOKEN }}
tag: "${{ forgejo.ref_name }}-dev"
release-dir: ./release
- name: Rename files
if: ${{ forgejo.ref_type == 'tag' && steps.check.outputs.exists == 'true' }}
working-directory: release
run: for f in *; do mv "$f" "$(echo "$f" | sed s/-dev-/-/)"; done
- name: Delete dev release
if: ${{ forgejo.ref_type == 'tag' && steps.check.outputs.exists == 'true' }}
uses: actions/delete-release@v1
with:
release_name: "${{ forgejo.ref_name }}-dev"
- name: Check previous release
id: check_prev
run: |
git fetch --tags
[[ ${{ forgejo.ref_type }} == 'tag' ]] && skip=0 || skip=1
last_tag=$(git describe --abbrev=0 --tags $(git rev-list --tags --skip=${skip} --max-count=1)) || true
echo "last_tag=${last_tag}" >> "$FORGEJO_OUTPUT"
- uses: actions/forgejo-release@v2.11.3
if: ${{ forgejo.ref_type == 'tag' && steps.check.outputs.exists == 'true' }}
with:
direction: upload
token: ${{ secrets.RELEASE_TOKEN }}
tag: ${{ forgejo.ref_name }}
override: false
prerelease: false
release-dir: ./release
release-notes: "Full Changelog: [${{ steps.check_prev.outputs.last_tag }}...${{ steps.version.outputs.v }}](https://code.gri.mw/${{ forgejo.repository }}/compare/${{ steps.check_prev.outputs.last_tag }}...${{ steps.version.outputs.v }})"
android:
if: ${{ forgejo.ref_type == 'branch' || needs.version.outputs.exists == 'false' }}
runs-on: macos
needs: version
steps:
- uses: actions/checkout@v6
- name: Checkout submodules
run: |
sed -i -- 's#https://code\.gri\.mw#${{ secrets.REPO_HOST }}#g' .gitmodules
git submodule update --init --recursive --remote
- name: Restore cargo cache
id: cache-cargo-restore
uses: actions/cache/restore@v5
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: grim-android-cargo-${{ hashFiles('**/Cargo.lock') }}
- name: Build libs
run: |
rustup -q update
chmod +x scripts/android.sh && ./scripts/android.sh lib ${{ needs.version.outputs.v }}
- name: Save cargo cache
uses: actions/cache/save@v5
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ steps.cache-cargo-restore.outputs.cache-primary-key }}
- name: Restore gradle cache
id: cache-gradle-restore
uses: actions/cache/restore@v5
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: grim-android-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
- name: Setup build
run: |
chmod +x android/gradlew
echo "${{ secrets.ANDROID_KEYSTORE }}" > release.keystore.txt
base64 -d < release.keystore.txt -o android/keystore
echo "${{ secrets.ANDROID_KEYSTORE_PROPS }}" > release.keystore.props.txt
base64 -d < release.keystore.props.txt -o android/keystore.properties
mkdir ~/.gradle && touch ~/.gradle/gradle.properties
printf "mavenHost=${{ secrets.MAVEN_LOCAL_HOST }}\n" >> ~/.gradle/gradle.properties
printf "mavenUser=${{ secrets.MAVEN_USER }}\n" >> ~/.gradle/gradle.properties
printf "mavenPassword=${{ secrets.MAVEN_PASSWORD }}" >> ~/.gradle/gradle.properties
- name: Release ARMv7+v8 APK
working-directory: android
run: |
jni_path=app/src/main/jniLibs
mv ${jni_path}/x86_64 x86_64
./gradlew assembleCiSignedRelease
apk_path=app/build/outputs/apk/ci/signedRelease/app-ci-signedRelease.apk
name=grim-${{ needs.version.outputs.v }}-android.apk
mv ${apk_path} "${name}"
- name: Checksum ARM APK
working-directory: android
run: |
name=grim-${{ needs.version.outputs.v }}-android.apk
checksum=grim-${{ needs.version.outputs.v }}-android-sha256sum.txt
sha256sum "${name}" > "${checksum}"
- name: Release x86_64 APK
working-directory: android
run: |
./gradlew clean
jni_path=app/src/main/jniLibs
rm -rf ${jni_path}/*
mv x86_64 ${jni_path}
./gradlew assembleCiSignedRelease
apk_path=app/build/outputs/apk/ci/signedRelease/app-ci-signedRelease.apk
name=grim-${{ needs.version.outputs.v }}-android-x86_64.apk
mv ${apk_path} "${name}"
- name: Save gradle cache
uses: actions/cache/save@v5
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ steps.cache-gradle-restore.outputs.cache-primary-key }}
- name: Checksum x86_64 APK
working-directory: android
run: |
name=grim-${{ needs.version.outputs.v }}-android.apk
checksum=grim-${{ needs.version.outputs.v }}-android-x86_64-sha256sum.txt
sha256sum "${name}" > "${checksum}"
- name: Upload artifacts
run: |
mkdir release
mv android/grim* release
tar -czf android.tar.gz release
curl -s -u ${{ secrets.MAVEN_USER }}:${{ secrets.MAVEN_PASSWORD }} --upload-file android.tar.gz ${{ secrets.MAVEN_LOCAL_HOST }}/repository/grim-ci/${{ forgejo.repository }}/android.tar.gz
linux_arm:
if: ${{ forgejo.ref_type == 'branch' || needs.version.outputs.exists == 'false' }}
runs-on: debian-rust-arm
needs: [version]
steps:
- uses: actions/checkout@v6
- name: Checkout submodules
run: |
sed -i 's#https://code\.gri\.mw#${{ secrets.REPO_HOST }}#g' .gitmodules
git submodule update --init --recursive --remote
- name: Restore cargo cache
id: cache-cargo-restore
uses: actions/cache/restore@v5
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: grim-linux-arm-cargo-${{ hashFiles('**/Cargo.lock') }}
- name: Setup registry
run: |
echo -e '[registries.nexus]\nindex = "sparse+${{ secrets.MAVEN_LOCAL_HOST }}/repository/cargo/"\n[registry]\ndefault = "nexus"\n[source.crates-io]\nreplace-with = "nexus"\n[source.nexus]\nregistry = "sparse+${{ secrets.MAVEN_LOCAL_HOST }}/repository/cargo/"' > ~/.cargo/config.toml
- name: Release Linux ARM
run: |
rustup -q update
cargo zigbuild --release --target aarch64-unknown-linux-gnu
- name: Save cargo cache
uses: actions/cache/save@v5
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ steps.cache-cargo-restore.outputs.cache-primary-key }}
- name: Upload artifacts
run: |
curl -s -u ${{ secrets.MAVEN_USER }}:${{ secrets.MAVEN_PASSWORD }} --upload-file target/aarch64-unknown-linux-gnu/release/grim ${{ secrets.MAVEN_LOCAL_HOST }}/repository/grim-ci/${{ forgejo.repository }}/grim-linux-arm
linux_arm_appimage:
if: ${{ forgejo.ref_type == 'branch' || needs.version.outputs.exists == 'false' }}
runs-on: debian-arm
needs: [version, linux_arm]
steps:
- uses: actions/checkout@v6
- name: Download Artifact
run: |
wget ${{ secrets.MAVEN_LOCAL_HOST }}/repository/grim-ci/${{ forgejo.repository }}/grim-linux-arm
- name: AppImage
shell: bash
run: |
mkdir release
chmod +x grim-linux-arm
mv grim-linux-arm linux/Grim.AppDir/AppRun
ARCH=aarch64 appimagetool linux/Grim.AppDir grim-${{ needs.version.outputs.v }}-linux-arm.AppImage
mv grim-${{ needs.version.outputs.v }}-linux-arm.AppImage release/
- name: Checksum AppImage ARM
working-directory: release
run: sha256sum grim-${{ needs.version.outputs.v }}-linux-arm.AppImage > grim-${{ needs.version.outputs.v }}-linux-arm-appimage-sha256sum.txt
- name: Upload artifacts
run: |
tar -czf linux-arm.appimage.tar.gz release
curl -s -u ${{ secrets.MAVEN_USER }}:${{ secrets.MAVEN_PASSWORD }} --upload-file linux-arm.appimage.tar.gz ${{ secrets.MAVEN_LOCAL_HOST }}/repository/grim-ci/${{ forgejo.repository }}/linux-arm.appimage.tar.gz
linux_x86:
if: ${{ forgejo.ref_type == 'branch' || needs.version.outputs.exists == 'false' }}
runs-on: debian-rust-x86_64
needs: [version]
steps:
- uses: actions/checkout@v6
- name: Checkout submodules
run: |
sed -i 's#https://code\.gri\.mw#${{ secrets.REPO_HOST }}#g' .gitmodules
git submodule update --init --recursive --remote
- name: Restore cargo cache
id: cache-cargo-restore
uses: actions/cache/restore@v5
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: grim-linux-arm-cargo-${{ hashFiles('**/Cargo.lock') }}
- name: Setup registry
run: |
echo -e '[registries.nexus]\nindex = "sparse+${{ secrets.MAVEN_LOCAL_HOST }}/repository/cargo/"\n[registry]\ndefault = "nexus"\n[source.crates-io]\nreplace-with = "nexus"\n[source.nexus]\nregistry = "sparse+${{ secrets.MAVEN_LOCAL_HOST }}/repository/cargo/"' > ~/.cargo/config.toml
- name: Release Linux x86
run: |
rustup -q update
cargo zigbuild --release --target x86_64-unknown-linux-gnu
- name: Save cargo cache
uses: actions/cache/save@v5
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ steps.cache-cargo-restore.outputs.cache-primary-key }}
- name: AppImage x86
run: |
mkdir release
mv target/x86_64-unknown-linux-gnu/release/grim linux/Grim.AppDir/AppRun
appimagetool linux/Grim.AppDir grim-${{ needs.version.outputs.v }}-linux-x86_64.AppImage
mv grim-${{ needs.version.outputs.v }}-linux-x86_64.AppImage release/
- name: Checksum AppImage x86
working-directory: release
run: sha256sum grim-${{ needs.version.outputs.v }}-linux-x86_64.AppImage > grim-${{ needs.version.outputs.v }}-linux-x86_64-appimage-sha256sum.txt
- name: Upload artifacts
run: |
tar -czf linux-x86_64.appimage.tar.gz release
curl -s -u ${{ secrets.MAVEN_USER }}:${{ secrets.MAVEN_PASSWORD }} --upload-file linux-x86_64.appimage.tar.gz ${{ secrets.MAVEN_LOCAL_HOST }}/repository/grim-ci/${{ forgejo.repository }}/linux-x86_64.appimage.tar.gz
macos:
if: ${{ forgejo.ref_type == 'branch' || needs.version.outputs.exists == 'false' }}
runs-on: macos
needs: [version, android]
steps:
- uses: actions/checkout@v6
- name: Checkout submodules
run: |
sed -i -- 's#https://code\.gri\.mw#${{ secrets.REPO_HOST }}#g' .gitmodules
git submodule update --init --recursive --remote
- run: mkdir release
- name: Restore cargo cache
id: cache-cargo-restore
uses: actions/cache/restore@v5
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: grim-macos-cargo-${{ hashFiles('**/Cargo.lock') }}
- name: Release MacOS Universal
run: |
rustup -q update
export MACOSX_DEPLOYMENT_TARGET=11.0
cargo build --release --target x86_64-apple-darwin
cargo build --release --target aarch64-apple-darwin
lipo -create -output grim "target/x86_64-apple-darwin/release/grim" "target/aarch64-apple-darwin/release/grim"
mv grim macos/Grim.app/Contents/MacOS
- name: Save cargo cache
uses: actions/cache/save@v5
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ steps.cache-cargo-restore.outputs.cache-primary-key }}
- name: Archive Universal
working-directory: macos
run: |
zip -r grim-${{ needs.version.outputs.v }}-macos-universal.zip Grim.app
mv grim-${{ needs.version.outputs.v }}-macos-universal.zip ../release
- name: Checksum Release Universal
working-directory: release
run: sha256sum grim-${{ needs.version.outputs.v }}-macos-universal.zip > grim-${{ needs.version.outputs.v }}-macos-universal-sha256sum.txt
- name: Upload artifacts
run: |
tar -czf macos.tar.gz release
curl -s -u ${{ secrets.MAVEN_USER }}:${{ secrets.MAVEN_PASSWORD }} --upload-file macos.tar.gz ${{ secrets.MAVEN_LOCAL_HOST }}/repository/grim-ci/${{ forgejo.repository }}/macos.tar.gz
windows:
if: ${{ forgejo.ref_type == 'branch' || needs.version.outputs.exists == 'false' }}
runs-on: windows
needs: [version]
steps:
- uses: actions/checkout@v6
- name: Checkout submodules
run: |
(Get-content .gitmodules) | Foreach-Object {$_ -replace "https://code.gri.mw", "${{ secrets.REPO_HOST }}"} | Set-Content .gitmodules
git submodule update --init --recursive --remote
- name: Update UpgradeCode
shell: powershell
run: |
$guid = [guid]::NewGuid().ToString()
$wix = [xml](Get-Content wix/main.wxs)
$wix.Wix.Product.UpgradeCode = $guid
$wix.Save("wix/main.wxs")
Get-Content wix/main.wxs
- run: mkdir release
- name: Release Windows x86
run: |
rustup -q update
cargo wix -p grim -o grim-${{ needs.version.outputs.v }}-win-x86_64.msi --nocapture
mv grim-${{ needs.version.outputs.v }}-win-x86_64.msi release\
Compress-Archive -Path target\release\grim.exe -DestinationPath grim-${{ needs.version.outputs.v }}-win-x86_64.zip
mv grim-${{ needs.version.outputs.v }}-win-x86_64.zip release\
- name: Checksum Archive x86
working-directory: release
run: |
certutil -hashfile grim-${{ needs.version.outputs.v }}-win-x86_64.msi SHA256 > grim-${{ needs.version.outputs.v }}-win-x86_64-msi-sha256sum.txt
certutil -hashfile grim-${{ needs.version.outputs.v }}-win-x86_64.zip SHA256 > grim-${{ needs.version.outputs.v }}-win-x86_64-sha256sum.txt
- name: Upload artifacts
run: |
tar -czf windows.tar.gz release
Remove-Item alias:curl
curl -s -u ${{ secrets.MAVEN_USER }}:${{ secrets.MAVEN_PASSWORD }} --upload-file windows.tar.gz ${{ secrets.MAVEN_LOCAL_HOST }}/repository/grim-ci/${{ forgejo.repository }}/windows.tar.gz
release:
if: ${{ forgejo.ref_type == 'branch' || needs.version.outputs.exists == 'false' }}
runs-on: debian-release
needs: [version, android, linux_x86, linux_arm_appimage, macos, windows]
steps:
- name: Download All Artifacts
run: |
curl -s -o android.tar.gz -u ${{ secrets.MAVEN_USER }}:${{ secrets.MAVEN_PASSWORD }} ${{ secrets.MAVEN_HOST }}/repository/grim-ci/${{ forgejo.repository }}/android.tar.gz
tar -xzf android.tar.gz
rm android.tar.gz
curl -s -o linux-x86_64.appimage.tar.gz -u ${{ secrets.MAVEN_USER }}:${{ secrets.MAVEN_PASSWORD }} ${{ secrets.MAVEN_HOST }}/repository/grim-ci/${{ forgejo.repository }}/linux-x86_64.appimage.tar.gz
tar -xzf linux-x86_64.appimage.tar.gz
rm linux-x86_64.appimage.tar.gz
curl -s -o linux-arm.appimage.tar.gz -u ${{ secrets.MAVEN_USER }}:${{ secrets.MAVEN_PASSWORD }} ${{ secrets.MAVEN_HOST }}/repository/grim-ci/${{ forgejo.repository }}/linux-arm.appimage.tar.gz
tar -xzf linux-arm.appimage.tar.gz
rm linux-arm.appimage.tar.gz
curl -s -o macos.tar.gz -u ${{ secrets.MAVEN_USER }}:${{ secrets.MAVEN_PASSWORD }} ${{ secrets.MAVEN_HOST }}/repository/grim-ci/${{ forgejo.repository }}/macos.tar.gz
tar -xzf macos.tar.gz
rm macos.tar.gz
curl -s -o windows.tar.gz -u ${{ secrets.MAVEN_USER }}:${{ secrets.MAVEN_PASSWORD }} ${{ secrets.MAVEN_HOST }}/repository/grim-ci/${{ forgejo.repository }}/windows.tar.gz
tar -xzf windows.tar.gz
rm windows.tar.gz
- name: Upload release to Forgejo
uses: actions/forgejo-release@v2.11.3
with:
direction: upload
token: ${{ secrets.RELEASE_TOKEN }}
tag: ${{ needs.version.outputs.v }}
override: true
prerelease: ${{ needs.version.outputs.pre == 'true' }}
release-dir: release
release-notes: "Full Changelog: [${{ needs.version.outputs.last_tag }}...${{ needs.version.outputs.v }}](https://code.gri.mw/${{ forgejo.repository }}/compare/${{ needs.version.outputs.last_tag }}...${{ needs.version.outputs.v }})"
- name: Telegram Notify Channel
if: always()
uses: actions/telegram-notifier@main
with:
api_url: ${{ secrets.TELEGRAM_API_URL }}
token: ${{ secrets.TELEGRAM_BOT_TOKEN }}
chat_id: ${{ secrets.TELEGRAM_CHANNEL_ID }}
status: ${{ job.status }}
notify_fields: "actor,repository,workflow,branch,commit"
- name: Telegram Notify Group
if: always()
uses: actions/telegram-notifier@main
with:
api_url: ${{ secrets.TELEGRAM_API_URL }}
token: ${{ secrets.TELEGRAM_BOT_TOKEN }}
chat_id: ${{ secrets.TELEGRAM_GROUP_ID }}
status: ${{ job.status }}
notify_fields: "actor,repository,workflow,branch,commit"
release-telegram:
runs-on: debian-release
needs: [version, release]
steps:
- name: Download All Artifacts
run: |
mkdir release
cd release
wget ${{ secrets.REPO_HOST }}/${{ forgejo.repository }}/releases/download/${{ needs.version.outputs.v }}/grim-${{ needs.version.outputs.v }}-android.apk
wget ${{ secrets.REPO_HOST }}/${{ forgejo.repository }}/releases/download/${{ needs.version.outputs.v }}/grim-${{ needs.version.outputs.v }}-android-x86_64.apk
wget ${{ secrets.REPO_HOST }}/${{ forgejo.repository }}/releases/download/${{ needs.version.outputs.v }}/grim-${{ needs.version.outputs.v }}-linux-arm.AppImage
wget ${{ secrets.REPO_HOST }}/${{ forgejo.repository }}/releases/download/${{ needs.version.outputs.v }}/grim-${{ needs.version.outputs.v }}-linux-x86_64.AppImage
wget ${{ secrets.REPO_HOST }}/${{ forgejo.repository }}/releases/download/${{ needs.version.outputs.v }}/grim-${{ needs.version.outputs.v }}-macos-universal.zip
wget ${{ secrets.REPO_HOST }}/${{ forgejo.repository }}/releases/download/${{ needs.version.outputs.v }}/grim-${{ needs.version.outputs.v }}-win-x86_64.msi
wget ${{ secrets.REPO_HOST }}/${{ forgejo.repository }}/releases/download/${{ needs.version.outputs.v }}/grim-${{ needs.version.outputs.v }}-win-x86_64.zip
- name: Upload files to Telegram
uses: actions/telegram-send-file@main
with:
api_url: ${{ secrets.TELEGRAM_API_URL }}
chat_ids: |
${{ secrets.TELEGRAM_CHANNEL_ID }}
${{ secrets.TELEGRAM_GROUP_ID }}
body: '🎁 Release <a href="https://code.gri.mw/${{ forgejo.repository }}/releases">${{ needs.version.outputs.v }}</a> is ready!'
token: ${{ secrets.TELEGRAM_BOT_TOKEN }}
pin: true
files: |
release/grim-${{ needs.version.outputs.v }}-android.apk
release/grim-${{ needs.version.outputs.v }}-android-x86_64.apk
release/grim-${{ needs.version.outputs.v }}-linux-arm.AppImage
release/grim-${{ needs.version.outputs.v }}-linux-x86_64.AppImage
release/grim-${{ needs.version.outputs.v }}-macos-universal.zip
release/grim-${{ needs.version.outputs.v }}-win-x86_64.msi
release/grim-${{ needs.version.outputs.v }}-win-x86_64.zip
+23
View File
@@ -0,0 +1,23 @@
#!/bin/bash
HOST=https://code.gri.mw
REPO_NAME=$1
TAG=$2
DOWNLOAD_URL=${HOST}/${REPO_NAME}/releases/download/${TAG}
FILES=( "grim-${TAG}-android.apk" "grim-${TAG}-android-x86_64.apk" "grim-${TAG}-linux-arm.AppImage" "grim-${TAG}-linux-x86_64.AppImage" "grim-${TAG}-macos-universal.zip" "grim-${TAG}-win-x86_64.msi" "grim-${TAG}-win-x86_64.zip" )
# Download release files
for f in "${FILES[@]}"; do
wget -q ${DOWNLOAD_URL}/${f}
echo Downloading ${f}...
while [ ! -f ${f} ]; do
sleep 5
echo Retry ${f}...
wget -q ${DOWNLOAD_URL}/${f}
done
done
# Save release notes
INFO_URL=${HOST}/api/v1/repos/${REPO_NAME}/releases/tags/${TAG}
curl -s "${INFO_URL}" | jq -r '.body' > release_notes.txt
+9 -40
View File
@@ -2,48 +2,13 @@ name: Build
on: [push, pull_request]
jobs:
android:
name: Android Build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
cache: gradle
- name: Setup build
run: |
cargo install cargo-ndk
rustup target add aarch64-linux-android
rustup target add armv7-linux-androideabi
rustup target add x86_64-linux-android
- name: Setup Java build
run: |
chmod +x android/gradlew
echo "${{ secrets.ANDROID_RELEASE_KEYSTORE }}" > release.keystore.asc
gpg -d --passphrase "${{ secrets.ANDROID_RELEASE_SECRET }}" --batch release.keystore.asc > android/keystore
echo -e "storePassword=${{ secrets.ANDROID_PASS }}\nkeyPassword=${{ secrets.ANDROID_PASS }}\nkeyAlias=grim\nstoreFile=../keystore" > android/keystore.properties
- name: Build lib 1/2
continue-on-error: true
run: |
sed -i -e 's/"rlib"/"cdylib","rlib"/g' Cargo.toml
export CPPFLAGS="-DMDB_USE_ROBUST=0" && export CFLAGS="-DMDB_USE_ROBUST=0" && cargo ndk -t arm64-v8a build --profile release-apk
- name: Build lib 2/2
run: |
unset CPPFLAGS && unset CFLAGS && cargo ndk -t arm64-v8a -o android/app/src/main/jniLibs build --profile release-apk
sed -i -e 's/"cdylib","rlib"/"rlib"/g' Cargo.toml
- name: Build APK
working-directory: android
run: |
./gradlew assembleRelease
linux:
name: Linux Build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
submodules: recursive
- name: Release build
run: cargo build --release
@@ -51,7 +16,9 @@ jobs:
name: Windows Build
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
submodules: recursive
- name: Release build
run: cargo build --release
@@ -59,6 +26,8 @@ jobs:
name: MacOS Build
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
submodules: recursive
- name: Release build
run: cargo build --release
+16 -233
View File
@@ -6,243 +6,26 @@ on:
- "v*.*.*"
jobs:
android_release:
name: Android Release
create_release:
name: Create Release
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
cache: gradle
- name: Setup Rust build
run: |
cargo install cargo-ndk
rustup target add aarch64-linux-android
rustup target add armv7-linux-androideabi
rustup target add x86_64-linux-android
- name: Setup Java build
run: |
chmod +x android/gradlew
echo "${{ secrets.ANDROID_RELEASE_KEYSTORE }}" > release.keystore.asc
gpg -d --passphrase "${{ secrets.ANDROID_RELEASE_SECRET }}" --batch release.keystore.asc > android/keystore
echo -e "storePassword=${{ secrets.ANDROID_PASS }}\nkeyPassword=${{ secrets.ANDROID_PASS }}\nkeyAlias=grim\nstoreFile=../keystore" > android/keystore.properties
- name: Build lib ARMv8 1/2
continue-on-error: true
run: |
sed -i -e 's/"rlib"/"cdylib","rlib"/g' Cargo.toml
export CPPFLAGS="-DMDB_USE_ROBUST=0" && export CFLAGS="-DMDB_USE_ROBUST=0" && cargo ndk -t arm64-v8a build --profile release-apk
- name: Build lib ARMv8 2/2
run: |
unset CPPFLAGS && unset CFLAGS && cargo ndk -t arm64-v8a -o android/app/src/main/jniLibs build --profile release-apk
sed -i -e 's/"cdylib","rlib"/"rlib"/g' Cargo.toml
- name: Build lib ARMv7 1/2
continue-on-error: true
run: |
sed -i -e 's/"rlib"/"cdylib","rlib"/g' Cargo.toml
export CPPFLAGS="-DMDB_USE_ROBUST=0" && export CFLAGS="-DMDB_USE_ROBUST=0" && cargo ndk -t armeabi-v7a build --profile release-apk
- name: Build lib ARMv7 2/2
run: |
unset CPPFLAGS && unset CFLAGS && cargo ndk -t armeabi-v7a -o android/app/src/main/jniLibs build --profile release-apk
sed -i -e 's/"cdylib","rlib"/"rlib"/g' Cargo.toml
- name: Build APK ARM
working-directory: android
run: |
rm -rf app/build
./gradlew assembleRelease
mv app/build/outputs/apk/release/app-release.apk grim-${{ github.ref_name }}-android.apk
rm -rf app/src/main/jniLibs/*
- name: Checksum APK ARM
working-directory: android
shell: pwsh
run: get-filehash -algorithm sha256 grim-${{ github.ref_name }}-android.apk | Format-List | Out-String | ForEach-Object { $_.Trim() } > grim-${{ github.ref_name }}-android-sha256sum.txt
- name: Build lib x86 1/2
continue-on-error: true
run: |
sed -i -e 's/"rlib"/"cdylib","rlib"/g' Cargo.toml
export CPPFLAGS="-DMDB_USE_ROBUST=0" && export CFLAGS="-DMDB_USE_ROBUST=0" && cargo ndk -t x86_64 build --profile release-apk
- name: Build lib x86 2/2
run: |
unset CPPFLAGS && unset CFLAGS && cargo ndk -t x86_64 -o android/app/src/main/jniLibs build --profile release-apk
sed -i -e 's/"cdylib","rlib"/"rlib"/g' Cargo.toml
- name: Build APK x86
working-directory: android
run: |
rm -rf app/build
./gradlew assembleRelease
mv app/build/outputs/apk/release/app-release.apk grim-${{ github.ref_name }}-android-x86_64.apk
- name: Checksum APK x86
working-directory: android
shell: pwsh
run: get-filehash -algorithm sha256 grim-${{ github.ref_name }}-android-x86_64.apk | Format-List | Out-String | ForEach-Object { $_.Trim() } > grim-${{ github.ref_name }}-android-x86_64-sha256sum.txt
- name: Download release
run: chmod +x .github/download_release.sh && .github/download_release.sh GUI/grim ${{ github.ref_name }}
- name: Release
uses: softprops/action-gh-release@v1
uses: softprops/action-gh-release@v2
with:
body_path: release_notes.txt
overwrite_files: true
files: |
android/grim-${{ github.ref_name }}-android.apk
android/grim-${{ github.ref_name }}-android-sha256sum.txt
android/grim-${{ github.ref_name }}-android-x86_64.apk
android/grim-${{ github.ref_name }}-android-x86_64-sha256sum.txt
linux_release:
name: Linux Release
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Download appimagetools
run: |
wget https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage
chmod +x appimagetool-x86_64.AppImage
sudo apt install libfuse2
- name: Zig Setup
uses: goto-bus-stop/setup-zig@v2
with:
version: 0.12.1
- name: Install cargo-zigbuild
run: cargo install cargo-zigbuild
- name: Release x86
run: cargo zigbuild --release --target x86_64-unknown-linux-gnu
- name: Release ARM
run: |
rustup target add aarch64-unknown-linux-gnu
cargo zigbuild --release --target aarch64-unknown-linux-gnu
- name: AppImage x86
run: |
cp target/x86_64-unknown-linux-gnu/release/grim linux/Grim.AppDir/AppRun
./appimagetool-x86_64.AppImage linux/Grim.AppDir target/x86_64-unknown-linux-gnu/release/grim-${{ github.ref_name }}-linux-x86_64.AppImage
- name: Checksum AppImage x86
working-directory: target/x86_64-unknown-linux-gnu/release
shell: pwsh
run: get-filehash -algorithm sha256 grim-${{ github.ref_name }}-linux-x86_64.AppImage | Format-List | Out-String | ForEach-Object { $_.Trim() } > grim-${{ github.ref_name }}-linux-x86_64-appimage-sha256sum.txt
- name: AppImage ARM
run: |
cp target/aarch64-unknown-linux-gnu/release/grim linux/Grim.AppDir/AppRun
./appimagetool-x86_64.AppImage linux/Grim.AppDir target/aarch64-unknown-linux-gnu/release/grim-${{ github.ref_name }}-linux-arm.AppImage
- name: Checksum AppImage ARM
working-directory: target/aarch64-unknown-linux-gnu/release
shell: pwsh
run: get-filehash -algorithm sha256 grim-${{ github.ref_name }}-linux-arm.AppImage | Format-List | Out-String | ForEach-Object { $_.Trim() } > grim-${{ github.ref_name }}-linux-arm-appimage-sha256sum.txt
- name: Release
uses: softprops/action-gh-release@v1
with:
files: |
target/x86_64-unknown-linux-gnu/release/grim-${{ github.ref_name }}-linux-x86_64.AppImage
target/x86_64-unknown-linux-gnu/release/grim-${{ github.ref_name }}-linux-x86_64-appimage-sha256sum.txt
target/aarch64-unknown-linux-gnu/release/grim-${{ github.ref_name }}-linux-arm.AppImage
target/aarch64-unknown-linux-gnu/release/grim-${{ github.ref_name }}-linux-arm-appimage-sha256sum.txt
windows_release:
name: Windows Release
runs-on: windows-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Build release
run: cargo build --release
- name: Archive release
uses: vimtor/action-zip@v1
with:
files: target/release/grim.exe
dest: target/release/grim-${{ github.ref_name }}-win-x86_64.zip
- name: Checksum release
working-directory: target/release
shell: pwsh
run: get-filehash -algorithm sha256 grim-${{ github.ref_name }}-win-x86_64.zip | Format-List | Out-String | ForEach-Object { $_.Trim() } > grim-${{ github.ref_name }}-win-x86_64-sha256sum.txt
- name: Install cargo-wix
run: cargo install cargo-wix
- name: Run cargo-wix
run: cargo wix -p grim -o ./target/wix/grim-${{ github.ref_name }}-win-x86_64.msi --nocapture
- name: Checksum msi
working-directory: target/wix
shell: pwsh
run: get-filehash -algorithm sha256 grim-${{ github.ref_name }}-win-x86_64.msi | Format-List | Out-String | ForEach-Object { $_.Trim() } > grim-${{ github.ref_name }}-win-x86_64-msi-sha256sum.txt
- name: Release
uses: softprops/action-gh-release@v1
with:
files: |
target/release/grim-${{ github.ref_name }}-win-x86_64.zip
target/release/grim-${{ github.ref_name }}-win-x86_64-sha256sum.txt
target/wix/grim-${{ github.ref_name }}-win-x86_64.msi
target/wix/grim-${{ github.ref_name }}-win-x86_64-msi-sha256sum.txt
macos_release:
name: MacOS Release
runs-on: macos-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Zig Setup
uses: goto-bus-stop/setup-zig@v2
with:
version: 0.12.1
- name: Install cargo-zigbuild
run: cargo install cargo-zigbuild
- name: Release x86
run: |
rustup target add x86_64-apple-darwin
cargo zigbuild --release --target x86_64-apple-darwin
mkdir macos/Grim.app/Contents/MacOS
yes | cp -rf target/x86_64-apple-darwin/release/grim macos/Grim.app/Contents/MacOS
- name: Archive x86
run: |
cd macos
zip -r grim-${{ github.ref_name }}-macos-x86_64.zip Grim.app
mv grim-${{ github.ref_name }}-macos-x86_64.zip ../target/x86_64-apple-darwin/release
cd ..
- name: Checksum Release x86
working-directory: target/x86_64-apple-darwin/release
shell: pwsh
run: get-filehash -algorithm sha256 grim-${{ github.ref_name }}-macos-x86_64.zip | Format-List | Out-String | ForEach-Object { $_.Trim() } > grim-${{ github.ref_name }}-macos-x86_64-sha256sum.txt
- name: Release ARM
run: |
rustup target add aarch64-apple-darwin
cargo zigbuild --release --target aarch64-apple-darwin
yes | cp -rf target/aarch64-apple-darwin/release/grim macos/Grim.app/Contents/MacOS
- name: Archive ARM
run: |
cd macos
zip -r grim-${{ github.ref_name }}-macos-arm.zip Grim.app
mv grim-${{ github.ref_name }}-macos-arm.zip ../target/aarch64-apple-darwin/release
cd ..
- name: Checksum Release ARM
working-directory: target/aarch64-apple-darwin/release
shell: pwsh
run: get-filehash -algorithm sha256 grim-${{ github.ref_name }}-macos-arm.zip | Format-List | Out-String | ForEach-Object { $_.Trim() } > grim-${{ github.ref_name }}-macos-arm-sha256sum.txt
- name: Release Universal
run: |
rustup target add aarch64-apple-darwin
rustup target add x86_64-apple-darwin
cargo zigbuild --release --target universal2-apple-darwin
yes | cp -rf target/universal2-apple-darwin/release/grim macos/Grim.app/Contents/MacOS
- name: Archive Universal
run: |
cd macos
zip -r grim-${{ github.ref_name }}-macos-universal.zip Grim.app
mv grim-${{ github.ref_name }}-macos-universal.zip ../target/universal2-apple-darwin/release
cd ..
- name: Checksum Release Universal
working-directory: target/universal2-apple-darwin/release
shell: pwsh
run: get-filehash -algorithm sha256 grim-${{ github.ref_name }}-macos-universal.zip | Format-List | Out-String | ForEach-Object { $_.Trim() } > grim-${{ github.ref_name }}-macos-universal-sha256sum.txt
- name: Release
uses: softprops/action-gh-release@v1
with:
files: |
target/x86_64-apple-darwin/release/grim-${{ github.ref_name }}-macos-x86_64.zip
target/x86_64-apple-darwin/release/grim-${{ github.ref_name }}-macos-x86_64-sha256sum.txt
target/aarch64-apple-darwin/release/grim-${{ github.ref_name }}-macos-arm.zip
target/aarch64-apple-darwin/release/grim-${{ github.ref_name }}-macos-arm-sha256sum.txt
target/universal2-apple-darwin/release/grim-${{ github.ref_name }}-macos-universal.zip
target/universal2-apple-darwin/release/grim-${{ github.ref_name }}-macos-universal-sha256sum.txt
grim-${{ github.ref_name }}-android.apk
grim-${{ github.ref_name }}-android-x86_64.apk
grim-${{ github.ref_name }}-linux-arm.AppImage
grim-${{ github.ref_name }}-linux-x86_64.AppImage
grim-${{ github.ref_name }}-macos-universal.zip
grim-${{ github.ref_name }}-win-x86_64.msi
grim-${{ github.ref_name }}-win-x86_64.zip
+170
View File
@@ -0,0 +1,170 @@
name: Release
on:
push:
tags:
- "v*.*.*"
jobs:
linux_release:
name: Linux Release
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Download appimagetools
run: |
wget https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage
chmod +x appimagetool-x86_64.AppImage
sudo apt install libfuse2
- name: Zig Setup
uses: goto-bus-stop/setup-zig@v2
with:
version: 0.12.1
- name: Install cargo-zigbuild
run: cargo install cargo-zigbuild
- name: Release x86
run: cargo zigbuild --release --target x86_64-unknown-linux-gnu
- name: Release ARM
run: |
rustup target add aarch64-unknown-linux-gnu
cargo zigbuild --release --target aarch64-unknown-linux-gnu
- name: AppImage x86
run: |
cp target/x86_64-unknown-linux-gnu/release/grim linux/Grim.AppDir/AppRun
./appimagetool-x86_64.AppImage linux/Grim.AppDir target/x86_64-unknown-linux-gnu/release/grim-${{ github.ref_name }}-linux-x86_64.AppImage
- name: Checksum AppImage x86
working-directory: target/x86_64-unknown-linux-gnu/release
shell: bash
run: sha256sum grim-${{ github.ref_name }}-linux-x86_64.AppImage > grim-${{ github.ref_name }}-linux-x86_64-appimage-sha256sum.txt
- name: AppImage ARM
run: |
cp target/aarch64-unknown-linux-gnu/release/grim linux/Grim.AppDir/AppRun
./appimagetool-x86_64.AppImage linux/Grim.AppDir target/aarch64-unknown-linux-gnu/release/grim-${{ github.ref_name }}-linux-arm.AppImage
- name: Checksum AppImage ARM
working-directory: target/aarch64-unknown-linux-gnu/release
shell: bash
run: sha256sum grim-${{ github.ref_name }}-linux-arm.AppImage > grim-${{ github.ref_name }}-linux-arm-appimage-sha256sum.txt
- name: Release
uses: softprops/action-gh-release@v1
with:
files: |
target/x86_64-unknown-linux-gnu/release/grim-${{ github.ref_name }}-linux-x86_64.AppImage
target/x86_64-unknown-linux-gnu/release/grim-${{ github.ref_name }}-linux-x86_64-appimage-sha256sum.txt
target/aarch64-unknown-linux-gnu/release/grim-${{ github.ref_name }}-linux-arm.AppImage
target/aarch64-unknown-linux-gnu/release/grim-${{ github.ref_name }}-linux-arm-appimage-sha256sum.txt
windows_release:
name: Windows Release
runs-on: windows-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Build release
run: cargo build --release
- name: Archive release
uses: vimtor/action-zip@v1
with:
files: target/release/grim.exe
dest: target/release/grim-${{ github.ref_name }}-win-x86_64.zip
- name: Checksum release
working-directory: target/release
shell: bash
run: sha256sum grim-${{ github.ref_name }}-win-x86_64.zip > grim-${{ github.ref_name }}-win-x86_64-sha256sum.txt
- name: Install cargo-wix
run: cargo install cargo-wix
- name: Run cargo-wix
run: cargo wix -p grim -o ./target/wix/grim-${{ github.ref_name }}-win-x86_64.msi --nocapture
- name: Checksum msi
working-directory: target/wix
shell: bash
run: sha256sum grim-${{ github.ref_name }}-win-x86_64.msi > grim-${{ github.ref_name }}-win-x86_64-msi-sha256sum.txt
- name: Release
uses: softprops/action-gh-release@v1
with:
files: |
target/release/grim-${{ github.ref_name }}-win-x86_64.zip
target/release/grim-${{ github.ref_name }}-win-x86_64-sha256sum.txt
target/wix/grim-${{ github.ref_name }}-win-x86_64.msi
target/wix/grim-${{ github.ref_name }}-win-x86_64-msi-sha256sum.txt
macos_release:
name: MacOS Release
runs-on: macos-12
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install coreutils
run: brew install coreutils
- name: Zig Setup
uses: goto-bus-stop/setup-zig@v2
with:
version: 0.12.1
- name: Install cargo-zigbuild
run: cargo install cargo-zigbuild
- name: Download SDK
run: wget https://github.com/phracker/MacOSX-SDKs/releases/download/11.3/MacOSX11.0.sdk.tar.xz
- name: Setup SDK env
run: tar xf ${{ github.workspace }}/MacOSX11.0.sdk.tar.xz && echo "SDKROOT=${{ github.workspace }}/MacOSX11.0.sdk" >> $GITHUB_ENV
- name: Setup platform env
run: echo "MACOSX_DEPLOYMENT_TARGET=11.0" >> $GITHUB_ENV
- name: Release x86
run: |
rustup target add x86_64-apple-darwin
cargo zigbuild --release --target x86_64-apple-darwin
yes | cp -rf target/x86_64-apple-darwin/release/grim macos/Grim.app/Contents/MacOS
- name: Archive x86
run: |
cd macos
zip -r grim-${{ github.ref_name }}-macos-x86_64.zip Grim.app
mv grim-${{ github.ref_name }}-macos-x86_64.zip ../target/x86_64-apple-darwin/release
cd ..
- name: Checksum Release x86
working-directory: target/x86_64-apple-darwin/release
shell: bash
run: sha256sum grim-${{ github.ref_name }}-macos-x86_64.zip > grim-${{ github.ref_name }}-macos-x86_64-sha256sum.txt
- name: Release ARM
run: |
rustup target add aarch64-apple-darwin
cargo zigbuild --release --target aarch64-apple-darwin
yes | cp -rf target/aarch64-apple-darwin/release/grim macos/Grim.app/Contents/MacOS
- name: Archive ARM
run: |
cd macos
zip -r grim-${{ github.ref_name }}-macos-arm.zip Grim.app
mv grim-${{ github.ref_name }}-macos-arm.zip ../target/aarch64-apple-darwin/release
cd ..
- name: Checksum Release ARM
working-directory: target/aarch64-apple-darwin/release
shell: bash
run: sha256sum grim-${{ github.ref_name }}-macos-arm.zip > grim-${{ github.ref_name }}-macos-arm-sha256sum.txt
- name: Release Universal
run: |
cargo zigbuild --release --target universal2-apple-darwin
yes | cp -rf target/universal2-apple-darwin/release/grim macos/Grim.app/Contents/MacOS
- name: Archive Universal
run: |
cd macos
zip -r grim-${{ github.ref_name }}-macos-universal.zip Grim.app
mv grim-${{ github.ref_name }}-macos-universal.zip ../target/universal2-apple-darwin/release
cd ..
- name: Checksum Release Universal
working-directory: target/universal2-apple-darwin/release
shell: pwsh
run: sha256sum grim-${{ github.ref_name }}-macos-universal.zip > grim-${{ github.ref_name }}-macos-universal-sha256sum.txt
- name: Release
uses: softprops/action-gh-release@v1
with:
files: |
target/x86_64-apple-darwin/release/grim-${{ github.ref_name }}-macos-x86_64.zip
target/x86_64-apple-darwin/release/grim-${{ github.ref_name }}-macos-x86_64-sha256sum.txt
target/aarch64-apple-darwin/release/grim-${{ github.ref_name }}-macos-arm.zip
target/aarch64-apple-darwin/release/grim-${{ github.ref_name }}-macos-arm-sha256sum.txt
target/universal2-apple-darwin/release/grim-${{ github.ref_name }}-macos-universal.zip
target/universal2-apple-darwin/release/grim-${{ github.ref_name }}-macos-universal-sha256sum.txt
+6 -2
View File
@@ -1,9 +1,13 @@
*.iml
android/build
android/.idea
android/.gradle
android/local.properties
android/keystore
android/keystore.asc
android/keystore.properties
android/*.apk
android/*sha256sum.txt
/.idea
.DS_Store
/captures
@@ -13,7 +17,7 @@ android/keystore.properties
target
.cargo/
app/src/main/jniLibs
macos/Grim.app/Contents/MacOS/grim
macos/cert.pem
linux/Grim.AppDir/AppRun
.intentionally-empty-file.o
.intentionally-empty-file.o
Cargo.toml-e
+8
View File
@@ -0,0 +1,8 @@
[submodule "wallet"]
path = wallet
url = https://code.gri.mw/ardocrat/wallet
branch = grim-staging
[submodule "tor/webtunnel"]
path = tor/webtunnel
url = https://code.gri.mw/WEB/webtunnel
branch = grim
+50
View File
@@ -0,0 +1,50 @@
#!/usr/bin/env bash
# Copyright 2026 The Grim Developers
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
rustfmt --version &>/dev/null
if [ $? != 0 ]; then
printf "[pre_commit] \033[0;31merror\033[0m: \"rustfmt\" not available. \n"
printf "[pre_commit] \033[0;31merror\033[0m: rustfmt can be installed via - \n"
printf "[pre_commit] $ rustup component add rustfmt-preview \n"
exit 1
fi
problem_files=()
# first collect all the files that need reformatting
for file in $(git diff --name-only --cached); do
if [ ${file: -3} == ".rs" ]; then
rustfmt --check $file &>/dev/null
if [ $? != 0 ]; then
problem_files+=($file)
fi
fi
done
if [ ${#problem_files[@]} == 0 ]; then
# nothing to do
printf "[pre_commit] rustfmt \033[0;32mok\033[0m \n"
else
# reformat the files that need it and re-stage them.
printf "[pre_commit] the following files were rustfmt'd before commit: \n"
for file in ${problem_files[@]}; do
rustfmt $file
git add $file
printf "\033[0;32m $file\033[0m \n"
done
fi
exit 0
Generated
+5423 -3874
View File
File diff suppressed because it is too large Load Diff
+85 -84
View File
@@ -1,12 +1,13 @@
[package]
name = "grim"
version = "0.1.2"
authors = ["Ardocrat <ardocrat@proton.me>"]
version = "0.3.6"
authors = ["Ardocrat <ardocrat@gri.mw>"]
description = "Cross-platform GUI for Grin with focus on usability and availability to be used by anyone, anywhere."
license = "Apache-2.0"
repository = "https://github.com/ardocrat/grim"
repository = "https://code.gri.mw/GUI/grim"
keywords = [ "crypto", "grin", "mimblewimble" ]
edition = "2021"
edition = "2024"
build = "build.rs"
[[bin]]
name = "grim"
@@ -16,9 +17,6 @@ path = "src/main.rs"
name="grim"
crate-type = ["rlib"]
[profile.release]
debug = 1
[profile.release-apk]
inherits = "release"
strip = true
@@ -28,108 +26,111 @@ codegen-units = 1
panic = "abort"
[dependencies]
log = "0.4"
log = "0.4.27"
## node
openssl-sys = { version = "0.9.82", features = ["vendored"] }
grin_api = "5.3.1"
grin_chain = "5.3.1"
grin_config = "5.3.1"
grin_core = "5.3.1"
grin_p2p = "5.3.1"
grin_servers = "5.3.1"
grin_keychain = "5.3.1"
grin_util = "5.3.1"
# node
grin_api = { path = "wallet/grin/api" }
grin_chain = { path = "wallet/grin/chain" }
grin_config = { path = "wallet/grin/config" }
grin_core = { path = "wallet/grin/core" }
grin_p2p = { path = "wallet/grin/p2p" }
grin_servers = { path = "wallet/grin/servers" }
grin_keychain = { path = "wallet/grin/keychain" }
grin_util = { path = "wallet/grin/util" }
## wallet
grin_wallet_impls = "5.3.1"
grin_wallet_api = "5.3.1"
grin_wallet_libwallet = "5.3.1"
grin_wallet_util = "5.3.1"
grin_wallet_controller = "5.3.1"
# wallet
grin_wallet_impls = { path = "wallet/impls" }
grin_wallet_api = { path = "wallet/api"}
grin_wallet_libwallet = { path = "wallet/libwallet" }
grin_wallet_util = { path = "wallet/util" }
grin_wallet_controller = { path = "wallet/controller" }
## ui
egui = { version = "0.28.1", default-features = false }
egui_extras = { version = "0.28.1", features = ["image", "svg"] }
rust-i18n = "2.3.1"
egui = { version = "0.33.3", default-features = false }
egui_extras = { version = "0.33.3", features = ["image", "svg"] }
egui-async = "0.3.4"
rust-i18n = "3.1.5"
## other
backtrace = "0.3"
panic-message = "0.3.0"
thiserror = "1.0.58"
futures = "0.3"
dirs = "5.0.1"
sys-locale = "0.3.0"
chrono = "0.4.31"
parking_lot = "0.12.1"
lazy_static = "1.4.0"
toml = "0.8.2"
serde = "1.0.170"
local-ip-address = "0.6.1"
url = "2.4.0"
rand = "0.8.5"
serde_derive = "1.0.197"
serde_json = "1.0.115"
tokio = { version = "1.37.0", features = ["full"] }
image = "0.25.1"
rqrr = "0.7.1"
log4rs = "1.4.0"
backtrace = "0.3.76"
futures = "0.3.31"
dirs = "6.0.0"
sys-locale = "0.3.2"
chrono = "0.4.43"
parking_lot = "0.12.3"
lazy_static = "1.5.0"
toml = "0.9.11+spec-1.1.0"
serde = "1.0.228"
local-ip-address = "0.6.9"
url = "2.5.8"
rand = "0.9.2"
serde_derive = "1.0.228"
serde_json = "1.0.149"
tokio = { version = "1.49.0", features = ["full"] }
image = "0.25.9"
rqrr = "0.10.1"
qrcodegen = "1.8.0"
qrcode = "0.14.0"
qrcode = "0.14.1"
ur = "0.4.1"
gif = "0.13.1"
rkv = { version = "0.19.0", features = ["lmdb"] }
gif = "0.14.1"
rkv = "0.20.0"
usvg = "0.45.1"
ring = "0.16.20"
hyper = { version = "1.6.0", features = ["full"], package = "hyper" }
hyper-util = { version = "0.1.19", features = ["http1", "client", "client-legacy"] }
http-body-util = "0.1.3"
bytes = "1.11.0"
hyper-socks2 = "0.9.1"
hyper-proxy2 = "0.1.0"
hyper-tls = "0.6.0"
async-std = "1.13.2"
uuid = { version = "0.8.2", features = ["v4"] }
num-bigint = "0.4.6"
## tor
arti-client = { version = "0.19.0", features = ["pt-client", "static", "onion-service-service", "onion-service-client"] }
tor-rtcompat = { version = "0.19.0", features = ["static"] }
tor-config = "0.19.0"
fs-mistrust = "0.7.9"
tor-hsservice = "0.19.0"
tor-hsrproxy = "0.19.0"
tor-keymgr = "0.19.0"
tor-llcrypto = "0.19.0"
tor-hscrypto = "0.19.0"
arti-hyper = "0.19.0"
sha2 = "0.10.0"
arti-client = { version = "0.43.0", features = ["static", "pt-client", "onion-service-service", "onion-service-client"] }
tor-rtcompat = { version = "0.43.0", features = ["static"] }
tor-config = "0.43.0"
fs-mistrust = "0.14.2"
tor-hsservice = "0.43.0"
tor-hsrproxy = "0.43.0"
tor-keymgr = "0.43.0"
tor-llcrypto = "0.43.0"
tor-hscrypto = "0.43.0"
sha2 = "0.10.8"
ed25519-dalek = "2.1.1"
curve25519-dalek = "4.1.2"
hyper = { version = "0.14.28", features = ["full"] }
hyper-tls = "0.5.0"
tls-api = "0.9.0"
tls-api-native-tls = "0.9.0"
curve25519-dalek = "4.1.3"
safelog = "0.8.1"
## stratum server
tokio-old = {version = "0.2", features = ["full"], package = "tokio" }
tokio-old = { version = "0.2", features = ["full"], package = "tokio" }
tokio-util-old = { version = "0.2", features = ["codec"], package = "tokio-util" }
[target.'cfg(all(not(target_os = "windows"), not(target_os = "android")))'.dependencies]
eye = { version = "0.5.0", default-features = false }
[target.'cfg(target_os = "linux")'.dependencies]
nokhwa = { version = "0.10.10", default-features = false, features = ["input-v4l"] }
[target.'cfg(target_os = "windows")'.dependencies]
nokhwa = { version = "0.10.4", default-features = false, features = ["input-msmf"] }
nokhwa = { version = "0.10.10", default-features = false, features = ["input-msmf"] }
[target.'cfg(target_os = "macos")'.dependencies]
tls-api-openssl = "0.9.0"
openpnp_capture_sys = "0.4.0"
nokhwa = { version = "0.10.10", default-features = false, features = ["input-avfoundation", "output-threaded"] }
[target.'cfg(not(target_os = "android"))'.dependencies]
env_logger = "0.11.3"
winit = { version = "0.29.15" }
eframe = { version = "0.28.1", features = ["wgpu", "glow"] }
winit = { version = "0.30.12" }
wgpu = { version = "27.0.1" }
eframe = { version = "0.33.2", features = ["wgpu"] }
arboard = "3.2.0"
rfd = "0.14.1"
dark-light = "1.1.1"
rfd = "0.17.2"
interprocess = { version = "2.2.1", features = ["tokio"] }
[target.'cfg(target_os = "android")'.dependencies]
android_logger = "0.13.1"
android_logger = "0.15.0"
jni = "0.21.1"
android-activity = { version = "0.6.0", features = ["game-activity"] }
wgpu = "0.20.1"
winit = { version = "0.29.15", features = ["android-game-activity"] }
eframe = { version = "0.28.1", features = ["wgpu", "android-game-activity"] }
winit = { version = "0.30.12", features = ["android-game-activity"] }
eframe = { version = "0.33.2", default-features = false, features = ["glow", "android-game-activity"] }
[patch.crates-io]
### patch grin store
#grin_store = { path = "../grin-store" }
### fix cross-compilation support for macos
openpnp_capture_sys = { git = "https://github.com/ardocrat/openpnp-capture-rs", branch = "cross_compilation_support" }
[build-dependencies]
built = "0.8.0"
+4 -3
View File
@@ -1,11 +1,11 @@
# <img height="22" src="https://github.com/ardocrat/grim/blob/master/android/app/src/main/ic_launcher-playstore.png?raw=true"> Grim <img height="20" src="https://github.com/mimblewimble/site/blob/master/assets/images/grin-logo.png?raw=true"> <img height="20" src="https://github.com/ardocrat/grim/blob/master/img/logo.png?raw=true">
# Grim <img height="20" src="img/grin-logo.png"/> <img height="20" src="img/logo.png"/>
Cross-platform GUI for [GRiN ツ](https://grin.mw) in [Rust](https://www.rust-lang.org/)
for maximum compatibility with original [Mimblewimble](https://github.com/mimblewimble/grin) implementation.
Initially supported platforms are Linux, Mac, Windows, limited Android and possible web support with help of [egui](https://github.com/emilk/egui) - immediate mode GUI library in pure Rust.
Named by the character [Grim](http://harrypotter.wikia.com/wiki/Grim) - the shape of a large, black, menacing, spectral giant dog.
![image](https://github.com/user-attachments/assets/a925b1c8-02c9-4b08-b888-0315d11138b6)
![image](img/cover.png)
## Build instructions
@@ -20,6 +20,7 @@ Follow instructions on [Windows](https://forge.rust-lang.org/infra/other-install
To build and run application go to project directory and run:
```
git submodule update --init --recursive
cargo build --release
./target/release/grim
```
@@ -31,7 +32,7 @@ Install Android SDK / NDK / Platform Tools for your OS according to this [FAQ](h
#### Build the project
Run Android emulator or connect a real device. Command `adb devices` should show at least one device.
In the root of the repo run `./scripts/build_run_android.sh debug|release v7|v8`, where is `v7`, `v8` - device CPU architecture.
In the root of the repo run `./scripts/android.sh build|release v7|v8|x86`, where is `v7`, `v8`, `x86` - device CPU architecture for `build` type, for `release` specify version number in format `major.minor.patch`.
## License
+77 -24
View File
@@ -2,36 +2,49 @@ plugins {
id 'com.android.application'
}
def keystorePropertiesFile = rootProject.file("keystore.properties")
def keystoreProperties = new Properties()
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
android {
compileSdk 33
ndkVersion '26.0.10792818'
compileSdk = 36
ndkVersion '29.0.14206865'
buildToolsVersion = '36.1.0'
defaultConfig {
applicationId "mw.gri.android"
minSdk 24
targetSdk 33
versionCode 3
versionName "0.1.2"
targetSdk 36
versionCode 5
versionName "0.3.6"
}
signingConfigs {
release {
keyAlias keystoreProperties['keyAlias']
keyPassword keystoreProperties['keyPassword']
storeFile file(keystoreProperties['storeFile'])
storePassword keystoreProperties['storePassword']
lint {
checkReleaseBuilds false
}
def keystorePropertiesFile = rootProject.file("keystore.properties")
def keystoreProperties = new Properties()
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
signingConfigs {
release {
keyAlias keystoreProperties['keyAlias']
keyPassword keystoreProperties['keyPassword']
storeFile file(keystoreProperties['storeFile'])
storePassword keystoreProperties['storePassword']
}
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.release
}
if (keystorePropertiesFile.exists()) {
signedRelease {
initWith release
signingConfig signingConfigs.release
}
}
debug {
minifyEnabled false
@@ -39,21 +52,61 @@ android {
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
namespace 'mw.gri.android'
flavorDimensions "mode"
productFlavors {
ci {
dimension "mode"
}
local {
dimension "mode"
}
}
applicationVariants.all { variant ->
def flavor = variant.productFlavors[0].name
if (flavor == "ci") {
repositories {
maven {
credentials {
username "$mavenUser"
password "$mavenPassword"
}
url "$mavenHost/repository/maven-central/"
allowInsecureProtocol = true
}
maven {
credentials {
username "$mavenUser"
password "$mavenPassword"
}
url "$mavenHost/repository/google-android-maven/"
allowInsecureProtocol = true
}
}
} else if (flavor == "local") {
repositories {
google()
mavenCentral()
}
}
}
}
dependencies {
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'androidx.appcompat:appcompat:1.7.0'
// To use the Games Activity library
implementation "androidx.games:games-activity:2.0.2"
// Android Camera
implementation 'androidx.camera:camera-core:1.2.3'
implementation 'androidx.camera:camera-camera2:1.2.3'
implementation 'androidx.camera:camera-lifecycle:1.2.3'
}
implementation 'androidx.camera:camera-core:1.5.1'
implementation 'androidx.camera:camera-camera2:1.5.1'
implementation 'androidx.camera:camera-lifecycle:1.5.1'
}
+41 -23
View File
@@ -1,52 +1,70 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
>
<manifest xmlns:tools="http://schemas.android.com/tools" xmlns:android="http://schemas.android.com/apk/res/android">
<uses-feature android:name="android.hardware.camera" android:required="false"/>
<uses-permission android:name="android.permission.EXPAND_STATUS_BAR" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" tools:ignore="ScopedStorage"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" tools:ignore="ScopedStorage"/>
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" tools:ignore="ScopedStorage"/>
<application
android:hardwareAccelerated="true"
android:largeHeap="true"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="Grim"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Main">
android:hardwareAccelerated="true"
android:largeHeap="true"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="Grim"
android:roundIcon="@mipmap/ic_launcher_round"
android:theme="@style/Theme.Main"
android:enableOnBackInvokedCallback="false"
android:extractNativeLibs="true"
tools:ignore="UnusedAttribute">
<receiver android:name=".NotificationActionsReceiver"/>
<provider
android:name=".FileProvider"
android:authorities="mw.gri.android.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
android:name=".FileProvider"
android:authorities="mw.gri.android.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/paths" />
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/paths" />
</provider>
<activity
android:launchMode="singleTask"
android:name=".MainActivity"
android:configChanges="orientation|screenSize"
android:exported="true">
android:launchMode="singleTask"
android:name=".MainActivity"
android:configChanges="orientation|screenSize"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter android:scheme="http" tools:ignore="AppLinkUrlError">
<action android:name="android.intent.action.SEND" />
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/*" />
<data android:mimeType="application/*" />
<data android:pathPattern=".*\\.slatepack" />
</intent-filter>
<meta-data android:name="android.app.lib_name" android:value="grim" />
</activity>
<service android:name=".BackgroundService" android:stopWithTask="true" />
<service
android:name=".BackgroundService"
android:stopWithTask="true"
android:foregroundServiceType="dataSync" />
</application>
</manifest>
@@ -2,13 +2,13 @@ package mw.gri.android;
import android.annotation.SuppressLint;
import android.app.*;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.*;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;
import androidx.core.content.ContextCompat;
import java.util.List;
@@ -16,7 +16,7 @@ import static android.app.Notification.EXTRA_NOTIFICATION_ID;
public class BackgroundService extends Service {
private static final String TAG = BackgroundService.class.getSimpleName();
private PowerManager.WakeLock mWakeLock;
private final Handler mHandler = new Handler(Looper.getMainLooper());
@@ -31,26 +31,6 @@ public class BackgroundService extends Service {
public static final String ACTION_START_NODE = "start_node";
public static final String ACTION_STOP_NODE = "stop_node";
public static final String ACTION_EXIT = "exit";
public static final String ACTION_REFRESH = "refresh";
public static final String ACTION_STOP = "stop";
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@SuppressLint("RestrictedApi")
@Override
public void onReceive(Context context, Intent intent) {
if (intent.getAction().equals(ACTION_STOP)) {
mStopped = true;
// Remove actions buttons.
mNotificationBuilder.mActions.clear();
NotificationManager manager = getSystemService(NotificationManager.class);
manager.notify(NOTIFICATION_ID, mNotificationBuilder.build());
} else {
mHandler.removeCallbacks(mUpdateSyncStatus);
mHandler.post(mUpdateSyncStatus);
}
}
};
private final Runnable mUpdateSyncStatus = new Runnable() {
@SuppressLint("RestrictedApi")
@@ -101,18 +81,6 @@ public class BackgroundService extends Service {
.getBroadcast(BackgroundService.this, 1, startStopIntent, PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_ONE_SHOT);
mNotificationBuilder.addAction(R.drawable.ic_stop, getStopText(), i);
}
// Set up a button to exit from the app.
if (canStart || canStop) {
Intent exitIntent = new Intent(BackgroundService.this, NotificationActionsReceiver.class);
if (Build.VERSION.SDK_INT > 25) {
exitIntent.putExtra(EXTRA_NOTIFICATION_ID, NOTIFICATION_ID);
}
exitIntent.setAction(ACTION_EXIT);
PendingIntent i = PendingIntent
.getBroadcast(BackgroundService.this, 1, exitIntent, PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_ONE_SHOT);
mNotificationBuilder.addAction(R.drawable.ic_close, getExitText(), i);
}
}
// Update notification.
@@ -152,13 +120,17 @@ public class BackgroundService extends Service {
// Show notification with sync status.
Intent i = getPackageManager().getLaunchIntentForPackage(this.getPackageName());
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, i, PendingIntent.FLAG_IMMUTABLE);
mNotificationBuilder = new NotificationCompat.Builder(this, TAG)
.setContentTitle(this.getSyncTitle())
.setContentText(this.getSyncStatusText())
.setStyle(new NotificationCompat.BigTextStyle().bigText(this.getSyncStatusText()))
.setSmallIcon(R.drawable.ic_stat_name)
.setPriority(NotificationCompat.PRIORITY_MAX)
.setContentIntent(pendingIntent);
try {
mNotificationBuilder = new NotificationCompat.Builder(this, TAG)
.setContentTitle(this.getSyncTitle())
.setContentText(this.getSyncStatusText())
.setStyle(new NotificationCompat.BigTextStyle().bigText(this.getSyncStatusText()))
.setSmallIcon(R.drawable.ic_stat_name)
.setPriority(NotificationCompat.PRIORITY_MAX)
.setContentIntent(pendingIntent);
} catch (UnsatisfiedLinkError e) {
return;
}
Notification notification = mNotificationBuilder.build();
// Start service at foreground state to prevent killing by system.
@@ -166,9 +138,6 @@ public class BackgroundService extends Service {
// Update sync status at notification.
mHandler.post(mUpdateSyncStatus);
// Register receiver to refresh notifications by intent.
registerReceiver(mReceiver, new IntentFilter(ACTION_REFRESH));
}
@Override
@@ -199,7 +168,6 @@ public class BackgroundService extends Service {
// Stop updating the notification.
mHandler.removeCallbacks(mUpdateSyncStatus);
unregisterReceiver(mReceiver);
clearNotification();
// Remove service from foreground state.
@@ -222,12 +190,12 @@ public class BackgroundService extends Service {
}
// Start the service.
public static void start(Context context) {
if (!isServiceRunning(context)) {
public static void start(Context c) {
if (!isServiceRunning(c)) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(new Intent(context, BackgroundService.class));
ContextCompat.startForegroundService(c, new Intent(c, BackgroundService.class));
} else {
context.startService(new Intent(context, BackgroundService.class));
c.startService(new Intent(c, BackgroundService.class));
}
}
}
@@ -266,9 +234,6 @@ public class BackgroundService extends Service {
// Check if stop node is possible.
private native boolean canStopNode();
// Get exit text for notification.
private native String getExitText();
// Check if app from the app is needed after node stop.
private native boolean exitAppAfterNodeStop();
}
}
@@ -7,15 +7,15 @@ import android.content.*;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.*;
import android.os.Process;
import android.provider.Settings;
import android.system.ErrnoException;
import android.system.Os;
import android.util.Size;
import android.util.Log;
import android.view.KeyEvent;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull;
@@ -28,9 +28,11 @@ import androidx.core.view.DisplayCutoutCompat;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import com.google.androidgamesdk.GameActivity;
import com.google.androidgamesdk.gametextinput.State;
import com.google.common.util.concurrent.ListenableFuture;
import java.io.*;
import java.util.Objects;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@@ -50,15 +52,13 @@ public class MainActivity extends GameActivity {
private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context ctx, Intent i) {
if (i.getAction().equals(STOP_APP_ACTION)) {
onExit();
Process.killProcess(Process.myPid());
if (Objects.equals(i.getAction(), STOP_APP_ACTION)) {
exit();
}
}
};
private final ImageAnalysis mImageAnalysis = new ImageAnalysis.Builder()
.setTargetResolution(new Size(640, 480))
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build();
@@ -67,21 +67,30 @@ public class MainActivity extends GameActivity {
private ExecutorService mCameraExecutor = null;
private boolean mUseBackCamera = true;
private ActivityResultLauncher<Intent> mFilePickResultLauncher = null;
private ActivityResultLauncher<Intent> mFilePickResult = null;
private ActivityResultLauncher<Intent> mOpenFilePermissionsResult = null;
@SuppressLint("UnspecifiedRegisterReceiverFlag")
@Override
protected void onCreate(Bundle savedInstanceState) {
// Check if activity was launched to exclude from recent apps on exit.
if ((getIntent().getFlags() & Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS) != 0) {
super.onCreate(null);
finish();
return;
}
// Clear cache on start.
if (savedInstanceState == null) {
if (savedInstanceState == null && getExternalCacheDir() != null) {
Utils.deleteDirectoryContent(new File(getExternalCacheDir().getPath()), false);
}
// Setup environment variables for native code.
try {
Os.setenv("HOME", getExternalFilesDir("").getPath(), true);
Os.setenv("XDG_CACHE_HOME", getExternalCacheDir().getPath(), true);
Os.setenv("HOME", Objects.requireNonNull(getExternalFilesDir("")).getPath(), true);
Os.setenv("XDG_CACHE_HOME", Objects.requireNonNull(getExternalCacheDir()).getPath(), true);
Os.setenv("ARTI_FS_DISABLE_PERMISSION_CHECKS", "true", true);
Os.setenv("NATIVE_LIBS_DIR", getApplicationInfo().nativeLibraryDir, true);
} catch (ErrnoException e) {
throw new RuntimeException(e);
}
@@ -89,17 +98,30 @@ public class MainActivity extends GameActivity {
super.onCreate(null);
// Register receiver to finish activity from the BackgroundService.
registerReceiver(mBroadcastReceiver, new IntentFilter(STOP_APP_ACTION));
ContextCompat.registerReceiver(this, mBroadcastReceiver, new IntentFilter(STOP_APP_ACTION), ContextCompat.RECEIVER_NOT_EXPORTED);
// Register file pick result launcher.
mFilePickResultLauncher = registerForActivityResult(
// Register associated file opening result.
mOpenFilePermissionsResult = registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(),
result -> {
if (Build.VERSION.SDK_INT >= 30) {
if (Environment.isExternalStorageManager()) {
onFile();
}
} else if (result.getResultCode() == RESULT_OK) {
onFile();
}
}
);
// Register file pick result.
mFilePickResult = registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(),
result -> {
int resultCode = result.getResultCode();
Intent data = result.getData();
if (resultCode == Activity.RESULT_OK) {
String path = "";
if (data != null) {
if (data != null && data.getData() != null) {
Uri uri = data.getData();
String name = "pick" + Utils.getFileExtension(uri, this);
File file = new File(getExternalCacheDir(), name);
@@ -107,11 +129,13 @@ public class MainActivity extends GameActivity {
OutputStream os = new FileOutputStream(file)) {
byte[] buffer = new byte[1024];
int length;
while ((length = is.read(buffer)) > 0) {
while (true) {
assert is != null;
if (!((length = is.read(buffer)) > 0)) break;
os.write(buffer, 0, length);
}
} catch (Exception e) {
e.printStackTrace();
Log.e("grim", e.toString());
}
path = file.getPath();
}
@@ -124,7 +148,7 @@ public class MainActivity extends GameActivity {
// Listener for display insets (cutouts) to pass values into native code.
View content = getWindow().getDecorView().findViewById(android.R.id.content);
ViewCompat.setOnApplyWindowInsetsListener(content, (v, insets) -> {
// Setup cutouts values.
// Get display cutouts.
DisplayCutoutCompat dc = insets.getDisplayCutout();
int cutoutTop = 0;
int cutoutRight = 0;
@@ -140,7 +164,7 @@ public class MainActivity extends GameActivity {
// Get display insets.
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
// Setup values to pass into native code.
// Pass values into native code.
int[] values = new int[]{0, 0, 0, 0};
values[0] = Utils.pxToDp(Integer.max(cutoutTop, systemBars.top), this);
values[1] = Utils.pxToDp(Integer.max(cutoutRight, systemBars.right), this);
@@ -166,8 +190,65 @@ public class MainActivity extends GameActivity {
BackgroundService.start(this);
}
});
// Check if intent has data on launch.
if (savedInstanceState == null) {
onNewIntent(getIntent());
}
}
// Pass display insets into native code.
public native void onDisplayInsets(int[] cutouts);
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
String action = intent.getAction();
// Check if file was open with the application.
if (action != null && action.equals(Intent.ACTION_VIEW)) {
Intent i = getIntent();
i.setData(intent.getData());
setIntent(i);
onFile();
}
}
// Callback when associated file was open.
private void onFile() {
Uri data = getIntent().getData();
if (data == null) {
return;
}
if (Build.VERSION.SDK_INT >= 30) {
if (!Environment.isExternalStorageManager()) {
Intent i = new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION);
mOpenFilePermissionsResult.launch(i);
return;
}
}
try {
ParcelFileDescriptor parcelFile = getContentResolver().openFileDescriptor(data, "r");
assert parcelFile != null;
FileReader fileReader = new FileReader(parcelFile.getFileDescriptor());
BufferedReader reader = new BufferedReader(fileReader);
String line;
StringBuilder buff = new StringBuilder();
while ((line = reader.readLine()) != null) {
buff.append(line);
}
reader.close();
fileReader.close();
// Provide file content into native code.
onData(buff.toString());
} catch (Exception e) {
Log.e("grim", e.toString());
}
}
// Pass data into native code.
public native void onData(String data);
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
@@ -184,65 +265,110 @@ public class MainActivity extends GameActivity {
if (results.length != 0 && results[0] == PackageManager.PERMISSION_GRANTED) {
switch (requestCode) {
case NOTIFICATIONS_PERMISSION_CODE: {
// Start notification service.
BackgroundService.start(this);
return;
}
case CAMERA_PERMISSION_CODE: {
// Start camera.
startCamera();
}
}
}
}
@Override
protected void onTextInputEventNative(long l, State state) {
super.onTextInputEventNative(l, state);
if (state.selectionEnd > state.composingRegionStart && state.composingRegionStart >= 0) {
String input = String.valueOf(state.text.charAt(state.composingRegionStart));
if (input.contains("\n")) {
onEnterInput();
} else {
onTextInput(input);
}
}
}
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
// To support non-english input.
if (event.getAction() == KeyEvent.ACTION_MULTIPLE && event.getKeyCode() == KeyEvent.KEYCODE_UNKNOWN) {
if (!event.getCharacters().isEmpty()) {
onInput(event.getCharacters());
if (event.getAction() == KeyEvent.ACTION_DOWN) {
if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
onBack();
return false;
} else if (event.getKeyCode() == KeyEvent.KEYCODE_DEL) {
onClearInput();
return false;
} else if (event.getKeyCode() == KeyEvent.KEYCODE_ENTER) {
onEnterInput();
return false;
} else if (event.getKeyCode() == KeyEvent.KEYCODE_0) {
onTextInput("0");
return false;
} else if (event.getKeyCode() == KeyEvent.KEYCODE_1) {
onTextInput("1");
return false;
} else if (event.getKeyCode() == KeyEvent.KEYCODE_2) {
onTextInput("2");
return false;
} else if (event.getKeyCode() == KeyEvent.KEYCODE_3) {
onTextInput("3");
return false;
} else if (event.getKeyCode() == KeyEvent.KEYCODE_4) {
onTextInput("4");
return false;
} else if (event.getKeyCode() == KeyEvent.KEYCODE_5) {
onTextInput("5");
return false;
} else if (event.getKeyCode() == KeyEvent.KEYCODE_6) {
onTextInput("6");
return false;
} else if (event.getKeyCode() == KeyEvent.KEYCODE_7) {
onTextInput("7");
return false;
} else if (event.getKeyCode() == KeyEvent.KEYCODE_8) {
onTextInput("8");
return false;
} else if (event.getKeyCode() == KeyEvent.KEYCODE_9) {
onTextInput("9");
return false;
}
// Pass any other input values into native code.
} else if (event.getAction() == KeyEvent.ACTION_MULTIPLE && event.getKeyCode() == KeyEvent.KEYCODE_UNKNOWN) {
if (!event.getCharacters().isEmpty()) {
onTextInput(event.getCharacters());
return false;
}
// Pass any other input values into native code.
} else if (event.getAction() == KeyEvent.ACTION_UP &&
event.getKeyCode() != KeyEvent.KEYCODE_ENTER &&
event.getKeyCode() != KeyEvent.KEYCODE_BACK) {
onInput(String.valueOf((char)event.getUnicodeChar()));
onTextInput(String.valueOf((char)event.getUnicodeChar()));
return false;
}
return super.dispatchKeyEvent(event);
}
// Provide last entered character from soft keyboard into native code.
public native void onInput(String character);
// Implemented into native code to handle display insets change.
native void onDisplayInsets(int[] cutouts);
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK) {
onBack();
return true;
}
return super.onKeyDown(keyCode, event);
}
// Implemented into native code to handle key code BACK event.
// Pass back navigation event into native code.
public native void onBack();
// Actions on app exit.
private void onExit() {
unregisterReceiver(mBroadcastReceiver);
BackgroundService.stop(this);
// Pass clear key event into native code.
public native void onClearInput();
// Pass enter key event into native code.
public native void onEnterInput();
// Pass last entered character from soft keyboard into native code.
public native void onTextInput(String character);
// Called from native code to exit app.
public void exit() {
finishAndRemoveTask();
}
@Override
protected void onDestroy() {
onExit();
unregisterReceiver(mBroadcastReceiver);
BackgroundService.stop(this);
// Kill process after 3 seconds if app was terminated from recent apps to prevent app hanging.
// Kill process after 3 secs if app was terminated from recent apps to prevent app hang.
new Thread(() -> {
try {
onTermination();
@@ -253,9 +379,7 @@ public class MainActivity extends GameActivity {
}
}).start();
// Destroy an app and kill process.
super.onDestroy();
Process.killProcess(Process.myPid());
}
// Notify native code to stop activity (e.g. node) if app was terminated from recent apps.
@@ -271,45 +395,33 @@ public class MainActivity extends GameActivity {
// Called from native code to get text from clipboard.
public String pasteText() {
ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
String text;
ClipDescription desc = clipboard.getPrimaryClipDescription();
ClipData data = clipboard.getPrimaryClip();
String text = "";
if (!(clipboard.hasPrimaryClip())) {
text = "";
} else if (!(clipboard.getPrimaryClipDescription().hasMimeType(MIMETYPE_TEXT_PLAIN))
&& !(clipboard.getPrimaryClipDescription().hasMimeType(MIMETYPE_TEXT_HTML))) {
} else if (desc != null && (!(desc.hasMimeType(MIMETYPE_TEXT_PLAIN))
&& !(desc.hasMimeType(MIMETYPE_TEXT_HTML)))) {
text = "";
} else {
ClipData.Item item = clipboard.getPrimaryClip().getItemAt(0);
} else if (data != null) {
ClipData.Item item = data.getItemAt(0);
text = item.getText().toString();
}
return text;
}
// Called from native code to show keyboard.
public void showKeyboard() {
InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
imm.showSoftInput(getWindow().getDecorView(), InputMethodManager.SHOW_IMPLICIT);
}
// Called from native code to hide keyboard.
public void hideKeyboard() {
InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(getWindow().getDecorView().getWindowToken(), 0);
}
// Called from native code to start camera.
public void startCamera() {
// Check permissions.
String notificationsPermission = Manifest.permission.CAMERA;
if (checkSelfPermission(notificationsPermission) != PackageManager.PERMISSION_GRANTED) {
requestPermissions(new String[] { notificationsPermission }, CAMERA_PERMISSION_CODE);
} else {
// Start .
if (mCameraProviderFuture == null) {
mCameraProviderFuture = ProcessCameraProvider.getInstance(this);
mCameraProviderFuture.addListener(() -> {
try {
mCameraProvider = mCameraProviderFuture.get();
// Launch camera.
// Start camera.
openCamera();
} catch (Exception e) {
View content = findViewById(android.R.id.content);
@@ -381,14 +493,14 @@ public class MainActivity extends GameActivity {
// Pass image from camera into native code.
public native void onCameraImage(byte[] buff, int rotation);
// Called from native code to share image from provided path.
public void shareImage(String path) {
// Called from native code to share data from provided path.
public void shareData(String path) {
File file = new File(path);
Uri uri = FileProvider.getUriForFile(this, "mw.gri.android.fileprovider", file);
Intent intent = new Intent(Intent.ACTION_SEND);
intent.putExtra(Intent.EXTRA_STREAM, uri);
intent.setType("image/*");
startActivity(Intent.createChooser(intent, "Share image"));
intent.setType("text/*");
startActivity(Intent.createChooser(intent, "Share data"));
}
// Called from native code to check if device is using dark theme.
@@ -400,9 +512,9 @@ public class MainActivity extends GameActivity {
// Called from native code to pick the file.
public void pickFile() {
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.setType("*/*");
intent.setType("text/*");
try {
mFilePickResultLauncher.launch(Intent.createChooser(intent, "Pick file"));
mFilePickResult.launch(Intent.createChooser(intent, "Pick file"));
} catch (android.content.ActivityNotFoundException ex) {
onFilePick("");
}
@@ -4,23 +4,18 @@ import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import java.util.Objects;
public class NotificationActionsReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent i) {
String a = i.getAction();
if (a.equals(BackgroundService.ACTION_START_NODE)) {
if (Objects.equals(a, BackgroundService.ACTION_START_NODE)) {
startNode();
context.sendBroadcast(new Intent(BackgroundService.ACTION_REFRESH));
} else if (a.equals(BackgroundService.ACTION_STOP_NODE)) {
} else if (Objects.equals(a, BackgroundService.ACTION_STOP_NODE)) {
stopNode();
context.sendBroadcast(new Intent(BackgroundService.ACTION_REFRESH));
} else {
if (isNodeRunning()) {
stopNodeToExit();
context.sendBroadcast(new Intent(BackgroundService.ACTION_REFRESH));
} else {
context.sendBroadcast(new Intent(MainActivity.STOP_APP_ACTION));
}
stopNodeToExit();
}
}
@@ -30,6 +25,4 @@ public class NotificationActionsReceiver extends BroadcastReceiver {
native void stopNode();
// Stop node and exit from the app.
native void stopNodeToExit();
// Check if node is running.
native boolean isNodeRunning();
}
@@ -153,4 +153,4 @@ public class Utils {
String fileType = context.getContentResolver().getType(uri);
return MimeTypeMap.getSingleton().getExtensionFromMimeType(fileType);
}
}
}
@@ -3,6 +3,7 @@
<item name="android:statusBarColor">@color/yellow</item>
<item name="android:windowLightStatusBar">true</item>
<item name="android:navigationBarColor">@color/black</item>
<item name="android:windowLightNavigationBar" tools:targetApi="27">false</item>
<item name="android:windowLayoutInDisplayCutoutMode" tools:targetApi="o_mr1">shortEdges</item>
</style>
</resources>
+1 -1
View File
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<paths>
<external-cache-path name="images" path="images/" />
<external-cache-path name="share" path="share/" />
</paths>
+3 -8
View File
@@ -1,10 +1,5 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
id 'com.android.application' version '8.1.1' apply false
id 'com.android.library' version '8.1.1' apply false
}
task clean(type: Delete) {
delete rootProject.buildDir
}
id 'com.android.application' version '8.10.0' apply false
id 'com.android.library' version '8.10.0' apply false
}
+1 -2
View File
@@ -6,7 +6,7 @@
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
org.gradle.jvmargs=-Xmx4096m -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
@@ -19,5 +19,4 @@ android.useAndroidX=true
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true
android.defaults.buildfeatures.buildconfig=true
android.nonFinalResIds=false
+1 -1
View File
@@ -1,6 +1,6 @@
#Mon May 02 15:39:12 BST 2022
distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip
distributionUrl=https\://code.gri.mw/DEV/gradle/releases/download/v8.11.1/gradle-8.11.1-bin.zip
distributionPath=wrapper/dists
zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
+25 -12
View File
@@ -1,16 +1,29 @@
pluginManagement {
repositories {
gradlePluginPortal()
google()
mavenCentral()
maven {
credentials {
username "$mavenUser"
password "$mavenPassword"
}
url "$mavenHost/repository/gradle-plugin-portal/"
allowInsecureProtocol = true
}
maven {
credentials {
username "$mavenUser"
password "$mavenPassword"
}
url "$mavenHost/repository/google-maven/"
allowInsecureProtocol = true
}
maven {
credentials {
username "$mavenUser"
password "$mavenPassword"
}
url "$mavenHost/repository/maven-central/"
allowInsecureProtocol = true
}
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
}
}
//rootProject.name = "Rust Template"
include ':app'
include ':app'
+101
View File
@@ -0,0 +1,101 @@
use std::path::PathBuf;
use std::process::Command;
use std::{env, fs};
fn main() {
built::write_built_file().expect("Failed to acquire build-time information");
// Setting up git hooks in the project: rustfmt and so on.
let git_hooks = format!(
"git config core.hooksPath {}",
PathBuf::from("./.hooks").to_str().unwrap()
);
if cfg!(target_os = "windows") {
Command::new("cmd")
.args(&["/C", &git_hooks])
.output()
.expect("failed to execute git config for hooks");
} else {
Command::new("sh")
.args(&["-c", &git_hooks])
.output()
.expect("failed to execute git config for hooks");
}
let out_dir = env::var("OUT_DIR").unwrap();
let tor_out_dir = format!("{}/tor", out_dir);
let mut webtunnel_file = format!("{}/webtunnel", tor_out_dir);
let exists = fs::exists(&webtunnel_file).unwrap();
if !exists {
// Create empty webtunnel file to allow build with include_bytes! macro.
fs::create_dir(&tor_out_dir).unwrap_or_default();
fs::File::create(&webtunnel_file).unwrap();
}
let target = env::var("TARGET").unwrap();
let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap();
if target_os == "ios" {
return;
}
let is_android = target_os == "android";
if is_android {
// Set a path to Android Webtunnel binary.
let arch = if target.contains("aarch64") {
"arm64-v8a"
} else if target.contains("arm") {
"armeabi-v7a"
} else {
"x86_64"
};
let root = env::var("CARGO_MANIFEST_DIR").unwrap();
webtunnel_file = format!(
"{}/android/app/src/main/jniLibs/{}/libwebtunnel.so",
root, arch
);
}
// Build if Webtunnel binary is empty or not exists.
let empty = match fs::File::open(&webtunnel_file) {
Ok(file) => file.metadata().unwrap().len() == 0,
Err(_) => true,
};
let build = !exists || empty;
if build {
// Setup GOOS env variable.
let go_os = if target_os == "macos" {
"darwin"
} else {
target_os.as_str()
};
// Setup GOARCH env variable.
let go_arch = if target.contains("aarch64") {
"arm64"
} else if target.contains("arm") {
"arm"
} else {
"amd64"
};
// Run Webtunnel Go build.
let output = if env::consts::OS == "windows" {
Command::new("./scripts/webtunnel.bat")
.arg(go_os)
.arg(go_arch)
.arg(webtunnel_file)
.output()
} else {
Command::new("bash")
.arg("./scripts/webtunnel.sh")
.arg(go_os)
.arg(go_arch)
.arg(webtunnel_file)
.output()
};
if let Ok(out) = output {
if out.status.code().is_none() || out.status.code().unwrap() != 0 {
panic!("webtunnel go build failed:\n{:?}", out);
}
}
}
}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

+2 -1
View File
@@ -3,4 +3,5 @@ Name=Grim
Exec=grim
Icon=grim
Type=Application
Categories=Finance
Categories=Finance
MimeType=application/x-slatepack;text/plain;
+3 -3
View File
@@ -4,7 +4,7 @@ case $2 in
x86_64|arm)
;;
*)
echo "Usage: release_linux.sh [version] [platform]\n - platform: 'x86_64', 'arm'" >&2
echo "Usage: release_linux.sh [platform] [version]\n - platform: 'x86_64', 'arm'" >&2
exit 1
esac
@@ -17,11 +17,11 @@ cd ..
[[ $2 == "x86_64" ]] && arch+=(x86_64-unknown-linux-gnu)
[[ $2 == "arm" ]] && arch+=(aarch64-unknown-linux-gnu)
# Start release build with zig linker for cross-compilation
rustup target add ${arch}
cargo install cargo-zigbuild
cargo zigbuild --release --target ${arch}
# Create AppImage with https://github.com/AppImage/appimagetool
cp target/${arch}/release/grim linux/Grim.AppDir/AppRun
rm target/${arch}/release/*.AppImage
appimagetool linux/Grim.AppDir target/${arch}/release/grim-v$1-linux-$2.AppImage
appimagetool linux/Grim.AppDir target/${arch}/release/grim-v$2-linux-$1.AppImage
+71 -5
View File
@@ -25,9 +25,20 @@ share: teilen
theme: 'Theme:'
dark: Dunkel
light: Hell
file: Datei
choose_file: Datei auswählen
choose_folder: Ordner auswählen
crash_report: Absturzbericht
crash_report_warning: Anwendung wurde beim letzten Mal unerwartet geschlossen, Sie können den Absturzbericht mit Entwicklern teilen.
confirmation: Bestätigung
enter_url: URL eingeben
max_short: MAX
files_location: Dateistandort
moving_files: Dateien verschieben
wrong_path_error: Falscher Weg angegeben
check_updates: Suchen Sie beim Start nach Updates
update_available: Update ist verfügbar!
changelog: 'Wechselbuch:'
wallets:
await_conf_amount: Erwarte Bestätigung
await_fin_amount: Warten auf die Fertigstellung
@@ -82,6 +93,7 @@ wallets:
tx_canceled: Abgebrochen
tx_cancelling: Abbrechen
tx_finalizing: Finalisierung
tx_posting: Buchungsvorgang
tx_confirmed: Bestätigt
txs: Transaktionen
tx: Transaktion
@@ -125,6 +137,12 @@ wallets:
tx_receive_cancel_conf: 'Sind Sie sicher, dass Sie das Empfangen von %{amount} ツ abbrechen wollen?'
rec_phrase_not_found: Wiederhestellungsphrase nicht gefunden.
restore_wallet_desc: Stellen Sie das Wallet wieder her, indem Sie alle Dateien löschen. Wenn die normale Reparatur nicht geholfen hat, müssen Sie Ihr Wallet erneut öffnen.
fee_base_desc: 'Gebühr (basiswert%{value}):'
payment_proof: Zahlungsnachweis
payment_proof_desc: 'Geben Sie den erhaltenen Zahlungsnachweis ein, um die Transaktion zu verifizieren:'
payment_proof_valid: 'Der eingegebene Zahlungsnachweis ist gültig:'
payment_proof_error: 'Der eingetragene Zahlungsnachweis ist nicht gültig:'
tx_delete_confirmation: Bist du sicher, dass du die Transaktion aus dem Verlauf löschen möchtest?
transport:
desc: 'Transport verwenden, um Nachrichten synchron zu empfangen oder zu senden:'
tor_network: Tor Netzwek
@@ -134,10 +152,11 @@ transport:
conn_error: Verbindungsproblem
disconnected: Verbindung getrennt
receiver_address: 'Empfängeraddresse:'
sender_address: 'Absenderadresse:'
incorrect_addr_err: 'Eingegebene Addresse ist inkorrekt:'
tor_send_error: Beim Senden über Tor ist ein Fehler aufgetreten. Stellen Sie sicher, dass der Empfänger online ist. Die Transaktion wurde abgebrochen.
tor_autorun_desc: Gibt an, ob beim Öffnen des Wallets der Tor-Dienst gestartet werden soll, um Transaktionen synchron zu empfangen.
tor_sending: 'Sende %{amount} ツ über Tor'
tor_sending: Sende über Tor
tor_settings: Tor Einstellungen
bridges: Brücken
bridges_desc: Richten Sie Brücken ein, um die Zensur des Tor-Netzwerks zu umgehen, wenn die normale Verbindung nicht funktioniert.
@@ -282,13 +301,60 @@ network_settings:
ban_window_desc: Die Entscheidung über das Verbot trifft der Knoten auf der Grundlage der Korrektheit der von der Gegenstelle erhaltenen Daten.
max_inbound_count: 'Maximale Anzahl der eingehenden Peer-Verbindungen:'
max_outbound_count: 'Maximale Anzahl von ausgehenden Peer-Verbindungen:'
reset_peers_desc: Peer-Daten zurücksetzen. Verwenden Sie diese Funktion nur, wenn es Probleme beim finden von Peers gibt.
reset_peers: Peers zurücksetzten
reset_data_desc: Reset-Knotendaten. Verwenden Sie diese Funktion nur, wenn es Probleme mit der Synchronisation gibt.
reset_data: Daten zurücksetzten
ip_listen_all: Hören Sie auf allen Schnittstellen
modal:
cancel: Abbrechen
save: Speichern
confirmation: Bestätigung
add: Hinzufügen
modal_exit:
description: Sind Sie sicher, dass Sie die Anwendung beenden wollen?
exit: Schließen
exit: Schließen
app_settings:
proxy: Proxy
proxy_desc: Lohnt es sich, einen Proxy für Netzwerkanfragen von der Anwendung zu verwenden.
keyboard:
1: 1
2: 2
3: 3
4: 4
5: 5
6: 6
7: 7
8: 8
9: 9
0: 0
01: ß
q: q
w: w
e: e
r: r
t: t
y: z
u: u
i: i
o: o
p: p
p1: ü
a: a
s: s
d: d
f: f
g: g
h: h
j: j
k: k
l: l
l1: ö
l2: ä
z: y
x: x
c: c
v: v
b: b
n: n
m: m
m1: ','
m2: .
m3: '/'
+71 -5
View File
@@ -25,9 +25,20 @@ share: Share
theme: 'Theme:'
dark: Dark
light: Light
file: File
choose_file: Choose file
choose_folder: Choose folder
crash_report: Crash report
crash_report_warning: Application closed unexpectedly last time, you can share crash report with developers.
confirmation: Confirmation
enter_url: Enter URL
max_short: MAX
files_location: Files location
moving_files: Moving files
wrong_path_error: Wrong path specified
check_updates: Check for updates at startup
update_available: Update is available!
changelog: 'Changelog:'
wallets:
await_conf_amount: Awaiting confirmation
await_fin_amount: Awaiting finalization
@@ -82,6 +93,7 @@ wallets:
tx_canceled: Canceled
tx_cancelling: Cancelling
tx_finalizing: Finalizing
tx_posting: Posting
tx_confirmed: Confirmed
txs: Transactions
tx: Transaction
@@ -125,6 +137,12 @@ wallets:
tx_receive_cancel_conf: 'Are you sure you want to cancel receiving of %{amount} ツ?'
rec_phrase_not_found: Recovery phrase not found.
restore_wallet_desc: Restore wallet by deleting all files if usual repair not helped, you will need to re-open your wallet.
fee_base_desc: 'Fee (base value%{value}):'
payment_proof: Payment proof
payment_proof_desc: 'Enter received payment proof to verify transaction:'
payment_proof_valid: 'Entered payment proof is valid:'
payment_proof_error: 'Entered payment proof is not valid:'
tx_delete_confirmation: Are you sure you want to delete the transaction from history?
transport:
desc: 'Use transport to receive or send messages synchronously:'
tor_network: Tor network
@@ -134,10 +152,11 @@ transport:
conn_error: Connection error
disconnected: Disconnected
receiver_address: 'Address of the receiver:'
sender_address: 'Address of the sender:'
incorrect_addr_err: 'Entered address is incorrect:'
tor_send_error: An error occurred during sending over Tor, make sure receiver is online, transaction was canceled.
tor_autorun_desc: Whether to launch Tor service on wallet opening to receive transactions synchronously.
tor_sending: 'Sending %{amount} ツ over Tor'
tor_sending: Sending over Tor
tor_settings: Tor Settings
bridges: Bridges
bridges_desc: Setup bridges to bypass Tor network censorship if usual connection is not working.
@@ -282,13 +301,60 @@ network_settings:
ban_window_desc: The decision to ban is made by node, based on the correctness of the data received from the peer.
max_inbound_count: 'Maximum number of inbound peer connections:'
max_outbound_count: 'Maximum number of outbound peer connections:'
reset_peers_desc: Reset peers data. Use it with a caution only if there are problems with finding peers.
reset_peers: Reset peers
reset_data_desc: Reset the node data. Use it with a caution only if there are problems with synchronization.
reset_data: Reset data
ip_listen_all: Listen on all interfaces
modal:
cancel: Cancel
save: Save
confirmation: Confirmation
add: Add
modal_exit:
description: Are you sure you want to quit the application?
exit: Exit
exit: Exit
app_settings:
proxy: Proxy
proxy_desc: Whether to use proxy for network requests from the application.
keyboard:
1: 1
2: 2
3: 3
4: 4
5: 5
6: 6
7: 7
8: 8
9: 9
0: 0
01: '-'
q: q
w: w
e: e
r: r
t: t
y: y
u: u
i: i
o: o
p: p
p1: '"'
a: a
s: s
d: d
f: f
g: g
h: h
j: j
k: k
l: l
l1: \
l2: ':'
z: z
x: x
c: c
v: v
b: b
n: n
m: m
m1: ','
m2: .
m3: /
+71 -5
View File
@@ -25,9 +25,20 @@ share: Partager
theme: 'Thème:'
dark: Sombre
light: Clair
file: Fichier
choose_file: Choisir un fichier
choose_folder: Choisir un dossier
crash_report: Rapport d'échec
crash_report_warning: L'application s'est fermée de manière inattendue la dernière fois, vous pouvez partager un rapport d'incident avec les développeurs.
confirmation: Confirmation
enter_url: Entrez l'URL
max_short: MAX
files_location: Emplacement du fichier
moving_files: Déplacer des fichiers
wrong_path_error: Chemin incorrect spécifié
check_updates: Vérifiez les mises à jour au démarrage
update_available: Mise à jour disponible!
changelog: 'Journal des modifications:'
wallets:
await_conf_amount: En attente de confirmation
await_fin_amount: En attente de finalisation
@@ -82,6 +93,7 @@ wallets:
tx_canceled: Annulé
tx_cancelling: Annulation
tx_finalizing: Finalisation
tx_posting: Publication
tx_confirmed: Confirmé
txs: Transactions
tx: Transaction
@@ -125,6 +137,12 @@ wallets:
tx_receive_cancel_conf: 'Êtes-vous sûr de vouloir annuler la réception de %{amount} ツ?'
rec_phrase_not_found: Phrase de récupération non trouvée.
restore_wallet_desc: "Restaurer le portefeuille en supprimant tous les fichiers si la réparation habituelle n'a pas aidé. Vous devrez rouvrir votre portefeuille."
fee_base_desc: 'Frais (valeur de base%{value}):'
payment_proof: Preuve de paiement
payment_proof_desc: 'Saisissez la preuve de paiement reçue pour vérifier la transaction:'
payment_proof_valid: 'La preuve de paiement saisie est valide:'
payment_proof_error: "La preuve de paiement saisie n'est pas valide:"
tx_delete_confirmation: Êtes-vous sûr de vouloir supprimer la transaction de l'historique?
transport:
desc: 'Utilisez le transport pour recevoir ou envoyer des messages de manière synchronisée:'
tor_network: Réseau Tor
@@ -134,10 +152,11 @@ transport:
conn_error: Erreur de connexion
disconnected: Déconnecté
receiver_address: 'Adresse du destinataire:'
sender_address: "Adresse de l'expéditeur:"
incorrect_addr_err: 'Adresse entrée incorrecte:'
tor_send_error: "Une erreur s'est produite lors de l'envoi via Tor. Assurez-vous que le destinataire est en ligne, la transaction a été annulée."
tor_autorun_desc: "Lancer automatiquement le service Tor à l'ouverture du portefeuille pour recevoir les transactions de manière synchronisée."
tor_sending: 'Envoi de %{amount} ツ via Tor'
tor_sending: Envoi via Tor
tor_settings: Paramètres Tor
bridges: Passerelles
bridges_desc: Configurez des passerelles pour contourner la censure du réseau Tor si la connexion habituelle ne fonctionne pas.
@@ -282,13 +301,60 @@ network_settings:
ban_window_desc: La décision de bannir est prise par le noeud, en fonction de la validité des données reçues du pair.
max_inbound_count: 'Nombre maximum de connexions de pairs entrants :'
max_outbound_count: 'Nombre maximum de connexions de pairs sortants :'
reset_peers_desc: Réinitialiser les données des pairs. Utilisez-le avec précaution uniquement en cas de problèmes pour trouver des pairs.
reset_peers: Réinitialiser les pairs
reset_data_desc: Réinitialisez les données du noeud. Utilisez-le avec prudence uniquement en cas de problème de synchronisation.
reset_data: Réinitialisation des données
ip_listen_all: Écoutez sur toutes les interfaces
modal:
cancel: Annuler
save: Sauvegarder
confirmation: Confirmation
add: Ajouter
modal_exit:
description: "Êtes-vous sûr de vouloir quitter l'application ?"
exit: Quitter
exit: Quitter
app_settings:
proxy: Proxy
proxy_desc: Vaut-il la peine d'utiliser un proxy pour les requêtes réseau de l'application.
keyboard:
1: 1
2: 2
3: 3
4: 4
5: 5
6: 6
7: 7
8: 8
9: 9
0: 0
01: '`'
q: a
w: z
e: e
r: r
t: t
y: y
u: u
i: i
o: o
p: p
p1: ç
a: q
s: s
d: d
f: f
g: g
h: h
j: j
k: k
l: l
l1: m
l2: ù
z: w
x: x
c: c
v: v
b: b
n: n
m: ','
m1: .
m2: ':'
m3: /
+71 -5
View File
@@ -25,9 +25,20 @@ share: Поделиться
theme: 'Тема:'
dark: Тёмная
light: Светлая
file: Файл
choose_file: Выбрать файл
choose_folder: Выбрать папку
crash_report: Отчёт о сбое
crash_report_warning: В прошлый раз приложение неожиданно закрылось, вы можете поделиться отчетом о сбое с разработчиками.
confirmation: Подтверждение
enter_url: Введите URL-адрес
max_short: МАКС
files_location: Расположение файлов
moving_files: Перемещение файлов
wrong_path_error: Указан неправильный путь
check_updates: Проверять обновления при запуске
update_available: Доступно обновление!
changelog: 'Журнал изменений:'
wallets:
await_conf_amount: Ожидает подтверждения
await_fin_amount: Ожидает завершения
@@ -82,6 +93,7 @@ wallets:
tx_canceled: Отменено
tx_cancelling: Отмена
tx_finalizing: Завершение
tx_posting: Публикация
tx_confirmed: Подтверждено
txs: Транзакции
tx: Транзакция
@@ -125,6 +137,12 @@ wallets:
tx_receive_cancel_conf: 'Вы действительно хотите отменить получение %{amount} ツ?'
rec_phrase_not_found: Фраза восстановления не найдена.
restore_wallet_desc: Восстановить кошелёк, удалив все файлы, если обычное исправление не помогло. Необходимо переоткрыть кошелёк.
fee_base_desc: 'Комиссия (базовое значение%{value}):'
payment_proof: Подтверждение оплаты
payment_proof_desc: 'Введите полученное подтверждение оплаты для проверки транзакции:'
payment_proof_valid: 'Введённое подтверждение оплаты действительно:'
payment_proof_error: 'Введённое подтверждение оплаты недействительно:'
tx_delete_confirmation: Вы уверены, что хотите удалить транзакцию из истории?
transport:
desc: 'Используйте транспорт для синхронных получения или отправки сообщений:'
tor_network: Сеть Tor
@@ -134,10 +152,11 @@ transport:
conn_error: Ошибка подключения
disconnected: Отключено
receiver_address: 'Адрес получателя:'
sender_address: 'Адрес отправителя:'
incorrect_addr_err: 'Введённый адрес неверен:'
tor_send_error: Во время отправки через Tor произошла ошибка, убедитесь, что получатель находится онлайн, транзакция была отменена.
tor_autorun_desc: Запускать ли Tor сервис при открытии кошелька для синхронного получения транзакций.
tor_sending: 'Отправка %{amount} ツ через Tor'
tor_sending: Отправка через Tor
tor_settings: Настройки Tor
bridges: Мосты
bridges_desc: Настройте мосты для обхода цензуры сети Tor, если обычное соединение не работает.
@@ -282,13 +301,60 @@ network_settings:
ban_window_desc: Решение о запрете принимается узлом, основываясь на корректности данных полученных от пира.
max_inbound_count: 'Максимальное количество входящих подключений пиров:'
max_outbound_count: 'Максимальное количество исходящих подключений к пирам:'
reset_peers_desc: Сбросить данные пиров. Используйте с осторожностью, только при наличии проблем с поиском пиров.
reset_peers: Сбросить пиры
reset_data_desc: Сбросить данные узла. Используйте с осторожностью, только при наличии проблем с синхронизацией.
reset_data: Сброс данных
ip_listen_all: Слушать на всех интерфейсах
modal:
cancel: Отмена
save: Сохранить
confirmation: Подтверждение
add: Добавить
modal_exit:
description: Вы уверены, что хотите выйти из приложения?
exit: Выход
exit: Выход
app_settings:
proxy: Прокси
proxy_desc: Стоит ли использовать прокси для сетевых запросов из приложения.
keyboard:
1: 1
2: 2
3: 3
4: 4
5: 5
6: 6
7: 7
8: 8
9: 9
0: 0
01: ъ
q: й
w: ц
e: у
r: к
t: е
y: н
u: г
i: ш
o: щ
p: з
p1: х
a: ф
s: ы
d: в
f: а
g: п
h: р
j: о
k: л
l: д
l1: ж
l2: э
z: я
x: ч
c: с
v: м
b: и
n: т
m: ь
m1: б
m2: ю
m3: ё
+71 -5
View File
@@ -25,9 +25,20 @@ share: Paylasmak
theme: 'Tema:'
dark: Karanlik
light: Isik
file: Dosya
choose_file: Dosya seçin
choose_folder: Klasör seç
crash_report: Ariza Raporu
crash_report_warning: Uygulama beklenmedik bir sekilde kapandi son kez, kilitlenme raporunu gelistiricilerle paylasabilirsiniz.
confirmation: Onay
enter_url: URL'yi girin
max_short: MAKS
files_location: Dosya konumu
moving_files: Dosyalari Tasima
wrong_path_error: Yanlis yol belirtildi
check_updates: Başlangiçta güncellemeleri kontrol edin
update_available: Güncelleme mevcut!
changelog: 'Değişiklik Günlüğü:'
wallets:
await_conf_amount: Onay bekleniyor
await_fin_amount: Tamamlanma bekleniyor
@@ -82,6 +93,7 @@ wallets:
tx_canceled: Iptal edildi
tx_cancelling: Iptal ediliyor
tx_finalizing: Islem tamamlaniyor
tx_posting: Islem kaydetme
tx_confirmed: Onaylandi
txs: Islemler
tx: Islem
@@ -125,6 +137,12 @@ wallets:
tx_receive_cancel_conf: Gelen tx iptal
rec_phrase_not_found: Sifre kelime bulunmuyor
restore_wallet_desc: Cuzdani restore et
fee_base_desc: 'Ücret (taban değeri%{value}):'
payment_proof: Ödeme kaniti
payment_proof_desc: 'Islemi doğrulamak için alinan ödeme kanitini girin:'
payment_proof_valid: 'Girilen ödeme kaniti geçerlidir:'
payment_proof_error: 'Girilen ödeme kaniti geçerli değildir:'
tx_delete_confirmation: Islemi geçmişten silmek istediğinizden emin misiniz?
transport:
desc: 'Adresten senkronize GONDER veya AL:'
tor_network: Tor network
@@ -134,10 +152,11 @@ transport:
conn_error: Bagalanti hatasi
disconnected: Baglanti yok
receiver_address: 'Alicinin adresi:'
sender_address: 'Gönderici adresi:'
incorrect_addr_err: 'Girilen adres hatali:'
tor_send_error: Tor adresi uzerinden gonderimde aksaklik olustu, alici online olmasi gerek, islem iptal edildi.
tor_autorun_desc: Islemleri Tor adresi olarak AL,bunun için cuzdan acilisinda Tor hizmetinin baslatilip baslatilmayacagi.
tor_sending: 'Tor adrese %{amount} ツ gonderiliyor.'
tor_sending: Tor adrese gonderiliyor
tor_settings: Tor Ayarlar
bridges: Bridges
bridges_desc: Setup bridges to bypass Tor network censorship if usual connection is not working.
@@ -282,13 +301,60 @@ network_settings:
ban_window_desc: Banlama karari, peerden alinan verilerin dogruluguna bagli olarak Node tarafindan verilir.
max_inbound_count: 'Maksimum gelen Peer baglanti sayisi:'
max_outbound_count: 'Maksimum giden Peer baglanti sayisi:'
reset_peers_desc: Peers verilerini sifirlayin. Yalnizca Peers bulma konusunda sorun yasiyorsaniz dikkatli kullanin.
reset_peers: Peers Resetle
reset_data_desc: Node verisini sifirlama. Sadece senkronizasyonda sorun varsa dikkatli kullanin.
reset_data: Verileri sifirlama
ip_listen_all: Tüm arayüzlerde dinle
modal:
cancel: Iptal
save: Kaydet
confirmation: Onay
add: Ekle
modal_exit:
description: Uygulamadan cikmak için exit, emin misiniz?
exit: Exit
exit: Exit
app_settings:
proxy: Proxy
proxy_desc: Uygulamadan gelen ağ istekleri için bir proxy kullanmaya değer mi.
keyboard:
1: 1
2: 2
3: 3
4: 4
5: 5
6: 6
7: 7
8: 8
9: 9
0: 0
01: '-'
q: q
w: w
e: e
r: r
t: t
y: y
u: u
i: i
o: o
p: p
p1: ü
a: a
s: s
d: d
f: f
g: g
h: h
j: j
k: k
l: l
l1: ö
l2: ':'
z: z
x: x
c: c
v: v
b: b
n: n
m: m
m1: ','
m2: .
m3: /
+360
View File
@@ -0,0 +1,360 @@
lang_name: 英语
copy: 复制
paste: 粘贴
continue: 继续
complete: 完成
error: 错误
retry: 重试
close: 关闭
change: 更改
show: 显示
delete: 删除
clear: 清楚
create: 创建
id: 标识
kernel: 核心
settings: 设置
language: 语言
scan: 扫描
qr_code: 二维码
scan_qr: 扫描二维码
repeat: 重复
scan_result: 扫描结果
back: 返回
share: 分享
theme: '主题:'
dark: 深色
light: 淡色
file: 文件
choose_file: 选择文件
choose_folder: 选择文件夹
crash_report: 崩溃报告
crash_report_warning: 上次应用程序意外关闭,您可以报告开发人员崩溃事件.
confirmation: 确认
enter_url: 输入 URL
max_short: 最大數量
files_location: 檔案位置
moving_files: 檔案移動
wrong_path_error: 指定錯誤路徑
check_updates: 啟動時請查看更新
update_available: 最新消息已发布!
changelog: '更新日誌:'
wallets:
await_conf_amount: 等待确认中
await_fin_amount: 等待确定中
locked_amount: 锁定帐户
txs_empty: '手动接收资金或通过传输接收资金 %{message} or %{transport} 更改钱包设置, 请按屏幕底部的按钮 %{settings} 按钮.'
title: 钱包
create_desc: 创建或种子单词导入已有钱包.
add: 添加钱包
name: '用户名:'
pass: '密码:'
pass_empty: 输入钱包的密码
current_pass: '目前密码:'
new_pass: '新密码:'
min_tx_conf_count: '确认交易的最低数量:'
recover: 恢复
recovery_phrase: 助记词
words_count: '字数:'
enter_word: '输入单词 #%{number}:'
not_valid_word: 输入的单词无效
not_valid_phrase: 输入的助记词无效
create_phrase_desc: 已安全地写下并保存助记词.
restore_phrase_desc: 从已保存的助记词中输入.
setup_conn_desc: 选择钱包连接到网络的方式.
conn_method: 连接方式
ext_conn: '外部连接:'
add_node: 添加节点
node_url: '节点网址:'
node_secret: 'API 密钥 (可选):'
invalid_url: 输入的网址无效
open: 打开钱包
wrong_pass: 输入的密码错误
locked: 已锁定
unlocked: 解锁
enable_node: '通过选择屏幕底部的按钮 %{settings} 启用集成节点以使用钱包或更改连接设置.'
node_loading: '集成节点同步后钱包会加载,你可选择屏幕底部的按钮 %{settings} 更改连接.'
loading: 正在加载
closing: 正在关闭
checking: 检查中
default_wallet: 默认钱包
new_account_desc: '输入新帐户的名称:'
wallet_loading: 加载钱包
wallet_closing: 关闭钱包
wallet_checking: 检查钱包
tx_loading: 加载事务
default_account: 默认账户
accounts: 账户
tx_sent: 已发送
tx_received: 已接收
tx_sending: 发送中
tx_receiving: 接收中
tx_confirming: 等待确认
tx_canceled: 已取消
tx_cancelling: 取消
tx_finalizing: 完成
tx_posting: 过账交易
tx_confirmed: 已确认
txs: 所有交易
tx: 交易
messages: 消息
transport: 传输
input_slatepack_desc: '输入收到的 Slatepack 消息创建响应或完成的请求:'
parse_slatepack_err: '读取消息时出错,请检查输入:'
pay_balance_error: '账户余额不足以支付 %{amount} ツ 和网络费用.'
parse_i1_slatepack_desc: '要支付 %{amount} ツ 请将此消息发送给接收者:'
parse_i2_slatepack_desc: '完成交易以接收 %{amount} ツ:'
parse_i3_slatepack_desc: '发布交易以完成 %{amount} ツ的接收 ツ:'
parse_s1_slatepack_desc: '要接收 %{amount} ツ 请将此消息发送给发件人:'
parse_s2_slatepack_desc: '完成交易以发送 %{amount} ツ:'
parse_s3_slatepack_desc: '发布交易以完成 %{amount} ツ的发送:'
resp_slatepack_err: '创建响应时出错,请检查输入数据或重试:'
resp_exists_err: 此交易已存在.
resp_canceled_err: 此交易已被取消.
create_request_desc: '创建发送或接收资金的请求:'
send_request_desc: '您已创建发送请求 %{amount} ツ. 将此消息发送给接收者:'
send_slatepack_err: 创建发送资金请求时出错,请检查输入数据或重试.
invoice_desc: '您已创建接收请求 %{amount} ツ. 将此消息发送给发送者:'
invoice_slatepack_err: 发票开具时出错,请检查输入数据或重试.
finalize_slatepack_err: '完结时出错,请检查输入数据或重试:'
finalize: 完成
use_dandelion: 使用蒲公英
enter_amount_send: '你有 %{amount} ツ. 输入要发送的金额:'
enter_amount_receive: '输入要接收的金额:'
recovery: 恢复
repair_wallet: 修复钱包
repair_desc: 检查钱包,必要时修复和恢复丢失的输出. 此操作需要时间.
repair_unavailable: 您需要与节点建立有效连接并完成钱包同步.
delete: 删除钱包
delete_conf: 您确定要删除钱包吗?
delete_desc: 确保您已保存恢复助记语,以便日后使用资金。.
wallet_loading_err: '同步钱包时出错,你可以通过选择屏幕底部的按钮 %{settings} 来重试或更改连接设置.'
wallet: 钱包
send: 发送
receive: 接收
settings: 钱包设置
tx_send_cancel_conf: '您确定要取消 %{amount} ツ的发送吗?'
tx_receive_cancel_conf: '您确定要取消 %{amount} ツ的接收吗?'
rec_phrase_not_found: 找不到恢复助记词.
restore_wallet_desc: 如果常规修复没有帮助,通过删除所有文件来恢复钱包.您将需要重新打开您的钱包.
fee_base_desc: '费用 (基值%{value}):'
payment_proof: 付款證明
payment_proof_desc: '輸入已收款證明以驗證交易:'
payment_proof_valid: '輸入的付款證明有效:'
payment_proof_error: '輸入的付款證明無效:'
tx_delete_confirmation: 你確定要從歷史紀錄中刪除這筆交易嗎?
transport:
desc: '使用传输同步接收或发送消息:'
tor_network: Tor 网络
connected: 已连接
connecting: 正在连接
disconnecting: 断开连接
conn_error: 连接错误
disconnected: 已断开连接
receiver_address: '接收者的地址:'
sender_address: '发件人地址:'
incorrect_addr_err: '输入的地址不正确:'
tor_send_error: 通过 Tor 发送时出错,请确保接收方在线, 交易已取消.
tor_autorun_desc: 是否在开钱包时启动 Tor 服务以同步接收交易.
tor_sending: 通过 Tor 发送
tor_settings: Tor 设置
bridges: 桥梁
bridges_desc: 如果常规连接不正常,设置网桥,可以绕过 Tor 网络审查.
bin_file: '二进制文件:'
conn_line: '连接线:'
bridges_disabled: 网桥已禁用
bridge_name: '网桥%{b}'
network:
self: 网络
type: '网络类型:'
mainnet: 主网
testnet: 测试网
connections: 连接
node: 集成节点
metrics: 指标
mining: 挖矿
settings: 节点设置
enable_node: 启用节点
autorun: 自动运行
disabled_server: '按屏幕左上角的按钮 %{dots}启用集成节点或添加其他连接方法.'
no_ips: T您的系统上没有可用的 IP 地址,服务器无法启动,请检查您的网络连接.
available: 可用
not_available: 不可用
availability_check: 检查是否可用
android_warning: Android 用户注意 .要成功同步集成节点,您必须在手机的系统设置中允许访问通知并取消 Grim 应用程序的电池使用限制.这是在后台正确运行应用程序的必要操作.
sync_status:
node_restarting: 节点正在重新启动
node_down: 节点已关闭
initial: 节点正在启动
no_sync: 节点正在运行
awaiting_peers: 等待网络对点
header_sync: 正下载标题
header_sync_percent: '正在下载标题: %{percent}%'
tx_hashset_pibd: 下载状态 (PIBD)
tx_hashset_pibd_percent: '下载状态 (PIBD): %{percent}%'
tx_hashset_download: 正在下载状态
tx_hashset_download_percent: '下载状态: %{percent}%'
tx_hashset_setup_history: '正在准备状态(历史记录): %{percent}%'
tx_hashset_setup_position: '正在准备状态(位置): %{percent}%'
tx_hashset_setup: 正在准备状态
tx_hashset_range_proofs_validation: '验证状态(范围证明): %{percent}%'
tx_hashset_kernels_validation: '正在验证状态(核心): %{percent}%'
tx_hashset_save: 最终确定链状态
body_sync: 下载区块
body_sync_percent: '下载区块中: %{percent}%'
shutdown: 节点正在关闭
network_node:
header: 标题
block: 区块
hash: 哈希值
height: 高度
difficulty: 难度
time: 时间
main_pool: 主池
stem_pool: stem池
data: 数据
size: 大小 (GB)
peers: 网络对点
error_clean: 点数据已损坏,需要重新同步.
resync: 重新同步
error_p2p_api: '%{p2p_api} 服务器初始化时出错,请选择屏幕底部的按钮 %{p2p_api} 来检查 %{settings}设置.'
error_config: '配置初始化时出错,请选择屏幕底部的按钮 %{settings} 检查设置.'
error_unknown: '初始化时出错,请选择屏幕底部的按钮 %{settings} 来检查集成节点设置,或者重新同步.'
network_metrics:
loading: 指标在同步后将可用
emission: 发射
inflation: 通货膨胀
supply: 供应
block_time: Block time
reward: 奖励
difficulty_window: '难度窗口 %{size}'
network_mining:
loading: 同步后即可挖矿
info: '挖矿服务器已启用,您可以通过选择屏幕底部的按钮 %{settings} 来更改其设置。连接设备后,数据会更新.'
restart_server_required: 需要重启服务器才能应用更改.
rewards_wallet: 奖励钱包
server: 阶层服务器
address: 地址
miners: 矿工
devices: 设备
blocks_found: 找到的区块
hashrate: '哈希率 (C%{bits})'
connected: 已连接
disconnected: 已断开连接
network_settings:
change_value: 更改值
stratum_ip: '层 IP 地址:'
stratum_port: '层端口:'
port_unavailable: 指定的端口不可用
restart_node_required: 需要重启节点才能应用更改.
choose_wallet: 选择钱包
stratum_wallet_warning: 必须打开钱包才能获得奖励.
enable: 启用
disable: 禁用
restart: 重新启动
server: 服务器
api_ip: 'API IP 地址:'
api_port: 'API 端口:'
api_secret: '其它API 和 V2 所有者 API 令牌:'
foreign_api_secret: '外部 API 令牌:'
disabled: 已禁用
enabled: 已启用
ftl: '未来时间限制 (FTL):'
ftl_description: 限制未来多长时间, 相对于节点的本地时间,以秒为单位, 新区块的时间戳可以被接受.
not_valid_value: 输入的值无效
full_validation: 完全验证
full_validation_description: 在处理每个区块时是否运行全链验证(同步期间除外).
archive_mode: 存档模式
archive_mode_desc: 以全部存档模式运行全节点(同步需要更多的磁盘空间和时间).
attempt_time: '尝试挖矿时间 (秒):'
attempt_time_desc: 在停止并从池中重新收集交易之前尝试对特定标题进行挖矿的时间
min_share_diff: '可接受的最低份额难度:'
reset_settings_desc: 将节点设置重置为默认值
reset_settings: 重置设置
reset: 重置
tx_pool: 交易池
pool_fee: '接受到矿池的基本费用:'
reorg_period: '重组缓存保留期(以分钟为单位):'
max_tx_pool: '池中的最大交易数:'
max_tx_stempool: 'stem池中的最大交易数:'
max_tx_weight: '可以选择构建区块交易的最大总权重:'
epoch_duration: '纪元持续时间(以秒为单位):'
embargo_timer: '禁止计时器(以秒为单位):'
aggregation_period: '聚合周期(以秒为单位):'
stem_probability: 'stem助记词概率:'
stem_txs: stem交易
p2p_server: P2P 服务器
p2p_port: 'P2P 端口:'
add_seed: 添加 DNS 种子
seed_address: 'DNS 种子地址:'
add_peer: 添加网络对点
peer_address: '网络对点地址:'
peer_address_error: '以正确的格式输入 IP 地址或 DNS 名称(确保指定的主机可用),例如:192.168.0.11234 或 example.com:5678'
default: 默认
allow_list: 允许列表
allow_list_desc: 仅连接到此列表中的网络对点.
deny_list: 拒绝列表
deny_list_desc: 切勿连接到此列表中的网络对点.
favourites: 收藏夹
favourites_desc: 要连接的首选网络对点列表.
ban_window: '被封禁的网络对点应该保持被封禁多长时间(以秒为单位):'
ban_window_desc: 禁止的决定是由节点 根据从网络对点收到的数据的正确性做出的.
max_inbound_count: '入站网络对点连接的最大数量:'
max_outbound_count: '最大出站网络对点连接数:'
reset_data_desc: 重置节点数据。只有在出现同步问题时才需谨慎使用.
reset_data: 重置数据
ip_listen_all: 在所有接口上监听
modal:
cancel: 取消
save: 保存
add: 添加
modal_exit:
description: 您确定要退出应用程序吗?
exit: 退出手
app_settings:
proxy: 代理
proxy_desc: 是否值得对来自应用程序的网络请求使用代理.
keyboard:
1: 1
2: 2
3: 3
4: 4
5: 5
6: 6
7: 7
8: 8
9: 9
0: 0
01: '-'
q:
w:
e:
r:
t: 廿
y:
u:
i:
o:
p:
p1: '"'
a:
s:
d:
f:
g:
h:
j:
k:
l:
l1: \
l2: ':'
z:
x:
c:
v:
b:
n:
m:
m1: ','
m2: .
m3: /
+63 -47
View File
@@ -1,49 +1,65 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleDisplayName</key>
<string>Grim</string>
<key>CFBundleExecutable</key>
<string>grim</string>
<key>CFBundleIconFile</key>
<string>AppIcon</string>
<key>CFBundleIconName</key>
<string>AppIcon</string>
<key>CFBundleIdentifier</key>
<string>mw.gri.macos</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>Grim</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>0.1.0</string>
<key>CFBundleSupportedPlatforms</key>
<array>
<string>MacOSX</string>
</array>
<key>CFBundleVersion</key>
<string>1</string>
<key>LSApplicationCategoryType</key>
<string>public.app-category.finance</string>
<key>NSHumanReadableCopyright</key>
<string>2024</string>
</dict>
</plist>
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleDisplayName</key>
<string>Grim</string>
<key>CFBundleExecutable</key>
<string>grim</string>
<key>CFBundleIconFile</key>
<string>AppIcon</string>
<key>CFBundleIconName</key>
<string>AppIcon</string>
<key>CFBundleIdentifier</key>
<string>mw.gri.macos</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>Grim</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>0.3.6</string>
<key>CFBundleSupportedPlatforms</key>
<array>
<string>MacOSX</string>
</array>
<key>CFBundleVersion</key>
<string>1</string>
<key>NSCameraUsageDescription</key>
<string>Grim needs an access to your camera to scan QR code.</string>
<key>CFBundleDocumentTypes</key>
<array>
<dict>
<key>CFBundleTypeName</key>
<string>Apple SimpleText document</string>
<key>CFBundleTypeRole</key>
<string>Viewer</string>
<key>LSItemContentTypes</key>
<array>
<string>com.apple.traditional-mac-plain-text</string>
</array>
<key>NSDocumentClass</key>
<string>Document</string>
</dict>
<dict>
<key>CFBundleTypeName</key>
<string>Unknown document</string>
<key>CFBundleTypeRole</key>
<string>Viewer</string>
<key>LSItemContentTypes</key>
<array>
<string>public.data</string>
</array>
<key>NSDocumentClass</key>
<string>Document</string>
</dict>
</array>
<key>LSApplicationCategoryType</key>
<string>public.app-category.finance</string>
<key>NSHumanReadableCopyright</key>
<string>2024</string>
</dict>
</plist>
+2
View File
@@ -0,0 +1,2 @@
!.gitignore
grim
+15 -22
View File
@@ -1,22 +1,21 @@
#!/bin/bash
set -e
case $2 in
case $1 in
x86_64|arm|universal)
;;
*)
echo "Usage: release_macos.sh [version] [platform]\n - platform: 'x86_64', 'arm', 'universal'" >&2
echo "Usage: release_macos.sh [platform] [version]\n - platform: 'x86_64', 'arm', 'universal'" >&2
exit 1
esac
if [[ ! -v SDKROOT ]]; then
if [[ "$OSTYPE" != "darwin"* ]]; then
if [ -z ${SDKROOT+x} ]; then
echo "MacOS SDKROOT is not set"
exit 1
elif [[ -z "SDKROOT" ]]; then
echo "MacOS SDKROOT is set to the empty string"
exit 1
else
else
echo "Use MacOS SDK: ${SDKROOT}"
fi
fi
# Setup build directory
@@ -25,31 +24,25 @@ cd ${BASEDIR}
cd ..
# Setup platform
[[ $1 == "x86_64" ]] && arch+=(x86_64-apple-darwin)
[[ $1 == "arm" ]] && arch+=(aarch64-apple-darwin)
rustup target add x86_64-apple-darwin
rustup target add aarch64-apple-darwin
rm -rf target/x86_64-apple-darwin
rm -rf target/aarch64-apple-darwin
[[ $2 == "x86_64" ]] && arch+=(x86_64-apple-darwin)
[[ $2 == "arm" ]] && arch+=(aarch64-apple-darwin)
[[ $2 == "universal" ]] && arch+=(universal2-apple-darwin)
# Start release build with zig linker for cross-compilation
# zig 0.12+ required
[[ $1 == "universal" ]]; arch+=(universal2-apple-darwin)
cargo install cargo-zigbuild
cargo zigbuild --release --target ${arch}
rm -rf .intentionally-empty-file.o
mkdir macos/Grim.app/Contents/MacOS
rm -f .intentionally-empty-file.o
yes | cp -rf target/${arch}/release/grim macos/Grim.app/Contents/MacOS
### Sign .app resources on change:
# Sign .app resources on change:
#rcodesign generate-self-signed-certificate
#rcodesign sign --pem-file cert.pem macos/Grim.app
# Create release package
FILE_NAME=grim-v$1-macos-$2.zip
rm -rf target/${arch}/release/${FILE_NAME}
FILE_NAME=grim-v$2-macos-$1.zip
cd macos
zip -r ${FILE_NAME} Grim.app
mv ${FILE_NAME} ../target/${arch}/release
+2
View File
@@ -0,0 +1,2 @@
hard_tabs = true
edition = "2024"
+110 -64
View File
@@ -1,81 +1,127 @@
#!/bin/bash
usage="Usage: build_run_android.sh [type] [platform]\n - type: 'debug', 'release'\n - platform: 'v7', 'v8'"
usage="Usage: android.sh [type] [platform|version] [flavor]\n - type: 'build' to run locally, 'lib' - .so for all platforms, 'release' - .apk for all platforms\n - platform, for 'build' type: 'v7', 'v8', 'x86'\n - version for 'lib' and 'release', example: '0.2.2'\n - optional flavor, for non-'lib' type: 'ci' for local maven, default - 'local' for external"
case $1 in
debug|release)
build|lib|release)
;;
*)
printf "$usage"
exit 1
esac
case $2 in
v7|v8)
;;
*)
printf "$usage"
exit 1
esac
# Setup build directory
BASEDIR=$(cd $(dirname $0) && pwd)
cd ${BASEDIR}
cd ..
# Setup release argument
type=$1
[[ ${type} == "release" ]] && release_param="--profile release-apk"
# Setup platform argument
[[ $2 == "v7" ]] && arch+=(armeabi-v7a)
[[ $2 == "v8" ]] && arch+=(arm64-v8a)
# Setup platform path
[[ $2 == "v7" ]] && platform+=(armv7-linux-androideabi)
[[ $2 == "v8" ]] && platform+=(aarch64-linux-android)
# Install platform
[[ $2 == "v7" ]] && rustup target install armv7-linux-androideabi
[[ $2 == "v8" ]] && rustup target install aarch64-linux-android
# Build native code
cargo install cargo-ndk
sed -i -e 's/"rlib"/"cdylib","rlib"/g' Cargo.toml
# temp fix for https://stackoverflow.com/questions/57193895/error-use-of-undeclared-identifier-pthread-mutex-robust-cargo-build-liblmdb-s
success=0
export CPPFLAGS="-DMDB_USE_ROBUST=0" && export CFLAGS="-DMDB_USE_ROBUST=0"
cargo ndk -t ${arch} build ${release_param}
unset CPPFLAGS && unset CFLAGS
cargo ndk -t ${arch} -o android/app/src/main/jniLibs build ${release_param}
if [ $? -eq 0 ]
then
success=1
if [[ $1 == "build" ]]; then
case $2 in
v7|v8|x86)
;;
*)
printf "$usage"
exit 1
esac
fi
sed -i -e 's/"cdylib","rlib"/"rlib"/g' Cargo.toml
# Setup build directory
BASEDIR=$(cd "$(dirname "$0")" && pwd)
cd "${BASEDIR}" || exit 1
cd ..
# Build Android application and launch at all connected devices
if [ $success -eq 1 ]
then
cd android
# Install platforms and tools
rustup target add armv7-linux-androideabi
rustup target add aarch64-linux-android
rustup target add x86_64-linux-android
cargo install cargo-ndk
# Setup gradle argument
[[ $1 == "release" ]] && gradle_param+=(assembleRelease)
[[ $1 == "debug" ]] && gradle_param+=(build)
success=1
### Build native code
function build_lib() {
[[ $1 == "v7" ]] && arch=armeabi-v7a
[[ $1 == "v8" ]] && arch=arm64-v8a
[[ $1 == "x86" ]] && arch=x86_64
sed -i -e 's/"cdylib","rlib"]/"rlib"]/g' Cargo.toml
sed -i -e 's/"rlib"]/"cdylib","rlib"]/g' Cargo.toml
cargo ndk -t "${arch}" -o android/app/src/main/jniLibs build --profile release-apk
if [ $? -ne 0 ]; then
success=0
fi
sed -i -e 's/"cdylib","rlib"]/"rlib"]/g' Cargo.toml
rm -f Cargo.toml-e
}
### Build application
function build_apk() {
flavor=$3
[[ flavor == "" ]] && flavor="local"
cd android || exit 1
./gradlew clean
./gradlew ${gradle_param}
# Build signed apk if keystore exists
if [ ! -f keystore.properties ]; then
./gradlew assemble${flavor}Debug
if [ $? -ne 0 ]; then
success=0
fi
apk_path=app/build/outputs/apk/${flavor}/debug/app-${flavor}-debug.apk
else
./gradlew assemble${flavor}SignedRelease
if [ $? -ne 0 ]; then
success=0
fi
apk_path=app/build/outputs/apk/${flavor}/signedRelease/app-${flavor}-signedRelease.apk
fi
# Setup apk path
[[ $1 == "release" ]] && apk_path+=(app/build/outputs/apk/release/app-release.apk)
[[ $1 == "debug" ]] && apk_path+=(app/build/outputs/apk/debug/app-debug.apk)
if [[ $1 == "" ]] && [ $success -eq 1 ]; then
# Launch application at all connected devices.
for SERIAL in $(adb devices | grep -v List | cut -f 1);
do
adb -s "$SERIAL" install ${apk_path}
sleep 1s
adb -s "$SERIAL" shell am start -n mw.gri.android/.MainActivity;
done
elif [ $success -eq 1 ]; then
# Get version
version=$2
if [[ -z "$version" ]]; then
version=v$(grep -m 1 -Po 'version = "\K[^"]*' ../Cargo.toml)
fi
# Setup release file name
name=grim-${version}-android-$1.apk
[[ $1 == "arm" ]] && name=grim-${version}-android.apk
rm -f "${name}"
mv ${apk_path} "${name}"
# Calculate checksum
checksum=grim-${version}-android-$1-sha256sum.txt
[[ $1 == "arm" ]] && checksum=grim-${version}-android-sha256sum.txt
rm -f "${checksum}"
sha256sum "${name}" > "${checksum}"
fi
for SERIAL in $(adb devices | grep -v List | cut -f 1);
do
adb -s $SERIAL install ${apk_path}
sleep 1s
adb -s $SERIAL shell am start -n mw.gri.android/.MainActivity;
done
fi
cd ..
}
rm -rf android/app/src/main/jniLibs/*
if [[ $1 == "lib" ]]; then
build_lib "v7"
[ $success -eq 1 ] && build_lib "v8"
[ $success -eq 1 ] && build_lib "x86"
[ $success -eq 1 ] && exit 0
elif [[ $1 == "build" ]]; then
build_lib "$2"
[ $success -eq 1 ] && build_apk "" "" "$3"
[ $success -eq 1 ] && exit 0
else
rm -rf target/release-apk
rm -rf target/aarch64-linux-android
rm -rf target/x86_64-linux-android
rm -rf target/armv7-linux-androideabi
build_lib "v7"
[ $success -eq 1 ] && build_lib "v8"
[ $success -eq 1 ] && build_apk "arm" "$2" "$3"
rm -rf android/app/src/main/jniLibs/*
[ $success -eq 1 ] && build_lib "x86"
[ $success -eq 1 ] && build_apk "x86_64" "$2" "$3"
[ $success -eq 1 ] && exit 0
fi
exit 1
+10 -8
View File
@@ -1,25 +1,27 @@
#!/bin/bash
case $1 in
debug|release)
debug|build)
;;
*)
echo "Usage: build_run.sh [type] where is type is 'debug' or 'release'" >&2
echo "Usage: build_run.sh [type] where is type is 'debug' or 'build'" >&2
exit 1
esac
# Setup build directory
BASEDIR=$(cd $(dirname $0) && pwd)
cd ${BASEDIR}
BASEDIR=$(cd "$(dirname $0)" && pwd)
cd "${BASEDIR}" || return
cd ..
# Build application
type=$1
[[ ${type} == "release" ]] && release_param+=(--release)
cargo build ${release_param[@]}
[[ ${type} == "build" ]] && release_param+=(--release)
cargo --config profile.release.incremental=true build "${release_param[@]}"
# Start application
if [ $? -eq 0 ]
then
./target/${type}/grim
fi
path=${type}
[[ ${type} == "build" ]] && path="release"
./target/"${path}"/grim
fi
+102
View File
@@ -0,0 +1,102 @@
#!/bin/bash
# Usage to bump version
# ./version.sh patch
# ./version.sh minor
# ./version.sh major
# Setup base directory
BASEDIR=$(cd $(dirname $0) && pwd)
cd ${BASEDIR}
cd ..
# Exit script if command fails or uninitialized variables used
set -euo pipefail
# ==================================
# Verify repo is clean
# ==================================
# List uncommitted changes and
# check if the output is not empty
if [ -n "$(git status --porcelain)" ]; then
# Print error message
printf "\nError: repo has uncommitted changes\n\n"
# Exit with error code
exit 1
fi
# ==================================
# Get latest version from git tags
# ==================================
# List git tags sorted lexicographically
# so version numbers sorted correctly
GIT_TAGS=$(git tag --sort=version:refname)
# Get last line of output which returns the
# last tag (most recent version)
GIT_TAG_LATEST=$(echo "$GIT_TAGS" | tail -n 1)
# If no tag found, default to v0.1.0
if [ -z "$GIT_TAG_LATEST" ]; then
GIT_TAG_LATEST="v0.1.0"
fi
# Strip prefix 'v' from the tag to easily increment
GIT_TAG_LATEST=$(echo "$GIT_TAG_LATEST" | sed 's/^v//')
# ==================================
# Increment version number
# ==================================
# Get version type from first argument passed to script
VERSION_TYPE="${1-}"
VERSION_NEXT=""
if [ "$VERSION_TYPE" = "patch" ]; then
# Increment patch version
VERSION_NEXT="$(echo "$GIT_TAG_LATEST" | awk -F. '{$NF++; print $1"."$2"."$NF}')"
elif [ "$VERSION_TYPE" = "minor" ]; then
# Increment minor version
VERSION_NEXT="$(echo "$GIT_TAG_LATEST" | awk -F. '{$2++; $3=0; print $1"."$2"."$3}')"
elif [ "$VERSION_TYPE" = "major" ]; then
# Increment major version
VERSION_NEXT="$(echo "$GIT_TAG_LATEST" | awk -F. '{$1++; $2=0; $3=0; print $1"."$2"."$3}')"
else
# Print error for unknown versioning type
printf "\nError: invalid VERSION_TYPE arg passed, must be 'patch', 'minor' or 'major'\n\n"
# Exit with error code
exit 1
fi
# Update MacOS version.
sed -i '' -e 's/'"$GIT_TAG_LATEST"'/'"$VERSION_NEXT"'/' macos/Grim.app/Contents/Info.plist
# Update version for Windows installer.
sed -i '' -e 's/" Version="[^\"]*"/" Version="'"$VERSION_NEXT"'"/g' wix/main.wxs
sed -i '' -e 's/<Package Id="[^\"]*"/<Package Id="'"$(uuidgen)"'"/g' wix/main.wxs
# Update Android version in build.gradle
sed -i'.bak' -e 's/versionName [0-9a-zA-Z -_]*/versionName "'"$VERSION_NEXT"'"/' android/app/build.gradle
rm -f android/app/build.gradle.bak
# Update version in Cargo.toml
sed -i'.bak' -e "s/^version = .*/version = \"$VERSION_NEXT\"/" Cargo.toml
rm -f Cargo.toml.bak
# Update Cargo.lock as this changes when
# updating the version in your manifest
cargo update -p grim
# Commit the changes
git add .
git commit -m "build: version $VERSION_NEXT"
# ==================================
# Create git tag for new version
# ==================================
# Create a tag and push to master branch
#git tag "v$VERSION_NEXT" master
#git push origin master --follow-tags
+70
View File
@@ -0,0 +1,70 @@
@echo off
setlocal enabledelayedexpansion
:: Change directory to the script's location
cd /d "%~dp0"
:: Skip if Go not found.
where go >nul 2>nul
if %ERRORLEVEL% neq 0 (
echo Go could not be found
exit /b 0
)
set "go_os=%~1"
set "go_arch=%~2"
set "output_path=%~3"
echo Go build for os: %go_os%, arch: %go_arch%
:: Setup vars for Android.
if "%go_os%"=="android" (
:: Setup NDK root path env.
if "%ANDROID_NDK_HOME%"=="" (
:: Extract ndkVersion from build.gradle
:: Equivalent to: cat ../android/app/build.gradle | grep 'ndkVersion' | cut -d ' -f 2
for /f "tokens=2 delims='" %%a in ('findstr "ndkVersion" ..\android\app\build.gradle') do (
set "NDK_VERSION=%%a"
)
set "ANDROID_NDK_HOME=%ANDROID_HOME%\ndk\!NDK_VERSION!"
)
:: Setup NDK host path.
:: Since this is a Batch script, the host is Windows.
set "arch_host=windows-x86_64"
:: Setup NDK target arch.
if "%go_arch%"=="arm64" (
set "arch_bin_prefix=aarch64-linux-android"
) else if "%go_arch%"=="arm" (
set "arch_bin_prefix=armv7a-linux-androideabi"
) else (
set "arch_bin_prefix=x86_64-linux-android"
)
:: Build for current target.
set "CGO_ENABLED=1"
set "GOOS=%go_os%"
set "GOARCH=%go_arch%"
:: Define CC and CXX paths
set "CC=!ANDROID_NDK_HOME!\toolchains\llvm\prebuilt\!arch_host!\bin\!arch_bin_prefix!35-clang"
set "CXX=!ANDROID_NDK_HOME!\toolchains\llvm\prebuilt\!arch_host!\bin\!arch_bin_prefix!35-clang++"
go build -C "../tor/webtunnel" -ldflags="-s -w" -o "%output_path%" code.gri.mw/WEB/webtunnel/main/client
) else (
set "extra_flag="
if "%go_os%"=="windows" (
set "extra_flag=-H=windowsgui"
)
set "GOOS=%go_os%"
set "GOARCH=%go_arch%"
:: Build for non-android targets
go build -C "../tor/webtunnel" -ldflags="-s -w !extra_flag!" -o "%output_path%" code.gri.mw/WEB/webtunnel/main/client
)
endlocal
+51
View File
@@ -0,0 +1,51 @@
#!/bin/bash
cd "$(dirname "$0")"
# Skip if Go not found.
if ! command -v go >/dev/null 2>&1
then
echo "Go could not be found"
exit 0
fi
go_os=$1
go_arch=$2
echo "Go build for os: $go_os, arch: $go_arch"
# Setup vars for Android.
if [[ "$go_os" == "android" ]]; then
# Setup NDK root path env.
if [[ -z "$ANDROID_NDK_HOME" ]]; then
NDK_VERSION=$(cat ../android/app/build.gradle | grep 'ndkVersion' | cut -d \' -f 2)
ANDROID_NDK_HOME=$ANDROID_HOME/ndk/$NDK_VERSION
fi
# Setup NDK host path.
if [[ "$(uname)" == "Darwin" ]]; then
arch_host=darwin-x86_64
else
if [[ "$(uname -m)" == "aarch64" ]]; then
arch_host=linux-arm64
else
arch_host=linux-x86_64
fi
fi
# Setup NDK target arch.
if [[ "$go_arch" == "arm64" ]]; then
arch_bin_prefix=aarch64-linux-android
elif [[ "$go_arch" == "arm" ]]; then
arch_bin_prefix=armv7a-linux-androideabi
else
arch_bin_prefix=x86_64-linux-android
fi
# Build for current target.
CGO_ENABLED=1 GOOS=$1 GOARCH=$2 CC="${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/${arch_host}/bin/${arch_bin_prefix}35-clang" CXX="${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/${arch_path}/bin/${arch_bin_prefix}35-clang++" go build -C "../tor/webtunnel" -ldflags="-s -w" -o "$3" code.gri.mw/WEB/webtunnel/main/client
else
if [[ "$go_os" == "windows" ]]; then
extra_flag="-H=windowsgui"
fi
GOOS=$1 GOARCH=$2 go build -C "../tor/webtunnel" -ldflags="-s -w ${extra_flag}" -o "$3" code.gri.mw/WEB/webtunnel/main/client
fi
Regular → Executable
+372 -350
View File
@@ -12,408 +12,430 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use std::sync::atomic::{AtomicBool, Ordering};
use egui::epaint::RectShape;
use egui::{
Align, Context, CornerRadius, CursorIcon, LayerId, Layout, Modifiers, Order, ResizeDirection,
Stroke, StrokeKind, UiBuilder, ViewportCommand,
};
use lazy_static::lazy_static;
use egui::{Align, Context, CursorIcon, Layout, Modifiers, Rect, ResizeDirection, Rounding, Stroke, ViewportCommand};
use egui::epaint::{RectShape};
use egui::os::OperatingSystem;
use std::sync::atomic::{AtomicBool, Ordering};
use crate::AppConfig;
use crate::gui::Colors;
use crate::gui::icons::{ARROWS_IN, ARROWS_OUT, CARET_DOWN, MOON, SUN, X};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Content, TitlePanel, View};
use crate::gui::views::types::ContentContainer;
use crate::gui::views::{Content, KeyboardContent, Modal, TitlePanel, View};
lazy_static! {
/// State to check if platform Back button was pressed.
static ref BACK_BUTTON_PRESSED: AtomicBool = AtomicBool::new(false);
/// State to check if platform Back button was pressed.
static ref BACK_BUTTON_PRESSED: AtomicBool = AtomicBool::new(false);
}
/// Implements ui entry point and contains platform-specific callbacks.
pub struct App<Platform> {
/// Platform specific callbacks handler.
pub(crate) platform: Platform,
/// Handles platform-specific functionality.
pub platform: Platform,
/// Main ui content.
content: Content,
/// Main content.
content: Content,
/// Last window resize direction.
resize_direction: Option<ResizeDirection>
/// Last window resize direction.
resize_direction: Option<ResizeDirection>,
/// Flag to check if it's first draw.
first_draw: bool,
}
impl<Platform: PlatformCallbacks> App<Platform> {
pub fn new(platform: Platform) -> Self {
Self { platform, content: Content::default(), resize_direction: None }
}
pub fn new(platform: Platform) -> Self {
Self {
platform,
content: Content::default(),
resize_direction: None,
first_draw: true,
}
}
/// Draw application content.
pub fn ui(&mut self, ctx: &Context) {
// Handle Esc keyboard key event and platform Back button key event.
let back_pressed = BACK_BUTTON_PRESSED.load(Ordering::Relaxed);
if ctx.input_mut(|i| i.consume_key(Modifiers::NONE, egui::Key::Escape)) || back_pressed {
self.content.on_back();
if back_pressed {
BACK_BUTTON_PRESSED.store(false, Ordering::Relaxed);
}
// Request repaint to update previous content.
ctx.request_repaint();
}
/// Called of first content draw.
fn on_first_draw(&mut self, ctx: &Context) {
// Set platform context.
if View::is_desktop() {
self.platform.set_context(ctx);
}
// Setup visuals.
crate::setup_fonts(ctx);
crate::setup_visuals(ctx);
}
// Handle Close event (on desktop).
if ctx.input(|i| i.viewport().close_requested()) {
if !self.content.exit_allowed {
ctx.send_viewport_cmd(ViewportCommand::CancelClose);
Content::show_exit_modal();
} else {
ctx.input(|i| {
if let Some(rect) = i.viewport().inner_rect {
AppConfig::save_window_size(rect.width(), rect.height());
}
if let Some(rect) = i.viewport().outer_rect {
AppConfig::save_window_pos(rect.left(), rect.top());
}
});
}
}
/// Draw application content.
pub fn ui(&mut self, ctx: &Context) {
if self.first_draw {
self.on_first_draw(ctx);
self.first_draw = false;
}
// Show main content with custom frame on desktop.
egui::CentralPanel::default()
.frame(egui::Frame {
..Default::default()
})
.show(ctx, |ui| {
let is_mac_os = OperatingSystem::from_target_os() == OperatingSystem::Mac;
if View::is_desktop() && !is_mac_os {
self.desktop_window_ui(ui);
} else {
if is_mac_os {
self.window_title_ui(ui);
ui.add_space(-1.0);
}
self.content.ui(ui, &self.platform);
}
});
}
// Handle Esc keyboard key event and platform Back button key event.
let back_pressed = BACK_BUTTON_PRESSED.load(Ordering::Relaxed);
if back_pressed
|| ctx.input_mut(|i| {
i.consume_key(Modifiers::NONE, egui::Key::Escape)
|| i.consume_key(Modifiers::NONE, egui::Key::BrowserBack)
}) {
// Pass event to content.
self.content.on_back(ctx, &self.platform);
if back_pressed {
BACK_BUTTON_PRESSED.store(false, Ordering::Relaxed);
}
// Request repaint to update previous content.
ctx.request_repaint();
}
/// Draw custom resizeable window content.
fn desktop_window_ui(&mut self, ui: &mut egui::Ui) {
let is_fullscreen = ui.ctx().input(|i| {
i.viewport().fullscreen.unwrap_or(false)
});
// Handle Close event on desktop.
if View::is_desktop() && ctx.input(|i| i.viewport().close_requested()) {
if !self.content.exit_allowed {
ctx.send_viewport_cmd(ViewportCommand::CancelClose);
Content::show_exit_modal();
} else {
let (w, h) = View::window_size(ctx);
AppConfig::save_window_size(w, h);
ctx.input(|i| {
if let Some(rect) = i.viewport().outer_rect {
AppConfig::save_window_pos(rect.left(), rect.top());
}
});
}
}
let title_stroke_rect = {
let mut rect = ui.max_rect();
if !is_fullscreen {
rect = rect.shrink(Content::WINDOW_FRAME_MARGIN);
}
rect.max.y = if !is_fullscreen {
Content::WINDOW_FRAME_MARGIN
} else {
0.0
} + Content::WINDOW_TITLE_HEIGHT + TitlePanel::DEFAULT_HEIGHT + 0.5;
rect
};
let title_stroke = RectShape {
rect: title_stroke_rect,
rounding: Rounding {
nw: 8.0,
ne: 8.0,
sw: 0.0,
se: 0.0,
},
fill: Colors::yellow(),
stroke: Stroke {
width: 1.0,
color: egui::Color32::from_gray(200)
},
blur_width: 0.0,
fill_texture_id: Default::default(),
uv: Rect::ZERO
};
// Draw title stroke.
ui.painter().add(title_stroke);
// Show main content.
egui::CentralPanel::default()
.frame(egui::Frame {
..Default::default()
})
.show(ctx, |ui| {
if View::is_desktop() {
let is_fullscreen =
ui.ctx().input(|i| i.viewport().fullscreen.unwrap_or(false));
let os = egui::os::OperatingSystem::from_target_os();
match os {
egui::os::OperatingSystem::Mac => {
self.window_title_ui(ui, is_fullscreen);
ui.add_space(-1.0);
Self::title_panel_bg(ui, true);
self.content.ui(ui, &self.platform);
}
egui::os::OperatingSystem::Windows => {
Self::title_panel_bg(ui, false);
self.content.ui(ui, &self.platform);
}
_ => {
self.custom_frame_ui(ui, is_fullscreen);
}
}
} else {
Self::title_panel_bg(ui, false);
self.content.ui(ui, &self.platform);
}
});
let content_stroke_rect = {
let mut rect = ui.max_rect();
if !is_fullscreen {
rect = rect.shrink(Content::WINDOW_FRAME_MARGIN);
}
let top = Content::WINDOW_TITLE_HEIGHT + TitlePanel::DEFAULT_HEIGHT + 0.5;
rect.min += egui::vec2(0.0, top);
rect
};
let content_stroke = RectShape {
rect: content_stroke_rect,
rounding: Rounding::ZERO,
fill: Colors::fill(),
stroke: Stroke {
width: 1.0,
color: Colors::stroke()
},
blur_width: 0.0,
fill_texture_id: Default::default(),
uv: Rect::ZERO
};
// Draw content stroke.
ui.painter().add(content_stroke);
// Check if desktop window was focused after requested attention.
if self.platform.user_attention_required()
&& ctx.input(|i| i.viewport().focused.unwrap_or(true))
{
self.platform.clear_user_attention();
}
// Draw window content.
let mut content_rect = ui.max_rect();
if !is_fullscreen {
content_rect = content_rect.shrink(Content::WINDOW_FRAME_MARGIN);
}
ui.allocate_ui_at_rect(content_rect, |ui| {
self.window_title_ui(ui);
self.window_content(ui);
});
// Show modal or keyboard window above opened Modal.
if Modal::opened().is_some() {
ctx.move_to_top(LayerId::new(Order::Middle, egui::Id::new(Modal::WINDOW_ID)));
let keyboard_showing = if let Some(l) = ctx.top_layer_id() {
l.id == egui::Id::new(KeyboardContent::WINDOW_ID)
} else {
false
};
if keyboard_showing {
ctx.move_to_top(LayerId::new(
Order::Middle,
egui::Id::new(KeyboardContent::WINDOW_ID),
));
}
}
// Reset keyboard state for newly opened modal.
if Modal::first_draw() {
KeyboardContent::reset_window_state();
}
}
// Setup resize areas.
if !is_fullscreen {
self.resize_area_ui(ui, ResizeDirection::North);
self.resize_area_ui(ui, ResizeDirection::East);
self.resize_area_ui(ui, ResizeDirection::South);
self.resize_area_ui(ui, ResizeDirection::West);
self.resize_area_ui(ui, ResizeDirection::NorthWest);
self.resize_area_ui(ui, ResizeDirection::NorthEast);
self.resize_area_ui(ui, ResizeDirection::SouthEast);
self.resize_area_ui(ui, ResizeDirection::SouthWest);
}
}
/// Draw custom desktop window frame content.
fn custom_frame_ui(&mut self, ui: &mut egui::Ui, is_fullscreen: bool) {
let content_bg_rect = {
let mut r = ui.max_rect();
if !is_fullscreen {
r = r.shrink(Content::WINDOW_FRAME_MARGIN);
}
r.min.y += Content::WINDOW_TITLE_HEIGHT + TitlePanel::HEIGHT;
r
};
let content_bg = RectShape::new(
content_bg_rect,
CornerRadius::ZERO,
Colors::fill_lite(),
View::default_stroke(),
StrokeKind::Outside,
);
// Draw content background.
ui.painter().add(content_bg);
/// Draw window content for desktop.
fn window_content(&mut self, ui: &mut egui::Ui) {
let content_rect = {
let mut rect = ui.max_rect();
rect.min.y += Content::WINDOW_TITLE_HEIGHT;
rect
};
// Draw main content.
let mut content_ui = ui.child_ui(content_rect, *ui.layout(), None);
self.content.ui(&mut content_ui, &self.platform);
}
let mut content_rect = ui.max_rect();
if !is_fullscreen {
content_rect = content_rect.shrink(Content::WINDOW_FRAME_MARGIN);
}
// Draw window content.
ui.scope_builder(UiBuilder::new().max_rect(content_rect), |ui| {
// Draw window title.
self.window_title_ui(ui, is_fullscreen);
ui.add_space(-1.0);
/// Draw custom window title content.
fn window_title_ui(&self, ui: &mut egui::Ui) {
let content_rect = ui.max_rect();
// Draw title panel background.
Self::title_panel_bg(ui, true);
let title_rect = {
let mut rect = content_rect;
rect.max.y = rect.min.y + Content::WINDOW_TITLE_HEIGHT;
rect
};
let content_rect = {
let mut rect = ui.max_rect();
rect.min.y += Content::WINDOW_TITLE_HEIGHT;
rect
};
let mut content_ui =
ui.new_child(UiBuilder::new().max_rect(content_rect).layout(*ui.layout()));
// Draw main content.
self.content.ui(&mut content_ui, &self.platform);
});
let is_fullscreen = ui.ctx().input(|i| {
i.viewport().fullscreen.unwrap_or(false)
});
// Setup resize areas.
if !is_fullscreen {
self.resize_area_ui(ui, ResizeDirection::North);
self.resize_area_ui(ui, ResizeDirection::East);
self.resize_area_ui(ui, ResizeDirection::South);
self.resize_area_ui(ui, ResizeDirection::West);
self.resize_area_ui(ui, ResizeDirection::NorthWest);
self.resize_area_ui(ui, ResizeDirection::NorthEast);
self.resize_area_ui(ui, ResizeDirection::SouthEast);
self.resize_area_ui(ui, ResizeDirection::SouthWest);
}
}
let window_title_bg = RectShape {
rect: title_rect,
rounding: if is_fullscreen {
Rounding::ZERO
} else {
Rounding {
nw: 8.0,
ne: 8.0,
sw: 0.0,
se: 0.0,
}
},
fill: Colors::yellow_dark(),
stroke: Stroke::NONE,
blur_width: 0.0,
fill_texture_id: Default::default(),
uv: Rect::ZERO
};
// Draw title background.
ui.painter().add(window_title_bg);
/// Draw title panel background.
fn title_panel_bg(ui: &mut egui::Ui, window_title: bool) {
let title_rect = {
let mut rect = ui.max_rect();
if window_title {
rect.min.y += Content::WINDOW_TITLE_HEIGHT - 0.5;
}
rect.max.y = rect.min.y + View::get_top_inset() + TitlePanel::HEIGHT;
rect
};
let title_bg = RectShape::filled(title_rect, CornerRadius::ZERO, Colors::yellow());
ui.painter().add(title_bg);
}
let painter = ui.painter();
/// Draw custom window title content.
fn window_title_ui(&self, ui: &mut egui::Ui, is_fullscreen: bool) {
let title_rect = {
let mut rect = ui.max_rect();
rect.max.y = rect.min.y + Content::WINDOW_TITLE_HEIGHT;
rect
};
let interact_rect = {
let mut rect = title_rect;
if !is_fullscreen {
rect.min.y += Content::WINDOW_FRAME_MARGIN;
}
rect
};
let title_resp = ui.interact(
interact_rect,
egui::Id::new("window_title"),
egui::Sense::click_and_drag(),
);
let title_bg_rect = {
let mut r = title_rect.clone();
r.max.y += TitlePanel::HEIGHT - 1.0;
r
};
let is_mac = egui::os::OperatingSystem::from_target_os() == egui::os::OperatingSystem::Mac;
let window_title_bg = RectShape::new(
title_bg_rect,
if is_fullscreen || is_mac {
CornerRadius::ZERO
} else {
CornerRadius {
nw: 8.0 as u8,
ne: 8.0 as u8,
sw: 0.0 as u8,
se: 0.0 as u8,
}
},
Colors::yellow_dark(),
Stroke::new(1.0, Colors::STROKE),
StrokeKind::Outside,
);
// Draw title background.
ui.painter().add(window_title_bg);
// Paint the title.
let dual_wallets_panel =
ui.available_width() >= (Content::SIDE_PANEL_WIDTH * 3.0) + View::get_right_inset();
let wallet_panel_opened = self.content.wallets.wallet_panel_opened();
let hide_app_name = if dual_wallets_panel {
!wallet_panel_opened || (AppConfig::show_wallets_at_dual_panel() &&
self.content.wallets.showing_wallet() && !self.content.wallets.creating_wallet())
} else if Content::is_dual_panel_mode(ui) {
!wallet_panel_opened
} else {
!Content::is_network_panel_open() && !wallet_panel_opened
};
let title_text = if hide_app_name {
"".to_string()
} else {
format!("Grim {}", crate::VERSION)
};
painter.text(
title_rect.center(),
egui::Align2::CENTER_CENTER,
title_text,
egui::FontId::proportional(15.0),
Colors::title(true),
);
let painter = ui.painter();
// Interact with the window title (drag to move window):
if !is_fullscreen && title_resp.double_clicked() {
ui.ctx().send_viewport_cmd(ViewportCommand::Fullscreen(!is_fullscreen));
}
let interact_rect = {
let mut rect = title_rect.clone();
rect.max.x -= 128.0;
rect.min.x += 85.0;
if !is_fullscreen {
rect.min.y += Content::WINDOW_FRAME_MARGIN;
}
rect
};
let title_resp = ui.interact(
interact_rect,
egui::Id::new("window_title"),
egui::Sense::drag(),
);
// Interact with the window title (drag to move window):
if !is_fullscreen && title_resp.drag_started_by(egui::PointerButton::Primary) {
ui.ctx().send_viewport_cmd(ViewportCommand::StartDrag);
}
if !is_fullscreen && title_resp.drag_started_by(egui::PointerButton::Primary) {
ui.ctx().send_viewport_cmd(ViewportCommand::StartDrag);
}
// Paint the title.
let title_text = format!("Grim {}", crate::VERSION);
painter.text(
title_rect.center(),
egui::Align2::CENTER_CENTER,
title_text,
egui::FontId::proportional(15.0),
Colors::title(true),
);
ui.allocate_ui_at_rect(title_rect, |ui| {
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
// Draw button to close window.
View::title_button_small(ui, X, |_| {
Content::show_exit_modal();
});
ui.scope_builder(UiBuilder::new().max_rect(title_rect), |ui| {
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
// Draw button to close window.
View::title_button_small(ui, X, |_| {
if Modal::opened().is_none() || Modal::opened_closeable() {
Content::show_exit_modal();
}
});
// Draw fullscreen button.
let fullscreen_icon = if is_fullscreen {
ARROWS_IN
} else {
ARROWS_OUT
};
View::title_button_small(ui, fullscreen_icon, |ui| {
ui.ctx().send_viewport_cmd(ViewportCommand::Fullscreen(!is_fullscreen));
});
// Draw fullscreen button.
let fullscreen_icon = if is_fullscreen { ARROWS_IN } else { ARROWS_OUT };
View::title_button_small(ui, fullscreen_icon, |ui| {
ui.ctx()
.send_viewport_cmd(ViewportCommand::Fullscreen(!is_fullscreen));
});
// Draw button to minimize window.
View::title_button_small(ui, CARET_DOWN, |ui| {
ui.ctx().send_viewport_cmd(ViewportCommand::Minimized(true));
});
// Draw button to minimize window.
View::title_button_small(ui, CARET_DOWN, |ui| {
ui.ctx().send_viewport_cmd(ViewportCommand::Minimized(true));
});
// Draw application icon.
let layout_size = ui.available_size();
ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| {
// Draw button to minimize window.
let use_dark = AppConfig::dark_theme().unwrap_or(false);
let theme_icon = if use_dark {
SUN
} else {
MOON
};
View::title_button_small(ui, theme_icon, |ui| {
AppConfig::set_dark_theme(!use_dark);
crate::setup_visuals(ui.ctx());
});
});
});
});
}
// Draw application icon.
let layout_size = ui.available_size();
ui.allocate_ui_with_layout(
layout_size,
Layout::left_to_right(Align::Center),
|ui| {
// Draw button to minimize window.
let use_dark = AppConfig::dark_theme().unwrap_or(false);
let theme_icon = if use_dark { SUN } else { MOON };
View::title_button_small(ui, theme_icon, |ui| {
AppConfig::set_dark_theme(!use_dark);
crate::setup_visuals(ui.ctx());
});
},
);
});
});
}
/// Setup window resize area.
fn resize_area_ui(&mut self, ui: &egui::Ui, direction: ResizeDirection) {
let mut rect = ui.max_rect();
/// Setup window resize area.
fn resize_area_ui(&mut self, ui: &egui::Ui, direction: ResizeDirection) {
let mut rect = ui.max_rect();
// Setup area id, cursor and area rect based on direction.
let (id, cursor, rect) = match direction {
ResizeDirection::North => ("n", CursorIcon::ResizeNorth, {
rect.min.x += Content::WINDOW_FRAME_MARGIN * 2.0;
rect.max.y = rect.min.y + Content::WINDOW_FRAME_MARGIN;
rect.max.x -= Content::WINDOW_FRAME_MARGIN * 2.0;
rect
}),
ResizeDirection::East => ("e", CursorIcon::ResizeEast, {
rect.min.y += Content::WINDOW_FRAME_MARGIN * 2.0;
rect.min.x = rect.max.x - Content::WINDOW_FRAME_MARGIN;
rect.max.y -= Content::WINDOW_FRAME_MARGIN * 2.0;
rect
}),
ResizeDirection::South => ("s", CursorIcon::ResizeSouth, {
rect.min.x += Content::WINDOW_FRAME_MARGIN * 2.0;
rect.min.y = rect.max.y - Content::WINDOW_FRAME_MARGIN;
rect.max.x -= Content::WINDOW_FRAME_MARGIN * 2.0;
rect
}),
ResizeDirection::West => ("w", CursorIcon::ResizeWest, {
rect.min.y += Content::WINDOW_FRAME_MARGIN * 2.0;
rect.max.x = rect.min.x + Content::WINDOW_FRAME_MARGIN;
rect.max.y -= Content::WINDOW_FRAME_MARGIN * 2.0;
rect
}),
ResizeDirection::NorthWest => ("nw", CursorIcon::ResizeNorthWest, {
rect.max.y = rect.min.y + Content::WINDOW_FRAME_MARGIN * 2.0;
rect.max.x = rect.max.y + Content::WINDOW_FRAME_MARGIN * 2.0;
rect
}),
ResizeDirection::NorthEast => ("ne", CursorIcon::ResizeNorthEast, {
rect.min.x = rect.max.x - Content::WINDOW_FRAME_MARGIN * 2.0;
rect.max.y = Content::WINDOW_FRAME_MARGIN * 2.0;
rect
}),
ResizeDirection::SouthEast => ("se", CursorIcon::ResizeSouthEast, {
rect.min.y = rect.max.y - Content::WINDOW_FRAME_MARGIN * 2.0;
rect.min.x = rect.max.x - Content::WINDOW_FRAME_MARGIN * 2.0;
rect
}),
ResizeDirection::SouthWest => ("sw", CursorIcon::ResizeSouthWest, {
rect.min.y = rect.max.y - Content::WINDOW_FRAME_MARGIN * 2.0;
rect.max.x = rect.min.x + Content::WINDOW_FRAME_MARGIN * 2.0;
rect
}),
};
// Setup area id, cursor and area rect based on direction.
let (id, cursor, rect) = match direction {
ResizeDirection::North => ("n", CursorIcon::ResizeNorth, {
rect.min.x += Content::WINDOW_FRAME_MARGIN * 2.0;
rect.max.y = rect.min.y + Content::WINDOW_FRAME_MARGIN;
rect.max.x -= Content::WINDOW_FRAME_MARGIN * 2.0;
rect
}),
ResizeDirection::East => ("e", CursorIcon::ResizeEast, {
rect.min.y += Content::WINDOW_FRAME_MARGIN * 2.0;
rect.min.x = rect.max.x - Content::WINDOW_FRAME_MARGIN;
rect.max.y -= Content::WINDOW_FRAME_MARGIN * 2.0;
rect
}),
ResizeDirection::South => ("s", CursorIcon::ResizeSouth, {
rect.min.x += Content::WINDOW_FRAME_MARGIN * 2.0;
rect.min.y = rect.max.y - Content::WINDOW_FRAME_MARGIN;
rect.max.x -= Content::WINDOW_FRAME_MARGIN * 2.0;
rect
}),
ResizeDirection::West => ("w", CursorIcon::ResizeWest, {
rect.min.y += Content::WINDOW_FRAME_MARGIN * 2.0;
rect.max.x = rect.min.x + Content::WINDOW_FRAME_MARGIN;
rect.max.y -= Content::WINDOW_FRAME_MARGIN * 2.0;
rect
}),
ResizeDirection::NorthWest => ("nw", CursorIcon::ResizeNorthWest, {
rect.max.y = rect.min.y + Content::WINDOW_FRAME_MARGIN * 2.0;
rect.max.x = rect.max.y + Content::WINDOW_FRAME_MARGIN * 2.0;
rect
}),
ResizeDirection::NorthEast => ("ne", CursorIcon::ResizeNorthEast, {
rect.min.x = rect.max.x - Content::WINDOW_FRAME_MARGIN * 2.0;
rect.max.y = Content::WINDOW_FRAME_MARGIN * 2.0;
rect
}),
ResizeDirection::SouthEast => ("se", CursorIcon::ResizeSouthEast, {
rect.min.y = rect.max.y - Content::WINDOW_FRAME_MARGIN * 2.0;
rect.min.x = rect.max.x - Content::WINDOW_FRAME_MARGIN * 2.0;
rect
}),
ResizeDirection::SouthWest => ("sw", CursorIcon::ResizeSouthWest, {
rect.min.y = rect.max.y - Content::WINDOW_FRAME_MARGIN * 2.0;
rect.max.x = rect.min.x + Content::WINDOW_FRAME_MARGIN * 2.0;
rect
}),
};
// Setup resize area.
let id = egui::Id::new("window_resize").with(id);
let sense = egui::Sense::drag();
let area_resp = ui.interact(rect, id, sense).on_hover_cursor(cursor);
if area_resp.dragged() {
if self.resize_direction.is_none() {
self.resize_direction = Some(direction.clone());
ui.ctx().send_viewport_cmd(ViewportCommand::BeginResize(direction));
}
}
if area_resp.drag_stopped() {
self.resize_direction = None;
}
}
// Setup resize area.
let id = egui::Id::new("window_resize").with(id);
let sense = egui::Sense::drag();
let area_resp = ui.interact(rect, id, sense).on_hover_cursor(cursor);
if area_resp.dragged() {
if self.resize_direction.is_none() {
self.resize_direction = Some(direction.clone());
ui.ctx()
.send_viewport_cmd(ViewportCommand::BeginResize(direction));
}
}
if area_resp.drag_stopped() {
self.resize_direction = None;
}
}
}
/// To draw with egui`s eframe (for wgpu, glow backends and wasm target).
impl<Platform: PlatformCallbacks> eframe::App for App<Platform> {
fn update(&mut self, ctx: &Context, _: &mut eframe::Frame) {
self.ui(ctx);
}
fn update(&mut self, ctx: &Context, _: &mut eframe::Frame) {
ctx.plugin_or_default::<egui_async::EguiAsyncPlugin>();
self.ui(ctx);
}
fn clear_color(&self, _visuals: &egui::Visuals) -> [f32; 4] {
if View::is_desktop() {
let is_mac_os = OperatingSystem::from_target_os() == OperatingSystem::Mac;
if is_mac_os {
Colors::fill().to_normalized_gamma_f32()
} else {
egui::Rgba::TRANSPARENT.to_array()
}
} else {
Colors::fill().to_normalized_gamma_f32()
}
}
fn clear_color(&self, _visuals: &egui::Visuals) -> [f32; 4] {
let os = egui::os::OperatingSystem::from_target_os();
let is_win = os == egui::os::OperatingSystem::Windows;
let is_mac = os == egui::os::OperatingSystem::Mac;
if !View::is_desktop() || is_win || is_mac {
return Colors::fill_lite().to_normalized_gamma_f32();
}
Colors::TRANSPARENT.to_normalized_gamma_f32()
}
}
#[allow(dead_code)]
#[cfg(target_os = "android")]
#[allow(non_snake_case)]
#[no_mangle]
#[unsafe(no_mangle)]
/// Handle Back key code event from Android.
pub extern "C" fn Java_mw_gri_android_MainActivity_onBack(
_env: jni::JNIEnv,
_class: jni::objects::JObject,
_activity: jni::objects::JObject,
_env: jni::JNIEnv,
_class: jni::objects::JObject,
_activity: jni::objects::JObject,
) {
BACK_BUTTON_PRESSED.store(true, Ordering::Relaxed);
}
BACK_BUTTON_PRESSED.store(true, Ordering::Relaxed);
}
+141 -157
View File
@@ -26,21 +26,31 @@ const SEMI_TRANSPARENT: Color32 = Color32::from_black_alpha(100);
const DARK_SEMI_TRANSPARENT: Color32 = Color32::from_black_alpha(170);
const GOLD: Color32 = Color32::from_rgb(255, 215, 0);
const GOLD_DARK: Color32 = Color32::from_rgb(240, 203, 1);
const YELLOW: Color32 = Color32::from_rgb(254, 241, 2);
const YELLOW_DARK: Color32 = Color32::from_rgb(239, 229, 3);
const GREEN: Color32 = Color32::from_rgb(0, 0x64, 0);
const GREEN_DARK: Color32 = Color32::from_rgb(0, (0x64 as f32 * 1.3 + 0.5) as u8, 0);
const RED: Color32 = Color32::from_rgb(0x8B, 0, 0);
const RED_DARK: Color32 = Color32::from_rgb((0x8B as f32 * 1.3 + 0.5) as u8, 50, 30);
const BLUE: Color32 = Color32::from_rgb(0, 0x66, 0xE4);
const BLUE_DARK: Color32 = Color32::from_rgb(
0,
(0x66 as f32 * 1.3 + 0.5) as u8,
(0xE4 as f32 * 1.3 + 0.5) as u8,
);
const FILL: Color32 = Color32::from_gray(244);
const FILL_DARK: Color32 = Color32::from_gray(24);
const FILL_DARK: Color32 = Color32::from_gray(26);
const FILL_DEEP: Color32 = Color32::from_gray(238);
const FILL_DEEP_DARK: Color32 = Color32::from_gray(18);
const FILL_DEEP_DARK: Color32 = Color32::from_gray(32);
const FILL_LITE: Color32 = Color32::from_gray(249);
const FILL_LITE_DARK: Color32 = Color32::from_gray(21);
const TEXT: Color32 = Color32::from_gray(80);
const TEXT_DARK: Color32 = Color32::from_gray(185);
@@ -54,13 +64,9 @@ const TEXT_BUTTON_DARK: Color32 = Color32::from_gray(195);
const TITLE: Color32 = Color32::from_gray(60);
const TITLE_DARK: Color32 = Color32::from_gray(205);
const BUTTON: Color32 = Color32::from_gray(249);
const BUTTON_DARK: Color32 = Color32::from_gray(16);
const GRAY: Color32 = Color32::from_gray(120);
const GRAY_DARK: Color32 = Color32::from_gray(145);
const STROKE: Color32 = Color32::from_gray(200);
const STROKE_DARK: Color32 = Color32::from_gray(50);
const INACTIVE_TEXT: Color32 = Color32::from_gray(150);
@@ -77,177 +83,155 @@ const ITEM_HOVER_DARK: Color32 = Color32::from_gray(48);
/// Check if dark theme should be used.
fn use_dark() -> bool {
AppConfig::dark_theme().unwrap_or(false)
AppConfig::dark_theme().unwrap_or(false)
}
impl Colors {
pub const TRANSPARENT: Color32 = Color32::from_rgba_premultiplied(0, 0, 0, 0);
pub const FILL_DEEP: Color32 = Color32::from_gray(238);
pub const TRANSPARENT: Color32 = Color32::from_rgba_premultiplied(0, 0, 0, 0);
pub const STROKE: Color32 = Color32::from_gray(200);
pub fn white_or_black(black_in_white: bool) -> Color32 {
if use_dark() {
if black_in_white {
WHITE
} else {
BLACK
}
} else {
if black_in_white {
BLACK
} else {
WHITE
}
}
}
pub fn white_or_black(black_in_white: bool) -> Color32 {
if use_dark() {
if black_in_white { WHITE } else { BLACK }
} else {
if black_in_white { BLACK } else { WHITE }
}
}
pub fn semi_transparent() -> Color32 {
if use_dark() {
DARK_SEMI_TRANSPARENT
} else {
SEMI_TRANSPARENT
}
}
pub fn semi_transparent() -> Color32 {
if use_dark() {
DARK_SEMI_TRANSPARENT
} else {
SEMI_TRANSPARENT
}
}
pub fn gold() -> Color32 {
if use_dark() {
GOLD.gamma_multiply(0.9)
} else {
GOLD
}
}
pub fn gold() -> Color32 {
if use_dark() {
GOLD.gamma_multiply(0.9)
} else {
GOLD
}
}
pub fn yellow() -> Color32 {
YELLOW
}
pub fn gold_dark() -> Color32 {
if use_dark() {
GOLD_DARK.gamma_multiply(0.9)
} else {
GOLD_DARK
}
}
pub fn yellow_dark() -> Color32 {
YELLOW_DARK
}
pub fn yellow() -> Color32 {
YELLOW
}
pub fn green() -> Color32 {
if use_dark() {
GREEN.gamma_multiply(1.3)
} else {
GREEN
}
}
pub fn yellow_dark() -> Color32 {
YELLOW_DARK
}
pub fn red() -> Color32 {
if use_dark() {
RED.gamma_multiply(1.3)
} else {
RED
}
}
pub fn green() -> Color32 {
if use_dark() { GREEN_DARK } else { GREEN }
}
pub fn blue() -> Color32 {
if use_dark() {
BLUE.gamma_multiply(1.3)
} else {
BLUE
}
}
pub fn red() -> Color32 {
if use_dark() { RED_DARK } else { RED }
}
pub fn fill() -> Color32 {
if use_dark() {
FILL_DARK
} else {
FILL
}
}
pub fn blue() -> Color32 {
if use_dark() { BLUE_DARK } else { BLUE }
}
pub fn fill_deep() -> Color32 {
if use_dark() {
FILL_DEEP_DARK
} else {
FILL_DEEP
}
}
pub fn fill() -> Color32 {
if use_dark() { FILL_DARK } else { FILL }
}
pub fn checkbox() -> Color32 {
if use_dark() {
CHECKBOX_DARK
} else {
CHECKBOX
}
}
pub fn fill_deep() -> Color32 {
if use_dark() {
FILL_DEEP_DARK
} else {
Self::FILL_DEEP
}
}
pub fn text(always_light: bool) -> Color32 {
if use_dark() && !always_light {
TEXT_DARK
} else {
TEXT
}
}
pub fn fill_lite() -> Color32 {
if use_dark() {
FILL_LITE_DARK
} else {
FILL_LITE
}
}
pub fn text_button() -> Color32 {
if use_dark() {
TEXT_BUTTON_DARK
} else {
TEXT_BUTTON
}
}
pub fn checkbox() -> Color32 {
if use_dark() { CHECKBOX_DARK } else { CHECKBOX }
}
pub fn title(always_light: bool) -> Color32 {
if use_dark() && !always_light {
TITLE_DARK
} else {
TITLE
}
}
pub fn text(always_light: bool) -> Color32 {
if use_dark() && !always_light {
TEXT_DARK
} else {
TEXT
}
}
pub fn button() -> Color32 {
if use_dark() {
BUTTON_DARK
} else {
BUTTON
}
}
pub fn text_button() -> Color32 {
if use_dark() {
TEXT_BUTTON_DARK
} else {
TEXT_BUTTON
}
}
pub fn gray() -> Color32 {
if use_dark() {
GRAY_DARK
} else {
GRAY
}
}
pub fn title(always_light: bool) -> Color32 {
if use_dark() && !always_light {
TITLE_DARK
} else {
TITLE
}
}
pub fn stroke() -> Color32 {
if use_dark() {
STROKE_DARK
} else {
STROKE
}
}
pub fn gray() -> Color32 {
if use_dark() { GRAY_DARK } else { GRAY }
}
pub fn inactive_text() -> Color32 {
if use_dark() {
INACTIVE_TEXT_DARK
} else {
INACTIVE_TEXT
}
}
pub fn stroke() -> Color32 {
if use_dark() {
STROKE_DARK
} else {
Self::STROKE
}
}
pub fn item_button() -> Color32 {
if use_dark() {
ITEM_BUTTON_DARK
} else {
ITEM_BUTTON
}
}
pub fn inactive_text() -> Color32 {
if use_dark() {
INACTIVE_TEXT_DARK
} else {
INACTIVE_TEXT
}
}
pub fn item_stroke() -> Color32 {
if use_dark() {
ITEM_STROKE_DARK
} else {
ITEM_STROKE
}
}
pub fn item_button_text() -> Color32 {
if use_dark() {
ITEM_BUTTON_DARK
} else {
ITEM_BUTTON
}
}
pub fn item_hover() -> Color32 {
if use_dark() {
ITEM_HOVER_DARK
} else {
ITEM_HOVER
}
}
}
pub fn item_stroke() -> Color32 {
if use_dark() {
ITEM_STROKE_DARK
} else {
ITEM_STROKE
}
}
pub fn item_hover() -> Color32 {
if use_dark() {
ITEM_HOVER_DARK
} else {
ITEM_HOVER
}
}
}
+1 -2
View File
@@ -12,13 +12,12 @@
// See the License for the specific language governing permissions and
// limitations under the License.
mod app;
pub use app::App;
mod colors;
pub use colors::Colors;
pub mod icons;
pub mod platform;
pub mod views;
pub mod icons;
+181 -149
View File
@@ -12,14 +12,14 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use lazy_static::lazy_static;
use parking_lot::RwLock;
use std::env;
use std::ffi::OsString;
use std::fs::File;
use std::io::Write;
use std::path::PathBuf;
use lazy_static::lazy_static;
use std::sync::Arc;
use parking_lot::RwLock;
use jni::JNIEnv;
use jni::objects::{JByteArray, JObject, JString, JValue};
@@ -30,185 +30,217 @@ use crate::gui::platform::PlatformCallbacks;
/// Android platform implementation.
#[derive(Clone)]
pub struct Android {
android_app: AndroidApp,
/// Android related state.
android_app: AndroidApp,
/// Context to repaint content and handle viewport commands.
ctx: Arc<RwLock<Option<egui::Context>>>,
}
impl Android {
/// Create new Android platform instance from provided [`AndroidApp`].
pub fn new(app: AndroidApp) -> Self {
Self {
android_app: app,
}
}
/// Create new Android platform instance from provided [`AndroidApp`].
pub fn new(app: AndroidApp) -> Self {
Self {
android_app: app,
ctx: Arc::new(RwLock::new(None)),
}
}
/// Call Android Activity method with JNI.
pub fn call_java_method(&self, name: &str, s: &str, a: &[JValue]) -> Option<jni::sys::jvalue> {
let vm = unsafe { jni::JavaVM::from_raw(self.android_app.vm_as_ptr() as _) }.unwrap();
let mut env = vm.attach_current_thread().unwrap();
let activity = unsafe {
JObject::from_raw(self.android_app.activity_as_ptr() as jni::sys::jobject)
};
if let Ok(result) = env.call_method(activity, name, s, a) {
return Some(result.as_jni().clone());
}
None
}
/// Call Android Activity method with JNI.
pub fn call_java_method(&self, name: &str, s: &str, a: &[JValue]) -> Option<jni::sys::jvalue> {
let vm = unsafe { jni::JavaVM::from_raw(self.android_app.vm_as_ptr() as _) }.unwrap();
let mut env = vm.attach_current_thread().unwrap();
let activity =
unsafe { JObject::from_raw(self.android_app.activity_as_ptr() as jni::sys::jobject) };
if let Ok(result) = env.call_method(activity, name, s, a) {
return Some(result.as_jni().clone());
}
None
}
}
impl PlatformCallbacks for Android {
fn show_keyboard(&self) {
// Disable NDK soft input show call before fix for egui.
// self.android_app.show_soft_input(false);
fn set_context(&mut self, ctx: &egui::Context) {
let mut w_ctx = self.ctx.write();
*w_ctx = Some(ctx.clone());
}
self.call_java_method("showKeyboard", "()V", &[]).unwrap();
}
fn exit(&self) {
let _ = self.call_java_method("exit", "()V", &[]);
}
fn hide_keyboard(&self) {
// Disable NDK soft input hide call before fix for egui.
// self.android_app.hide_soft_input(false);
fn copy_string_to_buffer(&self, data: String) {
let vm = unsafe { jni::JavaVM::from_raw(self.android_app.vm_as_ptr() as _) }.unwrap();
let env = vm.attach_current_thread().unwrap();
let arg_value = env.new_string(data).unwrap();
let _ = self.call_java_method(
"copyText",
"(Ljava/lang/String;)V",
&[JValue::Object(&JObject::from(arg_value))],
);
}
self.call_java_method("hideKeyboard", "()V", &[]).unwrap();
}
fn get_string_from_buffer(&self) -> String {
let result = self
.call_java_method("pasteText", "()Ljava/lang/String;", &[])
.unwrap();
let vm = unsafe { jni::JavaVM::from_raw(self.android_app.vm_as_ptr() as _) }.unwrap();
let mut env = vm.attach_current_thread().unwrap();
let j_object: jni::sys::jobject = unsafe { result.l };
let paste_data: String = unsafe {
env.get_string(JString::from(JObject::from_raw(j_object)).as_ref())
.unwrap()
.into()
};
paste_data
}
fn copy_string_to_buffer(&self, data: String) {
let vm = unsafe { jni::JavaVM::from_raw(self.android_app.vm_as_ptr() as _) }.unwrap();
let env = vm.attach_current_thread().unwrap();
let arg_value = env.new_string(data).unwrap();
self.call_java_method("copyText",
"(Ljava/lang/String;)V",
&[JValue::Object(&JObject::from(arg_value))]).unwrap();
}
fn start_camera(&self) {
// Clear image.
let mut w_image = LAST_CAMERA_IMAGE.write();
*w_image = None;
// Start camera.
let _ = self.call_java_method("startCamera", "()V", &[]);
}
fn get_string_from_buffer(&self) -> String {
let result = self.call_java_method("pasteText", "()Ljava/lang/String;", &[]).unwrap();
let vm = unsafe { jni::JavaVM::from_raw(self.android_app.vm_as_ptr() as _) }.unwrap();
let mut env = vm.attach_current_thread().unwrap();
let j_object: jni::sys::jobject = unsafe { result.l };
let paste_data: String = unsafe {
env.get_string(JString::from(JObject::from_raw(j_object)).as_ref()).unwrap().into()
};
paste_data
}
fn stop_camera(&self) {
// Stop camera.
let _ = self.call_java_method("stopCamera", "()V", &[]);
// Clear image.
let mut w_image = LAST_CAMERA_IMAGE.write();
*w_image = None;
}
fn start_camera(&self) {
// Clear image.
let mut w_image = LAST_CAMERA_IMAGE.write();
*w_image = None;
// Start camera.
self.call_java_method("startCamera", "()V", &[]).unwrap();
}
fn camera_image(&self) -> Option<(Vec<u8>, u32)> {
let r_image = LAST_CAMERA_IMAGE.read();
if r_image.is_some() {
return Some(r_image.clone().unwrap());
}
None
}
fn stop_camera(&self) {
// Stop camera.
self.call_java_method("stopCamera", "()V", &[]).unwrap();
// Clear image.
let mut w_image = LAST_CAMERA_IMAGE.write();
*w_image = None;
}
fn can_switch_camera(&self) -> bool {
if let Some(res) = self.call_java_method("camerasAmount", "()I", &[]) {
let amount = unsafe { res.i };
return amount > 1;
}
false
}
fn camera_image(&self) -> Option<(Vec<u8>, u32)> {
let r_image = LAST_CAMERA_IMAGE.read();
if r_image.is_some() {
return Some(r_image.clone().unwrap());
}
None
}
fn switch_camera(&self) {
let _ = self.call_java_method("switchCamera", "()V", &[]);
}
fn can_switch_camera(&self) -> bool {
let result = self.call_java_method("camerasAmount", "()I", &[]).unwrap();
let amount = unsafe { result.i };
amount > 1
}
fn share_data(&self, name: String, data: Vec<u8>) -> Result<(), std::io::Error> {
let default_cache = OsString::from(dirs::cache_dir().unwrap());
let mut file = PathBuf::from(env::var_os("XDG_CACHE_HOME").unwrap_or(default_cache));
// File path for Android provider.
file.push("share");
if !file.exists() {
std::fs::create_dir(file.clone())?;
}
file.push(name);
if file.exists() {
std::fs::remove_file(file.clone())?;
}
let mut image = File::create_new(file.clone())?;
image.write_all(data.as_slice())?;
image.sync_all()?;
// Call share modal at system.
let vm = unsafe { jni::JavaVM::from_raw(self.android_app.vm_as_ptr() as _) }.unwrap();
let env = vm.attach_current_thread().unwrap();
let arg_value = env.new_string(file.to_str().unwrap()).unwrap();
let _ = self.call_java_method(
"shareData",
"(Ljava/lang/String;)V",
&[JValue::Object(&JObject::from(arg_value))],
);
Ok(())
}
fn switch_camera(&self) {
self.call_java_method("switchCamera", "()V", &[]).unwrap();
}
fn pick_file(&self) -> Option<String> {
// Clear previous result.
let mut w_path = PICKED_FILE_PATH.write();
*w_path = None;
// Launch file picker.
let _ = self.call_java_method("pickFile", "()V", &[]);
// Return empty string to identify async pick.
Some("".to_string())
}
fn share_data(&self, name: String, data: Vec<u8>) -> Result<(), std::io::Error> {
// Create file at cache dir.
let default_cache = OsString::from(dirs::cache_dir().unwrap());
let mut cache = PathBuf::from(env::var_os("XDG_CACHE_HOME").unwrap_or(default_cache));
cache.push("images");
std::fs::create_dir_all(cache.to_str().unwrap())?;
cache.push(name);
let mut image = File::create_new(cache.clone()).unwrap();
image.write_all(data.as_slice()).unwrap();
image.sync_all().unwrap();
// Call share modal at system.
let vm = unsafe { jni::JavaVM::from_raw(self.android_app.vm_as_ptr() as _) }.unwrap();
let env = vm.attach_current_thread().unwrap();
let arg_value = env.new_string(cache.to_str().unwrap()).unwrap();
self.call_java_method("shareImage",
"(Ljava/lang/String;)V",
&[JValue::Object(&JObject::from(arg_value))]).unwrap();
Ok(())
}
fn pick_folder(&self) -> Option<String> {
// Clear previous result.
let mut w_path = PICKED_FILE_PATH.write();
*w_path = None;
// Launch file picker.
let _ = self.call_java_method("pickFolder", "()V", &[]);
// Return empty string to identify async pick.
Some("".to_string())
}
fn pick_file(&self) -> Option<String> {
// Clear previous result.
let mut w_path = PICKED_FILE_PATH.write();
*w_path = None;
// Launch file picker.
let _ = self.call_java_method("pickFile", "()V", &[]).unwrap();
// Return empty string to identify async pick.
Some("".to_string())
}
fn picked_file(&self) -> Option<String> {
let has_file = {
let r_path = PICKED_FILE_PATH.read();
r_path.is_some()
};
if has_file {
let mut w_path = PICKED_FILE_PATH.write();
let path = Some(w_path.clone().unwrap());
*w_path = None;
return path;
}
None
}
fn picked_file(&self) -> Option<String> {
let has_file = {
let r_path = PICKED_FILE_PATH.read();
r_path.is_some()
};
if has_file {
let mut w_path = PICKED_FILE_PATH.write();
let path = Some(w_path.clone().unwrap());
*w_path = None;
return path
}
None
}
fn request_user_attention(&self) {}
fn user_attention_required(&self) -> bool {
false
}
fn clear_user_attention(&self) {}
}
lazy_static! {
/// Last image data from camera.
static ref LAST_CAMERA_IMAGE: Arc<RwLock<Option<(Vec<u8>, u32)>>> = Arc::new(RwLock::new(None));
/// Picked file path.
static ref PICKED_FILE_PATH: Arc<RwLock<Option<String>>> = Arc::new(RwLock::new(None));
/// Last image data from camera.
static ref LAST_CAMERA_IMAGE: Arc<RwLock<Option<(Vec<u8>, u32)>>> = Arc::new(RwLock::new(None));
/// Picked file path.
static ref PICKED_FILE_PATH: Arc<RwLock<Option<String>>> = Arc::new(RwLock::new(None));
}
/// Callback from Java code with last entered character from soft keyboard.
#[allow(non_snake_case)]
#[no_mangle]
#[unsafe(no_mangle)]
pub extern "C" fn Java_mw_gri_android_MainActivity_onCameraImage(
env: JNIEnv,
_class: JObject,
buff: jni::sys::jbyteArray,
rotation: jni::sys::jint,
env: JNIEnv,
_class: JObject,
buff: jni::sys::jbyteArray,
rotation: jni::sys::jint,
) {
let arr = unsafe { JByteArray::from_raw(buff) };
let image : Vec<u8> = env.convert_byte_array(arr).unwrap();
let mut w_image = LAST_CAMERA_IMAGE.write();
*w_image = Some((image, rotation as u32));
let arr = unsafe { JByteArray::from_raw(buff) };
let image: Vec<u8> = env.convert_byte_array(arr).unwrap();
let mut w_image = LAST_CAMERA_IMAGE.write();
*w_image = Some((image, rotation as u32));
}
/// Callback from Java code with picked file path.
#[allow(non_snake_case)]
#[no_mangle]
#[unsafe(no_mangle)]
pub extern "C" fn Java_mw_gri_android_MainActivity_onFilePick(
_env: JNIEnv,
_class: JObject,
char: jni::sys::jstring
_env: JNIEnv,
_class: JObject,
char: jni::sys::jstring,
) {
use std::ops::Add;
unsafe {
let j_obj = JString::from_raw(char);
let j_str = _env.get_string_unchecked(j_obj.as_ref()).unwrap();
match j_str.to_str() {
Ok(str) => {
let mut w_path = PICKED_FILE_PATH.write();
*w_path = Some(w_path.clone().unwrap_or("".to_string()).add(str));
}
Err(_) => {}
}
}
}
use std::ops::Add;
unsafe {
let j_obj = JString::from_raw(char);
let j_str = _env.get_string_unchecked(j_obj.as_ref()).unwrap();
match j_str.to_str() {
Ok(str) => {
let mut w_path = PICKED_FILE_PATH.write();
*w_path = Some(w_path.clone().unwrap_or("".to_string()).add(str));
}
Err(_) => {}
}
}
}
+290 -171
View File
@@ -12,197 +12,316 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use std::fs::File;
use std::io:: Write;
use egui::{UserAttentionType, ViewportCommand, WindowLevel};
use lazy_static::lazy_static;
use parking_lot::RwLock;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::thread;
use rfd::FileDialog;
use std::fs::File;
use std::io::Write;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::thread;
use crate::gui::platform::PlatformCallbacks;
/// Desktop platform related actions.
#[derive(Clone)]
pub struct Desktop {
/// Flag to check if camera stop is needed.
stop_camera: Arc<AtomicBool>,
}
/// Context to repaint content and handle viewport commands.
ctx: Arc<RwLock<Option<egui::Context>>>,
impl Default for Desktop {
fn default() -> Self {
Self {
stop_camera: Arc::new(AtomicBool::new(false)),
}
}
}
/// Cameras amount.
cameras_amount: Arc<AtomicUsize>,
/// Camera index.
camera_index: Arc<AtomicUsize>,
/// Flag to check if camera stop is needed.
stop_camera: Arc<AtomicBool>,
impl PlatformCallbacks for Desktop {
fn show_keyboard(&self) {}
fn hide_keyboard(&self) {}
fn copy_string_to_buffer(&self, data: String) {
let mut clipboard = arboard::Clipboard::new().unwrap();
clipboard.set_text(data).unwrap();
}
fn get_string_from_buffer(&self) -> String {
let mut clipboard = arboard::Clipboard::new().unwrap();
clipboard.get_text().unwrap_or("".to_string())
}
fn start_camera(&self) {
// Clear image.
{
let mut w_image = LAST_CAMERA_IMAGE.write();
*w_image = None;
}
// Setup stop camera flag.
let stop_camera = self.stop_camera.clone();
stop_camera.store(false, Ordering::Relaxed);
// Capture images at separate thread.
thread::spawn(move || {
Self::start_camera_capture(stop_camera);
});
}
fn stop_camera(&self) {
// Stop camera.
self.stop_camera.store(true, Ordering::Relaxed);
}
fn camera_image(&self) -> Option<(Vec<u8>, u32)> {
let r_image = LAST_CAMERA_IMAGE.read();
if r_image.is_some() {
return r_image.clone();
}
None
}
fn can_switch_camera(&self) -> bool {
false
}
fn switch_camera(&self) {
return;
}
fn share_data(&self, name: String, data: Vec<u8>) -> Result<(), std::io::Error> {
let folder = FileDialog::new()
.set_title(t!("share"))
.set_directory(dirs::home_dir().unwrap())
.set_file_name(name.clone())
.save_file();
if let Some(folder) = folder {
let mut image = File::create(folder)?;
image.write_all(data.as_slice())?;
image.sync_all()?;
}
Ok(())
}
fn pick_file(&self) -> Option<String> {
let file = FileDialog::new()
.set_title(t!("choose_file"))
.set_directory(dirs::home_dir().unwrap())
.pick_file();
if let Some(file) = file {
return Some(file.to_str().unwrap_or_default().to_string());
}
None
}
fn picked_file(&self) -> Option<String> {
None
}
/// Flag to check if attention required after window focusing.
attention_required: Arc<AtomicBool>,
}
impl Desktop {
#[allow(dead_code)]
#[cfg(target_os = "windows")]
fn start_camera_capture(stop_camera: Arc<AtomicBool>) {
use nokhwa::Camera;
use nokhwa::pixel_format::RgbFormat;
use nokhwa::utils::{CameraIndex, RequestedFormat, RequestedFormatType};
let index = CameraIndex::Index(0);
let requested = RequestedFormat::new::<RgbFormat>(
RequestedFormatType::AbsoluteHighestFrameRate
);
// Create and open camera.
let mut camera = Camera::new(index, requested).unwrap();
if let Ok(_) = camera.open_stream() {
loop {
// Stop if camera was stopped.
if stop_camera.load(Ordering::Relaxed) {
stop_camera.store(false, Ordering::Relaxed);
// Clear image.
let mut w_image = LAST_CAMERA_IMAGE.write();
*w_image = None;
break;
}
// Get a frame.
if let Ok(frame) = camera.frame() {
// Save image.
let mut w_image = LAST_CAMERA_IMAGE.write();
*w_image = Some((frame.buffer().to_vec(), 0));
} else {
// Clear image.
let mut w_image = LAST_CAMERA_IMAGE.write();
*w_image = None;
break;
}
}
camera.stop_stream().unwrap();
};
}
pub fn new() -> Self {
Self {
ctx: Arc::new(RwLock::new(None)),
cameras_amount: Arc::new(AtomicUsize::new(0)),
camera_index: Arc::new(AtomicUsize::new(0)),
stop_camera: Arc::new(AtomicBool::new(false)),
attention_required: Arc::new(AtomicBool::new(false)),
}
}
#[allow(dead_code)]
#[cfg(not(target_os = "windows"))]
fn start_camera_capture(stop_camera: Arc<AtomicBool>) {
use eye::hal::{traits::{Context, Device, Stream}, PlatformContext};
use image::ImageEncoder;
// #[allow(dead_code)]
#[cfg(not(target_os = "macos"))]
fn start_camera_capture(
cameras_amount: Arc<AtomicUsize>,
camera_index: Arc<AtomicUsize>,
stop_camera: Arc<AtomicBool>,
) {
use nokhwa::Camera;
use nokhwa::pixel_format::RgbFormat;
use nokhwa::utils::ApiBackend;
use nokhwa::utils::{CameraIndex, RequestedFormat, RequestedFormatType};
let ctx = PlatformContext::default();
let devices = ctx.devices().unwrap();
let dev = ctx.open_device(&devices[0].uri).unwrap();
let devices = nokhwa::query(ApiBackend::Auto).unwrap();
cameras_amount.store(devices.len(), Ordering::Relaxed);
let index = camera_index.load(Ordering::Relaxed);
if devices.is_empty() || index >= devices.len() {
return;
}
let streams = dev.streams().unwrap();
let stream_desc = streams[0].clone();
let w = stream_desc.width;
let h = stream_desc.height;
thread::spawn(move || {
let index = CameraIndex::Index(camera_index.load(Ordering::Relaxed) as u32);
let requested =
RequestedFormat::new::<RgbFormat>(RequestedFormatType::AbsoluteHighestFrameRate);
// Create and open camera.
if let Ok(mut camera) = Camera::new(index, requested) {
if let Ok(_) = camera.open_stream() {
loop {
// Stop if camera was stopped.
if stop_camera.load(Ordering::Relaxed) {
stop_camera.store(false, Ordering::Relaxed);
// Clear image.
let mut w_image = LAST_CAMERA_IMAGE.write();
*w_image = None;
break;
}
// Get a frame.
if let Ok(frame) = camera.frame() {
// Save image.
let mut w_image = LAST_CAMERA_IMAGE.write();
*w_image = Some((frame.buffer().to_vec(), 0));
} else {
// Clear image.
let mut w_image = LAST_CAMERA_IMAGE.write();
*w_image = None;
break;
}
}
camera.stop_stream().unwrap();
};
}
});
}
let mut stream = dev.start_stream(&stream_desc).unwrap();
#[allow(dead_code)]
#[cfg(target_os = "macos")]
fn start_camera_capture(
cameras_amount: Arc<AtomicUsize>,
camera_index: Arc<AtomicUsize>,
stop_camera: Arc<AtomicBool>,
) {
use nokhwa::CallbackCamera;
use nokhwa::nokhwa_initialize;
use nokhwa::pixel_format::RgbFormat;
use nokhwa::query;
use nokhwa::utils::ApiBackend;
use nokhwa::utils::{CameraIndex, RequestedFormat, RequestedFormatType};
loop {
// Stop if camera was stopped.
if stop_camera.load(Ordering::Relaxed) {
stop_camera.store(false, Ordering::Relaxed);
// Clear image.
let mut w_image = LAST_CAMERA_IMAGE.write();
*w_image = None;
break;
}
// Get a frame.
let frame = stream.next().expect("Stream is dead").expect("Failed to capture a frame");
let mut out = vec![];
if let Some(buf) = image::ImageBuffer::<image::Rgb<u8>, &[u8]>::from_raw(w, h, &frame) {
image::codecs::jpeg::JpegEncoder::new(&mut out)
.write_image(buf.as_raw(), w, h, image::ExtendedColorType::Rgb8).unwrap();
} else {
out = frame.to_vec();
}
// Save image.
let mut w_image = LAST_CAMERA_IMAGE.write();
*w_image = Some((out, 0));
}
}
// Ask permission to open camera.
nokhwa_initialize(|_| {});
thread::spawn(move || {
let cameras = query(ApiBackend::Auto).unwrap();
cameras_amount.store(cameras.len(), Ordering::Relaxed);
let index = camera_index.load(Ordering::Relaxed);
if cameras.is_empty() || index >= cameras.len() {
return;
}
// Start camera.
let camera_index = CameraIndex::Index(camera_index.load(Ordering::Relaxed) as u32);
let camera_callback = CallbackCamera::new(
camera_index,
RequestedFormat::new::<RgbFormat>(RequestedFormatType::AbsoluteHighestFrameRate),
|_| {},
);
if let Ok(mut cb) = camera_callback {
if cb.open_stream().is_ok() {
loop {
// Stop if camera was stopped.
if stop_camera.load(Ordering::Relaxed) {
stop_camera.store(false, Ordering::Relaxed);
// Clear image.
let mut w_image = LAST_CAMERA_IMAGE.write();
*w_image = None;
break;
}
// Get image from camera.
if let Ok(frame) = cb.poll_frame() {
let image = frame.decode_image::<RgbFormat>().unwrap();
let mut bytes: Vec<u8> = Vec::new();
let format = image::ImageFormat::Jpeg;
// Convert image to Jpeg format.
image
.write_to(&mut std::io::Cursor::new(&mut bytes), format)
.unwrap();
let mut w_image = LAST_CAMERA_IMAGE.write();
*w_image = Some((bytes, 0));
} else {
// Clear image.
let mut w_image = LAST_CAMERA_IMAGE.write();
*w_image = None;
break;
}
}
}
}
});
}
}
impl PlatformCallbacks for Desktop {
fn set_context(&mut self, ctx: &egui::Context) {
let mut w_ctx = self.ctx.write();
*w_ctx = Some(ctx.clone());
}
fn exit(&self) {
let r_ctx = self.ctx.read();
if r_ctx.is_some() {
let ctx = r_ctx.as_ref().unwrap();
ctx.send_viewport_cmd(ViewportCommand::Close);
}
}
fn copy_string_to_buffer(&self, data: String) {
let mut clipboard = arboard::Clipboard::new().unwrap();
clipboard.set_text(data).unwrap();
}
fn get_string_from_buffer(&self) -> String {
let mut clipboard = arboard::Clipboard::new().unwrap();
clipboard.get_text().unwrap_or("".to_string())
}
fn start_camera(&self) {
// Clear image.
{
let mut w_image = LAST_CAMERA_IMAGE.write();
*w_image = None;
}
// Setup stop camera flag.
let stop_camera = self.stop_camera.clone();
stop_camera.store(false, Ordering::Relaxed);
Self::start_camera_capture(
self.cameras_amount.clone(),
self.camera_index.clone(),
stop_camera,
);
}
fn stop_camera(&self) {
// Stop camera.
self.stop_camera.store(true, Ordering::Relaxed);
}
fn camera_image(&self) -> Option<(Vec<u8>, u32)> {
let r_image = LAST_CAMERA_IMAGE.read();
if r_image.is_some() {
return r_image.clone();
}
None
}
fn can_switch_camera(&self) -> bool {
let amount = self.cameras_amount.load(Ordering::Relaxed);
amount > 1
}
fn switch_camera(&self) {
self.stop_camera();
let index = self.camera_index.load(Ordering::Relaxed);
let amount = self.cameras_amount.load(Ordering::Relaxed);
if index == amount - 1 {
self.camera_index.store(0, Ordering::Relaxed);
} else {
self.camera_index.store(index + 1, Ordering::Relaxed);
}
self.start_camera();
}
fn share_data(&self, name: String, data: Vec<u8>) -> Result<(), std::io::Error> {
let folder = FileDialog::new()
.set_title(t!("share"))
.set_directory(dirs::home_dir().unwrap())
.set_file_name(name.clone())
.save_file();
if let Some(folder) = folder {
let mut image = File::create(folder)?;
image.write_all(data.as_slice())?;
image.sync_all()?;
}
Ok(())
}
fn pick_file(&self) -> Option<String> {
let file = FileDialog::new()
.set_title(t!("choose_file"))
.set_directory(dirs::home_dir().unwrap())
.pick_file();
if let Some(file) = file {
return Some(file.to_str().unwrap_or_default().to_string());
}
None
}
fn pick_folder(&self) -> Option<String> {
let file = FileDialog::new()
.set_title(t!("choose_folder"))
.set_directory(dirs::home_dir().unwrap())
.pick_folder();
if let Some(file) = file {
return Some(file.to_str().unwrap_or_default().to_string());
}
None
}
fn picked_file(&self) -> Option<String> {
None
}
fn request_user_attention(&self) {
let r_ctx = self.ctx.read();
if r_ctx.is_some() {
let ctx = r_ctx.as_ref().unwrap();
// Request attention on taskbar.
ctx.send_viewport_cmd(ViewportCommand::RequestUserAttention(
UserAttentionType::Informational,
));
// Un-minimize window.
if ctx.input(|i| i.viewport().minimized.unwrap_or(false)) {
ctx.send_viewport_cmd(ViewportCommand::Minimized(false));
}
// Focus to window.
if !ctx.input(|i| i.viewport().focused.unwrap_or(false)) {
ctx.send_viewport_cmd(ViewportCommand::WindowLevel(WindowLevel::AlwaysOnTop));
ctx.send_viewport_cmd(ViewportCommand::Focus);
}
ctx.request_repaint();
}
self.attention_required.store(true, Ordering::Relaxed);
}
fn user_attention_required(&self) -> bool {
self.attention_required.load(Ordering::Relaxed)
}
fn clear_user_attention(&self) {
let r_ctx = self.ctx.read();
if r_ctx.is_some() {
let ctx = r_ctx.as_ref().unwrap();
ctx.send_viewport_cmd(ViewportCommand::RequestUserAttention(
UserAttentionType::Reset,
));
ctx.send_viewport_cmd(ViewportCommand::WindowLevel(WindowLevel::Normal));
}
self.attention_required.store(false, Ordering::Relaxed);
}
}
lazy_static! {
/// Last captured image from started camera.
static ref LAST_CAMERA_IMAGE: Arc<RwLock<Option<(Vec<u8>, u32)>>> = Arc::new(RwLock::new(None));
/// Last captured image from started camera.
static ref LAST_CAMERA_IMAGE: Arc<RwLock<Option<(Vec<u8>, u32)>>> = Arc::new(RwLock::new(None));
}
+17 -13
View File
@@ -22,16 +22,20 @@ pub mod platform;
pub mod platform;
pub trait PlatformCallbacks {
fn show_keyboard(&self);
fn hide_keyboard(&self);
fn copy_string_to_buffer(&self, data: String);
fn get_string_from_buffer(&self) -> String;
fn start_camera(&self);
fn stop_camera(&self);
fn camera_image(&self) -> Option<(Vec<u8>, u32)>;
fn can_switch_camera(&self) -> bool;
fn switch_camera(&self);
fn share_data(&self, name: String, data: Vec<u8>) -> Result<(), std::io::Error>;
fn pick_file(&self) -> Option<String>;
fn picked_file(&self) -> Option<String>;
}
fn set_context(&mut self, ctx: &egui::Context);
fn exit(&self);
fn copy_string_to_buffer(&self, data: String);
fn get_string_from_buffer(&self) -> String;
fn start_camera(&self);
fn stop_camera(&self);
fn camera_image(&self) -> Option<(Vec<u8>, u32)>;
fn can_switch_camera(&self) -> bool;
fn switch_camera(&self);
fn share_data(&self, name: String, data: Vec<u8>) -> Result<(), std::io::Error>;
fn pick_file(&self) -> Option<String>;
fn pick_folder(&self) -> Option<String>;
fn picked_file(&self) -> Option<String>;
fn request_user_attention(&self);
fn user_attention_required(&self) -> bool;
fn clear_user_attention(&self);
}
+436 -393
View File
@@ -12,432 +12,475 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use std::sync::Arc;
use parking_lot::RwLock;
use std::thread;
use eframe::emath::Align;
use egui::load::SizedTexture;
use egui::{Layout, Pos2, Rect, RichText, TextureOptions, Widget};
use image::{DynamicImage, EncodableLayout, ImageFormat};
use egui::{Pos2, Rect, RichText, TextureOptions, UiBuilder, Widget};
use grin_keychain::mnemonic::WORDS;
use grin_util::ZeroingString;
use grin_wallet_libwallet::SlatepackAddress;
use grin_keychain::mnemonic::WORDS;
use image::{DynamicImage, EncodableLayout};
use parking_lot::RwLock;
use std::sync::Arc;
use std::thread;
use crate::gui::Colors;
use crate::gui::icons::CAMERA_ROTATE;
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::types::{QrScanResult, QrScanState};
use crate::gui::views::View;
use crate::wallet::types::PhraseSize;
use crate::gui::views::{Modal, View};
use crate::wallet::WalletUtils;
use crate::wallet::types::PhraseSize;
/// Camera QR code scanner.
pub struct CameraContent {
/// QR code scanning progress and result.
qr_scan_state: Arc<RwLock<QrScanState>>,
/// Uniform Resources URIs collected from QR code scanning.
ur_data: Arc<RwLock<Option<(Vec<String>, usize)>>>,
/// QR code scanning progress and result.
qr_scan_state: Arc<RwLock<QrScanState>>,
/// Uniform Resources URIs collected from QR code scanning.
ur_data: Arc<RwLock<Option<(Vec<String>, usize)>>>,
}
impl Default for CameraContent {
fn default() -> Self {
Self {
qr_scan_state: Arc::new(RwLock::new(QrScanState::default())),
ur_data: Arc::new(RwLock::new(None)),
}
}
fn default() -> Self {
Self {
qr_scan_state: Arc::new(RwLock::new(QrScanState::default())),
ur_data: Arc::new(RwLock::new(None)),
}
}
}
impl CameraContent {
/// Draw camera content.
pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
// Draw last image from camera or loader.
if let Some(img_data) = cb.camera_image() {
// Load image to draw.
if let Ok(mut img) =
image::load_from_memory_with_format(&*img_data.0, ImageFormat::Jpeg) {
// Process image to find QR code.
self.scan_qr(&img);
// Setup image rotation.
img = match img_data.1 {
90 => img.rotate90(),
180 => img.rotate180(),
270 => img.rotate270(),
_ => img
};
// Convert to ColorImage to add at content.
let color_img = match &img {
DynamicImage::ImageRgb8(image) => {
egui::ColorImage::from_rgb(
[image.width() as usize, image.height() as usize],
image.as_bytes(),
)
},
other => {
let image = other.to_rgba8();
egui::ColorImage::from_rgba_unmultiplied(
[image.width() as usize, image.height() as usize],
image.as_bytes(),
)
},
};
// Create image texture.
let texture = ui.ctx().load_texture("camera_image",
color_img.clone(),
TextureOptions::default());
let img_size = egui::emath::vec2(color_img.width() as f32,
color_img.height() as f32);
let sized_img = SizedTexture::new(texture.id(), img_size);
// Add image to content.
ui.vertical_centered(|ui| {
egui::Image::from_texture(sized_img)
// Setup to crop image at square.
.uv(Rect::from([
Pos2::new(1.0 - (img_size.y / img_size.x), 0.0),
Pos2::new(1.0, 1.0)
]))
.max_height(ui.available_width())
.maintain_aspect_ratio(false)
.shrink_to_fit()
.ui(ui);
});
/// Draw camera content.
pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
let rect = if let Some(img_data) = cb.camera_image() {
if let Ok(img) = image::load_from_memory(&*img_data.0) {
// Process image to find QR code.
self.scan_qr(&img);
// Show UR scan progress.
let show_ur_progress = {
self.ur_data.clone().read().is_some()
};
let ur_progress = self.ur_progress();
if show_ur_progress && ur_progress != 0 {
ui.add_space(-52.0);
ui.vertical_centered(|ui| {
ui.label(RichText::new(format!("{}%", ur_progress))
.size(16.0)
.color(Colors::yellow()));
});
}
// Draw image.
let img_rect = self.image_ui(ui, img, img_data.1);
// Show button to switch cameras.
if cb.can_switch_camera() {
ui.add_space(-52.0);
let mut size = ui.available_size();
size.y = 48.0;
ui.allocate_ui_with_layout(size, Layout::right_to_left(Align::Max), |ui| {
ui.add_space(4.0);
View::button(ui, CAMERA_ROTATE.to_string(), Colors::white_or_black(false), || {
cb.switch_camera();
});
});
}
} else {
self.loading_content_ui(ui);
}
} else {
self.loading_content_ui(ui);
}
img_rect
} else {
self.loading_ui(ui)
}
} else {
self.loading_ui(ui)
};
// Request redraw.
ui.ctx().request_repaint();
}
// Show UR scan progress.
self.ur_progress_ui(ui, &rect);
/// Draw camera loading progress content.
fn loading_content_ui(&self, ui: &mut egui::Ui) {
let space = (ui.available_width() - View::BIG_SPINNER_SIZE) / 2.0;
ui.vertical_centered(|ui| {
ui.add_space(space);
View::big_loading_spinner(ui);
ui.add_space(space);
});
}
// Show button to switch cameras.
if cb.can_switch_camera() {
let r = {
let mut r = rect.clone();
r.min.y = r.max.y - 52.0;
r.min.x = r.max.x - 52.0;
r
};
ui.scope_builder(UiBuilder::new().max_rect(r), |ui| {
let rotate_img = CAMERA_ROTATE.to_string();
View::button(ui, rotate_img, Colors::white_or_black(false), || {
cb.switch_camera();
});
});
}
ui.add_space(6.0);
ui.ctx().request_repaint();
}
/// Check if image is processing to find QR code.
fn image_processing(&self) -> bool {
let r_scan = self.qr_scan_state.read();
r_scan.image_processing
}
/// Draw modal camera content.
pub fn modal_ui(
&mut self,
ui: &mut egui::Ui,
cb: &dyn PlatformCallbacks,
mut on_result: impl FnMut(Option<QrScanResult>),
) {
if let Some(result) = self.qr_scan_result() {
on_result(Some(result));
} else {
ui.add_space(6.0);
self.ui(ui, cb);
ui.add_space(6.0);
/// Get UR scanning progress in percents.
fn ur_progress(&self) -> i32 {
// Setup data.
let r_data = self.ur_data.read();
let (data, total) = r_data.clone().unwrap_or((vec![], 0));
if data.is_empty() {
return 0;
}
// Calculate progress.
let mut complete = 0;
for i in &data {
if !i.is_empty() {
complete += 1;
}
}
(100 * complete / total) as i32
}
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
/// Parse QR code from provided image data.
fn scan_qr(&self, image_data: &DynamicImage) {
// Do not scan when another image is processing.
if self.image_processing() {
return;
}
// Setup scanning flag.
{
let mut w_scan = self.qr_scan_state.write();
w_scan.image_processing = true;
}
// Show buttons to close modal or come back to sending input.
ui.columns(2, |cols| {
cols[0].vertical_centered_justified(|ui| {
View::button(ui, t!("close"), Colors::white_or_black(false), || {
cb.stop_camera();
on_result(None);
Modal::close();
});
});
cols[1].vertical_centered_justified(|ui| {
View::button(ui, t!("back"), Colors::white_or_black(false), || {
on_result(None);
});
});
});
ui.add_space(6.0);
}
}
let image_data = image_data.clone();
let qr_scan_state = self.qr_scan_state.clone();
let ur_data = self.ur_data.clone();
/// Draw camera image.
fn image_ui(&mut self, ui: &mut egui::Ui, mut img: DynamicImage, rotation: u32) -> Rect {
// Setup image rotation.
img = match rotation {
90 => img.rotate90(),
180 => img.rotate180(),
270 => img.rotate270(),
_ => img,
};
if View::is_desktop() {
img = img.fliph();
}
// Convert to ColorImage.
let color_img = match &img {
DynamicImage::ImageRgb8(image) => egui::ColorImage::from_rgb(
[image.width() as usize, image.height() as usize],
image.as_bytes(),
),
other => {
let image = other.to_rgba8();
egui::ColorImage::from_rgba_unmultiplied(
[image.width() as usize, image.height() as usize],
image.as_bytes(),
)
}
};
// Create image texture.
let texture =
ui.ctx()
.load_texture("camera_image", color_img.clone(), TextureOptions::default());
let img_size = egui::emath::vec2(color_img.width() as f32, color_img.height() as f32);
let sized_img = SizedTexture::new(texture.id(), img_size);
egui::Image::from_texture(sized_img)
// Setup to crop image at square.
.uv(Rect::from([
if img_size.y > img_size.x {
Pos2::new(0.0, 1.0 - (img_size.x / img_size.y))
} else {
Pos2::new(1.0 - (img_size.y / img_size.x), 0.0)
},
Pos2::new(1.0, 1.0),
]))
.max_height(ui.available_width())
.maintain_aspect_ratio(false)
.shrink_to_fit()
.ui(ui)
.rect
}
let on_scan = async move {
// Prepare image data.
let img = image_data.to_luma8();
let mut img: rqrr::PreparedImage<image::GrayImage>
= rqrr::PreparedImage::prepare(img);
// Scan and save results.
let grids = img.detect_grids();
if let Some(g) = grids.get(0) {
let mut qr_data = vec![];
if let Ok(_) = g.decode_to(&mut qr_data) {
// Setup scanned data into text.
let text = String::from_utf8(qr_data.clone()).unwrap_or("".to_string());
// Setup current text.
let cur_text = {
let r_scan = qr_scan_state.read();
let text = if let Some(res) = r_scan.qr_scan_result.clone() {
res.text()
} else {
"".to_string()
};
text
};
// Parse non-empty data if parsed text is different from saved.
if !qr_data.is_empty() && (cur_text.is_empty() || text != cur_text) {
let res = Self::parse_qr_code(qr_data);
match res {
QrScanResult::URPart(uri, index, total) => {
// Setup current UR data.
let mut cur_data = {
let r_data = ur_data.read();
let mut cur_data = vec!["".to_string(); total];
if let Some((d, _)) = r_data.clone() {
cur_data = d;
}
cur_data
};
if !cur_data.contains(&uri) {
// Save part of UR data.
{
cur_data.insert(index, uri);
let mut w_data = ur_data.write();
*w_data = Some((cur_data.clone(), total));
}
// Setup UR decoder.
let mut decoder = ur::Decoder::default();
for m in cur_data {
if !m.is_empty() {
if let Ok(_) = decoder.receive(m.as_str()) {
continue;
} else {
break;
}
}
}
// Check if UR data is complete.
if decoder.complete() {
if let Ok(data) = decoder.message() {
// Parse complete data.
let res = Self::parse_qr_code(data.unwrap_or(vec![]));
// Clean UR data.
let mut w_data = ur_data.write();
*w_data = None;
// Save scan result.
let mut w_scan = qr_scan_state.write();
w_scan.qr_scan_result = Some(res);
return;
}
}
}
}
_ => {
// Clean UR data.
let mut w_data = ur_data.write();
*w_data = None;
// Save scan result.
let mut w_scan = qr_scan_state.write();
w_scan.qr_scan_result = Some(res);
return;
}
}
}
}
}
// Reset scanning flag to process again.
{
let mut w_scan = qr_scan_state.write();
w_scan.image_processing = false;
}
};
/// Draw animated QR code scanning progress.
fn ur_progress_ui(&self, ui: &mut egui::Ui, rect: &Rect) {
let show_ur_progress = { self.ur_data.as_ref().read().is_some() };
if show_ur_progress {
ui.scope_builder(UiBuilder::new().max_rect(rect.clone()), |ui| {
ui.centered_and_justified(|ui| {
ui.label(
RichText::new(format!("{}%", self.ur_progress()))
.size(32.0)
.color(Colors::gold_dark()),
);
});
});
}
}
// Launch scanner at separate thread.
thread::spawn(move || {
tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.unwrap()
.block_on(on_scan);
});
}
/// Draw camera loading progress content.
fn loading_ui(&self, ui: &mut egui::Ui) -> Rect {
let space = (ui.available_width() - View::BIG_SPINNER_SIZE) / 2.0;
ui.vertical_centered(|ui| {
ui.add_space(space);
View::big_loading_spinner(ui);
ui.add_space(space);
})
.response
.rect
}
/// Parse QR code scan result.
fn parse_qr_code(data: Vec<u8>) -> QrScanResult {
// Check if string starts with Grin address prefix.
let text_string = String::from_utf8(data.clone()).unwrap_or("".to_string());
let text = text_string.trim();
if text.starts_with("tgrin") || text.starts_with("grin") {
if SlatepackAddress::try_from(text).is_ok() {
return QrScanResult::Address(ZeroingString::from(text));
}
}
/// Check if image is processing to find QR code.
fn image_processing(&self) -> bool {
let r_scan = self.qr_scan_state.read();
r_scan.image_processing
}
// Check if string contains Slatepack message prefix and postfix.
if text.starts_with("BEGINSLATEPACK.") && text.ends_with("ENDSLATEPACK.") {
return QrScanResult::Slatepack(ZeroingString::from(text));
}
/// Get UR scanning progress in percents.
fn ur_progress(&self) -> i32 {
// Setup data.
let r_data = self.ur_data.read();
let (data, total) = r_data.clone().unwrap_or((vec![], 0));
if data.is_empty() {
return 0;
}
// Calculate progress.
let mut complete = 0;
for i in &data {
if !i.is_empty() {
complete += 1;
}
}
(100 * complete / total) as i32
}
// Check Uniform Resource data.
// https://github.com/BlockchainCommons/Research/blob/master/papers/bcr-2020-005-ur.md
if text.starts_with("ur:bytes/") {
let split = text.split("/").collect::<Vec<_>>();
if let Some(index_total) = split.get(1) {
if let Some((index, total)) = index_total.split_once("-") {
let index = index.parse::<usize>();
let total = total.parse::<usize>();
if index.is_ok() && total.is_ok() {
let index = index.unwrap() - 1;
let total = total.unwrap();
return QrScanResult::URPart(text_string, index, total);
}
}
}
}
/// Parse QR code from provided image data.
fn scan_qr(&self, image_data: &DynamicImage) {
// Do not scan when another image is processing.
if self.image_processing() {
return;
}
// Setup scanning flag.
{
let mut w_scan = self.qr_scan_state.write();
w_scan.image_processing = true;
}
// Check Compact SeedQR format.
// https://github.com/SeedSigner/seedsigner/blob/dev/docs/seed_qr/README.md#compactseedqr-specification
if data.len() <= 32 && 16 <= data.len() && data.len() % 4 == 0 {
// Setup words amount.
let total_bits = data.len() * 8;
let checksum_bits = total_bits / 32;
let total_words = (total_bits + checksum_bits) / 11;
// Setup entropy.
let mut entropy = data.clone();
WalletUtils::setup_checksum(&mut entropy);
// Setup bits.
let mut bits = vec![false; entropy.len() * 8];
for i in 0..entropy.len() {
for j in 0..8 {
bits[(i * 8) + j] = (entropy[i] & (1 << (7 - j))) != 0;
}
}
// Extract word index.
let extract_index = |i: usize| -> usize {
let mut index = 0;
for j in 0..11 {
index = index << 1;
if bits[(i * 11) + j] {
index += 1;
}
}
return index;
};
// Setup words.
let mut words = "".to_string();
for n in 0..total_words {
// Setup word index.
let index = extract_index(n);
// Setup word.
let empty_word = "".to_string();
let word = WORDS.get(index).clone().unwrap_or(&empty_word).clone();
if word.is_empty() {
words = empty_word;
break;
}
words = if words.is_empty() {
format!("{}", word)
} else {
format!("{} {}", words, word)
};
}
if !words.is_empty() {
return QrScanResult::SeedQR(ZeroingString::from(words));
}
}
let image_data = image_data.clone();
let qr_scan_state = self.qr_scan_state.clone();
let ur_data = self.ur_data.clone();
// Check Standard SeedQR format.
// https://github.com/SeedSigner/seedsigner/blob/dev/docs/seed_qr/README.md#standard-seedqr-specification
let only_numbers = || {
for c in text.chars() {
if !c.is_numeric() {
return false;
}
}
true
};
if !text.is_empty() && data.len() <= 96 && data.len() % 4 == 0 && only_numbers() {
if let Some(_) = PhraseSize::type_for_value(text.len() / 4) {
let chars: Vec<char> = text.trim().chars().collect();
let split = &chars.chunks(4)
.map(|chunk| chunk.iter().collect::<String>()
.trim()
.trim_start_matches("0")
.to_string()
)
.collect::<Vec<_>>();
let mut words = "".to_string();
for i in split {
let index = if i.is_empty() {
0usize
} else {
i.parse::<usize>().unwrap_or(WORDS.len())
};
let empty_word = "".to_string();
let word = WORDS.get(index).clone().unwrap_or(&empty_word).clone();
// Return text result when BIP39 word was not found.
if word.is_empty() {
return QrScanResult::Text(ZeroingString::from(text));
}
words = if words.is_empty() {
format!("{}", word)
} else {
format!("{} {}", words, word)
};
}
return QrScanResult::SeedQR(ZeroingString::from(words));
}
}
let on_scan = async move {
// Prepare image data.
let img = image_data.to_luma8();
let mut img: rqrr::PreparedImage<image::GrayImage> = rqrr::PreparedImage::prepare(img);
// Scan and save results.
let grids = img.detect_grids();
if let Some(g) = grids.get(0) {
let mut qr_data = vec![];
if let Ok(_) = g.decode_to(&mut qr_data) {
// Setup scanned data into text.
let text = String::from_utf8(qr_data.clone()).unwrap_or("".to_string());
// Setup current text.
let cur_text = {
let r_scan = qr_scan_state.read();
let text = if let Some(res) = r_scan.qr_scan_result.clone() {
res.text()
} else {
"".to_string()
};
text
};
// Parse non-empty data if parsed text is different from saved.
if !qr_data.is_empty() && (cur_text.is_empty() || text != cur_text) {
let res = Self::parse_qr_code(qr_data);
match res {
QrScanResult::URPart(uri, index, total) => {
// Setup current UR data.
let mut cur_data = {
let r_data = ur_data.read();
let mut cur_data = vec!["".to_string(); total];
if let Some((d, _)) = r_data.clone() {
cur_data = d;
}
cur_data
};
if !cur_data.contains(&uri) {
// Save part of UR data.
{
cur_data.insert(index, uri);
let mut w_data = ur_data.write();
*w_data = Some((cur_data.clone(), total));
}
// Setup UR decoder.
let mut decoder = ur::Decoder::default();
for m in cur_data {
if !m.is_empty() {
if let Ok(_) = decoder.receive(m.as_str()) {
continue;
} else {
break;
}
}
}
// Check if UR data is complete.
if decoder.complete() {
if let Ok(data) = decoder.message() {
// Parse complete data.
let res = Self::parse_qr_code(data.unwrap_or(vec![]));
// Clean UR data.
let mut w_data = ur_data.write();
*w_data = None;
// Save scan result.
let mut w_scan = qr_scan_state.write();
w_scan.qr_scan_result = Some(res);
return;
}
}
}
}
_ => {
// Clean UR data.
let mut w_data = ur_data.write();
*w_data = None;
// Save scan result.
let mut w_scan = qr_scan_state.write();
w_scan.qr_scan_result = Some(res);
return;
}
}
}
}
}
// Reset scanning flag to process again.
{
let mut w_scan = qr_scan_state.write();
w_scan.image_processing = false;
}
};
// Return default text result.
QrScanResult::Text(ZeroingString::from(text))
}
// Launch scanner at separate thread.
thread::spawn(move || {
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap()
.block_on(on_scan);
});
}
/// Get QR code scan result.
pub fn qr_scan_result(&self) -> Option<QrScanResult> {
let r_scan = self.qr_scan_state.read();
if r_scan.qr_scan_result.is_some() {
return Some(r_scan.qr_scan_result.clone().unwrap());
}
None
}
/// Parse QR code scan result.
fn parse_qr_code(data: Vec<u8>) -> QrScanResult {
// Check if string starts with Grin address prefix.
let text_string = String::from_utf8(data.clone()).unwrap_or("".to_string());
let text = text_string.trim();
if text.starts_with("tgrin") || text.starts_with("grin") {
if SlatepackAddress::try_from(text).is_ok() {
return QrScanResult::Address(ZeroingString::from(text));
}
}
/// Reset camera content state to default.
pub fn clear_state(&mut self) {
// Clear QR code scanning state.
let mut w_scan = self.qr_scan_state.write();
*w_scan = QrScanState::default();
// Clear UR data.
let mut w_data = self.ur_data.write();
*w_data = None;
}
}
// Check if string contains Slatepack message prefix and postfix.
if text.starts_with("BEGINSLATEPACK.") && text.ends_with("ENDSLATEPACK.") {
return QrScanResult::Slatepack(text.to_string());
}
// Check Uniform Resource data.
// https://github.com/BlockchainCommons/Research/blob/master/papers/bcr-2020-005-ur.md
if text.starts_with("ur:bytes/") {
let split = text.split("/").collect::<Vec<_>>();
if let Some(index_total) = split.get(1) {
if let Some((index, total)) = index_total.split_once("-") {
let index = index.parse::<usize>();
let total = total.parse::<usize>();
if index.is_ok() && total.is_ok() {
let index = index.unwrap() - 1;
let total = total.unwrap();
return QrScanResult::URPart(text_string, index, total);
}
}
}
}
// Check Compact SeedQR format.
// https://github.com/SeedSigner/seedsigner/blob/dev/docs/seed_qr/README.md#compactseedqr-specification
if data.len() <= 32 && 16 <= data.len() && data.len() % 4 == 0 {
// Setup words amount.
let total_bits = data.len() * 8;
let checksum_bits = total_bits / 32;
let total_words = (total_bits + checksum_bits) / 11;
// Setup entropy.
let mut entropy = data.clone();
WalletUtils::setup_checksum(&mut entropy);
// Setup bits.
let mut bits = vec![false; entropy.len() * 8];
for i in 0..entropy.len() {
for j in 0..8 {
bits[(i * 8) + j] = (entropy[i] & (1 << (7 - j))) != 0;
}
}
// Extract word index.
let extract_index = |i: usize| -> usize {
let mut index = 0;
for j in 0..11 {
index = index << 1;
if bits[(i * 11) + j] {
index += 1;
}
}
return index;
};
// Setup words.
let mut words = "".to_string();
for n in 0..total_words {
// Setup word index.
let index = extract_index(n);
// Setup word.
let empty_word = "".to_string();
let word = WORDS.get(index).clone().unwrap_or(&empty_word).clone();
if word.is_empty() {
words = empty_word;
break;
}
words = if words.is_empty() {
format!("{}", word)
} else {
format!("{} {}", words, word)
};
}
if !words.is_empty() {
return QrScanResult::SeedQR(ZeroingString::from(words));
}
}
// Check Standard SeedQR format.
// https://github.com/SeedSigner/seedsigner/blob/dev/docs/seed_qr/README.md#standard-seedqr-specification
let only_numbers = || {
for c in text.chars() {
if !c.is_numeric() {
return false;
}
}
true
};
if !text.is_empty() && data.len() <= 96 && data.len() % 4 == 0 && only_numbers() {
if let Some(_) = PhraseSize::type_for_value(text.len() / 4) {
let chars: Vec<char> = text.trim().chars().collect();
let split = &chars
.chunks(4)
.map(|chunk| {
chunk
.iter()
.collect::<String>()
.trim()
.trim_start_matches("0")
.to_string()
})
.collect::<Vec<_>>();
let mut words = "".to_string();
for i in split {
let index = if i.is_empty() {
0usize
} else {
i.parse::<usize>().unwrap_or(WORDS.len())
};
let empty_word = "".to_string();
let word = WORDS.get(index).clone().unwrap_or(&empty_word).clone();
// Return text result when BIP39 word was not found.
if word.is_empty() {
return QrScanResult::Text(ZeroingString::from(text));
}
words = if words.is_empty() {
format!("{}", word)
} else {
format!("{} {}", words, word)
};
}
return QrScanResult::SeedQR(ZeroingString::from(words));
}
}
// Return default text result.
QrScanResult::Text(ZeroingString::from(text))
}
/// Get QR code scan result.
pub fn qr_scan_result(&self) -> Option<QrScanResult> {
let r_scan = self.qr_scan_state.read();
if r_scan.qr_scan_result.is_some() {
return Some(r_scan.qr_scan_result.clone().unwrap());
}
None
}
}
+299 -391
View File
@@ -12,432 +12,340 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use egui::RichText;
use egui::os::OperatingSystem;
use lazy_static::lazy_static;
use std::fs;
use std::sync::atomic::{AtomicBool, Ordering};
use egui::os::OperatingSystem;
use egui::{Align, Layout, RichText};
use lazy_static::lazy_static;
use crate::gui::Colors;
use crate::gui::icons::FILE_X;
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::network::NetworkContent;
use crate::gui::views::types::{ContentContainer, ModalPosition};
use crate::gui::views::wallets::WalletsContent;
use crate::gui::views::{Modal, View};
use crate::gui::views::types::{ModalContainer, ModalPosition};
use crate::node::Node;
use crate::{AppConfig, Settings};
use crate::gui::icons::{CHECK, CHECK_FAT, FILE_X};
use crate::gui::views::network::{NetworkContent, NodeSetup};
use crate::gui::views::wallets::WalletsContent;
lazy_static! {
/// Global state to check if [`NetworkContent`] panel is open.
static ref NETWORK_PANEL_OPEN: AtomicBool = AtomicBool::new(false);
/// Global state to check if [`NetworkContent`] panel is open.
static ref NETWORK_PANEL_OPEN: AtomicBool = AtomicBool::new(false);
}
/// Contains main ui content, handles side panel state.
pub struct Content {
/// Side panel [`NetworkContent`] content.
network: NetworkContent,
/// Central panel [`WalletsContent`] content.
pub wallets: WalletsContent,
/// Side panel [`NetworkContent`] content.
network: NetworkContent,
/// Check if app exit is allowed on close event of [`eframe::App`] implementation.
pub(crate) exit_allowed: bool,
/// Flag to show exit progress at [`Modal`].
show_exit_progress: bool,
/// Central panel [`WalletsContent`] content.
wallets: WalletsContent,
/// Flag to check it's first draw of content.
first_draw: bool,
/// Check if app exit is allowed on Desktop close event.
pub exit_allowed: bool,
/// Flag to show exit progress at [`Modal`].
show_exit_progress: bool,
/// List of allowed [`Modal`] ids for this [`ModalContainer`].
allowed_modal_ids: Vec<&'static str>
/// Flag to check it's first draw of content.
first_draw: bool,
}
impl Default for Content {
fn default() -> Self {
// Exit from eframe only for non-mobile platforms.
let os = OperatingSystem::from_target_os();
let exit_allowed = os == OperatingSystem::Android || os == OperatingSystem::IOS;
Self {
network: NetworkContent::default(),
wallets: WalletsContent::default(),
exit_allowed,
show_exit_progress: false,
first_draw: true,
allowed_modal_ids: vec![
Self::EXIT_CONFIRMATION_MODAL,
Self::SETTINGS_MODAL,
Self::ANDROID_INTEGRATED_NODE_WARNING_MODAL,
Self::CRASH_REPORT_MODAL
],
}
}
fn default() -> Self {
// Exit from eframe only for non-mobile platforms.
let os = OperatingSystem::from_target_os();
let exit_allowed = os == OperatingSystem::Android || os == OperatingSystem::IOS;
Self {
network: NetworkContent::default(),
wallets: WalletsContent::default(),
exit_allowed,
show_exit_progress: false,
first_draw: true,
}
}
}
impl ModalContainer for Content {
fn modal_ids(&self) -> &Vec<&'static str> {
&self.allowed_modal_ids
}
/// Identifier for integrated node warning [`Modal`] on Android.
const ANDROID_INTEGRATED_NODE_WARNING_MODAL: &'static str = "android_node_warning_modal";
/// Identifier for crash report [`Modal`].
const CRASH_REPORT_MODAL: &'static str = "crash_report_modal";
fn modal_ui(&mut self,
ui: &mut egui::Ui,
modal: &Modal,
cb: &dyn PlatformCallbacks) {
match modal.id {
Self::EXIT_CONFIRMATION_MODAL => self.exit_modal_content(ui, modal),
Self::SETTINGS_MODAL => self.settings_modal_ui(ui, modal),
Self::ANDROID_INTEGRATED_NODE_WARNING_MODAL => self.android_warning_modal_ui(ui, modal),
Self::CRASH_REPORT_MODAL => self.crash_report_modal_ui(ui, modal, cb),
_ => {}
}
}
impl ContentContainer for Content {
fn modal_ids(&self) -> Vec<&'static str> {
vec![
Self::EXIT_CONFIRMATION_MODAL,
ANDROID_INTEGRATED_NODE_WARNING_MODAL,
CRASH_REPORT_MODAL,
]
}
fn modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
match modal.id {
Self::EXIT_CONFIRMATION_MODAL => self.exit_modal_content(ui, modal, cb),
ANDROID_INTEGRATED_NODE_WARNING_MODAL => self.android_warning_modal_ui(ui),
CRASH_REPORT_MODAL => self.crash_report_modal_ui(ui, cb),
_ => {}
}
}
fn container_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
let dual_panel = Self::is_dual_panel_mode(ui.ctx());
let (is_panel_open, mut panel_width) = network_panel_state_width(ui.ctx(), dual_panel);
if self.network.showing_settings() {
panel_width = ui.available_width();
}
// Show network content.
egui::SidePanel::left("network_panel")
.resizable(false)
.exact_width(panel_width)
.frame(egui::Frame {
..Default::default()
})
.show_animated_inside(ui, is_panel_open, |ui| {
self.network.ui(ui, cb);
});
// Show wallets content.
egui::CentralPanel::default()
.frame(egui::Frame {
..Default::default()
})
.show_inside(ui, |ui| {
self.wallets.ui(ui, cb);
});
if self.first_draw {
// Show crash report or integrated node Android warning.
if Settings::crash_check_path().exists() {
Modal::new(CRASH_REPORT_MODAL)
.closeable(false)
.position(ModalPosition::Center)
.title(t!("crash_report"))
.show();
} else if OperatingSystem::from_target_os() == OperatingSystem::Android
&& AppConfig::android_integrated_node_warning_needed()
{
Modal::new(ANDROID_INTEGRATED_NODE_WARNING_MODAL)
.title(t!("network.node"))
.show();
}
self.first_draw = false;
}
}
}
impl Content {
/// Identifier for exit confirmation [`Modal`].
pub const EXIT_CONFIRMATION_MODAL: &'static str = "exit_confirmation_modal";
/// Identifier for wallet opening [`Modal`].
pub const SETTINGS_MODAL: &'static str = "settings_modal";
/// Identifier for integrated node warning [`Modal`] on Android.
const ANDROID_INTEGRATED_NODE_WARNING_MODAL: &'static str = "android_node_warning_modal";
/// Identifier for crash report [`Modal`].
const CRASH_REPORT_MODAL: &'static str = "crash_report_modal";
/// Default width of side panel at application UI.
pub const SIDE_PANEL_WIDTH: f32 = 400.0;
/// Desktop window title height.
pub const WINDOW_TITLE_HEIGHT: f32 = 38.0;
/// Margin of window frame at desktop.
pub const WINDOW_FRAME_MARGIN: f32 = 6.0;
/// Default width of side panel at application UI.
pub const SIDE_PANEL_WIDTH: f32 = 400.0;
/// Desktop window title height.
pub const WINDOW_TITLE_HEIGHT: f32 = 38.0;
/// Margin of window frame at desktop.
pub const WINDOW_FRAME_MARGIN: f32 = 6.0;
/// Identifier for exit confirmation [`Modal`].
pub const EXIT_CONFIRMATION_MODAL: &'static str = "exit_confirmation_modal";
pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
// Draw modal content for current ui container.
self.current_modal_ui(ui, cb);
/// Called to navigate back, return `true` if action was not consumed.
pub fn on_back(&mut self, ctx: &egui::Context, cb: &dyn PlatformCallbacks) -> bool {
if Modal::on_back() {
let dual_panel = Self::is_dual_panel_mode(ctx);
if !dual_panel && Self::is_network_panel_open() {
if self.network.on_back() {
Self::toggle_network_panel();
return false;
}
} else if self.wallets.on_back(cb) {
Self::show_exit_modal();
return false;
}
}
true
}
let dual_panel = Self::is_dual_panel_mode(ui);
let (is_panel_open, panel_width) = Self::network_panel_state_width(ui, dual_panel);
/// Check if ui can show [`NetworkContent`] and [`WalletsContent`] at same time.
pub fn is_dual_panel_mode(ctx: &egui::Context) -> bool {
let (w, h) = View::window_size(ctx);
// Screen is wide if width is greater than height or just 20% smaller.
let is_wide_screen = w > h || w + (w * 0.2) >= h;
// Dual panel mode is available when window is wide and its width is at least 2 times
// greater than minimal width of the side panel plus display insets from both sides.
let side_insets = View::get_left_inset() + View::get_right_inset();
is_wide_screen && w >= (Self::SIDE_PANEL_WIDTH * 2.0) + side_insets
}
// Show network content.
egui::SidePanel::left("network_panel")
.resizable(false)
.exact_width(panel_width)
.frame(egui::Frame {
..Default::default()
})
.show_animated_inside(ui, is_panel_open, |ui| {
self.network.ui(ui, cb);
});
/// Toggle [`NetworkContent`] panel state.
pub fn toggle_network_panel() {
let is_open = NETWORK_PANEL_OPEN.load(Ordering::Relaxed);
NETWORK_PANEL_OPEN.store(!is_open, Ordering::Relaxed);
}
// Show wallets content.
egui::CentralPanel::default()
.frame(egui::Frame {
..Default::default()
})
.show_inside(ui, |ui| {
self.wallets.ui(ui, cb);
});
/// Check if [`NetworkContent`] panel is open.
pub fn is_network_panel_open() -> bool {
NETWORK_PANEL_OPEN.load(Ordering::Relaxed)
}
if self.first_draw {
// Show crash report if needed.
if AppConfig::show_crash() {
Modal::new(Self::CRASH_REPORT_MODAL)
.closeable(false)
.position(ModalPosition::Center)
.title(t!("crash_report"))
.show();
} else {
// Show integrated node warning on Android if needed.
if OperatingSystem::from_target_os() == OperatingSystem::Android &&
AppConfig::android_integrated_node_warning_needed() {
Modal::new(Self::ANDROID_INTEGRATED_NODE_WARNING_MODAL)
.title(t!("network.node"))
.show();
}
}
self.first_draw = false;
}
}
/// Show exit confirmation [`Modal`].
pub fn show_exit_modal() {
Modal::new(Self::EXIT_CONFIRMATION_MODAL)
.title(t!("confirmation"))
.show();
}
/// Get [`NetworkContent`] panel state and width.
fn network_panel_state_width(ui: &mut egui::Ui, dual_panel: bool) -> (bool, f32) {
let is_panel_open = dual_panel || Self::is_network_panel_open();
let panel_width = if dual_panel {
Self::SIDE_PANEL_WIDTH + View::get_left_inset()
} else {
let is_fullscreen = ui.ctx().input(|i| {
i.viewport().fullscreen.unwrap_or(false)
});
View::window_size(ui).0 - if View::is_desktop() && !is_fullscreen &&
OperatingSystem::from_target_os() != OperatingSystem::Mac {
Self::WINDOW_FRAME_MARGIN * 2.0
} else {
0.0
}
};
(is_panel_open, panel_width)
}
/// Draw exit confirmation modal content.
fn exit_modal_content(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
if self.show_exit_progress {
if !Node::is_running() && !Node::data_dir_changing() {
self.exit_allowed = true;
cb.exit();
Modal::close();
}
ui.add_space(16.0);
ui.vertical_centered(|ui| {
View::small_loading_spinner(ui);
ui.add_space(12.0);
let exit_status_text = if Node::data_dir_changing() {
t!("moving_files")
} else {
t!("sync_status.shutdown")
};
ui.label(
RichText::new(exit_status_text)
.size(17.0)
.color(Colors::text(false)),
);
});
ui.add_space(10.0);
} else {
ui.add_space(8.0);
ui.vertical_centered(|ui| {
ui.label(
RichText::new(t!("modal_exit.description"))
.size(17.0)
.color(Colors::text(false)),
);
});
ui.add_space(10.0);
/// Check if ui can show [`NetworkContent`] and [`WalletsContent`] at same time.
pub fn is_dual_panel_mode(ui: &egui::Ui) -> bool {
let (w, h) = View::window_size(ui);
// Screen is wide if width is greater than height or just 20% smaller.
let is_wide_screen = w > h || w + (w * 0.2) >= h;
// Dual panel mode is available when window is wide and its width is at least 2 times
// greater than minimal width of the side panel plus display insets from both sides.
let side_insets = View::get_left_inset() + View::get_right_inset();
is_wide_screen && w >= (Self::SIDE_PANEL_WIDTH * 2.0) + side_insets
}
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
/// Toggle [`NetworkContent`] panel state.
pub fn toggle_network_panel() {
let is_open = NETWORK_PANEL_OPEN.load(Ordering::Relaxed);
NETWORK_PANEL_OPEN.store(!is_open, Ordering::Relaxed);
}
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(
ui,
t!("modal.cancel"),
Colors::white_or_black(false),
|| {
Modal::close();
},
);
});
columns[1].vertical_centered_justified(|ui| {
View::button_ui(
ui,
t!("modal_exit.exit"),
Colors::white_or_black(false),
|_| {
if !Node::is_running() && !Node::data_dir_changing() {
self.exit_allowed = true;
cb.exit();
Modal::close();
} else {
Node::stop(true);
modal.disable_closing();
Modal::set_title(t!("modal_exit.exit"));
self.show_exit_progress = true;
}
},
);
});
});
ui.add_space(6.0);
}
}
/// Check if [`NetworkContent`] panel is open.
pub fn is_network_panel_open() -> bool {
NETWORK_PANEL_OPEN.load(Ordering::Relaxed)
}
/// Draw content for integrated node warning [`Modal`] on Android.
fn android_warning_modal_ui(&mut self, ui: &mut egui::Ui) {
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(
RichText::new(t!("network.android_warning"))
.size(16.0)
.color(Colors::text(false)),
);
});
ui.add_space(8.0);
ui.vertical_centered_justified(|ui| {
View::button(ui, t!("continue"), Colors::white_or_black(false), || {
AppConfig::show_android_integrated_node_warning();
Modal::close();
});
});
ui.add_space(6.0);
}
/// Show exit confirmation [`Modal`].
pub fn show_exit_modal() {
Modal::new(Self::EXIT_CONFIRMATION_MODAL)
.title(t!("modal.confirmation"))
.show();
}
/// Draw content for integrated node warning [`Modal`] on Android.
fn crash_report_modal_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(
RichText::new(t!("crash_report_warning"))
.size(16.0)
.color(Colors::text(false)),
);
ui.add_space(6.0);
// Draw button to share log file.
let text = format!("{} {}", FILE_X, t!("share"));
View::colored_text_button(
ui,
text,
Colors::blue(),
Colors::white_or_black(false),
|| {
if let Ok(data) = fs::read_to_string(Settings::log_path()) {
let name = Settings::LOG_FILE_NAME.to_string();
let _ = cb.share_data(name, data.as_bytes().to_vec());
}
Settings::delete_crash_check();
Modal::close();
},
);
});
ui.add_space(8.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(8.0);
ui.vertical_centered_justified(|ui| {
View::button(
ui,
t!("modal.cancel"),
Colors::white_or_black(false),
|| {
Settings::delete_crash_check();
Modal::close();
},
);
});
ui.add_space(6.0);
}
}
/// Draw exit confirmation modal content.
fn exit_modal_content(&mut self, ui: &mut egui::Ui, modal: &Modal) {
if self.show_exit_progress {
if !Node::is_running() {
self.exit_allowed = true;
ui.ctx().send_viewport_cmd(egui::ViewportCommand::Close);
modal.close();
}
ui.add_space(16.0);
ui.vertical_centered(|ui| {
View::small_loading_spinner(ui);
ui.add_space(12.0);
ui.label(RichText::new(t!("sync_status.shutdown"))
.size(17.0)
.color(Colors::text(false)));
});
ui.add_space(10.0);
} else {
ui.add_space(8.0);
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("modal_exit.description"))
.size(17.0)
.color(Colors::text(false)));
});
ui.add_space(10.0);
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
modal.close();
});
});
columns[1].vertical_centered_justified(|ui| {
View::button_ui(ui, t!("modal_exit.exit"), Colors::white_or_black(false), |ui| {
if !Node::is_running() {
self.exit_allowed = true;
ui.ctx().send_viewport_cmd(egui::ViewportCommand::Close);
modal.close();
} else {
Node::stop(true);
modal.disable_closing();
Modal::set_title(t!("modal_exit.exit"));
self.show_exit_progress = true;
}
});
});
});
ui.add_space(6.0);
}
}
/// Handle Back key event.
pub fn on_back(&mut self) {
if Modal::on_back() {
if self.wallets.on_back() {
Self::show_exit_modal()
}
}
}
/// Draw creating wallet name/password input [`Modal`] content.
pub fn settings_modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal) {
ui.add_space(6.0);
// Draw chain type selection.
NodeSetup::chain_type_ui(ui);
ui.add_space(8.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(8.0);
// Draw theme selection.
Self::theme_selection_ui(ui);
ui.add_space(8.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(RichText::new(format!("{}:", t!("language")))
.size(16.0)
.color(Colors::gray())
);
});
ui.add_space(8.0);
// Draw available list of languages to select.
let locales = rust_i18n::available_locales!();
for (index, locale) in locales.iter().enumerate() {
Self::language_item_ui(locale, ui, index, locales.len(), modal);
}
ui.add_space(8.0);
// Show button to close modal.
ui.vertical_centered_justified(|ui| {
View::button(ui, t!("close"), Colors::white_or_black(false), || {
modal.close();
});
});
ui.add_space(6.0);
}
/// Draw theme selection content.
fn theme_selection_ui(ui: &mut egui::Ui) {
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("theme")).size(16.0).color(Colors::gray()));
});
let saved_use_dark = AppConfig::dark_theme().unwrap_or(false);
let mut selected_use_dark = saved_use_dark;
ui.add_space(8.0);
ui.columns(2, |columns| {
columns[0].vertical_centered(|ui| {
View::radio_value(ui, &mut selected_use_dark, false, t!("light"));
});
columns[1].vertical_centered(|ui| {
View::radio_value(ui, &mut selected_use_dark, true, t!("dark"));
})
});
ui.add_space(8.0);
if saved_use_dark != selected_use_dark {
AppConfig::set_dark_theme(selected_use_dark);
crate::setup_visuals(ui.ctx());
}
}
/// Draw language selection item content.
fn language_item_ui(locale: &str, ui: &mut egui::Ui, index: usize, len: usize, modal: &Modal) {
// Setup layout size.
let mut rect = ui.available_rect_before_wrap();
rect.set_height(50.0);
// Draw round background.
let bg_rect = rect.clone();
let item_rounding = View::item_rounding(index, len, false);
ui.painter().rect(bg_rect, item_rounding, Colors::fill(), View::item_stroke());
ui.vertical(|ui| {
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
// Draw button to select language.
let is_current = if let Some(lang) = AppConfig::locale() {
lang == locale
} else {
rust_i18n::locale() == locale
};
if !is_current {
View::item_button(ui, View::item_rounding(index, len, true), CHECK, None, || {
rust_i18n::set_locale(locale);
AppConfig::save_locale(locale);
modal.close();
});
} else {
ui.add_space(14.0);
ui.label(RichText::new(CHECK_FAT).size(20.0).color(Colors::green()));
ui.add_space(14.0);
}
let layout_size = ui.available_size();
ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| {
ui.add_space(12.0);
ui.vertical(|ui| {
// Draw language name.
ui.add_space(12.0);
let color = if is_current {
Colors::title(false)
} else {
Colors::gray()
};
ui.label(RichText::new(t!("lang_name", locale = locale))
.size(17.0)
.color(color));
ui.add_space(3.0);
});
});
});
});
}
/// Draw content for integrated node warning [`Modal`] on Android.
fn android_warning_modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal) {
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("network.android_warning"))
.size(16.0)
.color(Colors::text(false)));
});
ui.add_space(8.0);
ui.vertical_centered_justified(|ui| {
View::button(ui, t!("continue"), Colors::white_or_black(false), || {
AppConfig::show_android_integrated_node_warning();
modal.close();
});
});
ui.add_space(6.0);
}
/// Draw content for integrated node warning [`Modal`] on Android.
fn crash_report_modal_ui(&mut self,
ui: &mut egui::Ui,
modal: &Modal,
cb: &dyn PlatformCallbacks) {
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("crash_report_warning"))
.size(16.0)
.color(Colors::text(false)));
ui.add_space(6.0);
// Draw button to share crash report.
let text = format!("{} {}", FILE_X, t!("share"));
View::colored_text_button(ui, text, Colors::blue(), Colors::white_or_black(false), || {
if let Ok(data) = fs::read_to_string(Settings::crash_report_path()) {
cb.share_data(Settings::CRASH_REPORT_FILE_NAME.to_string(),
data.as_bytes().to_vec()).unwrap_or_default()
}
AppConfig::set_show_crash(false);
modal.close();
});
});
ui.add_space(8.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(8.0);
ui.vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
AppConfig::set_show_crash(false);
modal.close();
});
});
ui.add_space(6.0);
}
}
/// Get [`NetworkContent`] panel state and width.
fn network_panel_state_width(ctx: &egui::Context, dual_panel: bool) -> (bool, f32) {
let is_panel_open = dual_panel || Content::is_network_panel_open();
let panel_width = if dual_panel {
Content::SIDE_PANEL_WIDTH + View::get_left_inset()
} else {
let is_fullscreen = ctx.input(|i| i.viewport().fullscreen.unwrap_or(false));
View::window_size(ctx).0
- if View::is_desktop()
&& !is_fullscreen
&& OperatingSystem::from_target_os() != OperatingSystem::Mac
{
Content::WINDOW_FRAME_MARGIN * 2.0
} else {
0.0
}
};
(is_panel_open, panel_width)
}
-116
View File
@@ -1,116 +0,0 @@
// Copyright 2024 The Grim Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::{fs, thread};
use parking_lot::RwLock;
use crate::gui::Colors;
use crate::gui::icons::ARCHIVE_BOX;
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::View;
/// Button to pick file and parse its data into text.
pub struct FilePickButton {
/// Flag to check if file is picking.
pub file_picking: Arc<AtomicBool>,
/// Flag to check if file is parsing.
pub file_parsing: Arc<AtomicBool>,
/// File parsing result.
pub file_parsing_result: Arc<RwLock<Option<String>>>
}
impl Default for FilePickButton {
fn default() -> Self {
Self {
file_picking: Arc::new(AtomicBool::new(false)),
file_parsing: Arc::new(AtomicBool::new(false)),
file_parsing_result: Arc::new(RwLock::new(None))
}
}
}
impl FilePickButton {
/// Draw button content.
pub fn ui(&mut self,
ui: &mut egui::Ui,
cb: &dyn PlatformCallbacks,
on_result: impl FnOnce(String)) {
if self.file_picking.load(Ordering::Relaxed) {
View::small_loading_spinner(ui);
// Check file pick result.
if let Some(path) = cb.picked_file() {
self.file_picking.store(false, Ordering::Relaxed);
if !path.is_empty() {
self.on_file_pick(path);
}
}
} else if self.file_parsing.load(Ordering::Relaxed) {
View::small_loading_spinner(ui);
// Check file parsing result.
let has_result = {
let r_res = self.file_parsing_result.read();
r_res.is_some()
};
if has_result {
let text = {
let r_res = self.file_parsing_result.read();
r_res.clone().unwrap()
};
// Callback on result.
on_result(text);
// Clear result.
let mut w_res = self.file_parsing_result.write();
*w_res = None;
self.file_parsing.store(false, Ordering::Relaxed);
}
} else {
// Draw button to pick file.
let file_text = format!("{} {}", ARCHIVE_BOX, t!("choose_file"));
View::colored_text_button(ui, file_text, Colors::blue(), Colors::button(), || {
if let Some(path) = cb.pick_file() {
self.on_file_pick(path);
}
});
}
}
/// Handle picked file path.
fn on_file_pick(&self, path: String) {
// Wait for asynchronous file pick result if path is empty.
if path.is_empty() {
self.file_picking.store(true, Ordering::Relaxed);
return;
}
self.file_parsing.store(true, Ordering::Relaxed);
let result = self.file_parsing_result.clone();
thread::spawn(move || {
if path.ends_with(".gif") {
//TODO: Detect QR codes on GIF file.
} else if path.ends_with(".jpeg") || path.ends_with(".jpg") ||
path.ends_with(".png") {
//TODO: Detect QR codes on image files.
} else {
// Parse file as plain text.
let mut w_res = result.write();
if let Ok(text) = fs::read_to_string(path) {
*w_res = Some(text);
} else {
*w_res = Some("".to_string());
}
}
});
}
}
+195
View File
@@ -0,0 +1,195 @@
// Copyright 2024 The Grim Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use egui::CornerRadius;
use parking_lot::RwLock;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::{fs, thread};
use crate::gui::Colors;
use crate::gui::icons::ARCHIVE_BOX;
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::View;
/// Type of button.
pub enum FilePickContentType {
Button(String),
ItemButton(CornerRadius),
Tab,
}
/// Button to pick file and parse its data into text.
pub struct FilePickContent {
/// Content type.
content_type: FilePickContentType,
/// Flag to check if button is active.
active: bool,
/// Flag to check if file is picking.
file_picking: Arc<AtomicBool>,
/// Flag to check if folder should be picked.
pick_folder: bool,
/// Flag to parse file content after pick.
parse_file: bool,
/// Flag to check if file is parsing.
file_parsing: Arc<AtomicBool>,
/// File parsing result.
file_parsing_result: Arc<RwLock<Option<String>>>,
}
impl FilePickContent {
/// Create new content from provided type.
pub fn new(content_type: FilePickContentType) -> Self {
Self {
content_type,
active: false,
file_picking: Arc::new(AtomicBool::new(false)),
pick_folder: false,
parse_file: true,
file_parsing: Arc::new(AtomicBool::new(false)),
file_parsing_result: Arc::new(RwLock::new(None)),
}
}
/// Pick folder.
pub fn pick_folder(mut self) -> Self {
self.pick_folder = true;
self
}
/// Do not parse file content.
pub fn no_parse(mut self) -> Self {
self.parse_file = false;
self
}
/// Enable or disable the button.
pub fn set_active(&mut self, active: bool) {
self.active = active;
}
/// Draw content with provided callback to return path of the file.
pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks, pick: impl FnOnce(String)) {
if self.file_picking.load(Ordering::Relaxed) {
View::small_loading_spinner(ui);
// Check file pick result.
if let Some(path) = cb.picked_file() {
self.file_picking.store(false, Ordering::Relaxed);
if !path.is_empty() {
if self.parse_file {
self.parse_file(path);
} else {
pick(path);
}
}
}
} else if self.file_parsing.load(Ordering::Relaxed) {
View::small_loading_spinner(ui);
// Check file parsing result.
let has_result = {
let r_res = self.file_parsing_result.read();
r_res.is_some()
};
if has_result {
let text = {
let r_res = self.file_parsing_result.read();
r_res.clone().unwrap()
};
// Callback on result.
pick(text);
// Clear result.
let mut w_res = self.file_parsing_result.write();
*w_res = None;
self.file_parsing.store(false, Ordering::Relaxed);
}
} else {
// Draw button to pick file.
match &self.content_type {
FilePickContentType::Button(text) => {
let text = format!("{} {}", ARCHIVE_BOX, text);
let text_color = Colors::blue();
let fill = Colors::white_or_black(false);
View::colored_text_button(ui, text, text_color, fill, || {
self.on_file_pick(pick, cb);
});
}
FilePickContentType::ItemButton(r) => {
View::item_button(ui, r.clone(), ARCHIVE_BOX, Some(Colors::blue()), || {
self.on_file_pick(pick, cb);
});
}
FilePickContentType::Tab => {
let active = match self.active {
true => Some(
self.file_parsing.load(Ordering::Relaxed)
|| self.file_picking.load(Ordering::Relaxed),
),
false => None,
};
View::tab_button(ui, ARCHIVE_BOX, Some(Colors::blue()), active, |_| {
self.on_file_pick(pick, cb);
});
}
}
}
}
/// Handle pick file request.
fn on_file_pick(&self, on_pick: impl FnOnce(String), cb: &dyn PlatformCallbacks) {
let path = if self.pick_folder {
cb.pick_folder()
} else {
cb.pick_file()
};
if path.is_none() {
return;
}
let path = path.unwrap();
// Wait for asynchronous file pick result if path is empty.
if path.is_empty() {
self.file_picking.store(true, Ordering::Relaxed);
return;
}
// Parse result if needed.
if self.parse_file {
self.parse_file(path);
} else {
on_pick(path);
}
}
/// Handle picked file path.
fn parse_file(&self, path: String) {
self.file_parsing.store(true, Ordering::Relaxed);
let result = self.file_parsing_result.clone();
thread::spawn(move || {
if path.ends_with(".gif") {
//TODO: Detect QR codes on GIF file.
} else if path.ends_with(".jpeg") || path.ends_with(".jpg") || path.ends_with(".png") {
//TODO: Detect QR codes on image files.
} else {
// Parse file as plain text.
let mut w_res = result.write();
if let Ok(text) = fs::read_to_string(path) {
*w_res = Some(text);
} else {
*w_res = Some("".to_string());
}
}
});
}
}
+475
View File
@@ -0,0 +1,475 @@
// Copyright 2025 The Grim Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use egui::text_edit::TextEditState;
use egui::{Align, Layout, TextBuffer, TextStyle, ViewportCommand, Widget};
use lazy_static::lazy_static;
use parking_lot::RwLock;
use std::sync::Arc;
use crate::gui::Colors;
use crate::gui::icons::{CLIPBOARD_TEXT, COPY, EYE, EYE_SLASH, SCAN};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::input::keyboard::KeyboardContent;
use crate::gui::views::{KeyboardEvent, View};
/// Text input content.
pub struct TextEdit {
/// View identifier.
id: egui::Id,
/// Check if input is enabled or disabled.
enabled: bool,
/// Horizontal text centering is needed.
h_center: bool,
/// Focus is needed.
focus: bool,
/// Focus request was passed.
focus_request: bool,
/// Hide letters and draw button to show/hide letters.
password: bool,
/// Show copy button.
copy: bool,
/// Show paste button.
paste: bool,
/// Show button to scan QR code into text.
scan_qr: bool,
/// Scan button was pressed.
pub scan_pressed: bool,
/// Tab or Enter keys were pressed to focus on next line.
pub enter_pressed: bool,
/// Flag to enter only numbers.
numeric: bool,
/// Flag to not show soft keyboard.
no_soft_keyboard: bool,
}
impl TextEdit {
/// Default height of [`egui::TextEdit`] view.
const TEXT_EDIT_HEIGHT: f32 = 42.0;
pub fn new(id: egui::Id) -> Self {
Self {
id,
enabled: true,
h_center: false,
focus: true,
focus_request: false,
password: false,
copy: false,
paste: false,
scan_qr: false,
scan_pressed: false,
enter_pressed: false,
numeric: false,
no_soft_keyboard: is_android(),
}
}
/// Draw text input.
pub fn ui(&mut self, ui: &mut egui::Ui, input: &mut String, cb: &dyn PlatformCallbacks) {
self.input_ui(ui, input, |_| {}, cb);
}
/// Draw text input with additional buttons (right to left order).
pub fn custom_buttons_ui(
&mut self,
ui: &mut egui::Ui,
input: &mut String,
cb: &dyn PlatformCallbacks,
buttons_content: impl FnOnce(&mut egui::Ui),
) {
self.input_ui(ui, input, buttons_content, cb);
}
/// Draw text input content.
fn input_ui(
&mut self,
ui: &mut egui::Ui,
input: &mut String,
buttons_content: impl FnOnce(&mut egui::Ui),
cb: &dyn PlatformCallbacks,
) {
let mut layout_rect = ui.available_rect_before_wrap();
layout_rect.set_height(Self::TEXT_EDIT_HEIGHT);
ui.allocate_ui_with_layout(
layout_rect.size(),
Layout::right_to_left(Align::Max),
|ui| {
let mut hide_input = false;
if self.password {
let show_pass_id = egui::Id::new(self.id).with("_show_pass");
hide_input = ui.data(|data| data.get_temp(show_pass_id)).unwrap_or(true);
// Draw button to show/hide current password.
let eye_icon = if hide_input { EYE } else { EYE_SLASH };
View::button_ui(
ui,
eye_icon.to_string(),
Colors::white_or_black(false),
|ui| {
hide_input = !hide_input;
ui.data_mut(|data| {
data.insert_temp(show_pass_id, hide_input);
});
},
);
ui.add_space(8.0);
}
// Extra buttons content.
(buttons_content)(ui);
// Setup copy button.
if self.copy {
let copy_icon = COPY.to_string();
View::button(ui, copy_icon, Colors::white_or_black(false), || {
cb.copy_string_to_buffer(input.clone());
});
ui.add_space(8.0);
}
// Setup paste button.
if self.paste {
let paste_icon = CLIPBOARD_TEXT.to_string();
View::button(ui, paste_icon, Colors::white_or_black(false), || {
*input = cb.get_string_from_buffer();
});
ui.add_space(8.0);
}
// Setup scan QR code button.
if self.scan_qr {
let scan_icon = SCAN.to_string();
View::button(ui, scan_icon, Colors::white_or_black(false), || {
cb.start_camera();
self.scan_pressed = true;
});
ui.add_space(8.0);
}
let layout_size = ui.available_size();
ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Min), |ui| {
// Setup text edit size.
let mut edit_rect = ui.available_rect_before_wrap();
edit_rect.set_height(Self::TEXT_EDIT_HEIGHT);
// Setup focused input value to avoid dismiss when click on keyboard.
let focused_input_id = egui::Id::new("focused_input_id");
let focused = ui
.data(|data| data.get_temp(focused_input_id))
.unwrap_or(egui::Id::new(""))
== self.id;
// Show text edit.
let text_edit_resp = egui::TextEdit::singleline(input)
.text_color(if self.enabled {
Colors::text(false)
} else {
Colors::inactive_text()
})
.interactive(self.enabled)
.id(self.id)
.font(TextStyle::Heading)
.min_size(edit_rect.size())
.margin(if View::is_desktop() {
egui::Margin::symmetric(4, 2)
} else {
egui::Margin::symmetric(8, 8)
})
.horizontal_align(if self.h_center {
Align::Center
} else {
Align::Min
})
.vertical_align(Align::Center)
.password(hide_input)
.cursor_at_end(true)
.ui(ui);
// Setup focus state.
let clicked = text_edit_resp.clicked();
if !text_edit_resp.has_focus()
&& (self.focus || self.focus_request || clicked || focused)
{
text_edit_resp.request_focus();
}
// Reset keyboard state for newly focused.
if clicked || self.focus_request {
ui.ctx()
.send_viewport_cmd(ViewportCommand::IMEAllowed(true));
KeyboardContent::reset_window_state();
}
// Apply text from software input.
if text_edit_resp.has_focus() {
ui.data_mut(|data| {
data.insert_temp(focused_input_id, self.id);
});
self.enter_pressed = self.on_soft_input(ui, self.id, false, input);
// Check Enter or Tab keys press.
if !self.focus_request {
if ui.ctx().input(|i| {
i.key_pressed(egui::Key::Enter) || i.key_pressed(egui::Key::Tab)
}) {
self.enter_pressed = true;
}
}
if self.enter_pressed {
KeyboardContent::unshift();
}
if !self.no_soft_keyboard {
KeyboardContent::default().window_ui(self.numeric, ui.ctx());
}
}
});
},
);
// Immediate repaint when input is open.
ui.ctx().request_repaint();
}
/// Apply soft keyboard input data to provided String, returns `true` if Enter was pressed.
fn on_soft_input(
&self,
ui: &mut egui::Ui,
id: egui::Id,
multiline: bool,
value: &mut String,
) -> bool {
let event: Option<KeyboardEvent> = if is_android() {
let mut w_input = LAST_SOFT_KEYBOARD_EVENT.write();
w_input.take()
} else {
KeyboardContent::consume_event()
};
// Handle keyboard input event.
if let Some(e) = event {
let mut enter_pressed = false;
let mut state = TextEditState::load(ui.ctx(), id).unwrap();
match state.cursor.char_range() {
None => {}
Some(range) => {
let mut r = range.clone();
let mut index = r.primary.index;
let selected = r.primary.index != r.secondary.index;
let start_select =
f32::min(r.primary.index as f32, r.secondary.index as f32) as usize;
let end_select =
f32::max(r.primary.index as f32, r.secondary.index as f32) as usize;
match e {
KeyboardEvent::TEXT(text) => {
if selected {
*value = {
let part1: String =
value.chars().skip(0).take(start_select).collect();
let part2: String = value
.chars()
.skip(end_select)
.take(value.len() - end_select)
.collect();
format!("{}{}{}", part1, text, part2)
};
index = start_select + 1;
} else {
value.insert_text(text.as_str(), index);
index = index + 1;
}
}
KeyboardEvent::CLEAR => {
if selected {
*value = {
let part1: String =
value.chars().skip(0).take(start_select).collect();
let part2: String = value
.chars()
.skip(end_select)
.take(value.len() - end_select)
.collect();
format!("{}{}", part1, part2)
};
index = start_select;
} else if index != 0 {
*value = {
let part1: String =
value.chars().skip(0).take(index - 1).collect();
let part2: String = value
.chars()
.skip(index)
.take(value.len() - index)
.collect();
format!("{}{}", part1, part2)
};
index = index - 1;
}
}
KeyboardEvent::ENTER => {
if multiline {
value.insert_text("\n", index);
index = index + 1;
} else {
enter_pressed = true;
}
}
}
// Setup cursor index.
r.primary.index = index;
r.secondary.index = r.primary.index;
state.cursor.set_char_range(Some(r));
TextEditState::store(state, ui.ctx(), id);
}
}
return enter_pressed;
}
false
}
/// Set cursor to the end of text.
pub fn cursor_to_end(&self, text_len: usize, ui: &mut egui::Ui) {
let mut state = TextEditState::load(ui.ctx(), self.id).unwrap();
match state.cursor.char_range() {
None => {}
Some(range) => {
let mut r = range.clone();
r.primary.index = text_len;
r.secondary.index = text_len;
state.cursor.set_char_range(Some(r));
TextEditState::store(state, ui.ctx(), self.id);
}
}
}
/// Disable input.
pub fn disable(mut self) -> Self {
self.enabled = false;
self
}
/// Center text horizontally.
pub fn h_center(mut self) -> Self {
self.h_center = true;
self
}
/// Enable or disable constant focus.
pub fn focus(mut self, focus: bool) -> Self {
self.focus = focus;
self
}
/// Focus on field.
pub fn focus_request(&mut self) {
self.focus_request = true;
}
/// Allow input of numbers only.
pub fn numeric(mut self) -> Self {
self.numeric = true;
self
}
/// Hide letters and draw button to show/hide letters.
pub fn password(mut self) -> Self {
self.password = true;
self
}
/// Show button to copy text.
pub fn copy(mut self) -> Self {
self.copy = true;
self
}
/// Show button to paste text.
pub fn paste(mut self) -> Self {
self.paste = true;
self
}
/// Show button to scan QR code to text.
pub fn scan_qr(mut self) -> Self {
self.scan_qr = true;
self.scan_pressed = false;
self
}
/// Do not show soft keyboard for input.
pub fn no_soft_keyboard(mut self) -> Self {
self.no_soft_keyboard = true;
self
}
}
/// Check if current system is Android.
fn is_android() -> bool {
egui::os::OperatingSystem::from_target_os() == egui::os::OperatingSystem::Android
}
lazy_static! {
static ref LAST_SOFT_KEYBOARD_EVENT: Arc<RwLock<Option<KeyboardEvent>>> =
Arc::new(RwLock::new(None));
}
#[allow(dead_code)]
#[cfg(target_os = "android")]
#[allow(non_snake_case)]
#[unsafe(no_mangle)]
/// Callback from Java code with last entered character from soft keyboard.
pub extern "C" fn Java_mw_gri_android_MainActivity_onTextInput(
_env: jni::JNIEnv,
_class: jni::objects::JObject,
char: jni::sys::jstring,
) {
use jni::objects::JString;
unsafe {
let j_obj = JString::from_raw(char);
let j_str = _env.get_string_unchecked(j_obj.as_ref()).unwrap();
match j_str.to_str() {
Ok(str) => {
let mut w_input = LAST_SOFT_KEYBOARD_EVENT.write();
*w_input = Some(KeyboardEvent::TEXT(str.to_string()));
}
Err(_) => {}
}
}
}
#[allow(dead_code)]
#[cfg(target_os = "android")]
#[allow(non_snake_case)]
#[unsafe(no_mangle)]
/// Callback from Java code when Clear key was pressed at soft keyboard.
pub extern "C" fn Java_mw_gri_android_MainActivity_onClearInput(
_env: jni::JNIEnv,
_class: jni::objects::JObject,
) {
let mut w_input = LAST_SOFT_KEYBOARD_EVENT.write();
*w_input = Some(KeyboardEvent::CLEAR);
}
#[allow(dead_code)]
#[cfg(target_os = "android")]
#[allow(non_snake_case)]
#[unsafe(no_mangle)]
/// Callback from Java code when Enter key was pressed at soft keyboard.
pub extern "C" fn Java_mw_gri_android_MainActivity_onEnterInput(
_env: jni::JNIEnv,
_class: jni::objects::JObject,
) {
let mut w_input = LAST_SOFT_KEYBOARD_EVENT.write();
*w_input = Some(KeyboardEvent::ENTER);
}
+549
View File
@@ -0,0 +1,549 @@
// Copyright 2025 The Grim Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use egui::{
Align, Align2, Button, Color32, CursorIcon, Layout, Margin, Rect, Response, RichText, Sense,
Shadow, Vec2, Widget,
};
use lazy_static::lazy_static;
use parking_lot::RwLock;
use std::string::ToString;
use std::sync::Arc;
use std::sync::atomic::Ordering;
use crate::AppConfig;
use crate::gui::Colors;
use crate::gui::icons::{ARROW_FAT_UP, BACKSPACE, GLOBE_SIMPLE, KEY_RETURN};
use crate::gui::views::{KeyboardEvent, KeyboardLayout, KeyboardState, View};
lazy_static! {
/// Keyboard window state.
static ref WINDOW_STATE: Arc<RwLock<KeyboardState >> = Arc::new(
RwLock::new(KeyboardState::default())
);
}
/// Software keyboard content.
pub struct KeyboardContent {
/// Keyboard content state.
state: KeyboardState,
}
impl Default for KeyboardContent {
fn default() -> Self {
Self {
state: KeyboardState::default(),
}
}
}
impl KeyboardContent {
/// Maximum keyboard content width.
const MAX_WIDTH: f32 = 600.0;
/// Maximum numbers layout width.
const MAX_WIDTH_NUMBERS: f32 = 400.0;
/// Keyboard window id.
pub const WINDOW_ID: &'static str = "soft_keyboard_window";
/// Draw keyboard content as separate [`Window`].
pub fn window_ui(&mut self, numeric: bool, ctx: &egui::Context) {
let width = ctx.content_rect().width();
let layer_id = egui::Window::new(Self::WINDOW_ID)
.title_bar(false)
.resizable(false)
.collapsible(false)
.min_width(width)
.default_width(width)
.anchor(Align2::CENTER_BOTTOM, Vec2::new(0.0, 0.0))
.frame(egui::Frame {
shadow: Shadow {
offset: Default::default(),
blur: 30.0 as u8,
spread: 3.0 as u8,
color: Color32::from_black_alpha(32),
},
inner_margin: Margin {
left: View::get_left_inset() as i8,
right: View::get_right_inset() as i8,
top: 1.0 as i8,
bottom: View::get_bottom_inset() as i8,
},
fill: Colors::fill(),
..Default::default()
})
.show(ctx, |ui| {
ui.set_min_width(width);
// Setup state.
{
let r_state = WINDOW_STATE.read();
self.state = (*r_state).clone();
}
// Calculate content width.
let side_insets = View::get_left_inset() + View::get_right_inset();
let available_width = width - side_insets;
let w = f32::min(
available_width,
if numeric {
Self::MAX_WIDTH_NUMBERS
} else {
Self::MAX_WIDTH
},
);
// Draw content.
View::max_width_ui(ui, w, |ui| {
self.ui(numeric, ui);
});
// Save state.
let mut w_state = WINDOW_STATE.write();
*w_state = self.state.clone();
})
.unwrap()
.response
.layer_id;
// Always show keyboard above others windows.
ctx.move_to_top(layer_id);
}
/// Draw keyboard content.
pub fn ui(&mut self, numeric: bool, ui: &mut egui::Ui) {
// Setup layout.
if numeric {
self.state.layout = Arc::new(KeyboardLayout::NUMBERS);
} else if *self.state.layout == KeyboardLayout::NUMBERS {
self.state.layout = Arc::new(KeyboardLayout::TEXT);
}
// Setup spacing between buttons.
ui.style_mut().spacing.item_spacing = egui::vec2(0.0, 0.0);
// Setup vertical padding inside buttons.
ui.style_mut().spacing.button_padding = egui::vec2(0.0, if numeric { 12.0 } else { 10.0 });
// Draw input buttons.
let button_rect = match *self.state.layout {
KeyboardLayout::TEXT => self.text_ui(ui),
KeyboardLayout::SYMBOLS => self.symbols_ui(ui),
KeyboardLayout::NUMBERS => self.numbers_ui(ui),
};
// Draw bottom keyboard buttons.
let bottom_size = {
let mut r = button_rect.clone();
r.set_width(ui.available_width());
r.size()
};
let button_width = ui.available_width()
/ match *self.state.layout {
KeyboardLayout::TEXT => 11.0,
KeyboardLayout::SYMBOLS => 10.0,
KeyboardLayout::NUMBERS => 4.0,
};
ui.allocate_ui_with_layout(bottom_size, Layout::right_to_left(Align::Center), |ui| {
match *self.state.layout {
KeyboardLayout::TEXT => {
// Enter key input.
ui.horizontal_centered(|ui| {
ui.set_max_width(button_width * 2.0);
self.custom_button_ui(
KEY_RETURN.to_string(),
Colors::white_or_black(false),
Some(Colors::green()),
ui,
|_, c| {
c.state.last_event = Arc::new(Some(KeyboardEvent::ENTER));
},
);
});
// Custom input key.
ui.horizontal_centered(|ui| {
ui.set_max_width(button_width);
self.input_button_ui("m3", true, ui);
});
// Space key input.
ui.horizontal_centered(|ui| {
ui.set_max_width(button_width * 5.0);
self.custom_button_ui(
" ".to_string(),
Colors::inactive_text(),
None,
ui,
|l, c| {
c.state.last_event = Arc::new(Some(KeyboardEvent::TEXT(l)));
},
);
});
// Switch to english and back.
ui.horizontal_centered(|ui| {
ui.set_max_width(button_width);
self.custom_button_ui(
GLOBE_SIMPLE.to_string(),
Colors::text_button(),
Some(Colors::fill_lite()),
ui,
|_, _| AppConfig::toggle_english_keyboard(),
);
});
// Switch to symbols layout.
self.custom_button_ui(
"!@ツ".to_string(),
Colors::text_button(),
Some(Colors::fill_lite()),
ui,
|_, c| {
c.state.layout = Arc::new(KeyboardLayout::SYMBOLS);
},
);
}
KeyboardLayout::SYMBOLS => {
// Enter key input.
ui.horizontal_centered(|ui| {
ui.set_max_width(button_width * 2.0);
self.custom_button_ui(
KEY_RETURN.to_string(),
Colors::white_or_black(false),
Some(Colors::green()),
ui,
|_, c| {
c.state.last_event = Arc::new(Some(KeyboardEvent::ENTER));
},
);
});
// Custom input key.
ui.horizontal_centered(|ui| {
ui.set_max_width(button_width);
self.input_button_ui("", false, ui);
});
// Space key input.
ui.horizontal_centered(|ui| {
ui.set_max_width(button_width * 4.0);
self.custom_button_ui(
" ".to_string(),
Colors::inactive_text(),
None,
ui,
|l, c| {
c.state.last_event = Arc::new(Some(KeyboardEvent::TEXT(l)));
},
);
});
// Switch to text layout.
let label = {
let q = t!("keyboard.q", locale = Self::input_locale().as_str());
let w = t!("keyboard.w", locale = Self::input_locale().as_str());
let e = t!("keyboard.e", locale = Self::input_locale().as_str());
format!("{}{}{}", q, w, e).to_uppercase()
};
self.custom_button_ui(
label,
Colors::text_button(),
Some(Colors::fill_lite()),
ui,
|_, c| {
c.state.layout = Arc::new(KeyboardLayout::TEXT);
},
);
}
KeyboardLayout::NUMBERS => {
ui.horizontal_centered(|ui| {
ui.set_max_width(button_width * 2.0);
self.custom_button_ui(
KEY_RETURN.to_string(),
Colors::white_or_black(false),
Some(Colors::green()),
ui,
|_, c| {
c.state.last_event = Arc::new(Some(KeyboardEvent::ENTER));
},
);
});
ui.horizontal_centered(|ui| {
ui.set_max_width(button_width);
self.input_button_ui("0", true, ui);
});
ui.horizontal_centered(|ui| {
ui.set_max_width(button_width);
self.input_button_ui(".", false, ui);
});
}
}
});
}
/// Draw numbers content returning button [`Rect`].
fn numbers_ui(&mut self, ui: &mut egui::Ui) -> Rect {
let mut button_rect = ui.available_rect_before_wrap();
let tl_0: Vec<&str> = vec!["1", "2", "3", "+"];
ui.columns(tl_0.len(), |columns| {
for (index, s) in tl_0.iter().enumerate() {
let last = index == tl_0.len() - 1;
button_rect = self.input_button_ui(s, !last, &mut columns[index]);
}
});
let tl_1: Vec<&str> = vec!["4", "5", "6", ","];
ui.columns(tl_1.len(), |columns| {
for (index, s) in tl_1.iter().enumerate() {
let last = index == tl_1.len() - 1;
self.input_button_ui(s, !last, &mut columns[index]);
}
});
let tl_2: Vec<&str> = vec!["7", "8", "9", BACKSPACE];
ui.columns(tl_2.len(), |columns| {
for (index, s) in tl_2.iter().enumerate() {
if index == tl_2.len() - 1 {
self.custom_button_ui(
BACKSPACE.to_string(),
Colors::red(),
Some(Colors::fill_lite()),
&mut columns[index],
|_, c| {
c.state.last_event = Arc::new(Some(KeyboardEvent::CLEAR));
},
);
} else {
self.input_button_ui(s, true, &mut columns[index]);
}
}
});
button_rect
}
/// Draw text content returning button [`Rect`].
fn text_ui(&mut self, ui: &mut egui::Ui) -> Rect {
let mut button_rect = ui.available_rect_before_wrap();
let tl_0: Vec<&str> = vec!["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "01"];
ui.columns(tl_0.len(), |columns| {
for (index, s) in tl_0.iter().enumerate() {
button_rect = self.input_button_ui(s, true, &mut columns[index]);
}
});
let tl_1: Vec<&str> = vec!["q", "w", "e", "r", "t", "y", "u", "i", "o", "p", "p1"];
ui.columns(tl_1.len(), |columns| {
for (index, s) in tl_1.iter().enumerate() {
self.input_button_ui(s, true, &mut columns[index]);
}
});
let tl_2: Vec<&str> = vec!["a", "s", "d", "f", "g", "h", "j", "k", "l", "l1", "l2"];
ui.columns(tl_2.len(), |columns| {
for (index, s) in tl_2.iter().enumerate() {
self.input_button_ui(s, true, &mut columns[index]);
}
});
let tl_3: Vec<&str> = vec![
ARROW_FAT_UP,
"z",
"x",
"c",
"v",
"b",
"n",
"m",
"m1",
"m2",
BACKSPACE,
];
ui.columns(tl_3.len(), |columns| {
for (index, s) in tl_3.iter().enumerate() {
if index == 0 {
let shift = self.state.shift.load(Ordering::Relaxed);
let color = if shift {
Colors::yellow_dark()
} else {
Colors::inactive_text()
};
self.custom_button_ui(
ARROW_FAT_UP.to_string(),
color,
Some(Colors::fill_lite()),
&mut columns[index],
|_, c| {
c.state.shift.store(!shift, Ordering::Relaxed);
},
);
} else if index == tl_3.len() - 1 {
self.custom_button_ui(
BACKSPACE.to_string(),
Colors::red(),
Some(Colors::fill_lite()),
&mut columns[index],
|_, c| {
c.state.last_event = Arc::new(Some(KeyboardEvent::CLEAR));
},
);
} else {
self.input_button_ui(s, true, &mut columns[index]);
}
}
});
button_rect
}
/// Draw symbols content returning button [`Rect`].
fn symbols_ui(&mut self, ui: &mut egui::Ui) -> Rect {
let mut button_rect = ui.available_rect_before_wrap();
let tl_0: Vec<&str> = vec!["[", "]", "{", "}", "#", "%", "^", "*", "+", "="];
ui.columns(tl_0.len(), |columns| {
for (index, s) in tl_0.iter().enumerate() {
button_rect = self.input_button_ui(s, false, &mut columns[index]);
}
});
let tl_1: Vec<&str> = vec!["_", "\\", "|", "~", "<", ">", "", "", "π", ""];
ui.columns(tl_1.len(), |columns| {
for (index, s) in tl_1.iter().enumerate() {
self.input_button_ui(s, false, &mut columns[index]);
}
});
let tl_2: Vec<&str> = vec!["-", "/", ":", ";", "(", ")", "`", "&", "@", "\""];
ui.columns(tl_2.len(), |columns| {
for (index, s) in tl_2.iter().enumerate() {
self.input_button_ui(s, false, &mut columns[index]);
}
});
let tl_3: Vec<&str> = vec![".", ",", "?", "!", "", "£", "¥", "$", "¢", BACKSPACE];
ui.columns(tl_3.len(), |columns| {
for (index, s) in tl_3.iter().enumerate() {
if index == tl_3.len() - 1 {
self.custom_button_ui(
BACKSPACE.to_string(),
Colors::red(),
Some(Colors::fill_lite()),
&mut columns[index],
|_, c| {
c.state.last_event = Arc::new(Some(KeyboardEvent::CLEAR));
},
);
} else {
self.input_button_ui(s, false, &mut columns[index]);
}
}
});
button_rect
}
/// Draw custom keyboard button.
fn custom_button_ui(
&mut self,
s: String,
color: Color32,
bg: Option<Color32>,
ui: &mut egui::Ui,
cb: impl FnOnce(String, &mut KeyboardContent),
) -> Response {
ui.vertical_centered_justified(|ui| {
// Disable expansion on click/hover.
ui.style_mut().visuals.widgets.hovered.expansion = 0.0;
ui.style_mut().visuals.widgets.active.expansion = 0.0;
// Setup fill colors.
ui.visuals_mut().widgets.inactive.weak_bg_fill = Colors::white_or_black(false);
ui.visuals_mut().widgets.hovered.weak_bg_fill = Colors::fill_lite();
ui.visuals_mut().widgets.active.weak_bg_fill = Colors::fill();
// Setup stroke colors.
ui.visuals_mut().widgets.inactive.bg_stroke = View::item_stroke();
ui.visuals_mut().widgets.hovered.bg_stroke = View::item_stroke();
ui.visuals_mut().widgets.active.bg_stroke = View::hover_stroke();
let shift = self.state.shift.load(Ordering::Relaxed);
let label = if shift {
s.to_uppercase()
} else {
s.to_string()
};
let mut button = Button::new(RichText::new(label.clone()).size(18.0).color(color))
.corner_radius(egui::CornerRadius::ZERO);
if let Some(bg) = bg {
button = button.fill(bg);
}
// Setup long press/touch.
let long_press = s == BACKSPACE;
if long_press {
button = button.sense(Sense::click_and_drag());
}
// Draw button.
let resp = button.ui(ui).on_hover_cursor(CursorIcon::PointingHand);
if resp.clicked() || resp.long_touched() || resp.dragged() {
cb(label, self);
}
})
.response
}
/// Draw input button.
fn input_button_ui(&mut self, s: &str, translate: bool, ui: &mut egui::Ui) -> Rect {
let value = if translate {
t!(
format!("keyboard.{}", s),
locale = Self::input_locale().as_str()
)
.into()
} else {
s.to_string()
};
let rect = self
.custom_button_ui(value, Colors::text_button(), None, ui, |l, c| {
c.state.last_event = Arc::new(Some(KeyboardEvent::TEXT(l)));
c.state.shift.store(false, Ordering::Relaxed);
})
.rect;
rect
}
/// Get input locale.
fn input_locale() -> String {
let english = AppConfig::english_keyboard();
if english {
"en".to_string()
} else {
AppConfig::locale().unwrap_or("en".to_string())
}
}
/// Check last keyboard input event.
pub fn consume_event() -> Option<KeyboardEvent> {
let empty = {
let r_state = WINDOW_STATE.read();
r_state.last_event.is_none()
};
if !empty {
let mut w_state = WINDOW_STATE.write();
let event = w_state.last_event.as_ref().clone().unwrap();
w_state.last_event = Arc::new(None);
return Some(event);
}
None
}
/// Emulate stop of Shift key press.
pub fn unshift() {
let r_state = WINDOW_STATE.read();
r_state.shift.store(false, Ordering::Relaxed);
}
/// Reset keyboard window state.
pub fn reset_window_state() {
let mut w_state = WINDOW_STATE.write();
w_state.layout = Arc::new(KeyboardLayout::TEXT);
// *w_state = KeyboardState::default();
}
}
+22
View File
@@ -0,0 +1,22 @@
// Copyright 2025 The Grim Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
mod types;
pub use types::*;
mod edit;
pub use edit::*;
mod keyboard;
pub use keyboard::*;
+53
View File
@@ -0,0 +1,53 @@
// Copyright 2025 The Grim Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
/// Software keyboard input type.
#[derive(Clone, PartialOrd, PartialEq)]
pub enum KeyboardLayout {
TEXT,
SYMBOLS,
NUMBERS,
}
/// Software keyboard input event.
#[derive(Clone)]
pub enum KeyboardEvent {
TEXT(String),
CLEAR,
ENTER,
}
/// Software keyboard Window State.
#[derive(Clone)]
pub struct KeyboardState {
/// Last input event.
pub last_event: Arc<Option<KeyboardEvent>>,
/// Current layout.
pub layout: Arc<KeyboardLayout>,
/// Flag to enter uppercase symbol first.
pub shift: Arc<AtomicBool>,
}
impl Default for KeyboardState {
fn default() -> Self {
Self {
last_event: Arc::new(None),
layout: Arc::new(KeyboardLayout::TEXT),
shift: Arc::new(AtomicBool::new(false)),
}
}
}
+10 -4
View File
@@ -27,7 +27,7 @@ mod content;
pub use content::*;
pub mod network;
pub mod settings;
pub mod wallets;
mod camera;
@@ -36,8 +36,14 @@ pub use camera::*;
mod qr;
pub use qr::*;
mod file;
pub use file::*;
mod file_pick;
pub use file_pick::*;
mod pull_to_refresh;
pub use pull_to_refresh::*;
pub use pull_to_refresh::*;
mod scan;
pub use scan::*;
mod input;
pub use input::*;
Regular → Executable
+328 -276
View File
@@ -12,326 +12,378 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use lazy_static::lazy_static;
use std::sync::Arc;
use parking_lot::RwLock;
use std::sync::atomic::{AtomicBool, Ordering};
use egui::{Align2, Rect, RichText, Rounding, Stroke, Vec2};
use egui::epaint::{RectShape, Shadow};
use egui::os::OperatingSystem;
use egui::{Align2, Color32, CornerRadius, RichText, Stroke, StrokeKind, UiBuilder, Vec2};
use lazy_static::lazy_static;
use parking_lot::RwLock;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use crate::gui::Colors;
use crate::gui::views::{Content, View};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::types::{ModalPosition, ModalState};
use crate::gui::views::{Content, View};
lazy_static! {
/// Showing [`Modal`] state to be accessible from different ui parts.
static ref MODAL_STATE: Arc<RwLock<ModalState>> = Arc::new(RwLock::new(ModalState::default()));
/// Showing [`Modal`] state to be accessible from different ui parts.
static ref MODAL_STATE: Arc<RwLock<ModalState>> = Arc::new(RwLock::new(ModalState::default()));
}
/// Stores data to draw modal [`egui::Window`] at ui.
/// Modal [`egui::Window`] container.
#[derive(Clone)]
pub struct Modal {
/// Identifier for modal.
pub(crate) id: &'static str,
/// Position on the screen.
position: ModalPosition,
/// To check if it can be closed.
closeable: Arc<AtomicBool>,
/// Title text
title: Option<String>
/// Identifier for modal.
pub(crate) id: &'static str,
/// Position on the screen.
pub position: ModalPosition,
/// Flag to check if modal can be closed by keys.
closeable: Arc<AtomicBool>,
/// Title text.
title: Option<String>,
/// Flag to check first content render.
first_draw: Arc<AtomicBool>,
/// Background color.
fill: Option<Color32>,
}
impl Modal {
/// Margin from [`Modal`] window at top/left/right.
const DEFAULT_MARGIN: f32 = 8.0;
/// Maximum width of the content.
const DEFAULT_WIDTH: f32 = Content::SIDE_PANEL_WIDTH - (2.0 * Self::DEFAULT_MARGIN);
/// Margin from [`Modal`] window at top/left/right.
const DEFAULT_MARGIN: f32 = 8.0;
/// Maximum width of the content.
const DEFAULT_WIDTH: f32 = Content::SIDE_PANEL_WIDTH - (2.0 * Self::DEFAULT_MARGIN);
/// Modal content [`egui::Window`] id.
pub const WINDOW_ID: &'static str = "modal_window";
/// Create closeable [`Modal`] with center position.
pub fn new(id: &'static str) -> Self {
Self {
id,
position: ModalPosition::Center,
closeable: Arc::new(AtomicBool::new(true)),
title: None
}
}
/// Create closeable [`Modal`] with center position.
pub fn new(id: &'static str) -> Self {
Self {
id,
position: ModalPosition::Center,
closeable: Arc::new(AtomicBool::new(true)),
title: None,
first_draw: Arc::new(AtomicBool::new(true)),
fill: None,
}
}
/// Setup position of [`Modal`] on the screen.
pub fn position(mut self, position: ModalPosition) -> Self {
self.position = position;
self
}
/// Setup position of [`Modal`] on the screen.
pub fn position(mut self, position: ModalPosition) -> Self {
self.position = position;
self
}
/// Mark [`Modal`] closed.
pub fn close(&self) {
let mut w_nav = MODAL_STATE.write();
w_nav.modal = None;
}
/// Change [`Modal`] position on the screen.
pub fn change_position(position: ModalPosition) {
let mut w_state = MODAL_STATE.write();
w_state.modal.as_mut().unwrap().position = position;
}
/// Setup possibility to close [`Modal`].
pub fn closeable(self, closeable: bool) -> Self {
self.closeable.store(closeable, Ordering::Relaxed);
self
}
/// Close [`Modal`] by clearing its state.
pub fn close() {
let mut w_nav = MODAL_STATE.write();
w_nav.modal = None;
}
/// Disable possibility to close [`Modal`].
pub fn disable_closing(&self) {
self.closeable.store(false, Ordering::Relaxed);
}
/// Setup possibility to close [`Modal`].
pub fn closeable(self, closeable: bool) -> Self {
self.closeable.store(closeable, Ordering::Relaxed);
self
}
/// Enable possibility to close [`Modal`].
pub fn enable_closing(&self) {
self.closeable.store(true, Ordering::Relaxed);
}
/// Disable possibility to close [`Modal`].
pub fn disable_closing(&self) {
self.closeable.store(false, Ordering::Relaxed);
}
/// Check if [`Modal`] is closeable.
pub fn is_closeable(&self) -> bool {
self.closeable.load(Ordering::Relaxed)
}
/// Enable possibility to close [`Modal`].
pub fn enable_closing(&self) {
self.closeable.store(true, Ordering::Relaxed);
}
/// Set title text on [`Modal`] creation.
pub fn title(mut self, title: String) -> Self {
self.title = Some(title.to_uppercase());
self
}
/// Check if [`Modal`] is closeable.
pub fn is_closeable(&self) -> bool {
self.closeable.load(Ordering::Relaxed)
}
/// Set [`Modal`] instance into state to show at ui.
pub fn show(self) {
let mut w_nav = MODAL_STATE.write();
w_nav.modal = Some(self);
}
/// Set title text on [`Modal`] creation.
pub fn title(mut self, title: impl Into<String>) -> Self {
self.title = Some(title.into().to_uppercase());
self
}
/// Remove [`Modal`] from [`ModalState`] if it's showing and can be closed.
/// Return `false` if Modal existed in [`ModalState`] before call.
pub fn on_back() -> bool {
let mut w_state = MODAL_STATE.write();
/// Set [`Modal`] instance into state to show at ui.
pub fn show(self) {
let mut w_nav = MODAL_STATE.write();
self.first_draw.store(true, Ordering::Relaxed);
w_nav.modal = Some(self);
}
// If Modal is showing and closeable, remove it from state.
if w_state.modal.is_some() {
let modal = w_state.modal.as_ref().unwrap();
if modal.is_closeable() {
w_state.modal = None;
}
return false;
}
true
}
/// Remove [`Modal`] from [`ModalState`] if it's showing and can be closed.
/// Return `false` if modal existed in state before call.
pub fn on_back() -> bool {
if Self::opened().is_some() {
if Self::opened_closeable() {
Self::close();
}
return false;
}
true
}
/// Return id of opened [`Modal`].
pub fn opened() -> Option<&'static str> {
// Check if modal is showing.
{
if MODAL_STATE.read().modal.is_none() {
return None;
}
}
/// Return identifier of opened [`Modal`].
pub fn opened() -> Option<&'static str> {
// Check if modal is showing.
{
if MODAL_STATE.read().modal.is_none() {
return None;
}
}
// Get identifier of opened modal.
let r_state = MODAL_STATE.read();
let modal = r_state.modal.as_ref().unwrap();
Some(modal.id)
}
// Get identifier of opened modal.
let r_state = MODAL_STATE.read();
let modal = r_state.modal.as_ref().unwrap();
Some(modal.id)
}
/// Set title text for current opened [`Modal`].
pub fn set_title(title: String) {
// Save state.
let mut w_state = MODAL_STATE.write();
if w_state.modal.is_some() {
let mut modal = w_state.modal.clone().unwrap();
modal.title = Some(title.to_uppercase());
w_state.modal = Some(modal);
}
}
/// Check if [`Modal`] is opened and can be closed.
pub fn opened_closeable() -> bool {
// Check if modal is showing.
{
if MODAL_STATE.read().modal.is_none() {
return false;
}
}
let r_state = MODAL_STATE.read();
let modal = r_state.modal.as_ref().unwrap();
modal.closeable.load(Ordering::Relaxed)
}
/// Draw opened [`Modal`] content.
pub fn ui(ctx: &egui::Context, add_content: impl FnOnce(&mut egui::Ui, &Modal)) {
let has_modal = {
MODAL_STATE.read().modal.is_some()
};
if has_modal {
let modal = {
let r_state = MODAL_STATE.read();
r_state.modal.clone().unwrap()
};
modal.window_ui(ctx, add_content);
}
}
/// Set title text for current opened [`Modal`].
pub fn set_title(title: impl Into<String>) {
let mut w_state = MODAL_STATE.write();
if w_state.modal.is_some() {
let mut modal = w_state.modal.clone().unwrap();
modal.title = Some(title.into().to_uppercase());
w_state.modal = Some(modal);
}
}
/// Draw [`egui::Window`] with provided content.
fn window_ui(&self, ctx: &egui::Context, add_content: impl FnOnce(&mut egui::Ui, &Modal)) {
let is_fullscreen = ctx.input(|i| {
i.viewport().fullscreen.unwrap_or(false)
});
let is_mac_os = OperatingSystem::from_target_os() == OperatingSystem::Mac;
/// Check for first [`Modal`] content rendering.
pub fn first_draw() -> bool {
if Self::opened().is_none() {
return false;
}
let r_state = MODAL_STATE.read();
let modal = r_state.modal.as_ref().unwrap();
modal.first_draw.load(Ordering::Relaxed)
}
let mut rect = ctx.screen_rect();
if View::is_desktop() && !is_mac_os {
let margin = if !is_fullscreen {
Content::WINDOW_FRAME_MARGIN
} else {
0.0
};
rect = rect.shrink(margin - 0.5);
rect.min += egui::vec2(0.0, Content::WINDOW_TITLE_HEIGHT + 0.5);
rect.max.x += 0.5;
}
egui::Window::new("modal_bg_window")
.title_bar(false)
.resizable(false)
.collapsible(false)
.fixed_rect(rect)
.frame(egui::Frame {
fill: Colors::semi_transparent(),
..Default::default()
})
.show(ctx, |ui| {
ui.set_min_size(rect.size());
});
pub fn ui(
ctx: &egui::Context,
cb: &dyn PlatformCallbacks,
add_content: impl FnOnce(&mut egui::Ui, &Modal, &dyn PlatformCallbacks),
) {
let has_modal = { MODAL_STATE.read().modal.is_some() };
if has_modal {
let modal = {
let r_state = MODAL_STATE.read();
r_state.modal.clone().unwrap()
};
modal.window_ui(ctx, cb, add_content);
}
}
// Setup width of modal content.
let side_insets = View::get_left_inset() + View::get_right_inset();
let available_width = rect.width() - (side_insets + Self::DEFAULT_MARGIN);
let width = f32::min(available_width, Self::DEFAULT_WIDTH);
/// Draw [`egui::Window`] with provided content.
fn window_ui(
&self,
ctx: &egui::Context,
cb: &dyn PlatformCallbacks,
add_content: impl FnOnce(&mut egui::Ui, &Modal, &dyn PlatformCallbacks),
) {
let is_fullscreen = ctx.input(|i| i.viewport().fullscreen.unwrap_or(false));
// Show main content Window at given position.
let (content_align, content_offset) = self.modal_position(is_fullscreen);
let layer_id = egui::Window::new(format!("modal_window_{}", self.id))
.title_bar(false)
.resizable(false)
.collapsible(false)
.min_width(width)
.default_width(width)
.anchor(content_align, content_offset)
.frame(egui::Frame {
shadow: Shadow {
offset: Default::default(),
blur: 30.0,
spread: 3.0,
color: egui::Color32::from_black_alpha(32),
},
rounding: Rounding::same(8.0),
fill: Colors::fill(),
..Default::default()
})
.show(ctx, |ui| {
if self.title.is_some() {
self.title_ui(ui);
}
self.content_ui(ui, add_content);
}).unwrap().response.layer_id;
// Setup background rect.
let is_win = OperatingSystem::Windows == OperatingSystem::from_target_os();
let bg_rect = if View::is_desktop() && !is_win {
let mut r = ctx.content_rect();
let is_mac = OperatingSystem::Mac == OperatingSystem::from_target_os();
if !is_mac && !is_fullscreen {
r = r.shrink(Content::WINDOW_FRAME_MARGIN - 1.0);
}
r.min.y += Content::WINDOW_TITLE_HEIGHT;
r
} else {
ctx.content_rect()
};
// Always show main content Window above background Window.
ctx.move_to_top(layer_id);
// Draw modal background.
egui::Window::new("modal_bg_window")
.title_bar(false)
.resizable(false)
.collapsible(false)
.fixed_rect(bg_rect)
.frame(egui::Frame {
fill: Colors::semi_transparent(),
..Default::default()
})
.show(ctx, |ui| {
ui.set_min_size(bg_rect.size());
});
}
// Setup width of modal content.
let side_insets = View::get_left_inset() + View::get_right_inset();
let available_width = ctx.content_rect().width() - (side_insets + Self::DEFAULT_MARGIN);
let width = f32::min(available_width, Self::DEFAULT_WIDTH);
/// Get [`egui::Window`] position based on [`ModalPosition`].
fn modal_position(&self, is_fullscreen: bool) -> (Align2, Vec2) {
let align = match self.position {
ModalPosition::CenterTop => Align2::CENTER_TOP,
ModalPosition::Center => Align2::CENTER_CENTER
};
// Show main content window at given position.
let (content_align, content_offset) = self.modal_position();
egui::Window::new(Self::WINDOW_ID)
.title_bar(false)
.resizable(false)
.collapsible(false)
.min_width(width)
.default_width(width)
.anchor(content_align, content_offset)
.frame(egui::Frame {
shadow: Shadow {
offset: Default::default(),
blur: 30.0 as u8,
spread: 3.0 as u8,
color: egui::Color32::from_black_alpha(32),
},
corner_radius: CornerRadius::same(8.0 as u8),
..Default::default()
})
.show(ctx, |ui| {
if let Some(title) = &self.title {
title_ui(title, ui);
}
self.content_ui(ui, cb, add_content);
});
let x_align = View::get_left_inset() - View::get_right_inset();
// Setup first draw flag.
if Self::first_draw() {
let r_state = MODAL_STATE.read();
let modal = r_state.modal.as_ref().unwrap();
modal.first_draw.store(false, Ordering::Relaxed);
}
}
let is_mac_os = OperatingSystem::from_target_os() == OperatingSystem::Mac;
let extra_y = if View::is_desktop() && !is_mac_os {
Content::WINDOW_TITLE_HEIGHT + if !is_fullscreen {
Content::WINDOW_FRAME_MARGIN
} else {
0.0
}
} else {
0.0
};
let y_align = View::get_top_inset() + Self::DEFAULT_MARGIN + extra_y;
/// Get [`egui::Window`] position based on [`ModalPosition`].
fn modal_position(&self) -> (Align2, Vec2) {
let align = match self.position {
ModalPosition::CenterTop => Align2::CENTER_TOP,
ModalPosition::Center => Align2::CENTER_CENTER,
};
let offset = match self.position {
ModalPosition::CenterTop => Vec2::new(x_align, y_align),
ModalPosition::Center => Vec2::new(x_align, 0.0)
};
(align, offset)
}
let x_align = View::get_left_inset() - View::get_right_inset();
let is_mac = OperatingSystem::Mac == OperatingSystem::from_target_os();
let is_win = OperatingSystem::Windows == OperatingSystem::from_target_os();
let extra_y = if View::is_desktop() && !is_win {
Content::WINDOW_TITLE_HEIGHT
+ if !is_mac {
Content::WINDOW_FRAME_MARGIN
} else {
0.0
}
} else {
0.0
};
let y_align = View::get_top_inset() + Self::DEFAULT_MARGIN / 2.0 + extra_y;
/// Draw provided content.
fn content_ui(&self, ui: &mut egui::Ui, add_content: impl FnOnce(&mut egui::Ui, &Modal)) {
let mut rect = ui.available_rect_before_wrap();
rect.min += egui::emath::vec2(6.0, 0.0);
rect.max -= egui::emath::vec2(6.0, 0.0);
let offset = match self.position {
ModalPosition::CenterTop => Vec2::new(x_align, y_align),
ModalPosition::Center => Vec2::new(x_align, 0.0),
};
(align, offset)
}
// Create background shape.
let rounding = if self.title.is_some() {
Rounding {
nw: 0.0,
ne: 0.0,
sw: 8.0,
se: 8.0,
}
} else {
Rounding::same(8.0)
};
let mut bg_shape = RectShape {
rect,
rounding,
fill: Colors::fill(),
stroke: Stroke::NONE,
blur_width: 0.0,
fill_texture_id: Default::default(),
uv: Rect::ZERO
};
let bg_idx = ui.painter().add(bg_shape);
/// Set custom background color.
pub fn set_background_color(&self, color: Color32) {
let mut w_state = MODAL_STATE.write();
w_state.modal.as_mut().unwrap().fill = Some(color);
}
// Draw main content.
let mut content_rect = ui.allocate_ui_at_rect(rect, |ui| {
(add_content)(ui, self);
}).response.rect;
/// Draw provided content.
fn content_ui(
&self,
ui: &mut egui::Ui,
cb: &dyn PlatformCallbacks,
add_content: impl FnOnce(&mut egui::Ui, &Modal, &dyn PlatformCallbacks),
) {
let mut rect = ui.available_rect_before_wrap();
// Setup background shape to be painted behind main content.
content_rect.min -= egui::emath::vec2(6.0, 0.0);
content_rect.max += egui::emath::vec2(6.0, 0.0);
bg_shape.rect = content_rect;
ui.painter().set(bg_idx, bg_shape);
}
// Create background shape.
let mut bg_shape = RectShape::new(
rect,
if self.title.is_none() {
CornerRadius::same(8.0 as u8)
} else {
CornerRadius {
nw: 0.0 as u8,
ne: 0.0 as u8,
sw: 8.0 as u8,
se: 8.0 as u8,
}
},
self.fill.unwrap_or(Colors::fill_lite()),
Stroke::NONE,
StrokeKind::Outside,
);
let bg_idx = ui.painter().add(bg_shape.clone());
/// Draw title content.
fn title_ui(&self, ui: &mut egui::Ui) {
let rect = ui.available_rect_before_wrap();
rect.min += egui::emath::vec2(6.0, 0.0);
rect.max -= egui::emath::vec2(6.0, 0.0);
let resp = ui
.scope_builder(UiBuilder::new().max_rect(rect), |ui| {
(add_content)(ui, self, cb);
})
.response;
// Create background shape.
let mut bg_shape = RectShape {
rect,
rounding: Rounding {
nw: 8.0,
ne: 8.0,
sw: 0.0,
se: 0.0,
},
fill: Colors::yellow(),
stroke: Stroke::NONE,
blur_width: 0.0,
fill_texture_id: Default::default(),
uv: Rect::ZERO
};
let bg_idx = ui.painter().add(bg_shape);
// Setup background size.
let bg_rect = {
let mut r = resp.rect.clone();
r.min -= egui::emath::vec2(6.0, 0.0);
r.max += egui::emath::vec2(6.0, 0.0);
r
};
bg_shape.rect = bg_rect;
ui.painter().set(bg_idx, bg_shape);
}
}
// Draw title content.
let title_resp = ui.allocate_ui_at_rect(rect, |ui| {
ui.vertical_centered_justified(|ui| {
ui.add_space(Self::DEFAULT_MARGIN + 1.0);
ui.label(RichText::new(self.title.as_ref().unwrap())
.size(19.0)
.color(Colors::title(true))
);
ui.add_space(Self::DEFAULT_MARGIN);
// Draw line below title.
View::horizontal_line(ui, Colors::item_stroke());
});
}).response;
/// Draw title content.
fn title_ui(title: &String, ui: &mut egui::Ui) {
let rect = ui.available_rect_before_wrap();
// Setup background shape to be painted behind title content.
bg_shape.rect = title_resp.rect;
ui.painter().set(bg_idx, bg_shape);
}
}
// Create background shape.
let mut bg_shape = RectShape::new(
rect,
CornerRadius {
nw: 8.0 as u8,
ne: 8.0 as u8,
sw: 0.0 as u8,
se: 0.0 as u8,
},
Colors::yellow(),
Stroke::NONE,
StrokeKind::Outside,
);
let bg_idx = ui.painter().add(bg_shape.clone());
// Draw title content.
let resp = ui
.vertical_centered(|ui| {
ui.add_space(Modal::DEFAULT_MARGIN + 2.0);
ui.label(RichText::new(title).size(19.0).color(Colors::title(true)));
ui.add_space(Modal::DEFAULT_MARGIN + 1.0);
// Draw line below title.
View::horizontal_line(ui, Colors::item_stroke());
})
.response;
// Setup background size.
bg_shape.rect = resp.rect;
ui.painter().set(bg_idx, bg_shape);
}
+352 -202
View File
@@ -12,235 +12,385 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use egui::{Align, Layout, RichText, Rounding};
use eframe::epaint::RectShape;
use egui::{
Align, Color32, CornerRadius, CursorIcon, Layout, RichText, Sense, StrokeKind, UiBuilder,
};
use crate::AppConfig;
use crate::gui::Colors;
use crate::gui::icons::{CARET_RIGHT, CHECK_CIRCLE, COMPUTER_TOWER, DOTS_THREE_CIRCLE, GLOBE_SIMPLE, PENCIL, PLUS_CIRCLE, POWER, TRASH, X_CIRCLE};
use crate::gui::icons::{
CHECK_CIRCLE, COMPUTER_TOWER, DOTS_THREE_CIRCLE, GLOBE_SIMPLE, PLUS_CIRCLE, POWER, QR_CODE,
TRASH, WARNING_CIRCLE, X_CIRCLE,
};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Modal, View};
use crate::gui::views::network::modals::ExternalConnectionModal;
use crate::gui::views::network::NodeSetup;
use crate::gui::views::types::{ModalContainer, ModalPosition};
use crate::gui::views::network::modals::{ExternalConnectionModal, ShareConnectionContent};
use crate::gui::views::network::types::ShareConnection;
use crate::gui::views::types::{ContentContainer, ModalPosition};
use crate::gui::views::{Modal, View};
use crate::node::{Node, NodeConfig};
use crate::wallet::{ConnectionsConfig, ExternalConnection};
/// Network connections content.
pub struct ConnectionsContent {
/// External connection [`Modal`] content.
ext_conn_modal: ExternalConnectionModal,
/// Flag to check connections state on first draw.
first_draw: bool,
/// [`Modal`] identifiers allowed at this ui container.
modal_ids: Vec<&'static str>
/// External connection [`Modal`] content.
ext_conn_modal_content: ExternalConnectionModal,
/// [`Modal`] content to share connection with QR code.
share_conn_modal_content: Option<ShareConnectionContent>,
}
impl Default for ConnectionsContent {
fn default() -> Self {
ExternalConnection::check_ext_conn_availability(None);
Self {
ext_conn_modal: ExternalConnectionModal::new(None),
modal_ids: vec![
ExternalConnectionModal::NETWORK_ID
],
}
}
fn default() -> Self {
Self {
first_draw: true,
ext_conn_modal_content: ExternalConnectionModal::new(None),
share_conn_modal_content: None,
}
}
}
impl ModalContainer for ConnectionsContent {
fn modal_ids(&self) -> &Vec<&'static str> {
&self.modal_ids
}
/// Identifier for [`Modal`] to share connection.
const SHARE_CONN_QR_MODAL: &'static str = "share_conn_qr_modal";
fn modal_ui(&mut self,
ui: &mut egui::Ui,
modal: &Modal,
cb: &dyn PlatformCallbacks) {
match modal.id {
ExternalConnectionModal::NETWORK_ID => {
self.ext_conn_modal.ui(ui, cb, modal, |_| {});
},
_ => {}
}
}
impl ContentContainer for ConnectionsContent {
fn modal_ids(&self) -> Vec<&'static str> {
vec![ExternalConnectionModal::NETWORK_ID, SHARE_CONN_QR_MODAL]
}
fn modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
match modal.id {
ExternalConnectionModal::NETWORK_ID => {
self.ext_conn_modal_content.ui(ui, cb, modal, |_| {});
}
SHARE_CONN_QR_MODAL => {
if let Some(c) = self.share_conn_modal_content.as_mut() {
c.ui(ui, modal, cb);
}
}
_ => {}
}
}
fn container_ui(&mut self, ui: &mut egui::Ui, _: &dyn PlatformCallbacks) {
// Check connections state on first draw.
if self.first_draw {
ExternalConnection::check(None, ui.ctx());
self.first_draw = false;
}
ui.add_space(2.0);
// Show network type selection.
let saved_chain_type = AppConfig::chain_type();
NodeSetup::chain_type_ui(ui);
ui.add_space(6.0);
// Check connections availability.
if saved_chain_type != AppConfig::chain_type() {
ExternalConnection::check(None, ui.ctx());
}
// Show integrated node info content.
Self::integrated_node_item_ui(
ui,
Colors::fill_lite(),
(true, || {
AppConfig::toggle_show_connections_network_panel();
}),
|ui| {
let r = View::item_rounding(0, 1, true);
View::item_button(ui, r, QR_CODE, None, || {
let (api_address, api_port) = NodeConfig::get_api_address();
if let Ok(c) = ShareConnectionContent::new(ShareConnection {
url: format!("http://{}:{}", api_address, api_port),
username: "grin".to_string(),
secret: NodeConfig::get_api_secret(true).unwrap_or("".to_string()),
}) {
self.share_conn_modal_content = Some(c);
// Show QR code to share integrated node connection.
Modal::new(SHARE_CONN_QR_MODAL)
.position(ModalPosition::Center)
.title(t!("network.node"))
.show();
}
});
true
},
);
// Show external connections.
ui.add_space(8.0);
ui.label(
RichText::new(t!("wallets.ext_conn"))
.size(16.0)
.color(Colors::gray()),
);
ui.add_space(6.0);
// Show button to add new external node connection.
let add_node_text = format!("{} {}", PLUS_CIRCLE, t!("wallets.add_node"));
View::button(ui, add_node_text, Colors::white_or_black(false), || {
self.show_add_ext_conn_modal(None);
});
ui.add_space(4.0);
let ext_conn_list = ConnectionsConfig::ext_conn_list();
let len = ext_conn_list.len();
if len != 0 {
ui.add_space(8.0);
for (i, c) in ext_conn_list.iter().enumerate() {
ui.horizontal_wrapped(|ui| {
let mut show_qr_content: Option<ShareConnectionContent> = None;
// Draw external connection list item.
let bg = Colors::fill_lite();
Self::ext_conn_item_ui(
ui,
bg,
c,
i,
len,
(true, || {
self.show_add_ext_conn_modal(Some(c.clone()));
}),
|ui| {
// Draw button to delete connection.
let r = View::item_rounding(i, len, true);
View::item_button(ui, r, TRASH, Some(Colors::inactive_text()), || {
ConnectionsConfig::remove_ext_conn(c.id);
});
// Draw button to share connection
let r = CornerRadius::default();
View::item_button(ui, r, QR_CODE, None, || {
if let Ok(c) = ShareConnectionContent::new(ShareConnection {
url: c.url.clone(),
username: "grin".to_string(),
secret: c.secret.clone().unwrap_or("".to_string()),
}) {
show_qr_content = Some(c);
}
});
},
);
if let Some(c) = show_qr_content {
self.share_conn_modal_content = Some(c);
// Show QR code to share external connection.
Modal::new(SHARE_CONN_QR_MODAL)
.position(ModalPosition::Center)
.title(t!("wallets.ext_conn").replace(":", ""))
.show();
}
});
}
}
}
}
impl ConnectionsContent {
/// Draw connections content.
pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
self.current_modal_ui(ui, cb);
/// Draw integrated node connection item content.
pub fn integrated_node_item_ui(
ui: &mut egui::Ui,
bg: Color32,
on_click: (bool, impl FnOnce()),
custom_button: impl FnOnce(&mut egui::Ui) -> bool,
) {
// Draw round background.
let mut rect = ui.available_rect_before_wrap();
rect.set_height(78.0);
let r = View::item_rounding(0, 1, false);
let mut bg_shape = RectShape::new(rect, r, bg, View::item_stroke(), StrokeKind::Outside);
let bg_idx = ui.painter().add(bg_shape.clone());
ui.add_space(2.0);
let res = ui
.scope_builder(
UiBuilder::new()
.sense(Sense::click())
.layout(Layout::right_to_left(Align::Center))
.max_rect(rect),
|ui| {
// Draw custom button.
let extra_button = custom_button(ui);
// Show network type selection.
let saved_chain_type = AppConfig::chain_type();
NodeSetup::chain_type_ui(ui);
ui.add_space(6.0);
// Draw buttons to start/stop node.
if Node::get_error().is_none() {
let rounding = if extra_button {
CornerRadius::default()
} else {
View::item_rounding(0, 1, true)
};
if !Node::is_running() {
View::item_button(ui, rounding, POWER, Some(Colors::green()), || {
Node::start();
});
} else if !Node::is_starting()
&& !Node::is_stopping()
&& !Node::is_restarting()
{
View::item_button(ui, rounding, POWER, Some(Colors::red()), || {
Node::stop(false);
});
}
}
// Check connections availability.
if saved_chain_type != AppConfig::chain_type() {
ExternalConnection::check_ext_conn_availability(None);
}
let layout_size = ui.available_size();
ui.allocate_ui_with_layout(
layout_size,
Layout::left_to_right(Align::Center),
|ui| {
ui.add_space(6.0);
ui.vertical(|ui| {
ui.add_space(3.0);
ui.with_layout(Layout::left_to_right(Align::Min), |ui| {
ui.add_space(1.0);
ui.label(
RichText::new(t!("network.node"))
.size(18.0)
.color(Colors::title(false)),
);
});
// Show integrated node info content.
Self::integrated_node_item_ui(ui, |ui| {
// Draw button to show integrated node info.
View::item_button(ui, View::item_rounding(0, 1, true), CARET_RIGHT, None, || {
AppConfig::toggle_show_connections_network_panel();
});
});
// Setup node status text.
let has_error = Node::get_error().is_some();
let status_icon = if has_error {
WARNING_CIRCLE
} else if !Node::is_running() {
X_CIRCLE
} else if Node::not_syncing() {
CHECK_CIRCLE
} else {
DOTS_THREE_CIRCLE
};
let status_text = format!(
"{} {}",
status_icon,
if has_error {
t!("error").into()
} else {
Node::get_sync_status_text()
}
);
View::ellipsize_text(ui, status_text, 15.0, Colors::text(false));
ui.add_space(1.0);
// Show external connections.
ui.add_space(8.0);
ui.label(RichText::new(t!("wallets.ext_conn")).size(16.0).color(Colors::gray()));
ui.add_space(6.0);
// Setup node API address text.
let (api_address, api_port) = NodeConfig::get_api_address();
let address_text = format!(
"{} http://{}:{}",
COMPUTER_TOWER, api_address, api_port
);
ui.label(
RichText::new(address_text).size(15.0).color(Colors::gray()),
);
})
},
);
},
)
.response;
let (clickable, on_click) = on_click;
let clicked = res.clicked() || res.long_touched();
// Setup background and cursor.
if clickable && res.hovered() {
res.on_hover_cursor(CursorIcon::PointingHand);
bg_shape.fill = Colors::fill();
}
ui.painter().set(bg_idx, bg_shape);
// Handle clicks on layout.
if clicked && clickable {
on_click();
}
}
// Show button to add new external node connection.
let add_node_text = format!("{} {}", PLUS_CIRCLE, t!("wallets.add_node"));
View::button(ui, add_node_text, Colors::white_or_black(false), || {
self.show_add_ext_conn_modal(None, cb);
});
/// Draw external connection item content.
pub fn ext_conn_item_ui(
ui: &mut egui::Ui,
bg: Color32,
conn: &ExternalConnection,
index: usize,
len: usize,
on_click: (bool, impl FnOnce()),
custom_button: impl FnOnce(&mut egui::Ui),
) {
// Draw round background.
let mut rect = ui.available_rect_before_wrap();
rect.set_height(52.0);
let r = View::item_rounding(index, len, false);
let mut bg_shape = RectShape::new(rect, r, bg, View::item_stroke(), StrokeKind::Outside);
let bg_idx = ui.painter().add(bg_shape.clone());
ui.add_space(4.0);
let res = ui
.scope_builder(
UiBuilder::new()
.sense(Sense::click())
.layout(Layout::right_to_left(Align::Center))
.max_rect(rect),
|ui| {
// Draw custom button.
custom_button(ui);
let ext_conn_list = ConnectionsConfig::ext_conn_list();
if !ext_conn_list.is_empty() {
ui.add_space(8.0);
for (index, conn) in ext_conn_list.iter().enumerate() {
ui.horizontal_wrapped(|ui| {
// Draw connection list item.
let len = ext_conn_list.len();
Self::ext_conn_item_ui(ui, conn, index, len, |ui| {
// Draw buttons for non-default connections.
if conn.url != ExternalConnection::DEFAULT_MAIN_URL {
let button_rounding = View::item_rounding(index, len, true);
View::item_button(ui, button_rounding, TRASH, None, || {
ConnectionsConfig::remove_ext_conn(conn.id);
});
View::item_button(ui, Rounding::default(), PENCIL, None, || {
self.show_add_ext_conn_modal(Some(conn.clone()), cb);
});
}
});
});
}
}
}
let layout_size = ui.available_size();
ui.allocate_ui_with_layout(
layout_size,
Layout::left_to_right(Align::Center),
|ui| {
ui.add_space(6.0);
ui.vertical(|ui| {
// Draw connections URL.
ui.add_space(4.0);
let conn_text = format!("{} {}", GLOBE_SIMPLE, conn.url);
View::ellipsize_text(ui, conn_text, 15.0, Colors::title(false));
ui.add_space(1.0);
/// Draw integrated node connection item content.
pub fn integrated_node_item_ui(ui: &mut egui::Ui, custom_button: impl FnOnce(&mut egui::Ui)) {
// Draw round background.
let mut rect = ui.available_rect_before_wrap();
rect.set_height(78.0);
let rounding = View::item_rounding(0, 1, false);
ui.painter().rect(rect, rounding, Colors::fill(), View::item_stroke());
// Setup connection status text.
let status_text = if let Some(available) = conn.available {
if available {
format!("{} {}", CHECK_CIRCLE, t!("network.available"))
} else {
format!("{} {}", X_CIRCLE, t!("network.not_available"))
}
} else {
format!(
"{} {}",
DOTS_THREE_CIRCLE,
t!("network.availability_check")
)
};
ui.label(
RichText::new(status_text).size(15.0).color(Colors::gray()),
);
ui.add_space(3.0);
});
},
);
},
)
.response;
let (clickable, on_click) = on_click;
let clicked = res.clicked() || res.long_touched();
// Setup background and cursor.
if clickable && res.hovered() {
res.on_hover_cursor(CursorIcon::PointingHand);
bg_shape.fill = Colors::fill();
}
ui.painter().set(bg_idx, bg_shape);
// Handle clicks on layout.
if clicked && clickable {
on_click();
}
}
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
// Draw custom button.
custom_button(ui);
if !Node::is_running() {
// Draw button to start integrated node.
View::item_button(ui, Rounding::default(), POWER, Some(Colors::green()), || {
Node::start();
});
} else if !Node::is_starting() && !Node::is_stopping() && !Node::is_restarting() {
// Draw button to stop integrated node.
View::item_button(ui, Rounding::default(), POWER, Some(Colors::red()), || {
Node::stop(false);
});
}
let layout_size = ui.available_size();
ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| {
ui.add_space(6.0);
ui.vertical(|ui| {
ui.add_space(3.0);
ui.with_layout(Layout::left_to_right(Align::Min), |ui| {
ui.add_space(1.0);
ui.label(RichText::new(t!("network.node"))
.size(18.0)
.color(Colors::title(false)));
});
// Setup node status text.
let status_icon = if !Node::is_running() {
X_CIRCLE
} else if Node::not_syncing() {
CHECK_CIRCLE
} else {
DOTS_THREE_CIRCLE
};
let status_text = format!("{} {}", status_icon, Node::get_sync_status_text());
ui.label(RichText::new(status_text).size(15.0).color(Colors::text(false)));
ui.add_space(1.0);
// Setup node API address text.
let api_address = NodeConfig::get_api_address();
let address_text = format!("{} http://{}", COMPUTER_TOWER, api_address);
ui.label(RichText::new(address_text).size(15.0).color(Colors::gray()));
})
});
});
}
/// Draw external connection item content.
pub fn ext_conn_item_ui(ui: &mut egui::Ui,
conn: &ExternalConnection,
index: usize,
len: usize,
buttons_ui: impl FnOnce(&mut egui::Ui)) {
// Setup layout size.
let mut rect = ui.available_rect_before_wrap();
rect.set_height(52.0);
// Draw round background.
let bg_rect = rect.clone();
let item_rounding = View::item_rounding(index, len, false);
ui.painter().rect(bg_rect, item_rounding, Colors::fill(), View::item_stroke());
ui.vertical(|ui| {
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
// Draw provided buttons.
buttons_ui(ui);
let layout_size = ui.available_size();
ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| {
ui.add_space(6.0);
ui.vertical(|ui| {
// Draw connections URL.
ui.add_space(4.0);
let conn_text = format!("{} {}", GLOBE_SIMPLE, conn.url);
View::ellipsize_text(ui, conn_text, 15.0, Colors::title(false));
ui.add_space(1.0);
// Setup connection status text.
let status_text = if let Some(available) = conn.available {
if available {
format!("{} {}", CHECK_CIRCLE, t!("network.available"))
} else {
format!("{} {}", X_CIRCLE, t!("network.not_available"))
}
} else {
format!("{} {}", DOTS_THREE_CIRCLE, t!("network.availability_check"))
};
ui.label(RichText::new(status_text).size(15.0).color(Colors::gray()));
ui.add_space(3.0);
});
});
});
});
}
/// Show [`Modal`] to add external connection.
pub fn show_add_ext_conn_modal(&mut self,
conn: Option<ExternalConnection>,
cb: &dyn PlatformCallbacks) {
self.ext_conn_modal = ExternalConnectionModal::new(conn);
// Show modal.
Modal::new(ExternalConnectionModal::NETWORK_ID)
.position(ModalPosition::CenterTop)
.title(t!("wallets.add_node"))
.show();
cb.show_keyboard();
}
}
/// Show [`Modal`] to add external connection.
pub fn show_add_ext_conn_modal(&mut self, conn: Option<ExternalConnection>) {
self.ext_conn_modal_content = ExternalConnectionModal::new(conn);
// Show modal.
Modal::new(ExternalConnectionModal::NETWORK_ID)
.position(ModalPosition::CenterTop)
.title(t!("wallets.add_node"))
.show();
}
}
+404 -295
View File
@@ -12,326 +12,435 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use std::time::Duration;
use egui::{Id, Margin, RichText, ScrollArea};
use egui::scroll_area::ScrollBarVisibility;
use egui::{Id, Margin, RichText, ScrollArea};
use crate::AppConfig;
use crate::gui::Colors;
use crate::gui::icons::{ARROWS_COUNTER_CLOCKWISE, BRIEFCASE, DATABASE, DOTS_THREE_OUTLINE_VERTICAL, FACTORY, FADERS, GAUGE, POWER};
use crate::gui::icons::{
ARROW_LEFT, ARROWS_COUNTER_CLOCKWISE, BRIEFCASE, DATABASE, DOTS_THREE_OUTLINE_VERTICAL,
FACTORY, FADERS, GAUGE, GEAR, GLOBE, POWER,
};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::network::types::{NodeTab, NodeTabType};
use crate::gui::views::network::{
ConnectionsContent, NetworkMetrics, NetworkMining, NetworkNode, NetworkSettings,
};
use crate::gui::views::settings::SettingsContent;
use crate::gui::views::types::{ContentContainer, LinePosition, TitleContentType, TitleType};
use crate::gui::views::{Content, TitlePanel, View};
use crate::gui::views::network::{ConnectionsContent, NetworkMetrics, NetworkMining, NetworkNode, NetworkSettings};
use crate::gui::views::network::types::{NetworkTab, NetworkTabType};
use crate::gui::views::types::{TitleContentType, TitleType};
use crate::node::{Node, NodeError};
use crate::wallet::ExternalConnection;
use crate::node::{Node, NodeConfig, NodeError};
/// Network content.
pub struct NetworkContent {
/// Current integrated node tab content.
node_tab_content: Box<dyn NetworkTab>,
/// Connections content.
connections: ConnectionsContent,
/// Current integrated node tab content.
node_tab_content: Box<dyn NodeTab>,
/// Connections content.
connections: ConnectionsContent,
/// Application settings content.
settings_content: Option<SettingsContent>,
}
impl Default for NetworkContent {
fn default() -> Self {
Self {
node_tab_content: Box::new(NetworkNode::default()),
connections: ConnectionsContent::default(),
}
}
fn default() -> Self {
Self {
node_tab_content: Box::new(NetworkNode::default()),
connections: ConnectionsContent::default(),
settings_content: None,
}
}
}
impl NetworkContent {
pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
let show_connections = AppConfig::show_connections_network_panel();
let dual_panel = Content::is_dual_panel_mode(ui);
pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
let show_settings = self.showing_settings();
let show_connections = AppConfig::show_connections_network_panel();
let dual_panel = Content::is_dual_panel_mode(ui.ctx());
// Show title panel.
self.title_ui(ui, show_connections);
// Show title panel.
self.title_ui(ui, dual_panel, show_connections);
// Show integrated node tabs content.
if !show_connections {
egui::TopBottomPanel::bottom("node_tabs_content")
.min_height(0.5)
.resizable(false)
.frame(egui::Frame {
inner_margin: Margin {
left: View::get_left_inset() + View::TAB_ITEMS_PADDING,
right: View::far_right_inset_margin(ui) + View::TAB_ITEMS_PADDING,
top: View::TAB_ITEMS_PADDING,
bottom: View::get_bottom_inset() + View::TAB_ITEMS_PADDING,
},
fill: Colors::fill(),
..Default::default()
})
.show_inside(ui, |ui| {
ui.vertical_centered(|ui| {
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
self.tabs_ui(ui);
});
});
});
}
// Show integrated node tabs content.
if !show_connections && !show_settings {
let side_padding = View::TAB_ITEMS_PADDING + if View::is_desktop() { 0.0 } else { 4.0 };
let tabs_margin = Margin {
left: (View::get_left_inset() + side_padding) as i8,
right: (View::far_right_inset_margin(ui) + side_padding) as i8,
top: View::TAB_ITEMS_PADDING as i8,
bottom: (View::get_bottom_inset() + View::TAB_ITEMS_PADDING) as i8,
};
egui::TopBottomPanel::bottom("network_tabs_content")
.min_height(0.5)
.resizable(false)
.frame(egui::Frame {
inner_margin: tabs_margin,
fill: Colors::fill(),
..Default::default()
})
.show_inside(ui, |ui| {
let rect = ui.available_rect_before_wrap();
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
self.tabs_ui(ui);
});
// Draw content divider line.
let r = {
let mut r = rect.clone();
r.min.x -= tabs_margin.left as f32;
r.min.y -= tabs_margin.top as f32;
r.max.x += tabs_margin.right as f32;
r
};
View::line(ui, LinePosition::TOP, &r, Colors::stroke());
});
}
// Show current node tab content.
egui::SidePanel::right("node_tab_content")
.resizable(false)
.exact_width(ui.available_width())
.frame(egui::Frame {
..Default::default()
})
.show_animated_inside(ui, !show_connections, |ui| {
egui::CentralPanel::default()
.frame(egui::Frame {
fill: Colors::white_or_black(false),
stroke: View::item_stroke(),
inner_margin: Margin {
left: View::get_left_inset() + 4.0,
right: View::far_right_inset_margin(ui) + 4.0,
top: 3.0,
bottom: 4.0,
},
..Default::default()
})
.show_inside(ui, |ui| {
self.node_tab_content.ui(ui, cb);
});
});
// Show settings or integrated node content.
egui::SidePanel::right("network_side_content")
.resizable(false)
.exact_width(ui.available_width())
.frame(egui::Frame {
..Default::default()
})
.show_animated_inside(ui, show_settings || !show_connections, |ui| {
egui::CentralPanel::default()
.frame(egui::Frame {
inner_margin: Margin {
left: (View::get_left_inset() + View::content_padding()) as i8,
right: (View::far_right_inset_margin(ui) + View::content_padding())
as i8,
top: 3.0 as i8,
bottom: 4.0 as i8,
},
..Default::default()
})
.show_inside(ui, |ui| {
let rect = ui.available_rect_before_wrap();
if let Some(c) = &mut self.settings_content {
ScrollArea::vertical()
.id_salt("app_settings_network")
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.auto_shrink([false; 2])
.show(ui, |ui| {
ui.add_space(1.0);
ui.vertical_centered(|ui| {
// Show application settings content.
View::max_width_ui(
ui,
Content::SIDE_PANEL_WIDTH * 1.3,
|ui| {
c.ui(ui, cb);
},
);
});
});
} else if self.node_tab_content.get_type() != NodeTabType::Settings {
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
let node_err = Node::get_error();
if let Some(err) = node_err {
node_error_ui(ui, err);
} else if !Node::is_running() {
disabled_node_ui(ui);
} else if Node::get_stats().is_none()
|| Node::is_restarting() || Node::is_stopping()
{
NetworkContent::loading_ui(ui, None::<String>);
} else {
self.node_tab_content.tab_ui(ui, cb);
}
});
} else {
self.node_tab_content.tab_ui(ui, cb);
}
// Draw content divider line.
let r = {
let mut r = rect.clone();
r.min.y -= 3.0;
r.max.x += View::content_padding();
r.max.y += View::content_padding();
r
};
if dual_panel {
View::line(ui, LinePosition::RIGHT, &r, Colors::item_stroke());
}
});
});
// Show connections content.
egui::CentralPanel::default()
.frame(egui::Frame {
stroke: View::item_stroke(),
inner_margin: Margin {
left: if show_connections {
View::get_left_inset() + 4.0
} else {
0.0
},
right: if show_connections {
View::far_right_inset_margin(ui) + 4.0
} else {
0.0
},
top: 3.0,
bottom: if View::is_desktop() && show_connections {
6.0
} else {
4.0
},
},
fill: Colors::button(),
..Default::default()
})
.show_inside(ui, |ui| {
ScrollArea::vertical()
.id_source("connections_content")
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.auto_shrink([false; 2])
.show(ui, |ui| {
ui.add_space(1.0);
ui.vertical_centered(|ui| {
let max_width = if !dual_panel {
Content::SIDE_PANEL_WIDTH * 1.3
} else {
ui.available_width()
};
View::max_width_ui(ui, max_width, |ui| {
self.connections.ui(ui, cb);
});
});
});
});
// Show connections content.
egui::CentralPanel::default()
.frame(egui::Frame {
inner_margin: Margin {
left: if show_connections {
View::get_left_inset() + View::content_padding()
} else {
0.0
} as i8,
right: if show_connections {
View::far_right_inset_margin(ui) + View::content_padding()
} else {
0.0
} as i8,
top: 3.0 as i8,
bottom: 0.0 as i8,
},
..Default::default()
})
.show_inside(ui, |ui| {
let rect = ui.available_rect_before_wrap();
ScrollArea::vertical()
.id_salt("connections_scroll")
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.auto_shrink([false; 2])
.show(ui, |ui| {
ui.add_space(1.0);
ui.vertical_centered(|ui| {
let max_width = if !dual_panel {
Content::SIDE_PANEL_WIDTH * 1.3
} else {
ui.available_width()
};
View::max_width_ui(ui, max_width, |ui| {
self.connections.ui(ui, cb);
});
});
ui.add_space(32.0);
});
// Draw content divider line.
let r = {
let mut r = rect.clone();
r.min.y -= 3.0;
r.max.x += View::content_padding();
r.max.y += View::content_padding() + View::get_bottom_inset();
r
};
if show_connections && dual_panel {
View::line(ui, LinePosition::RIGHT, &r, Colors::item_stroke());
}
});
// Redraw after delay.
if Node::is_running() {
ui.ctx().request_repaint_after(Node::STATS_UPDATE_DELAY);
} else if show_connections {
ui.ctx().request_repaint_after(Duration::from_millis(1000));
}
}
// Redraw after delay if node is running at non-dual-panel mode.
if ((!dual_panel && Content::is_network_panel_open()) || dual_panel) && Node::is_running() {
ui.ctx().request_repaint_after(Node::STATS_UPDATE_DELAY);
}
}
/// Draw tab buttons in the bottom of the screen.
fn tabs_ui(&mut self, ui: &mut egui::Ui) {
ui.vertical_centered(|ui| {
// Setup spacing between tabs.
ui.style_mut().spacing.item_spacing = egui::vec2(View::TAB_ITEMS_PADDING, 0.0);
// Setup vertical padding inside tab button.
ui.style_mut().spacing.button_padding = egui::vec2(0.0, 4.0);
/// Navigate back, return `true` if action was not consumed.
pub fn on_back(&mut self) -> bool {
if self.showing_settings() {
// Close settings.
self.settings_content = None;
return false;
}
true
}
// Draw tab buttons.
let current_type = self.node_tab_content.get_type();
ui.columns(4, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::tab_button(ui, DATABASE, current_type == NetworkTabType::Node, || {
self.node_tab_content = Box::new(NetworkNode::default());
});
});
columns[1].vertical_centered_justified(|ui| {
View::tab_button(ui, GAUGE, current_type == NetworkTabType::Metrics, || {
self.node_tab_content = Box::new(NetworkMetrics::default());
});
});
columns[2].vertical_centered_justified(|ui| {
View::tab_button(ui, FACTORY, current_type == NetworkTabType::Mining, || {
self.node_tab_content = Box::new(NetworkMining::default());
});
});
columns[3].vertical_centered_justified(|ui| {
View::tab_button(ui, FADERS, current_type == NetworkTabType::Settings, || {
self.node_tab_content = Box::new(NetworkSettings::default());
});
});
});
});
}
/// Check if application settings content is showing.
pub fn showing_settings(&self) -> bool {
self.settings_content.is_some()
}
/// Draw title content.
fn title_ui(&mut self, ui: &mut egui::Ui, show_connections: bool) {
// Setup values for title panel.
let title_text = self.node_tab_content.get_type().title().to_uppercase();
let subtitle_text = Node::get_sync_status_text();
let not_syncing = Node::not_syncing();
let title_content = if !show_connections {
TitleContentType::WithSubTitle(title_text, subtitle_text, !not_syncing)
} else {
TitleContentType::Title(t!("network.connections").to_uppercase())
};
/// Draw tab buttons at bottom of the screen.
fn tabs_ui(&mut self, ui: &mut egui::Ui) {
ui.vertical_centered(|ui| {
// Setup spacing between tabs.
ui.style_mut().spacing.item_spacing = egui::vec2(View::TAB_ITEMS_PADDING, 0.0);
// Draw title panel.
TitlePanel::new(Id::from("network_title_panel")).ui(TitleType::Single(title_content), |ui| {
if !show_connections {
View::title_button_big(ui, DOTS_THREE_OUTLINE_VERTICAL, |_| {
AppConfig::toggle_show_connections_network_panel();
if AppConfig::show_connections_network_panel() {
ExternalConnection::check_ext_conn_availability(None);
}
});
}
}, |ui| {
if !Content::is_dual_panel_mode(ui) {
View::title_button_big(ui, BRIEFCASE, |_| {
Content::toggle_network_panel();
});
}
}, ui);
}
// Draw tab buttons.
let current_type = self.node_tab_content.get_type();
ui.columns(4, |columns| {
columns[0].vertical_centered_justified(|ui| {
let active = Some(current_type == NodeTabType::Info);
View::tab_button(ui, DATABASE, None, active, |_| {
self.node_tab_content = Box::new(NetworkNode::default());
});
});
columns[1].vertical_centered_justified(|ui| {
let active = Some(current_type == NodeTabType::Metrics);
View::tab_button(ui, GAUGE, None, active, |_| {
self.node_tab_content = Box::new(NetworkMetrics::default());
});
});
columns[2].vertical_centered_justified(|ui| {
let active = Some(current_type == NodeTabType::Mining);
View::tab_button(ui, FACTORY, None, active, |_| {
self.node_tab_content = Box::new(NetworkMining::default());
});
});
columns[3].vertical_centered_justified(|ui| {
let active = Some(current_type == NodeTabType::Settings);
View::tab_button(ui, FADERS, None, active, |_| {
self.node_tab_content = Box::new(NetworkSettings::default());
});
});
});
});
}
/// Content to draw when node is disabled.
pub fn disabled_node_ui(ui: &mut egui::Ui) {
View::center_content(ui, 156.0, |ui| {
let text = t!("network.disabled_server", "dots" => DOTS_THREE_OUTLINE_VERTICAL);
ui.label(RichText::new(text)
.size(16.0)
.color(Colors::inactive_text())
);
ui.add_space(8.0);
View::action_button(ui, format!("{} {}", POWER, t!("network.enable_node")), || {
Node::start();
});
ui.add_space(2.0);
Self::autorun_node_ui(ui);
});
}
/// Draw title content.
fn title_ui(&mut self, ui: &mut egui::Ui, dual_panel: bool, show_connections: bool) {
let show_settings = self.showing_settings();
/// Content to draw on loading.
pub fn loading_ui(ui: &mut egui::Ui, text: Option<String>) {
match text {
None => {
ui.centered_and_justified(|ui| {
View::big_loading_spinner(ui);
});
}
Some(t) => {
View::center_content(ui, 162.0, |ui| {
View::big_loading_spinner(ui);
ui.add_space(18.0);
ui.label(RichText::new(t)
.size(16.0)
.color(Colors::inactive_text())
);
});
}
}
}
// Setup values for title panel.
let title_text = self.node_tab_content.get_type().title();
let subtitle_text = Node::get_sync_status_text().into();
let not_syncing = Node::not_syncing() && !Node::data_dir_changing();
let title_content = if show_settings {
TitleContentType::Title(t!("settings").into())
} else if !show_connections {
TitleContentType::WithSubTitle(title_text, subtitle_text, !not_syncing)
} else {
TitleContentType::Title(t!("network.connections").into())
};
/// Draw checkbox to run integrated node on application launch.
pub fn autorun_node_ui(ui: &mut egui::Ui) {
let autostart = AppConfig::autostart_node();
View::checkbox(ui, autostart, t!("network.autorun"), || {
AppConfig::toggle_node_autostart();
});
}
// Draw title panel.
TitlePanel::new(Id::from("network_title_panel")).ui(
TitleType::Single(title_content),
|ui| {
if show_settings {
View::title_button_big(ui, ARROW_LEFT, |_| {
self.settings_content = None;
});
} else if !show_connections {
View::title_button_big(ui, GLOBE, |_| {
AppConfig::toggle_show_connections_network_panel();
});
} else if !dual_panel {
View::title_button_big(ui, GEAR, |_| {
self.settings_content = Some(SettingsContent::default());
});
}
},
|ui| {
if !dual_panel && !show_settings {
View::title_button_big(ui, BRIEFCASE, |_| {
Content::toggle_network_panel();
});
}
},
ui,
);
}
/// Draw integrated node error content.
pub fn node_error_ui(ui: &mut egui::Ui, e: NodeError) {
match e {
NodeError::Storage => {
View::center_content(ui, 156.0, |ui| {
ui.label(RichText::new(t!("network_node.error_clean"))
.size(16.0)
.color(Colors::red())
);
ui.add_space(8.0);
let btn_txt = format!("{} {}",
ARROWS_COUNTER_CLOCKWISE,
t!("network_node.resync"));
View::action_button(ui, btn_txt, || {
Node::clean_up_data();
Node::start();
});
ui.add_space(2.0);
});
return;
}
NodeError::P2P | NodeError::API => {
let msg_type = match e {
NodeError::API => "API",
_ => "P2P"
};
View::center_content(ui, 106.0, |ui| {
let text = t!(
"network_node.error_p2p_api",
"p2p_api" => msg_type,
"settings" => FADERS
);
ui.label(RichText::new(text)
.size(16.0)
.color(Colors::red())
);
ui.add_space(2.0);
});
return;
}
NodeError::Configuration => {
View::center_content(ui, 106.0, |ui| {
ui.label(RichText::new(t!("network_node.error_config", "settings" => FADERS))
.size(16.0)
.color(Colors::red())
);
ui.add_space(2.0);
});
}
NodeError::Unknown => {
View::center_content(ui, 156.0, |ui| {
ui.label(RichText::new(t!("network_node.error_unknown", "settings" => FADERS))
.size(16.0)
.color(Colors::red())
);
ui.add_space(8.0);
let btn_txt = format!("{} {}",
ARROWS_COUNTER_CLOCKWISE,
t!("network_node.resync"));
View::action_button(ui, btn_txt, || {
Node::clean_up_data();
Node::start();
});
ui.add_space(2.0);
});
}
}
}
}
/// Content to draw on loading.
pub fn loading_ui(ui: &mut egui::Ui, text: Option<impl Into<String>>) {
match text {
None => {
ui.centered_and_justified(|ui| {
View::big_loading_spinner(ui);
});
}
Some(t) => {
View::center_content(ui, 162.0, |ui| {
View::big_loading_spinner(ui);
ui.add_space(18.0);
ui.label(RichText::new(t).size(16.0).color(Colors::inactive_text()));
});
}
}
}
/// Draw checkbox to run integrated node on application launch.
pub fn autorun_node_ui(ui: &mut egui::Ui) {
let autostart = AppConfig::autostart_node();
View::checkbox(ui, autostart, t!("network.autorun"), || {
AppConfig::toggle_node_autostart();
});
}
}
/// Content to draw when node is disabled.
fn disabled_node_ui(ui: &mut egui::Ui) {
View::center_content(ui, 156.0, |ui| {
let text = t!("network.disabled_server", "dots" => DOTS_THREE_OUTLINE_VERTICAL);
ui.label(
RichText::new(text)
.size(16.0)
.color(Colors::inactive_text()),
);
ui.add_space(8.0);
View::action_button(
ui,
format!("{} {}", POWER, t!("network.enable_node")),
|| {
Node::start();
},
);
ui.add_space(2.0);
NetworkContent::autorun_node_ui(ui);
});
}
/// Draw integrated node error content.
pub fn node_error_ui(ui: &mut egui::Ui, e: NodeError) {
match e {
NodeError::Storage => {
View::center_content(ui, 156.0, |ui| {
ui.label(
RichText::new(t!("network_node.error_clean"))
.size(16.0)
.color(Colors::red()),
);
ui.add_space(8.0);
let btn_txt = format!("{} {}", ARROWS_COUNTER_CLOCKWISE, t!("network_node.resync"));
View::action_button(ui, btn_txt, || {
Node::clean_up_data();
Node::start();
});
ui.add_space(2.0);
});
return;
}
NodeError::P2P | NodeError::API => {
let msg_type = match e {
NodeError::API => "API",
_ => "P2P",
};
View::center_content(ui, 106.0, |ui| {
let text = t!(
"network_node.error_p2p_api",
"p2p_api" => msg_type,
"settings" => FADERS
);
ui.label(RichText::new(text).size(16.0).color(Colors::red()));
ui.add_space(2.0);
});
return;
}
NodeError::Configuration => {
View::center_content(ui, 106.0, |ui| {
ui.label(
RichText::new(t!("network_node.error_config", "settings" => FADERS))
.size(16.0)
.color(Colors::red()),
);
ui.add_space(8.0);
let btn_txt = format!(
"{} {}",
ARROWS_COUNTER_CLOCKWISE,
t!("network_settings.reset")
);
View::action_button(ui, btn_txt, || {
NodeConfig::reset_to_default();
Node::start();
});
ui.add_space(2.0);
});
}
NodeError::Unknown => {
View::center_content(ui, 156.0, |ui| {
ui.label(
RichText::new(t!("network_node.error_unknown", "settings" => FADERS))
.size(16.0)
.color(Colors::red()),
);
ui.add_space(8.0);
let btn_txt = format!("{} {}", ARROWS_COUNTER_CLOCKWISE, t!("network_node.resync"));
View::action_button(ui, btn_txt, || {
Node::clean_up_data();
Node::start();
});
ui.add_space(2.0);
});
}
}
}
+164 -171
View File
@@ -12,204 +12,197 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use egui::{RichText, Rounding, ScrollArea, vec2};
use egui::scroll_area::ScrollBarVisibility;
use egui::{CornerRadius, RichText, ScrollArea, StrokeKind, vec2};
use grin_core::consensus::{DAY_HEIGHT, GRIN_BASE, HOUR_SEC, REWARD};
use grin_servers::{DiffBlock, ServerStats};
use crate::gui::Colors;
use crate::gui::icons::{AT, COINS, CUBE_TRANSPARENT, HOURGLASS_LOW, HOURGLASS_MEDIUM, TIMER};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Content, View};
use crate::gui::views::network::NetworkContent;
use crate::gui::views::network::types::{NetworkTab, NetworkTabType};
use crate::gui::views::network::types::{NodeTab, NodeTabType};
use crate::gui::views::{Content, View};
use crate::node::Node;
/// Chain metrics tab content.
#[derive(Default)]
pub struct NetworkMetrics;
const BLOCK_REWARD: f64 = 60.0;
// 1 year is calculated as 365 days and 6 hours (31557600).
const YEARLY_SUPPLY: f64 = ((60 * 60 * 24 * 365) + 6 * 60 * 60) as f64;
const BLOCK_REWARD: u64 = REWARD / GRIN_BASE;
// 1 year as 365 days and 6 hours (31557600).
const YEARLY_SUPPLY: u64 = (BLOCK_REWARD * DAY_HEIGHT * 365) + 6 * HOUR_SEC;
impl NetworkTab for NetworkMetrics {
fn get_type(&self) -> NetworkTabType {
NetworkTabType::Metrics
}
impl NodeTab for NetworkMetrics {
fn get_type(&self) -> NodeTabType {
NodeTabType::Metrics
}
fn ui(&mut self, ui: &mut egui::Ui, _: &dyn PlatformCallbacks) {
// Show an error content when available.
let node_err = Node::get_error();
if node_err.is_some() {
NetworkContent::node_error_ui(ui, node_err.unwrap());
return;
}
// Show message to enable node when it's not running.
if !Node::is_running() {
NetworkContent::disabled_node_ui(ui);
return;
}
// Show loading spinner when node is stopping.
if Node::is_stopping() {
NetworkContent::loading_ui(ui, None);
return;
}
// Show message when metrics are not available.
let server_stats = Node::get_stats();
if server_stats.is_none() || Node::is_restarting()
|| server_stats.as_ref().unwrap().diff_stats.height == 0 {
NetworkContent::loading_ui(ui, Some(t!("network_metrics.loading")));
return;
}
ui.add_space(1.0);
ui.vertical_centered(|ui| {
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
let stats = server_stats.as_ref().unwrap();
// Show emission and difficulty info.
info_ui(ui, stats);
// Show difficulty adjustment window blocks.
blocks_ui(ui, stats);
});
});
}
fn tab_ui(&mut self, ui: &mut egui::Ui, _: &dyn PlatformCallbacks) {
let server_stats = Node::get_stats();
let stats = server_stats.as_ref().unwrap();
if stats.diff_stats.height == 0 {
NetworkContent::loading_ui(ui, Some(t!("network_metrics.loading")));
return;
}
ui.add_space(1.0);
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
// Show emission and difficulty info.
info_ui(ui, stats);
// Show difficulty adjustment window blocks.
blocks_ui(ui, stats);
});
}
}
const BLOCK_ITEM_HEIGHT: f32 = 78.0;
/// Draw emission and difficulty info.
fn info_ui(ui: &mut egui::Ui, stats: &ServerStats) {
// Show emission info.
View::sub_title(ui, format!("{} {}", COINS, t!("network_metrics.emission")));
ui.columns(3, |columns| {
let supply = stats.header_stats.height as f64 * BLOCK_REWARD;
let rate = (YEARLY_SUPPLY * 100.0) / supply;
// Show emission info.
View::sub_title(ui, format!("{} {}", COINS, t!("network_metrics.emission")));
ui.columns(3, |columns| {
let supply = stats.header_stats.height * BLOCK_REWARD;
let rate = (YEARLY_SUPPLY * 100) / supply;
columns[0].vertical_centered(|ui| {
View::rounded_box(ui,
format!("{}", BLOCK_REWARD),
t!("network_metrics.reward"),
[true, false, true, false]);
});
columns[1].vertical_centered(|ui| {
View::rounded_box(ui,
format!("{:.2}%", rate),
t!("network_metrics.inflation"),
[false, false, false, false]);
});
columns[2].vertical_centered(|ui| {
View::rounded_box(ui,
supply.to_string(),
t!("network_metrics.supply"),
[false, true, false, true]);
});
});
ui.add_space(5.0);
columns[0].vertical_centered(|ui| {
View::label_box(
ui,
format!("{}", BLOCK_REWARD),
t!("network_metrics.reward"),
[true, false, true, false],
);
});
columns[1].vertical_centered(|ui| {
View::label_box(
ui,
format!("{:.2}%", rate),
t!("network_metrics.inflation"),
[false, false, false, false],
);
});
columns[2].vertical_centered(|ui| {
View::label_box(
ui,
supply.to_string(),
t!("network_metrics.supply"),
[false, true, false, true],
);
});
});
ui.add_space(5.0);
// Show difficulty adjustment window info.
let difficulty_title = t!(
"network_metrics.difficulty_window",
"size" => stats.diff_stats.window_size
);
View::sub_title(ui, format!("{} {}", HOURGLASS_MEDIUM, difficulty_title));
ui.columns(3, |columns| {
columns[0].vertical_centered(|ui| {
View::rounded_box(ui,
stats.diff_stats.height.to_string(),
t!("network_node.height"),
[true, false, true, false]);
});
columns[1].vertical_centered(|ui| {
View::rounded_box(ui,
format!("{}s", stats.diff_stats.average_block_time),
t!("network_metrics.block_time"),
[false, false, false, false]);
});
columns[2].vertical_centered(|ui| {
View::rounded_box(ui,
stats.diff_stats.average_difficulty.to_string(),
t!("network_node.difficulty"),
[false, true, false, true]);
});
});
// Show difficulty adjustment window info.
let difficulty_title = t!(
"network_metrics.difficulty_window",
"size" => stats.diff_stats.window_size
);
View::sub_title(ui, format!("{} {}", HOURGLASS_MEDIUM, difficulty_title));
ui.columns(3, |columns| {
columns[0].vertical_centered(|ui| {
View::label_box(
ui,
stats.diff_stats.height.to_string(),
t!("network_node.height"),
[true, false, true, false],
);
});
columns[1].vertical_centered(|ui| {
View::label_box(
ui,
format!("{}s", stats.diff_stats.average_block_time),
t!("network_metrics.block_time"),
[false, false, false, false],
);
});
columns[2].vertical_centered(|ui| {
View::label_box(
ui,
stats.diff_stats.average_difficulty.to_string(),
t!("network_node.difficulty"),
[false, true, false, true],
);
});
});
}
const BLOCK_ITEM_HEIGHT: f32 = 77.0;
/// Draw difficulty adjustment window blocks content.
fn blocks_ui(ui: &mut egui::Ui, stats: &ServerStats) {
let blocks_size = stats.diff_stats.last_blocks.len();
ui.add_space(4.0);
ScrollArea::vertical()
.id_source("difficulty_scroll")
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.auto_shrink([false; 2])
.stick_to_bottom(true)
.show_rows(
ui,
BLOCK_ITEM_HEIGHT,
blocks_size,
|ui, row_range| {
for index in row_range {
// Add space before the first item.
if index == 0 {
ui.add_space(4.0);
}
let db = stats.diff_stats.last_blocks.get(index).unwrap();
block_item_ui(ui, db, View::item_rounding(index, blocks_size, false));
}
},
);
let blocks_size = stats.diff_stats.last_blocks.len();
ui.add_space(4.0);
ScrollArea::vertical()
.id_salt("mining_difficulty_scroll")
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.auto_shrink([false; 2])
.stick_to_bottom(true)
.show_rows(ui, BLOCK_ITEM_HEIGHT, blocks_size, |ui, row_range| {
ui.add_space(4.0);
for index in row_range {
let db = stats.diff_stats.last_blocks.get(index).unwrap();
block_item_ui(ui, db, View::item_rounding(index, blocks_size, false));
}
});
}
/// Draw block difficulty item.
fn block_item_ui(ui: &mut egui::Ui, db: &DiffBlock, rounding: Rounding) {
let mut rect = ui.available_rect_before_wrap();
rect.set_height(BLOCK_ITEM_HEIGHT);
ui.allocate_ui_at_rect(rect, |ui| {
ui.horizontal(|ui| {
ui.add_space(6.0);
ui.vertical(|ui| {
ui.add_space(3.0);
fn block_item_ui(ui: &mut egui::Ui, db: &DiffBlock, rounding: CornerRadius) {
let mut rect = ui.available_rect_before_wrap();
rect.set_height(BLOCK_ITEM_HEIGHT);
ui.allocate_ui(rect.size(), |ui| {
ui.horizontal(|ui| {
ui.add_space(6.0);
ui.vertical(|ui| {
ui.add_space(4.0);
// Draw round background.
rect.min += vec2(8.0, 0.0);
rect.max -= vec2(8.0, 0.0);
ui.painter().rect(rect, rounding, Colors::white_or_black(false), View::item_stroke());
// Draw round background.
rect.min += vec2(8.0, 0.0);
rect.max -= vec2(8.0, 0.0);
ui.painter().rect(
rect,
rounding,
Colors::fill(),
View::item_stroke(),
StrokeKind::Outside,
);
// Draw block hash.
ui.horizontal(|ui| {
ui.add_space(7.0);
ui.label(RichText::new(db.block_hash.to_string())
.color(Colors::white_or_black(true))
.size(17.0));
});
// Draw block difficulty and height.
ui.horizontal(|ui| {
ui.add_space(6.0);
let diff_text = format!("{} {} {} {}",
CUBE_TRANSPARENT,
db.difficulty,
AT,
db.block_height);
ui.label(RichText::new(diff_text).color(Colors::title(false)).size(16.0));
});
// Draw block date.
ui.horizontal(|ui| {
ui.add_space(6.0);
let block_time = View::format_time(db.time as i64);
ui.label(RichText::new(format!("{} {}s {} {}",
TIMER,
db.duration,
HOURGLASS_LOW,
block_time))
.color(Colors::gray())
.size(16.0));
});
ui.add_space(3.0);
});
ui.add_space(6.0);
});
});
}
// Draw block hash.
ui.horizontal(|ui| {
ui.add_space(8.0);
ui.label(
RichText::new(db.block_hash.to_string())
.color(Colors::white_or_black(true))
.size(17.0),
);
});
// Draw block difficulty and height.
ui.horizontal(|ui| {
ui.add_space(7.0);
let diff_text = format!(
"{} {} {} {}",
CUBE_TRANSPARENT, db.difficulty, AT, db.block_height
);
ui.label(
RichText::new(diff_text)
.color(Colors::title(false))
.size(15.0),
);
});
// Draw block date.
ui.horizontal(|ui| {
ui.add_space(7.0);
let block_time = View::format_time(db.time as i64);
ui.label(
RichText::new(format!(
"{} {}s {} {}",
TIMER, db.duration, HOURGLASS_LOW, block_time
))
.color(Colors::gray())
.size(15.0),
);
});
ui.add_space(3.0);
});
ui.add_space(6.0);
});
});
}
+250 -248
View File
@@ -12,290 +12,292 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use egui::{RichText, Rounding, ScrollArea};
use egui::scroll_area::ScrollBarVisibility;
use egui::{CornerRadius, RichText, ScrollArea, StrokeKind};
use grin_chain::SyncStatus;
use grin_servers::WorkerStats;
use crate::gui::Colors;
use crate::gui::icons::{BARBELL, CLOCK_AFTERNOON, CPU, CUBE, FADERS, FOLDER_DASHED, FOLDER_SIMPLE_MINUS, FOLDER_SIMPLE_PLUS, HARD_DRIVES, PLUGS, PLUGS_CONNECTED, POLYGON};
use crate::gui::icons::{
BARBELL, CLOCK_AFTERNOON, CPU, CUBE, FADERS, FOLDER_DASHED, FOLDER_SIMPLE_MINUS,
FOLDER_SIMPLE_PLUS, HARD_DRIVES, PLUGS, PLUGS_CONNECTED, POLYGON,
};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Content, View};
use crate::gui::views::network::NetworkContent;
use crate::gui::views::network::setup::StratumSetup;
use crate::gui::views::network::types::{NetworkTab, NetworkTabType};
use crate::gui::views::network::types::{NodeTab, NodeTabType};
use crate::gui::views::types::ContentContainer;
use crate::gui::views::{Content, View};
use crate::node::{Node, NodeConfig};
use crate::wallet::WalletConfig;
/// Mining tab content.
pub struct NetworkMining {
/// Stratum server setup content.
stratum_server_setup: StratumSetup,
/// Wallet name for rewards.
wallet_name: String,
/// Stratum server setup content.
stratum_server_setup: StratumSetup,
}
impl Default for NetworkMining {
fn default() -> Self {
let wallet_name = if let Some(id) = NodeConfig::get_stratum_wallet_id() {
WalletConfig::name_by_id(id).unwrap_or("-".to_string())
} else {
"-".to_string()
};
Self {
stratum_server_setup: StratumSetup::default(),
wallet_name,
}
}
fn default() -> Self {
Self {
stratum_server_setup: StratumSetup::default(),
}
}
}
impl NetworkTab for NetworkMining {
fn get_type(&self) -> NetworkTabType {
NetworkTabType::Mining
}
impl NodeTab for NetworkMining {
fn get_type(&self) -> NodeTabType {
NodeTabType::Mining
}
fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
// Show an error content when available.
let node_err = Node::get_error();
if node_err.is_some() {
NetworkContent::node_error_ui(ui, node_err.unwrap());
return;
}
fn tab_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
if Node::is_stratum_starting() || Node::get_sync_status().unwrap() != SyncStatus::NoSync {
NetworkContent::loading_ui(ui, Some(t!("network_mining.loading")));
return;
}
// Show message to enable node when it's not running.
if !Node::is_running() {
NetworkContent::disabled_node_ui(ui);
return;
}
// Show stratum server setup when mining server is not running.
let stratum_stats = Node::get_stratum_stats();
if !stratum_stats.is_running {
ScrollArea::vertical()
.id_salt("stratum_setup_scroll")
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.auto_shrink([false; 2])
.show(ui, |ui| {
ui.add_space(1.0);
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
self.stratum_server_setup.ui(ui, cb);
});
});
return;
}
// Show loading spinner when node is stopping or stratum server is starting.
if Node::is_stopping() || Node::is_stratum_starting() {
NetworkContent::loading_ui(ui, None);
return;
}
ui.add_space(1.0);
// Show message when mining is not available.
let server_stats = Node::get_stats();
if server_stats.is_none() || Node::is_restarting()
|| Node::get_sync_status().unwrap() != SyncStatus::NoSync {
NetworkContent::loading_ui(ui, Some(t!("network_mining.loading")));
return;
}
// Show stratum mining server info.
View::sub_title(
ui,
format!("{} {}", HARD_DRIVES, t!("network_mining.server")),
);
ui.columns(2, |columns| {
columns[0].vertical_centered(|ui| {
let (stratum_addr, stratum_port) = NodeConfig::get_stratum_address();
View::label_box(
ui,
format!("{}:{}", stratum_addr, stratum_port),
t!("network_mining.address"),
[true, false, true, false],
);
});
columns[1].vertical_centered(|ui| {
View::label_box(
ui,
self.stratum_server_setup
.wallet_name
.clone()
.unwrap_or("-".to_string()),
t!("network_mining.rewards_wallet"),
[false, true, false, true],
);
});
});
ui.add_space(4.0);
// Show stratum server setup when mining server is not running.
let stratum_stats = Node::get_stratum_stats();
if !stratum_stats.is_running {
ScrollArea::vertical()
.id_source("stratum_setup_scroll")
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.auto_shrink([false; 2])
.show(ui, |ui| {
ui.add_space(1.0);
ui.vertical_centered(|ui| {
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
self.stratum_server_setup.ui(ui, cb);
});
});
});
return;
}
// Show network info.
View::sub_title(ui, format!("{} {}", POLYGON, t!("network.self")));
ui.columns(3, |columns| {
columns[0].vertical_centered(|ui| {
let difficulty = if stratum_stats.network_difficulty > 0 {
stratum_stats.network_difficulty.to_string()
} else {
"-".into()
};
View::label_box(
ui,
difficulty,
t!("network_node.difficulty"),
[true, false, true, false],
);
});
columns[1].vertical_centered(|ui| {
let block_height = if stratum_stats.block_height > 0 {
stratum_stats.block_height.to_string()
} else {
"-".into()
};
View::label_box(
ui,
block_height,
t!("network_node.header"),
[false, false, false, false],
);
});
columns[2].vertical_centered(|ui| {
let hashrate = if stratum_stats.network_hashrate > 0.0 {
format!("{:.*}", 2, stratum_stats.network_hashrate)
} else {
"-".into()
};
View::label_box(
ui,
hashrate,
t!("network_mining.hashrate", "bits" => stratum_stats.edge_bits),
[false, true, false, true],
);
});
});
ui.add_space(4.0);
ui.add_space(1.0);
// Show mining info.
View::sub_title(ui, format!("{} {}", CPU, t!("network_mining.miners")));
ui.columns(2, |columns| {
columns[0].vertical_centered(|ui| {
View::label_box(
ui,
stratum_stats.num_workers.to_string(),
t!("network_mining.devices"),
[true, false, true, false],
);
});
// Show stratum mining server info.
View::sub_title(ui, format!("{} {}", HARD_DRIVES, t!("network_mining.server")));
ui.columns(2, |columns| {
columns[0].vertical_centered(|ui| {
let (stratum_addr, stratum_port) = NodeConfig::get_stratum_address();
View::rounded_box(ui,
format!("{}:{}", stratum_addr, stratum_port),
t!("network_mining.address"),
[true, false, true, false]);
});
columns[1].vertical_centered(|ui| {
View::rounded_box(ui,
self.wallet_name.clone(),
t!("network_mining.rewards_wallet"),
[false, true, false, true]);
});
});
ui.add_space(4.0);
columns[1].vertical_centered(|ui| {
View::label_box(
ui,
stratum_stats.blocks_found.to_string(),
t!("network_mining.blocks_found"),
[false, true, false, true],
);
});
});
ui.add_space(4.0);
// Show network info.
View::sub_title(ui, format!("{} {}", POLYGON, t!("network.self")));
ui.columns(3, |columns| {
columns[0].vertical_centered(|ui| {
let difficulty = if stratum_stats.network_difficulty > 0 {
stratum_stats.network_difficulty.to_string()
} else {
"-".into()
};
View::rounded_box(ui,
difficulty,
t!("network_node.difficulty"),
[true, false, true, false]);
});
columns[1].vertical_centered(|ui| {
let block_height = if stratum_stats.block_height > 0 {
stratum_stats.block_height.to_string()
} else {
"-".into()
};
View::rounded_box(ui,
block_height,
t!("network_node.header"),
[false, false, false, false]);
});
columns[2].vertical_centered(|ui| {
let hashrate = if stratum_stats.network_hashrate > 0.0 {
format!("{:.*}", 2, stratum_stats.network_hashrate)
} else {
"-".into()
};
View::rounded_box(ui,
hashrate,
t!("network_mining.hashrate", "bits" => stratum_stats.edge_bits),
[false, true, false, true]);
});
});
ui.add_space(4.0);
// Show mining info.
View::sub_title(ui, format!("{} {}", CPU, t!("network_mining.miners")));
ui.columns(2, |columns| {
columns[0].vertical_centered(|ui| {
View::rounded_box(ui,
stratum_stats.num_workers.to_string(),
t!("network_mining.devices"),
[true, false, true, false]);
});
columns[1].vertical_centered(|ui| {
View::rounded_box(ui,
stratum_stats.blocks_found.to_string(),
t!("network_mining.blocks_found"),
[false, true, false, true]);
});
});
ui.add_space(4.0);
// Show workers stats or info text when possible.
let workers_size = stratum_stats.worker_stats.len();
if workers_size != 0 && stratum_stats.num_workers > 0 {
ui.add_space(4.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(4.0);
ScrollArea::vertical()
.id_source("stratum_workers_scroll")
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.auto_shrink([false; 2])
.show_rows(
ui,
WORKER_ITEM_HEIGHT,
workers_size,
|ui, row_range| {
for index in row_range {
// Add space before the first item.
if index == 0 {
ui.add_space(4.0);
}
let worker = stratum_stats.worker_stats.get(index).unwrap();
let item_rounding = View::item_rounding(index, workers_size, false);
worker_item_ui(ui, worker, item_rounding);
}
},
);
} else if ui.available_height() > 142.0 {
View::center_content(ui, 142.0, |ui| {
ui.label(RichText::new(t!("network_mining.info", "settings" => FADERS))
.size(16.0)
.color(Colors::inactive_text())
);
});
}
}
// Show workers stats or info text when possible.
let workers_size = stratum_stats.worker_stats.len();
if workers_size != 0 && stratum_stats.num_workers > 0 {
ui.add_space(4.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(4.0);
ScrollArea::vertical()
.id_salt("stratum_workers_scroll")
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.auto_shrink([false; 2])
.show_rows(ui, WORKER_ITEM_HEIGHT, workers_size, |ui, row_range| {
for index in row_range {
// Add space before the first item.
if index == 0 {
ui.add_space(4.0);
}
let worker = stratum_stats.worker_stats.get(index).unwrap();
let item_rounding = View::item_rounding(index, workers_size, false);
worker_item_ui(ui, worker, item_rounding);
}
});
} else if ui.available_height() > 142.0 {
View::center_content(ui, 142.0, |ui| {
ui.label(
RichText::new(t!("network_mining.info", "settings" => FADERS))
.size(16.0)
.color(Colors::inactive_text()),
);
});
}
}
}
/// Height of Stratum server worker list item.
const WORKER_ITEM_HEIGHT: f32 = 76.0;
/// Draw worker statistics item.
fn worker_item_ui(ui: &mut egui::Ui, ws: &WorkerStats, rounding: Rounding) {
ui.horizontal_wrapped(|ui| {
ui.vertical_centered_justified(|ui| {
// Draw round background.
let mut rect = ui.available_rect_before_wrap();
rect.set_height(WORKER_ITEM_HEIGHT);
ui.painter().rect(rect, rounding, Colors::white_or_black(false), View::item_stroke());
fn worker_item_ui(ui: &mut egui::Ui, ws: &WorkerStats, rounding: CornerRadius) {
ui.horizontal_wrapped(|ui| {
ui.vertical_centered_justified(|ui| {
// Draw round background.
let mut rect = ui.available_rect_before_wrap();
rect.set_height(WORKER_ITEM_HEIGHT);
ui.painter().rect(
rect,
rounding,
Colors::white_or_black(false),
View::item_stroke(),
StrokeKind::Outside,
);
ui.add_space(2.0);
ui.horizontal(|ui| {
ui.add_space(5.0);
ui.add_space(2.0);
ui.horizontal(|ui| {
ui.add_space(5.0);
// Draw worker connection status.
let (status_text, status_icon, status_color) = match ws.is_connected {
true => (
t!("network_mining.connected"),
PLUGS_CONNECTED,
Colors::white_or_black(true)
),
false => (t!("network_mining.disconnected"), PLUGS, Colors::inactive_text())
};
let status_line_text = format!("{} {} {}", status_icon, ws.id, status_text);
ui.heading(RichText::new(status_line_text)
.color(status_color)
.size(17.0));
ui.add_space(2.0);
});
ui.horizontal(|ui| {
ui.add_space(6.0);
// Draw worker connection status.
let (status_text, status_icon, status_color) = match ws.is_connected {
true => (
t!("network_mining.connected"),
PLUGS_CONNECTED,
Colors::white_or_black(true),
),
false => (
t!("network_mining.disconnected"),
PLUGS,
Colors::inactive_text(),
),
};
let status_line_text = format!("{} {} {}", status_icon, ws.id, status_text);
ui.heading(
RichText::new(status_line_text)
.color(status_color)
.size(17.0),
);
ui.add_space(2.0);
});
ui.horizontal(|ui| {
ui.add_space(6.0);
// Draw difficulty.
let diff_text = format!("{} {}", BARBELL, ws.pow_difficulty);
ui.heading(RichText::new(diff_text)
.color(Colors::title(false))
.size(16.0));
ui.add_space(6.0);
// Draw difficulty.
let diff_text = format!("{} {}", BARBELL, ws.pow_difficulty);
ui.heading(
RichText::new(diff_text)
.color(Colors::title(false))
.size(16.0),
);
ui.add_space(6.0);
// Draw accepted shares.
let accepted_text = format!("{} {}", FOLDER_SIMPLE_PLUS, ws.num_accepted);
ui.heading(RichText::new(accepted_text)
.color(Colors::green())
.size(16.0));
ui.add_space(6.0);
// Draw accepted shares.
let accepted_text = format!("{} {}", FOLDER_SIMPLE_PLUS, ws.num_accepted);
ui.heading(
RichText::new(accepted_text)
.color(Colors::green())
.size(16.0),
);
ui.add_space(6.0);
// Draw rejected shares.
let rejected_text = format!("{} {}", FOLDER_SIMPLE_MINUS, ws.num_rejected);
ui.heading(RichText::new(rejected_text)
.color(Colors::red())
.size(16.0));
ui.add_space(6.0);
// Draw rejected shares.
let rejected_text = format!("{} {}", FOLDER_SIMPLE_MINUS, ws.num_rejected);
ui.heading(RichText::new(rejected_text).color(Colors::red()).size(16.0));
ui.add_space(6.0);
// Draw stale shares.
let stale_text = format!("{} {}", FOLDER_DASHED, ws.num_stale);
ui.heading(RichText::new(stale_text)
.color(Colors::gray())
.size(16.0));
ui.add_space(6.0);
// Draw stale shares.
let stale_text = format!("{} {}", FOLDER_DASHED, ws.num_stale);
ui.heading(RichText::new(stale_text).color(Colors::gray()).size(16.0));
ui.add_space(6.0);
// Draw blocks found.
let blocks_found_text = format!("{} {}", CUBE, ws.num_blocks_found);
ui.heading(RichText::new(blocks_found_text)
.color(Colors::title(false))
.size(16.0));
});
ui.horizontal(|ui| {
ui.add_space(6.0);
// Draw blocks found.
let blocks_found_text = format!("{} {}", CUBE, ws.num_blocks_found);
ui.heading(
RichText::new(blocks_found_text)
.color(Colors::title(false))
.size(16.0),
);
});
ui.horizontal(|ui| {
ui.add_space(6.0);
// Draw block time
let seen_ts = ws.last_seen.duration_since(std::time::UNIX_EPOCH).unwrap().as_secs();
let seen_time = View::format_time(seen_ts as i64);
let seen_text = format!("{} {}", CLOCK_AFTERNOON, seen_time);
ui.heading(RichText::new(seen_text)
.color(Colors::gray())
.size(16.0));
});
});
});
}
// Draw block time
let seen_ts = ws
.last_seen
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs();
let seen_time = View::format_time(seen_ts as i64);
let seen_text = format!("{} {}", CLOCK_AFTERNOON, seen_time);
ui.heading(RichText::new(seen_text).color(Colors::gray()).size(16.0));
});
});
});
}
+1 -1
View File
@@ -33,5 +33,5 @@ pub use content::*;
mod connections;
pub use connections::*;
pub mod modals;
pub mod types;
pub mod modals;
+238 -131
View File
@@ -14,153 +14,260 @@
use egui::{Id, RichText};
use url::Url;
use crate::gui::Colors;
use crate::gui::icons::SCAN;
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Modal, View};
use crate::gui::views::types::TextEditOptions;
use crate::gui::views::network::types::ShareConnection;
use crate::gui::views::{CameraContent, Modal, TextEdit, View};
use crate::wallet::{ConnectionsConfig, ExternalConnection};
/// Content to create or update external wallet connection.
pub struct ExternalConnectionModal {
/// Flag to check if [`Modal`] was just opened to focus on input field.
first_modal_launch: bool,
/// External connection URL value for [`Modal`].
ext_node_url_edit: String,
/// External connection API secret value for [`Modal`].
ext_node_secret_edit: String,
/// Flag to show URL format error at [`Modal`].
ext_node_url_error: bool,
/// Editing external connection identifier for [`Modal`].
ext_conn_id: Option<i64>,
/// Flag to check if content was just rendered.
first_draw: bool,
/// Editing external connection identifier.
id: Option<i64>,
/// External connection URL.
url_edit: String,
/// Flag to show URL format error.
url_error: bool,
// /// External connection username.
// username_edit: String,
/// External connection API secret.
secret_edit: String,
/// QR code scanner content.
scan_qr_content: Option<CameraContent>,
}
impl ExternalConnectionModal {
/// Network [`Modal`] identifier.
pub const NETWORK_ID: &'static str = "net_ext_conn_modal";
/// Wallet [`Modal`] identifier.
pub const WALLET_ID: &'static str = "wallet_ext_conn_modal";
/// Network [`Modal`] identifier.
pub const NETWORK_ID: &'static str = "net_ext_conn_modal";
/// Wallet [`Modal`] identifier.
pub const WALLET_ID: &'static str = "wallet_ext_conn_modal";
/// Create new instance from optional provided connection to update.
pub fn new(conn: Option<ExternalConnection>) -> Self {
let (ext_node_url_edit, ext_node_secret_edit, ext_conn_id) = if let Some(c) = conn {
(c.url, c.secret.unwrap_or("".to_string()), Some(c.id))
} else {
("".to_string(), "".to_string(), None)
};
Self {
first_modal_launch: true,
ext_node_url_edit,
ext_node_secret_edit,
ext_node_url_error: false,
ext_conn_id,
}
}
/// Create new instance from optional provided connection to update.
pub fn new(conn: Option<ExternalConnection>) -> Self {
let (url_edit, secret_edit, id) = if let Some(c) = conn {
// let username = c.username.unwrap_or("grin".to_string());
let secret = c.secret.unwrap_or("".to_string());
(c.url, secret, Some(c.id))
} else {
("".to_string(), "".to_string(), None)
};
Self {
first_draw: true,
url_edit,
url_error: false,
secret_edit,
id,
scan_qr_content: None,
}
}
/// Draw external connection [`Modal`] content.
pub fn ui(&mut self,
ui: &mut egui::Ui,
cb: &dyn PlatformCallbacks,
modal: &Modal,
on_save: impl Fn(ExternalConnection)) {
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("wallets.node_url"))
.size(17.0)
.color(Colors::gray()));
ui.add_space(8.0);
/// Draw external connection [`Modal`] content.
pub fn ui(
&mut self,
ui: &mut egui::Ui,
cb: &dyn PlatformCallbacks,
modal: &Modal,
on_save: impl Fn(ExternalConnection),
) {
// Show QR code scanner content.
if let Some(scan_content) = self.scan_qr_content.as_mut() {
if let Some(result) = scan_content.qr_scan_result() {
cb.stop_camera();
modal.enable_closing();
self.scan_qr_content = None;
// Parse scan result.
if let Ok(c) = serde_json::from_str::<ShareConnection>(&result.text()) {
let ext_conn = ExternalConnection::new(c.url, Some(c.username), Some(c.secret));
ConnectionsConfig::add_ext_conn(ext_conn.clone());
ExternalConnection::check(Some(ext_conn.id), ui.ctx());
Modal::close();
}
} else {
scan_content.ui(ui, cb);
}
ui.add_space(8.0);
// Draw node URL text edit.
let url_edit_id = Id::from(modal.id).with(self.ext_conn_id);
let mut url_edit_opts = TextEditOptions::new(url_edit_id).paste().no_focus();
if self.first_modal_launch {
self.first_modal_launch = false;
url_edit_opts.focus = true;
}
View::text_edit(ui, cb, &mut self.ext_node_url_edit, &mut url_edit_opts);
ui.add_space(8.0);
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
ui.label(RichText::new(t!("wallets.node_secret"))
.size(17.0)
.color(Colors::gray()));
ui.add_space(8.0);
// Show buttons to close modal or scanner.
ui.columns(2, |cols| {
cols[0].vertical_centered_justified(|ui| {
View::button(ui, t!("close"), Colors::white_or_black(false), || {
cb.stop_camera();
self.scan_qr_content = None;
Modal::close();
});
});
cols[1].vertical_centered_justified(|ui| {
View::button(ui, t!("back"), Colors::white_or_black(false), || {
cb.stop_camera();
self.scan_qr_content = None;
modal.enable_closing();
});
});
});
ui.add_space(6.0);
return;
}
// Add connection button callback.
let on_add = |ui: &mut egui::Ui, m: &mut ExternalConnectionModal| {
let url = if !m.url_edit.starts_with("http") {
format!("https://{}", m.url_edit)
} else {
m.url_edit.clone()
};
let error = Url::parse(url.trim()).is_err();
m.url_error = error;
if !error {
let username = if m.secret_edit.is_empty() {
Some("grin".to_string())
} else {
Some(m.secret_edit.clone())
};
let secret = if m.secret_edit.is_empty() {
None
} else {
Some(m.secret_edit.clone())
};
// Update or create new connection.
let mut ext_conn = ExternalConnection::new(url, username, secret);
if let Some(id) = m.id {
ext_conn.id = id;
}
ConnectionsConfig::add_ext_conn(ext_conn.clone());
ExternalConnection::check(Some(ext_conn.id), ui.ctx());
on_save(ext_conn);
// Draw node API secret text edit.
let secret_edit_id = Id::from(modal.id).with(self.ext_conn_id).with("node_secret");
let mut secret_edit_opts = TextEditOptions::new(secret_edit_id).paste().no_focus();
View::text_edit(ui, cb, &mut self.ext_node_secret_edit, &mut secret_edit_opts);
// Close modal.
m.url_edit = "".to_string();
m.secret_edit = "".to_string();
m.url_error = false;
Modal::close();
}
};
// Show error when specified URL is not valid.
if self.ext_node_url_error {
ui.add_space(12.0);
ui.label(RichText::new(t!("wallets.invalid_url"))
.size(17.0)
.color(Colors::red()));
}
ui.add_space(12.0);
});
ui.vertical_centered(|ui| {
ui.add_space(6.0);
ui.label(
RichText::new(t!("wallets.node_url"))
.size(17.0)
.color(Colors::gray()),
);
ui.add_space(8.0);
// Show modal buttons.
ui.scope(|ui| {
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
// Draw node URL text edit.
let url_edit_id = Id::from(modal.id).with(self.id).with("node_url");
let mut url_edit = TextEdit::new(url_edit_id).paste().focus(self.first_draw);
let url_edit_before = self.url_edit.clone();
url_edit.ui(ui, &mut self.url_edit, cb);
if self.url_edit != url_edit_before {
self.url_error = false;
}
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
// Close modal.
self.ext_node_url_edit = "".to_string();
self.ext_node_secret_edit = "".to_string();
self.ext_node_url_error = false;
cb.hide_keyboard();
modal.close();
});
});
columns[1].vertical_centered_justified(|ui| {
// Add connection button callback.
let mut on_add = || {
if !self.ext_node_url_edit.starts_with("http") {
self.ext_node_url_edit = format!("http://{}", self.ext_node_url_edit)
}
let error = Url::parse(self.ext_node_url_edit.as_str()).is_err();
self.ext_node_url_error = error;
if !error {
let url = self.ext_node_url_edit.to_owned();
let secret = if self.ext_node_secret_edit.is_empty() {
None
} else {
Some(self.ext_node_secret_edit.to_owned())
};
// ui.add_space(8.0);
// ui.label(RichText::new(t!("wallets.name"))
// .size(17.0)
// .color(Colors::gray()));
// ui.add_space(8.0);
//
// // Draw node username text edit (disabled by default).
// let username_edit_id = Id::from(modal.id).with(self.id).with("node_username");
// let mut username_edit = TextEdit::new(username_edit_id).focus(false).disable();
// username_edit.ui(ui, &mut self.username_edit, cb);
// Update or create new connection.
let mut ext_conn = ExternalConnection::new(url, secret);
if let Some(id) = self.ext_conn_id {
ext_conn.id = id;
}
ConnectionsConfig::add_ext_conn(ext_conn.clone());
ExternalConnection::check_ext_conn_availability(Some(ext_conn.id));
on_save(ext_conn);
ui.add_space(8.0);
ui.label(
RichText::new(t!("wallets.node_secret"))
.size(17.0)
.color(Colors::gray()),
);
ui.add_space(8.0);
// Close modal.
self.ext_node_url_edit = "".to_string();
self.ext_node_secret_edit = "".to_string();
self.ext_node_url_error = false;
cb.hide_keyboard();
modal.close();
}
};
View::on_enter_key(ui, || {
(on_add)();
});
View::button(ui, if self.ext_conn_id.is_some() {
t!("modal.save")
} else {
t!("modal.add")
}, Colors::white_or_black(false), on_add);
});
});
ui.add_space(6.0);
});
}
}
// Draw node API secret text edit.
let secret_edit_id = Id::from(modal.id).with(self.id).with("node_secret");
let mut secret_edit = TextEdit::new(secret_edit_id)
.h_center()
.password()
.paste()
.focus(false);
if url_edit.enter_pressed {
secret_edit.focus_request();
}
secret_edit.ui(ui, &mut self.secret_edit, cb);
if secret_edit.enter_pressed {
on_add(ui, self);
}
// Show error when specified URL is not valid.
if self.url_error {
ui.add_space(12.0);
ui.label(
RichText::new(t!("wallets.invalid_url"))
.size(17.0)
.color(Colors::red()),
);
}
ui.add_space(12.0);
let scan_text = format!("{} {}", SCAN, t!("scan"));
View::button(ui, scan_text, Colors::white_or_black(false), || {
modal.disable_closing();
self.scan_qr_content = Some(CameraContent::default());
cb.start_camera();
});
});
ui.add_space(8.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(8.0);
// Show modal buttons.
ui.scope(|ui| {
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(
ui,
t!("modal.cancel"),
Colors::white_or_black(false),
|| {
// Close modal.
self.url_edit = "".to_string();
self.secret_edit = "".to_string();
self.url_error = false;
Modal::close();
},
);
});
columns[1].vertical_centered_justified(|ui| {
View::button_ui(
ui,
if self.id.is_some() {
t!("modal.save")
} else {
t!("modal.add")
},
Colors::white_or_black(false),
|ui| {
(on_add)(ui, self);
},
);
});
});
ui.add_space(6.0);
});
self.first_draw = false;
}
}
+4 -1
View File
@@ -13,4 +13,7 @@
// limitations under the License.
mod ext_conn;
pub use ext_conn::*;
pub use ext_conn::*;
mod share_conn;
pub use share_conn::*;
@@ -0,0 +1,57 @@
// Copyright 2024 The Grim Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use crate::AppConfig;
use crate::gui::Colors;
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::network::types::ShareConnection;
use crate::gui::views::{Modal, QrCodeContent, View};
/// [`Modal`] content to share connection with QR code.
pub struct ShareConnectionContent {
/// QR code content.
pub qr_details_content: QrCodeContent,
}
impl ShareConnectionContent {
/// Create new content instance from connection details.
pub fn new(details: ShareConnection) -> Result<Self, serde_json::Error> {
let details = serde_json::to_string_pretty(&details)?;
let c = Self {
qr_details_content: QrCodeContent::new(details, false).hide_text().no_copy(),
};
Ok(c)
}
/// Draw QR code content.
pub fn ui(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
let dark_theme = AppConfig::dark_theme().unwrap_or(false);
// Set light theme for better scanning.
AppConfig::set_dark_theme(false);
modal.set_background_color(Colors::FILL_DEEP);
crate::setup_visuals(ui.ctx());
// Draw QR code content.
ui.add_space(6.0);
self.qr_details_content.ui(ui, cb);
ui.vertical_centered_justified(|ui| {
View::button(ui, t!("close"), Colors::white_or_black(false), || {
Modal::close();
});
});
ui.add_space(6.0);
// Set color theme back.
AppConfig::set_dark_theme(dark_theme);
crate::setup_visuals(ui.ctx());
}
}
+212 -193
View File
@@ -12,224 +12,243 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use egui::{RichText, Rounding, ScrollArea};
use egui::scroll_area::ScrollBarVisibility;
use egui::{CornerRadius, RichText, ScrollArea, StrokeKind};
use grin_servers::PeerStats;
use crate::gui::Colors;
use crate::gui::icons::{AT, CUBE, DEVICES, FLOW_ARROW, HANDSHAKE, PACKAGE, SHARE_NETWORK};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::network::types::{NodeTab, NodeTabType};
use crate::gui::views::{Content, View};
use crate::gui::views::network::NetworkContent;
use crate::gui::views::network::types::{NetworkTab, NetworkTabType};
use crate::node::{Node, NodeConfig};
/// Integrated node tab content.
#[derive(Default)]
pub struct NetworkNode;
impl NetworkTab for NetworkNode {
fn get_type(&self) -> NetworkTabType {
NetworkTabType::Node
}
impl NodeTab for NetworkNode {
fn get_type(&self) -> NodeTabType {
NodeTabType::Info
}
fn ui(&mut self, ui: &mut egui::Ui, _: &dyn PlatformCallbacks) {
// Show an error content when available.
let node_err = Node::get_error();
if node_err.is_some() {
NetworkContent::node_error_ui(ui, node_err.unwrap());
return;
}
// Show message to enable node when it's not running.
if !Node::is_running() {
NetworkContent::disabled_node_ui(ui);
return;
}
// Show loading spinner when stats are not available.
let server_stats = Node::get_stats();
if server_stats.is_none() || Node::is_restarting() || Node::is_stopping() {
NetworkContent::loading_ui(ui, None);
return;
}
ScrollArea::vertical()
.id_source("integrated_node")
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.auto_shrink([false; 2])
.show(ui, |ui| {
ui.add_space(2.0);
ui.vertical_centered(|ui| {
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
// Show node stats content.
node_stats_ui(ui);
});
});
});
}
fn tab_ui(&mut self, ui: &mut egui::Ui, _: &dyn PlatformCallbacks) {
ScrollArea::vertical()
.id_salt("integrated_node_info_scroll")
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.auto_shrink([false; 2])
.show(ui, |ui| {
ui.add_space(2.0);
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
// Show node stats content.
node_stats_ui(ui);
});
});
}
}
/// Draw node statistics content.
fn node_stats_ui(ui: &mut egui::Ui) {
let server_stats = Node::get_stats();
let stats = server_stats.as_ref().unwrap();
let server_stats = Node::get_stats();
let stats = server_stats.as_ref().unwrap();
// Show header info.
View::sub_title(ui, format!("{} {}", FLOW_ARROW, t!("network_node.header")));
ui.columns(2, |columns| {
columns[0].vertical_centered(|ui| {
View::rounded_box(ui,
stats.header_stats.last_block_h.to_string(),
t!("network_node.hash"),
[true, false, false, false]);
});
columns[1].vertical_centered(|ui| {
View::rounded_box(ui,
stats.header_stats.height.to_string(),
t!("network_node.height"),
[false, true, false, false]);
});
});
ui.columns(2, |columns| {
columns[0].vertical_centered(|ui| {
View::rounded_box(ui,
stats.header_stats.total_difficulty.to_string(),
t!("network_node.difficulty"),
[false, false, true, false]);
});
columns[1].vertical_centered(|ui| {
let h_ts = stats.header_stats.latest_timestamp.timestamp();
let h_time = View::format_time(h_ts);
View::rounded_box(ui,
h_time,
t!("network_node.time"),
[false, false, false, true]);
});
});
ui.add_space(5.0);
// Show header info.
View::sub_title(ui, format!("{} {}", FLOW_ARROW, t!("network_node.header")));
ui.columns(2, |columns| {
columns[0].vertical_centered(|ui| {
View::label_box(
ui,
stats.header_stats.last_block_h.to_string(),
t!("network_node.hash"),
[true, false, false, false],
);
});
columns[1].vertical_centered(|ui| {
View::label_box(
ui,
stats.header_stats.height.to_string(),
t!("network_node.height"),
[false, true, false, false],
);
});
});
ui.columns(2, |columns| {
columns[0].vertical_centered(|ui| {
View::label_box(
ui,
stats.header_stats.total_difficulty.to_string(),
t!("network_node.difficulty"),
[false, false, true, false],
);
});
columns[1].vertical_centered(|ui| {
let h_ts = stats.header_stats.latest_timestamp.timestamp();
let h_time = View::format_time(h_ts);
View::label_box(
ui,
h_time,
t!("network_node.time"),
[false, false, false, true],
);
});
});
ui.add_space(5.0);
// Show block info.
View::sub_title(ui, format!("{} {}", CUBE, t!("network_node.block")));
ui.columns(2, |columns| {
columns[0].vertical_centered(|ui| {
View::rounded_box(ui,
stats.chain_stats.last_block_h.to_string(),
t!("network_node.hash"),
[true, false, false, false]);
});
columns[1].vertical_centered(|ui| {
View::rounded_box(ui,
stats.chain_stats.height.to_string(),
t!("network_node.height"),
[false, true, false, false]);
});
});
ui.columns(2, |columns| {
columns[0].vertical_centered(|ui| {
View::rounded_box(ui,
stats.chain_stats.total_difficulty.to_string(),
t!("network_node.difficulty"),
[false, false, true, false]);
});
columns[1].vertical_centered(|ui| {
let b_ts = stats.chain_stats.latest_timestamp.timestamp();
let b_time = View::format_time(b_ts);
View::rounded_box(ui,
b_time,
t!("network_node.time"),
[false, false, false, true]);
});
});
ui.add_space(5.0);
// Show block info.
View::sub_title(ui, format!("{} {}", CUBE, t!("network_node.block")));
ui.columns(2, |columns| {
columns[0].vertical_centered(|ui| {
View::label_box(
ui,
stats.chain_stats.last_block_h.to_string(),
t!("network_node.hash"),
[true, false, false, false],
);
});
columns[1].vertical_centered(|ui| {
View::label_box(
ui,
stats.chain_stats.height.to_string(),
t!("network_node.height"),
[false, true, false, false],
);
});
});
ui.columns(2, |columns| {
columns[0].vertical_centered(|ui| {
View::label_box(
ui,
stats.chain_stats.total_difficulty.to_string(),
t!("network_node.difficulty"),
[false, false, true, false],
);
});
columns[1].vertical_centered(|ui| {
let b_ts = stats.chain_stats.latest_timestamp.timestamp();
let b_time = View::format_time(b_ts);
View::label_box(
ui,
b_time,
t!("network_node.time"),
[false, false, false, true],
);
});
});
ui.add_space(5.0);
// Show data info.
View::sub_title(ui, format!("{} {}", SHARE_NETWORK, t!("network_node.data")));
ui.columns(2, |columns| {
columns[0].vertical_centered(|ui| {
let tx_stat = match &stats.tx_stats {
None => "0 (0)".to_string(),
Some(tx) => format!("{} ({})", tx.tx_pool_size, tx.tx_pool_kernels)
};
View::rounded_box(ui,
tx_stat,
t!("network_node.main_pool"),
[true, false, false, false]);
});
columns[1].vertical_centered(|ui| {
let stem_tx_stat = match &stats.tx_stats {
None => "0 (0)".to_string(),
Some(stx) => format!("{} ({})",
stx.stem_pool_size,
stx.stem_pool_kernels)
};
View::rounded_box(ui,
stem_tx_stat,
t!("network_node.stem_pool"),
[false, true, false, false]);
});
});
ui.columns(2, |columns| {
columns[0].vertical_centered(|ui| {
View::rounded_box(ui,
stats.disk_usage_gb.to_string(),
t!("network_node.size"),
[false, false, true, false]);
});
columns[1].vertical_centered(|ui| {
let peers_txt = format!("{} ({})",
stats.peer_count,
NodeConfig::get_max_outbound_peers());
View::rounded_box(ui, peers_txt, t!("network_node.peers"), [false, false, false, true]);
});
});
ui.add_space(5.0);
// Show data info.
View::sub_title(ui, format!("{} {}", SHARE_NETWORK, t!("network_node.data")));
ui.columns(2, |columns| {
columns[0].vertical_centered(|ui| {
let tx_stat = match &stats.tx_stats {
None => "0 (0)".to_string(),
Some(tx) => format!("{} ({})", tx.tx_pool_size, tx.tx_pool_kernels),
};
View::label_box(
ui,
tx_stat,
t!("network_node.main_pool"),
[true, false, false, false],
);
});
columns[1].vertical_centered(|ui| {
let stem_tx_stat = match &stats.tx_stats {
None => "0 (0)".to_string(),
Some(stx) => format!("{} ({})", stx.stem_pool_size, stx.stem_pool_kernels),
};
View::label_box(
ui,
stem_tx_stat,
t!("network_node.stem_pool"),
[false, true, false, false],
);
});
});
ui.columns(2, |columns| {
columns[0].vertical_centered(|ui| {
View::label_box(
ui,
stats.disk_usage_gb.to_string(),
t!("network_node.size"),
[false, false, true, false],
);
});
columns[1].vertical_centered(|ui| {
let peers_txt = format!(
"{} ({})",
stats.peer_count,
NodeConfig::get_max_outbound_peers()
);
View::label_box(
ui,
peers_txt,
t!("network_node.peers"),
[false, false, false, true],
);
});
});
ui.add_space(5.0);
// Show peer stats when available.
if stats.peer_count > 0 {
View::sub_title(ui, format!("{} {}", HANDSHAKE, t!("network_node.peers")));
let peers = &stats.peer_stats;
for (index, ps) in peers.iter().enumerate() {
peer_item_ui(ui, ps, View::item_rounding(index, peers.len(), false));
}
ui.add_space(5.0);
}
// Show peer stats when available.
if stats.peer_count > 0 {
View::sub_title(ui, format!("{} {}", HANDSHAKE, t!("network_node.peers")));
let peers = &stats.peer_stats;
for (index, ps) in peers.iter().enumerate() {
peer_item_ui(ui, ps, View::item_rounding(index, peers.len(), false));
}
ui.add_space(5.0);
}
}
const PEER_ITEM_HEIGHT: f32 = 77.0;
/// Draw connected peer info item.
fn peer_item_ui(ui: &mut egui::Ui, peer: &PeerStats, rounding: Rounding) {
let mut rect = ui.available_rect_before_wrap();
rect.set_height(79.0);
ui.allocate_ui_at_rect(rect, |ui| {
ui.vertical(|ui| {
ui.add_space(4.0);
fn peer_item_ui(ui: &mut egui::Ui, peer: &PeerStats, r: CornerRadius) {
let mut rect = ui.available_rect_before_wrap();
rect.set_height(PEER_ITEM_HEIGHT);
ui.allocate_ui(rect.size(), |ui| {
ui.vertical(|ui| {
ui.add_space(4.0);
// Draw round background.
ui.painter().rect(rect, rounding, Colors::white_or_black(false), View::item_stroke());
// Draw round background.
ui.painter().rect(
rect,
r,
Colors::fill(),
View::item_stroke(),
StrokeKind::Outside,
);
// Draw peer address
ui.horizontal(|ui| {
ui.add_space(7.0);
ui.label(RichText::new(&peer.addr).color(Colors::white_or_black(true)).size(17.0));
});
// Draw peer difficulty and height
ui.horizontal(|ui| {
ui.add_space(6.0);
let diff_text = format!("{} {} {} {}",
PACKAGE,
peer.total_difficulty,
AT,
peer.height);
ui.label(RichText::new(diff_text).color(Colors::title(false)).size(16.0));
});
// Draw peer user-agent
ui.horizontal(|ui| {
ui.add_space(6.0);
let agent_text = format!("{} {}", DEVICES, &peer.user_agent);
ui.label(RichText::new(agent_text).color(Colors::gray()).size(16.0));
});
// Draw IP address.
ui.horizontal(|ui| {
ui.add_space(7.0);
ui.label(
RichText::new(&peer.addr)
.color(Colors::white_or_black(true))
.size(17.0),
);
});
// Draw difficulty and height.
ui.horizontal(|ui| {
ui.add_space(6.0);
let diff_text = format!(
"{} {} {} {}",
PACKAGE, peer.total_difficulty, AT, peer.height
);
ui.label(
RichText::new(diff_text)
.color(Colors::title(false))
.size(15.0),
);
});
// Draw user-agent.
ui.horizontal(|ui| {
ui.add_space(6.0);
let agent_text = format!("{} {}", DEVICES, &peer.user_agent);
ui.label(RichText::new(agent_text).color(Colors::gray()).size(15.0));
});
ui.add_space(3.0);
});
});
}
ui.add_space(3.0);
});
});
}
+298 -212
View File
@@ -12,252 +12,338 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use egui::{RichText, ScrollArea};
use egui::scroll_area::ScrollBarVisibility;
use crate::gui::Colors;
use crate::gui::icons::ARROW_COUNTER_CLOCKWISE;
use crate::gui::icons::{ARROW_COUNTER_CLOCKWISE, TRASH};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Modal, Content, View};
use crate::gui::views::network::setup::{DandelionSetup, NodeSetup, P2PSetup, PoolSetup, StratumSetup};
use crate::gui::views::network::types::{NetworkTab, NetworkTabType};
use crate::gui::views::types::{ModalContainer, ModalPosition};
use crate::gui::views::network::setup::{
DandelionSetup, NodeSetup, P2PSetup, PoolSetup, StratumSetup,
};
use crate::gui::views::network::types::{NodeTab, NodeTabType};
use crate::gui::views::types::{ContentContainer, ModalPosition};
use crate::gui::views::{Content, Modal, View};
use crate::node::{Node, NodeConfig};
use egui::scroll_area::ScrollBarVisibility;
use egui::{RichText, ScrollArea};
/// Integrated node settings tab content.
pub struct NetworkSettings {
/// Integrated node general setup content.
node: NodeSetup,
/// P2P server setup content.
p2p: P2PSetup,
/// Stratum server setup content.
stratum: StratumSetup,
/// Pool setup content.
pool: PoolSetup,
/// Dandelion server setup content.
dandelion: DandelionSetup,
/// Integrated node general setup content.
node: NodeSetup,
/// P2P server setup content.
p2p: P2PSetup,
/// Stratum server setup content.
stratum: StratumSetup,
/// Pool setup content.
pool: PoolSetup,
/// Dandelion server setup content.
dandelion: DandelionSetup,
/// [`Modal`] identifiers allowed at this ui container.
modal_ids: Vec<&'static str>
/// Flag to check if reset of data was called.
data_reset: bool,
}
/// Identifier for settings reset confirmation [`Modal`].
pub const RESET_SETTINGS_MODAL: &'static str = "reset_settings";
pub const RESET_SETTINGS_CONFIRMATION_MODAL: &'static str = "reset_settings_confirmation";
impl Default for NetworkSettings {
fn default() -> Self {
Self {
node: NodeSetup::default(),
p2p: P2PSetup::default(),
stratum: StratumSetup::default(),
pool: PoolSetup::default(),
dandelion: DandelionSetup::default(),
modal_ids: vec![
RESET_SETTINGS_MODAL
]
}
}
fn default() -> Self {
Self {
node: NodeSetup::default(),
p2p: P2PSetup::default(),
stratum: StratumSetup::default(),
pool: PoolSetup::default(),
dandelion: DandelionSetup::default(),
data_reset: false,
}
}
}
impl ModalContainer for NetworkSettings {
fn modal_ids(&self) -> &Vec<&'static str> {
&self.modal_ids
}
impl ContentContainer for NetworkSettings {
fn modal_ids(&self) -> Vec<&'static str> {
vec![RESET_SETTINGS_CONFIRMATION_MODAL]
}
fn modal_ui(&mut self,
ui: &mut egui::Ui,
modal: &Modal,
_: &dyn PlatformCallbacks) {
match modal.id {
RESET_SETTINGS_MODAL => reset_settings_confirmation_modal(ui, modal),
_ => {}
}
}
fn modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal, _: &dyn PlatformCallbacks) {
match modal.id {
RESET_SETTINGS_CONFIRMATION_MODAL => reset_settings_confirmation_modal(ui),
_ => {}
}
}
fn container_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
ScrollArea::vertical()
.id_salt("node_settings_scroll")
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.auto_shrink([false; 2])
.show(ui, |ui| {
ui.add_space(1.0);
ui.vertical_centered(|ui| {
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
// Draw node setup section.
self.node.ui(ui, cb);
ui.add_space(6.0);
View::horizontal_line(ui, Colors::stroke());
ui.add_space(4.0);
// Draw P2P server setup section.
self.p2p.ui(ui, cb);
ui.add_space(6.0);
View::horizontal_line(ui, Colors::stroke());
ui.add_space(4.0);
// Draw Stratum server setup section.
self.stratum.ui(ui, cb);
ui.add_space(6.0);
View::horizontal_line(ui, Colors::stroke());
ui.add_space(4.0);
// Draw pool setup section.
self.pool.ui(ui, cb);
ui.add_space(6.0);
View::horizontal_line(ui, Colors::stroke());
ui.add_space(4.0);
// Draw Dandelion server setup section.
self.dandelion.ui(ui, cb);
// Draw content to reset the data.
if !Node::is_restarting() && !self.data_reset {
ui.add_space(4.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(6.0);
self.reset_data_ui(ui);
}
ui.add_space(6.0);
View::horizontal_line(ui, Colors::stroke());
ui.add_space(6.0);
// Draw reset settings content.
reset_settings_ui(ui);
});
});
});
}
}
impl NetworkTab for NetworkSettings {
fn get_type(&self) -> NetworkTabType {
NetworkTabType::Settings
}
impl NodeTab for NetworkSettings {
fn get_type(&self) -> NodeTabType {
NodeTabType::Settings
}
fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
// Draw modal content for current ui container.
self.current_modal_ui(ui, cb);
ScrollArea::vertical()
.id_source("network_settings")
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.auto_shrink([false; 2])
.show(ui, |ui| {
ui.add_space(1.0);
ui.vertical_centered(|ui| {
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
// Draw node setup section.
self.node.ui(ui, cb);
ui.add_space(6.0);
View::horizontal_line(ui, Colors::stroke());
ui.add_space(4.0);
// Draw P2P server setup section.
self.p2p.ui(ui, cb);
ui.add_space(6.0);
View::horizontal_line(ui, Colors::stroke());
ui.add_space(4.0);
// Draw Stratum server setup section.
self.stratum.ui(ui, cb);
ui.add_space(6.0);
View::horizontal_line(ui, Colors::stroke());
ui.add_space(4.0);
// Draw pool setup section.
self.pool.ui(ui, cb);
ui.add_space(6.0);
View::horizontal_line(ui, Colors::stroke());
ui.add_space(4.0);
// Draw Dandelion server setup section.
self.dandelion.ui(ui, cb);
ui.add_space(6.0);
View::horizontal_line(ui, Colors::stroke());
ui.add_space(6.0);
// Draw reset settings content.
reset_settings_ui(ui);
});
});
});
}
fn tab_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
self.ui(ui, cb);
}
}
impl NetworkSettings {
/// Reminder to restart enabled node to show on edit setting at [`Modal`].
pub fn node_restart_required_ui(ui: &mut egui::Ui) {
if Node::is_running() {
ui.add_space(12.0);
ui.label(RichText::new(t!("network_settings.restart_node_required"))
.size(16.0)
.color(Colors::green())
);
}
}
/// Reminder to restart enabled node to show on edit setting at [`Modal`].
pub fn node_restart_required_ui(ui: &mut egui::Ui) {
if Node::is_running() {
ui.add_space(12.0);
ui.label(
RichText::new(t!("network_settings.restart_node_required"))
.size(16.0)
.color(Colors::green()),
);
}
}
/// Draw IP addresses as radio buttons.
pub fn ip_addrs_ui(ui: &mut egui::Ui,
saved_ip: &String,
ips: &Vec<String>,
on_change: impl FnOnce(&String)) {
let mut selected_ip = saved_ip;
/// Draw IP addresses as radio buttons.
pub fn ip_addrs_ui(
ui: &mut egui::Ui,
saved_ip: &String,
ips: &Vec<String>,
on_change: impl FnOnce(&String),
) {
let mut all = NodeConfig::ALL_INTERFACES.to_string();
let all_ips = saved_ip == &all || saved_ip == &format!("[{}]", &all);
if all_ips {
all = saved_ip.clone();
}
// Set first IP address as current if saved is not present at system.
if !ips.contains(saved_ip) {
selected_ip = ips.get(0).unwrap();
}
let mut selected_ip = saved_ip.clone();
ui.add_space(2.0);
let mut listen_all_changed = false;
View::checkbox(ui, all_ips, t!("network_settings.ip_listen_all"), || {
listen_all_changed = true;
});
if listen_all_changed {
let new_ip = if all_ips {
ips.get(0).unwrap_or(&all).clone()
} else {
all.clone()
};
selected_ip = new_ip;
}
// Show available IP addresses on the system.
let _ = ips.chunks(2).map(|x| {
if x.len() == 2 {
ui.columns(2, |columns| {
let ip_left = x.get(0).unwrap();
columns[0].vertical_centered(|ui| {
View::radio_value(ui, &mut selected_ip, ip_left, ip_left.to_string());
});
let ip_right = x.get(1).unwrap();
columns[1].vertical_centered(|ui| {
View::radio_value(ui, &mut selected_ip, ip_right, ip_right.to_string());
})
});
} else {
let ip = x.get(0).unwrap();
View::radio_value(ui, &mut selected_ip, ip, ip.to_string());
}
ui.add_space(12.0);
}).collect::<Vec<_>>();
ui.add_space(8.0);
if saved_ip != selected_ip {
(on_change)(&selected_ip.to_string());
}
}
if selected_ip != all {
// Set first IP address as current if saved is not present at system.
if !ips.contains(&saved_ip) {
selected_ip = ips.get(0).unwrap().clone();
}
/// Show message when IP addresses are not available at system.
pub fn no_ip_address_ui(ui: &mut egui::Ui) {
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("network.no_ips"))
.size(16.0)
.color(Colors::inactive_text())
);
ui.add_space(6.0);
});
}
// Show available IP addresses on the system.
let _ = ips
.chunks(2)
.map(|x| {
if x.len() == 2 {
ui.columns(2, |columns| {
let ip_left = x.get(0).unwrap();
let val = if all_ips {
&mut ip_left.clone()
} else {
&mut selected_ip
};
columns[0].vertical_centered(|ui| {
View::radio_value(ui, val, ip_left.clone(), ip_left.to_string());
});
let ip_right = x.get(1).unwrap();
let val = if all_ips {
&mut ip_right.clone()
} else {
&mut selected_ip
};
columns[1].vertical_centered(|ui| {
View::radio_value(ui, val, ip_right.clone(), ip_right.to_string());
})
});
} else {
let ip = x.get(0).unwrap();
let val = if all_ips {
&mut ip.clone()
} else {
&mut selected_ip
};
View::radio_value(ui, val, ip.clone(), ip.to_string());
}
ui.add_space(12.0);
})
.collect::<Vec<_>>();
}
if saved_ip != &selected_ip {
on_change(&selected_ip.to_string());
}
}
/// Show message when IP addresses are not available at system.
pub fn no_ip_address_ui(ui: &mut egui::Ui) {
ui.vertical_centered(|ui| {
ui.label(
RichText::new(t!("network.no_ips"))
.size(16.0)
.color(Colors::inactive_text()),
);
ui.add_space(6.0);
});
}
/// Draw content to reset data.
fn reset_data_ui(&mut self, ui: &mut egui::Ui) {
ui.add_space(4.0);
View::colored_text_button(
ui,
format!("{} {}", TRASH, t!("network_settings.reset_data")),
Colors::red(),
Colors::white_or_black(false),
|| {
Node::reset_data(false);
self.data_reset = true;
},
);
ui.add_space(6.0);
ui.label(
RichText::new(t!("network_settings.reset_data_desc"))
.size(16.0)
.color(Colors::inactive_text()),
);
ui.add_space(4.0);
}
}
/// Draw button to reset integrated node settings to default values.
fn reset_settings_ui(ui: &mut egui::Ui) {
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("network_settings.reset_settings_desc"))
.size(16.0)
.color(Colors::text(false)));
ui.add_space(8.0);
let button_text = format!("{} {}",
ARROW_COUNTER_CLOCKWISE,
t!("network_settings.reset_settings"));
View::action_button(ui, button_text, || {
// Show modal to confirm settings reset.
Modal::new(RESET_SETTINGS_MODAL)
.position(ModalPosition::Center)
.title(t!("modal.confirmation"))
.show();
});
// Show reminder to restart enabled node.
if Node::is_running() {
ui.add_space(12.0);
ui.label(RichText::new(t!("network_settings.restart_node_required"))
.size(16.0)
.color(Colors::gray())
);
}
ui.add_space(12.0);
});
ui.label(
RichText::new(t!("network_settings.reset_settings_desc"))
.size(16.0)
.color(Colors::text(false)),
);
ui.add_space(8.0);
let button_text = format!(
"{} {}",
ARROW_COUNTER_CLOCKWISE,
t!("network_settings.reset_settings")
);
View::action_button(ui, button_text, || {
// Show modal to confirm settings reset.
Modal::new(RESET_SETTINGS_CONFIRMATION_MODAL)
.position(ModalPosition::Center)
.title(t!("confirmation"))
.show();
});
// Show reminder to restart enabled node.
if Node::is_running() {
ui.add_space(12.0);
ui.label(
RichText::new(t!("network_settings.restart_node_required"))
.size(16.0)
.color(Colors::gray()),
);
}
ui.add_space(10.0);
}
/// Confirmation to reset settings to default values.
fn reset_settings_confirmation_modal(ui: &mut egui::Ui, modal: &Modal) {
ui.add_space(6.0);
ui.vertical_centered(|ui| {
let reset_text = format!("{}?", t!("network_settings.reset_settings_desc"));
ui.label(RichText::new(reset_text)
.size(17.0)
.color(Colors::text(false)));
ui.add_space(8.0);
});
fn reset_settings_confirmation_modal(ui: &mut egui::Ui) {
ui.add_space(6.0);
ui.vertical_centered(|ui| {
let reset_text = format!("{}?", t!("network_settings.reset_settings_desc"));
ui.label(
RichText::new(reset_text)
.size(17.0)
.color(Colors::text(false)),
);
ui.add_space(8.0);
});
// Show modal buttons.
ui.scope(|ui| {
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
// Show modal buttons.
ui.scope(|ui| {
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(ui, t!("network_settings.reset"), Colors::white_or_black(false), || {
NodeConfig::reset_to_default();
modal.close();
});
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
modal.close();
});
});
});
ui.add_space(6.0);
});
}
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(
ui,
t!("network_settings.reset"),
Colors::white_or_black(false),
|| {
NodeConfig::reset_to_default();
Modal::close();
},
);
});
columns[1].vertical_centered_justified(|ui| {
View::button(
ui,
t!("modal.cancel"),
Colors::white_or_black(false),
|| {
Modal::close();
},
);
});
});
ui.add_space(6.0);
});
}
+424 -367
View File
@@ -17,27 +17,24 @@ use egui::{Id, RichText};
use crate::gui::Colors;
use crate::gui::icons::{CLOCK_COUNTDOWN, GRAPH, TIMER, WATCH};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Modal, View};
use crate::gui::views::network::settings::NetworkSettings;
use crate::gui::views::types::{ModalContainer, ModalPosition, TextEditOptions};
use crate::gui::views::network::NetworkSettings;
use crate::gui::views::types::{ContentContainer, ModalPosition};
use crate::gui::views::{Modal, TextEdit, View};
use crate::node::NodeConfig;
/// Dandelion server setup section content.
pub struct DandelionSetup {
/// Epoch duration value in seconds.
epoch_edit: String,
/// Epoch duration value in seconds.
epoch_edit: String,
/// Embargo expiration time value in seconds to fluff and broadcast if tx not seen on network.
embargo_edit: String,
/// Embargo expiration time value in seconds to fluff and broadcast if tx not seen on network.
embargo_edit: String,
/// Aggregation period value in seconds.
aggregation_edit: String,
/// Aggregation period value in seconds.
aggregation_edit: String,
/// Stem phase probability value (stem 90% of the time, fluff 10% of the time by default).
stem_prob_edit: String,
/// [`Modal`] identifiers allowed at this ui container.
modal_ids: Vec<&'static str>,
/// Stem phase probability value (stem 90% of the time, fluff 10% of the time by default).
stem_prob_edit: String,
}
/// Identifier epoch duration value [`Modal`].
@@ -50,393 +47,453 @@ pub const AGGREGATION_MODAL: &'static str = "aggregation_secs";
pub const STEM_PROBABILITY_MODAL: &'static str = "stem_probability";
impl Default for DandelionSetup {
fn default() -> Self {
Self {
epoch_edit: NodeConfig::get_dandelion_epoch(),
embargo_edit: NodeConfig::get_reorg_cache_period(),
aggregation_edit: NodeConfig::get_dandelion_aggregation(),
stem_prob_edit: NodeConfig::get_stem_probability(),
modal_ids: vec![
EPOCH_MODAL,
EMBARGO_MODAL,
AGGREGATION_MODAL,
STEM_PROBABILITY_MODAL
]
}
}
fn default() -> Self {
Self {
epoch_edit: NodeConfig::get_dandelion_epoch(),
embargo_edit: NodeConfig::get_reorg_cache_period(),
aggregation_edit: NodeConfig::get_dandelion_aggregation(),
stem_prob_edit: NodeConfig::get_stem_probability(),
}
}
}
impl ModalContainer for DandelionSetup {
fn modal_ids(&self) -> &Vec<&'static str> {
&self.modal_ids
}
impl ContentContainer for DandelionSetup {
fn modal_ids(&self) -> Vec<&'static str> {
vec![
EPOCH_MODAL,
EMBARGO_MODAL,
AGGREGATION_MODAL,
STEM_PROBABILITY_MODAL,
]
}
fn modal_ui(&mut self,
ui: &mut egui::Ui,
modal: &Modal,
cb: &dyn PlatformCallbacks) {
match modal.id {
EPOCH_MODAL => self.epoch_modal(ui, modal, cb),
EMBARGO_MODAL => self.embargo_modal(ui, modal, cb),
AGGREGATION_MODAL => self.aggregation_modal(ui, modal, cb),
STEM_PROBABILITY_MODAL => self.stem_prob_modal(ui, modal, cb),
_ => {}
}
}
fn modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
match modal.id {
EPOCH_MODAL => self.epoch_modal(ui, modal, cb),
EMBARGO_MODAL => self.embargo_modal(ui, modal, cb),
AGGREGATION_MODAL => self.aggregation_modal(ui, modal, cb),
STEM_PROBABILITY_MODAL => self.stem_prob_modal(ui, modal, cb),
_ => {}
}
}
fn container_ui(&mut self, ui: &mut egui::Ui, _: &dyn PlatformCallbacks) {
View::sub_title(ui, format!("{} {}", GRAPH, "Dandelion"));
View::horizontal_line(ui, Colors::stroke());
ui.add_space(6.0);
ui.vertical_centered(|ui| {
// Show epoch duration setup.
self.epoch_ui(ui);
ui.add_space(6.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(6.0);
// Show embargo expiration time setup.
self.embargo_ui(ui);
ui.add_space(6.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(6.0);
// Show aggregation period setup.
self.aggregation_ui(ui);
ui.add_space(6.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(6.0);
// Show Stem phase probability setup.
self.stem_prob_ui(ui);
ui.add_space(6.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(8.0);
// Show setup to always stem our txs.
let always_stem = NodeConfig::always_stem_our_txs();
View::checkbox(ui, always_stem, t!("network_settings.stem_txs"), || {
NodeConfig::toggle_always_stem_our_txs();
});
ui.add_space(6.0);
});
}
}
impl DandelionSetup {
pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
// Draw modal content for current ui container.
self.current_modal_ui(ui, cb);
/// Draw epoch duration setup content.
fn epoch_ui(&mut self, ui: &mut egui::Ui) {
ui.label(
RichText::new(t!("network_settings.epoch_duration"))
.size(16.0)
.color(Colors::gray()),
);
ui.add_space(6.0);
View::sub_title(ui, format!("{} {}", GRAPH, "Dandelion"));
View::horizontal_line(ui, Colors::stroke());
ui.add_space(6.0);
let epoch = NodeConfig::get_dandelion_epoch();
View::button(
ui,
format!("{} {}", WATCH, &epoch),
Colors::white_or_black(false),
|| {
// Setup values for modal.
self.epoch_edit = epoch;
// Show epoch setup modal.
Modal::new(EPOCH_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("network_settings.change_value"))
.show();
},
);
ui.add_space(6.0);
}
ui.vertical_centered(|ui| {
// Show epoch duration setup.
self.epoch_ui(ui, cb);
/// Draw epoch duration [`Modal`] content.
fn epoch_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
// Save button callback.
let on_save = |c: &mut DandelionSetup| {
if let Ok(epoch) = c.epoch_edit.parse::<u16>() {
NodeConfig::save_dandelion_epoch(epoch);
Modal::close();
}
};
ui.add_space(6.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(6.0);
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(
RichText::new(t!("network_settings.epoch_duration"))
.size(17.0)
.color(Colors::gray()),
);
ui.add_space(8.0);
// Show embargo expiration time setup.
self.embargo_ui(ui, cb);
// Draw epoch text edit.
let mut epoch_edit = TextEdit::new(Id::from(modal.id)).h_center().numeric();
epoch_edit.ui(ui, &mut self.epoch_edit, cb);
if epoch_edit.enter_pressed {
on_save(self);
}
ui.add_space(6.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(6.0);
// Show error when specified value is not valid or reminder to restart enabled node.
if self.epoch_edit.parse::<u16>().is_err() {
ui.add_space(12.0);
ui.label(
RichText::new(t!("network_settings.not_valid_value"))
.size(17.0)
.color(Colors::red()),
);
} else {
NetworkSettings::node_restart_required_ui(ui);
}
ui.add_space(12.0);
});
// Show aggregation period setup.
self.aggregation_ui(ui, cb);
// Show modal buttons.
ui.scope(|ui| {
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
ui.add_space(6.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(6.0);
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(
ui,
t!("modal.cancel"),
Colors::white_or_black(false),
|| {
// Close modal.
Modal::close();
},
);
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.save"), Colors::white_or_black(false), || {
on_save(self);
});
});
});
ui.add_space(6.0);
});
}
// Show Stem phase probability setup.
self.stem_prob_ui(ui, cb);
/// Draw embargo expiration time setup content.
fn embargo_ui(&mut self, ui: &mut egui::Ui) {
ui.label(
RichText::new(t!("network_settings.embargo_timer"))
.size(16.0)
.color(Colors::gray()),
);
ui.add_space(6.0);
ui.add_space(6.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(8.0);
let embargo = NodeConfig::get_dandelion_embargo();
View::button(
ui,
format!("{} {}", TIMER, &embargo),
Colors::white_or_black(false),
|| {
self.embargo_edit = embargo;
// Show embargo setup modal.
Modal::new(EMBARGO_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("network_settings.change_value"))
.show();
},
);
ui.add_space(6.0);
}
// Show setup to always stem our txs.
let always_stem = NodeConfig::always_stem_our_txs();
View::checkbox(ui, always_stem, t!("network_settings.stem_txs"), || {
NodeConfig::toggle_always_stem_our_txs();
});
ui.add_space(6.0);
});
}
/// Draw epoch duration [`Modal`] content.
fn embargo_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
// Save button callback.
let on_save = |c: &mut DandelionSetup| {
if let Ok(embargo) = c.embargo_edit.parse::<u16>() {
NodeConfig::save_dandelion_embargo(embargo);
Modal::close();
}
};
/// Draw epoch duration setup content.
fn epoch_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
ui.label(RichText::new(t!("network_settings.epoch_duration"))
.size(16.0)
.color(Colors::gray())
);
ui.add_space(6.0);
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(
RichText::new(t!("network_settings.embargo_timer"))
.size(17.0)
.color(Colors::gray()),
);
ui.add_space(8.0);
let epoch = NodeConfig::get_dandelion_epoch();
View::button(ui, format!("{} {}", WATCH, epoch.clone()), Colors::button(), || {
// Setup values for modal.
self.epoch_edit = epoch;
// Show epoch setup modal.
Modal::new(EPOCH_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("network_settings.change_value"))
.show();
cb.show_keyboard();
});
ui.add_space(6.0);
}
// Draw embargo text edit.
let mut embargo_edit = TextEdit::new(Id::from(modal.id)).h_center().numeric();
embargo_edit.ui(ui, &mut self.embargo_edit, cb);
if embargo_edit.enter_pressed {
on_save(self);
}
/// Draw epoch duration [`Modal`] content.
fn epoch_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("network_settings.epoch_duration"))
.size(17.0)
.color(Colors::gray()));
ui.add_space(8.0);
// Show error when specified value is not valid or reminder to restart enabled node.
if self.embargo_edit.parse::<u16>().is_err() {
ui.add_space(12.0);
ui.label(
RichText::new(t!("network_settings.not_valid_value"))
.size(17.0)
.color(Colors::red()),
);
} else {
NetworkSettings::node_restart_required_ui(ui);
}
ui.add_space(12.0);
});
// Draw epoch text edit.
let mut epoch_edit_opts = TextEditOptions::new(Id::from(modal.id)).h_center();
View::text_edit(ui, cb, &mut self.epoch_edit, &mut epoch_edit_opts);
// Show modal buttons.
ui.scope(|ui| {
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
// Show error when specified value is not valid or reminder to restart enabled node.
if self.epoch_edit.parse::<u16>().is_err() {
ui.add_space(12.0);
ui.label(RichText::new(t!("network_settings.not_valid_value"))
.size(17.0)
.color(Colors::red()));
} else {
NetworkSettings::node_restart_required_ui(ui);
}
ui.add_space(12.0);
});
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(
ui,
t!("modal.cancel"),
Colors::white_or_black(false),
|| {
// Close modal.
Modal::close();
},
);
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.save"), Colors::white_or_black(false), || {
on_save(self);
});
});
});
ui.add_space(6.0);
});
}
// Show modal buttons.
ui.scope(|ui| {
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
/// Draw aggregation period setup content.
fn aggregation_ui(&mut self, ui: &mut egui::Ui) {
ui.label(
RichText::new(t!("network_settings.aggregation_period"))
.size(16.0)
.color(Colors::gray()),
);
ui.add_space(6.0);
// Save button callback.
let on_save = || {
if let Ok(epoch) = self.epoch_edit.parse::<u16>() {
NodeConfig::save_dandelion_epoch(epoch);
cb.hide_keyboard();
modal.close();
}
};
let ag = NodeConfig::get_dandelion_aggregation();
View::button(
ui,
format!("{} {}", CLOCK_COUNTDOWN, &ag),
Colors::white_or_black(false),
|| {
// Setup values for modal.
self.aggregation_edit = ag;
// Show aggregation setup modal.
Modal::new(AGGREGATION_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("network_settings.change_value"))
.show();
},
);
ui.add_space(6.0);
}
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
// Close modal.
cb.hide_keyboard();
modal.close();
});
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.save"), Colors::white_or_black(false), on_save);
});
});
ui.add_space(6.0);
});
}
/// Draw aggregation period [`Modal`] content.
fn aggregation_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
// Save button callback.
let on_save = |c: &mut DandelionSetup| {
if let Ok(embargo) = c.aggregation_edit.parse::<u16>() {
NodeConfig::save_dandelion_aggregation(embargo);
Modal::close();
}
};
/// Draw embargo expiration time setup content.
fn embargo_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
ui.label(RichText::new(t!("network_settings.embargo_timer"))
.size(16.0)
.color(Colors::gray())
);
ui.add_space(6.0);
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(
RichText::new(t!("network_settings.aggregation_period"))
.size(17.0)
.color(Colors::gray()),
);
ui.add_space(8.0);
let embargo = NodeConfig::get_dandelion_embargo();
View::button(ui, format!("{} {}", TIMER, embargo.clone()), Colors::button(), || {
// Setup values for modal.
self.embargo_edit = embargo;
// Show embargo setup modal.
Modal::new(EMBARGO_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("network_settings.change_value"))
.show();
cb.show_keyboard();
});
ui.add_space(6.0);
}
// Draw aggregation period text edit.
let mut aggregation_edit = TextEdit::new(Id::from(modal.id)).h_center().numeric();
aggregation_edit.ui(ui, &mut self.aggregation_edit, cb);
if aggregation_edit.enter_pressed {
on_save(self);
}
/// Draw epoch duration [`Modal`] content.
fn embargo_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("network_settings.embargo_timer"))
.size(17.0)
.color(Colors::gray()));
ui.add_space(8.0);
// Show error when specified value is not valid or reminder to restart enabled node.
if self.aggregation_edit.parse::<u16>().is_err() {
ui.add_space(12.0);
ui.label(
RichText::new(t!("network_settings.not_valid_value"))
.size(17.0)
.color(Colors::red()),
);
} else {
NetworkSettings::node_restart_required_ui(ui);
}
ui.add_space(12.0);
});
// Draw embargo text edit.
let mut embargo_edit_opts = TextEditOptions::new(Id::from(modal.id)).h_center();
View::text_edit(ui, cb, &mut self.embargo_edit, &mut embargo_edit_opts);
// Show modal buttons.
ui.scope(|ui| {
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
// Show error when specified value is not valid or reminder to restart enabled node.
if self.embargo_edit.parse::<u16>().is_err() {
ui.add_space(12.0);
ui.label(RichText::new(t!("network_settings.not_valid_value"))
.size(17.0)
.color(Colors::red()));
} else {
NetworkSettings::node_restart_required_ui(ui);
}
ui.add_space(12.0);
});
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(
ui,
t!("modal.cancel"),
Colors::white_or_black(false),
|| {
// Close modal.
Modal::close();
},
);
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.save"), Colors::white_or_black(false), || {
on_save(self);
});
});
});
ui.add_space(6.0);
});
}
// Show modal buttons.
ui.scope(|ui| {
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
/// Draw stem phase probability setup content.
fn stem_prob_ui(&mut self, ui: &mut egui::Ui) {
ui.label(
RichText::new(t!("network_settings.stem_probability"))
.size(16.0)
.color(Colors::gray()),
);
ui.add_space(6.0);
// Save button callback.
let on_save = || {
if let Ok(embargo) = self.embargo_edit.parse::<u16>() {
NodeConfig::save_dandelion_embargo(embargo);
cb.hide_keyboard();
modal.close();
}
};
let stem_prob = NodeConfig::get_stem_probability();
View::button(
ui,
format!("{}%", &stem_prob),
Colors::white_or_black(false),
|| {
// Setup values for modal.
self.stem_prob_edit = stem_prob;
// Show stem probability setup modal.
Modal::new(STEM_PROBABILITY_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("network_settings.change_value"))
.show();
},
);
ui.add_space(6.0);
}
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
// Close modal.
cb.hide_keyboard();
modal.close();
});
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.save"), Colors::white_or_black(false), on_save);
});
});
ui.add_space(6.0);
});
}
/// Draw stem phase probability [`Modal`] content.
fn stem_prob_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
// Save button callback.
let on_save = |c: &mut DandelionSetup| {
if let Ok(prob) = c.stem_prob_edit.parse::<u8>() {
NodeConfig::save_stem_probability(prob);
Modal::close();
}
};
/// Draw aggregation period setup content.
fn aggregation_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
ui.label(RichText::new(t!("network_settings.aggregation_period"))
.size(16.0)
.color(Colors::gray())
);
ui.add_space(6.0);
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(
RichText::new(t!("network_settings.stem_probability"))
.size(17.0)
.color(Colors::gray()),
);
ui.add_space(8.0);
let agg = NodeConfig::get_dandelion_aggregation();
View::button(ui, format!("{} {}", CLOCK_COUNTDOWN, agg.clone()), Colors::button(), || {
// Setup values for modal.
self.aggregation_edit = agg;
// Show aggregation setup modal.
Modal::new(AGGREGATION_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("network_settings.change_value"))
.show();
cb.show_keyboard();
});
ui.add_space(6.0);
}
// Draw stem phase probability text edit.
let mut stem_prob_edit = TextEdit::new(Id::from(modal.id)).h_center().numeric();
stem_prob_edit.ui(ui, &mut self.stem_prob_edit, cb);
if stem_prob_edit.enter_pressed {
on_save(self);
}
/// Draw aggregation period [`Modal`] content.
fn aggregation_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("network_settings.aggregation_period"))
.size(17.0)
.color(Colors::gray()));
ui.add_space(8.0);
// Show error when specified value is not valid or reminder to restart enabled node.
if self.stem_prob_edit.parse::<u8>().is_err() {
ui.add_space(12.0);
ui.label(
RichText::new(t!("network_settings.not_valid_value"))
.size(17.0)
.color(Colors::red()),
);
} else {
NetworkSettings::node_restart_required_ui(ui);
}
ui.add_space(12.0);
});
// Draw aggregation period text edit.
let mut aggregation_edit_opts = TextEditOptions::new(Id::from(modal.id)).h_center();
View::text_edit(ui, cb, &mut self.aggregation_edit, &mut aggregation_edit_opts);
// Show modal buttons.
ui.scope(|ui| {
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
// Show error when specified value is not valid or reminder to restart enabled node.
if self.aggregation_edit.parse::<u16>().is_err() {
ui.add_space(12.0);
ui.label(RichText::new(t!("network_settings.not_valid_value"))
.size(17.0)
.color(Colors::red()));
} else {
NetworkSettings::node_restart_required_ui(ui);
}
ui.add_space(12.0);
});
// Show modal buttons.
ui.scope(|ui| {
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
// Save button callback.
let on_save = || {
if let Ok(embargo) = self.aggregation_edit.parse::<u16>() {
NodeConfig::save_dandelion_aggregation(embargo);
cb.hide_keyboard();
modal.close();
}
};
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
// Close modal.
cb.hide_keyboard();
modal.close();
});
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.save"), Colors::white_or_black(false), on_save);
});
});
ui.add_space(6.0);
});
}
/// Draw stem phase probability setup content.
fn stem_prob_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
ui.label(RichText::new(t!("network_settings.stem_probability"))
.size(16.0)
.color(Colors::gray())
);
ui.add_space(6.0);
let stem_prob = NodeConfig::get_stem_probability();
View::button(ui, format!("{}%", stem_prob.clone()), Colors::button(), || {
// Setup values for modal.
self.stem_prob_edit = stem_prob;
// Show stem probability setup modal.
Modal::new(STEM_PROBABILITY_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("network_settings.change_value"))
.show();
cb.show_keyboard();
});
ui.add_space(6.0);
}
/// Draw stem phase probability [`Modal`] content.
fn stem_prob_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("network_settings.stem_probability"))
.size(17.0)
.color(Colors::gray()));
ui.add_space(8.0);
// Draw stem phase probability text edit.
let mut stem_prob_edit_opts = TextEditOptions::new(Id::from(modal.id)).h_center();
View::text_edit(ui, cb, &mut self.stem_prob_edit, &mut stem_prob_edit_opts);
// Show error when specified value is not valid or reminder to restart enabled node.
if self.stem_prob_edit.parse::<u8>().is_err() {
ui.add_space(12.0);
ui.label(RichText::new(t!("network_settings.not_valid_value"))
.size(17.0)
.color(Colors::red()));
} else {
NetworkSettings::node_restart_required_ui(ui);
}
ui.add_space(12.0);
});
// Show modal buttons.
ui.scope(|ui| {
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
// Save button callback.
let on_save = || {
if let Ok(prob) = self.stem_prob_edit.parse::<u8>() {
NodeConfig::save_stem_probability(prob);
cb.hide_keyboard();
modal.close();
}
};
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
// Close modal.
cb.hide_keyboard();
modal.close();
});
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.save"), Colors::white_or_black(false), on_save);
});
});
ui.add_space(6.0);
});
}
}
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(
ui,
t!("modal.cancel"),
Colors::white_or_black(false),
|| {
// Close modal.
Modal::close();
},
);
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.save"), Colors::white_or_black(false), || {
on_save(self);
});
});
});
ui.add_space(6.0);
});
}
}
+1 -1
View File
@@ -25,4 +25,4 @@ mod dandelion;
pub use dandelion::DandelionSetup;
mod stratum;
pub use stratum::StratumSetup;
pub use stratum::StratumSetup;
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+490 -435
View File
@@ -18,42 +18,39 @@ use grin_chain::SyncStatus;
use crate::gui::Colors;
use crate::gui::icons::{BARBELL, HARD_DRIVES, PLUG, POWER, TIMER};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Modal, View};
use crate::gui::views::network::settings::NetworkSettings;
use crate::gui::views::types::{ModalContainer, ModalPosition, TextEditOptions};
use crate::gui::views::wallets::modals::WalletsModal;
use crate::gui::views::types::{ContentContainer, ModalPosition};
use crate::gui::views::wallets::modals::WalletListModal;
use crate::gui::views::{Modal, TextEdit, View};
use crate::node::{Node, NodeConfig};
use crate::wallet::{WalletConfig, WalletList};
/// Stratum server setup section content.
pub struct StratumSetup {
/// Wallet list to select for mining rewards.
wallets: WalletList,
/// Wallets [`Modal`] content.
wallets_modal: WalletsModal,
/// Wallet list to select for mining rewards.
wallets: WalletList,
/// Wallets [`Modal`] content.
wallets_modal: WalletListModal,
/// IP Addresses available at system.
available_ips: Vec<String>,
/// IP Addresses available at system.
available_ips: Vec<String>,
/// Stratum port value.
stratum_port_edit: String,
/// Flag to check if stratum port is available.
stratum_port_available_edit: bool,
/// Stratum port value.
stratum_port_edit: String,
/// Flag to check if stratum port is available.
stratum_port_available_edit: bool,
/// Flag to check if stratum port from saved config value is available.
is_port_available: bool,
/// Flag to check if stratum port from saved config value is available.
is_port_available: bool,
/// Wallet name to receive rewards.
wallet_name: Option<String>,
/// Wallet name to receive rewards.
pub wallet_name: Option<String>,
/// Attempt time value in seconds to mine on a particular header.
attempt_time_edit: String,
/// Attempt time value in seconds to mine on a particular header.
attempt_time_edit: String,
/// Minimum share difficulty value to request from miners.
min_share_diff_edit: String,
/// [`Modal`] identifiers allowed at this ui container.
modal_ids: Vec<&'static str>
/// Minimum share difficulty value to request from miners.
min_share_diff_edit: String,
}
/// Identifier for wallet selection [`Modal`].
@@ -66,456 +63,514 @@ const ATTEMPT_TIME_MODAL: &'static str = "stratum_attempt_time";
const MIN_SHARE_DIFF_MODAL: &'static str = "stratum_min_share_diff";
impl Default for StratumSetup {
fn default() -> Self {
let (ip, port) = NodeConfig::get_stratum_address();
let is_port_available = NodeConfig::is_stratum_port_available(&ip, &port);
fn default() -> Self {
let (ip, port) = NodeConfig::get_stratum_address();
let is_port_available = NodeConfig::is_stratum_port_available(&ip, &port);
// Setup mining rewards wallet name and identifier.
let mut wallet_id = NodeConfig::get_stratum_wallet_id();
let wallet_name = if let Some(id) = wallet_id {
WalletConfig::name_by_id(id)
} else {
None
};
if wallet_name.is_none() {
wallet_id = None;
}
// Setup mining rewards wallet name and identifier.
let mut wallet_id = NodeConfig::get_stratum_wallet_id();
let wallet_name = if let Some(id) = wallet_id {
WalletConfig::read_name_by_id(id)
} else {
None
};
if wallet_name.is_none() {
wallet_id = None;
}
Self {
wallets: WalletList::default(),
wallets_modal: WalletsModal::new(wallet_id),
available_ips: NodeConfig::get_ip_addrs(),
stratum_port_edit: port,
stratum_port_available_edit: is_port_available,
is_port_available,
wallet_name,
attempt_time_edit: NodeConfig::get_stratum_attempt_time(),
min_share_diff_edit: NodeConfig::get_stratum_min_share_diff(),
modal_ids: vec![
WALLET_SELECTION_MODAL,
STRATUM_PORT_MODAL,
ATTEMPT_TIME_MODAL,
MIN_SHARE_DIFF_MODAL
]
}
}
Self {
wallets: WalletList::default(),
wallets_modal: WalletListModal::new(wallet_id, None, false),
available_ips: NodeConfig::get_ip_addrs(),
stratum_port_edit: port,
stratum_port_available_edit: is_port_available,
is_port_available,
wallet_name,
attempt_time_edit: NodeConfig::get_stratum_attempt_time(),
min_share_diff_edit: NodeConfig::get_stratum_min_share_diff(),
}
}
}
impl ModalContainer for StratumSetup {
fn modal_ids(&self) -> &Vec<&'static str> {
&self.modal_ids
}
impl ContentContainer for StratumSetup {
fn modal_ids(&self) -> Vec<&'static str> {
vec![
WALLET_SELECTION_MODAL,
STRATUM_PORT_MODAL,
ATTEMPT_TIME_MODAL,
MIN_SHARE_DIFF_MODAL,
]
}
fn modal_ui(&mut self,
ui: &mut egui::Ui,
modal: &Modal,
cb: &dyn PlatformCallbacks) {
match modal.id {
WALLET_SELECTION_MODAL => self.wallets_modal.ui(ui, modal, &self.wallets, |id| {
NodeConfig::save_stratum_wallet_id(id);
self.wallet_name = WalletConfig::name_by_id(id);
}),
STRATUM_PORT_MODAL => self.port_modal(ui, modal, cb),
ATTEMPT_TIME_MODAL => self.attempt_modal(ui, modal, cb),
MIN_SHARE_DIFF_MODAL => self.min_diff_modal(ui, modal, cb),
_ => {}
}
}
fn modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
match modal.id {
WALLET_SELECTION_MODAL => self.wallets_modal.ui(ui, &mut self.wallets, |wallet, _| {
let id = wallet.get_config().id;
NodeConfig::save_stratum_wallet_id(id);
self.wallet_name = WalletConfig::read_name_by_id(id);
}),
STRATUM_PORT_MODAL => self.port_modal(ui, modal, cb),
ATTEMPT_TIME_MODAL => self.attempt_modal(ui, modal, cb),
MIN_SHARE_DIFF_MODAL => self.min_diff_modal(ui, modal, cb),
_ => {}
}
}
fn container_ui(&mut self, ui: &mut egui::Ui, _: &dyn PlatformCallbacks) {
View::sub_title(
ui,
format!("{} {}", HARD_DRIVES, t!("network_mining.server")),
);
View::horizontal_line(ui, Colors::stroke());
ui.add_space(6.0);
ui.vertical_centered(|ui| {
// Show loading indicator or controls to start/stop stratum server.
if Node::get_sync_status().unwrap_or(SyncStatus::Initial) == SyncStatus::NoSync
&& self.is_port_available
&& self.wallet_name.is_some()
{
if Node::is_stratum_starting() || Node::is_stratum_stopping() {
ui.vertical_centered(|ui| {
ui.add_space(8.0);
View::small_loading_spinner(ui);
ui.add_space(8.0);
});
} else if Node::get_stratum_stats().is_running {
ui.add_space(6.0);
let disable_text = format!("{} {}", POWER, t!("network_settings.disable"));
View::action_button(ui, disable_text, || {
Node::stop_stratum();
let (ip, port) = NodeConfig::get_stratum_address();
self.is_port_available = NodeConfig::is_stratum_port_available(&ip, &port);
});
ui.add_space(6.0);
} else {
ui.add_space(6.0);
let enable_text = format!("{} {}", POWER, t!("network_settings.enable"));
View::action_button(ui, enable_text, || {
Node::start_stratum();
});
ui.add_space(6.0);
}
}
// Show stratum server autorun checkbox.
let stratum_enabled = NodeConfig::is_stratum_autorun_enabled();
View::checkbox(ui, stratum_enabled, t!("network.autorun"), || {
NodeConfig::toggle_stratum_autorun();
});
// Show reminder to restart running server.
if Node::get_stratum_stats().is_running {
ui.add_space(2.0);
ui.label(
RichText::new(t!("network_mining.restart_server_required"))
.size(16.0)
.color(Colors::inactive_text()),
);
}
ui.add_space(8.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(8.0);
// Show wallet name.
ui.label(
RichText::new(self.wallet_name.as_ref().unwrap_or(&"-".to_string()))
.size(16.0)
.color(Colors::white_or_black(true)),
);
ui.add_space(8.0);
// Show button to select wallet.
View::button(
ui,
t!("network_settings.choose_wallet"),
Colors::white_or_black(false),
|| {
self.show_wallets_modal();
},
);
ui.add_space(12.0);
if self.wallet_name.is_some() {
ui.label(
RichText::new(t!("network_settings.stratum_wallet_warning"))
.size(16.0)
.color(Colors::inactive_text()),
);
ui.add_space(12.0);
}
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(6.0);
});
// Show message when IP addresses are not available on the system.
if self.available_ips.is_empty() {
NetworkSettings::no_ip_address_ui(ui);
return;
}
ui.vertical_centered(|ui| {
ui.label(
RichText::new(t!("network_settings.stratum_ip"))
.size(16.0)
.color(Colors::gray()),
);
ui.add_space(6.0);
// Show stratum IP addresses to select.
let (ip, port) = NodeConfig::get_stratum_address();
NetworkSettings::ip_addrs_ui(ui, &ip, &self.available_ips, |selected_ip| {
NodeConfig::save_stratum_address(selected_ip, &port);
self.is_port_available = NodeConfig::is_stratum_port_available(selected_ip, &port);
});
// Show stratum port setup.
self.port_setup_ui(ui);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(6.0);
// Show attempt time setup.
self.attempt_time_ui(ui);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(6.0);
// Show minimum acceptable share difficulty setup.
self.min_diff_ui(ui);
});
}
}
impl StratumSetup {
pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
// Draw modal content for current ui container.
self.current_modal_ui(ui, cb);
/// Show wallet selection [`Modal`].
fn show_wallets_modal(&mut self) {
self.wallets_modal = WalletListModal::new(NodeConfig::get_stratum_wallet_id(), None, false);
// Show modal.
Modal::new(WALLET_SELECTION_MODAL)
.position(ModalPosition::Center)
.title(t!("network_settings.choose_wallet"))
.show();
}
View::sub_title(ui, format!("{} {}", HARD_DRIVES, t!("network_mining.server")));
View::horizontal_line(ui, Colors::stroke());
ui.add_space(6.0);
/// Draw stratum port value setup content.
fn port_setup_ui(&mut self, ui: &mut egui::Ui) {
ui.label(
RichText::new(t!("network_settings.stratum_port"))
.size(16.0)
.color(Colors::gray()),
);
ui.add_space(6.0);
ui.vertical_centered(|ui| {
// Show loading indicator or controls to start/stop stratum server.
if Node::get_sync_status().unwrap_or(SyncStatus::Initial) == SyncStatus::NoSync &&
self.is_port_available && self.wallet_name.is_some() {
if Node::is_stratum_starting() || Node::is_stratum_stopping() {
ui.vertical_centered(|ui| {
ui.add_space(8.0);
View::small_loading_spinner(ui);
ui.add_space(8.0);
});
} else if Node::get_stratum_stats().is_running {
ui.add_space(6.0);
let disable_text = format!("{} {}", POWER, t!("network_settings.disable"));
View::action_button(ui, disable_text, || {
Node::stop_stratum();
let (ip, port) = NodeConfig::get_stratum_address();
self.is_port_available = NodeConfig::is_stratum_port_available(&ip, &port);
});
ui.add_space(6.0);
} else {
ui.add_space(6.0);
let enable_text = format!("{} {}", POWER, t!("network_settings.enable"));
View::action_button(ui, enable_text, || {
Node::start_stratum();
});
ui.add_space(6.0);
}
}
let (_, port) = NodeConfig::get_stratum_address();
View::button(
ui,
format!("{} {}", PLUG, &port),
Colors::white_or_black(false),
|| {
// Setup values for modal.
self.stratum_port_edit = port;
self.stratum_port_available_edit = self.is_port_available;
// Show stratum port modal.
Modal::new(STRATUM_PORT_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("network_settings.change_value"))
.show();
},
);
ui.add_space(12.0);
// Show stratum server autorun checkbox.
let stratum_enabled = NodeConfig::is_stratum_autorun_enabled();
View::checkbox(ui, stratum_enabled, t!("network.autorun"), || {
NodeConfig::toggle_stratum_autorun();
});
// Show error when stratum server port is unavailable.
if !self.is_port_available {
ui.add_space(6.0);
ui.label(
RichText::new(t!("network_settings.port_unavailable"))
.size(16.0)
.color(Colors::red()),
);
ui.add_space(12.0);
}
}
// Show reminder to restart running server.
if Node::get_stratum_stats().is_running {
ui.add_space(2.0);
ui.label(RichText::new(t!("network_mining.restart_server_required"))
.size(16.0)
.color(Colors::inactive_text())
);
}
ui.add_space(8.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(8.0);
/// Draw stratum port [`Modal`] content.
fn port_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
let on_save = |c: &mut StratumSetup| {
// Check if port is available.
let (stratum_ip, _) = NodeConfig::get_stratum_address();
let available =
NodeConfig::is_stratum_port_available(&stratum_ip, &c.stratum_port_edit);
c.stratum_port_available_edit = available;
// Show wallet name.
ui.label(RichText::new(self.wallet_name.as_ref().unwrap_or(&"-".to_string()))
.size(16.0)
.color(Colors::white_or_black(true)));
ui.add_space(8.0);
// Save port at config if it's available.
if available {
NodeConfig::save_stratum_address(&stratum_ip, &c.stratum_port_edit);
// Show button to select wallet.
View::button(ui, t!("network_settings.choose_wallet"), Colors::button(), || {
self.show_wallets_modal();
});
ui.add_space(12.0);
c.is_port_available = true;
Modal::close();
}
};
if self.wallet_name.is_some() {
ui.label(RichText::new(t!("network_settings.stratum_wallet_warning"))
.size(16.0)
.color(Colors::inactive_text())
);
ui.add_space(12.0);
}
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(6.0);
});
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(
RichText::new(t!("network_settings.stratum_port"))
.size(17.0)
.color(Colors::gray()),
);
ui.add_space(8.0);
// Show message when IP addresses are not available on the system.
if self.available_ips.is_empty() {
NetworkSettings::no_ip_address_ui(ui);
return;
}
// Draw stratum port text edit.
let mut edit = TextEdit::new(Id::from(modal.id)).h_center().numeric();
edit.ui(ui, &mut self.stratum_port_edit, cb);
if edit.enter_pressed {
on_save(self);
}
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("network_settings.stratum_ip"))
.size(16.0)
.color(Colors::gray())
);
ui.add_space(6.0);
// Show stratum IP addresses to select.
let (ip, port) = NodeConfig::get_stratum_address();
NetworkSettings::ip_addrs_ui(ui, &ip, &self.available_ips, |selected_ip| {
NodeConfig::save_stratum_address(selected_ip, &port);
self.is_port_available = NodeConfig::is_stratum_port_available(selected_ip, &port);
// Show error when specified port is unavailable.
if !self.stratum_port_available_edit {
ui.add_space(12.0);
ui.label(
RichText::new(t!("network_settings.port_unavailable"))
.size(17.0)
.color(Colors::red()),
);
} else {
server_restart_required_ui(ui);
}
});
// Show stratum port setup.
self.port_setup_ui(ui, cb);
ui.add_space(12.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(6.0);
// Show modal buttons.
ui.scope(|ui| {
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
// Show attempt time setup.
self.attempt_time_ui(ui, cb);
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(
ui,
t!("modal.cancel"),
Colors::white_or_black(false),
|| {
// Close modal.
Modal::close();
},
);
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.save"), Colors::white_or_black(false), || {
on_save(self);
});
});
});
ui.add_space(6.0);
});
});
}
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(6.0);
/// Draw attempt time value setup content.
fn attempt_time_ui(&mut self, ui: &mut egui::Ui) {
ui.label(
RichText::new(t!("network_settings.attempt_time"))
.size(16.0)
.color(Colors::gray()),
);
ui.add_space(6.0);
// Show minimum acceptable share difficulty setup.
self.min_diff_ui(ui, cb);
});
}
let time = NodeConfig::get_stratum_attempt_time();
View::button(
ui,
format!("{} {}", TIMER, &time),
Colors::white_or_black(false),
|| {
// Setup values for modal.
self.attempt_time_edit = time;
/// Show wallet selection [`Modal`].
fn show_wallets_modal(&mut self) {
self.wallets_modal = WalletsModal::new(NodeConfig::get_stratum_wallet_id());
// Show modal.
Modal::new(WALLET_SELECTION_MODAL)
.position(ModalPosition::Center)
.title(t!("network_settings.choose_wallet"))
.show();
}
// Show attempt time modal.
Modal::new(ATTEMPT_TIME_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("network_settings.change_value"))
.show();
},
);
ui.add_space(6.0);
ui.label(
RichText::new(t!("network_settings.attempt_time_desc"))
.size(16.0)
.color(Colors::inactive_text()),
);
ui.add_space(6.0);
}
/// Draw stratum port value setup content.
fn port_setup_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
ui.label(RichText::new(t!("network_settings.stratum_port"))
.size(16.0)
.color(Colors::gray())
);
ui.add_space(6.0);
/// Draw attempt time [`Modal`] content.
fn attempt_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
let on_save = |c: &mut StratumSetup| {
if let Ok(time) = c.attempt_time_edit.parse::<u32>() {
NodeConfig::save_stratum_attempt_time(time);
Modal::close();
}
};
let (_, port) = NodeConfig::get_stratum_address();
View::button(ui, format!("{} {}", PLUG, port.clone()), Colors::button(), || {
// Setup values for modal.
self.stratum_port_edit = port;
self.stratum_port_available_edit = self.is_port_available;
// Show stratum port modal.
Modal::new(STRATUM_PORT_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("network_settings.change_value"))
.show();
cb.show_keyboard();
});
ui.add_space(12.0);
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(
RichText::new(t!("network_settings.attempt_time"))
.size(17.0)
.color(Colors::gray()),
);
ui.add_space(8.0);
// Show error when stratum server port is unavailable.
if !self.is_port_available {
ui.add_space(6.0);
ui.label(RichText::new(t!("network_settings.port_unavailable"))
.size(16.0)
.color(Colors::red()));
ui.add_space(12.0);
}
}
// Draw attempt time text edit.
let mut edit = TextEdit::new(Id::from(modal.id)).h_center().numeric();
edit.ui(ui, &mut self.attempt_time_edit, cb);
if edit.enter_pressed {
on_save(self);
}
/// Draw stratum port [`Modal`] content.
fn port_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("network_settings.stratum_port"))
.size(17.0)
.color(Colors::gray()));
ui.add_space(8.0);
// Show error when specified value is not valid or reminder to restart enabled node.
if self.attempt_time_edit.parse::<u32>().is_err() {
ui.add_space(12.0);
ui.label(
RichText::new(t!("network_settings.not_valid_value"))
.size(17.0)
.color(Colors::red()),
);
} else {
server_restart_required_ui(ui);
}
ui.add_space(12.0);
});
// Draw stratum port text edit.
let mut text_edit_opts = TextEditOptions::new(Id::from(modal.id)).h_center();
View::text_edit(ui, cb, &mut self.stratum_port_edit, &mut text_edit_opts);
// Show modal buttons.
ui.scope(|ui| {
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
// Show error when specified port is unavailable.
if !self.stratum_port_available_edit {
ui.add_space(12.0);
ui.label(RichText::new(t!("network_settings.port_unavailable"))
.size(17.0)
.color(Colors::red()));
} else {
server_restart_required_ui(ui);
}
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(
ui,
t!("modal.cancel"),
Colors::white_or_black(false),
|| {
// Close modal.
Modal::close();
},
);
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.save"), Colors::white_or_black(false), || {
on_save(self);
});
});
});
ui.add_space(6.0);
});
}
ui.add_space(12.0);
/// Draw minimum share difficulty value setup content.
fn min_diff_ui(&mut self, ui: &mut egui::Ui) {
ui.label(
RichText::new(t!("network_settings.min_share_diff"))
.size(16.0)
.color(Colors::gray()),
);
ui.add_space(6.0);
// Show modal buttons.
ui.scope(|ui| {
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
let diff = NodeConfig::get_stratum_min_share_diff();
View::button(
ui,
format!("{} {}", BARBELL, &diff),
Colors::white_or_black(false),
|| {
// Setup values for modal.
self.min_share_diff_edit = diff;
// Save button callback.
let on_save = || {
// Check if port is available.
let (stratum_ip, _) = NodeConfig::get_stratum_address();
let available = NodeConfig::is_stratum_port_available(
&stratum_ip,
&self.stratum_port_edit
);
self.stratum_port_available_edit = available;
// Show share difficulty setup modal.
Modal::new(MIN_SHARE_DIFF_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("network_settings.change_value"))
.show();
},
);
ui.add_space(6.0);
}
// Save port at config if it's available.
if available {
NodeConfig::save_stratum_address(&stratum_ip, &self.stratum_port_edit);
/// Draw minimum acceptable share difficulty [`Modal`] content.
fn min_diff_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
let on_save = |c: &mut StratumSetup| {
if let Ok(diff) = c.min_share_diff_edit.parse::<u64>() {
NodeConfig::save_stratum_min_share_diff(diff);
Modal::close();
}
};
self.is_port_available = true;
cb.hide_keyboard();
modal.close();
}
};
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(
RichText::new(t!("network_settings.min_share_diff"))
.size(17.0)
.color(Colors::gray()),
);
ui.add_space(8.0);
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
// Close modal.
cb.hide_keyboard();
modal.close();
});
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.save"), Colors::white_or_black(false), on_save);
});
});
ui.add_space(6.0);
});
});
}
// Draw share difficulty text edit.
let mut edit = TextEdit::new(Id::from(modal.id)).h_center().numeric();
edit.ui(ui, &mut self.min_share_diff_edit, cb);
if edit.enter_pressed {
on_save(self);
}
/// Draw attempt time value setup content.
fn attempt_time_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
ui.label(RichText::new(t!("network_settings.attempt_time"))
.size(16.0)
.color(Colors::gray())
);
ui.add_space(6.0);
// Show error when specified value is not valid or reminder to restart enabled node.
if self.min_share_diff_edit.parse::<u64>().is_err() {
ui.add_space(12.0);
ui.label(
RichText::new(t!("network_settings.not_valid_value"))
.size(17.0)
.color(Colors::red()),
);
} else {
server_restart_required_ui(ui);
}
ui.add_space(12.0);
});
let time = NodeConfig::get_stratum_attempt_time();
View::button(ui, format!("{} {}", TIMER, time.clone()), Colors::button(), || {
// Setup values for modal.
self.attempt_time_edit = time;
// Show modal buttons.
ui.scope(|ui| {
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
// Show attempt time modal.
Modal::new(ATTEMPT_TIME_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("network_settings.change_value"))
.show();
cb.show_keyboard();
});
ui.add_space(6.0);
ui.label(RichText::new(t!("network_settings.attempt_time_desc"))
.size(16.0)
.color(Colors::inactive_text())
);
ui.add_space(6.0);
}
/// Draw attempt time [`Modal`] content.
fn attempt_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("network_settings.attempt_time"))
.size(17.0)
.color(Colors::gray()));
ui.add_space(8.0);
// Draw attempt time text edit.
let mut text_edit_opts = TextEditOptions::new(Id::from(modal.id)).h_center();
View::text_edit(ui, cb, &mut self.attempt_time_edit, &mut text_edit_opts);
// Show error when specified value is not valid or reminder to restart enabled node.
if self.attempt_time_edit.parse::<u32>().is_err() {
ui.add_space(12.0);
ui.label(RichText::new(t!("network_settings.not_valid_value"))
.size(17.0)
.color(Colors::red()));
} else {
server_restart_required_ui(ui);
}
ui.add_space(12.0);
});
// Show modal buttons.
ui.scope(|ui| {
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
// Save button callback.
let on_save = || {
if let Ok(time) = self.attempt_time_edit.parse::<u32>() {
NodeConfig::save_stratum_attempt_time(time);
cb.hide_keyboard();
modal.close();
}
};
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
// Close modal.
cb.hide_keyboard();
modal.close();
});
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.save"), Colors::white_or_black(false), on_save);
});
});
ui.add_space(6.0);
});
}
/// Draw minimum share difficulty value setup content.
fn min_diff_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
ui.label(RichText::new(t!("network_settings.min_share_diff"))
.size(16.0)
.color(Colors::gray())
);
ui.add_space(6.0);
let diff = NodeConfig::get_stratum_min_share_diff();
View::button(ui, format!("{} {}", BARBELL, diff.clone()), Colors::button(), || {
// Setup values for modal.
self.min_share_diff_edit = diff;
// Show share difficulty setup modal.
Modal::new(MIN_SHARE_DIFF_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("network_settings.change_value"))
.show();
cb.show_keyboard();
});
ui.add_space(6.0);
}
/// Draw minimum acceptable share difficulty [`Modal`] content.
fn min_diff_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("network_settings.min_share_diff"))
.size(17.0)
.color(Colors::gray()));
ui.add_space(8.0);
// Draw share difficulty text edit.
let mut text_edit_opts = TextEditOptions::new(Id::from(modal.id)).h_center();
View::text_edit(ui, cb, &mut self.min_share_diff_edit, &mut text_edit_opts);
// Show error when specified value is not valid or reminder to restart enabled node.
if self.min_share_diff_edit.parse::<u64>().is_err() {
ui.add_space(12.0);
ui.label(RichText::new(t!("network_settings.not_valid_value"))
.size(17.0)
.color(Colors::red()));
} else {
server_restart_required_ui(ui);
}
ui.add_space(12.0);
});
// Show modal buttons.
ui.scope(|ui| {
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
// Save button callback.
let on_save = || {
if let Ok(diff) = self.min_share_diff_edit.parse::<u64>() {
NodeConfig::save_stratum_min_share_diff(diff);
cb.hide_keyboard();
modal.close();
}
};
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
// Close modal.
cb.hide_keyboard();
modal.close();
});
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.save"), Colors::white_or_black(false), on_save);
});
});
ui.add_space(6.0);
});
}
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(
ui,
t!("modal.cancel"),
Colors::white_or_black(false),
|| {
// Close modal.
Modal::close();
},
);
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.save"), Colors::white_or_black(false), || {
on_save(self);
});
});
});
ui.add_space(6.0);
});
}
}
/// Reminder to restart enabled node to show on edit setting at [`Modal`].
pub fn server_restart_required_ui(ui: &mut egui::Ui) {
if Node::get_stratum_stats().is_running {
ui.add_space(12.0);
ui.label(RichText::new(t!("network_mining.restart_server_required"))
.size(16.0)
.color(Colors::green())
);
}
}
if Node::get_stratum_stats().is_running {
ui.add_space(12.0);
ui.label(
RichText::new(t!("network_mining.restart_server_required"))
.size(16.0)
.color(Colors::green()),
);
}
}
+30 -20
View File
@@ -13,29 +13,39 @@
// limitations under the License.
use crate::gui::platform::PlatformCallbacks;
use serde_derive::{Deserialize, Serialize};
/// Network tab content interface.
pub trait NetworkTab {
fn get_type(&self) -> NetworkTabType;
fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks);
/// Integrated node tab content interface.
pub trait NodeTab {
fn get_type(&self) -> NodeTabType;
fn tab_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks);
}
/// Type of [`NetworkTab`] content.
/// Type of [`NodeTab`] content.
#[derive(PartialEq)]
pub enum NetworkTabType {
Node,
Metrics,
Mining,
Settings
pub enum NodeTabType {
Info,
Metrics,
Mining,
Settings,
}
impl NetworkTabType {
pub fn title(&self) -> String {
match *self {
NetworkTabType::Node => { t!("network.node") }
NetworkTabType::Metrics => { t!("network.metrics") }
NetworkTabType::Mining => { t!("network.mining") }
NetworkTabType::Settings => { t!("network.settings") }
}
}
}
impl NodeTabType {
pub fn title(&self) -> String {
match *self {
NodeTabType::Info => t!("network.node").into(),
NodeTabType::Metrics => t!("network.metrics").into(),
NodeTabType::Mining => t!("network.mining").into(),
NodeTabType::Settings => t!("network.settings").into(),
}
}
}
/// Connection details to share.
#[derive(Serialize, Deserialize, Clone)]
pub struct ShareConnection {
#[serde(rename(serialize = "ipPort", deserialize = "ipPort"))]
pub url: String,
pub username: String,
pub secret: String,
}
+297 -286
View File
@@ -12,359 +12,370 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use egui::epaint::{Pos2, Shape, Stroke, emath::lerp, vec2};
use egui::scroll_area::ScrollAreaOutput;
use egui::{Sense, Align2, Area, Color32, Id, Rect, Response, Widget, Vec2};
use egui::epaint::{emath::lerp, vec2, Pos2, Shape, Stroke};
use egui::{Align2, Area, Color32, Id, Rect, Response, Sense, UiBuilder, Vec2, Widget};
/// A spinner widget used to indicate loading.
/// This was taken from egui and modified slightly to allow passing a progress value
#[must_use = "You should put this widget in an ui with `ui.add(widget);`"]
#[derive(Default)]
pub struct ProgressSpinner {
/// Uses the style's `interact_size` if `None`.
size: Option<f32>,
color: Option<Color32>,
progress: Option<f64>,
/// Uses the style's `interact_size` if `None`.
size: Option<f32>,
color: Option<Color32>,
progress: Option<f64>,
}
impl ProgressSpinner {
/// Create a new spinner that uses the style's `interact_size` unless changed.
pub fn new() -> Self {
Self::default()
}
/// Create a new spinner that uses the style's `interact_size` unless changed.
pub fn new() -> Self {
Self::default()
}
/// Sets the spinner's size. The size sets both the height and width, as the spinner is always
/// square. If the size isn't set explicitly, the active style's `interact_size` is used.
#[allow(unused)]
pub fn size(mut self, size: f32) -> Self {
self.size = Some(size);
self
}
/// Sets the spinner's size. The size sets both the height and width, as the spinner is always
/// square. If the size isn't set explicitly, the active style's `interact_size` is used.
#[allow(unused)]
pub fn size(mut self, size: f32) -> Self {
self.size = Some(size);
self
}
/// Sets the spinner's color.
pub fn color(mut self, color: impl Into<Color32>) -> Self {
self.color = Some(color.into());
self
}
/// Sets the spinner's color.
pub fn color(mut self, color: impl Into<Color32>) -> Self {
self.color = Some(color.into());
self
}
/// Sets the spinner's progress.
/// Should be in the range `[0.0, 1.0]`.
pub fn progress(mut self, progress: impl Into<Option<f64>>) -> Self {
self.progress = progress.into();
self
}
/// Sets the spinner's progress.
/// Should be in the range `[0.0, 1.0]`.
pub fn progress(mut self, progress: impl Into<Option<f64>>) -> Self {
self.progress = progress.into();
self
}
/// Paint the spinner in the given rectangle.
pub fn paint_at(&self, ui: &egui::Ui, rect: Rect) {
if ui.is_rect_visible(rect) {
ui.ctx().request_repaint(); // because it is animated
/// Paint the spinner in the given rectangle.
pub fn paint_at(&self, ui: &egui::Ui, rect: Rect) {
if ui.is_rect_visible(rect) {
ui.ctx().request_repaint(); // because it is animated
let color = self
.color
.unwrap_or_else(|| ui.visuals().strong_text_color());
let radius = (rect.height() / 2.0) - 2.0;
let n_points = 20;
let color = self
.color
.unwrap_or_else(|| ui.visuals().strong_text_color());
let radius = (rect.height() / 2.0) - 2.0;
let n_points = 20;
let (start_angle, end_angle) = if let Some(progress) = self.progress {
let start_angle = 0f64.to_radians();
let end_angle = start_angle + 360f64.to_radians() * progress;
(start_angle, end_angle)
} else {
let time = ui.input(|i| i.time);
let start_angle = time * std::f64::consts::TAU;
let end_angle = start_angle + 240f64.to_radians() * time.sin();
(start_angle, end_angle)
};
let (start_angle, end_angle) = if let Some(progress) = self.progress {
let start_angle = 0f64.to_radians();
let end_angle = start_angle + 360f64.to_radians() * progress;
(start_angle, end_angle)
} else {
let time = ui.input(|i| i.time);
let start_angle = time * std::f64::consts::TAU;
let end_angle = start_angle + 240f64.to_radians() * time.sin();
(start_angle, end_angle)
};
let points: Vec<Pos2> = (0..=n_points)
.map(|i| {
let angle = lerp(start_angle..=end_angle, i as f64 / n_points as f64);
let (sin, cos) = angle.sin_cos();
rect.center() + radius * vec2(cos as f32, sin as f32)
})
.collect();
ui.painter()
.add(Shape::line(points, Stroke::new(3.0, color)));
}
}
let points: Vec<Pos2> = (0..=n_points)
.map(|i| {
let angle = lerp(start_angle..=end_angle, i as f64 / n_points as f64);
let (sin, cos) = angle.sin_cos();
rect.center() + radius * vec2(cos as f32, sin as f32)
})
.collect();
ui.painter()
.add(Shape::line(points, Stroke::new(3.0, color)));
}
}
}
impl Widget for ProgressSpinner {
fn ui(self, ui: &mut egui::Ui) -> Response {
let size = self
.size
.unwrap_or_else(|| ui.style().spacing.interact_size.y);
let (rect, response) = ui.allocate_exact_size(vec2(size, size), Sense::hover());
self.paint_at(ui, rect);
fn ui(self, ui: &mut egui::Ui) -> Response {
let size = self
.size
.unwrap_or_else(|| ui.style().spacing.interact_size.y);
let (rect, response) = ui.allocate_exact_size(vec2(size, size), Sense::hover());
self.paint_at(ui, rect);
response
}
response
}
}
/// The current state of the pull to refresh widget.
#[derive(Debug, Clone)]
pub enum PullToRefreshState {
/// The widget is idle, no refresh is happening.
Idle,
/// The user is dragging.
Dragging {
/// `distance` is the distance the user dragged.
distance: f32,
/// `far_enough` is true if the user dragged far enough to trigger a refresh.
far_enough: bool,
},
/// The user dragged far enough to trigger a refresh and released the pointer.
DoRefresh,
/// The refresh is currently happening.
Refreshing,
/// The widget is idle, no refresh is happening.
Idle,
/// The user is dragging.
Dragging {
/// `distance` is the distance the user dragged.
distance: f32,
/// `far_enough` is true if the user dragged far enough to trigger a refresh.
far_enough: bool,
},
/// The user dragged far enough to trigger a refresh.
DoRefresh,
/// The refresh is currently happening.
Refreshing,
}
impl PullToRefreshState {
fn progress(&self, min_distance: f32) -> Option<f64> {
match self {
PullToRefreshState::Idle => Some(0.0),
PullToRefreshState::Dragging { distance, .. } => {
Some((distance / min_distance).min(1.0).max(0.0) as f64)
}
PullToRefreshState::DoRefresh => Some(1.0),
PullToRefreshState::Refreshing => None,
}
}
fn progress(&self, min_distance: f32) -> Option<f64> {
match self {
PullToRefreshState::Idle => Some(0.0),
PullToRefreshState::Dragging { distance, .. } => {
Some((distance / min_distance).min(1.0).max(0.0) as f64)
}
PullToRefreshState::DoRefresh => Some(1.0),
PullToRefreshState::Refreshing => None,
}
}
}
/// The response of the pull to refresh widget.
#[derive(Debug, Clone)]
pub struct PullToRefreshResponse<T> {
/// Current state of the pull to refresh widget.
pub state: PullToRefreshState,
/// The inner response of the widget you wrapped in [`PullToRefresh::ui`] or [`PullToRefresh::scroll_area_ui`].
pub inner: T,
/// Current state of the pull to refresh widget.
pub state: PullToRefreshState,
/// The inner response of the widget you wrapped in [`PullToRefresh::ui`] or [`PullToRefresh::scroll_area_ui`].
pub inner: T,
}
impl<T> PullToRefreshResponse<T> {
/// Returns true if the user dragged far enough to trigger a refresh.
pub fn should_refresh(&self) -> bool {
matches!(self.state, PullToRefreshState::DoRefresh)
}
/// Returns true if the user dragged far enough to trigger a refresh.
pub fn should_refresh(&self) -> bool {
matches!(self.state, PullToRefreshState::DoRefresh)
}
}
/// A widget that allows the user to pull to refresh.
pub struct PullToRefresh {
id: Id,
loading: bool,
min_refresh_distance: f32,
can_refresh: bool,
id: Id,
loading: bool,
min_refresh_distance: f32,
can_refresh: bool,
}
impl PullToRefresh {
/// Creates a new pull to refresh widget.
/// If `loading` is true, the widget will show the loading indicator.
pub fn new(loading: bool) -> Self {
Self {
id: Id::new("pull_to_refresh"),
loading,
min_refresh_distance: 100.0,
can_refresh: true,
}
}
/// Creates a new pull to refresh widget.
/// If `loading` is true, the widget will show the loading indicator.
pub fn new(loading: bool) -> Self {
Self {
id: Id::new("pull_to_refresh"),
loading,
min_refresh_distance: 100.0,
can_refresh: true,
}
}
/// Sets the minimum distance the user needs to drag to trigger a refresh.
pub fn min_refresh_distance(mut self, min_refresh_distance: f32) -> Self {
self.min_refresh_distance = min_refresh_distance;
self
}
/// Sets the minimum distance the user needs to drag to trigger a refresh.
pub fn min_refresh_distance(mut self, min_refresh_distance: f32) -> Self {
self.min_refresh_distance = min_refresh_distance;
self
}
/// You need to provide a id if you use multiple pull to refresh widgets at once.
pub fn id(mut self, id: Id) -> Self {
self.id = id;
self
}
/// You need to provide a id if you use multiple pull to refresh widgets at once.
pub fn id(mut self, id: Id) -> Self {
self.id = id;
self
}
/// If `can_refresh` is false, pulling will not trigger a refresh.
pub fn can_refresh(mut self, can_refresh: bool) -> Self {
self.can_refresh = can_refresh;
self
}
/// If `can_refresh` is false, pulling will not trigger a refresh.
pub fn can_refresh(mut self, can_refresh: bool) -> Self {
self.can_refresh = can_refresh;
self
}
/// Shows the pull to refresh widget.
/// Note: If you want to use the pull to refresh widget in a scroll area, use [`Self::scroll_area_ui`].
/// You might want to disable text selection via [`egui::style::Interaction`]
/// to avoid conflicts with the drag gesture.
pub fn ui<T>(
self,
ui: &mut egui::Ui,
content: impl FnOnce(&mut egui::Ui) -> T,
) -> PullToRefreshResponse<T> {
let mut child = ui.child_ui(ui.available_rect_before_wrap(), *ui.layout(), None);
/// Shows the pull to refresh widget.
/// Note: If you want to use the pull to refresh widget in a scroll area, use [`Self::scroll_area_ui`].
/// You might want to disable text selection via [`egui::style::Interaction`]
/// to avoid conflicts with the drag gesture.
pub fn ui<T>(
self,
ui: &mut egui::Ui,
content: impl FnOnce(&mut egui::Ui) -> T,
) -> PullToRefreshResponse<T> {
let mut child = ui.new_child(
UiBuilder::new()
.max_rect(ui.available_rect_before_wrap())
.layout(*ui.layout()),
);
let output = content(&mut child);
let output = content(&mut child);
let can_refresh = self.can_refresh;
let state = self.internal_ui(ui, can_refresh, None, child.min_rect());
let can_refresh = self.can_refresh;
let state = self.internal_ui(ui, can_refresh, None, child.min_rect());
PullToRefreshResponse {
state,
inner: output,
}
}
PullToRefreshResponse {
state,
inner: output,
}
}
/// Shows the pull to refresh widget, wrapping a [egui::ScrollArea].
/// Pass the output of the scroll area to the content function.
pub fn scroll_area_ui<T>(
self,
ui: &mut egui::Ui,
content: impl FnOnce(&mut egui::Ui) -> ScrollAreaOutput<T>,
) -> PullToRefreshResponse<ScrollAreaOutput<T>> {
let scroll_output = content(ui);
let content_rect = scroll_output.inner_rect;
let can_refresh = scroll_output.state.offset.y == 0.0 && self.can_refresh;
// This is the id used in the Sense of the scroll area
// I hope this id is stable across egui patches...
let allow_dragged_id = scroll_output.id.with("area");
let state = self.internal_ui(ui, can_refresh, Some(allow_dragged_id), content_rect);
PullToRefreshResponse {
state,
inner: scroll_output,
}
}
/// Shows the pull to refresh widget, wrapping a [egui::ScrollArea].
/// Pass the output of the scroll area to the content function.
pub fn scroll_area_ui<T>(
self,
ui: &mut egui::Ui,
content: impl FnOnce(&mut egui::Ui) -> ScrollAreaOutput<T>,
) -> PullToRefreshResponse<ScrollAreaOutput<T>> {
let scroll_output = content(ui);
let content_rect = scroll_output.inner_rect;
let can_refresh = scroll_output.state.offset.y == 0.0 && self.can_refresh;
// This is the id used in the Sense of the scroll area
// I hope this id is stable across egui patches...
let allow_dragged_id = scroll_output.id.with("area");
let state = self.internal_ui(ui, can_refresh, Some(allow_dragged_id), content_rect);
PullToRefreshResponse {
state,
inner: scroll_output,
}
}
fn internal_ui(
self,
ui: &mut egui::Ui,
can_refresh: bool,
allow_dragged_id: Option<Id>,
content_rect: Rect,
) -> PullToRefreshState {
let last_state = ui.data_mut(|data| {
data.get_temp_mut_or(self.id, PullToRefreshState::Idle)
.clone()
});
fn internal_ui(
self,
ui: &mut egui::Ui,
can_refresh: bool,
allow_dragged_id: Option<Id>,
content_rect: Rect,
) -> PullToRefreshState {
let last_state = ui.data_mut(|data| {
data.get_temp_mut_or(self.id, PullToRefreshState::Idle)
.clone()
});
let mut state = last_state;
if self.loading {
state = PullToRefreshState::Refreshing;
}
let mut state = last_state;
if self.loading {
state = PullToRefreshState::Refreshing;
}
if !self.loading && matches!(state, PullToRefreshState::Refreshing) {
state = PullToRefreshState::Idle;
}
if !self.loading && matches!(state, PullToRefreshState::Refreshing) {
state = PullToRefreshState::Idle;
}
if can_refresh && !self.loading {
let sense = ui.interact(content_rect, self.id, Sense::hover());
if can_refresh && !self.loading {
let sense = ui.interact(content_rect, self.id, Sense::hover());
let is_something_blocking_drag = ui.ctx().dragged_id().is_some()
&& !allow_dragged_id.map_or(false, |id| ui.ctx().is_being_dragged(id));
let is_something_blocking_drag = ui.ctx().dragged_id().is_some()
&& !allow_dragged_id.map_or(false, |id| ui.ctx().is_being_dragged(id));
if sense.contains_pointer() && !is_something_blocking_drag {
let (delta, any_released) = ui.input(|input| {
(
if input.pointer.is_decidedly_dragging() {
Some(input.pointer.delta())
} else {
None
},
input.pointer.any_released(),
)
});
if let Some(delta) = delta {
if matches!(state, PullToRefreshState::Idle) {
state = PullToRefreshState::Dragging {
distance: 0.0,
far_enough: false,
};
}
if let PullToRefreshState::Dragging { distance: drag, .. } = state.clone() {
let dist = drag + delta.y;
state = PullToRefreshState::Dragging {
distance: dist,
far_enough: dist > self.min_refresh_distance,
};
}
} else {
state = PullToRefreshState::Idle;
}
if any_released {
if let PullToRefreshState::Dragging {
far_enough: enough, ..
} = state.clone()
{
if enough {
state = PullToRefreshState::DoRefresh;
} else {
state = PullToRefreshState::Idle;
}
} else {
state = PullToRefreshState::Idle;
}
}
} else {
state = PullToRefreshState::Idle;
}
} else {
state = PullToRefreshState::Idle;
}
if sense.contains_pointer() && !is_something_blocking_drag {
let (delta, any_released) = ui.input(|input| {
(
if input.pointer.is_decidedly_dragging() {
Some(input.pointer.delta())
} else {
None
},
input.pointer.any_released(),
)
});
if let Some(delta) = delta {
if matches!(state, PullToRefreshState::Idle) {
state = PullToRefreshState::Dragging {
distance: 0.0,
far_enough: false,
};
}
if let PullToRefreshState::Dragging { distance: drag, .. } = state.clone() {
let dist = drag + delta.y;
state = PullToRefreshState::Dragging {
distance: dist,
far_enough: dist > self.min_refresh_distance,
};
}
} else {
state = PullToRefreshState::Idle;
}
if any_released {
if let PullToRefreshState::Dragging {
far_enough: enough, ..
} = state.clone()
{
if enough {
state = PullToRefreshState::DoRefresh;
} else {
state = PullToRefreshState::Idle;
}
} else {
state = PullToRefreshState::Idle;
}
} else if let PullToRefreshState::Dragging {
far_enough: enough, ..
} = state.clone()
{
if enough {
state = PullToRefreshState::DoRefresh;
}
}
} else {
state = PullToRefreshState::Idle;
}
} else {
state = PullToRefreshState::Idle;
}
if self.loading {
state = PullToRefreshState::Refreshing;
}
if self.loading {
state = PullToRefreshState::Refreshing;
}
let spinner_size = Vec2::splat(24.0);
let spinner_size = Vec2::splat(24.0);
let progress_for_offset = match &state {
PullToRefreshState::Idle => 0.0,
PullToRefreshState::Dragging { .. } => {
state.progress(self.min_refresh_distance).unwrap_or(1.0)
}
PullToRefreshState::DoRefresh => 1.0,
PullToRefreshState::Refreshing => 1.0,
} as f32;
let progress_for_offset = match &state {
PullToRefreshState::Idle => 0.0,
PullToRefreshState::Dragging { .. } => {
state.progress(self.min_refresh_distance).unwrap_or(1.0)
}
PullToRefreshState::DoRefresh => 1.0,
PullToRefreshState::Refreshing => 1.0,
} as f32;
let anim_progress = ui.ctx().animate_value_with_time(
self.id.with("offset_top"),
progress_for_offset,
ui.style().animation_time,
);
let anim_progress = ui.ctx().animate_value_with_time(
self.id.with("offset_top"),
progress_for_offset,
ui.style().animation_time,
);
let offset_top = -spinner_size.y + spinner_size.y * anim_progress * 2.0;
let offset_top = -spinner_size.y + spinner_size.y * anim_progress * 2.0;
if anim_progress > 0.0 {
Area::new(Id::new("Pull to refresh indicator"))
.fixed_pos(content_rect.center_top())
.pivot(Align2::CENTER_TOP)
.show(ui.ctx(), |ui| {
let (rect, _) = ui.allocate_exact_size(spinner_size, Sense::hover());
if anim_progress > 0.0 {
Area::new(Id::new("Pull to refresh indicator"))
.fixed_pos(content_rect.center_top())
.pivot(Align2::CENTER_TOP)
.show(ui.ctx(), |ui| {
let (rect, _) = ui.allocate_exact_size(spinner_size, Sense::hover());
ui.set_clip_rect(Rect::everything_below(rect.min.y));
ui.set_clip_rect(Rect::everything_below(rect.min.y));
let rect = rect.translate(Vec2::new(0.0, offset_top));
let rect = rect.translate(Vec2::new(0.0, offset_top));
ui.painter().circle(
rect.center(),
spinner_size.x / 1.5,
ui.style().visuals.widgets.inactive.bg_fill,
ui.visuals().widgets.inactive.bg_stroke,
);
ui.painter().circle(
rect.center(),
spinner_size.x / 1.5,
ui.style().visuals.widgets.inactive.bg_fill,
ui.visuals().widgets.inactive.bg_stroke,
);
let mut spinner_color = ui.style().visuals.widgets.inactive.fg_stroke.color;
if anim_progress < 1.0 {
spinner_color = Color32::from_rgba_unmultiplied(
spinner_color.r(),
spinner_color.g(),
spinner_color.b(),
(spinner_color.a() as f32 * 0.7).round() as u8,
);
}
ProgressSpinner::new()
.color(spinner_color)
.progress(state.progress(self.min_refresh_distance))
.paint_at(ui, rect);
});
}
let mut spinner_color = ui.style().visuals.widgets.inactive.fg_stroke.color;
if anim_progress < 1.0 {
spinner_color = Color32::from_rgba_unmultiplied(
spinner_color.r(),
spinner_color.g(),
spinner_color.b(),
(spinner_color.a() as f32 * 0.7).round() as u8,
);
}
ProgressSpinner::new()
.color(spinner_color)
.progress(state.progress(self.min_refresh_distance))
.paint_at(ui, rect);
});
}
ui.data_mut(|data| {
data.insert_temp(self.id, state.clone());
});
ui.data_mut(|data| {
data.insert_temp(self.id, state.clone());
});
state
}
}
state
}
}
+464 -376
View File
@@ -12,426 +12,514 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use egui::epaint::RectShape;
use egui::{SizeHint, TextureHandle, UiBuilder};
use image::codecs::png::{CompressionType, FilterType, PngEncoder};
use image::{ExtendedColorType, ImageEncoder};
use parking_lot::RwLock;
use qrcodegen::QrCode;
use std::mem::size_of;
use std::sync::Arc;
use parking_lot::RwLock;
use std::thread;
use egui::{SizeHint, TextureHandle};
use egui::epaint::RectShape;
use image::{ExtendedColorType, ImageEncoder};
use image::codecs::png::{CompressionType, FilterType, PngEncoder};
use qrcodegen::QrCode;
use crate::gui::Colors;
use crate::gui::icons::IMAGES_SQUARE;
use crate::gui::icons::{COPY, IMAGES_SQUARE};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::types::QrImageState;
use crate::gui::views::View;
use crate::gui::views::types::QrImageState;
/// QR code image from text.
pub struct QrCodeContent {
/// Text to create QR code.
pub(crate) text: String,
/// QR code text.
pub text: String,
/// Flag to show text below QR code.
show_text: bool,
/// Flag to copy text below QR code.
can_copy_text: bool,
/// Flag to draw animated QR with Uniform Resources
/// https://github.com/BlockchainCommons/Research/blob/master/papers/bcr-2020-005-ur.md
animated: bool,
/// Index of current image at animation.
animated_index: Option<usize>,
/// Time of last image draw.
animation_time: Option<i64>,
/// Maximum QR code size.
max_size: f32,
/// Texture handle to show image when created.
texture_handle: Option<TextureHandle>,
/// QR code view data state.
qr_image_state: Arc<RwLock<QrImageState>>,
/// Flag to draw animated QR with Uniform Resources
/// https://github.com/BlockchainCommons/Research/blob/master/papers/bcr-2020-005-ur.md
animated: bool,
/// Index of current image at animation.
animated_index: Option<usize>,
/// Time of last image draw.
animation_time: Option<i64>,
/// Texture handle to show image when created.
texture_handle: Option<TextureHandle>,
/// QR code view data state.
qr_image_state: Arc<RwLock<QrImageState>>,
}
const DEFAULT_QR_SIZE: u32 = 512;
impl QrCodeContent {
pub fn new(text: String, animated: bool) -> Self {
Self {
text,
animated,
animated_index: None,
animation_time: None,
texture_handle: None,
qr_image_state: Arc::new(RwLock::new(QrImageState::default())),
}
}
pub fn new(text: String, animated: bool) -> Self {
Self {
text,
show_text: true,
can_copy_text: true,
max_size: DEFAULT_QR_SIZE as f32,
animated,
animated_index: None,
animation_time: None,
texture_handle: None,
qr_image_state: Arc::new(RwLock::new(QrImageState::default())),
}
}
/// Draw QR code.
pub fn ui(&mut self, ui: &mut egui::Ui, text: String, cb: &dyn PlatformCallbacks) {
if self.animated {
// Show animated QR code.
self.animated_ui(ui, text, cb);
} else {
// Show static QR code.
self.static_ui(ui, text, cb);
}
}
/// Setup maximum QR code size.
pub fn with_max_size(mut self, max_size: f32) -> Self {
self.max_size = max_size;
self
}
/// Draw animated QR code content.
fn animated_ui(&mut self, ui: &mut egui::Ui, text: String, cb: &dyn PlatformCallbacks) {
if !self.has_image() {
let space = (ui.available_width() - View::BIG_SPINNER_SIZE) / 2.0;
ui.vertical_centered(|ui| {
ui.add_space(space);
View::big_loading_spinner(ui);
ui.add_space(space);
});
/// Hide text below QR code.
pub fn hide_text(mut self) -> Self {
self.show_text = false;
self
}
// Create multiple vector images from text if not creating.
if !self.loading() {
self.create_svg_list(text);
}
} else {
let svg_list = {
let r_create = self.qr_image_state.read();
r_create.svg_list.clone().unwrap()
};
/// Do not show button to copy QR code text.
pub fn no_copy(mut self) -> Self {
self.can_copy_text = false;
self
}
// Setup animated index.
let now = chrono::Utc::now().timestamp_millis();
if now - *self.animation_time.get_or_insert(now) > 100 {
if let Some(i) = self.animated_index {
self.animated_index = Some(i + 1);
}
if *self.animated_index.get_or_insert(0) == svg_list.len() {
self.animated_index = Some(0);
}
self.animation_time = Some(now);
}
/// Draw QR code.
pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
if self.animated {
// Show animated QR code.
self.animated_ui(ui, cb);
} else {
// Show static QR code.
self.static_ui(ui, cb);
}
ui.add_space(8.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(8.0);
}
let svg = svg_list[self.animated_index.unwrap_or(0)].clone();
/// Draw animated QR code content.
fn animated_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
if !self.has_image() {
let space = (ui.available_width() - View::BIG_SPINNER_SIZE) / 2.0;
ui.vertical_centered(|ui| {
ui.add_space(space);
View::big_loading_spinner(ui);
ui.add_space(space);
});
// Create images from SVG data.
self.qr_image_ui(svg, ui);
// Create multiple vector images from text if not creating.
if !self.loading() {
self.create_svg_list();
}
} else {
let svg_list = {
let r_create = self.qr_image_state.read();
r_create.svg_list.clone().unwrap()
};
// Show QR code text.
ui.add_space(6.0);
View::ellipsize_text(ui, text.clone(), 16.0, Colors::inactive_text());
ui.add_space(6.0);
// Setup animated index.
let now = chrono::Utc::now().timestamp_millis();
if now - *self.animation_time.get_or_insert(now) > 100 {
if let Some(i) = self.animated_index {
self.animated_index = Some(i + 1);
}
if *self.animated_index.get_or_insert(0) == svg_list.len() {
self.animated_index = Some(0);
}
self.animation_time = Some(now);
}
ui.vertical_centered(|ui| {
let sharing = {
let r_state = self.qr_image_state.read();
r_state.exporting || r_state.gif_creating
};
if !sharing {
// Show button to share QR.
let share_text = format!("{} {}", IMAGES_SQUARE, t!("share"));
View::colored_text_button(ui,
share_text,
Colors::blue(),
Colors::white_or_black(false), || {
{
let mut w_state = self.qr_image_state.write();
w_state.exporting = true;
}
// Create GIF to export.
self.create_qr_gif(text, DEFAULT_QR_SIZE as usize);
});
} else {
ui.vertical_centered(|ui| {
ui.add_space(2.0);
View::small_loading_spinner(ui);
ui.add_space(1.0);
});
}
let svg = svg_list[self.animated_index.unwrap_or(0)].clone();
ui.add_space(8.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(8.0);
// Create images from SVG data.
self.qr_image_ui(svg, ui);
// Check if GIF was created to share.
let has_gif = {
let r_state = self.qr_image_state.read();
r_state.gif_data.is_some()
};
if has_gif {
let data = {
let r_state = self.qr_image_state.read();
r_state.gif_data.clone().unwrap()
};
let name = format!("{}.gif", chrono::Utc::now().timestamp());
cb.share_data(name, data).unwrap_or_default();
// Clear GIF data and exporting flag.
{
let mut w_state = self.qr_image_state.write();
w_state.gif_data = None;
w_state.exporting = false;
}
}
});
// Show QR code text.
if self.show_text {
self.text_ui(ui);
}
ui.ctx().request_repaint();
}
}
ui.vertical_centered(|ui| {
let sharing = {
let r_state = self.qr_image_state.read();
r_state.exporting || r_state.gif_creating
};
if !sharing {
ui.vertical_centered(|ui| {
// Show button to share QR.
let share_text = format!("{} {}", IMAGES_SQUARE, t!("share"));
View::colored_text_button(
ui,
share_text,
Colors::blue(),
Colors::white_or_black(false),
|| {
{
let mut w_state = self.qr_image_state.write();
w_state.exporting = true;
}
// Create GIF to export.
self.create_qr_gif();
},
);
});
} else {
ui.vertical_centered(|ui| {
ui.add_space(2.0);
View::small_loading_spinner(ui);
ui.add_space(1.0);
});
}
/// Draw static QR code content.
fn static_ui(&mut self, ui: &mut egui::Ui, text: String, cb: &dyn PlatformCallbacks) {
if !self.has_image() {
let space = (ui.available_width() - View::BIG_SPINNER_SIZE) / 2.0;
ui.vertical_centered(|ui| {
ui.add_space(space);
View::big_loading_spinner(ui);
ui.add_space(space);
});
// Check if GIF was created to share.
let has_gif = {
let r_state = self.qr_image_state.read();
r_state.gif_data.is_some()
};
if has_gif {
let data = {
let r_state = self.qr_image_state.read();
r_state.gif_data.clone().unwrap()
};
let name = format!("{}.gif", chrono::Utc::now().timestamp());
cb.share_data(name, data).unwrap_or_default();
// Clear GIF data and exporting flag.
{
let mut w_state = self.qr_image_state.write();
w_state.gif_data = None;
w_state.exporting = false;
}
}
});
// Create vector image from text if not creating.
if !self.loading() {
self.create_svg(text);
}
} else {
// Create image from SVG data.
let svg = {
let r_state = self.qr_image_state.read();
r_state.svg.clone().unwrap()
};
self.qr_image_ui(svg, ui);
ui.ctx().request_repaint();
}
}
// Show QR code text.
ui.add_space(6.0);
View::ellipsize_text(ui, text.clone(), 16.0, Colors::inactive_text());
ui.add_space(6.0);
/// Draw static QR code content.
fn static_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
if !self.has_image() {
let space = (ui.available_width() - View::BIG_SPINNER_SIZE) / 2.0;
ui.vertical_centered(|ui| {
ui.add_space(space);
View::big_loading_spinner(ui);
ui.add_space(space);
});
// Show button to share QR.
ui.vertical_centered(|ui| {
let share_text = format!("{} {}", IMAGES_SQUARE, t!("share"));
View::colored_text_button(ui,
share_text,
Colors::blue(),
Colors::white_or_black(false), || {
if let Ok(qr) = QrCode::encode_text(text.as_str(), qrcodegen::QrCodeEcc::Low) {
if let Some(data) = Self::qr_to_image_data(qr, DEFAULT_QR_SIZE as usize) {
let mut png = vec![];
let png_enc = PngEncoder::new_with_quality(&mut png,
CompressionType::Best,
FilterType::NoFilter);
if let Ok(()) = png_enc.write_image(data.as_slice(),
DEFAULT_QR_SIZE,
DEFAULT_QR_SIZE,
ExtendedColorType::L8) {
let name = format!("{}.png", chrono::Utc::now().timestamp());
cb.share_data(name, png).unwrap_or_default();
}
}
}
});
});
ui.add_space(8.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(8.0);
}
}
// Create vector image from text if not creating.
if !self.loading() {
self.create_svg();
}
} else {
// Create image from SVG data.
let svg = {
let r_state = self.qr_image_state.read();
r_state.svg.clone().unwrap()
};
self.qr_image_ui(svg, ui);
/// Draw QR code image content.
fn qr_image_ui(&mut self, svg: Vec<u8>, ui: &mut egui::Ui) {
let mut rect = ui.available_rect_before_wrap();
rect.min += egui::emath::vec2(10.0, 0.0);
rect.max -= egui::emath::vec2(10.0, 0.0);
// Show QR code text.
if self.show_text {
self.text_ui(ui);
} else {
ui.add_space(8.0);
}
// Create background shape.
let mut bg_shape = RectShape {
rect,
rounding: egui::Rounding::default(),
fill: egui::Color32::WHITE,
stroke: egui::Stroke::NONE,
blur_width: 0.0,
fill_texture_id: Default::default(),
uv: egui::Rect::ZERO
};
let bg_idx = ui.painter().add(bg_shape);
if self.can_copy_text {
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(6.0, 0.0);
// Draw QR code image content.
let mut content_rect = ui.allocate_ui_at_rect(rect, |ui| {
ui.add_space(10.0);
let size = SizeHint::Size(ui.available_width() as u32, ui.available_width() as u32);
self.texture_handle = Some(View::svg_image(ui, "qr_code", svg.as_slice(), Some(size)));
ui.add_space(10.0);
}).response.rect;
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
// Draw copy button.
let copy_text = format!("{} {}", COPY, t!("copy"));
View::button(ui, copy_text, Colors::white_or_black(false), || {
cb.copy_string_to_buffer(self.text.clone());
});
});
columns[1].vertical_centered_justified(|ui| {
self.share_static_button_ui(ui, cb);
});
});
} else {
ui.vertical_centered(|ui| {
self.share_static_button_ui(ui, cb);
});
}
}
}
// Setup background shape to be painted behind content.
content_rect.min -= egui::emath::vec2(10.0, 0.0);
content_rect.max += egui::emath::vec2(10.0, 0.0);
bg_shape.rect = content_rect;
ui.painter().set(bg_idx, bg_shape);
}
/// Draw button to share static QR code.
fn share_static_button_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
let share_text = format!("{} {}", IMAGES_SQUARE, t!("share"));
View::colored_text_button(
ui,
share_text,
Colors::blue(),
Colors::white_or_black(false),
|| {
self.share_static(cb);
},
);
}
/// Check if QR code is loading.
fn loading(&self) -> bool {
let r_state = self.qr_image_state.read();
r_state.loading
}
/// Share static QR code image.
fn share_static(&self, cb: &dyn PlatformCallbacks) {
let text = self.text.as_str();
if let Ok(qr) = QrCode::encode_text(text, qrcodegen::QrCodeEcc::Low) {
let size = DEFAULT_QR_SIZE as usize;
if let Some(data) = Self::qr_to_image_data(qr, size) {
let mut png = vec![];
let png_enc = PngEncoder::new_with_quality(
&mut png,
CompressionType::Best,
FilterType::NoFilter,
);
if let Ok(()) = png_enc.write_image(
data.as_slice(),
DEFAULT_QR_SIZE,
DEFAULT_QR_SIZE,
ExtendedColorType::L8,
) {
let name = format!("{}.png", chrono::Utc::now().timestamp());
cb.share_data(name, png).unwrap_or_default();
}
}
}
}
/// Create multiple vector QR code images at separate thread.
fn create_svg_list(&self, text: String) {
let qr_state = self.qr_image_state.clone();
thread::spawn(move || {
let mut encoder = ur::Encoder::bytes(text.as_bytes(), 100).unwrap();
let mut data = Vec::with_capacity(encoder.fragment_count());
for _ in 0..encoder.fragment_count() {
let ur = encoder.next_part().unwrap();
if let Ok(qr) = QrCode::encode_text(ur.as_str(), qrcodegen::QrCodeEcc::Low) {
let svg = Self::qr_to_svg(qr, 0);
data.push(svg.into_bytes());
}
}
let mut w_state = qr_state.write();
if !data.is_empty() {
w_state.svg_list = Some(data);
}
w_state.loading = false;
});
}
/// Draw QR code image content.
fn qr_image_ui(&mut self, svg: Vec<u8>, ui: &mut egui::Ui) {
View::max_width_ui(ui, self.max_size, |ui| {
let mut rect = ui.available_rect_before_wrap();
rect.min += egui::emath::vec2(10.0, 0.0);
rect.max -= egui::emath::vec2(10.0, 0.0);
/// Check if image was created.
fn has_image(&self) -> bool {
let r_state = self.qr_image_state.read();
r_state.svg.is_some() || r_state.svg_list.is_some()
}
// Create background shape.
let mut bg_shape = RectShape::new(
rect,
egui::CornerRadius::default(),
egui::Color32::WHITE,
egui::Stroke::NONE,
egui::StrokeKind::Outside,
);
let bg_idx = ui.painter().add(bg_shape.clone());
/// Create vector QR code image at separate thread.
fn create_svg(&self, text: String) {
let qr_state = self.qr_image_state.clone();
thread::spawn(move || {
if let Ok(qr) = QrCode::encode_text(text.as_str(), qrcodegen::QrCodeEcc::Low) {
let svg = Self::qr_to_svg(qr, 0);
let mut w_state = qr_state.write();
w_state.loading = false;
w_state.svg = Some(svg.into_bytes());
}
});
}
// Draw QR code image.
let mut content_rect = ui
.scope_builder(UiBuilder::new().max_rect(rect), |ui| {
ui.add_space(10.0);
let size = SizeHint::Size {
width: ui.available_width() as u32,
height: ui.available_width() as u32,
maintain_aspect_ratio: true,
};
self.texture_handle = Some(View::svg_image(ui, "qr", svg.as_slice(), size));
ui.add_space(10.0);
})
.response
.rect;
/// Convert QR code to SVG string.
fn qr_to_svg(qr: QrCode, border: i32) -> String {
let mut result = String::new();
let dimension = qr.size().checked_add(border.checked_mul(2).unwrap()).unwrap();
result += "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
result += "<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n";
result += &format!(
"<svg xmlns=\"http://www.w3.org/2000/svg\" version=\"1.1\" viewBox=\"0 0 {0} {0}\" stroke=\"none\">\n", dimension);
result += "\t<rect width=\"100%\" height=\"100%\" fill=\"#FFFFFF\"/>\n";
result += "\t<path d=\"";
for y in 0 .. qr.size() {
for x in 0 .. qr.size() {
if qr.get_module(x, y) {
if x != 0 || y != 0 {
result += " ";
}
result += &format!("M{},{}h1v1h-1z", x + border, y + border);
}
}
}
result += "\" fill=\"#000000\"/>\n";
result += "</svg>\n";
result
}
// Setup background size.
content_rect.min -= egui::emath::vec2(10.0, 0.0);
content_rect.max += egui::emath::vec2(10.0, 0.0);
bg_shape.rect = content_rect;
ui.painter().set(bg_idx, bg_shape);
});
}
/// Create GIF image at separate thread.
fn create_qr_gif(&self, text: String, size: usize) {
{
let mut w_state = self.qr_image_state.write();
w_state.gif_creating = true;
/// Draw QR code text.
fn text_ui(&self, ui: &mut egui::Ui) {
ui.add_space(6.0);
View::ellipsize_text(ui, self.text.clone(), 15.0, Colors::inactive_text());
ui.add_space(6.0);
}
}
let qr_state = self.qr_image_state.clone();
thread::spawn(move || {
// Setup GIF image encoder.
let mut gif = vec![];
{
// Generate QR codes from text.
let mut qrs = vec![];
let mut ur_enc = ur::Encoder::bytes(text.as_bytes(), 100).unwrap();
for _ in 0..ur_enc.fragment_count() {
let ur = ur_enc.next_part().unwrap();
if let Ok(qr) = qrcode::QrCode::with_error_correction_level(
ur.as_bytes(),
qrcode::EcLevel::L
) {
// Create an image from QR data.
let image = qr.render()
.max_dimensions(size as u32, size as u32)
.dark_color(image::Rgb([0, 0, 0]))
.light_color(image::Rgb([255, 255, 255]))
.build();
qrs.push(image);
}
}
/// Check if QR code is loading.
fn loading(&self) -> bool {
let r_state = self.qr_image_state.read();
r_state.loading
}
if !qrs.is_empty() {
// Generate GIF data.
let color_map = &[0, 0, 0, 0xFF, 0xFF, 0xFF];
let mut gif_enc = gif::Encoder::new(&mut gif,
qrs[0].width() as u16,
qrs[0].height() as u16,
color_map).unwrap();
gif_enc.set_repeat(gif::Repeat::Infinite).unwrap();
for qr in qrs {
let mut frame = gif::Frame::from_rgb(qr.width() as u16,
qr.height() as u16,
qr.as_raw().as_slice());
frame.delay = 10;
// Write an image to GIF encoder.
if let Ok(_) = gif_enc.write_frame(&frame) {
continue;
}
// Exit on error.
let mut w_state = qr_state.write();
w_state.gif_creating = false;
return;
}
}
}
// Setup GIF image data.
let mut w_state = qr_state.write();
if !gif.is_empty() {
w_state.gif_data = Some(gif);
}
w_state.gif_creating = false;
});
}
/// Create multiple vector QR code images at separate thread.
fn create_svg_list(&self) {
let qr_state = self.qr_image_state.clone();
let text = self.text.clone();
thread::spawn(move || {
let mut encoder = ur::Encoder::bytes(text.as_bytes(), 64).unwrap();
let mut data = Vec::with_capacity(encoder.fragment_count());
for _ in 0..encoder.fragment_count() {
let ur = encoder.next_part().unwrap();
if let Ok(qr) = QrCode::encode_text(ur.as_str(), qrcodegen::QrCodeEcc::Low) {
let svg = Self::qr_to_svg(qr, 0);
data.push(svg.into_bytes());
}
}
let mut w_state = qr_state.write();
if !data.is_empty() {
w_state.svg_list = Some(data);
}
w_state.loading = false;
});
}
/// Convert QR code to image data.
fn qr_to_image_data(qr: QrCode, size: usize) -> Option<Vec<u8>> {
if size >= 2usize.pow((size_of::<usize>() * 4) as u32) {
return None;
}
let margin_size = 1;
let s = qr.size();
let data_length = s as usize;
let data_length_with_margin = data_length + 2 * margin_size;
let point_size = size / data_length_with_margin;
if point_size == 0 {
return None;
}
let margin = (size - (point_size * data_length)) / 2;
let length = size * size;
let mut img_raw: Vec<u8> = vec![255u8; length];
for i in 0..s {
for j in 0..s {
if qr.get_module(i, j) {
let x = i as usize * point_size + margin;
let y = j as usize * point_size + margin;
/// Check if image was created.
fn has_image(&self) -> bool {
let r_state = self.qr_image_state.read();
r_state.svg.is_some() || r_state.svg_list.is_some()
}
for j in y..(y + point_size) {
let offset = j * size;
for i in x..(x + point_size) {
img_raw[offset + i] = 0;
}
}
}
}
}
Some(img_raw)
}
/// Create vector QR code image at separate thread.
fn create_svg(&self) {
let qr_state = self.qr_image_state.clone();
let text = self.text.clone();
thread::spawn(move || {
if let Ok(qr) = QrCode::encode_text(text.as_str(), qrcodegen::QrCodeEcc::Low) {
let svg = Self::qr_to_svg(qr, 0);
let mut w_state = qr_state.write();
w_state.loading = false;
w_state.svg = Some(svg.into_bytes());
}
});
}
/// Reset QR code image content state to default.
pub fn clear_state(&mut self) {
let mut w_create = self.qr_image_state.write();
*w_create = QrImageState::default();
}
}
/// Convert QR code to SVG string.
fn qr_to_svg(qr: QrCode, border: i32) -> String {
let mut result = String::new();
let dimension = qr
.size()
.checked_add(border.checked_mul(2).unwrap())
.unwrap();
result += "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
result += "<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n";
result += &format!(
"<svg xmlns=\"http://www.w3.org/2000/svg\" version=\"1.1\" viewBox=\"0 0 {0} {0}\" stroke=\"none\">\n",
dimension
);
result += "\t<rect width=\"100%\" height=\"100%\" fill=\"#FFFFFF\"/>\n";
result += "\t<path d=\"";
for y in 0..qr.size() {
for x in 0..qr.size() {
if qr.get_module(x, y) {
if x != 0 || y != 0 {
result += " ";
}
result += &format!("M{},{}h1v1h-1z", x + border, y + border);
}
}
}
result += "\" fill=\"#000000\"/>\n";
result += "</svg>\n";
result
}
/// Create GIF image at separate thread.
fn create_qr_gif(&self) {
{
let mut w_state = self.qr_image_state.write();
w_state.gif_creating = true;
}
let qr_state = self.qr_image_state.clone();
let text = self.text.clone();
thread::spawn(move || {
// Setup GIF image encoder.
let mut gif = vec![];
{
// Generate QR codes from text.
let mut qrs = vec![];
let mut ur_enc = ur::Encoder::bytes(text.as_bytes(), 100).unwrap();
for _ in 0..ur_enc.fragment_count() {
let ur = ur_enc.next_part().unwrap();
if let Ok(qr) = qrcode::QrCode::with_error_correction_level(
ur.as_bytes(),
qrcode::EcLevel::L,
) {
// Create an image from QR data.
let image = qr
.render()
.max_dimensions(DEFAULT_QR_SIZE, DEFAULT_QR_SIZE)
.dark_color(image::Rgb([0, 0, 0]))
.light_color(image::Rgb([255, 255, 255]))
.build();
qrs.push(image);
}
}
if !qrs.is_empty() {
// Generate GIF data.
let color_map = &[0, 0, 0, 0xFF, 0xFF, 0xFF];
let mut gif_enc = gif::Encoder::new(
&mut gif,
qrs[0].width() as u16,
qrs[0].height() as u16,
color_map,
)
.unwrap();
gif_enc.set_repeat(gif::Repeat::Infinite).unwrap();
for qr in qrs {
let mut frame = gif::Frame::from_rgb(
qr.width() as u16,
qr.height() as u16,
qr.as_raw().as_slice(),
);
frame.delay = 10;
// Write an image to GIF encoder.
if let Ok(_) = gif_enc.write_frame(&frame) {
continue;
}
// Exit on error.
let mut w_state = qr_state.write();
w_state.gif_creating = false;
return;
}
}
}
// Setup GIF image data.
let mut w_state = qr_state.write();
if !gif.is_empty() {
w_state.gif_data = Some(gif);
}
w_state.gif_creating = false;
});
}
/// Convert QR code to image data.
fn qr_to_image_data(qr: QrCode, size: usize) -> Option<Vec<u8>> {
if size >= 2usize.pow((size_of::<usize>() * 4) as u32) {
return None;
}
let margin_size = 1;
let s = qr.size();
let data_length = s as usize;
let data_length_with_margin = data_length + 2 * margin_size;
let point_size = size / data_length_with_margin;
if point_size == 0 {
return None;
}
let margin = (size - (point_size * data_length)) / 2;
let length = size * size;
let mut img_raw: Vec<u8> = vec![255u8; length];
for i in 0..s {
for j in 0..s {
if qr.get_module(i, j) {
let x = i as usize * point_size + margin;
let y = j as usize * point_size + margin;
for j in y..(y + point_size) {
let offset = j * size;
for i in x..(x + point_size) {
img_raw[offset + i] = 0;
}
}
}
}
}
Some(img_raw)
}
}
+152
View File
@@ -0,0 +1,152 @@
// Copyright 2024 The Grim Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use egui::scroll_area::ScrollBarVisibility;
use egui::{Id, ScrollArea};
use crate::gui::Colors;
use crate::gui::icons::COPY;
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::types::QrScanResult;
use crate::gui::views::{CameraContent, Modal, View};
/// QR code scanning content.
pub struct CameraScanContent {
/// Camera content.
camera_content: Option<CameraContent>,
/// Scan result.
qr_scan_result: Option<QrScanResult>,
}
impl Default for CameraScanContent {
fn default() -> Self {
Self {
camera_content: Some(CameraContent::default()),
qr_scan_result: None,
}
}
}
impl CameraScanContent {
/// Draw [`Modal`] content.
pub fn modal_ui(
&mut self,
ui: &mut egui::Ui,
cb: &dyn PlatformCallbacks,
mut on_result: impl FnMut(&QrScanResult),
) {
// Show scan result if exists or show camera content while scanning.
if let Some(result) = &self.qr_scan_result.clone() {
Self::result_ui(
ui,
result,
cb,
|| {
Modal::close();
},
|| {
self.qr_scan_result = None;
cb.start_camera();
Modal::set_title(t!("scan_qr"));
},
);
} else if let Some(camera_content) = self.camera_content.as_mut() {
if let Some(result) = camera_content.qr_scan_result() {
cb.stop_camera();
self.camera_content = None;
on_result(&result);
// Set result and rename modal title.
self.qr_scan_result = Some(result);
Modal::set_title(t!("scan_result"));
} else {
// Draw camera content.
ui.add_space(6.0);
self.camera_content.as_mut().unwrap().ui(ui, cb);
ui.add_space(12.0);
ui.vertical_centered_justified(|ui| {
View::button(
ui,
t!("modal.cancel"),
Colors::white_or_black(false),
|| {
cb.stop_camera();
self.camera_content = None;
Modal::close();
},
);
});
}
}
ui.add_space(6.0);
}
/// Draw scan result content.
pub fn result_ui(
ui: &mut egui::Ui,
result: &QrScanResult,
cb: &dyn PlatformCallbacks,
on_close: impl FnOnce(),
on_repeat: impl FnOnce(),
) {
let mut result_text = result.text();
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(3.0);
ScrollArea::vertical()
.id_salt(Id::from("qr_scan_result_input"))
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.max_height(128.0)
.auto_shrink([false; 2])
.show(ui, |ui| {
ui.add_space(7.0);
egui::TextEdit::multiline(&mut result_text)
.font(egui::TextStyle::Small)
.desired_rows(5)
.interactive(false)
.desired_width(f32::INFINITY)
.show(ui);
ui.add_space(6.0);
});
ui.add_space(2.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(10.0);
// Show copy button.
ui.vertical_centered(|ui| {
let copy_text = format!("{} {}", COPY, t!("copy"));
View::button(ui, copy_text, Colors::white_or_black(false), || {
cb.copy_string_to_buffer(result_text.to_string());
});
});
ui.add_space(10.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(6.0);
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(ui, t!("close"), Colors::white_or_black(false), || {
on_close();
});
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("repeat"), Colors::white_or_black(false), || {
on_repeat();
});
});
});
}
}
+83
View File
@@ -0,0 +1,83 @@
// Copyright 2025 The Grim Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use crate::AppConfig;
use crate::gui::Colors;
use crate::gui::icons::GLOBE_SIMPLE;
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::View;
use crate::gui::views::settings::{InterfaceSettingsContent, NetworkSettingsContent};
use crate::gui::views::types::ContentContainer;
/// Application settings content.
pub struct SettingsContent {
/// User interface settings.
interface_settings: InterfaceSettingsContent,
/// Network communication settings.
network_settings: NetworkSettingsContent,
// tor_settings: TorSettingsContent,
}
impl Default for SettingsContent {
fn default() -> Self {
Self {
interface_settings: InterfaceSettingsContent::default(),
network_settings: NetworkSettingsContent::default(),
//tor_settings: TorSettingsContent::default(),
}
}
}
impl SettingsContent {
/// Draw application settings content.
pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
ui.add_space(5.0);
View::checkbox(ui, AppConfig::check_updates(), t!("check_updates"), || {
AppConfig::toggle_check_updates();
});
ui.add_space(6.0);
View::horizontal_line(ui, Colors::stroke());
// Show interface settings.
self.interface_settings.ui(ui, cb);
ui.add_space(8.0);
View::horizontal_line(ui, Colors::stroke());
ui.add_space(6.0);
View::sub_title(ui, format!("{} {}", GLOBE_SIMPLE, t!("network.self")));
View::horizontal_line(ui, Colors::stroke());
ui.add_space(6.0);
// Show network settings.
self.network_settings.ui(ui, cb);
ui.add_space(8.0);
// Do not show Tor settings on Android.
// let os = OperatingSystem::from_target_os();
// let show_tor = os != OperatingSystem::Android;
// if show_tor {
// View::horizontal_line(ui, Colors::stroke());
// ui.add_space(6.0);
//
// View::sub_title(ui, format!("{} {}", CIRCLE_HALF, t!("transport.tor_network")));
// View::horizontal_line(ui, Colors::stroke());
// ui.add_space(6.0);
//
// // Show Tor settings.
// self.tor_settings.ui(ui, cb);
// ui.add_space(8.0);
// }
}
}
+149
View File
@@ -0,0 +1,149 @@
// Copyright 2025 The Grim Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use eframe::epaint::RectShape;
use egui::{Align, CursorIcon, Layout, RichText, Sense, StrokeKind, UiBuilder};
use crate::AppConfig;
use crate::gui::Colors;
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::types::ContentContainer;
use crate::gui::views::{Modal, View};
/// User interface settings content.
pub struct InterfaceSettingsContent {
/// Current locale.
locale: String,
}
impl ContentContainer for InterfaceSettingsContent {
fn modal_ids(&self) -> Vec<&'static str> {
vec![]
}
fn modal_ui(&mut self, _: &mut egui::Ui, _: &Modal, _: &dyn PlatformCallbacks) {}
fn container_ui(&mut self, ui: &mut egui::Ui, _: &dyn PlatformCallbacks) {
ui.add_space(5.0);
// Draw theme selection.
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("theme")).size(16.0).color(Colors::gray()));
});
let saved_use_dark = AppConfig::dark_theme().unwrap_or(false);
let mut selected_use_dark = saved_use_dark;
ui.add_space(8.0);
ui.columns(2, |columns| {
columns[0].vertical_centered(|ui| {
View::radio_value(ui, &mut selected_use_dark, false, t!("light"));
});
columns[1].vertical_centered(|ui| {
View::radio_value(ui, &mut selected_use_dark, true, t!("dark"));
})
});
ui.add_space(14.0);
if saved_use_dark != selected_use_dark {
AppConfig::set_dark_theme(selected_use_dark);
crate::setup_visuals(ui.ctx());
}
// Draw language selection.
let locales = rust_i18n::available_locales!();
for (index, locale) in locales.iter().enumerate() {
self.language_item_ui(locale, ui, index, locales.len());
}
ui.add_space(4.0);
}
}
impl Default for InterfaceSettingsContent {
fn default() -> Self {
let locale = if let Some(lang) = AppConfig::locale() {
lang
} else {
rust_i18n::locale().to_string()
};
Self { locale }
}
}
impl InterfaceSettingsContent {
/// Draw language selection item content.
fn language_item_ui(&mut self, locale: &str, ui: &mut egui::Ui, index: usize, len: usize) {
let is_current = self.locale == locale;
// Draw round background.
let mut rect = ui.available_rect_before_wrap();
rect.set_height(50.0);
let r = View::item_rounding(index, len, false);
let bg = if is_current {
Colors::fill()
} else {
Colors::fill_lite()
};
let mut bg_shape = RectShape::new(rect, r, bg, View::item_stroke(), StrokeKind::Outside);
let bg_idx = ui.painter().add(bg_shape.clone());
let res = ui
.scope_builder(
UiBuilder::new()
.sense(Sense::click())
.layout(Layout::right_to_left(Align::Center))
.max_rect(rect),
|ui| {
if is_current {
View::selected_item_check(ui);
}
let layout_size = ui.available_size();
ui.allocate_ui_with_layout(
layout_size,
Layout::left_to_right(Align::Center),
|ui| {
ui.add_space(12.0);
ui.vertical(|ui| {
// Draw language name.
ui.add_space(12.0);
let color = if is_current {
Colors::title(false)
} else {
Colors::gray()
};
ui.label(
RichText::new(t!("lang_name", locale = locale))
.size(17.0)
.color(color),
);
ui.add_space(14.0);
});
},
);
},
)
.response;
let clicked = res.clicked() || res.long_touched();
// Setup background and cursor.
if res.hovered() && !is_current {
res.on_hover_cursor(CursorIcon::PointingHand);
bg_shape.fill = Colors::fill();
}
ui.painter().set(bg_idx, bg_shape);
// Handle clicks on layout.
if clicked && !is_current {
rust_i18n::set_locale(locale);
AppConfig::save_locale(locale);
self.locale = locale.to_string();
}
}
}
+25
View File
@@ -0,0 +1,25 @@
// Copyright 2025 The Grim Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
mod content;
pub use content::*;
mod interface;
pub use interface::*;
mod network;
pub use network::*;
mod tor;
pub use tor::*;
+311
View File
@@ -0,0 +1,311 @@
// Copyright 2025 The Grim Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use eframe::epaint::RectShape;
use egui::{Align, CursorIcon, Id, Layout, RichText, Sense, StrokeKind, UiBuilder};
use url::Url;
use crate::AppConfig;
use crate::gui::Colors;
use crate::gui::icons::{CLOUD_CHECK, CLOUD_SLASH};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::types::{ContentContainer, ModalPosition};
use crate::gui::views::{Modal, TextEdit, View};
/// Network communication settings content.
pub struct NetworkSettingsContent {
/// Proxy URL input value for [`Modal`].
proxy_url_edit: String,
/// Flag to check if entered proxy address was correct.
proxy_url_error: bool,
}
/// Identifier for proxy URL edit [`Modal`].
const PROXY_URL_EDIT_MODAL: &'static str = "settings_proxy_edit_modal";
impl ContentContainer for NetworkSettingsContent {
fn modal_ids(&self) -> Vec<&'static str> {
vec![PROXY_URL_EDIT_MODAL]
}
fn modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
match modal.id {
PROXY_URL_EDIT_MODAL => self.proxy_modal_ui(ui, cb),
_ => {}
}
}
fn container_ui(&mut self, ui: &mut egui::Ui, _: &dyn PlatformCallbacks) {
let use_proxy = AppConfig::use_proxy();
View::checkbox(ui, use_proxy, t!("app_settings.proxy"), || {
// Show edit modal when both URLs are empty.
if AppConfig::http_proxy_url().is_none()
&& AppConfig::socks_proxy_url().is_none()
&& !use_proxy
{
Modal::new(PROXY_URL_EDIT_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("app_settings.proxy"))
.show();
} else {
AppConfig::toggle_use_proxy();
}
});
if !use_proxy {
ui.add_space(4.0);
ui.label(
RichText::new(t!("app_settings.proxy_desc"))
.size(16.0)
.color(Colors::inactive_text()),
);
ui.add_space(8.0);
} else {
ui.add_space(8.0);
// Draw proxy type selection.
Self::proxy_type_ui(ui);
// Draw proxy URL info.
self.proxy_item_ui(ui);
ui.add_space(6.0);
}
}
}
impl Default for NetworkSettingsContent {
fn default() -> Self {
Self {
proxy_url_edit: "".to_string(),
proxy_url_error: false,
}
}
}
impl NetworkSettingsContent {
/// Draw proxy edit modal content.
fn proxy_modal_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
let on_save = |c: &mut NetworkSettingsContent| {
let proxy = c.proxy_url_edit.trim().to_string();
let use_socks = AppConfig::use_socks_proxy();
// Clear value if empty.
if proxy.is_empty() {
if use_socks {
AppConfig::save_socks_proxy_url(None);
} else {
AppConfig::save_http_proxy_url(None);
}
Modal::close();
return;
}
// Format URL.
let http = "http://";
let socks = "socks5://";
let url = if use_socks {
let p = proxy.replace(http, "");
if !p.contains(socks) {
format!("{}{}", socks, p)
} else {
p
}
} else {
let p = proxy.replace(socks, "");
if !p.contains(http) {
format!("{}{}", http, p)
} else {
p
}
};
c.proxy_url_error = Url::parse(url.as_str()).is_err();
if !c.proxy_url_error {
// Save result when no error.
if !AppConfig::use_proxy() {
AppConfig::toggle_use_proxy();
}
if use_socks {
AppConfig::save_socks_proxy_url(Some(url))
} else {
AppConfig::save_http_proxy_url(Some(url));
}
Modal::close();
}
};
ui.add_space(6.0);
ui.vertical_centered(|ui| {
let label = format!("{}:", t!("enter_url"));
ui.label(RichText::new(label).size(17.0).color(Colors::gray()));
ui.add_space(8.0);
// Draw proxy URL text edit.
let mut edit =
TextEdit::new(Id::from("proxy_url_edit").with(PROXY_URL_EDIT_MODAL).with(
if AppConfig::use_proxy() {
"socks5"
} else {
"http"
},
))
.paste();
edit.ui(ui, &mut self.proxy_url_edit, cb);
if edit.enter_pressed {
on_save(self);
}
// Show error when specified address is incorrect.
if self.proxy_url_error {
ui.add_space(10.0);
ui.label(
RichText::new(t!("wallets.invalid_url"))
.size(16.0)
.color(Colors::red()),
);
}
ui.add_space(12.0);
// Show type selection when both URLs are empty.
if AppConfig::socks_proxy_url().is_none() && AppConfig::http_proxy_url().is_none() {
ui.add_space(6.0);
ui.vertical_centered(|ui| {
Self::proxy_type_ui(ui);
});
ui.add_space(4.0);
}
// Show modal buttons.
ui.scope(|ui| {
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(
ui,
t!("modal.cancel"),
Colors::white_or_black(false),
|| {
Modal::close();
},
);
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.save"), Colors::white_or_black(false), || {
on_save(self);
});
});
});
ui.add_space(6.0);
});
});
}
/// Draw proxy item content.
fn proxy_item_ui(&mut self, ui: &mut egui::Ui) {
// Draw round background.
let mut rect = ui.available_rect_before_wrap();
rect.set_height(56.0);
let r = View::item_rounding(0, 1, false);
let bg = Colors::fill_lite();
let mut bg_shape = RectShape::new(rect, r, bg, View::item_stroke(), StrokeKind::Outside);
let bg_idx = ui.painter().add(bg_shape.clone());
let res = ui
.scope_builder(
UiBuilder::new()
.sense(Sense::click())
.layout(Layout::right_to_left(Align::Center))
.max_rect(rect),
|ui| {
let layout_size = ui.available_size();
ui.allocate_ui_with_layout(
layout_size,
Layout::left_to_right(Align::Center),
|ui| {
ui.add_space(12.0);
ui.vertical(|ui| {
ui.add_space(4.0);
let use_socks = AppConfig::use_socks_proxy();
let proxy_url = if use_socks {
AppConfig::socks_proxy_url()
} else {
AppConfig::http_proxy_url()
};
let (url, color, icon, text) = if let Some(url) = proxy_url {
(
url,
Colors::title(false),
CLOUD_CHECK,
t!("network_settings.enabled"),
)
} else {
(
t!("enter_url").into(),
Colors::inactive_text(),
CLOUD_SLASH,
t!("network_settings.disabled"),
)
};
View::ellipsize_text(ui, url, 18.0, color);
ui.add_space(1.0);
let value = format!("{} {}", icon, text);
ui.label(RichText::new(value).size(15.0).color(Colors::gray()));
ui.add_space(3.0);
});
},
);
},
)
.response;
let clicked = res.clicked() || res.long_touched();
// Setup background and cursor.
if res.hovered() {
res.on_hover_cursor(CursorIcon::PointingHand);
bg_shape.fill = Colors::fill();
}
ui.painter().set(bg_idx, bg_shape);
// Handle clicks on layout.
if clicked {
let url = if AppConfig::use_socks_proxy() {
AppConfig::socks_proxy_url().unwrap_or("".to_string())
} else {
AppConfig::http_proxy_url().unwrap_or("".to_string())
};
self.proxy_url_edit = url;
// Show proxy URL edit modal.
Modal::new(PROXY_URL_EDIT_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("app_settings.proxy"))
.show();
}
}
/// Draw proxy type selection.
fn proxy_type_ui(ui: &mut egui::Ui) {
// Draw proxy type selection.
let saved_use_socks = AppConfig::use_socks_proxy();
let mut selected_use_socks = saved_use_socks;
ui.columns(2, |columns| {
columns[0].vertical_centered(|ui| {
View::radio_value(ui, &mut selected_use_socks, true, "SOCKS5".to_string());
});
columns[1].vertical_centered(|ui| {
View::radio_value(ui, &mut selected_use_socks, false, "HTTP".to_string());
})
});
ui.add_space(14.0);
if saved_use_socks != selected_use_socks {
AppConfig::toggle_use_socks_proxy();
}
}
}
+678
View File
@@ -0,0 +1,678 @@
// Copyright 2025 The Grim Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use eframe::epaint::RectShape;
use egui::scroll_area::ScrollBarVisibility;
use egui::{Align, CursorIcon, Id, Layout, RichText, ScrollArea, Sense, StrokeKind, UiBuilder};
use std::fs;
use url::Url;
use crate::gui::Colors;
use crate::gui::icons::{CLIPBOARD_TEXT, CLOUD_CHECK, NOTCHES, PENCIL, SCAN, TERMINAL};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::types::{ContentContainer, ModalPosition};
use crate::gui::views::{
CameraScanContent, FilePickContent, FilePickContentType, Modal, TextEdit, View,
};
use crate::tor::{TorBridge, TorConfig, TorProxy};
/// Transport settings content.
pub struct TorSettingsContent {
/// Flag to check if settings were changed.
pub settings_changed: bool,
/// Proxy URL input value for [`Modal`].
proxy_url_edit: String,
/// Flag to check if entered proxy address was correct.
proxy_url_error: bool,
/// Tor bridge binary path value for [`Modal`].
bridge_bin_path_edit: String,
/// Button to pick binary file for bridge.
bridge_bin_pick_file: FilePickContent,
/// Tor bridge connection line value for [`Modal`].
bridge_conn_line_edit: String,
/// Bridge line QR code scanner [`Modal`] content.
bridge_qr_scan_content: Option<CameraScanContent>,
}
/// Identifier for proxy URL edit [`Modal`].
const PROXY_URL_EDIT_MODAL: &'static str = "tor_proxy_edit_modal";
/// Identifier for bridge binary path input [`Modal`].
const BRIDGE_BIN_EDIT_MODAL: &'static str = "bridge_bin_edit_modal";
/// Identifier for bridge connection line input [`Modal`].
const BRIDGE_CONN_LINE_EDIT_MODAL: &'static str = "bridge_conn_line_edit_modal";
/// Identifier for [`Modal`] to scan bridge line from QR code.
const SCAN_BRIDGE_CONN_LINE_MODAL: &'static str = "scan_bridge_conn_line_modal";
impl Default for TorSettingsContent {
fn default() -> Self {
// Setup Tor bridge binary path edit text.
let bridge = TorConfig::get_bridge();
let (bin_path, conn_line) = if let Some(b) = bridge {
(b.binary_path(), b.connection_line())
} else {
("".to_string(), "".to_string())
};
Self {
settings_changed: false,
proxy_url_edit: "".to_string(),
proxy_url_error: false,
bridge_bin_path_edit: bin_path,
bridge_bin_pick_file: FilePickContent::new(FilePickContentType::ItemButton(
View::item_rounding(0, 1, true),
))
.no_parse(),
bridge_conn_line_edit: conn_line,
bridge_qr_scan_content: None,
}
}
}
impl ContentContainer for TorSettingsContent {
fn modal_ids(&self) -> Vec<&'static str> {
vec![
PROXY_URL_EDIT_MODAL,
BRIDGE_BIN_EDIT_MODAL,
BRIDGE_CONN_LINE_EDIT_MODAL,
SCAN_BRIDGE_CONN_LINE_MODAL,
]
}
fn modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
match modal.id {
PROXY_URL_EDIT_MODAL => self.proxy_modal_ui(ui, cb),
BRIDGE_BIN_EDIT_MODAL => self.bridge_bin_edit_modal_ui(ui, cb),
BRIDGE_CONN_LINE_EDIT_MODAL => self.bridge_conn_line_edit_modal_ui(ui, cb),
SCAN_BRIDGE_CONN_LINE_MODAL => {
if let Some(content) = self.bridge_qr_scan_content.as_mut() {
let mut close = false;
content.modal_ui(ui, cb, |res| {
// Save connection line after scanning.
let line = res.text();
let bridge = TorConfig::get_bridge().unwrap();
if bridge.connection_line() != line {
TorBridge::save_bridge_conn_line(&bridge, line);
self.settings_changed = true;
}
close = true;
});
if close {
self.bridge_qr_scan_content = None;
cb.stop_camera();
Modal::close();
}
}
}
_ => {}
}
}
fn container_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
ui.label(
RichText::new(format!("{}:", t!("wallets.conn_method")))
.size(17.0)
.color(Colors::inactive_text()),
);
ui.add_space(10.0);
let mut proxy = TorConfig::get_proxy();
let current_proxy = proxy.clone();
ui.columns(2, |columns| {
columns[0].vertical_centered(|ui| {
let name = t!("network_settings.default");
View::radio_value(ui, &mut proxy, None, name);
});
columns[1].vertical_centered(|ui| {
let name = t!("app_settings.proxy");
let val = current_proxy
.clone()
.unwrap_or(TorProxy::SOCKS5(TorProxy::DEFAULT_SOCKS5_URL.to_string()));
View::radio_value(ui, &mut proxy, Some(val), name);
});
});
ui.add_space(14.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(6.0);
if let Some(p) = proxy.as_mut() {
ui.label(
RichText::new(format!("{}:", t!("app_settings.proxy")))
.size(17.0)
.color(Colors::inactive_text()),
);
ui.add_space(10.0);
ui.columns(2, |columns| {
columns[0].vertical_centered(|ui| {
let value = TorConfig::get_socks5_proxy();
View::radio_value(ui, p, value, "SOCKS5".to_string());
});
columns[1].vertical_centered(|ui| {
let value = TorConfig::get_http_proxy();
View::radio_value(ui, p, value, "HTTP".to_string());
});
});
ui.add_space(14.0);
// Show proxy settings.
self.proxy_item_ui(p.url(), ui);
ui.add_space(8.0);
}
// Check if proxy type was changed to save.
if current_proxy != proxy {
TorConfig::save_proxy(proxy.clone());
self.settings_changed = true;
}
if proxy.is_some() {
return;
}
let bridge = TorConfig::get_bridge();
ui.vertical_centered(|ui| {
ui.label(
RichText::new(t!("transport.bridges_desc"))
.size(17.0)
.color(Colors::inactive_text()),
);
// Draw checkbox to enable/disable bridges.
View::checkbox(ui, bridge.is_some(), t!("transport.bridges"), || {
let value = if bridge.is_some() {
None
} else {
let default_bridge = TorConfig::get_webtunnel();
self.bridge_bin_path_edit = default_bridge.binary_path();
self.bridge_conn_line_edit = default_bridge.connection_line();
Some(default_bridge)
};
TorConfig::save_bridge(value);
self.settings_changed = true;
});
});
if bridge.is_some() {
ui.add_space(6.0);
// Show bridge selection for desktop.
if View::is_desktop() {
let current_bridge = bridge.unwrap();
let mut bridge = current_bridge.clone();
ui.columns(2, |columns| {
columns[0].vertical_centered(|ui| {
// Show Webtunnel bridge selector.
let webtunnel = TorConfig::get_webtunnel();
let name = webtunnel.protocol_name().to_uppercase();
View::radio_value(ui, &mut bridge, webtunnel, name);
});
columns[1].vertical_centered(|ui| {
// Show Obfs4 bridge selector.
let obfs4 = TorConfig::get_obfs4();
let name = obfs4.protocol_name().to_uppercase();
View::radio_value(ui, &mut bridge, obfs4, name);
});
});
ui.add_space(10.0);
ui.vertical_centered(|ui| {
// Show Snowflake bridge selector.
let snowflake = TorConfig::get_snowflake();
let name = snowflake.protocol_name().to_uppercase();
View::radio_value(ui, &mut bridge, snowflake, name);
});
ui.add_space(16.0);
// Check if bridge type was changed to save.
if current_bridge != bridge {
TorConfig::save_bridge(Some(bridge.clone()));
self.bridge_bin_path_edit = bridge.binary_path();
self.bridge_conn_line_edit = bridge.connection_line();
self.settings_changed = true;
}
}
if let Some(br) = TorConfig::get_bridge().as_ref() {
// Show bridge connection line setup.
self.conn_line_ui(ui, br, cb);
// Show bridge binary setup for desktop.
if View::is_desktop() {
self.bridge_bin_ui(ui, br, cb);
}
}
ui.add_space(8.0);
}
}
}
impl TorSettingsContent {
/// Draw proxy edit modal content.
fn proxy_modal_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
let on_save = |c: &mut TorSettingsContent| {
let http = "http://";
let socks = "socks5://";
let url = c.proxy_url_edit.trim().to_string();
c.proxy_url_error = Url::parse(url.as_str()).is_err();
if !c.proxy_url_error {
let proxy = TorConfig::get_proxy().unwrap();
if url.contains(socks) {
TorConfig::save_proxy(Some(TorProxy::SOCKS5(url)));
} else if url.contains(http) {
TorConfig::save_proxy(Some(TorProxy::HTTP(url)));
} else {
match proxy {
TorProxy::SOCKS5(_) => {
TorConfig::save_proxy(Some(TorProxy::SOCKS5(url)));
}
TorProxy::HTTP(_) => {
TorConfig::save_proxy(Some(TorProxy::HTTP(url)));
}
}
}
Modal::close();
}
};
ui.add_space(6.0);
ui.vertical_centered(|ui| {
let label = format!("{}:", t!("enter_url"));
ui.label(RichText::new(label).size(17.0).color(Colors::gray()));
ui.add_space(8.0);
// Draw proxy URL text edit.
let mut edit =
TextEdit::new(Id::from("proxy_url_edit").with(PROXY_URL_EDIT_MODAL)).paste();
edit.ui(ui, &mut self.proxy_url_edit, cb);
if edit.enter_pressed {
on_save(self);
}
// Show error when specified address is incorrect.
if self.proxy_url_error {
ui.add_space(10.0);
ui.label(
RichText::new(t!("wallets.invalid_url"))
.size(16.0)
.color(Colors::red()),
);
}
ui.add_space(12.0);
// Show modal buttons.
ui.scope(|ui| {
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(
ui,
t!("modal.cancel"),
Colors::white_or_black(false),
|| {
Modal::close();
},
);
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.save"), Colors::white_or_black(false), || {
on_save(self);
});
});
});
ui.add_space(6.0);
});
});
}
/// Draw proxy item content.
fn proxy_item_ui(&mut self, url: String, ui: &mut egui::Ui) {
// Setup layout size.
let mut rect = ui.available_rect_before_wrap();
rect.set_height(56.0);
// Draw round background.
let bg_rect = rect.clone();
let item_rounding = View::item_rounding(0, 1, false);
ui.painter().rect(
bg_rect,
item_rounding,
Colors::fill(),
View::item_stroke(),
StrokeKind::Outside,
);
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
View::item_button(ui, View::item_rounding(0, 1, true), PENCIL, None, || {
self.proxy_url_edit = url.clone();
// Show proxy URL edit modal.
Modal::new(PROXY_URL_EDIT_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("app_settings.proxy"))
.show();
});
let layout_size = ui.available_size();
ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| {
ui.add_space(12.0);
ui.vertical(|ui| {
ui.add_space(4.0);
View::ellipsize_text(ui, url, 18.0, Colors::title(false));
ui.add_space(1.0);
let value = format!("{} {}", CLOUD_CHECK, t!("network_settings.enabled"));
ui.label(RichText::new(value).size(15.0).color(Colors::gray()));
ui.add_space(3.0);
});
});
});
}
/// Draw bridge connection line setup content.
fn conn_line_ui(&mut self, ui: &mut egui::Ui, bridge: &TorBridge, cb: &dyn PlatformCallbacks) {
// Draw round background.
let mut rect = ui.available_rect_before_wrap();
rect.set_height(56.0);
let r = if View::is_desktop() {
View::item_rounding(0, 2, false)
} else {
View::item_rounding(0, 1, false)
};
let bg = Colors::fill_lite();
let mut bg_shape = RectShape::new(rect, r, bg, View::item_stroke(), StrokeKind::Outside);
let bg_idx = ui.painter().add(bg_shape.clone());
let res = ui
.scope_builder(
UiBuilder::new()
.sense(Sense::click())
.layout(Layout::right_to_left(Align::Center))
.max_rect(rect),
|ui| {
View::item_button(ui, View::item_rounding(0, 1, true), SCAN, None, || {
self.show_qr_scan_bridge_modal(cb);
});
let layout_size = ui.available_size();
ui.allocate_ui_with_layout(
layout_size,
Layout::left_to_right(Align::Center),
|ui| {
ui.add_space(12.0);
ui.vertical(|ui| {
ui.add_space(4.0);
let line_text = bridge.connection_line();
View::ellipsize_text(ui, line_text, 18.0, Colors::title(false));
ui.add_space(1.0);
let line_desc = t!("transport.conn_line").replace(":", "");
let value = format!("{} {}", NOTCHES, line_desc);
ui.label(RichText::new(value).size(15.0).color(Colors::gray()));
ui.add_space(3.0);
});
},
);
},
)
.response;
let clicked = res.clicked() || res.long_touched();
// Setup background and cursor.
if res.hovered() {
res.on_hover_cursor(CursorIcon::PointingHand);
bg_shape.fill = Colors::fill();
}
ui.painter().set(bg_idx, bg_shape);
// Handle clicks on layout.
if clicked {
self.bridge_conn_line_edit = bridge.connection_line();
// Show connection line edit modal.
let title = bridge.protocol_name();
Modal::new(BRIDGE_CONN_LINE_EDIT_MODAL)
.position(ModalPosition::Center)
.title(title)
.show();
}
}
/// Show bridge connection line QR code scanner.
fn show_qr_scan_bridge_modal(&mut self, cb: &dyn PlatformCallbacks) {
self.bridge_qr_scan_content = Some(CameraScanContent::default());
// Show QR code scan modal.
Modal::new(SCAN_BRIDGE_CONN_LINE_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("scan_qr"))
.closeable(false)
.show();
cb.start_camera();
}
/// Draw bridge connection line input [`Modal`] content.
fn bridge_conn_line_edit_modal_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
let on_save = |c: &mut TorSettingsContent| {
let bridge = TorConfig::get_bridge().unwrap();
if bridge.connection_line() != c.bridge_conn_line_edit {
TorBridge::save_bridge_conn_line(&bridge, c.bridge_conn_line_edit.clone());
c.settings_changed = true;
}
Modal::close();
};
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(
RichText::new(t!("transport.conn_line"))
.size(17.0)
.color(Colors::gray()),
);
ui.add_space(8.0);
// Draw connection line text edit.
ui.vertical_centered(|ui| {
let scroll_id = Id::from(BRIDGE_CONN_LINE_EDIT_MODAL);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(3.0);
ScrollArea::both()
.id_salt(scroll_id)
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.max_height(128.0)
.auto_shrink([false; 2])
.show(ui, |ui| {
ui.add_space(7.0);
let input_id = scroll_id.with("_input");
egui::TextEdit::multiline(&mut self.bridge_conn_line_edit)
.id(input_id)
.font(egui::TextStyle::Body)
.desired_rows(5)
.interactive(false)
.desired_width(f32::INFINITY)
.show(ui);
ui.add_space(6.0);
});
});
ui.add_space(2.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(8.0);
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
// Draw paste button.
let paste_text = format!("{} {}", CLIPBOARD_TEXT, t!("paste"));
View::button(ui, paste_text, Colors::white_or_black(false), || {
self.bridge_conn_line_edit = cb.get_string_from_buffer();
});
});
columns[1].vertical_centered_justified(|ui| {
// Draw button to scan bridge QR code.
let scan_text = format!("{} {}", SCAN, t!("scan"));
View::button(ui, scan_text, Colors::white_or_black(false), || {
self.show_qr_scan_bridge_modal(cb);
});
});
});
ui.add_space(8.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(8.0);
// Show modal buttons.
ui.scope(|ui| {
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(
ui,
t!("modal.cancel"),
Colors::white_or_black(false),
|| {
// Close modal.
Modal::close();
},
);
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.save"), Colors::white_or_black(false), || {
on_save(self);
});
});
});
ui.add_space(6.0);
});
});
}
/// Draw bridge binary setup content.
fn bridge_bin_ui(&mut self, ui: &mut egui::Ui, bridge: &TorBridge, cb: &dyn PlatformCallbacks) {
// Draw round background.
let mut rect = ui.available_rect_before_wrap();
rect.set_height(56.0);
let r = View::item_rounding(1, 2, false);
let bg = Colors::fill_lite();
let mut bg_shape = RectShape::new(rect, r, bg, View::item_stroke(), StrokeKind::Outside);
let bg_idx = ui.painter().add(bg_shape.clone());
let res = ui
.scope_builder(
UiBuilder::new()
.sense(Sense::click())
.layout(Layout::right_to_left(Align::Center))
.max_rect(rect),
|ui| {
self.bridge_bin_pick_file.ui(ui, cb, |path| {
if bridge.binary_path() != path {
TorBridge::save_bridge_bin_path(bridge, path);
self.settings_changed = true;
}
});
let layout_size = ui.available_size();
ui.allocate_ui_with_layout(
layout_size,
Layout::left_to_right(Align::Center),
|ui| {
ui.add_space(12.0);
ui.vertical(|ui| {
ui.add_space(4.0);
let bin_text = bridge.binary_path();
View::ellipsize_text(ui, bin_text, 18.0, Colors::title(false));
ui.add_space(1.0);
let bin_desc = t!("transport.bin_file").replace(":", "");
let value = format!("{} {}", TERMINAL, bin_desc);
ui.label(RichText::new(value).size(15.0).color(Colors::gray()));
ui.add_space(3.0);
});
},
);
},
)
.response;
let clicked = res.clicked() || res.long_touched();
// Setup background and cursor.
if res.hovered() {
res.on_hover_cursor(CursorIcon::PointingHand);
bg_shape.fill = Colors::fill();
}
ui.painter().set(bg_idx, bg_shape);
// Handle clicks on layout.
if clicked {
self.bridge_bin_path_edit = bridge.binary_path();
// Show binary path edit modal.
let title = bridge.protocol_name();
Modal::new(BRIDGE_BIN_EDIT_MODAL)
.position(ModalPosition::CenterTop)
.title(title)
.show();
}
}
/// Draw bridge binary input [`Modal`] content.
fn bridge_bin_edit_modal_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
let on_save = |c: &mut TorSettingsContent| {
let bridge = TorConfig::get_bridge().unwrap();
let exists = fs::exists(&c.bridge_bin_path_edit).unwrap_or_default();
if !exists {
return;
}
if bridge.binary_path() != c.bridge_bin_path_edit {
TorBridge::save_bridge_bin_path(&bridge, c.bridge_bin_path_edit.clone());
c.settings_changed = true;
}
Modal::close();
};
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(
RichText::new(t!("transport.bin_file"))
.size(17.0)
.color(Colors::gray()),
);
ui.add_space(8.0);
// Draw bridge text edit.
let mut edit = TextEdit::new(Id::from(BRIDGE_BIN_EDIT_MODAL)).paste();
edit.ui(ui, &mut self.bridge_bin_path_edit, cb);
if edit.enter_pressed {
on_save(self);
}
ui.add_space(12.0);
// Show modal buttons.
ui.scope(|ui| {
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(
ui,
t!("modal.cancel"),
Colors::white_or_black(false),
|| {
// Close modal.
Modal::close();
},
);
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.save"), Colors::white_or_black(false), || {
on_save(self);
});
});
});
ui.add_space(6.0);
});
});
}
}
+94 -101
View File
@@ -12,118 +12,111 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use egui::{Margin, Id, Layout, Align};
use egui::{Align, Id, Layout, Margin, UiBuilder};
use crate::gui::Colors;
use crate::gui::views::{Content, View};
use crate::gui::views::types::{TitleContentType, TitleType};
use crate::gui::views::{Content, View};
/// Title panel with left/right action buttons and text in the middle.
pub struct TitlePanel {
/// Widget identifier.
id: Id
/// Widget identifier.
id: Id,
}
impl TitlePanel {
/// Default [`TitlePanel`] content height.
pub const DEFAULT_HEIGHT: f32 = 54.0;
/// Content height.
pub const HEIGHT: f32 = 54.0;
/// Create new title panel with provided identifier.
pub fn new(id: Id) -> Self {
Self {
id,
}
}
/// Create new title panel with provided identifier.
pub fn new(id: Id) -> Self {
Self { id }
}
pub fn ui(&self,
title: TitleType,
mut left_content: impl FnMut(&mut egui::Ui),
mut right_content: impl FnMut(&mut egui::Ui),
ui: &mut egui::Ui) {
// Draw title panel.
egui::TopBottomPanel::top(self.id)
.resizable(false)
.exact_height(Self::DEFAULT_HEIGHT + View::get_top_inset())
.frame(egui::Frame {
inner_margin: Margin {
left: View::far_left_inset_margin(ui),
right: View::far_right_inset_margin(ui),
top: View::get_top_inset(),
bottom: 0.0,
},
fill: Colors::yellow(),
..Default::default()
})
.show_inside(ui, |ui| {
let rect = ui.available_rect_before_wrap();
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Max), |ui| {
ui.horizontal_centered(|ui| {
(right_content)(ui);
});
ui.with_layout(Layout::left_to_right(Align::Min), |ui| {
ui.horizontal_centered(|ui| {
(left_content)(ui);
});
});
match title {
TitleType::Single(content) => {
let content_rect = {
let mut r = rect;
r.min.x += Self::DEFAULT_HEIGHT;
r.max.x -= Self::DEFAULT_HEIGHT;
r
};
ui.allocate_ui_at_rect(content_rect, |ui| {
Self::title_text_content(ui, content);
});
}
TitleType::Dual(first, second) => {
let first_rect = {
let mut r = rect;
r.max.x = r.min.x + Content::SIDE_PANEL_WIDTH - Self::DEFAULT_HEIGHT;
r.min.x += Self::DEFAULT_HEIGHT;
r
};
// Draw first title content.
ui.allocate_ui_at_rect(first_rect, |ui| {
Self::title_text_content(ui, first);
});
pub fn ui(
&self,
title: TitleType,
mut left_content: impl FnMut(&mut egui::Ui),
mut right_content: impl FnMut(&mut egui::Ui),
ui: &mut egui::Ui,
) {
// Draw title panel.
egui::TopBottomPanel::top(self.id)
.resizable(false)
.exact_height(Self::HEIGHT + View::get_top_inset())
.frame(egui::Frame {
inner_margin: Margin {
left: View::far_left_inset_margin(ui) as i8,
right: View::far_right_inset_margin(ui) as i8,
top: View::get_top_inset() as i8,
bottom: 0.0 as i8,
},
..Default::default()
})
.show_inside(ui, |ui| {
let rect = ui.available_rect_before_wrap();
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Max), |ui| {
ui.horizontal_centered(|ui| {
(right_content)(ui);
});
ui.with_layout(Layout::left_to_right(Align::Min), |ui| {
ui.horizontal_centered(|ui| {
(left_content)(ui);
});
});
match title {
TitleType::Single(content) => {
let content_rect = {
let mut r = rect.clone();
r.min.x += Self::HEIGHT;
r.max.x -= Self::HEIGHT;
r
};
ui.scope_builder(UiBuilder::new().max_rect(content_rect), |ui| {
Self::title_text_content(ui, content);
});
}
TitleType::Dual(first, second) => {
let first_rect = {
let mut r = rect.clone();
r.max.x = r.min.x + Content::SIDE_PANEL_WIDTH - Self::HEIGHT;
r.min.x += Self::HEIGHT;
r
};
// Draw first title content.
ui.scope_builder(UiBuilder::new().max_rect(first_rect), |ui| {
Self::title_text_content(ui, first);
});
let second_rect = {
let mut r = rect;
r.min.x = first_rect.max.x + 2.0 * Self::DEFAULT_HEIGHT;
r.max.x -= Self::DEFAULT_HEIGHT;
r
};
// Draw second title content.
ui.allocate_ui_at_rect(second_rect, |ui| {
Self::title_text_content(ui, second);
});
}
}
});
});
}
let second_rect = {
let mut r = rect.clone();
r.min.x = first_rect.max.x + 2.0 * Self::HEIGHT;
r.max.x -= Self::HEIGHT;
r
};
// Draw second title content.
ui.scope_builder(UiBuilder::new().max_rect(second_rect), |ui| {
Self::title_text_content(ui, second);
});
}
}
});
});
}
/// Setup title text content.
fn title_text_content(ui: &mut egui::Ui, content: TitleContentType) {
ui.vertical_centered(|ui| {
match content {
TitleContentType::Title(text) => {
ui.add_space(13.0 + if !View::is_desktop() {
1.0
} else {
0.0
});
View::ellipsize_text(ui, text, 19.0, Colors::title(true));
}
TitleContentType::WithSubTitle(text, subtitle, animate) => {
ui.add_space(4.0);
View::ellipsize_text(ui, text, 18.0, Colors::title(true));
ui.add_space(-2.0);
View::animate_text(ui, subtitle, 15.0, Colors::text(true), animate)
}
}
});
}
/// Setup title text content.
fn title_text_content(ui: &mut egui::Ui, content: TitleContentType) {
ui.vertical_centered(|ui| match content {
TitleContentType::Title(text) => {
ui.add_space(13.0 + if !View::is_desktop() { 1.0 } else { 0.0 });
View::ellipsize_text(ui, text.to_uppercase(), 19.0, Colors::title(true));
}
TitleContentType::WithSubTitle(text, subtitle, animate) => {
ui.add_space(4.0);
View::ellipsize_text(ui, text.to_uppercase(), 18.0, Colors::title(true));
ui.add_space(-2.0);
View::animate_text(ui, subtitle, 15.0, Colors::text(true), animate)
}
});
}
}
+96 -163
View File
@@ -12,211 +12,144 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use grin_util::ZeroingString;
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::Modal;
use grin_util::ZeroingString;
/// Title type, can be single or dual title in the row.
pub enum TitleType {
/// Single title content.
Single(TitleContentType),
/// Dual title content, will align first content for default panel size width.
Dual(TitleContentType, TitleContentType),
/// Single title content.
Single(TitleContentType),
/// Dual title content, will align first content for default panel size width.
Dual(TitleContentType, TitleContentType),
}
/// Title content type, can be single title or with animated subtitle.
pub enum TitleContentType {
/// Single text.
Title(String),
/// With optionally animated subtitle text.
WithSubTitle(String, String, bool)
/// Single text.
Title(String),
/// With optionally animated subtitle text.
WithSubTitle(String, String, bool),
}
/// Stroke position against content.
pub enum LinePosition {
TOP,
LEFT,
RIGHT,
BOTTOM,
}
/// Position of [`Modal`] on the screen.
#[derive(Clone)]
pub enum ModalPosition {
CenterTop,
Center
CenterTop,
Center,
}
/// Global [`Modal`] state.
#[derive(Default)]
pub struct ModalState {
/// Opened [`Modal`].
pub modal: Option<Modal>,
/// Opened [`Modal`].
pub modal: Option<Modal>,
}
/// Contains identifiers to draw opened [`Modal`] content for current ui container.
pub trait ModalContainer {
/// List of allowed [`Modal`] identifiers.
fn modal_ids(&self) -> &Vec<&'static str>;
/// Draw modal ui content.
fn modal_ui(&mut self,
ui: &mut egui::Ui,
modal: &Modal,
cb: &dyn PlatformCallbacks);
/// Draw [`Modal`] for current ui container if it's possible.
fn current_modal_ui(&mut self,
ui: &mut egui::Ui,
cb: &dyn PlatformCallbacks) {
let modal_id = Modal::opened();
let draw = modal_id.is_some() && self.modal_ids().contains(&modal_id.unwrap());
if draw {
Modal::ui(ui.ctx(), |ui, modal| {
self.modal_ui(ui, modal, cb);
});
}
}
}
/// Options for [`egui::TextEdit`] view.
pub struct TextEditOptions {
/// View identifier.
pub id: egui::Id,
/// Check if horizontal centering is needed.
pub h_center: bool,
/// Check if initial focus on field is needed.
pub focus: bool,
/// Hide letters and draw button to show/hide letters.
pub password: bool,
/// Show copy button.
pub copy: bool,
/// Show paste button.
pub paste: bool,
/// Show button to scan QR code into text.
pub scan_qr: bool,
/// Callback when scan button was pressed.
pub scan_pressed: bool,
}
impl TextEditOptions {
pub fn new(id: egui::Id) -> Self {
Self {
id,
h_center: false,
focus: true,
password: false,
copy: false,
paste: false,
scan_qr: false,
scan_pressed: false,
}
}
/// Center text horizontally.
pub fn h_center(mut self) -> Self {
self.h_center = true;
self
}
/// Disable initial focus.
pub fn no_focus(mut self) -> Self {
self.focus = false;
self
}
/// Hide letters and draw button to show/hide letters.
pub fn password(mut self) -> Self {
self.password = true;
self
}
/// Show button to copy text.
pub fn copy(mut self) -> Self {
self.copy = true;
self
}
/// Show button to paste text.
pub fn paste(mut self) -> Self {
self.paste = true;
self
}
/// Show button to scan QR code to text.
pub fn scan_qr(mut self) -> Self {
self.scan_qr = true;
self.scan_pressed = false;
self
}
/// Content container to simplify modals management and navigation.
pub trait ContentContainer {
/// List of allowed [`Modal`] identifiers.
fn modal_ids(&self) -> Vec<&'static str>;
/// Draw modal content.
fn modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks);
/// Draw container content.
fn container_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks);
/// Draw content, to call by parent container.
fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
// Draw modal content.
if let Some(id) = Modal::opened() {
if self.modal_ids().contains(&id) {
Modal::ui(ui.ctx(), cb, |ui, modal, cb| {
self.modal_ui(ui, modal, cb);
});
}
}
self.container_ui(ui, cb);
}
}
/// QR code scan result.
#[derive(Clone)]
pub enum QrScanResult {
/// Slatepack message.
Slatepack(ZeroingString),
/// Slatepack address.
Address(ZeroingString),
/// Parsed text.
Text(ZeroingString),
/// Recovery phrase in standard or compact SeedQR format.
/// https://github.com/SeedSigner/seedsigner/blob/dev/docs/seed_qr/README.md
SeedQR(ZeroingString),
/// Part of Uniform Resources as URI with current index and total messages amount.
/// https://github.com/BlockchainCommons/Research/blob/master/papers/bcr-2020-005-ur.md
URPart(String, usize, usize),
/// Slatepack message.
Slatepack(String),
/// Slatepack address.
Address(ZeroingString),
/// Parsed text.
Text(ZeroingString),
/// Recovery phrase in standard or compact SeedQR format.
/// https://github.com/SeedSigner/seedsigner/blob/dev/docs/seed_qr/README.md
SeedQR(ZeroingString),
/// Part of Uniform Resources as URI with current index and total messages amount.
/// https://github.com/BlockchainCommons/Research/blob/master/papers/bcr-2020-005-ur.md
URPart(String, usize, usize),
}
impl QrScanResult {
/// Get text scanning result.
pub fn text(&self) -> String {
match self {
QrScanResult::Slatepack(text) => text.to_string(),
QrScanResult::Address(text) => text.to_string(),
QrScanResult::Text(text) => text.to_string(),
QrScanResult::SeedQR(text) => text.to_string(),
QrScanResult::URPart(uri, _, _) => uri.to_string(),
}
}
/// Get text scanning result.
pub fn text(&self) -> String {
match self {
QrScanResult::Slatepack(text) => text.to_string(),
QrScanResult::Address(text) => text.to_string(),
QrScanResult::Text(text) => text.to_string(),
QrScanResult::SeedQR(text) => text.to_string(),
QrScanResult::URPart(uri, _, _) => uri.to_string(),
}
}
}
/// QR code scanning state.
pub struct QrScanState {
/// Flag to check if image is processing to find QR code.
pub image_processing: bool,
/// Processed QR code result.
pub qr_scan_result: Option<QrScanResult>
/// Flag to check if image is processing to find QR code.
pub image_processing: bool,
/// Processed QR code result.
pub qr_scan_result: Option<QrScanResult>,
}
impl Default for QrScanState {
fn default() -> Self {
Self {
image_processing: false,
qr_scan_result: None,
}
}
fn default() -> Self {
Self {
image_processing: false,
qr_scan_result: None,
}
}
}
/// QR code image data state.
pub struct QrImageState {
/// Flag to check if QR code image is loading.
pub loading: bool,
/// Flag to check if QR code image is exporting.
pub exporting: bool,
/// Flag to check if QR code image is loading.
pub loading: bool,
/// Flag to check if QR code image is exporting.
pub exporting: bool,
/// Created GIF data from animated QR code.
pub gif_data: Option<Vec<u8>>,
/// Flag to check if GIF is creating.
pub gif_creating: bool,
/// Created GIF data from animated QR code.
pub gif_data: Option<Vec<u8>>,
/// Flag to check if GIF is creating.
pub gif_creating: bool,
/// Vector image data.
pub svg: Option<Vec<u8>>,
/// Multiple vector image data for animated QR code.
pub svg_list: Option<Vec<Vec<u8>>>
/// Vector image data.
pub svg: Option<Vec<u8>>,
/// Multiple vector image data for animated QR code.
pub svg_list: Option<Vec<Vec<u8>>>,
}
impl Default for QrImageState {
fn default() -> Self {
Self {
loading: false,
exporting: false,
gif_data: None,
gif_creating: false,
svg: None,
svg_list: None,
}
}
}
fn default() -> Self {
Self {
loading: false,
exporting: false,
gif_data: None,
gif_creating: false,
svg: None,
svg_list: None,
}
}
}
+753 -728
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+376
View File
@@ -0,0 +1,376 @@
// Copyright 2023 The Grim Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use egui::scroll_area::ScrollBarVisibility;
use egui::{Id, Margin, RichText, ScrollArea};
use grin_util::ZeroingString;
use crate::gui::Colors;
use crate::gui::icons::{CHECK, CLIPBOARD_TEXT, COPY, SCAN};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::types::{ContentContainer, LinePosition, ModalPosition, QrScanResult};
use crate::gui::views::wallets::ConnectionSettings;
use crate::gui::views::wallets::creation::MnemonicSetup;
use crate::gui::views::wallets::creation::types::Step;
use crate::gui::views::{CameraScanContent, Content, Modal, View};
use crate::node::Node;
use crate::wallet::types::PhraseMode;
use crate::wallet::{ExternalConnection, Wallet};
/// Wallet creation content.
pub struct WalletCreationContent {
/// Wallet name.
pub name: String,
/// Wallet password.
pub pass: ZeroingString,
/// Wallet creation step.
step: Step,
/// QR code scanning [`Modal`] content.
scan_modal_content: Option<CameraScanContent>,
/// Mnemonic phrase setup content.
mnemonic_setup: MnemonicSetup,
/// Network setup content.
network_setup: ConnectionSettings,
/// Flag to check if an error occurred during wallet creation.
creation_error: Option<String>,
}
const QR_CODE_PHRASE_SCAN_MODAL: &'static str = "qr_code_rec_phrase_scan_modal";
impl ContentContainer for WalletCreationContent {
fn modal_ids(&self) -> Vec<&'static str> {
vec![QR_CODE_PHRASE_SCAN_MODAL]
}
fn modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
match modal.id {
QR_CODE_PHRASE_SCAN_MODAL => {
if let Some(content) = self.scan_modal_content.as_mut() {
content.modal_ui(ui, cb, |result| match result {
QrScanResult::Text(text) => {
self.mnemonic_setup.mnemonic.import(&text);
Modal::close();
}
QrScanResult::SeedQR(text) => {
self.mnemonic_setup.mnemonic.import(&text);
Modal::close();
}
_ => {}
});
}
}
_ => {}
}
}
fn container_ui(&mut self, _: &mut egui::Ui, _: &dyn PlatformCallbacks) {}
}
impl WalletCreationContent {
/// Create new wallet creation content from name and password.
pub fn new(name: String, pass: ZeroingString) -> Self {
Self {
name,
pass,
step: Step::EnterMnemonic,
scan_modal_content: None,
mnemonic_setup: MnemonicSetup::default(),
network_setup: ConnectionSettings::default(),
creation_error: None,
}
}
/// Draw wallet creation content.
pub fn content_ui(
&mut self,
ui: &mut egui::Ui,
cb: &dyn PlatformCallbacks,
on_create: impl FnMut(Wallet),
) {
self.ui(ui, cb);
egui::TopBottomPanel::bottom("wallet_creation_step_panel")
.frame(egui::Frame {
inner_margin: Margin {
left: (View::far_left_inset_margin(ui) + View::TAB_ITEMS_PADDING) as i8,
right: (View::get_right_inset() + View::TAB_ITEMS_PADDING) as i8,
top: View::TAB_ITEMS_PADDING as i8,
bottom: (View::get_bottom_inset() + View::TAB_ITEMS_PADDING) as i8,
},
fill: Colors::fill(),
..Default::default()
})
.show_inside(ui, |ui| {
// Draw divider line.
let rect = {
let mut r = ui.available_rect_before_wrap();
r.min.y -= View::TAB_ITEMS_PADDING;
r.min.x -= View::far_left_inset_margin(ui) + View::TAB_ITEMS_PADDING;
r.max.x += View::get_right_inset() + View::TAB_ITEMS_PADDING;
r
};
View::line(ui, LinePosition::TOP, &rect, Colors::item_stroke());
// Show step control content.
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
self.step_control_ui(ui, on_create, cb);
});
});
// Show wallet creation step content panel.
egui::CentralPanel::default()
.frame(egui::Frame {
inner_margin: Margin {
left: (View::far_left_inset_margin(ui) + 4.0) as i8,
right: (View::get_right_inset() + 4.0) as i8,
top: 3.0 as i8,
bottom: 4.0 as i8,
},
fill: Colors::fill_lite(),
..Default::default()
})
.show_inside(ui, |ui| {
ScrollArea::vertical()
.id_salt(Id::from(format!(
"creation_step_scroll_{}",
self.step.name()
)))
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.auto_shrink([false; 2])
.show(ui, |ui| {
let max_width = if self.step == Step::SetupConnection {
Content::SIDE_PANEL_WIDTH * 1.3
} else {
Content::SIDE_PANEL_WIDTH * 2.0
};
View::max_width_ui(ui, max_width, |ui| {
self.step_content_ui(ui, cb);
});
});
});
}
/// Draw [`Step`] description and confirmation control.
fn step_control_ui(
&mut self,
ui: &mut egui::Ui,
on_create: impl FnOnce(Wallet),
cb: &dyn PlatformCallbacks,
) {
let step = &self.step;
// Setup description and next step availability.
let (step_text, mut next) = match step {
Step::EnterMnemonic => {
let mode = &self.mnemonic_setup.mnemonic.mode();
let (text, available) = match mode {
PhraseMode::Generate => (t!("wallets.create_phrase_desc"), true),
PhraseMode::Import => {
let available = !self.mnemonic_setup.mnemonic.has_empty_or_invalid();
(t!("wallets.restore_phrase_desc"), available)
}
};
(text, available)
}
Step::ConfirmMnemonic => {
let text = t!("wallets.restore_phrase_desc");
let available = !self.mnemonic_setup.mnemonic.has_empty_or_invalid();
(text, available)
}
Step::SetupConnection => (t!("wallets.setup_conn_desc"), self.creation_error.is_none()),
};
// Show step description or error.
let generate_step = step == &Step::EnterMnemonic
&& self.mnemonic_setup.mnemonic.mode() == PhraseMode::Generate;
if (self.mnemonic_setup.mnemonic.valid() && self.creation_error.is_none()) || generate_step
{
ui.label(RichText::new(step_text).size(16.0).color(Colors::gray()));
ui.add_space(6.0);
} else {
next = false;
// Show error text.
if let Some(err) = &self.creation_error {
ui.add_space(10.0);
ui.label(RichText::new(err).size(16.0).color(Colors::red()));
ui.add_space(10.0);
} else {
ui.label(
RichText::new(t!("wallets.not_valid_phrase"))
.size(16.0)
.color(Colors::red()),
);
ui.add_space(4.0);
};
}
// Setup spacing between buttons.
ui.style_mut().spacing.item_spacing = egui::vec2(8.0, 0.0);
// Setup vertical padding inside button.
ui.style_mut().spacing.button_padding = egui::vec2(10.0, 7.0);
match step {
Step::EnterMnemonic => {
ui.columns(2, |columns| {
// Show copy or paste button for mnemonic phrase step.
columns[0].vertical_centered_justified(|ui| {
match self.mnemonic_setup.mnemonic.mode() {
PhraseMode::Generate => {
let c_t = format!("{} {}", COPY, t!("copy"));
View::button(ui, c_t, Colors::white_or_black(false), || {
cb.copy_string_to_buffer(
self.mnemonic_setup.mnemonic.get_phrase(),
);
});
}
PhraseMode::Import => {
let p_t = format!("{} {}", CLIPBOARD_TEXT, t!("paste"));
View::button(ui, p_t, Colors::white_or_black(false), || {
let data = ZeroingString::from(cb.get_string_from_buffer());
self.mnemonic_setup.mnemonic.import(&data);
});
}
}
});
// Show next step or QR code scan button.
columns[1].vertical_centered_justified(|ui| {
if next {
self.next_step_button_ui(ui, on_create);
} else {
let scan_text = format!("{} {}", SCAN, t!("scan"));
View::button(ui, scan_text, Colors::white_or_black(false), || {
self.scan_modal_content = Some(CameraScanContent::default());
// Show QR code scan modal.
Modal::new(QR_CODE_PHRASE_SCAN_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("scan_qr"))
.closeable(false)
.show();
cb.start_camera();
});
}
});
});
}
Step::ConfirmMnemonic => {
// Show next step or paste button.
if next {
self.next_step_button_ui(ui, on_create);
} else {
let paste_text = format!("{} {}", CLIPBOARD_TEXT, t!("paste"));
View::button(ui, paste_text, Colors::white_or_black(false), || {
let data = ZeroingString::from(cb.get_string_from_buffer());
self.mnemonic_setup.mnemonic.import(&data);
});
}
}
Step::SetupConnection => {
if next {
self.next_step_button_ui(ui, on_create);
ui.add_space(2.0);
}
}
}
}
/// Draw button to go to next [`Step`].
fn next_step_button_ui(&mut self, ui: &mut egui::Ui, on_create: impl FnOnce(Wallet)) {
// Setup button text.
let (next_text, text_color, bg_color) = if self.step == Step::SetupConnection {
(
format!("{} {}", CHECK, t!("complete")),
Colors::title(true),
Colors::gold(),
)
} else {
(
t!("continue").into(),
Colors::green(),
Colors::white_or_black(false),
)
};
// Show next step button.
View::colored_text_button_ui(ui, next_text.to_uppercase(), text_color, bg_color, |ui| {
self.step = match self.step {
Step::EnterMnemonic => {
if self.mnemonic_setup.mnemonic.mode() == PhraseMode::Generate {
Step::ConfirmMnemonic
} else {
Step::SetupConnection
}
}
Step::ConfirmMnemonic => Step::SetupConnection,
Step::SetupConnection => {
// Create wallet at last step.
match Wallet::create(
&self.name,
&self.pass,
&self.mnemonic_setup.mnemonic,
&self.network_setup.method,
) {
Ok(w) => {
self.mnemonic_setup.reset();
// Pass created wallet to callback.
(on_create)(w);
Step::EnterMnemonic
}
Err(e) => {
self.creation_error = Some(format!("{:?}", e));
Step::SetupConnection
}
}
}
};
// Check external connections availability on connection setup.
if self.step == Step::SetupConnection {
ExternalConnection::check(None, ui.ctx());
}
});
}
/// Draw wallet creation [`Step`] content.
fn step_content_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
match &self.step {
Step::EnterMnemonic => self.mnemonic_setup.enter_ui(ui, cb),
Step::ConfirmMnemonic => self.mnemonic_setup.confirm_ui(ui, cb),
Step::SetupConnection => {
// Redraw if node is running.
if Node::is_running() && !Content::is_dual_panel_mode(ui.ctx()) {
ui.ctx().request_repaint_after(Node::STATS_UPDATE_DELAY);
}
self.network_setup.ui(ui, cb);
}
}
}
/// Back to previous wallet creation [`Step`], return `true` to close creation.
pub fn on_back(&mut self) -> bool {
match &self.step {
Step::ConfirmMnemonic => {
self.step = Step::EnterMnemonic;
false
}
Step::SetupConnection => {
self.creation_error = None;
self.step = Step::EnterMnemonic;
false
}
_ => true,
}
}
}
-481
View File
@@ -1,481 +0,0 @@
// Copyright 2023 The Grim Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use egui::{Id, Margin, RichText, ScrollArea};
use egui::scroll_area::ScrollBarVisibility;
use grin_util::ZeroingString;
use crate::gui::Colors;
use crate::gui::icons::{CHECK, CLIPBOARD_TEXT, COPY, FOLDER_PLUS, SCAN, SHARE_FAT};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Modal, Content, View};
use crate::gui::views::types::{ModalPosition, TextEditOptions};
use crate::gui::views::wallets::creation::MnemonicSetup;
use crate::gui::views::wallets::creation::types::Step;
use crate::gui::views::wallets::settings::ConnectionSettings;
use crate::node::Node;
use crate::wallet::{ExternalConnection, Wallet};
use crate::wallet::types::PhraseMode;
/// Wallet creation content.
pub struct WalletCreation {
/// Wallet creation step.
step: Option<Step>,
/// Flag to check if wallet creation [`Modal`] was just opened to focus on first field.
modal_just_opened: bool,
/// Wallet name value.
name_edit: String,
/// Password to encrypt created wallet.
pass_edit: String,
/// Mnemonic phrase setup content.
pub(crate) mnemonic_setup: MnemonicSetup,
/// Network setup content.
pub(crate) network_setup: ConnectionSettings,
/// Flag to check if an error occurred during wallet creation.
creation_error: Option<String>,
}
impl Default for WalletCreation {
fn default() -> Self {
Self {
step: None,
modal_just_opened: true,
name_edit: String::from(""),
pass_edit: String::from(""),
mnemonic_setup: MnemonicSetup::default(),
network_setup: ConnectionSettings::default(),
creation_error: None,
}
}
}
impl WalletCreation {
/// Wallet name/password input modal identifier.
pub const NAME_PASS_MODAL: &'static str = "name_pass_modal";
/// Draw wallet creation content.
pub fn ui(&mut self,
ui: &mut egui::Ui,
cb: &dyn PlatformCallbacks,
on_create: impl FnOnce(Wallet)) {
// Show wallet creation step description and confirmation panel.
if self.step.is_some() {
egui::TopBottomPanel::bottom("wallet_creation_step_panel")
.frame(egui::Frame {
fill: Colors::fill(),
inner_margin: Margin {
left: View::far_left_inset_margin(ui) + 8.0,
right: View::get_right_inset() + 8.0,
top: 4.0,
bottom: View::get_bottom_inset(),
},
..Default::default()
})
.show_inside(ui, |ui| {
ui.vertical_centered(|ui| {
ui.vertical_centered(|ui| {
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 2.0, |ui| {
self.step_control_ui(ui, on_create, cb);
});
});
});
});
}
// Show wallet creation step content panel.
egui::CentralPanel::default()
.frame(egui::Frame {
stroke: View::item_stroke(),
inner_margin: Margin {
left: View::far_left_inset_margin(ui) + 4.0,
right: View::get_right_inset() + 4.0,
top: 3.0,
bottom: 4.0,
},
..Default::default()
})
.show_inside(ui, |ui| {
let id = if let Some(step) = &self.step {
format!("creation_step_scroll_{}", step.name())
} else {
"creation_step_scroll".to_owned()
};
ScrollArea::vertical()
.id_source(id)
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.auto_shrink([false; 2])
.show(ui, |ui| {
ui.vertical_centered(|ui| {
let max_width = if self.step == Some(Step::SetupConnection) {
Content::SIDE_PANEL_WIDTH * 1.3
} else {
Content::SIDE_PANEL_WIDTH * 2.0
};
View::max_width_ui(ui, max_width, |ui| {
self.step_content_ui(ui, cb);
});
});
});
});
}
/// Draw [`Step`] description and confirmation control.
fn step_control_ui(&mut self,
ui: &mut egui::Ui,
on_create: impl FnOnce(Wallet),
cb: &dyn PlatformCallbacks) {
if let Some(step) = self.step.clone() {
// Setup step description text and availability.
let (step_text, mut step_available) = match step {
Step::EnterMnemonic => {
let mode = &self.mnemonic_setup.mnemonic.mode;
let text = if mode == &PhraseMode::Generate {
t!("wallets.create_phrase_desc")
} else {
t!("wallets.restore_phrase_desc")
};
let available = !self
.mnemonic_setup
.mnemonic
.words
.contains(&String::from(""));
(text, available)
}
Step::ConfirmMnemonic => {
let text = t!("wallets.restore_phrase_desc");
let available = !self
.mnemonic_setup
.mnemonic
.confirm_words
.contains(&String::from(""));
(text, available)
}
Step::SetupConnection => {
(t!("wallets.setup_conn_desc"), self.creation_error.is_none())
}
};
// Show step description or error if entered phrase is not valid.
if self.mnemonic_setup.valid_phrase && self.creation_error.is_none() {
ui.add_space(2.0);
ui.label(RichText::new(step_text).size(16.0).color(Colors::gray()));
ui.add_space(2.0);
} else {
step_available = false;
// Show error text.
if let Some(err) = &self.creation_error {
ui.add_space(10.0);
ui.label(RichText::new(err)
.size(16.0)
.color(Colors::red()));
ui.add_space(10.0);
} else {
ui.label(RichText::new(&t!("wallets.not_valid_phrase"))
.size(16.0)
.color(Colors::red()));
ui.add_space(2.0);
};
}
if step == Step::EnterMnemonic {
ui.add_space(4.0);
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
ui.columns(2, |columns| {
// Show copy or paste button for mnemonic phrase step.
columns[0].vertical_centered_justified(|ui| {
self.copy_or_paste_button_ui(ui, cb);
});
columns[1].vertical_centered_justified(|ui| {
if step_available {
// Show next step button if there are no empty words.
self.next_step_button_ui(ui, step, on_create);
} else {
// Show QR code scan button.
let scan_text = format!("{} {}", SCAN, t!("scan").to_uppercase());
View::button(ui, scan_text, Colors::white_or_black(false), || {
self.mnemonic_setup.show_qr_scan_modal(cb);
});
}
});
});
ui.add_space(4.0);
} else if step == Step::ConfirmMnemonic {
ui.add_space(4.0);
// Show next step or paste button.
if step_available {
self.next_step_button_ui(ui, step, on_create);
} else {
let paste_text = format!("{} {}", CLIPBOARD_TEXT, t!("paste").to_uppercase());
View::button(ui, paste_text, Colors::white_or_black(false), || {
let data = ZeroingString::from(cb.get_string_from_buffer().trim());
self.mnemonic_setup.mnemonic.import_text(&data, true);
});
}
ui.add_space(4.0);
} else if step_available {
ui.add_space(4.0);
self.next_step_button_ui(ui, step, on_create);
ui.add_space(4.0);
}
ui.add_space(4.0);
}
}
/// Draw copy or paste button at [`Step::EnterMnemonic`].
fn copy_or_paste_button_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
match self.mnemonic_setup.mnemonic.mode {
PhraseMode::Generate => {
// Show copy button.
let c_t = format!("{} {}", COPY, t!("copy").to_uppercase());
View::button(ui, c_t.to_uppercase(), Colors::white_or_black(false), || {
cb.copy_string_to_buffer(self.mnemonic_setup.mnemonic.get_phrase());
});
}
PhraseMode::Import => {
// Show paste button.
let p_t = format!("{} {}", CLIPBOARD_TEXT, t!("paste").to_uppercase());
View::button(ui, p_t, Colors::white_or_black(false), || {
let data = ZeroingString::from(cb.get_string_from_buffer().trim());
self.mnemonic_setup.mnemonic.import_text(&data, false);
});
}
}
}
/// Draw button to go to next [`Step`].
fn next_step_button_ui(&mut self,
ui: &mut egui::Ui,
step: Step,
on_create: impl FnOnce(Wallet)) {
// Setup button text.
let (next_text, text_color, bg_color) = if step == Step::SetupConnection {
(format!("{} {}", CHECK, t!("complete")), Colors::title(true), Colors::gold())
} else {
let text = format!("{} {}", SHARE_FAT, t!("continue"));
(text, Colors::text_button(), Colors::white_or_black(false))
};
// Show next step button.
View::colored_text_button(ui, next_text.to_uppercase(), text_color, bg_color, || {
self.step = if let Some(step) = &self.step {
match step {
Step::EnterMnemonic => {
if self.mnemonic_setup.mnemonic.mode == PhraseMode::Generate {
Some(Step::ConfirmMnemonic)
} else {
// Check if entered phrase was valid.
if self.mnemonic_setup.valid_phrase {
Some(Step::SetupConnection)
} else {
Some(Step::EnterMnemonic)
}
}
}
Step::ConfirmMnemonic => {
Some(Step::SetupConnection)
},
Step::SetupConnection => {
// Create wallet at last step.
let conn_method = &self.network_setup.method;
match Wallet::create(&self.name_edit,
&self.pass_edit,
&self.mnemonic_setup.mnemonic,
conn_method) {
Ok(mut w) => {
// Open created wallet.
w.open(&self.pass_edit).unwrap();
// Pass created wallet to callback.
(on_create)(w);
// Reset input data.
self.step = None;
self.name_edit = String::from("");
self.pass_edit = String::from("");
self.mnemonic_setup.reset();
None
}
Err(e) => {
self.creation_error = Some(format!("{:?}", e));
Some(Step::SetupConnection)
}
}
}
}
} else {
Some(Step::EnterMnemonic)
};
// Check external connections availability on connection setup.
if self.step == Some(Step::SetupConnection) {
ExternalConnection::check_ext_conn_availability(None);
}
});
}
/// Draw wallet creation [`Step`] content.
fn step_content_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
match &self.step {
None => {
// Show wallet creation message if step is empty.
View::center_content(ui, 350.0 + View::get_bottom_inset(), |ui| {
// Show app logo.
View::app_logo_name_version(ui);
ui.add_space(4.0);
let text = t!("wallets.create_desc");
ui.label(RichText::new(text)
.size(16.0)
.color(Colors::gray())
);
ui.add_space(8.0);
// Show wallet creation button.
let add_text = format!("{} {}", FOLDER_PLUS, t!("wallets.add"));
View::button(ui, add_text, Colors::white_or_black(false), || {
self.show_name_pass_modal(cb);
});
});
}
Some(step) => {
match step {
Step::EnterMnemonic => self.mnemonic_setup.ui(ui, cb),
Step::ConfirmMnemonic => self.mnemonic_setup.confirm_ui(ui, cb),
Step::SetupConnection => {
// Redraw if node is running.
if Node::is_running() {
ui.ctx().request_repaint_after(Node::STATS_UPDATE_DELAY);
}
self.network_setup.create_ui(ui, cb)
}
}
}
}
}
/// Check if it's possible to go back for current step.
pub fn can_go_back(&self) -> bool {
self.step.is_some()
}
/// Back to previous wallet creation [`Step`].
pub fn back(&mut self) {
match &self.step {
None => {}
Some(step) => {
match step {
Step::EnterMnemonic => {
self.step = None;
self.name_edit = String::from("");
self.pass_edit = String::from("");
self.mnemonic_setup.reset();
self.creation_error = None;
},
Step::ConfirmMnemonic => self.step = Some(Step::EnterMnemonic),
Step::SetupConnection => self.step = Some(Step::EnterMnemonic)
}
}
}
}
/// Start wallet creation from showing [`Modal`] to enter name and password.
pub fn show_name_pass_modal(&mut self, cb: &dyn PlatformCallbacks) {
// Reset modal values.
self.modal_just_opened = true;
self.name_edit = t!("wallets.default_wallet");
self.pass_edit = String::from("");
// Show modal.
Modal::new(Self::NAME_PASS_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("wallets.add"))
.show();
cb.show_keyboard();
}
/// Draw creating wallet name/password input [`Modal`] content.
pub fn name_pass_modal_ui(&mut self,
ui: &mut egui::Ui,
modal: &Modal,
cb: &dyn PlatformCallbacks) {
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("wallets.name"))
.size(17.0)
.color(Colors::gray()));
ui.add_space(8.0);
// Show wallet name text edit.
let mut name_edit_opts = TextEditOptions::new(Id::from(modal.id).with("name"))
.no_focus();
if self.modal_just_opened {
self.modal_just_opened = false;
name_edit_opts.focus = true;
}
View::text_edit(ui, cb, &mut self.name_edit, &mut name_edit_opts);
ui.add_space(8.0);
ui.label(RichText::new(t!("wallets.pass"))
.size(17.0)
.color(Colors::gray()));
ui.add_space(8.0);
// Draw wallet password text edit.
let mut pass_text_edit_opts = TextEditOptions::new(Id::from(modal.id).with("pass"))
.password()
.no_focus();
View::text_edit(ui, cb, &mut self.pass_edit, &mut pass_text_edit_opts);
ui.add_space(12.0);
});
// Show modal buttons.
ui.scope(|ui| {
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
// Close modal.
cb.hide_keyboard();
modal.close();
});
});
columns[1].vertical_centered_justified(|ui| {
let mut on_next = || {
// Check if input values are not empty.
if self.name_edit.is_empty() || self.pass_edit.is_empty() {
return;
}
self.step = Some(Step::EnterMnemonic);
cb.hide_keyboard();
modal.close();
};
// Go to next creation step on Enter button press.
View::on_enter_key(ui, || {
(on_next)();
});
View::button(ui, t!("continue"), Colors::white_or_black(false), on_next);
});
});
ui.add_space(6.0);
});
}
}
+266 -390
View File
@@ -17,431 +17,307 @@ use egui::{Id, RichText};
use crate::gui::Colors;
use crate::gui::icons::PENCIL;
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{CameraContent, Modal, Content, View};
use crate::gui::views::types::{ModalContainer, ModalPosition, QrScanResult, TextEditOptions};
use crate::gui::views::types::{ContentContainer, ModalPosition};
use crate::gui::views::{Content, Modal, TextEdit, View};
use crate::wallet::Mnemonic;
use crate::wallet::types::{PhraseMode, PhraseSize};
use crate::wallet::types::{PhraseMode, PhraseSize, PhraseWord};
/// Mnemonic phrase setup content.
pub struct MnemonicSetup {
/// Current mnemonic phrase.
pub(crate) mnemonic: Mnemonic,
/// Current mnemonic phrase.
pub mnemonic: Mnemonic,
/// Flag to check if entered phrase was valid.
pub(crate) valid_phrase: bool,
/// Current word number to edit at [`Modal`].
word_num_edit: usize,
/// Entered word value for [`Modal`].
word_edit: String,
/// Flag to check if entered word is valid.
valid_word_edit: bool,
/// Camera content for QR scan [`Modal`].
camera_content: CameraContent,
/// Flag to check if recovery phrase was found at QR code scanning [`Modal`].
scan_phrase_not_found: Option<bool>,
/// [`Modal`] identifiers allowed at this ui container.
modal_ids: Vec<&'static str>
/// Current word number to edit at [`Modal`].
word_index_edit: usize,
/// Entered word value for [`Modal`].
word_edit: String,
/// Flag to check if entered word is valid at [`Modal`].
valid_word_edit: bool,
}
/// Identifier for word input [`Modal`].
pub const WORD_INPUT_MODAL: &'static str = "word_input_modal";
/// Identifier for QR code recovery phrase scan [`Modal`].
const QR_CODE_PHRASE_SCAN_MODAL: &'static str = "qr_code_rec_phrase_scan_modal";
impl Default for MnemonicSetup {
fn default() -> Self {
Self {
mnemonic: Mnemonic::default(),
valid_phrase: true,
word_num_edit: 0,
word_edit: String::from(""),
valid_word_edit: true,
camera_content: CameraContent::default(),
scan_phrase_not_found: None,
modal_ids: vec![
WORD_INPUT_MODAL,
QR_CODE_PHRASE_SCAN_MODAL
]
}
}
fn default() -> Self {
Self {
mnemonic: Mnemonic::default(),
word_index_edit: 0,
word_edit: String::from(""),
valid_word_edit: true,
}
}
}
impl ModalContainer for MnemonicSetup {
fn modal_ids(&self) -> &Vec<&'static str> {
&self.modal_ids
}
impl ContentContainer for MnemonicSetup {
fn modal_ids(&self) -> Vec<&'static str> {
vec![WORD_INPUT_MODAL]
}
fn modal_ui(&mut self,
ui: &mut egui::Ui,
modal: &Modal,
cb: &dyn PlatformCallbacks) {
match modal.id {
WORD_INPUT_MODAL => self.word_modal_ui(ui, modal, cb),
QR_CODE_PHRASE_SCAN_MODAL => self.scan_qr_modal_ui(ui, modal, cb),
_ => {}
}
}
fn modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
match modal.id {
WORD_INPUT_MODAL => self.word_modal_ui(ui, modal, cb),
_ => {}
}
}
fn container_ui(&mut self, _: &mut egui::Ui, _: &dyn PlatformCallbacks) {}
}
impl MnemonicSetup {
/// Draw content for phrase input step.
pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
// Draw modal content for current ui container.
self.current_modal_ui(ui, cb);
/// Draw content for phrase enter step.
pub fn enter_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
self.ui(ui, cb);
ui.add_space(10.0);
ui.add_space(10.0);
// Show mode setup.
let mut mode = self.mnemonic.mode();
ui.columns(2, |columns| {
columns[0].vertical_centered(|ui| {
let create_mode = PhraseMode::Generate;
let create_text = t!("create");
View::radio_value(ui, &mut mode, create_mode, create_text);
});
columns[1].vertical_centered(|ui| {
let import_mode = PhraseMode::Import;
let import_text = t!("wallets.recover");
View::radio_value(ui, &mut mode, import_mode, import_text);
});
});
if mode != self.mnemonic.mode() {
self.mnemonic.set_mode(mode);
}
// Show mode and type setup.
self.mode_type_ui(ui);
ui.add_space(10.0);
ui.vertical_centered(|ui| {
ui.label(
RichText::new(t!("wallets.words_count"))
.size(16.0)
.color(Colors::gray()),
);
});
ui.add_space(6.0);
ui.add_space(12.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(6.0);
// Show mnemonic phrase size setup.
let mut size = self.mnemonic.size();
ui.columns(5, |columns| {
for (index, word) in PhraseSize::VALUES.iter().enumerate() {
columns[index].vertical_centered(|ui| {
let text = word.value().to_string();
View::radio_value(ui, &mut size, word.clone(), text);
});
}
});
if size != self.mnemonic.size() {
self.mnemonic.set_size(size);
}
// Show words setup.
self.word_list_ui(ui, self.mnemonic.mode == PhraseMode::Import, cb);
}
ui.add_space(12.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(6.0);
/// Draw content for phrase confirmation step.
pub fn confirm_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
// Draw modal content for current ui container.
self.current_modal_ui(ui, cb);
// Show words setup.
self.word_list_ui(ui, self.mnemonic.mode() == PhraseMode::Import);
}
/// Draw content for phrase confirmation step.
pub fn confirm_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
self.ui(ui, cb);
ui.add_space(4.0);
ui.vertical_centered(|ui| {
let text = format!("{}:", t!("wallets.recovery_phrase"));
ui.label(RichText::new(text).size(16.0).color(Colors::gray()));
});
ui.add_space(4.0);
self.word_list_ui(ui, true, cb);
}
ui.add_space(4.0);
ui.vertical_centered(|ui| {
let text = format!("{}:", t!("wallets.recovery_phrase"));
ui.label(RichText::new(text).size(16.0).color(Colors::gray()));
});
ui.add_space(4.0);
self.word_list_ui(ui, true);
}
/// Draw mode and size setup.
fn mode_type_ui(&mut self, ui: &mut egui::Ui) {
// Show mode setup.
let mut mode = self.mnemonic.mode.clone();
ui.columns(2, |columns| {
columns[0].vertical_centered(|ui| {
let create_mode = PhraseMode::Generate;
let create_text = t!("create");
View::radio_value(ui, &mut mode, create_mode, create_text);
});
columns[1].vertical_centered(|ui| {
let import_mode = PhraseMode::Import;
let import_text = t!("wallets.recover");
View::radio_value(ui, &mut mode, import_mode, import_text);
});
});
if mode != self.mnemonic.mode {
self.mnemonic.set_mode(mode)
}
/// Draw grid of words for mnemonic phrase.
fn word_list_ui(&mut self, ui: &mut egui::Ui, edit: bool) {
ui.add_space(6.0);
ui.scope(|ui| {
// Setup spacing between columns.
ui.spacing_mut().item_spacing = egui::Vec2::new(6.0, 6.0);
ui.add_space(10.0);
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("wallets.words_count"))
.size(16.0)
.color(Colors::gray())
);
});
ui.add_space(6.0);
// Select list of words based on current mode and edit flag.
let words = self.mnemonic.words(edit);
// Show mnemonic phrase size setup.
let mut size = self.mnemonic.size.clone();
ui.columns(5, |columns| {
for (index, word) in PhraseSize::VALUES.iter().enumerate() {
columns[index].vertical_centered(|ui| {
let text = word.value().to_string();
View::radio_value(ui, &mut size, word.clone(), text);
});
}
});
if size != self.mnemonic.size {
self.mnemonic.set_size(size);
}
}
let mut word_number = 0;
let cols = list_columns_count(ui);
let _ = words
.chunks(cols)
.map(|chunk| {
let size = chunk.len();
word_number += 1;
if size > 1 {
ui.columns(cols, |columns| {
columns[0].horizontal(|ui| {
let word = chunk.get(0).unwrap();
self.word_item_ui(ui, word_number, word, edit);
});
columns[1].horizontal(|ui| {
word_number += 1;
let word = chunk.get(1).unwrap();
self.word_item_ui(ui, word_number, word, edit);
});
if size > 2 {
columns[2].horizontal(|ui| {
word_number += 1;
let word = chunk.get(2).unwrap();
self.word_item_ui(ui, word_number, word, edit);
});
}
if size > 3 {
columns[3].horizontal(|ui| {
word_number += 1;
let word = chunk.get(3).unwrap();
self.word_item_ui(ui, word_number, word, edit);
});
}
});
} else {
ui.columns(cols, |columns| {
columns[0].horizontal(|ui| {
let word = chunk.get(0).unwrap();
self.word_item_ui(ui, word_number, word, edit);
});
});
}
})
.collect::<Vec<_>>();
});
ui.add_space(6.0);
}
/// Draw list of words for mnemonic phrase.
fn word_list_ui(&mut self, ui: &mut egui::Ui, edit_words: bool, cb: &dyn PlatformCallbacks) {
ui.add_space(6.0);
ui.scope(|ui| {
// Setup spacing between columns.
ui.spacing_mut().item_spacing = egui::Vec2::new(6.0, 6.0);
/// Draw word grid item.
fn word_item_ui(&mut self, ui: &mut egui::Ui, num: usize, word: &PhraseWord, edit: bool) {
let color = if !word.valid || (word.text.is_empty() && !self.mnemonic.valid()) {
Colors::red()
} else {
Colors::white_or_black(true)
};
if edit {
ui.add_space(6.0);
View::button(
ui,
PENCIL.to_string(),
Colors::white_or_black(false),
|| {
self.word_index_edit = num - 1;
self.word_edit = word.text.clone();
self.valid_word_edit = word.valid;
// Show word edit modal.
Modal::new(WORD_INPUT_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("wallets.recovery_phrase"))
.show();
},
);
ui.label(
RichText::new(format!("#{} {}", num, word.text))
.size(17.0)
.color(color),
);
} else {
ui.add_space(12.0);
let text = format!("#{} {}", num, word.text);
ui.label(RichText::new(text).size(17.0).color(color));
}
}
// Select list of words based on current mode and edit flag.
let words = match self.mnemonic.mode {
PhraseMode::Generate => {
if edit_words {
&self.mnemonic.confirm_words
} else {
&self.mnemonic.words
}
}
PhraseMode::Import => &self.mnemonic.words
}.clone();
/// Reset mnemonic phrase state to default values.
pub fn reset(&mut self) {
self.mnemonic = Mnemonic::default();
}
let mut word_number = 0;
let cols = list_columns_count(ui);
let _ = words.chunks(cols).map(|chunk| {
let size = chunk.len();
word_number += 1;
if size > 1 {
ui.columns(cols, |columns| {
columns[0].horizontal(|ui| {
let word = chunk.get(0).unwrap();
self.word_item_ui(ui, word_number, word, edit_words, cb);
});
columns[1].horizontal(|ui| {
word_number += 1;
let word = chunk.get(1).unwrap();
self.word_item_ui(ui, word_number, word, edit_words, cb);
});
if size > 2 {
columns[2].horizontal(|ui| {
word_number += 1;
let word = chunk.get(2).unwrap();
self.word_item_ui(ui, word_number, word, edit_words, cb);
});
}
if size > 3 {
columns[3].horizontal(|ui| {
word_number += 1;
let word = chunk.get(3).unwrap();
self.word_item_ui(ui, word_number, word, edit_words, cb);
});
}
});
} else {
ui.columns(cols, |columns| {
columns[0].horizontal(|ui| {
let word = chunk.get(0).unwrap();
self.word_item_ui(ui, word_number, word, edit_words, cb);
});
});
}
}).collect::<Vec<_>>();
});
ui.add_space(6.0);
}
/// Draw word input [`Modal`] content.
fn word_modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
let on_save = |c: &mut MnemonicSetup| {
// Insert word checking validity.
let word = &c.word_edit.trim().to_string();
c.valid_word_edit = c.mnemonic.insert(c.word_index_edit, word);
if !c.valid_word_edit {
return;
}
// Close modal or go to next word to edit.
let next_word = c.mnemonic.get(c.word_index_edit + 1);
let close_modal = next_word.is_none()
|| (!next_word.as_ref().unwrap().text.is_empty() && next_word.unwrap().valid);
if close_modal {
Modal::close();
} else {
c.word_index_edit += 1;
c.word_edit = String::from("");
}
};
/// Draw word list item for current mode.
fn word_item_ui(&mut self,
ui: &mut egui::Ui,
num: usize,
word: &String,
edit: bool,
cb: &dyn PlatformCallbacks) {
if edit {
ui.add_space(6.0);
View::button(ui, PENCIL.to_string(), Colors::button(), || {
// Setup modal values.
self.word_num_edit = num;
self.word_edit = word.clone();
self.valid_word_edit = true;
// Show word edit modal.
Modal::new(WORD_INPUT_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("wallets.recovery_phrase"))
.show();
cb.show_keyboard();
});
ui.label(RichText::new(format!("#{} {}", num, word))
.size(17.0)
.color(Colors::white_or_black(true)));
} else {
ui.add_space(12.0);
let text = format!("#{} {}", num, word);
ui.label(RichText::new(text).size(17.0).color(Colors::white_or_black(true)));
}
}
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(
RichText::new(t!("wallets.enter_word", "number" => self.word_index_edit + 1))
.size(17.0)
.color(Colors::gray()),
);
ui.add_space(8.0);
/// Reset mnemonic phrase to default values.
pub fn reset(&mut self) {
self.mnemonic = Mnemonic::default();
self.valid_phrase = true;
}
// Draw word value text edit.
let mut word_edit = TextEdit::new(Id::from(modal.id).with(self.word_index_edit));
word_edit.ui(ui, &mut self.word_edit, cb);
if word_edit.enter_pressed {
on_save(self);
}
/// Draw word input [`Modal`] content.
fn word_modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("wallets.enter_word", "number" => self.word_num_edit))
.size(17.0)
.color(Colors::gray()));
ui.add_space(8.0);
// Show error when specified word is not valid.
if !self.valid_word_edit {
ui.add_space(12.0);
ui.label(
RichText::new(t!("wallets.not_valid_word"))
.size(17.0)
.color(Colors::red()),
);
}
ui.add_space(12.0);
});
// Draw word value text edit.
let mut text_edit_opts = TextEditOptions::new(
Id::from(modal.id).with(self.word_num_edit)
);
View::text_edit(ui, cb, &mut self.word_edit, &mut text_edit_opts);
// Show modal buttons.
ui.scope(|ui| {
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
// Show error when specified word is not valid.
if !self.valid_word_edit {
ui.add_space(12.0);
ui.label(RichText::new(t!("wallets.not_valid_word"))
.size(17.0)
.color(Colors::red()));
}
ui.add_space(12.0);
});
// Show modal buttons.
ui.scope(|ui| {
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
// Close modal.
cb.hide_keyboard();
modal.close();
});
});
columns[1].vertical_centered_justified(|ui| {
// Callback to save the word.
let mut save = || {
self.word_edit = self.word_edit.trim().to_string();
// Check if word is valid.
if !self.mnemonic.is_valid_word(&self.word_edit) {
self.valid_word_edit = false;
return;
}
self.valid_word_edit = true;
// Select list where to save word.
let words = match self.mnemonic.mode {
PhraseMode::Generate => &mut self.mnemonic.confirm_words,
PhraseMode::Import => &mut self.mnemonic.words
};
// Save word at list.
let word_index = self.word_num_edit - 1;
words.remove(word_index);
words.insert(word_index, self.word_edit.clone());
// Close modal or go to next word to edit.
let close_modal = words.len() == self.word_num_edit
|| !words.get(self.word_num_edit).unwrap().is_empty();
if close_modal {
// Check if entered phrase was valid when all words were entered.
if !self.mnemonic.words.contains(&String::from("")) {
self.valid_phrase = self.mnemonic.is_valid_phrase();
}
cb.hide_keyboard();
modal.close();
} else {
self.word_num_edit += 1;
self.word_edit = String::from("");
}
};
// Call save on Enter key press.
View::on_enter_key(ui, || {
(save)();
});
// Show save button.
View::button(ui, t!("continue"), Colors::white_or_black(false), save);
});
});
ui.add_space(6.0);
});
}
/// Show QR code recovery phrase scanner [`Modal`].
pub fn show_qr_scan_modal(&mut self, cb: &dyn PlatformCallbacks) {
self.scan_phrase_not_found = None;
// Show QR code scan modal.
Modal::new(QR_CODE_PHRASE_SCAN_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("scan_qr"))
.closeable(false)
.show();
cb.start_camera();
}
/// Draw QR code scan [`Modal`] content.
fn scan_qr_modal_ui(&mut self,
ui: &mut egui::Ui,
modal: &Modal,
cb: &dyn PlatformCallbacks) {
// Show scan result if exists or show camera content while scanning.
if let Some(_) = &self.scan_phrase_not_found {
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("wallets.rec_phrase_not_found"))
.size(17.0)
.color(Colors::red()));
});
ui.add_space(6.0);
} else if let Some(result) = self.camera_content.qr_scan_result() {
cb.stop_camera();
self.camera_content.clear_state();
match &result {
QrScanResult::Text(text) => {
self.mnemonic.import_text(text, false);
if self.mnemonic.is_valid_phrase() {
modal.close();
return;
}
}
_ => {}
}
// Set an error when found phrase was not valid.
self.scan_phrase_not_found = Some(true);
Modal::set_title(t!("scan_result"));
} else {
ui.add_space(6.0);
self.camera_content.ui(ui, cb);
ui.add_space(6.0);
}
if self.scan_phrase_not_found.is_some() {
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(ui, t!("close"), Colors::white_or_black(false), || {
self.scan_phrase_not_found = None;
modal.close();
});
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("repeat"), Colors::white_or_black(false), || {
Modal::set_title(t!("scan_qr"));
self.scan_phrase_not_found = None;
cb.start_camera();
});
});
});
} else {
ui.vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
cb.stop_camera();
modal.close();
});
});
}
ui.add_space(6.0);
}
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(
ui,
t!("modal.cancel"),
Colors::white_or_black(false),
|| {
// Close modal.
Modal::close();
},
);
});
columns[1].vertical_centered_justified(|ui| {
// Show save button.
View::button(ui, t!("continue"), Colors::white_or_black(false), || {
on_save(self);
});
});
});
ui.add_space(6.0);
});
}
}
/// Calculate word list columns count based on available ui width.
fn list_columns_count(ui: &mut egui::Ui) -> usize {
let w = ui.available_width();
let min_panel_w = Content::SIDE_PANEL_WIDTH - 12.0;
let double_min_panel_w = min_panel_w * 2.0;
if w >= min_panel_w * 1.5 && w < double_min_panel_w {
3
} else if w >= double_min_panel_w {
4
} else {
2
}
}
let w = ui.available_width();
let min_panel_w = Content::SIDE_PANEL_WIDTH - 12.0;
let double_min_panel_w = min_panel_w * 2.0;
if w >= min_panel_w * 1.5 && w < double_min_panel_w {
3
} else if w >= double_min_panel_w {
4
} else {
2
}
}
+3 -3
View File
@@ -15,7 +15,7 @@
mod mnemonic;
pub use mnemonic::MnemonicSetup;
mod creation;
pub use creation::WalletCreation;
mod content;
pub use content::WalletCreationContent;
pub mod types;
pub mod types;
+15 -15
View File
@@ -15,21 +15,21 @@
/// Wallet creation step.
#[derive(PartialEq, Clone)]
pub enum Step {
/// Mnemonic phrase input.
EnterMnemonic,
/// Mnemonic phrase confirmation.
ConfirmMnemonic,
/// Wallet connection setup.
SetupConnection
/// Mnemonic phrase input.
EnterMnemonic,
/// Mnemonic phrase confirmation.
ConfirmMnemonic,
/// Wallet connection setup.
SetupConnection,
}
impl Step {
/// Short name representing creation step.
pub fn name(&self) -> String {
match *self {
Step::EnterMnemonic => "enter_phrase".to_owned(),
Step::ConfirmMnemonic => "confirm_phrase".to_owned(),
Step::SetupConnection => "setup_conn".to_owned(),
}
}
}
/// Short name representing creation step.
pub fn name(&self) -> String {
match *self {
Step::EnterMnemonic => "enter_phrase".to_owned(),
Step::ConfirmMnemonic => "confirm_phrase".to_owned(),
Step::SetupConnection => "setup_conn".to_owned(),
}
}
}
+2 -2
View File
@@ -12,11 +12,11 @@
// See the License for the specific language governing permissions and
// limitations under the License.
pub mod modals;
mod creation;
pub mod modals;
mod content;
pub use content::*;
mod wallet;
use wallet::*;
use wallet::*;
+118
View File
@@ -0,0 +1,118 @@
// Copyright 2024 The Grim Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use egui::{Id, RichText};
use grin_util::ZeroingString;
use crate::gui::Colors;
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Modal, TextEdit, View};
/// Initial wallet creation [`Modal`] content.
pub struct AddWalletModal {
/// Wallet name.
pub name_edit: String,
/// Password to encrypt created wallet.
pub pass_edit: String,
}
impl Default for AddWalletModal {
fn default() -> Self {
Self {
name_edit: t!("wallets.default_wallet").into(),
pass_edit: "".to_string(),
}
}
}
impl AddWalletModal {
/// Draw creating wallet name/password input [`Modal`] content.
pub fn ui(
&mut self,
ui: &mut egui::Ui,
modal: &Modal,
cb: &dyn PlatformCallbacks,
mut on_input: impl FnMut(String, ZeroingString),
) {
let mut on_next = |m: &mut AddWalletModal| {
let name = m.name_edit.clone();
let pass = m.pass_edit.clone();
if name.is_empty() || pass.is_empty() {
return;
}
Modal::close();
on_input(name, ZeroingString::from(pass));
};
ui.vertical_centered(|ui| {
ui.add_space(6.0);
ui.label(
RichText::new(t!("wallets.name"))
.size(17.0)
.color(Colors::gray()),
);
ui.add_space(8.0);
// Show wallet name text edit.
let mut name_input = TextEdit::new(Id::from(modal.id).with("name")).focus(false);
name_input.ui(ui, &mut self.name_edit, cb);
ui.add_space(8.0);
ui.label(
RichText::new(t!("wallets.pass"))
.size(17.0)
.color(Colors::gray()),
);
ui.add_space(8.0);
// Show wallet password text edit.
let mut pass_input = TextEdit::new(Id::from(modal.id).with("pass"))
.password()
.focus(Modal::first_draw());
if name_input.enter_pressed {
pass_input.focus_request();
}
pass_input.ui(ui, &mut self.pass_edit, cb);
if pass_input.enter_pressed {
on_next(self);
}
ui.add_space(12.0);
});
// Show modal buttons.
ui.scope(|ui| {
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(
ui,
t!("modal.cancel"),
Colors::white_or_black(false),
|| {
// Close modal.
Modal::close();
},
);
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("continue"), Colors::white_or_black(false), || {
on_next(self);
});
});
});
ui.add_space(6.0);
});
}
}
+141
View File
@@ -0,0 +1,141 @@
// Copyright 2026 The Grim Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use egui::scroll_area::ScrollBarVisibility;
use egui::{Id, OpenUrl, RichText, ScrollArea};
use crate::gui::Colors;
use crate::gui::icons::{BRACKETS_CURLY, GITHUB_LOGO, TELEGRAM_LOGO};
use crate::gui::views::{Modal, View};
/// Application release changelog content.
pub struct ChangelogContent {
/// Changelog text.
changelog: String,
}
/// Endpoint for GitHub repository.
const GITHUB_URL: &'static str = "https://github.com/GetGrin/grim";
/// Endpoint for Telegram releases channel.
const TELEGRAM_URL: &'static str = "https://t.me/grim_releases";
/// Endpoint for git repository.
const GIT_URL: &'static str = "https://code.gri.mw/GUI/grim";
impl ChangelogContent {
/// Create new content instance.
pub fn new(changelog: String) -> Self {
Self { changelog }
}
/// Identifier for [`Modal`].
pub const MODAL_ID: &'static str = "release_changelog_modal";
/// Draw changelog [`Modal`] content.
pub fn ui(&mut self, ui: &mut egui::Ui) {
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(
RichText::new(t!("changelog"))
.size(16.0)
.color(Colors::gray()),
);
});
ui.add_space(6.0);
// Show changelog text.
ui.vertical_centered(|ui| {
let scroll_id = Id::from("release_changelog");
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(3.0);
ScrollArea::vertical()
.id_salt(scroll_id)
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.max_height(128.0)
.auto_shrink([false; 2])
.show(ui, |ui| {
ui.add_space(7.0);
let input_id = scroll_id.with("_input");
egui::TextEdit::multiline(&mut self.changelog)
.id(input_id)
.font(egui::TextStyle::Small)
.desired_rows(5)
.interactive(false)
.desired_width(f32::INFINITY)
.show(ui);
ui.add_space(6.0);
});
});
ui.add_space(2.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(8.0);
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
ui.columns(3, |columns| {
columns[0].vertical_centered_justified(|ui| {
// Draw button to open GitHub link.
let mut github_clicked = false;
View::button(ui, GITHUB_LOGO, Colors::white_or_black(false), || {
github_clicked = true;
});
if github_clicked {
ui.ctx().open_url(OpenUrl {
url: GITHUB_URL.into(),
new_tab: true,
});
}
});
columns[1].vertical_centered_justified(|ui| {
// Draw button to open Telegram link.
let mut tg_clicked = false;
View::button(ui, TELEGRAM_LOGO, Colors::white_or_black(false), || {
tg_clicked = true;
});
if tg_clicked {
ui.ctx().open_url(OpenUrl {
url: TELEGRAM_URL.into(),
new_tab: true,
});
}
});
columns[2].vertical_centered_justified(|ui| {
// Draw button to open repository link.
let mut git_clicked = false;
View::button(ui, BRACKETS_CURLY, Colors::white_or_black(false), || {
git_clicked = true;
});
if git_clicked {
ui.ctx().open_url(OpenUrl {
url: GIT_URL.into(),
new_tab: true,
});
}
});
});
ui.add_space(8.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(8.0);
// Show button to close modal.
ui.vertical_centered_justified(|ui| {
View::button(ui, t!("close"), Colors::white_or_black(false), || {
Modal::close();
});
});
ui.add_space(6.0);
}
}
-153
View File
@@ -1,153 +0,0 @@
// Copyright 2024 The Grim Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use egui::{RichText, ScrollArea};
use egui::scroll_area::ScrollBarVisibility;
use crate::gui::Colors;
use crate::gui::icons::{CHECK, CHECK_FAT, PLUS_CIRCLE};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Modal, View};
use crate::gui::views::network::ConnectionsContent;
use crate::gui::views::network::modals::ExternalConnectionModal;
use crate::wallet::{ConnectionsConfig, ExternalConnection};
/// Wallet connection [`Modal`] content.
pub struct WalletConnectionModal {
/// Current external connection.
pub ext_conn: Option<ExternalConnection>,
/// Flag to show connection creation.
show_conn_creation: bool,
/// External connection creation content.
add_ext_conn_content: ExternalConnectionModal
}
impl WalletConnectionModal {
/// Create from provided wallet connection.
pub fn new(ext_conn: Option<ExternalConnection>) -> Self {
ExternalConnection::check_ext_conn_availability(None);
Self {
ext_conn,
show_conn_creation: false,
add_ext_conn_content: ExternalConnectionModal::new(None),
}
}
/// Draw [`Modal`] content.
pub fn ui(&mut self,
ui: &mut egui::Ui,
modal: &Modal,
cb: &dyn PlatformCallbacks,
on_select: impl Fn(Option<i64>)) {
ui.add_space(4.0);
// Draw external connection creation content.
if self.show_conn_creation {
self.add_ext_conn_content.ui(ui, cb, modal, |conn| {
on_select(Some(conn.id));
});
return;
}
let ext_conn_list = ConnectionsConfig::ext_conn_list();
ScrollArea::vertical()
.max_height(if ext_conn_list.len() < 4 {
330.0
} else {
350.0
})
.id_source("integrated_node")
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.auto_shrink([true; 2])
.show(ui, |ui| {
ui.add_space(2.0);
// Show integrated node selection.
ConnectionsContent::integrated_node_item_ui(ui, |ui| {
let is_current_method = self.ext_conn.is_none();
if !is_current_method {
View::item_button(ui, View::item_rounding(0, 1, true), CHECK, None, || {
self.ext_conn = None;
on_select(None);
modal.close();
});
} else {
ui.add_space(14.0);
ui.label(RichText::new(CHECK_FAT).size(20.0).color(Colors::green()));
ui.add_space(14.0);
}
});
// Show button to add new external node connection.
ui.add_space(8.0);
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("wallets.ext_conn"))
.size(16.0)
.color(Colors::gray()));
ui.add_space(6.0);
let add_node_text = format!("{} {}", PLUS_CIRCLE, t!("wallets.add_node"));
View::button(ui, add_node_text, Colors::button(), || {
self.show_conn_creation = true;
});
});
ui.add_space(4.0);
if !ext_conn_list.is_empty() {
ui.add_space(8.0);
for (index, conn) in ext_conn_list.iter().enumerate() {
ui.horizontal_wrapped(|ui| {
// Draw external connection item.
let len = ext_conn_list.len();
ConnectionsContent::ext_conn_item_ui(ui, conn, index, len, |ui| {
// Draw button to select connection.
let is_current_method = if let Some(c) = self.ext_conn.as_ref() {
c.id == conn.id
} else {
false
};
if !is_current_method {
let button_rounding = View::item_rounding(index, len, true);
View::item_button(ui, button_rounding, CHECK, None, || {
self.ext_conn = Some(conn.clone());
on_select(Some(conn.id));
modal.close();
});
} else {
ui.add_space(12.0);
ui.label(RichText::new(CHECK_FAT)
.size(20.0)
.color(Colors::green()));
}
});
});
}
}
ui.add_space(4.0);
});
ui.add_space(2.0);
View::horizontal_line(ui, Colors::stroke());
ui.add_space(6.0);
// Show button to close modal.
ui.vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
modal.close();
});
});
ui.add_space(6.0);
}
}
+176
View File
@@ -0,0 +1,176 @@
// Copyright 2024 The Grim Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use egui::scroll_area::ScrollBarVisibility;
use egui::{Align, Layout, RichText, ScrollArea, StrokeKind};
use crate::gui::Colors;
use crate::gui::icons::{CHECK, COMPUTER_TOWER, FOLDER_OPEN, GLOBE_SIMPLE, PLUGS_CONNECTED};
use crate::gui::views::wallets::wallet::types::wallet_status_text;
use crate::gui::views::{Modal, View};
use crate::wallet::types::ConnectionMethod;
use crate::wallet::{Wallet, WalletList};
/// Wallet list [`Modal`] content
pub struct WalletListModal {
/// Selected wallet id.
selected_id: Option<i64>,
/// Optional data to pass after wallet selection.
data: Option<String>,
/// Flag to check if wallet can be opened from the list.
can_open: bool,
}
impl WalletListModal {
/// Create new content instance.
pub fn new(selected_id: Option<i64>, data: Option<String>, can_open: bool) -> Self {
Self {
selected_id,
data,
can_open,
}
}
/// Draw content.
pub fn ui(
&mut self,
ui: &mut egui::Ui,
wallets: &WalletList,
mut on_select: impl FnMut(Wallet, Option<String>),
) {
ui.add_space(4.0);
ScrollArea::vertical()
.max_height(373.0)
.id_salt("select_wallet_list_scroll")
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.auto_shrink([true; 2])
.show(ui, |ui| {
ui.add_space(2.0);
ui.vertical_centered(|ui| {
let data = self.data.clone();
for wallet in wallets.list() {
// Draw wallet list item.
self.wallet_item_ui(ui, wallet, || {
Modal::close();
on_select(wallet.clone(), data.clone());
});
ui.add_space(5.0);
}
});
});
ui.add_space(2.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(6.0);
// Show button to close modal.
ui.vertical_centered_justified(|ui| {
View::button(
ui,
t!("modal.cancel"),
Colors::white_or_black(false),
|| {
self.data = None;
Modal::close();
},
);
});
ui.add_space(6.0);
}
/// Draw wallet list item with provided callback on select.
fn wallet_item_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, on_select: impl FnOnce()) {
let config = wallet.get_config();
let id = config.id;
// Draw round background.
let mut rect = ui.available_rect_before_wrap();
rect.set_height(78.0);
let rounding = View::item_rounding(0, 1, false);
ui.painter().rect(
rect,
rounding,
Colors::fill(),
View::hover_stroke(),
StrokeKind::Outside,
);
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
if self.can_open {
// Show button to select or open closed wallet.
let icon = if wallet.is_open() { CHECK } else { FOLDER_OPEN };
View::item_button(ui, View::item_rounding(0, 1, true), icon, None, || {
on_select();
});
} else {
// Draw button to select wallet.
if self.selected_id.unwrap_or(0) == id {
View::selected_item_check(ui);
} else {
View::item_button(ui, View::item_rounding(0, 1, true), CHECK, None, || {
on_select();
});
}
}
let layout_size = ui.available_size();
ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| {
ui.add_space(6.0);
ui.vertical(|ui| {
ui.add_space(3.0);
// Show wallet name text.
ui.with_layout(Layout::left_to_right(Align::Min), |ui| {
ui.add_space(1.0);
View::ellipsize_text(ui, config.name, 18.0, Colors::title(false));
});
// Show wallet connection text.
let connection = wallet.get_current_connection();
let conn_text = match connection {
ConnectionMethod::Integrated => {
format!("{} {}", COMPUTER_TOWER, t!("network.node"))
}
ConnectionMethod::External(_, url) => format!("{} {}", GLOBE_SIMPLE, url),
};
ui.label(
RichText::new(conn_text)
.size(15.0)
.color(Colors::text(false)),
);
ui.add_space(1.0);
// Show wallet API text or open status.
if self.can_open {
ui.label(
RichText::new(wallet_status_text(wallet))
.size(15.0)
.color(Colors::gray()),
);
} else {
let address = if let Some(port) = config.api_port {
format!("127.0.0.1:{}", port)
} else {
"-".to_string()
};
let api_text = format!("{} {}", PLUGS_CONNECTED, address);
ui.label(RichText::new(api_text).size(15.0).color(Colors::gray()));
}
ui.add_space(3.0);
});
});
});
}
}

Some files were not shown because too many files have changed in this diff Show More