224 Commits

Author SHA1 Message Date
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
144 changed files with 18808 additions and 10635 deletions
+69
View File
@@ -0,0 +1,69 @@
name: Test build
on:
push:
tags-ignore:
- "*"
branches-ignore:
- master
- ci
jobs:
build:
runs-on: ubuntu
steps:
- uses: actions/checkout@v6
with:
submodules: recursive
- 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"
+454
View File
@@ -0,0 +1,454 @@
name: Release build
on:
push:
branches:
- master
- ci
tags-ignore:
- "*-dev*"
jobs:
version:
runs-on: ubuntu
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
with:
submodules: recursive
- 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
- 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 }})"
- 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
last_tag=$(git describe --abbrev=0 --tags $(git rev-list --tags --skip=1 --max-count=1))
echo "last_tag=${last_tag}" >> "$FORGEJO_OUTPUT"
android_libs:
if: ${{ forgejo.ref_type == 'branch' || needs.version.outputs.exists == 'false' }}
runs-on: ubuntu
needs: version
steps:
- uses: actions/checkout@v6
with:
submodules: recursive
- 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: 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 libs
run: |
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: Upload artifacts
run: |
cd android/app/src/main
tar -czf jniLibs.tar.gz jniLibs
curl -v -u ${{ secrets.MAVEN_USER }}:${{ secrets.MAVEN_PASSWORD }} --upload-file jniLibs.tar.gz ${{ secrets.MAVEN_HOST }}/repository/grim-ci-artifacts/${{ forgejo.repository }}/jniLibs.tar.gz
android_release:
if: ${{ forgejo.ref_type == 'branch' || needs.version.outputs.exists == 'false' }}
runs-on: ubuntu-android
needs: [version, android_libs]
steps:
- uses: actions/checkout@v6
with:
submodules: recursive
- 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: Download artifacts
run: |
cd android/app/src/main
curl -o jniLibs.tar.gz -u ${{ secrets.MAVEN_USER }}:${{ secrets.MAVEN_PASSWORD }} ${{ secrets.MAVEN_HOST }}/repository/grim-ci-artifacts/${{ forgejo.repository }}/jniLibs.tar.gz
tar -xzf jniLibs.tar.gz
rm jniLibs.tar.gz
- name: Setup build
run: |
chmod +x android/gradlew
echo "${{ secrets.ANDROID_KEYSTORE }}" > release.keystore.txt
base64 -d release.keystore.txt > android/keystore
echo "${{ secrets.ANDROID_KEYSTORE_PROPS }}" > release.keystore.props.txt
base64 -d release.keystore.props.txt > android/keystore.properties
mkdir ~/.gradle && touch ~/.gradle/gradle.properties
printf "mavenHost=${{ secrets.MAVEN_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 -v -u ${{ secrets.MAVEN_USER }}:${{ secrets.MAVEN_PASSWORD }} --upload-file android.tar.gz ${{ secrets.MAVEN_HOST }}/repository/grim-ci-artifacts/${{ forgejo.repository }}/android.tar.gz
linux_arm:
if: ${{ forgejo.ref_type == 'branch' || needs.version.outputs.exists == 'false' }}
runs-on: ubuntu
needs: [version, android_libs]
steps:
- uses: actions/checkout@v6
with:
submodules: recursive
- 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_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: Release Linux ARM
run: 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: AppImage ARM
run: |
mkdir release
cp target/aarch64-unknown-linux-gnu/release/grim linux/Grim.AppDir/AppRun
appimagetool linux/Grim.AppDir grim-${{ needs.version.outputs.v }}-linux-arm.AppImage
cp 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.tar.gz release
curl -v -u ${{ secrets.MAVEN_USER }}:${{ secrets.MAVEN_PASSWORD }} --upload-file linux-arm.tar.gz ${{ secrets.MAVEN_HOST }}/repository/grim-ci-artifacts/${{ forgejo.repository }}/linux-arm.tar.gz
linux_x86:
if: ${{ forgejo.ref_type == 'branch' || needs.version.outputs.exists == 'false' }}
runs-on: ubuntu-linux-x86
needs: [version]
steps:
- uses: actions/checkout@v6
with:
submodules: recursive
- 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-x86-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: Release Linux x86
run: 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
cp 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
cp 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.tar.gz release
curl -v -u ${{ secrets.MAVEN_USER }}:${{ secrets.MAVEN_PASSWORD }} --upload-file linux-x86_64.tar.gz ${{ secrets.MAVEN_HOST }}/repository/grim-ci-artifacts/${{ forgejo.repository }}/linux-x86_64.tar.gz
macos:
if: ${{ forgejo.ref_type == 'branch' || needs.version.outputs.exists == 'false' }}
runs-on: ubuntu
needs: [version, android_libs, linux]
steps:
- uses: actions/checkout@v6
with:
submodules: recursive
- 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: 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: Release MacOS Universal
run: |
cargo zigbuild --release --target universal2-apple-darwin
cp target/universal2-apple-darwin/release/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 -v -u ${{ secrets.MAVEN_USER }}:${{ secrets.MAVEN_PASSWORD }} --upload-file macos.tar.gz ${{ secrets.MAVEN_HOST }}/repository/grim-ci-artifacts/${{ 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
with:
submodules: recursive
- run: mkdir release
- name: Release Windows x86
run: |
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 -v -u ${{ secrets.MAVEN_USER }}:${{ secrets.MAVEN_PASSWORD }} --upload-file windows.tar.gz ${{ secrets.MAVEN_HOST }}/repository/grim-ci-artifacts/${{ forgejo.repository }}/windows.tar.gz
release:
if: ${{ forgejo.ref_type == 'branch' || needs.version.outputs.exists == 'false' }}
runs-on: ubuntu
needs: [version, android_release, linux, linux_x86, macos, windows]
steps:
- name: Download All Artifacts
run: |
curl -o android.tar.gz -u ${{ secrets.MAVEN_USER }}:${{ secrets.MAVEN_PASSWORD }} ${{ secrets.MAVEN_HOST }}/repository/grim-ci-artifacts/${{ forgejo.repository }}/android.tar.gz
tar -xzf android.tar.gz
rm android.tar.gz
curl -o linux-arm.tar.gz -u ${{ secrets.MAVEN_USER }}:${{ secrets.MAVEN_PASSWORD }} ${{ secrets.MAVEN_HOST }}/repository/grim-ci-artifacts/${{ forgejo.repository }}/linux-arm.tar.gz
tar -xzf linux-arm.tar.gz
rm linux-arm.tar.gz
curl -o linux-x86_64.tar.gz -u ${{ secrets.MAVEN_USER }}:${{ secrets.MAVEN_PASSWORD }} ${{ secrets.MAVEN_HOST }}/repository/grim-ci-artifacts/${{ forgejo.repository }}/linux-x86_64.tar.gz
tar -xzf linux-x86_64.tar.gz
rm linux-x86_64.tar.gz
curl -o macos.tar.gz -u ${{ secrets.MAVEN_USER }}:${{ secrets.MAVEN_PASSWORD }} ${{ secrets.MAVEN_HOST }}/repository/grim-ci-artifacts/${{ forgejo.repository }}/macos.tar.gz
tar -xzf macos.tar.gz
rm macos.tar.gz
curl -o windows.tar.gz -u ${{ secrets.MAVEN_USER }}:${{ secrets.MAVEN_PASSWORD }} ${{ secrets.MAVEN_HOST }}/repository/grim-ci-artifacts/${{ 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 -3
View File
@@ -6,7 +6,9 @@ jobs:
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
@@ -14,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
@@ -22,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 -161
View File
@@ -6,171 +6,26 @@ on:
- "v*.*.*"
jobs:
linux_release:
name: Linux 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: 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: 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: |
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/MacOSX10.15.sdk.tar.xz
- name: Setup SDK env
run: tar xf ${{ github.workspace }}/MacOSX10.15.sdk.tar.xz && echo "SDKROOT=${{ github.workspace }}/MacOSX10.15.sdk" >> $GITHUB_ENV
- name: Setup platform env
run: echo "MACOSX_DEPLOYMENT_TARGET=10.15" >> $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: 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 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
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
+3 -1
View File
@@ -1,4 +1,5 @@
*.iml
android/build
android/.idea
android/.gradle
android/local.properties
@@ -18,4 +19,5 @@ target
app/src/main/jniLibs
macos/cert.pem
linux/Grim.AppDir/AppRun
.intentionally-empty-file.o
.intentionally-empty-file.o
Cargo.toml-e
+11
View File
@@ -0,0 +1,11 @@
[submodule "node"]
path = node
url = https://code.gri.mw/ardocrat/node
[submodule "wallet"]
path = wallet
url = https://code.gri.mw/ardocrat/wallet
branch = grim
[submodule "tor/webtunnel"]
path = tor/webtunnel
url = https://code.gri.mw/WEB/webtunnel
branch = grim
Generated
+4505 -2907
View File
File diff suppressed because it is too large Load Diff
+84 -76
View File
@@ -1,12 +1,13 @@
[package]
name = "grim"
version = "0.2.2"
authors = ["Ardocrat <ardocrat@proton.me>"]
version = "0.3.4"
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"
@@ -25,111 +26,118 @@ codegen-units = 1
panic = "abort"
[dependencies]
log = "0.4.22"
log = "0.4.27"
## node
openssl-sys = { version = "0.9.103", features = ["vendored"] }
grin_api = "5.3.3"
grin_chain = "5.3.3"
grin_config = "5.3.3"
grin_core = "5.3.3"
grin_p2p = "5.3.3"
grin_servers = "5.3.3"
grin_keychain = "5.3.3"
grin_util = "5.3.3"
# node
grin_api = { path = "node/api" }
grin_chain = { path = "node/chain" }
grin_config = { path = "node/config" }
grin_core = { path = "node/core" }
grin_p2p = { path = "node/p2p" }
grin_servers = { path = "node/servers" }
grin_keychain = { path = "node/keychain" }
grin_util = { path = "node/util" }
## wallet
grin_wallet_impls = "5.3.3"
grin_wallet_api = "5.3.3"
grin_wallet_libwallet = "5.3.3"
grin_wallet_util = "5.3.3"
grin_wallet_controller = "5.3.3"
# 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.29.1", default-features = false }
egui_extras = { version = "0.29.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
anyhow = "1.0.89"
pin-project = "1.1.6"
backtrace = "0.3.74"
thiserror = "1.0.64"
log4rs = "1.4.0"
anyhow = "1.0.97"
pin-project = "1.1.10"
backtrace = "0.3.76"
thiserror = "2.0.18"
futures = "0.3.31"
dirs = "5.0.1"
sys-locale = "0.3.1"
chrono = "0.4.38"
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.8.19"
serde = "1.0.210"
local-ip-address = "0.6.3"
url = "2.5.2"
rand = "0.8.5"
serde_derive = "1.0.210"
serde_json = "1.0.128"
tokio = { version = "1.40.0", features = ["full"] }
image = "0.25.2"
rqrr = "0.8.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.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.23.0", features = ["pt-client", "static", "onion-service-service", "onion-service-client"] }
tor-rtcompat = { version = "0.23.0", features = ["static"] }
tor-config = "0.23.0"
fs-mistrust = "0.8.0"
tor-hsservice = "0.23.0"
tor-hsrproxy = "0.23.0"
tor-keymgr = "0.23.0"
tor-llcrypto = "0.23.0"
tor-hscrypto = "0.23.0"
tor-error = "0.23.0"
arti-client = { version = "0.41.0", features = ["pt-client", "static", "onion-service-service", "onion-service-client", "experimental-api", "bridge-client"] }
tor-rtcompat = { version = "0.41.0", features = ["static"] }
tor-config = "0.41.0"
fs-mistrust = "0.14.1"
tor-hsservice = "0.41.0"
tor-hsrproxy = "0.41.0"
tor-keymgr = "0.41.0"
tor-llcrypto = "0.41.0"
tor-hscrypto = "0.41.0"
tor-error = "0.41.0"
sha2 = "0.10.8"
ed25519-dalek = "2.1.1"
curve25519-dalek = "4.1.3"
hyper = { version = "0.14.30", features = ["full"] }
hyper-tls = "0.5.0"
tls-api = "0.9.0"
tls-api-native-tls = "0.9.0"
hyper-tor = { version = "0.14.32", features = ["full"], package = "hyper" }
tls-api = "0.12.0"
tls-api-native-tls = "0.12.1"
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(target_os = "linux")'.dependencies]
nokhwa = { version = "0.10.5", default-features = false, features = ["input-v4l"] }
nokhwa = { version = "0.10.10", default-features = false, features = ["input-v4l"] }
[target.'cfg(target_os = "windows")'.dependencies]
nokhwa = { version = "0.10.5", default-features = false, features = ["input-msmf"] }
nokhwa = { version = "0.10.10", default-features = false, features = ["input-msmf"] }
[target.'cfg(target_os = "macos")'.dependencies]
eye = { git = "https://github.com/raymanfx/eye-rs", rev = "5b7e3f7a1e79966091692896c568aab042e449ef", default-features = false }
tls-api-openssl = "0.9.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.30.5" }
eframe = { version = "0.29.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.15.0"
rfd = "0.17.2"
interprocess = { version = "2.2.1", features = ["tokio"] }
[target.'cfg(target_os = "android")'.dependencies]
android_logger = "0.14.1"
android_logger = "0.15.0"
jni = "0.21.1"
wgpu = "22.1.0"
android-activity = { version = "0.6.0", features = ["game-activity"] }
winit = { version = "0.30.5", features = ["android-game-activity"] }
eframe = { version = "0.29.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]
openpnp_capture = { git = "https://github.com/ardocrat/openpnp-capture-rs", rev = "f9b06f627c5e5d42c672d117650af700846ca6cf" }
egui_extras = { git = "https://github.com/emilk/egui", rev = "5b846b4554fe47269affb43efef2cad8710a8a47" }
egui = { git = "https://github.com/emilk/egui", rev = "5b846b4554fe47269affb43efef2cad8710a8a47" }
eframe = { git = "https://github.com/emilk/egui", rev = "5b846b4554fe47269affb43efef2cad8710a8a47" }
### patch grin store
#grin_store = { path = "../grin-store" }
[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
+58 -13
View File
@@ -3,15 +3,20 @@ plugins {
}
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.2.2"
targetSdk 36
versionCode 5
versionName "0.3.4"
}
lint {
checkReleaseBuilds false
}
def keystorePropertiesFile = rootProject.file("keystore.properties")
@@ -47,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'
}
+28 -28
View File
@@ -1,12 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:tools="http://schemas.android.com/tools" 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"/>
@@ -14,31 +14,34 @@
<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: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" />
@@ -51,19 +54,16 @@
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/*" />
</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="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.
@@ -170,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
@@ -203,7 +168,6 @@ public class BackgroundService extends Service {
// Stop updating the notification.
mHandler.removeCallbacks(mUpdateSyncStatus);
unregisterReceiver(mReceiver);
clearNotification();
// Remove service from foreground state.
@@ -226,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));
}
}
}
@@ -270,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();
}
}
@@ -12,10 +12,10 @@ 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,14 +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)) {
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();
@@ -80,15 +81,16 @@ public class MainActivity extends GameActivity {
}
// 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);
}
@@ -96,7 +98,7 @@ 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 associated file opening result.
mOpenFilePermissionsResult = registerForActivityResult(
@@ -119,19 +121,21 @@ public class MainActivity extends GameActivity {
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);
try (InputStream is = getContentResolver().openInputStream(uri);
OutputStream os = new FileOutputStream(file)) {
byte[] buffer = new byte[1024];
int length;
while ((length = is.read(buffer)) > 0) {
os.write(buffer, 0, length);
}
byte[] buffer = new byte[1024];
int length;
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();
}
@@ -193,6 +197,9 @@ public class MainActivity extends GameActivity {
}
}
// Pass display insets into native code.
public native void onDisplayInsets(int[] cutouts);
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
@@ -221,6 +228,7 @@ public class MainActivity extends GameActivity {
}
try {
ParcelFileDescriptor parcelFile = getContentResolver().openFileDescriptor(data, "r");
assert parcelFile != null;
FileReader fileReader = new FileReader(parcelFile.getFileDescriptor());
BufferedReader reader = new BufferedReader(fileReader);
String line;
@@ -234,7 +242,7 @@ public class MainActivity extends GameActivity {
// Provide file content into native code.
onData(buff.toString());
} catch (Exception e) {
e.printStackTrace();
Log.e("grim", e.toString());
}
}
@@ -257,54 +265,99 @@ 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();
// 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();
@@ -342,31 +395,21 @@ 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() {
String notificationsPermission = Manifest.permission.CAMERA;
@@ -456,7 +499,7 @@ public class MainActivity extends GameActivity {
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("*/*");
intent.setType("text/*");
startActivity(Intent.createChooser(intent, "Share data"));
}
@@ -469,7 +512,7 @@ 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 {
mFilePickResult.launch(Intent.createChooser(intent, "Pick file"));
} catch (android.content.ActivityNotFoundException ex) {
@@ -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>
+3 -4
View File
@@ -1,6 +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.6.1' apply false
id 'com.android.library' version '8.6.1' apply false
}
id 'com.android.application' version '8.10.0' apply false
id 'com.android.library' version '8.10.0' apply false
}
+1 -1
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
+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.7-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'
+76
View File
@@ -0,0 +1,76 @@
use std::process::Command;
use std::{env, fs};
fn main() {
built::write_built_file().expect("Failed to acquire build-time information");
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();
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

+5 -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,9 +17,11 @@ cd ..
[[ $2 == "x86_64" ]] && arch+=(x86_64-unknown-linux-gnu)
[[ $2 == "arm" ]] && arch+=(aarch64-unknown-linux-gnu)
cargo build --release --target ${arch}
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
+66 -2
View File
@@ -25,10 +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
@@ -83,6 +93,7 @@ wallets:
tx_canceled: Abgebrochen
tx_cancelling: Abbrechen
tx_finalizing: Finalisierung
tx_posting: Buchungsvorgang
tx_confirmed: Bestätigt
txs: Transaktionen
tx: Transaktion
@@ -126,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
@@ -138,7 +155,7 @@ transport:
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.
@@ -291,4 +308,51 @@ modal:
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: '/'
+66 -2
View File
@@ -25,10 +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
@@ -83,6 +93,7 @@ wallets:
tx_canceled: Canceled
tx_cancelling: Cancelling
tx_finalizing: Finalizing
tx_posting: Posting
tx_confirmed: Confirmed
txs: Transactions
tx: Transaction
@@ -126,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
@@ -138,7 +155,7 @@ transport:
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.
@@ -291,4 +308,51 @@ modal:
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: /
+66 -2
View File
@@ -25,10 +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
@@ -83,6 +93,7 @@ wallets:
tx_canceled: Annulé
tx_cancelling: Annulation
tx_finalizing: Finalisation
tx_posting: Publication
tx_confirmed: Confirmé
txs: Transactions
tx: Transaction
@@ -126,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
@@ -138,7 +155,7 @@ transport:
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.
@@ -291,4 +308,51 @@ modal:
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: /
+66 -2
View File
@@ -25,10 +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: Ожидает завершения
@@ -83,6 +93,7 @@ wallets:
tx_canceled: Отменено
tx_cancelling: Отмена
tx_finalizing: Завершение
tx_posting: Публикация
tx_confirmed: Подтверждено
txs: Транзакции
tx: Транзакция
@@ -126,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
@@ -138,7 +155,7 @@ transport:
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, если обычное соединение не работает.
@@ -291,4 +308,51 @@ modal:
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: ё
+66 -2
View File
@@ -25,10 +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
@@ -83,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
@@ -126,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
@@ -138,7 +155,7 @@ transport:
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.
@@ -291,4 +308,51 @@ modal:
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: /
+358
View File
@@ -0,0 +1,358 @@
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: '接收者的地址:'
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_peers_desc: 重置网络对点数据。仅当查找网络对点出现问题时,才请谨慎使用它.
reset_peers: 重置网络对点
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: /
+1 -1
View File
@@ -21,7 +21,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>0.2.2</string>
<string>0.3.4</string>
<key>CFBundleSupportedPlatforms</key>
<array>
<string>MacOSX</string>
+5 -17
View File
@@ -27,23 +27,11 @@ cd ..
[[ $1 == "x86_64" ]] && arch+=(x86_64-apple-darwin)
[[ $1 == "arm" ]] && arch+=(aarch64-apple-darwin)
if [[ "$OSTYPE" != "darwin"* ]]; then
# Start release build on non-MacOS with zig linker, requires zig 0.12.1
rustup target add x86_64-apple-darwin
rustup target add aarch64-apple-darwin
[[ $1 == "universal" ]]; arch+=(universal2-apple-darwin)
cargo install cargo-zigbuild
cargo zigbuild --release --target ${arch}
else
rustup target add ${arch}
if [[ $1 == "universal" ]]; then
cargo build --release --target x86_64-apple-darwin
cargo build --release --target aarch64-apple-darwin
lipo -create -output target/grim target/aarch64-apple-darwin/release/grim target/x86_64-apple-darwin/release/grim
else
cargo build --release --target ${arch}
fi
fi
rustup target add x86_64-apple-darwin
rustup target add aarch64-apple-darwin
[[ $1 == "universal" ]]; arch+=(universal2-apple-darwin)
cargo install cargo-zigbuild
cargo zigbuild --release --target ${arch}
rm -f .intentionally-empty-file.o
Submodule
+1
Submodule node added at 42b928a42f
+40 -30
View File
@@ -1,8 +1,8 @@
#!/bin/bash
usage="Usage: android.sh [type] [platform|version]\n - type: 'build', 'release'\n - platform, for 'build' type: 'v7', 'v8', 'x86'\n - optional version for 'release' (needed on MacOS), example: '0.2.2'"
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
build|release)
build|lib|release)
;;
*)
printf "$usage"
@@ -38,39 +38,43 @@ function build_lib() {
[[ $1 == "v8" ]] && arch=arm64-v8a
[[ $1 == "x86" ]] && arch=x86_64
sed -i -e 's/"rlib"/"cdylib","rlib"/g' Cargo.toml
sed -i -e 's/"cdylib","rlib"]/"rlib"]/g' Cargo.toml
sed -i -e 's/"rlib"]/"cdylib","rlib"]/g' Cargo.toml
# Fix for https://stackoverflow.com/questions/57193895/error-use-of-undeclared-identifier-pthread-mutex-robust-cargo-build-liblmdb-s
# Uncomment lines below for the 1st build:
#export CPPFLAGS="-DMDB_USE_ROBUST=0" && export CFLAGS="-DMDB_USE_ROBUST=0"
#cargo ndk -t ${arch} build --profile release-apk
#unset CPPFLAGS && unset CFLAGS
export CPPFLAGS="-DMDB_USE_ROBUST=0" && export CFLAGS="-DMDB_USE_ROBUST=0"
cargo ndk -t "${arch}" -o android/app/src/main/jniLibs build --profile release-apk
if [ $? -eq 0 ]
then
success=1
else
if [ $? -ne 0 ]; then
success=0
fi
unset CPPFLAGS && unset CFLAGS
sed -i -e 's/"cdylib","rlib"/"rlib"/g' Cargo.toml
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
# Build signed apk if keystore exists
if [ ! -f keystore.properties ]; then
./gradlew assembleRelease
apk_path=app/build/outputs/apk/release/app-release.apk
./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 assembleSignedRelease
apk_path=app/build/outputs/apk/signedRelease/app-signedRelease.apk
./gradlew assemble${flavor}SignedRelease
if [ $? -ne 0 ]; then
success=0
fi
apk_path=app/build/outputs/apk/${flavor}/signedRelease/app-${flavor}-signedRelease.apk
fi
if [[ $1 == "" ]]; then
if [[ $1 == "" ]] && [ $success -eq 1 ]; then
# Launch application at all connected devices.
for SERIAL in $(adb devices | grep -v List | cut -f 1);
do
@@ -78,18 +82,17 @@ function build_apk() {
sleep 1s
adb -s "$SERIAL" shell am start -n mw.gri.android/.MainActivity;
done
else
if [[ "$OSTYPE" != "darwin"* ]]; then
version=$(grep -m 1 -Po 'version = "\K[^"]*' Cargo.toml)
else
version=v$2
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
@@ -101,20 +104,27 @@ function build_apk() {
}
rm -rf android/app/src/main/jniLibs/*
if [[ $1 == "build" ]]; then
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
[ $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"
[ $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"
fi
[ $success -eq 1 ] && build_apk "x86_64" "$2" "$3"
[ $success -eq 1 ] && exit 0
fi
exit 1
+8 -7
View File
@@ -70,13 +70,14 @@ else
exit 1
fi
# ==================================
# Update Android build.gradle file
# and package version at Cargo.toml
# ==================================
# Update MacOS version.
sed -i '' -e 's/'"$GIT_TAG_LATEST"'/'"$VERSION_NEXT"'/' macos/Grim.app/Contents/Info.plist
# Update version in build.gradle
# 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
@@ -90,12 +91,12 @@ cargo update -p grim
# Commit the changes
git add .
git commit -m "release: v$VERSION_NEXT"
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 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
+75 -69
View File
@@ -13,17 +13,16 @@
// 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, ResizeDirection, Rounding, Stroke, UiBuilder, ViewportCommand};
use egui::epaint::{RectShape};
use egui::os::OperatingSystem;
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, Modal, TitlePanel, View};
use crate::wallet::ExternalConnection;
use crate::gui::views::types::ContentContainer;
use crate::gui::views::{Content, KeyboardContent, Modal, TitlePanel, View};
use crate::gui::Colors;
use crate::AppConfig;
lazy_static! {
/// State to check if platform Back button was pressed.
@@ -34,8 +33,10 @@ lazy_static! {
pub struct App<Platform> {
/// Handles platform-specific functionality.
pub platform: Platform,
/// Main content.
content: Content,
/// Last window resize direction.
resize_direction: Option<ResizeDirection>,
/// Flag to check if it's first draw.
@@ -58,9 +59,8 @@ impl<Platform: PlatformCallbacks> App<Platform> {
if View::is_desktop() {
self.platform.set_context(ctx);
}
// Check connections availability.
ExternalConnection::check(None, ctx);
// Setup visuals.
crate::setup_fonts(ctx);
crate::setup_visuals(ctx);
}
@@ -73,8 +73,10 @@ impl<Platform: PlatformCallbacks> App<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)) {
self.content.on_back(&self.platform);
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);
}
@@ -108,23 +110,25 @@ impl<Platform: PlatformCallbacks> App<Platform> {
let is_fullscreen = ui.ctx().input(|i| {
i.viewport().fullscreen.unwrap_or(false)
});
if OperatingSystem::from_target_os() != OperatingSystem::Mac {
self.desktop_window_ui(ui, is_fullscreen);
} else {
self.window_title_ui(ui, is_fullscreen);
ui.add_space(-1.0);
Self::title_panel_bg(ui);
self.content.ui(ui, &self.platform);
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.mobile_window_ui(ui);
}
// Provide incoming data to wallets.
if let Some(data) = crate::consume_incoming_data() {
if !data.is_empty() {
self.content.wallets.on_data(ui, Some(data), &self.platform);
}
Self::title_panel_bg(ui, false);
self.content.ui(ui, &self.platform);
}
});
@@ -133,16 +137,29 @@ impl<Platform: PlatformCallbacks> App<Platform> {
ctx.input(|i| i.viewport().focused.unwrap_or(true)) {
self.platform.clear_user_attention();
}
// 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();
}
}
/// Draw mobile platform window content.
fn mobile_window_ui(&mut self, ui: &mut egui::Ui) {
Self::title_panel_bg(ui);
self.content.ui(ui, &self.platform);
}
/// Draw desktop platform window content.
fn desktop_window_ui(&mut self, ui: &mut egui::Ui, is_fullscreen: bool) {
/// 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 {
@@ -152,9 +169,10 @@ impl<Platform: PlatformCallbacks> App<Platform> {
r
};
let content_bg = RectShape::new(content_bg_rect,
Rounding::ZERO,
CornerRadius::ZERO,
Colors::fill_lite(),
View::default_stroke());
View::default_stroke(),
StrokeKind::Outside);
// Draw content background.
ui.painter().add(content_bg);
@@ -163,13 +181,13 @@ impl<Platform: PlatformCallbacks> App<Platform> {
content_rect = content_rect.shrink(Content::WINDOW_FRAME_MARGIN);
}
// Draw window content.
ui.allocate_new_ui(UiBuilder::new().max_rect(content_rect), |ui| {
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 title panel background.
Self::title_panel_bg(ui);
Self::title_panel_bg(ui, true);
let content_rect = {
let mut rect = ui.max_rect();
@@ -197,16 +215,16 @@ impl<Platform: PlatformCallbacks> App<Platform> {
}
/// Draw title panel background.
fn title_panel_bg(ui: &mut egui::Ui) {
fn title_panel_bg(ui: &mut egui::Ui, window_title: bool) {
let title_rect = {
let mut rect = ui.max_rect();
if View::is_desktop() {
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, Rounding::ZERO, Colors::yellow());
let title_bg = RectShape::filled(title_rect, CornerRadius::ZERO, Colors::yellow());
ui.painter().add(title_bg);
}
@@ -223,17 +241,17 @@ impl<Platform: PlatformCallbacks> App<Platform> {
r.max.y += TitlePanel::HEIGHT - 1.0;
r
};
let is_mac = OperatingSystem::from_target_os() == OperatingSystem::Mac;
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 {
Rounding::ZERO
CornerRadius::ZERO
} else {
Rounding {
nw: 8.0,
ne: 8.0,
sw: 0.0,
se: 0.0,
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));
}, Colors::yellow_dark(), Stroke::new(1.0, Colors::STROKE), StrokeKind::Outside);
// Draw title background.
ui.painter().add(window_title_bg);
@@ -259,22 +277,7 @@ impl<Platform: PlatformCallbacks> App<Platform> {
}
// Paint the title.
let dual_wallets_panel = ui.available_width() >= (Content::SIDE_PANEL_WIDTH * 3.0) +
View::get_right_inset() + View::get_left_inset();
let wallet_panel_opened = self.content.wallets.showing_wallet();
let show_app_name = if dual_wallets_panel {
wallet_panel_opened && !AppConfig::show_wallets_at_dual_panel()
} else if Content::is_dual_panel_mode(ui.ctx()) {
wallet_panel_opened
} else {
Content::is_network_panel_open() || wallet_panel_opened
};
let creating_wallet = self.content.wallets.creating_wallet();
let title_text = if creating_wallet || show_app_name {
format!("Grim {}", crate::VERSION)
} else {
"".to_string()
};
let title_text = format!("Grim {}", crate::VERSION);
painter.text(
title_rect.center(),
egui::Align2::CENTER_CENTER,
@@ -283,7 +286,7 @@ impl<Platform: PlatformCallbacks> App<Platform> {
Colors::title(true),
);
ui.allocate_new_ui(UiBuilder::new().max_rect(title_rect), |ui| {
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, |_| {
@@ -397,12 +400,15 @@ impl<Platform: PlatformCallbacks> App<Platform> {
/// 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) {
ctx.plugin_or_default::<egui_async::EguiAsyncPlugin>();
self.ui(ctx);
}
fn clear_color(&self, _visuals: &egui::Visuals) -> [f32; 4] {
let is_mac = OperatingSystem::from_target_os() == OperatingSystem::Mac;
if !View::is_desktop() || is_mac {
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()
@@ -412,7 +418,7 @@ impl<Platform: PlatformCallbacks> eframe::App for App<Platform> {
#[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,
+16 -7
View File
@@ -26,6 +26,7 @@ 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);
@@ -34,20 +35,19 @@ 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, 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(16);
const FILL_LITE_DARK: Color32 = Color32::from_gray(21);
const TEXT: Color32 = Color32::from_gray(80);
const TEXT_DARK: Color32 = Color32::from_gray(185);
@@ -84,6 +84,7 @@ fn use_dark() -> bool {
}
impl Colors {
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);
@@ -119,6 +120,14 @@ impl Colors {
}
}
pub fn gold_dark() -> Color32 {
if use_dark() {
GOLD_DARK.gamma_multiply(0.9)
} else {
GOLD_DARK
}
}
pub fn yellow() -> Color32 {
YELLOW
}
@@ -163,7 +172,7 @@ impl Colors {
if use_dark() {
FILL_DEEP_DARK
} else {
FILL_DEEP
Self::FILL_DEEP
}
}
@@ -231,7 +240,7 @@ impl Colors {
}
}
pub fn item_button() -> Color32 {
pub fn item_button_text() -> Color32 {
if use_dark() {
ITEM_BUTTON_DARK
} else {
+12 -16
View File
@@ -70,20 +70,6 @@ impl PlatformCallbacks for Android {
let _ = self.call_java_method("exit", "()V", &[]);
}
fn show_keyboard(&self) {
// Disable NDK soft input show call before fix for egui.
// self.android_app.show_soft_input(false);
let _ = self.call_java_method("showKeyboard", "()V", &[]);
}
fn hide_keyboard(&self) {
// Disable NDK soft input hide call before fix for egui.
// self.android_app.hide_soft_input(false);
let _ = self.call_java_method("hideKeyboard", "()V", &[]);
}
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();
@@ -175,6 +161,16 @@ impl PlatformCallbacks for Android {
Some("".to_string())
}
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 picked_file(&self) -> Option<String> {
let has_file = {
let r_path = PICKED_FILE_PATH.read();
@@ -207,7 +203,7 @@ lazy_static! {
/// 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,
@@ -222,7 +218,7 @@ pub extern "C" fn Java_mw_gri_android_MainActivity_onCameraImage(
/// 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,
+49 -38
View File
@@ -52,7 +52,7 @@ impl Desktop {
}
}
#[allow(dead_code)]
// #[allow(dead_code)]
#[cfg(not(target_os = "macos"))]
fn start_camera_capture(cameras_amount: Arc<AtomicUsize>,
camera_index: Arc<AtomicUsize>,
@@ -109,52 +109,56 @@ impl Desktop {
fn start_camera_capture(cameras_amount: Arc<AtomicUsize>,
camera_index: Arc<AtomicUsize>,
stop_camera: Arc<AtomicBool>) {
use image::{ExtendedColorType, ImageBuffer, ImageEncoder, Rgb};
use eye::hal::{traits::{Context, Device, Stream}, PlatformContext};
use image::codecs::jpeg::JpegEncoder;
use nokhwa::nokhwa_initialize;
use nokhwa::pixel_format::RgbFormat;
use nokhwa::utils::{CameraIndex, RequestedFormat, RequestedFormatType};
use nokhwa::utils::ApiBackend;
use nokhwa::query;
use nokhwa::CallbackCamera;
let index = camera_index.load(Ordering::Relaxed);
let devices = PlatformContext::default().devices().unwrap_or(vec![]);
cameras_amount.store(devices.len(), Ordering::Relaxed);
if devices.is_empty() || index >= devices.len() {
return;
}
// Ask permission to open camera.
nokhwa_initialize(|_| {});
// Capture images at separate thread.
let uri = devices[camera_index.load(Ordering::Relaxed)].uri.clone();
thread::spawn(move || {
if let Ok(dev) = PlatformContext::default().open_device(&uri) {
let streams = dev.streams().unwrap_or(vec![]);
if streams.is_empty() {
return;
}
let stream_desc = streams[0].clone();
let w = stream_desc.width;
let h = stream_desc.height;
if let Ok(mut stream) = dev.start_stream(&stream_desc) {
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 a frame.
let frame = stream.next()
.expect("Stream is dead")
.expect("Failed to capture a frame");
let mut out = vec![];
if let Some(buf) = ImageBuffer::<Rgb<u8>, &[u8]>::from_raw(w, h, &frame) {
JpegEncoder::new(&mut out)
.write_image(buf.as_raw(), w, h, ExtendedColorType::Rgb8)
.unwrap_or_default();
// 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 {
out = frame.to_vec();
// Clear image.
let mut w_image = LAST_CAMERA_IMAGE.write();
*w_image = None;
break;
}
// Save image.
let mut w_image = LAST_CAMERA_IMAGE.write();
*w_image = Some((out, 0));
}
}
}
@@ -176,10 +180,6 @@ 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();
@@ -260,6 +260,17 @@ impl PlatformCallbacks for Desktop {
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
}
+1 -2
View File
@@ -24,8 +24,6 @@ pub mod platform;
pub trait PlatformCallbacks {
fn set_context(&mut self, ctx: &egui::Context);
fn exit(&self);
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);
@@ -35,6 +33,7 @@ pub trait PlatformCallbacks {
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;
+28 -22
View File
@@ -12,21 +12,21 @@
// 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 egui::load::SizedTexture;
use egui::{Pos2, Rect, RichText, TextureOptions, UiBuilder, Widget};
use image::{DynamicImage, EncodableLayout};
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::gui::Colors;
use crate::wallet::types::PhraseSize;
use crate::wallet::WalletUtils;
@@ -50,18 +50,14 @@ impl Default for CameraContent {
impl CameraContent {
/// Draw camera content.
pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
ui.ctx().request_repaint();
let rect = if let Some(img_data) = cb.camera_image() {
if let Ok(img) =
image::load_from_memory(&*img_data.0) {
if let Ok(img) = image::load_from_memory(&*img_data.0) {
// Process image to find QR code.
self.scan_qr(&img);
// Draw image.
let img_rect = self.image_ui(ui, img, img_data.1);
// Show UR scan progress.
self.ur_progress_ui(ui);
img_rect
} else {
self.loading_ui(ui)
@@ -70,6 +66,9 @@ impl CameraContent {
self.loading_ui(ui)
};
// Show UR scan progress.
self.ur_progress_ui(ui, &rect);
// Show button to switch cameras.
if cb.can_switch_camera() {
let r = {
@@ -78,13 +77,15 @@ impl CameraContent {
r.min.x = r.max.x - 52.0;
r
};
ui.allocate_new_ui(UiBuilder::new().max_rect(r), |ui| {
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();
}
/// Draw camera image.
@@ -125,7 +126,11 @@ impl CameraContent {
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),
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())
@@ -135,15 +140,17 @@ impl CameraContent {
}
/// Draw animated QR code scanning progress.
fn ur_progress_ui(&self, ui: &mut egui::Ui) {
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.centered_and_justified(|ui| {
ui.label(RichText::new(format!("{}%", self.ur_progress()))
.size(17.0)
.color(Colors::green()));
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()));
});
});
}
}
@@ -201,8 +208,7 @@ impl CameraContent {
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);
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) {
@@ -290,7 +296,7 @@ impl CameraContent {
// Launch scanner at separate thread.
thread::spawn(move || {
tokio::runtime::Builder::new_multi_thread()
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap()
@@ -311,7 +317,7 @@ impl CameraContent {
// Check if string contains Slatepack message prefix and postfix.
if text.starts_with("BEGINSLATEPACK.") && text.ends_with("ENDSLATEPACK.") {
return QrScanResult::Slatepack(ZeroingString::from(text));
return QrScanResult::Slatepack(text.to_string());
}
// Check Uniform Resource data.
+85 -196
View File
@@ -12,21 +12,21 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use egui::os::OperatingSystem;
use egui::RichText;
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::gui::Colors;
use crate::node::Node;
use crate::{AppConfig, Settings};
use crate::gui::icons::{CHECK, CHECK_FAT, FILE_X};
use crate::gui::views::network::NetworkContent;
use crate::gui::views::wallets::WalletsContent;
lazy_static! {
/// Global state to check if [`NetworkContent`] panel is open.
@@ -37,8 +37,9 @@ lazy_static! {
pub struct Content {
/// Side panel [`NetworkContent`] content.
network: NetworkContent,
/// Central panel [`WalletsContent`] content.
pub wallets: WalletsContent,
wallets: WalletsContent,
/// Check if app exit is allowed on Desktop close event.
pub exit_allowed: bool,
@@ -47,16 +48,8 @@ pub struct Content {
/// Flag to check it's first draw of content.
first_draw: bool,
/// List of allowed [`Modal`] ids for this [`ModalContainer`].
allowed_modal_ids: Vec<&'static str>
}
/// 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";
impl Default for Content {
fn default() -> Self {
// Exit from eframe only for non-mobile platforms.
@@ -68,53 +61,39 @@ impl Default for Content {
exit_allowed,
show_exit_progress: false,
first_draw: true,
allowed_modal_ids: vec![
Self::EXIT_CONFIRMATION_MODAL,
Self::SETTINGS_MODAL,
ANDROID_INTEGRATED_NODE_WARNING_MODAL,
CRASH_REPORT_MODAL
],
}
}
}
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";
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) {
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),
Self::SETTINGS_MODAL => self.settings_modal_ui(ui, modal),
ANDROID_INTEGRATED_NODE_WARNING_MODAL => self.android_warning_modal_ui(ui, modal),
CRASH_REPORT_MODAL => self.crash_report_modal_ui(ui, modal, cb),
ANDROID_INTEGRATED_NODE_WARNING_MODAL => self.android_warning_modal_ui(ui),
CRASH_REPORT_MODAL => self.crash_report_modal_ui(ui, cb),
_ => {}
}
}
}
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";
/// 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;
pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
self.current_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, panel_width) = network_panel_state_width(ui.ctx(), dual_panel);
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")
@@ -138,21 +117,50 @@ impl Content {
if self.first_draw {
// Show crash report or integrated node Android warning.
if Settings::crash_report_path().exists() {
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();
AppConfig::android_integrated_node_warning_needed() {
Modal::new(ANDROID_INTEGRATED_NODE_WARNING_MODAL)
.title(t!("network.node"))
.show();
}
self.first_draw = false;
}
}
}
impl Content {
/// 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";
/// 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
}
/// Check if ui can show [`NetworkContent`] and [`WalletsContent`] at same time.
pub fn is_dual_panel_mode(ctx: &egui::Context) -> bool {
@@ -186,16 +194,21 @@ impl Content {
/// 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() {
if !Node::is_running() && !Node::data_dir_changing() {
self.exit_allowed = true;
cb.exit();
modal.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"))
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)));
});
@@ -215,15 +228,15 @@ impl Content {
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
modal.close();
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() {
if !Node::is_running() && !Node::data_dir_changing() {
self.exit_allowed = true;
cb.exit();
modal.close();
Modal::close();
} else {
Node::stop(true);
modal.disable_closing();
@@ -237,129 +250,8 @@ impl Content {
}
}
/// Handle Back key event.
pub fn on_back(&mut self, cb: &dyn PlatformCallbacks) {
if Modal::on_back() {
if self.wallets.on_back(cb) {
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);
// Show 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.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) {
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"))
@@ -370,32 +262,29 @@ impl Content {
ui.vertical_centered_justified(|ui| {
View::button(ui, t!("continue"), Colors::white_or_black(false), || {
AppConfig::show_android_integrated_node_warning();
modal.close();
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) {
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 crash report.
// 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::crash_report_path()) {
let name = Settings::CRASH_REPORT_FILE_NAME.to_string();
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_report();
modal.close();
Settings::delete_crash_check();
Modal::close();
});
});
ui.add_space(8.0);
@@ -403,8 +292,8 @@ impl Content {
ui.add_space(8.0);
ui.vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
Settings::delete_crash_report();
modal.close();
Settings::delete_crash_check();
Modal::close();
});
});
ui.add_space(6.0);
+103 -27
View File
@@ -12,49 +12,87 @@
// 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 egui::CornerRadius;
use parking_lot::RwLock;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::{fs, thread};
use crate::gui::Colors;
use crate::gui::icons::ARCHIVE_BOX;
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::View;
use crate::gui::Colors;
/// Type of button.
pub enum FilePickContentType {
Button(String), ItemButton(CornerRadius), Tab
}
/// Button to pick file and parse its data into text.
pub struct FilePickButton {
pub struct FilePickContent {
/// Content type.
content_type: FilePickContentType,
/// Flag to check if button is active.
active: bool,
/// Flag to check if file is picking.
pub file_picking: Arc<AtomicBool>,
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.
pub file_parsing: Arc<AtomicBool>,
file_parsing: Arc<AtomicBool>,
/// File parsing result.
pub file_parsing_result: Arc<RwLock<Option<String>>>
file_parsing_result: Arc<RwLock<Option<String>>>,
}
impl Default for FilePickButton {
fn default() -> Self {
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))
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)) {
/// 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() {
self.on_file_pick(path);
if self.parse_file {
self.parse_file(path);
} else {
pick(path);
}
}
}
} else if self.file_parsing.load(Ordering::Relaxed) {
@@ -70,7 +108,7 @@ impl FilePickButton {
r_res.clone().unwrap()
};
// Callback on result.
on_result(text);
pick(text);
// Clear result.
let mut w_res = self.file_parsing_result.write();
*w_res = None;
@@ -78,22 +116,60 @@ impl FilePickButton {
}
} else {
// Draw button to pick file.
let text = format!("{} {}", ARCHIVE_BOX, t!("choose_file"));
View::colored_text_button(ui, text, Colors::blue(), Colors::white_or_black(false), || {
if let Some(path) = cb.pick_file() {
self.on_file_pick(path);
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 picked file path.
fn on_file_pick(&self, path: String) {
/// 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 || {
+453
View File
@@ -0,0 +1,453 @@
// 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::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};
use crate::gui::Colors;
/// 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);
}
+509
View File
@@ -0,0 +1,509 @@
// 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::string::ToString;
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::sync::atomic::Ordering;
use std::sync::Arc;
use crate::gui::icons::{ARROW_FAT_UP, BACKSPACE, GLOBE_SIMPLE, KEY_RETURN};
use crate::gui::views::{KeyboardEvent, KeyboardLayout, KeyboardState, View};
use crate::gui::Colors;
use crate::AppConfig;
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::*;
+49
View File
@@ -0,0 +1,49 @@
// 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)),
}
}
}
+5 -2
View File
@@ -27,8 +27,8 @@ mod content;
pub use content::*;
pub mod network;
pub mod wallets;
pub mod settings;
mod camera;
pub use camera::*;
@@ -43,4 +43,7 @@ mod pull_to_refresh;
pub use pull_to_refresh::*;
mod scan;
pub use scan::*;
pub use scan::*;
mod input;
pub use input::*;
Regular → Executable
+90 -56
View File
@@ -12,17 +12,18 @@
// 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, RichText, Rounding, Stroke, UiBuilder, 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::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use crate::gui::Colors;
use crate::gui::views::{Content, View};
use crate::gui::views::types::{ModalPosition, ModalState};
use crate::gui::views::{Content, View};
use crate::gui::Colors;
use crate::gui::platform::PlatformCallbacks;
lazy_static! {
/// Showing [`Modal`] state to be accessible from different ui parts.
@@ -40,6 +41,10 @@ pub struct Modal {
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 {
@@ -47,6 +52,8 @@ impl Modal {
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 {
@@ -55,6 +62,8 @@ impl Modal {
position: ModalPosition::Center,
closeable: Arc::new(AtomicBool::new(true)),
title: None,
first_draw: Arc::new(AtomicBool::new(true)),
fill: None,
}
}
@@ -70,8 +79,8 @@ impl Modal {
w_state.modal.as_mut().unwrap().position = position;
}
/// Mark [`Modal`] closed.
pub fn close(&self) {
/// Close [`Modal`] by clearing its state.
pub fn close() {
let mut w_nav = MODAL_STATE.write();
w_nav.modal = None;
}
@@ -98,27 +107,24 @@ impl Modal {
}
/// Set title text on [`Modal`] creation.
pub fn title(mut self, title: String) -> Self {
self.title = Some(title.to_uppercase());
pub fn title(mut self, title: impl Into<String>) -> Self {
self.title = Some(title.into().to_uppercase());
self
}
/// 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);
}
/// 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 {
let mut w_state = MODAL_STATE.write();
// 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;
if Self::opened().is_some() {
if Self::opened_closeable() {
Self::close();
}
return false;
}
@@ -154,18 +160,28 @@ impl Modal {
}
/// Set title text for current opened [`Modal`].
pub fn set_title(title: String) {
// Save state.
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.to_uppercase());
modal.title = Some(title.into().to_uppercase());
w_state.modal = Some(modal);
}
}
/// Draw opened [`Modal`] content.
pub fn ui(ctx: &egui::Context, add_content: impl FnOnce(&mut egui::Ui, &Modal)) {
/// 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)
}
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()
};
@@ -174,19 +190,23 @@ impl Modal {
let r_state = MODAL_STATE.read();
r_state.modal.clone().unwrap()
};
modal.window_ui(ctx, add_content);
modal.window_ui(ctx, cb, add_content);
}
}
/// Draw [`egui::Window`] with provided content.
fn window_ui(&self, ctx: &egui::Context, add_content: impl FnOnce(&mut egui::Ui, &Modal)) {
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)
});
// Setup background rect.
let bg_rect = if View::is_desktop() {
let mut r = ctx.screen_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);
@@ -194,7 +214,7 @@ impl Modal {
r.min.y += Content::WINDOW_TITLE_HEIGHT;
r
} else {
ctx.screen_rect()
ctx.content_rect()
};
// Draw modal background.
@@ -213,12 +233,12 @@ impl Modal {
// Setup width of modal content.
let side_insets = View::get_left_inset() + View::get_right_inset();
let available_width = ctx.screen_rect().width() - (side_insets + Self::DEFAULT_MARGIN);
let available_width = ctx.content_rect().width() - (side_insets + Self::DEFAULT_MARGIN);
let width = f32::min(available_width, Self::DEFAULT_WIDTH);
// Show main content window at given position.
let (content_align, content_offset) = self.modal_position();
let layer_id = egui::Window::new("modal_window")
egui::Window::new(Self::WINDOW_ID)
.title_bar(false)
.resizable(false)
.collapsible(false)
@@ -228,22 +248,26 @@ impl Modal {
.frame(egui::Frame {
shadow: Shadow {
offset: Default::default(),
blur: 30.0,
spread: 3.0,
blur: 30.0 as u8,
spread: 3.0 as u8,
color: egui::Color32::from_black_alpha(32),
},
rounding: Rounding::same(8.0),
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, add_content);
}).unwrap().response.layer_id;
self.content_ui(ui, cb, add_content);
});
// Always show main content window above background window.
ctx.move_to_top(layer_id);
// 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);
}
}
/// Get [`egui::Window`] position based on [`ModalPosition`].
@@ -255,7 +279,8 @@ impl Modal {
let x_align = View::get_left_inset() - View::get_right_inset();
let is_mac = OperatingSystem::Mac == OperatingSystem::from_target_os();
let extra_y = if View::is_desktop() {
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 {
@@ -273,27 +298,36 @@ impl Modal {
(align, offset)
}
/// 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 provided content.
fn content_ui(&self, ui: &mut egui::Ui, add_content: impl FnOnce(&mut egui::Ui, &Modal)) {
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();
// Create background shape.
let mut bg_shape = RectShape::new(rect, if self.title.is_none() {
Rounding::same(8.0)
CornerRadius::same(8.0 as u8)
} else {
Rounding {
nw: 0.0,
ne: 0.0,
sw: 8.0,
se: 8.0,
CornerRadius {
nw: 0.0 as u8,
ne: 0.0 as u8,
sw: 8.0 as u8,
se: 8.0 as u8,
}
}, Colors::fill(), Stroke::NONE);
let bg_idx = ui.painter().add(bg_shape);
}, self.fill.unwrap_or(Colors::fill_lite()), Stroke::NONE, StrokeKind::Outside);
let bg_idx = ui.painter().add(bg_shape.clone());
rect.min += egui::emath::vec2(6.0, 0.0);
rect.max -= egui::emath::vec2(6.0, 0.0);
let resp = ui.allocate_new_ui(UiBuilder::new().max_rect(rect), |ui| {
(add_content)(ui, self);
let resp = ui.scope_builder(UiBuilder::new().max_rect(rect), |ui| {
(add_content)(ui, self, cb);
}).response;
// Setup background size.
@@ -313,13 +347,13 @@ fn title_ui(title: &String, ui: &mut egui::Ui) {
let rect = ui.available_rect_before_wrap();
// Create background shape.
let mut bg_shape = RectShape::new(rect, Rounding {
nw: 8.0,
ne: 8.0,
sw: 0.0,
se: 0.0,
}, Colors::yellow(), Stroke::NONE);
let bg_idx = ui.painter().add(bg_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| {
+222 -130
View File
@@ -12,42 +12,52 @@
// 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, WARNING_CIRCLE, 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::modals::{ExternalConnectionModal, ShareConnectionContent};
use crate::gui::views::network::types::ShareConnection;
use crate::gui::views::network::NodeSetup;
use crate::gui::views::types::{ModalContainer, ModalPosition};
use crate::gui::views::types::{ContentContainer, ModalPosition};
use crate::gui::views::{Modal, View};
use crate::gui::Colors;
use crate::node::{Node, NodeConfig};
use crate::wallet::{ConnectionsConfig, ExternalConnection};
use crate::AppConfig;
/// 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 {
Self {
ext_conn_modal: ExternalConnectionModal::new(None),
modal_ids: vec![
ExternalConnectionModal::NETWORK_ID
],
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";
impl ContentContainer for ConnectionsContent {
fn modal_ids(&self) -> Vec<&'static str> {
vec![
ExternalConnectionModal::NETWORK_ID,
SHARE_CONN_QR_MODAL
]
}
fn modal_ui(&mut self,
@@ -56,17 +66,23 @@ impl ModalContainer for ConnectionsContent {
cb: &dyn PlatformCallbacks) {
match modal.id {
ExternalConnectionModal::NETWORK_ID => {
self.ext_conn_modal.ui(ui, cb, modal, |_| {});
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);
}
}
_ => {}
}
}
}
impl ConnectionsContent {
/// Draw connections content.
pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
self.current_modal_ui(ui, 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);
@@ -81,11 +97,25 @@ impl ConnectionsContent {
}
// 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();
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, || {
if let Ok(c) = ShareConnectionContent::new(ShareConnection {
url: format!("http://{}", NodeConfig::get_api_address()),
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.
@@ -96,153 +126,215 @@ impl ConnectionsContent {
// 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);
self.show_add_ext_conn_modal(None);
});
ui.add_space(4.0);
let ext_conn_list = ConnectionsConfig::ext_conn_list();
let ext_conn_size = ext_conn_list.len();
if ext_conn_size != 0 {
let len = ext_conn_list.len();
if len != 0 {
ui.add_space(8.0);
for (index, conn) in ext_conn_list.iter().filter(|c| !c.deleted).enumerate() {
for (i, c) in ext_conn_list.iter().enumerate() {
ui.horizontal_wrapped(|ui| {
// Draw connection list item.
Self::ext_conn_item_ui(ui, conn, index, ext_conn_size, |ui| {
let button_rounding = View::item_rounding(index, ext_conn_size, true);
View::item_button(ui, button_rounding, TRASH, None, || {
ConnectionsConfig::remove_ext_conn(conn.id);
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);
});
View::item_button(ui, Rounding::default(), PENCIL, None, || {
self.show_add_ext_conn_modal(Some(conn.clone()), cb);
// 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 integrated node connection item content.
pub fn integrated_node_item_ui(ui: &mut egui::Ui, custom_button: impl FnOnce(&mut egui::Ui)) {
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 rounding = View::item_rounding(0, 1, false);
ui.painter().rect(rect, rounding, Colors::fill(), View::item_stroke());
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.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
// Draw custom button.
custom_button(ui);
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);
// Draw buttons to start/stop node.
if Node::get_error().is_none() {
if !Node::is_running() {
View::item_button(ui, Rounding::default(), POWER, Some(Colors::green()), || {
Node::start();
});
} else if !Node::is_starting() && !Node::is_stopping() && !Node::is_restarting() {
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 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
// Draw buttons to start/stop node.
if Node::get_error().is_none() {
let rounding = if extra_button {
CornerRadius::default()
} else {
DOTS_THREE_CIRCLE
View::item_rounding(0, 1, true)
};
let status_text = format!("{} {}", status_icon, if has_error {
t!("error")
} else {
Node::get_sync_status_text()
});
View::ellipsize_text(ui, status_text, 15.0, Colors::text(false));
ui.add_space(1.0);
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);
});
}
}
// 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()));
})
});
});
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 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);
// 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()));
})
});
}).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();
}
}
/// Draw external connection item content.
pub fn ext_conn_item_ui(ui: &mut egui::Ui,
bg: Color32,
conn: &ExternalConnection,
index: usize,
len: usize,
buttons_ui: impl FnOnce(&mut egui::Ui)) {
// Setup layout size.
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());
// 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());
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);
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);
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"))
// 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!("{} {}", 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);
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();
}
}
/// 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);
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();
cb.show_keyboard();
}
}
+113 -59
View File
@@ -12,19 +12,19 @@
// 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 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::{ARROWS_COUNTER_CLOCKWISE, ARROW_LEFT, BRIEFCASE, DATABASE, DOTS_THREE_OUTLINE_VERTICAL, FACTORY, FADERS, GAUGE, GEAR, GLOBE, POWER};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Content, TitlePanel, View};
use crate::gui::views::network::{ConnectionsContent, NetworkMetrics, NetworkMining, NetworkNode, NetworkSettings};
use crate::gui::views::network::types::{NodeTab, NodeTabType};
use crate::gui::views::types::{LinePosition, TitleContentType, TitleType};
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::Colors;
use crate::node::{Node, NodeConfig, NodeError};
use crate::wallet::ExternalConnection;
use crate::AppConfig;
/// Network content.
pub struct NetworkContent {
@@ -32,6 +32,9 @@ pub struct NetworkContent {
node_tab_content: Box<dyn NodeTab>,
/// Connections content.
connections: ConnectionsContent,
/// Application settings content.
settings_content: Option<SettingsContent>,
}
impl Default for NetworkContent {
@@ -39,12 +42,14 @@ impl Default for NetworkContent {
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_settings = self.showing_settings();
let show_connections = AppConfig::show_connections_network_panel();
let dual_panel = Content::is_dual_panel_mode(ui.ctx());
@@ -52,17 +57,23 @@ impl NetworkContent {
self.title_ui(ui, dual_panel, show_connections);
// Show integrated node tabs content.
if !show_connections {
egui::TopBottomPanel::bottom("node_tabs")
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: 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,
},
inner_margin: tabs_margin,
fill: Colors::fill(),
..Default::default()
})
@@ -74,36 +85,53 @@ impl NetworkContent {
// Draw content divider line.
let r = {
let mut r = rect.clone();
r.min.x -= View::get_left_inset() + View::TAB_ITEMS_PADDING;
r.min.y -= View::TAB_ITEMS_PADDING;
r.max.x += View::far_right_inset_margin(ui) + View::TAB_ITEMS_PADDING;
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 integrated node tab content.
egui::SidePanel::right("node_tab_content")
// 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_connections, |ui| {
.show_animated_inside(ui, show_settings || !show_connections, |ui| {
egui::CentralPanel::default()
.frame(egui::Frame {
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,
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 self.node_tab_content.get_type() != NodeTabType::Settings {
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 {
@@ -112,21 +140,20 @@ impl NetworkContent {
disabled_node_ui(ui);
} else if Node::get_stats().is_none() || Node::is_restarting() ||
Node::is_stopping() {
NetworkContent::loading_ui(ui, None);
NetworkContent::loading_ui(ui, None::<String>);
} else {
self.node_tab_content.ui(ui, cb);
self.node_tab_content.tab_ui(ui, cb);
}
});
} else {
self.node_tab_content.ui(ui, cb);
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 += 4.0;
r.max.y += 4.0;
r.max.x += View::content_padding();
r.max.y += View::content_padding();
r
};
if dual_panel {
@@ -140,17 +167,17 @@ impl NetworkContent {
.frame(egui::Frame {
inner_margin: Margin {
left: if show_connections {
View::get_left_inset() + 4.0
View::get_left_inset() + View::content_padding()
} else {
0.0
},
} as i8,
right: if show_connections {
View::far_right_inset_margin(ui) + 4.0
View::far_right_inset_margin(ui) + View::content_padding()
} else {
0.0
},
top: 3.0,
bottom: 4.0 + View::get_bottom_inset(),
} as i8,
top: 3.0 as i8,
bottom: 0.0 as i8,
},
..Default::default()
})
@@ -172,13 +199,14 @@ impl NetworkContent {
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 += 4.0;
r.max.y += 4.0 + View::get_bottom_inset();
r.max.x += View::content_padding();
r.max.y += View::content_padding() + View::get_bottom_inset();
r
};
if show_connections && dual_panel {
@@ -187,39 +215,56 @@ impl NetworkContent {
});
// Redraw after delay if node is running at non-dual-panel mode.
if !dual_panel && Content::is_network_panel_open() && Node::is_running() {
if ((!dual_panel && Content::is_network_panel_open()) || dual_panel) && Node::is_running() {
ui.ctx().request_repaint_after(Node::STATS_UPDATE_DELAY);
}
}
/// 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
}
/// Check if application settings content is showing.
pub fn showing_settings(&self) -> bool {
self.settings_content.is_some()
}
/// 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);
// Setup vertical padding inside tab button.
ui.style_mut().spacing.button_padding = egui::vec2(0.0, 4.0);
// 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 == NodeTabType::Info, |_| {
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| {
View::tab_button(ui, GAUGE, current_type == NodeTabType::Metrics, |_| {
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| {
View::tab_button(ui, FACTORY, current_type == NodeTabType::Mining, |_| {
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| {
View::tab_button(ui, FADERS, current_type == NodeTabType::Settings, |_| {
let active = Some(current_type == NodeTabType::Settings);
View::tab_button(ui, FADERS, None, active, |_| {
self.node_tab_content = Box::new(NetworkSettings::default());
});
});
@@ -229,28 +274,37 @@ impl NetworkContent {
/// Draw title content.
fn title_ui(&mut self, ui: &mut egui::Ui, dual_panel: bool, show_connections: bool) {
let show_settings = self.showing_settings();
// Setup values for title panel.
let title_text = self.node_tab_content.get_type().title();
let subtitle_text = Node::get_sync_status_text();
let not_syncing = Node::not_syncing();
let title_content = if !show_connections {
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"))
TitleContentType::Title(t!("network.connections").into())
};
// 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, |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();
if AppConfig::show_connections_network_panel() {
ExternalConnection::check(None, ui.ctx());
}
});
} else if !dual_panel {
View::title_button_big(ui, GEAR, |_| {
self.settings_content = Some(SettingsContent::default());
});
}
}, |ui| {
if !dual_panel {
if !dual_panel && !show_settings {
View::title_button_big(ui, BRIEFCASE, |_| {
Content::toggle_network_panel();
});
@@ -259,7 +313,7 @@ impl NetworkContent {
}
/// Content to draw on loading.
pub fn loading_ui(ui: &mut egui::Ui, text: Option<String>) {
pub fn loading_ui(ui: &mut egui::Ui, text: Option<impl Into<String>>) {
match text {
None => {
ui.centered_and_justified(|ui| {
+8 -4
View File
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use egui::{RichText, Rounding, ScrollArea, vec2};
use egui::{RichText, CornerRadius, ScrollArea, vec2, StrokeKind};
use egui::scroll_area::ScrollBarVisibility;
use grin_core::consensus::{DAY_HEIGHT, GRIN_BASE, HOUR_SEC, REWARD};
use grin_servers::{DiffBlock, ServerStats};
@@ -38,7 +38,7 @@ impl NodeTab for NetworkMetrics {
NodeTabType::Metrics
}
fn ui(&mut self, ui: &mut egui::Ui, _: &dyn PlatformCallbacks) {
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 {
@@ -138,7 +138,7 @@ fn blocks_ui(ui: &mut egui::Ui, stats: &ServerStats) {
}
/// Draw block difficulty item.
fn block_item_ui(ui: &mut egui::Ui, db: &DiffBlock, rounding: Rounding) {
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| {
@@ -150,7 +150,11 @@ fn block_item_ui(ui: &mut egui::Ui, db: &DiffBlock, rounding: Rounding) {
// 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());
ui.painter().rect(rect,
rounding,
Colors::fill(),
View::item_stroke(),
StrokeKind::Outside);
// Draw block hash.
ui.horizontal(|ui| {
+9 -4
View File
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use egui::{RichText, Rounding, ScrollArea};
use egui::{RichText, CornerRadius, ScrollArea, StrokeKind};
use egui::scroll_area::ScrollBarVisibility;
use grin_chain::SyncStatus;
use grin_servers::WorkerStats;
@@ -24,6 +24,7 @@ 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::{NodeTab, NodeTabType};
use crate::gui::views::types::ContentContainer;
use crate::node::{Node, NodeConfig};
/// Mining tab content.
@@ -45,7 +46,7 @@ impl NodeTab for NetworkMining {
NodeTabType::Mining
}
fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
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;
@@ -190,13 +191,17 @@ impl NodeTab for NetworkMining {
const WORKER_ITEM_HEIGHT: f32 = 76.0;
/// Draw worker statistics item.
fn worker_item_ui(ui: &mut egui::Ui, ws: &WorkerStats, rounding: Rounding) {
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());
ui.painter().rect(rect,
rounding,
Colors::white_or_black(false),
View::item_stroke(),
StrokeKind::Outside);
ui.add_space(2.0);
ui.horizontal(|ui| {
+165 -83
View File
@@ -14,28 +14,36 @@
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::gui::Colors;
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";
@@ -44,17 +52,20 @@ impl ExternalConnectionModal {
/// 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))
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_modal_launch: true,
ext_node_url_edit,
ext_node_secret_edit,
ext_node_url_error: false,
ext_conn_id,
first_draw: true,
url_edit,
url_error: false,
secret_edit,
id,
scan_qr_content: None
}
}
@@ -64,43 +75,153 @@ impl ExternalConnectionModal {
cb: &dyn PlatformCallbacks,
modal: &Modal,
on_save: impl Fn(ExternalConnection)) {
ui.add_space(6.0);
// 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);
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.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);
// Close modal.
m.url_edit = "".to_string();
m.secret_edit = "".to_string();
m.url_error = false;
Modal::close();
}
};
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);
// 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;
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;
}
View::text_edit(ui, cb, &mut self.ext_node_url_edit, &mut url_edit_opts);
ui.add_space(8.0);
// 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);
ui.add_space(8.0);
ui.label(RichText::new(t!("wallets.node_secret"))
.size(17.0)
.color(Colors::gray()));
ui.add_space(8.0);
// 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);
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.ext_node_url_error {
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.
@@ -110,64 +231,25 @@ impl ExternalConnectionModal {
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();
self.url_edit = "".to_string();
self.secret_edit = "".to_string();
self.url_error = false;
Modal::close();
});
});
columns[1].vertical_centered_justified(|ui| {
// Add connection button callback.
let mut on_add = |ui: &mut egui::Ui| {
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())
};
// 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(Some(ext_conn.id), ui.ctx());
on_save(ext_conn);
// 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();
}
};
// Handle Enter key press.
let mut enter = false;
View::on_enter_key(ui, || {
enter = true;
});
if enter {
(on_add)(ui);
}
View::button_ui(ui, if self.ext_conn_id.is_some() {
View::button_ui(ui, if self.id.is_some() {
t!("modal.save")
} else {
t!("modal.add")
}, Colors::white_or_black(false), on_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());
}
}
+4 -4
View File
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use egui::{RichText, Rounding, ScrollArea};
use egui::{RichText, CornerRadius, ScrollArea, StrokeKind};
use egui::scroll_area::ScrollBarVisibility;
use grin_servers::PeerStats;
@@ -32,7 +32,7 @@ impl NodeTab for NetworkNode {
NodeTabType::Info
}
fn ui(&mut self, ui: &mut egui::Ui, _: &dyn PlatformCallbacks) {
fn tab_ui(&mut self, ui: &mut egui::Ui, _: &dyn PlatformCallbacks) {
ScrollArea::vertical()
.id_salt("integrated_node_info_scroll")
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
@@ -176,7 +176,7 @@ fn node_stats_ui(ui: &mut egui::Ui) {
const PEER_ITEM_HEIGHT: f32 = 77.0;
/// Draw connected peer info item.
fn peer_item_ui(ui: &mut egui::Ui, peer: &PeerStats, rounding: Rounding) {
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| {
@@ -184,7 +184,7 @@ fn peer_item_ui(ui: &mut egui::Ui, peer: &PeerStats, rounding: Rounding) {
ui.add_space(4.0);
// Draw round background.
ui.painter().rect(rect, rounding, Colors::fill_lite(), View::item_stroke());
ui.painter().rect(rect, r, Colors::fill(), View::item_stroke(), StrokeKind::Outside);
// Draw IP address.
ui.horizontal(|ui| {
+21 -24
View File
@@ -21,7 +21,7 @@ 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::{NodeTab, NodeTabType};
use crate::gui::views::types::{ModalContainer, ModalPosition};
use crate::gui::views::types::{ContentContainer, ModalPosition};
use crate::node::{Node, NodeConfig};
/// Integrated node settings tab content.
@@ -36,9 +36,6 @@ pub struct NetworkSettings {
pool: PoolSetup,
/// Dandelion server setup content.
dandelion: DandelionSetup,
/// [`Modal`] identifiers allowed at this ui container.
modal_ids: Vec<&'static str>
}
/// Identifier for settings reset confirmation [`Modal`].
@@ -52,16 +49,15 @@ impl Default for NetworkSettings {
stratum: StratumSetup::default(),
pool: PoolSetup::default(),
dandelion: DandelionSetup::default(),
modal_ids: vec![
RESET_SETTINGS_CONFIRMATION_MODAL
]
}
}
}
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,
@@ -69,21 +65,12 @@ impl ModalContainer for NetworkSettings {
modal: &Modal,
_: &dyn PlatformCallbacks) {
match modal.id {
RESET_SETTINGS_CONFIRMATION_MODAL => reset_settings_confirmation_modal(ui, modal),
RESET_SETTINGS_CONFIRMATION_MODAL => reset_settings_confirmation_modal(ui),
_ => {}
}
}
}
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);
fn container_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
ScrollArea::vertical()
.id_salt("node_settings_scroll")
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
@@ -135,6 +122,16 @@ impl NodeTab for NetworkSettings {
}
}
impl NodeTab for NetworkSettings {
fn get_type(&self) -> NodeTabType {
NodeTabType::Settings
}
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) {
@@ -230,7 +227,7 @@ fn reset_settings_ui(ui: &mut egui::Ui) {
}
/// Confirmation to reset settings to default values.
fn reset_settings_confirmation_modal(ui: &mut egui::Ui, modal: &Modal) {
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"));
@@ -249,12 +246,12 @@ fn reset_settings_confirmation_modal(ui: &mut egui::Ui, modal: &Modal) {
columns[0].vertical_centered_justified(|ui| {
View::button(ui, t!("network_settings.reset"), Colors::white_or_black(false), || {
NodeConfig::reset_to_default();
modal.close();
Modal::close();
});
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
modal.close();
Modal::close();
});
});
});
+91 -90
View File
@@ -14,12 +14,12 @@
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::types::{ContentContainer, ModalPosition};
use crate::gui::views::{Modal, TextEdit, View};
use crate::gui::Colors;
use crate::gui::views::network::NetworkSettings;
use crate::node::NodeConfig;
/// Dandelion server setup section content.
@@ -35,9 +35,6 @@ pub struct DandelionSetup {
/// 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>,
}
/// Identifier epoch duration value [`Modal`].
@@ -56,19 +53,18 @@ impl Default for DandelionSetup {
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
]
}
}
}
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,
@@ -83,41 +79,36 @@ impl ModalContainer for DandelionSetup {
_ => {}
}
}
}
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);
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, cb);
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, cb);
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, cb);
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, cb);
self.stem_prob_ui(ui);
ui.add_space(6.0);
View::horizontal_line(ui, Colors::item_stroke());
@@ -131,9 +122,11 @@ impl DandelionSetup {
ui.add_space(6.0);
});
}
}
impl DandelionSetup {
/// Draw epoch duration setup content.
fn epoch_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
fn epoch_ui(&mut self, ui: &mut egui::Ui) {
ui.label(RichText::new(t!("network_settings.epoch_duration"))
.size(16.0)
.color(Colors::gray())
@@ -149,13 +142,20 @@ impl DandelionSetup {
.position(ModalPosition::CenterTop)
.title(t!("network_settings.change_value"))
.show();
cb.show_keyboard();
});
ui.add_space(6.0);
}
/// 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);
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("network_settings.epoch_duration"))
@@ -164,8 +164,11 @@ impl DandelionSetup {
ui.add_space(8.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);
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);
}
// Show error when specified value is not valid or reminder to restart enabled node.
if self.epoch_edit.parse::<u16>().is_err() {
@@ -184,25 +187,17 @@ impl DandelionSetup {
// 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(epoch) = self.epoch_edit.parse::<u16>() {
NodeConfig::save_dandelion_epoch(epoch);
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();
Modal::close();
});
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.save"), Colors::white_or_black(false), on_save);
View::button(ui, t!("modal.save"), Colors::white_or_black(false), || {
on_save(self);
});
});
});
ui.add_space(6.0);
@@ -210,7 +205,7 @@ impl DandelionSetup {
}
/// Draw embargo expiration time setup content.
fn embargo_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
fn embargo_ui(&mut self, ui: &mut egui::Ui) {
ui.label(RichText::new(t!("network_settings.embargo_timer"))
.size(16.0)
.color(Colors::gray())
@@ -225,13 +220,20 @@ impl DandelionSetup {
.position(ModalPosition::CenterTop)
.title(t!("network_settings.change_value"))
.show();
cb.show_keyboard();
});
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();
}
};
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("network_settings.embargo_timer"))
@@ -240,8 +242,11 @@ impl DandelionSetup {
ui.add_space(8.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);
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);
}
// Show error when specified value is not valid or reminder to restart enabled node.
if self.embargo_edit.parse::<u16>().is_err() {
@@ -260,25 +265,17 @@ impl DandelionSetup {
// 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.embargo_edit.parse::<u16>() {
NodeConfig::save_dandelion_embargo(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();
Modal::close();
});
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.save"), Colors::white_or_black(false), on_save);
View::button(ui, t!("modal.save"), Colors::white_or_black(false), || {
on_save(self);
});
});
});
ui.add_space(6.0);
@@ -286,7 +283,7 @@ impl DandelionSetup {
}
/// Draw aggregation period setup content.
fn aggregation_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
fn aggregation_ui(&mut self, ui: &mut egui::Ui) {
ui.label(RichText::new(t!("network_settings.aggregation_period"))
.size(16.0)
.color(Colors::gray())
@@ -302,13 +299,20 @@ impl DandelionSetup {
.position(ModalPosition::CenterTop)
.title(t!("network_settings.change_value"))
.show();
cb.show_keyboard();
});
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();
}
};
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("network_settings.aggregation_period"))
@@ -317,8 +321,11 @@ impl DandelionSetup {
ui.add_space(8.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);
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);
}
// Show error when specified value is not valid or reminder to restart enabled node.
if self.aggregation_edit.parse::<u16>().is_err() {
@@ -337,25 +344,17 @@ impl DandelionSetup {
// 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();
Modal::close();
});
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.save"), Colors::white_or_black(false), on_save);
View::button(ui, t!("modal.save"), Colors::white_or_black(false), || {
on_save(self);
});
});
});
ui.add_space(6.0);
@@ -363,7 +362,7 @@ impl DandelionSetup {
}
/// Draw stem phase probability setup content.
fn stem_prob_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
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())
@@ -379,13 +378,20 @@ impl DandelionSetup {
.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) {
// 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();
}
};
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("network_settings.stem_probability"))
@@ -394,8 +400,11 @@ impl DandelionSetup {
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);
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);
}
// Show error when specified value is not valid or reminder to restart enabled node.
if self.stem_prob_edit.parse::<u8>().is_err() {
@@ -414,25 +423,17 @@ impl DandelionSetup {
// 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();
Modal::close();
});
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.save"), Colors::white_or_black(false), on_save);
View::button(ui, t!("modal.save"), Colors::white_or_black(false), || {
on_save(self);
});
});
});
ui.add_space(6.0);
+216 -100
View File
@@ -12,21 +12,28 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use egui::{Id, RichText};
use eframe::emath::Align;
use eframe::epaint::{RectShape, StrokeKind};
use egui::{CursorIcon, Id, Layout, RichText, Sense, UiBuilder};
use grin_core::global::ChainTypes;
use crate::AppConfig;
use crate::gui::Colors;
use crate::gui::icons::{CLOCK_CLOCKWISE, COMPUTER_TOWER, PLUG, POWER, SHIELD, SHIELD_SLASH};
use crate::gui::icons::{CLOCK_CLOCKWISE, COMPUTER_TOWER, FOLDERS, PLUG, POWER, SHIELD, SHIELD_SLASH};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Modal, View};
use crate::gui::views::network::NetworkContent;
use crate::gui::views::network::settings::NetworkSettings;
use crate::gui::views::types::{ModalContainer, ModalPosition, TextEditOptions};
use crate::gui::views::network::NetworkContent;
use crate::gui::views::types::{ContentContainer, ModalPosition};
use crate::gui::views::{FilePickContent, FilePickContentType, Modal, TextEdit, View};
use crate::gui::Colors;
use crate::node::{Node, NodeConfig};
use crate::AppConfig;
/// Integrated node general setup section content.
pub struct NodeSetup {
/// Data path value value for [`Modal`].
data_path_edit: String,
/// Button to pick directory for chain data.
pick_data_dir: FilePickContent,
/// IP Addresses available at system.
available_ips: Vec<String>,
@@ -43,44 +50,47 @@ pub struct NodeSetup {
/// Future Time Limit value.
ftl_edit: String,
/// [`Modal`] identifiers allowed at this ui container.
modal_ids: Vec<&'static str>
}
/// Identifier for chain data path value [`Modal`].
const DATA_PATH_MODAL: &'static str = "node_data_path";
/// Identifier for API port value [`Modal`].
pub const API_PORT_MODAL: &'static str = "api_port";
const API_PORT_MODAL: &'static str = "node_api_port";
/// Identifier for API secret value [`Modal`].
pub const API_SECRET_MODAL: &'static str = "api_secret";
const API_SECRET_MODAL: &'static str = "node_api_secret";
/// Identifier for Foreign API secret value [`Modal`].
pub const FOREIGN_API_SECRET_MODAL: &'static str = "foreign_api_secret";
const FOREIGN_API_SECRET_MODAL: &'static str = "node_foreign_api_secret";
/// Identifier for FTL value [`Modal`].
pub const FTL_MODAL: &'static str = "ftl";
const FTL_MODAL: &'static str = "node_ftl";
impl Default for NodeSetup {
fn default() -> Self {
let (api_ip, api_port) = NodeConfig::get_api_ip_port();
let is_api_port_available = NodeConfig::is_api_port_available(&api_ip, &api_port);
Self {
data_path_edit: NodeConfig::get_chain_data_path(),
pick_data_dir: FilePickContent::new(
FilePickContentType::ItemButton(View::item_rounding(0, 1, true))
).no_parse().pick_folder(),
available_ips: NodeConfig::get_ip_addrs(),
api_port_edit: api_port,
api_port_available_edit: is_api_port_available,
is_api_port_available,
secret_edit: "".to_string(),
ftl_edit: NodeConfig::get_ftl(),
modal_ids: vec![
API_PORT_MODAL,
API_SECRET_MODAL,
FOREIGN_API_SECRET_MODAL,
FTL_MODAL
]
}
}
}
impl ModalContainer for NodeSetup {
fn modal_ids(&self) -> &Vec<&'static str> {
&self.modal_ids
impl ContentContainer for NodeSetup {
fn modal_ids(&self) -> Vec<&'static str> {
vec![
DATA_PATH_MODAL,
API_PORT_MODAL,
API_SECRET_MODAL,
FOREIGN_API_SECRET_MODAL,
FTL_MODAL
]
}
fn modal_ui(&mut self,
@@ -88,6 +98,7 @@ impl ModalContainer for NodeSetup {
modal: &Modal,
cb: &dyn PlatformCallbacks) {
match modal.id {
DATA_PATH_MODAL => self.data_path_edit_modal_ui(ui, cb),
API_PORT_MODAL => self.api_port_modal(ui, modal, cb),
API_SECRET_MODAL => self.secret_modal(ui, modal, cb),
FOREIGN_API_SECRET_MODAL => self.secret_modal(ui, modal, cb),
@@ -95,13 +106,8 @@ impl ModalContainer for NodeSetup {
_ => {}
}
}
}
impl NodeSetup {
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);
fn container_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
View::sub_title(ui, format!("{} {}", COMPUTER_TOWER, t!("network_settings.server")));
View::horizontal_line(ui, Colors::stroke());
ui.add_space(6.0);
@@ -111,7 +117,8 @@ impl NodeSetup {
ui.add_space(2.0);
// Show loading indicator or controls to stop/start/restart node.
if Node::is_stopping() || Node::is_restarting() || Node::is_starting() {
if Node::is_stopping() || Node::is_restarting() || Node::is_starting()
|| Node::data_dir_changing() {
ui.vertical_centered(|ui| {
ui.add_space(8.0);
View::small_loading_spinner(ui);
@@ -163,6 +170,13 @@ impl NodeSetup {
});
ui.add_space(6.0);
// Show data location selection for Desktop when it already started or turned off.
if !Node::is_restarting() && !Node::is_stopping() && !Node::is_starting() &&
View::is_desktop() {
self.data_dir_ui(ui, cb);
ui.add_space(6.0);
}
if self.available_ips.is_empty() {
// Show message when IP addresses are not available on the system.
NetworkSettings::no_ip_address_ui(ui);
@@ -185,12 +199,12 @@ impl NodeSetup {
NodeConfig::save_api_address(selected_ip, &api_port);
});
// Show API port setup.
self.api_port_setup_ui(ui, cb);
self.api_port_setup_ui(ui);
// Show API secret setup.
self.secret_ui(API_SECRET_MODAL, ui, cb);
self.secret_ui(API_SECRET_MODAL, ui);
ui.add_space(12.0);
// Show Foreign API secret setup.
self.secret_ui(FOREIGN_API_SECRET_MODAL, ui, cb);
self.secret_ui(FOREIGN_API_SECRET_MODAL, ui);
ui.add_space(6.0);
});
}
@@ -201,7 +215,7 @@ impl NodeSetup {
ui.vertical_centered(|ui| {
// Show FTL setup.
self.ftl_ui(ui, cb);
self.ftl_ui(ui);
ui.add_space(6.0);
View::horizontal_line(ui, Colors::item_stroke());
@@ -218,6 +232,104 @@ impl NodeSetup {
self.archive_mode_ui(ui);
});
}
}
impl NodeSetup {
/// Draw content to change chain data directory.
fn data_dir_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
// 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| {
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
self.pick_data_dir.ui(ui, cb, |path| {
Node::change_data_dir(path);
});
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 path = NodeConfig::get_chain_data_path();
View::ellipsize_text(ui, path, 18.0, Colors::title(false));
ui.add_space(1.0);
let desc = format!("{} {}", FOLDERS, t!("files_location"));
ui.label(RichText::new(desc).size(15.0).color(Colors::gray()));
ui.add_space(8.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.data_path_edit = NodeConfig::get_chain_data_path();
// Show chain data path edit modal.
Modal::new(DATA_PATH_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("network.node"))
.show();
}
}
/// Draw data path input [`Modal`] content.
fn data_path_edit_modal_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
ui.add_space(6.0);
ui.vertical_centered(|ui| {
let on_save = |path: &String| {
Node::change_data_dir(path.clone());
Modal::close();
};
ui.label(RichText::new(format!("{}:", t!("files_location")))
.size(17.0)
.color(Colors::gray()));
ui.add_space(8.0);
// Draw chain data path text edit.
let mut edit = TextEdit::new(Id::from(DATA_PATH_MODAL)).paste();
edit.ui(ui, &mut self.data_path_edit, cb);
if edit.enter_pressed {
on_save(&self.data_path_edit);
}
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.data_path_edit);
});
});
});
ui.add_space(6.0);
});
});
}
/// Draw [`ChainTypes`] setup content.
pub fn chain_type_ui(ui: &mut egui::Ui) {
@@ -250,7 +362,7 @@ impl NodeSetup {
}
/// Draw API port setup content.
fn api_port_setup_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
fn api_port_setup_ui(&mut self, ui: &mut egui::Ui) {
ui.label(RichText::new(t!("network_settings.api_port")).size(16.0).color(Colors::gray()));
ui.add_space(6.0);
@@ -265,7 +377,6 @@ impl NodeSetup {
.position(ModalPosition::CenterTop)
.title(t!("network_settings.change_value"))
.show();
cb.show_keyboard();
});
ui.add_space(6.0);
@@ -281,6 +392,22 @@ impl NodeSetup {
/// Draw API port [`Modal`] content.
fn api_port_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
let on_save = |c: &mut NodeSetup| {
// Check if port is available.
let (api_ip, _) = NodeConfig::get_api_ip_port();
let available = NodeConfig::is_api_port_available(&api_ip, &c.api_port_edit);
c.api_port_available_edit = available;
if available {
// Save port at config if it's available.
NodeConfig::save_api_address(&api_ip, &c.api_port_edit);
if Node::is_running() {
Node::restart();
}
c.is_api_port_available = true;
Modal::close();
}
};
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("network_settings.api_port"))
@@ -289,8 +416,11 @@ impl NodeSetup {
ui.add_space(6.0);
// Draw API port text edit.
let mut api_port_edit_opts = TextEditOptions::new(Id::from(modal.id)).h_center();
View::text_edit(ui, cb, &mut self.api_port_edit, &mut api_port_edit_opts);
let mut api_port_edit = TextEdit::new(Id::from(modal.id)).h_center().numeric();
api_port_edit.ui(ui, &mut self.api_port_edit, cb);
if api_port_edit.enter_pressed {
on_save(self);
}
// Show error when specified port is unavailable or reminder to restart enabled node.
if !self.api_port_available_edit {
@@ -309,36 +439,16 @@ impl NodeSetup {
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
// Save button callback.
let on_save = || {
// Check if port is available.
let (api_ip, _) = NodeConfig::get_api_ip_port();
let available = NodeConfig::is_api_port_available(&api_ip, &self.api_port_edit);
self.api_port_available_edit = available;
if available {
// Save port at config if it's available.
NodeConfig::save_api_address(&api_ip, &self.api_port_edit);
if Node::is_running() {
Node::restart();
}
self.is_api_port_available = true;
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), || {
cb.hide_keyboard();
modal.close();
Modal::close();
});
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.save"), Colors::white_or_black(false), on_save);
View::button(ui, t!("modal.save"), Colors::white_or_black(false), || {
on_save(self);
});
});
});
ui.add_space(6.0);
@@ -346,7 +456,7 @@ impl NodeSetup {
}
/// Draw API secret token setup content.
fn secret_ui(&mut self, modal_id: &'static str, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
fn secret_ui(&mut self, modal_id: &'static str, ui: &mut egui::Ui) {
let secret_title = match modal_id {
API_SECRET_MODAL => t!("network_settings.api_secret"),
_ => t!("network_settings.foreign_api_secret")
@@ -376,12 +486,24 @@ impl NodeSetup {
.position(ModalPosition::CenterTop)
.title(t!("network_settings.change_value"))
.show();
cb.show_keyboard();
});
}
/// Draw API secret token [`Modal`] content.
fn secret_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
let on_save = |c: &mut NodeSetup| {
let secret = c.secret_edit.clone();
match modal.id {
API_SECRET_MODAL => {
NodeConfig::save_api_secret(&secret);
}
_ => {
NodeConfig::save_foreign_api_secret(&secret);
}
};
Modal::close();
};
ui.add_space(6.0);
ui.vertical_centered(|ui| {
let description = match modal.id {
@@ -392,8 +514,14 @@ impl NodeSetup {
ui.add_space(8.0);
// Draw API secret token value text edit.
let mut secret_edit_opts = TextEditOptions::new(Id::from(modal.id)).copy().paste();
View::text_edit(ui, cb, &mut self.secret_edit, &mut secret_edit_opts);
let mut secret_edit = TextEdit::new(Id::from(modal.id))
.copy()
.paste();
secret_edit.ui(ui, &mut self.secret_edit, cb);
if secret_edit.enter_pressed {
on_save(self);
}
ui.add_space(6.0);
// Show reminder to restart enabled node.
@@ -412,30 +540,16 @@ impl NodeSetup {
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
// Save button callback.
let on_save = || {
let secret = self.secret_edit.clone();
match modal.id {
API_SECRET_MODAL => {
NodeConfig::save_api_secret(&secret);
}
_ => {
NodeConfig::save_foreign_api_secret(&secret);
}
};
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), || {
cb.hide_keyboard();
modal.close();
Modal::close();
});
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.save"), Colors::white_or_black(false), on_save);
View::button(ui, t!("modal.save"), Colors::white_or_black(false), || {
on_save(self);
});
});
});
ui.add_space(6.0);
@@ -443,7 +557,7 @@ impl NodeSetup {
}
/// Draw FTL setup content.
fn ftl_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
fn ftl_ui(&mut self, ui: &mut egui::Ui) {
ui.label(RichText::new(t!("network_settings.ftl"))
.size(16.0)
.color(Colors::gray())
@@ -461,7 +575,6 @@ impl NodeSetup {
.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.ftl_description"))
@@ -472,6 +585,14 @@ impl NodeSetup {
/// Draw FTL [`Modal`] content.
fn ftl_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
// Save button callback.
let on_save = |c: &mut NodeSetup| {
if let Ok(ftl) = c.ftl_edit.parse::<u64>() {
NodeConfig::save_ftl(ftl);
Modal::close();
}
};
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("network_settings.ftl"))
@@ -480,8 +601,11 @@ impl NodeSetup {
ui.add_space(8.0);
// Draw ftl value text edit.
let mut ftl_edit_opts = TextEditOptions::new(Id::from(modal.id)).h_center();
View::text_edit(ui, cb, &mut self.ftl_edit, &mut ftl_edit_opts);
let mut ftl_edit = TextEdit::new(Id::from(modal.id)).h_center().numeric();
ftl_edit.ui(ui, &mut self.ftl_edit, cb);
if ftl_edit.enter_pressed {
on_save(self);
}
// Show error when specified value is not valid or reminder to restart enabled node.
if self.ftl_edit.parse::<u64>().is_err() {
@@ -500,25 +624,17 @@ impl NodeSetup {
// 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(ftl) = self.ftl_edit.parse::<u64>() {
NodeConfig::save_ftl(ftl);
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();
Modal::close();
});
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.save"), Colors::white_or_black(false), on_save);
View::button(ui, t!("modal.save"), Colors::white_or_black(false), || {
on_save(self);
});
});
});
ui.add_space(6.0);
+221 -202
View File
@@ -12,16 +12,17 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use egui::{Align, Id, Layout, RichText};
use egui::{Align, Id, Layout, RichText, StrokeKind};
use egui_async::Bind;
use grin_core::global::ChainTypes;
use crate::AppConfig;
use crate::gui::Colors;
use crate::gui::icons::{ARROW_FAT_LINES_DOWN, ARROW_FAT_LINES_UP, GLOBE_SIMPLE, HANDSHAKE, PLUG, PLUS_CIRCLE, PROHIBIT_INSET, TRASH};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Modal, View};
use crate::gui::views::{Modal, TextEdit, View};
use crate::gui::views::network::settings::NetworkSettings;
use crate::gui::views::types::{ModalContainer, ModalPosition, TextEditOptions};
use crate::gui::views::types::{ContentContainer, ModalPosition};
use crate::node::{Node, NodeConfig, PeersConfig};
/// Type of peer.
@@ -44,8 +45,10 @@ pub struct P2PSetup {
/// Flag to check if p2p port from saved config value is available.
is_port_available: bool,
/// Flag to check if entered peer address is correct and/or available.
is_correct_address_edit: bool,
/// Async check entered peer address.
address_check: Bind<bool, String>,
/// Flag to check if peer is correct and/or available.
address_available: Option<bool>,
/// Peer edit value for modal.
peer_edit: String,
@@ -65,9 +68,6 @@ pub struct P2PSetup {
/// Flag to check if reset of peers was called.
peers_reset: bool,
/// [`Modal`] identifiers allowed at this ui container.
modal_ids: Vec<&'static str>
}
/// Identifier for port value [`Modal`].
@@ -95,14 +95,15 @@ impl Default for P2PSetup {
.iter()
.map(|s| s.to_string())
.collect();
let default_test_seeds = grin_servers::TESTNET_DNS_SEEDS
let default_test_seeds = Node::TESTNET_DNS_SEEDS
.into_iter()
.map(|s| s.to_string())
.collect();
Self {
port_edit: port,
port_available_edit: is_port_available,
is_correct_address_edit: true,
address_check: Bind::new(false),
address_available: Some(true),
is_port_available,
peer_edit: "".to_string(),
default_main_seeds,
@@ -111,23 +112,22 @@ impl Default for P2PSetup {
max_inbound_count: NodeConfig::get_max_inbound_peers(),
max_outbound_count: NodeConfig::get_max_outbound_peers(),
peers_reset: false,
modal_ids: vec![
PORT_MODAL,
CUSTOM_SEED_MODAL,
ALLOW_PEER_MODAL,
DENY_PEER_MODAL,
PREFER_PEER_MODAL,
BAN_WINDOW_MODAL,
MAX_INBOUND_MODAL,
MAX_OUTBOUND_MODAL
]
}
}
}
impl ModalContainer for P2PSetup {
fn modal_ids(&self) -> &Vec<&'static str> {
&self.modal_ids
impl ContentContainer for P2PSetup {
fn modal_ids(&self) -> Vec<&'static str> {
vec![
PORT_MODAL,
CUSTOM_SEED_MODAL,
ALLOW_PEER_MODAL,
DENY_PEER_MODAL,
PREFER_PEER_MODAL,
BAN_WINDOW_MODAL,
MAX_INBOUND_MODAL,
MAX_OUTBOUND_MODAL
]
}
fn modal_ui(&mut self,
@@ -146,30 +146,22 @@ impl ModalContainer for P2PSetup {
_ => {}
}
}
}
impl P2PSetup {
/// Title for custom DNS Seeds setup section.
const DNS_SEEDS_TITLE: &'static str = "DNS Seeds";
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);
fn container_ui(&mut self, ui: &mut egui::Ui, _: &dyn PlatformCallbacks) {
View::sub_title(ui, format!("{} {}", HANDSHAKE, t!("network_settings.p2p_server")));
View::horizontal_line(ui, Colors::stroke());
ui.add_space(6.0);
ui.vertical_centered(|ui| {
// Show p2p port setup.
self.port_ui(ui, cb);
self.port_ui(ui);
ui.add_space(6.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(6.0);
// Show seeding type setup.
self.seeding_type_ui(ui, cb);
self.seeding_type_ui(ui);
ui.add_space(6.0);
View::horizontal_line(ui, Colors::item_stroke());
@@ -180,7 +172,7 @@ impl P2PSetup {
.color(Colors::gray()));
ui.add_space(6.0);
// Show allowed peers setup.
self.peer_list_ui(ui, &PeerType::Allowed, cb);
self.peer_list_ui(ui, &PeerType::Allowed);
ui.add_space(6.0);
View::horizontal_line(ui, Colors::item_stroke());
@@ -191,7 +183,7 @@ impl P2PSetup {
.color(Colors::gray()));
ui.add_space(6.0);
// Show denied peers setup.
self.peer_list_ui(ui, &PeerType::Denied, cb);
self.peer_list_ui(ui, &PeerType::Denied);
ui.add_space(6.0);
View::horizontal_line(ui, Colors::item_stroke());
@@ -202,29 +194,28 @@ impl P2PSetup {
.color(Colors::gray()));
ui.add_space(6.0);
// Show preferred peers setup.
self.peer_list_ui(ui, &PeerType::Preferred, cb);
self.peer_list_ui(ui, &PeerType::Preferred);
ui.add_space(6.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(6.0);
// Show ban window setup.
self.ban_window_ui(ui, cb);
self.ban_window_ui(ui);
ui.add_space(6.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(6.0);
// Show maximum inbound peers value setup.
self.max_inbound_ui(ui, cb);
self.max_inbound_ui(ui);
ui.add_space(6.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(6.0);
// Show maximum outbound peers value setup.
self.max_outbound_ui(ui, cb);
self.max_outbound_ui(ui);
if !Node::is_restarting() && !self.peers_reset {
ui.add_space(6.0);
@@ -236,9 +227,14 @@ impl P2PSetup {
}
});
}
}
/// Title for custom DNS Seeds setup section.
const DNS_SEEDS_TITLE: &'static str = "DNS Seeds";
impl P2PSetup {
/// Draw p2p port setup content.
fn port_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
fn port_ui(&mut self, ui: &mut egui::Ui) {
ui.label(RichText::new(t!("network_settings.p2p_port"))
.size(16.0)
.color(Colors::gray())
@@ -249,16 +245,15 @@ impl P2PSetup {
View::button(ui,
format!("{} {}", PLUG, &port),
Colors::white_or_black(false), || {
// Setup values for modal.
self.port_edit = port;
self.port_available_edit = self.is_port_available;
// Show p2p port modal.
Modal::new(PORT_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("network_settings.change_value"))
.show();
cb.show_keyboard();
});
// Setup values for modal.
self.port_edit = port;
self.port_available_edit = self.is_port_available;
// Show p2p port modal.
Modal::new(PORT_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("network_settings.change_value"))
.show();
});
ui.add_space(6.0);
// Show error when p2p port is unavailable.
@@ -273,6 +268,22 @@ impl P2PSetup {
/// Draw p2p port [`Modal`] content.
fn port_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
let on_save = |c: &mut P2PSetup| {
// Check if port is available.
let available = NodeConfig::is_p2p_port_available(&c.port_edit);
c.port_available_edit = available;
// Save port at config if it's available.
if available {
NodeConfig::save_p2p_port(c.port_edit.parse::<u16>().unwrap());
if Node::is_running() {
Node::restart();
}
c.is_port_available = true;
Modal::close();
}
};
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("network_settings.p2p_port"))
@@ -281,8 +292,11 @@ impl P2PSetup {
ui.add_space(8.0);
// Draw p2p port text edit.
let mut text_edit_opts = TextEditOptions::new(Id::from(modal.id)).h_center();
View::text_edit(ui, cb, &mut self.port_edit, &mut text_edit_opts);
let mut edit = TextEdit::new(Id::from(modal.id)).h_center().numeric();
edit.ui(ui, &mut self.port_edit, cb);
if edit.enter_pressed {
on_save(self);
}
// Show error when specified port is unavailable.
if !self.port_available_edit {
@@ -299,34 +313,17 @@ impl P2PSetup {
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
// Save button callback.
let on_save = || {
// Check if port is available.
let available = NodeConfig::is_p2p_port_available(&self.port_edit);
self.port_available_edit = available;
// Save port at config if it's available.
if available {
NodeConfig::save_p2p_port(self.port_edit.parse::<u16>().unwrap());
if Node::is_running() {
Node::restart();
}
self.is_port_available = true;
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();
Modal::close();
});
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.save"), Colors::white_or_black(false), on_save);
View::button(ui, t!("modal.save"), Colors::white_or_black(false), || {
on_save(self);
});
});
});
ui.add_space(6.0);
@@ -335,7 +332,7 @@ impl P2PSetup {
}
/// Draw peer list content based on provided [`PeerType`].
fn peer_list_ui(&mut self, ui: &mut egui::Ui, peer_type: &PeerType, cb: &dyn PlatformCallbacks) {
fn peer_list_ui(&mut self, ui: &mut egui::Ui, peer_type: &PeerType) {
let peers = match peer_type {
PeerType::DefaultSeed => {
if AppConfig::chain_type() == ChainTypes::Testnet {
@@ -370,8 +367,10 @@ impl P2PSetup {
ui.label(RichText::new(desc)
.size(16.0)
.color(Colors::inactive_text()));
ui.add_space(12.0);
} else if !peers.is_empty() {
ui.add_space(12.0);
}
ui.add_space(12.0);
let add_text = if peer_type == &PeerType::CustomSeed {
format!("{} {}", PLUS_CIRCLE, t!("network_settings.add_seed"))
@@ -381,7 +380,8 @@ impl P2PSetup {
};
View::button(ui, add_text, Colors::white_or_black(false), || {
// Setup values for modal.
self.is_correct_address_edit = true;
self.address_check = Bind::new(false);
self.address_available = Some(true);
self.peer_edit = "".to_string();
// Select modal id.
let modal_id = match peer_type {
@@ -392,17 +392,16 @@ impl P2PSetup {
};
// Select modal title.
let modal_title = match peer_type {
PeerType::Allowed => t!("network_settings.allow_list"),
PeerType::Denied => t!("network_settings.deny_list"),
PeerType::Preferred => t!("network_settings.favourites"),
_ => Self::DNS_SEEDS_TITLE.to_string()
PeerType::Allowed => t!("network_settings.allow_list").into(),
PeerType::Denied => t!("network_settings.deny_list").into(),
PeerType::Preferred => t!("network_settings.favourites").into(),
_ => DNS_SEEDS_TITLE.to_string()
};
// Show modal to add peer.
Modal::new(modal_id)
.position(ModalPosition::CenterTop)
.title(modal_title)
.show();
cb.show_keyboard();
});
}
ui.add_space(6.0);
@@ -410,6 +409,35 @@ impl P2PSetup {
/// Draw peer creation [`Modal`] content.
fn peer_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
if self.address_available.is_none() {
let peer = self.peer_edit.clone();
if let Some(res) = self.address_check.read_or_request(|| async {
let available = PeersConfig::peer_to_addr(peer).is_some();
Ok(available)
}) {
match res {
Ok(available) => {
self.address_available = Some(*available);
// Save peer at config.
if *available {
let peer = self.peer_edit.clone();
match modal.id {
CUSTOM_SEED_MODAL => NodeConfig::save_custom_seed(peer),
ALLOW_PEER_MODAL => NodeConfig::allow_peer(peer),
DENY_PEER_MODAL => NodeConfig::deny_peer(peer),
PREFER_PEER_MODAL => NodeConfig::prefer_peer(peer),
&_ => {}
}
Modal::close();
}
}
Err(_) => {
self.address_available = Some(false);
}
}
}
}
ui.add_space(6.0);
ui.vertical_centered(|ui| {
let label_text = match modal.id {
@@ -420,15 +448,23 @@ impl P2PSetup {
ui.add_space(8.0);
// Draw peer address text edit.
let mut peer_text_edit_opts = TextEditOptions::new(Id::from(modal.id)).paste();
View::text_edit(ui, cb, &mut self.peer_edit, &mut peer_text_edit_opts);
let mut edit = TextEdit::new(Id::from(modal.id)).paste();
if self.address_available.is_none() {
edit = edit.disable();
}
edit.ui(ui, &mut self.peer_edit, cb);
if edit.enter_pressed {
self.address_available = None;
}
// Show error when specified address is incorrect.
if !self.is_correct_address_edit {
ui.add_space(10.0);
ui.label(RichText::new(t!("network_settings.peer_address_error"))
.size(16.0)
.color(Colors::red()));
if let Some(available) = self.address_available {
if !available {
ui.add_space(10.0);
ui.label(RichText::new(t!("network_settings.peer_address_error"))
.size(16.0)
.color(Colors::red()));
}
}
ui.add_space(12.0);
@@ -437,39 +473,19 @@ impl P2PSetup {
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
// Save button callback.
let on_save = || {
// Check if peer is correct and/or available.
let peer = self.peer_edit.clone();
let is_correct_address = PeersConfig::peer_to_addr(peer.clone()).is_some();
self.is_correct_address_edit = is_correct_address;
// Save peer at config.
if is_correct_address {
match modal.id {
CUSTOM_SEED_MODAL => NodeConfig::save_custom_seed(peer),
ALLOW_PEER_MODAL => NodeConfig::allow_peer(peer),
DENY_PEER_MODAL => NodeConfig::deny_peer(peer),
PREFER_PEER_MODAL => NodeConfig::prefer_peer(peer),
&_ => {}
}
self.is_port_available = true;
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();
Modal::close();
});
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.save"), Colors::white_or_black(false), on_save);
View::button(ui, t!("modal.save"), Colors::white_or_black(false), || {
if self.address_available.is_some() {
self.address_available = None;
}
});
});
});
ui.add_space(6.0);
@@ -478,9 +494,8 @@ impl P2PSetup {
}
/// Draw seeding type setup content.
fn seeding_type_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
let title = Self::DNS_SEEDS_TITLE;
ui.label(RichText::new(title).size(16.0).color(Colors::gray()));
fn seeding_type_ui(&mut self, ui: &mut egui::Ui) {
ui.label(RichText::new(DNS_SEEDS_TITLE).size(16.0).color(Colors::gray()));
ui.add_space(2.0);
let default_seeding = NodeConfig::is_default_seeding_type();
@@ -494,11 +509,11 @@ impl P2PSetup {
} else {
PeerType::CustomSeed
};
self.peer_list_ui(ui, &peers_type, cb);
self.peer_list_ui(ui, &peers_type);
}
/// Draw ban window setup content.
fn ban_window_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
fn ban_window_ui(&mut self, ui: &mut egui::Ui) {
ui.label(RichText::new(t!("network_settings.ban_window"))
.size(16.0)
.color(Colors::gray())
@@ -509,15 +524,14 @@ impl P2PSetup {
View::button(ui,
format!("{} {}", PROHIBIT_INSET, &ban_window),
Colors::white_or_black(false), || {
// Setup values for modal.
self.ban_window_edit = ban_window;
// Show ban window period setup modal.
Modal::new(BAN_WINDOW_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("network_settings.change_value"))
.show();
cb.show_keyboard();
});
// Setup values for modal.
self.ban_window_edit = ban_window;
// Show ban window period setup modal.
Modal::new(BAN_WINDOW_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("network_settings.change_value"))
.show();
});
ui.add_space(6.0);
ui.label(RichText::new(t!("network_settings.ban_window_desc"))
.size(16.0)
@@ -528,6 +542,13 @@ impl P2PSetup {
/// Draw ban window [`Modal`] content.
fn ban_window_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
let on_save = |c: &mut P2PSetup| {
if let Ok(ban_window) = c.ban_window_edit.parse::<i64>() {
NodeConfig::save_p2p_ban_window(ban_window);
Modal::close();
}
};
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("network_settings.ban_window"))
@@ -536,8 +557,11 @@ impl P2PSetup {
ui.add_space(8.0);
// Draw ban window text edit.
let mut text_edit_opts = TextEditOptions::new(Id::from(modal.id)).h_center();
View::text_edit(ui, cb, &mut self.ban_window_edit, &mut text_edit_opts);
let mut edit = TextEdit::new(Id::from(modal.id)).h_center().numeric();
edit.ui(ui, &mut self.ban_window_edit, cb);
if edit.enter_pressed {
on_save(self);
}
// Show error when specified value is not valid or reminder to restart enabled node.
if self.ban_window_edit.parse::<i64>().is_err() {
@@ -556,25 +580,16 @@ impl P2PSetup {
// 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(ban_window) = self.ban_window_edit.parse::<i64>() {
NodeConfig::save_p2p_ban_window(ban_window);
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();
Modal::close();
});
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.save"), Colors::white_or_black(false), on_save);
View::button(ui, t!("modal.save"), Colors::white_or_black(false), || {
on_save(self);
});
});
});
ui.add_space(6.0);
@@ -582,7 +597,7 @@ impl P2PSetup {
}
/// Draw maximum number of inbound peers setup content.
fn max_inbound_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
fn max_inbound_ui(&mut self, ui: &mut egui::Ui) {
ui.label(RichText::new(t!("network_settings.max_inbound_count"))
.size(16.0)
.color(Colors::gray())
@@ -593,20 +608,26 @@ impl P2PSetup {
View::button(ui,
format!("{} {}", ARROW_FAT_LINES_DOWN, &max_inbound),
Colors::white_or_black(false), || {
// Setup values for modal.
self.max_inbound_count = max_inbound;
// Show maximum number of inbound peers setup modal.
Modal::new(MAX_INBOUND_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("network_settings.change_value"))
.show();
cb.show_keyboard();
});
// Setup values for modal.
self.max_inbound_count = max_inbound;
// Show maximum number of inbound peers setup modal.
Modal::new(MAX_INBOUND_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("network_settings.change_value"))
.show();
});
ui.add_space(6.0);
}
/// Draw maximum number of inbound peers [`Modal`] content.
fn max_inbound_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
let on_save = |c: &mut P2PSetup| {
if let Ok(max_inbound) = c.max_inbound_count.parse::<u32>() {
NodeConfig::save_max_inbound_peers(max_inbound);
Modal::close();
}
};
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("network_settings.max_inbound_count"))
@@ -615,8 +636,11 @@ impl P2PSetup {
ui.add_space(8.0);
// Draw maximum number of inbound peers text edit.
let mut text_edit_opts = TextEditOptions::new(Id::from(modal.id)).h_center();
View::text_edit(ui, cb, &mut self.max_inbound_count, &mut text_edit_opts);
let mut edit = TextEdit::new(Id::from(modal.id)).h_center().numeric();
edit.ui(ui, &mut self.max_inbound_count, cb);
if edit.enter_pressed {
on_save(self);
}
// Show error when specified value is not valid or reminder to restart enabled node.
if self.max_inbound_count.parse::<u32>().is_err() {
@@ -635,25 +659,16 @@ impl P2PSetup {
// 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(max_inbound) = self.max_inbound_count.parse::<u32>() {
NodeConfig::save_max_inbound_peers(max_inbound);
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();
Modal::close();
});
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.save"), Colors::white_or_black(false), on_save);
View::button(ui, t!("modal.save"), Colors::white_or_black(false), || {
on_save(self);
});
});
});
ui.add_space(6.0);
@@ -661,7 +676,7 @@ impl P2PSetup {
}
/// Draw maximum number of outbound peers setup content.
fn max_outbound_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
fn max_outbound_ui(&mut self, ui: &mut egui::Ui) {
ui.label(RichText::new(t!("network_settings.max_outbound_count"))
.size(16.0)
.color(Colors::gray())
@@ -671,20 +686,26 @@ impl P2PSetup {
View::button(ui,
format!("{} {}", ARROW_FAT_LINES_UP, &max_outbound),
Colors::white_or_black(false), || {
// Setup values for modal.
self.max_outbound_count = max_outbound;
// Show maximum number of outbound peers setup modal.
Modal::new(MAX_OUTBOUND_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("network_settings.change_value"))
.show();
cb.show_keyboard();
});
// Setup values for modal.
self.max_outbound_count = max_outbound;
// Show maximum number of outbound peers setup modal.
Modal::new(MAX_OUTBOUND_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("network_settings.change_value"))
.show();
});
ui.add_space(6.0);
}
/// Draw maximum number of outbound peers [`Modal`] content.
fn max_outbound_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
let on_save = |c: &mut P2PSetup| {
if let Ok(max_outbound) = c.max_outbound_count.parse::<u32>() {
NodeConfig::save_max_outbound_peers(max_outbound);
Modal::close();
}
};
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("network_settings.max_outbound_count"))
@@ -693,8 +714,11 @@ impl P2PSetup {
ui.add_space(8.0);
// Draw maximum number of outbound peers text edit.
let mut text_edit_opts = TextEditOptions::new(Id::from(modal.id)).h_center();
View::text_edit(ui, cb, &mut self.max_outbound_count, &mut text_edit_opts);
let mut edit = TextEdit::new(Id::from(modal.id)).h_center().numeric();
edit.ui(ui, &mut self.max_outbound_count, cb);
if edit.enter_pressed {
on_save(self);
}
// Show error when specified value is not valid or reminder to restart enabled node.
if self.max_outbound_count.parse::<u32>().is_err() {
@@ -713,25 +737,17 @@ impl P2PSetup {
// 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(max_outbound) = self.max_outbound_count.parse::<u32>() {
NodeConfig::save_max_outbound_peers(max_outbound);
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();
Modal::close();
});
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.save"), Colors::white_or_black(false), on_save);
View::button(ui, t!("modal.save"), Colors::white_or_black(false), || {
on_save(self);
});
});
});
ui.add_space(6.0);
@@ -745,9 +761,9 @@ impl P2PSetup {
format!("{} {}", TRASH, t!("network_settings.reset_peers")),
Colors::red(),
Colors::white_or_black(false), || {
Node::reset_peers(false);
self.peers_reset = true;
});
Node::reset_peers(false);
self.peers_reset = true;
});
ui.add_space(6.0);
ui.label(RichText::new(t!("network_settings.reset_peers_desc"))
.size(16.0)
@@ -767,16 +783,19 @@ fn peer_item_ui(ui: &mut egui::Ui,
rect.set_height(42.0);
// Draw round background.
let mut bg_rect = rect.clone();
bg_rect.min += egui::emath::vec2(6.0, 0.0);
let item_rounding = View::item_rounding(index, len, false);
ui.painter().rect(bg_rect, item_rounding, Colors::white_or_black(false), View::item_stroke());
ui.painter().rect(rect,
item_rounding,
Colors::fill(),
View::item_stroke(),
StrokeKind::Outside);
ui.vertical(|ui| {
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
// Draw delete button for non-default seed peers.
if peer_type != &PeerType::DefaultSeed {
View::item_button(ui, View::item_rounding(index, len, true), TRASH, None, || {
let r = View::item_rounding(index, len, true);
View::item_button(ui, r, TRASH, Some(Colors::inactive_text()), || {
match peer_type {
PeerType::CustomSeed => {
NodeConfig::remove_custom_seed(peer_addr);
@@ -797,7 +816,7 @@ fn peer_item_ui(ui: &mut egui::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.add_space(6.0);
// Draw peer address.
let peer_text = format!("{} {}", GLOBE_SIMPLE, &peer_addr);
ui.label(RichText::new(peer_text)
+109 -114
View File
@@ -17,42 +17,35 @@ use egui::{Id, RichText};
use crate::gui::Colors;
use crate::gui::icons::{BEZIER_CURVE, BOUNDING_BOX, CHART_SCATTER, CIRCLES_THREE, CLOCK_COUNTDOWN, HAND_COINS};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Modal, View};
use crate::gui::views::{Modal, TextEdit, View};
use crate::gui::views::network::settings::NetworkSettings;
use crate::gui::views::types::{ModalContainer, ModalPosition, TextEditOptions};
use crate::gui::views::types::{ContentContainer, ModalPosition};
use crate::node::NodeConfig;
/// Memory pool setup section content.
pub struct PoolSetup {
/// Base fee value that's accepted into the pool.
fee_base_edit: String,
/// Reorg cache retention period value in minutes.
reorg_period_edit: String,
/// Maximum number of transactions allowed in the pool.
pool_size_edit: String,
/// Maximum number of transactions allowed in the stempool.
stempool_size_edit: String,
/// Maximum total weight of transactions to build a block.
max_weight_edit: String,
/// [`Modal`] identifiers allowed at this ui container.
modal_ids: Vec<&'static str>,
}
/// Identifier for base fee value [`Modal`].
pub const FEE_BASE_MODAL: &'static str = "fee_base";
const FEE_BASE_MODAL: &'static str = "fee_base";
/// Identifier for reorg cache retention period value [`Modal`].
pub const REORG_PERIOD_MODAL: &'static str = "reorg_period";
const REORG_PERIOD_MODAL: &'static str = "reorg_period";
/// Identifier for maximum number of transactions in the pool [`Modal`].
pub const POOL_SIZE_MODAL: &'static str = "pool_size";
const POOL_SIZE_MODAL: &'static str = "pool_size";
/// Identifier for maximum number of transactions in the stempool [`Modal`].
pub const STEMPOOL_SIZE_MODAL: &'static str = "stempool_size";
const STEMPOOL_SIZE_MODAL: &'static str = "stempool_size";
/// Identifier for maximum total weight of transactions [`Modal`].
pub const MAX_WEIGHT_MODAL: &'static str = "max_weight";
const MAX_WEIGHT_MODAL: &'static str = "max_weight";
impl Default for PoolSetup {
fn default() -> Self {
@@ -62,20 +55,19 @@ impl Default for PoolSetup {
pool_size_edit: NodeConfig::get_max_pool_size(),
stempool_size_edit: NodeConfig::get_max_stempool_size(),
max_weight_edit: NodeConfig::get_mineable_max_weight(),
modal_ids: vec![
FEE_BASE_MODAL,
REORG_PERIOD_MODAL,
POOL_SIZE_MODAL,
STEMPOOL_SIZE_MODAL,
MAX_WEIGHT_MODAL
]
}
}
}
impl ModalContainer for PoolSetup {
fn modal_ids(&self) -> &Vec<&'static str> {
&self.modal_ids
impl ContentContainer for PoolSetup {
fn modal_ids(&self) -> Vec<&'static str> {
vec![
FEE_BASE_MODAL,
REORG_PERIOD_MODAL,
POOL_SIZE_MODAL,
STEMPOOL_SIZE_MODAL,
MAX_WEIGHT_MODAL
]
}
fn modal_ui(&mut self,
@@ -91,53 +83,50 @@ impl ModalContainer for PoolSetup {
_ => {}
}
}
}
impl PoolSetup {
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);
fn container_ui(&mut self, ui: &mut egui::Ui, _: &dyn PlatformCallbacks) {
View::sub_title(ui, format!("{} {}", CHART_SCATTER, t!("network_settings.tx_pool")));
View::horizontal_line(ui, Colors::stroke());
ui.add_space(6.0);
ui.vertical_centered(|ui| {
// Show base fee setup.
self.fee_base_ui(ui, cb);
self.fee_base_ui(ui);
ui.add_space(6.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(6.0);
// Show reorg cache retention period setup.
self.reorg_period_ui(ui, cb);
self.reorg_period_ui(ui);
ui.add_space(6.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(6.0);
// Show pool size setup.
self.pool_size_ui(ui, cb);
self.pool_size_ui(ui);
ui.add_space(6.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(6.0);
// Show stem pool size setup.
self.stem_size_ui(ui, cb);
self.stem_size_ui(ui);
ui.add_space(6.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(6.0);
// Show max weight of transactions setup.
self.max_weight_ui(ui, cb);
self.max_weight_ui(ui);
});
}
}
impl PoolSetup {
/// Draw fee base setup content.
fn fee_base_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
fn fee_base_ui(&mut self, ui: &mut egui::Ui) {
ui.label(RichText::new(t!("network_settings.pool_fee"))
.size(16.0)
.color(Colors::gray())
@@ -153,13 +142,19 @@ impl PoolSetup {
.position(ModalPosition::CenterTop)
.title(t!("network_settings.change_value"))
.show();
cb.show_keyboard();
});
ui.add_space(6.0);
}
/// Draw fee base [`Modal`] content.
fn fee_base_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
let on_save = |c: &mut PoolSetup| {
if let Ok(fee) = c.fee_base_edit.parse::<u64>() {
NodeConfig::save_base_fee(fee);
Modal::close();
}
};
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("network_settings.pool_fee"))
@@ -168,8 +163,11 @@ impl PoolSetup {
ui.add_space(8.0);
// Draw fee base text edit.
let mut fee_base_edit_opts = TextEditOptions::new(Id::from(modal.id)).h_center();
View::text_edit(ui, cb, &mut self.fee_base_edit, &mut fee_base_edit_opts);
let mut edit = TextEdit::new(Id::from(modal.id)).h_center().numeric();
edit.ui(ui, &mut self.fee_base_edit, cb);
if edit.enter_pressed {
on_save(self);
}
// Show error when specified value is not valid or reminder to restart enabled node.
if self.fee_base_edit.parse::<u64>().is_err() {
@@ -187,24 +185,17 @@ impl PoolSetup {
// 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(fee) = self.fee_base_edit.parse::<u64>() {
NodeConfig::save_base_fee(fee);
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();
Modal::close();
});
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.save"), Colors::white_or_black(false), on_save);
View::button(ui, t!("modal.save"), Colors::white_or_black(false), || {
on_save(self);
});
});
});
ui.add_space(6.0);
@@ -213,7 +204,7 @@ impl PoolSetup {
}
/// Draw reorg cache retention period setup content.
fn reorg_period_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
fn reorg_period_ui(&mut self, ui: &mut egui::Ui) {
ui.label(RichText::new(t!("network_settings.reorg_period"))
.size(16.0)
.color(Colors::gray())
@@ -230,13 +221,19 @@ impl PoolSetup {
.position(ModalPosition::CenterTop)
.title(t!("network_settings.change_value"))
.show();
cb.show_keyboard();
});
ui.add_space(6.0);
}
/// Draw reorg cache retention period [`Modal`] content.
fn reorg_period_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
let on_save = |c: &mut PoolSetup| {
if let Ok(period) = c.reorg_period_edit.parse::<u32>() {
NodeConfig::save_reorg_cache_period(period);
Modal::close();
}
};
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("network_settings.reorg_period"))
@@ -245,8 +242,11 @@ impl PoolSetup {
ui.add_space(8.0);
// Draw reorg period text edit.
let mut reorg_period_edit_opts = TextEditOptions::new(Id::from(modal.id)).h_center();
View::text_edit(ui, cb, &mut self.reorg_period_edit, &mut reorg_period_edit_opts);
let mut edit = TextEdit::new(Id::from(modal.id)).h_center().numeric();
edit.ui(ui, &mut self.reorg_period_edit, cb);
if edit.enter_pressed {
on_save(self);
}
// Show error when specified value is not valid or reminder to restart enabled node.
if self.reorg_period_edit.parse::<u32>().is_err() {
@@ -264,25 +264,17 @@ impl PoolSetup {
// 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(period) = self.reorg_period_edit.parse::<u32>() {
NodeConfig::save_reorg_cache_period(period);
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();
Modal::close();
});
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.save"), Colors::white_or_black(false), on_save);
View::button(ui, t!("modal.save"), Colors::white_or_black(false), || {
on_save(self);
});
});
});
ui.add_space(6.0);
@@ -291,7 +283,7 @@ impl PoolSetup {
}
/// Draw maximum number of transactions in the pool setup content.
fn pool_size_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
fn pool_size_ui(&mut self, ui: &mut egui::Ui) {
ui.label(RichText::new(t!("network_settings.max_tx_pool"))
.size(16.0)
.color(Colors::gray())
@@ -307,13 +299,19 @@ impl PoolSetup {
.position(ModalPosition::CenterTop)
.title(t!("network_settings.change_value"))
.show();
cb.show_keyboard();
});
ui.add_space(6.0);
}
/// Draw maximum number of transactions in the pool [`Modal`] content.
fn pool_size_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
let on_save = |c: &mut PoolSetup| {
if let Ok(size) = c.pool_size_edit.parse::<usize>() {
NodeConfig::save_max_pool_size(size);
Modal::close();
}
};
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("network_settings.max_tx_pool"))
@@ -322,8 +320,11 @@ impl PoolSetup {
ui.add_space(8.0);
// Draw pool size text edit.
let mut pool_size_edit_opts = TextEditOptions::new(Id::from(modal.id)).h_center();
View::text_edit(ui, cb, &mut self.pool_size_edit, &mut pool_size_edit_opts);
let mut edit = TextEdit::new(Id::from(modal.id)).h_center().numeric();
edit.ui(ui, &mut self.pool_size_edit, cb);
if edit.enter_pressed {
on_save(self);
}
// Show error when specified value is not valid or reminder to restart enabled node.
if self.pool_size_edit.parse::<usize>().is_err() {
@@ -341,25 +342,17 @@ impl PoolSetup {
// 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(size) = self.pool_size_edit.parse::<usize>() {
NodeConfig::save_max_pool_size(size);
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();
Modal::close();
});
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.save"), Colors::white_or_black(false), on_save);
View::button(ui, t!("modal.save"), Colors::white_or_black(false), || {
on_save(self);
});
});
});
ui.add_space(6.0);
@@ -368,7 +361,7 @@ impl PoolSetup {
}
/// Draw maximum number of transactions in the stempool setup content.
fn stem_size_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
fn stem_size_ui(&mut self, ui: &mut egui::Ui) {
ui.label(RichText::new(t!("network_settings.max_tx_stempool"))
.size(16.0)
.color(Colors::gray())
@@ -386,13 +379,19 @@ impl PoolSetup {
.position(ModalPosition::CenterTop)
.title(t!("network_settings.change_value"))
.show();
cb.show_keyboard();
});
ui.add_space(6.0);
}
/// Draw maximum number of transactions in the stempool [`Modal`] content.
fn stem_size_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
let on_save = |c: &mut PoolSetup| {
if let Ok(size) = c.stempool_size_edit.parse::<usize>() {
NodeConfig::save_max_stempool_size(size);
Modal::close();
}
};
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("network_settings.max_tx_stempool"))
@@ -401,8 +400,11 @@ impl PoolSetup {
ui.add_space(8.0);
// Draw stempool size text edit.
let mut stem_pool_edit_opts = TextEditOptions::new(Id::from(modal.id)).h_center();
View::text_edit(ui, cb, &mut self.stempool_size_edit, &mut stem_pool_edit_opts);
let mut edit = TextEdit::new(Id::from(modal.id)).h_center().numeric();
edit.ui(ui, &mut self.stempool_size_edit, cb);
if edit.enter_pressed {
on_save(self);
}
// Show error when specified value is not valid or reminder to restart enabled node.
if self.stempool_size_edit.parse::<usize>().is_err() {
@@ -420,25 +422,17 @@ impl PoolSetup {
// 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(size) = self.stempool_size_edit.parse::<usize>() {
NodeConfig::save_max_stempool_size(size);
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();
Modal::close();
});
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.save"), Colors::white_or_black(false), on_save);
View::button(ui, t!("modal.save"), Colors::white_or_black(false), || {
on_save(self);
});
});
});
ui.add_space(6.0);
@@ -447,7 +441,7 @@ impl PoolSetup {
}
/// Draw maximum total weight of transactions setup content.
fn max_weight_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
fn max_weight_ui(&mut self, ui: &mut egui::Ui) {
ui.label(RichText::new(t!("network_settings.max_tx_weight"))
.size(16.0)
.color(Colors::gray())
@@ -465,13 +459,19 @@ impl PoolSetup {
.position(ModalPosition::CenterTop)
.title(t!("network_settings.change_value"))
.show();
cb.show_keyboard();
});
ui.add_space(6.0);
}
/// Draw maximum total weight of transactions [`Modal`] content.
fn max_weight_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
let on_save = |c: &mut PoolSetup| {
if let Ok(weight) = c.max_weight_edit.parse::<u64>() {
NodeConfig::save_mineable_max_weight(weight);
Modal::close();
}
};
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("network_settings.max_tx_weight"))
@@ -480,8 +480,11 @@ impl PoolSetup {
ui.add_space(8.0);
// Draw tx weight text edit.
let mut mac_weight_edit_opts = TextEditOptions::new(Id::from(modal.id)).h_center();
View::text_edit(ui, cb, &mut self.max_weight_edit, &mut mac_weight_edit_opts);
let mut edit = TextEdit::new(Id::from(modal.id)).h_center().numeric();
edit.ui(ui, &mut self.max_weight_edit, cb);
if edit.enter_pressed {
on_save(self);
}
// Show error when specified value is not valid or reminder to restart enabled node.
if self.max_weight_edit.parse::<u64>().is_err() {
@@ -499,25 +502,17 @@ impl PoolSetup {
// 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(weight) = self.max_weight_edit.parse::<u64>() {
NodeConfig::save_mineable_max_weight(weight);
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();
Modal::close();
});
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.save"), Colors::white_or_black(false), on_save);
View::button(ui, t!("modal.save"), Colors::white_or_black(false), || {
on_save(self);
});
});
});
ui.add_space(6.0);
+87 -91
View File
@@ -18,10 +18,10 @@ 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::{Modal, TextEdit, 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::node::{Node, NodeConfig};
use crate::wallet::{WalletConfig, WalletList};
@@ -30,7 +30,7 @@ pub struct StratumSetup {
/// Wallet list to select for mining rewards.
wallets: WalletList,
/// Wallets [`Modal`] content.
wallets_modal: WalletsModal,
wallets_modal: WalletListModal,
/// IP Addresses available at system.
available_ips: Vec<String>,
@@ -51,9 +51,6 @@ pub struct StratumSetup {
/// 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>
}
/// Identifier for wallet selection [`Modal`].
@@ -73,7 +70,7 @@ impl Default for StratumSetup {
// 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)
WalletConfig::read_name_by_id(id)
} else {
None
};
@@ -83,7 +80,7 @@ impl Default for StratumSetup {
Self {
wallets: WalletList::default(),
wallets_modal: WalletsModal::new(wallet_id, None, false),
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,
@@ -91,19 +88,18 @@ impl Default for StratumSetup {
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
]
}
}
}
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,
@@ -112,10 +108,10 @@ impl ModalContainer for StratumSetup {
cb: &dyn PlatformCallbacks) {
match modal.id {
WALLET_SELECTION_MODAL => {
self.wallets_modal.ui(ui, modal, &mut self.wallets, cb, |wallet, _| {
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::name_by_id(id);
self.wallet_name = WalletConfig::read_name_by_id(id);
})
},
STRATUM_PORT_MODAL => self.port_modal(ui, modal, cb),
@@ -124,13 +120,8 @@ impl ModalContainer for StratumSetup {
_ => {}
}
}
}
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);
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);
@@ -192,8 +183,8 @@ impl StratumSetup {
View::button(ui,
t!("network_settings.choose_wallet"),
Colors::white_or_black(false), || {
self.show_wallets_modal();
});
self.show_wallets_modal();
});
ui.add_space(12.0);
if self.wallet_name.is_some() {
@@ -227,25 +218,27 @@ impl StratumSetup {
});
// Show stratum port setup.
self.port_setup_ui(ui, cb);
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, cb);
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, cb);
self.min_diff_ui(ui);
});
}
}
impl StratumSetup {
/// Show wallet selection [`Modal`].
fn show_wallets_modal(&mut self) {
self.wallets_modal = WalletsModal::new(NodeConfig::get_stratum_wallet_id(), None, false);
self.wallets_modal = WalletListModal::new(NodeConfig::get_stratum_wallet_id(), None, false);
// Show modal.
Modal::new(WALLET_SELECTION_MODAL)
.position(ModalPosition::Center)
@@ -254,7 +247,7 @@ impl StratumSetup {
}
/// Draw stratum port value setup content.
fn port_setup_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
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())
@@ -271,7 +264,6 @@ impl StratumSetup {
.position(ModalPosition::CenterTop)
.title(t!("network_settings.change_value"))
.show();
cb.show_keyboard();
});
ui.add_space(12.0);
@@ -287,6 +279,24 @@ impl StratumSetup {
/// 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;
// Save port at config if it's available.
if available {
NodeConfig::save_stratum_address(&stratum_ip, &c.stratum_port_edit);
c.is_port_available = true;
Modal::close();
}
};
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("network_settings.stratum_port"))
@@ -295,8 +305,11 @@ impl StratumSetup {
ui.add_space(8.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);
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);
}
// Show error when specified port is unavailable.
if !self.stratum_port_available_edit {
@@ -315,36 +328,17 @@ impl StratumSetup {
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
// 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;
// Save port at config if it's available.
if available {
NodeConfig::save_stratum_address(&stratum_ip, &self.stratum_port_edit);
self.is_port_available = true;
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();
Modal::close();
});
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.save"), Colors::white_or_black(false), on_save);
View::button(ui, t!("modal.save"), Colors::white_or_black(false), || {
on_save(self);
});
});
});
ui.add_space(6.0);
@@ -353,7 +347,7 @@ impl StratumSetup {
}
/// Draw attempt time value setup content.
fn attempt_time_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
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())
@@ -370,7 +364,6 @@ impl StratumSetup {
.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"))
@@ -382,6 +375,13 @@ impl StratumSetup {
/// 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();
}
};
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("network_settings.attempt_time"))
@@ -390,8 +390,11 @@ impl StratumSetup {
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);
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);
}
// Show error when specified value is not valid or reminder to restart enabled node.
if self.attempt_time_edit.parse::<u32>().is_err() {
@@ -410,25 +413,17 @@ impl StratumSetup {
// 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();
Modal::close();
});
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.save"), Colors::white_or_black(false), on_save);
View::button(ui, t!("modal.save"), Colors::white_or_black(false), || {
on_save(self);
});
});
});
ui.add_space(6.0);
@@ -436,7 +431,7 @@ impl StratumSetup {
}
/// Draw minimum share difficulty value setup content.
fn min_diff_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
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())
@@ -453,13 +448,19 @@ impl StratumSetup {
.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) {
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();
}
};
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("network_settings.min_share_diff"))
@@ -468,8 +469,11 @@ impl StratumSetup {
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);
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);
}
// Show error when specified value is not valid or reminder to restart enabled node.
if self.min_share_diff_edit.parse::<u64>().is_err() {
@@ -488,25 +492,17 @@ impl StratumSetup {
// 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();
Modal::close();
});
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.save"), Colors::white_or_black(false), on_save);
View::button(ui, t!("modal.save"), Colors::white_or_black(false), || {
on_save(self);
});
});
});
ui.add_space(6.0);
+15 -5
View File
@@ -12,12 +12,13 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use serde_derive::{Deserialize, Serialize};
use crate::gui::platform::PlatformCallbacks;
/// Integrated node tab content interface.
pub trait NodeTab {
fn get_type(&self) -> NodeTabType;
fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks);
fn tab_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks);
}
/// Type of [`NodeTab`] content.
@@ -32,10 +33,19 @@ pub enum NodeTabType {
impl NodeTabType {
pub fn title(&self) -> String {
match *self {
NodeTabType::Info => { t!("network.node") }
NodeTabType::Metrics => { t!("network.metrics") }
NodeTabType::Mining => { t!("network.mining") }
NodeTabType::Settings => { t!("network.settings") }
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
}
+7 -1
View File
@@ -113,7 +113,7 @@ pub enum PullToRefreshState {
/// `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.
/// The user dragged far enough to trigger a refresh.
DoRefresh,
/// The refresh is currently happening.
Refreshing,
@@ -298,6 +298,12 @@ impl PullToRefresh {
} else {
state = PullToRefreshState::Idle;
}
} else if let PullToRefreshState::Dragging {
far_enough: enough, ..
} = state.clone() {
if enough {
state = PullToRefreshState::DoRefresh;
}
}
} else {
state = PullToRefreshState::Idle;
+151 -81
View File
@@ -12,26 +12,33 @@
// 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, UiBuilder};
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::Colors;
/// QR code image from text.
pub struct QrCodeContent {
/// QR code text.
text: String,
pub text: String,
/// Flag to show text below QR code.
show_text: bool,
/// Flag to copy text below QR code.
can_copy_text: bool,
/// Maximum QR code size.
max_size: f32,
/// Flag to draw animated QR with Uniform Resources
/// https://github.com/BlockchainCommons/Research/blob/master/papers/bcr-2020-005-ur.md
@@ -53,6 +60,9 @@ impl QrCodeContent {
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,
@@ -61,6 +71,24 @@ impl QrCodeContent {
}
}
/// Setup maximum QR code size.
pub fn with_max_size(mut self, max_size: f32) -> Self {
self.max_size = max_size;
self
}
/// Hide text below QR code.
pub fn hide_text(mut self) -> Self {
self.show_text = false;
self
}
/// Do not show button to copy QR code text.
pub fn no_copy(mut self) -> Self {
self.can_copy_text = false;
self
}
/// Draw QR code.
pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
if self.animated {
@@ -70,6 +98,9 @@ impl QrCodeContent {
// 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);
}
/// Draw animated QR code content.
@@ -110,9 +141,9 @@ impl QrCodeContent {
self.qr_image_ui(svg, ui);
// Show QR code text.
ui.add_space(6.0);
View::ellipsize_text(ui, self.text.clone(), 16.0, Colors::inactive_text());
ui.add_space(6.0);
if self.show_text {
self.text_ui(ui);
}
ui.vertical_centered(|ui| {
let sharing = {
@@ -120,18 +151,20 @@ impl QrCodeContent {
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();
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| {
@@ -141,10 +174,6 @@ impl QrCodeContent {
});
}
ui.add_space(8.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(8.0);
// Check if GIF was created to share.
let has_gif = {
let r_state = self.qr_image_state.read();
@@ -193,69 +222,110 @@ impl QrCodeContent {
self.qr_image_ui(svg, ui);
// Show QR code text.
ui.add_space(6.0);
View::ellipsize_text(ui, self.text.clone(), 16.0, Colors::inactive_text());
ui.add_space(6.0);
if self.show_text {
self.text_ui(ui);
} else {
ui.add_space(8.0);
}
// 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), || {
let text = self.text.as_str();
if let Ok(qr) = QrCode::encode_text(text, 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();
}
}
}
if self.can_copy_text {
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(6.0, 0.0);
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);
});
}
}
}
/// 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);
});
ui.add_space(8.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(8.0);
}
/// 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();
}
}
}
}
/// 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);
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);
// Create background shape.
let mut bg_shape = RectShape::new(
rect,
egui::Rounding::default(),
egui::Color32::WHITE,
egui::Stroke::NONE
);
let bg_idx = ui.painter().add(bg_shape);
// 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());
// Draw QR code image content.
let mut content_rect = ui.allocate_new_ui(UiBuilder::new().max_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;
// 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;
// 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);
// 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);
});
}
/// 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);
}
/// Check if QR code is loading.
@@ -269,7 +339,7 @@ impl QrCodeContent {
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(), 100).unwrap();
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();
+74 -67
View File
@@ -21,15 +21,15 @@ use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{CameraContent, Modal, View};
use crate::gui::views::types::QrScanResult;
/// QR code scan [`Modal`] content.
pub struct CameraScanModal {
/// Camera content for QR scan [`Modal`].
/// QR code scanning content.
pub struct CameraScanContent {
/// Camera content.
camera_content: Option<CameraContent>,
/// QR code scan result
/// Scan result.
qr_scan_result: Option<QrScanResult>,
}
impl Default for CameraScanModal {
impl Default for CameraScanContent {
fn default() -> Self {
Self {
camera_content: Some(CameraContent::default()),
@@ -38,69 +38,20 @@ impl Default for CameraScanModal {
}
}
impl CameraScanModal {
impl CameraScanContent {
/// Draw [`Modal`] content.
pub fn ui(&mut self,
ui: &mut egui::Ui,
modal: &Modal,
cb: &dyn PlatformCallbacks,
mut on_result: impl FnMut(&QrScanResult)) {
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 {
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());
self.qr_scan_result = None;
modal.close();
});
});
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), || {
self.qr_scan_result = None;
self.camera_content = 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.qr_scan_result = None;
self.camera_content = Some(CameraContent::default());
cb.start_camera();
});
});
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() {
@@ -120,11 +71,67 @@ impl CameraScanModal {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
cb.stop_camera();
self.camera_content = None;
modal.close();
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::icons::GLOBE_SIMPLE;
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::settings::{InterfaceSettingsContent, NetworkSettingsContent};
use crate::gui::views::types::ContentContainer;
use crate::gui::views::View;
use crate::gui::Colors;
/// 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);
// }
}
}
+140
View File
@@ -0,0 +1,140 @@
// 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::gui::platform::PlatformCallbacks;
use crate::gui::views::types::ContentContainer;
use crate::gui::views::{Modal, View};
use crate::gui::Colors;
use crate::AppConfig;
/// 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::*;
+291
View File
@@ -0,0 +1,291 @@
// 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::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};
use crate::gui::Colors;
use crate::AppConfig;
/// 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();
}
}
}
+632
View File
@@ -0,0 +1,632 @@
// 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::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::gui::Colors;
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);
});
});
}
}
+8 -19
View File
@@ -16,7 +16,7 @@ use egui::{Margin, Id, Layout, Align, UiBuilder};
use crate::gui::Colors;
use crate::gui::views::{Content, View};
use crate::gui::views::types::{LinePosition, TitleContentType, TitleType};
use crate::gui::views::types::{TitleContentType, TitleType};
/// Title panel with left/right action buttons and text in the middle.
pub struct TitlePanel {
@@ -46,10 +46,10 @@ impl TitlePanel {
.exact_height(Self::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,
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()
})
@@ -72,7 +72,7 @@ impl TitlePanel {
r.max.x -= Self::HEIGHT;
r
};
ui.allocate_new_ui(UiBuilder::new().max_rect(content_rect), |ui| {
ui.scope_builder(UiBuilder::new().max_rect(content_rect), |ui| {
Self::title_text_content(ui, content);
});
}
@@ -84,7 +84,7 @@ impl TitlePanel {
r
};
// Draw first title content.
ui.allocate_new_ui(UiBuilder::new().max_rect(first_rect), |ui| {
ui.scope_builder(UiBuilder::new().max_rect(first_rect), |ui| {
Self::title_text_content(ui, first);
});
@@ -95,23 +95,12 @@ impl TitlePanel {
r
};
// Draw second title content.
ui.allocate_new_ui(UiBuilder::new().max_rect(second_rect), |ui| {
ui.scope_builder(UiBuilder::new().max_rect(second_rect), |ui| {
Self::title_text_content(ui, second);
});
}
}
});
// Draw content divider line.
let r = {
let mut r = rect.clone();
r.min.x -= View::far_left_inset_margin(ui);
r.max.x += View::far_right_inset_margin(ui);
r
};
if Content::is_dual_panel_mode(ui.ctx()) {
View::line(ui, LinePosition::BOTTOM, &r, Colors::stroke());
}
});
}
+18 -93
View File
@@ -51,100 +51,25 @@ pub struct ModalState {
pub modal: Option<Modal>,
}
/// Contains identifiers to draw opened [`Modal`] content for current ui container.
pub trait ModalContainer {
/// 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 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);
});
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);
});
}
}
}
}
/// 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
self.container_ui(ui, cb);
}
}
@@ -152,7 +77,7 @@ impl TextEditOptions {
#[derive(Clone)]
pub enum QrScanResult {
/// Slatepack message.
Slatepack(ZeroingString),
Slatepack(String),
/// Slatepack address.
Address(ZeroingString),
/// Parsed text.
+159 -267
View File
@@ -12,25 +12,21 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use std::sync::atomic::{AtomicI32, Ordering};
use std::sync::Arc;
use parking_lot::RwLock;
use lazy_static::lazy_static;
use egui::{Align, Button, CursorIcon, Layout, lerp, PointerState, Rect, Response, Rgba, RichText, Sense, SizeHint, Spinner, TextBuffer, TextStyle, TextureHandle, TextureOptions, Widget, UiBuilder};
use egui::epaint::{Color32, FontId, PathShape, PathStroke, RectShape, Rounding, Stroke};
use std::sync::atomic::{AtomicI32, Ordering};
use egui::emath::GuiRounding;
use egui::epaint::text::TextWrapping;
use egui::epaint::{Color32, FontId, PathShape, PathStroke, RectShape, Stroke};
use egui::load::SizedTexture;
use egui::os::OperatingSystem;
use egui::text::{LayoutJob, TextFormat};
use egui::text_edit::TextEditState;
use egui::{lerp, Button, CornerRadius, CursorIcon, Rect, Response, Rgba, RichText, Sense, SizeHint, Spinner, StrokeKind, TextureHandle, TextureOptions, UiBuilder, Widget};
use egui_extras::image::load_svg_bytes_with_size;
use crate::AppConfig;
use crate::gui::icons::{CHECK_FAT, CHECK_SQUARE, SQUARE};
use crate::gui::views::types::LinePosition;
use crate::gui::Colors;
use crate::gui::icons::{CHECK_SQUARE, CLIPBOARD_TEXT, COPY, EYE, EYE_SLASH, SCAN, SQUARE};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::types::{LinePosition, TextEditOptions};
use crate::AppConfig;
pub struct View;
@@ -87,17 +83,10 @@ impl View {
/// Get width and height of app window.
pub fn window_size(ctx: &egui::Context) -> (f32, f32) {
let rect = ctx.screen_rect();
let rect = ctx.content_rect();
(rect.width(), rect.height())
}
/// Callback on Enter key press event.
pub fn on_enter_key(ui: &mut egui::Ui, cb: impl FnOnce()) {
if ui.ctx().input(|i| i.key_pressed(egui::Key::Enter)) {
(cb)();
}
}
/// Calculate margin for far left view based on display insets (cutouts).
pub fn far_left_inset_margin(ui: &mut egui::Ui) -> f32 {
if ui.available_rect_before_wrap().min.x == 0.0 {
@@ -120,9 +109,18 @@ impl View {
}
}
/// Content padding for current platform.
pub fn content_padding() -> f32 {
if View::is_desktop() {
4.0
} else {
8.0
}
}
/// Cut long text with character.
fn ellipsize(text: String, size: f32, color: Color32) -> LayoutJob {
let mut job = LayoutJob::single_section(text, TextFormat {
fn ellipsize(text: impl Into<String>, size: f32, color: Color32) -> LayoutJob {
let mut job = LayoutJob::single_section(text.into(), TextFormat {
font_id: FontId::proportional(size), color, ..Default::default()
});
job.wrap = TextWrapping {
@@ -135,12 +133,16 @@ impl View {
}
/// Draw ellipsized text.
pub fn ellipsize_text(ui: &mut egui::Ui, text: String, size: f32, color: Color32) {
pub fn ellipsize_text(ui: &mut egui::Ui, text: impl Into<String>, size: f32, color: Color32) {
ui.label(Self::ellipsize(text, size, color));
}
/// Draw animated ellipsized text.
pub fn animate_text(ui: &mut egui::Ui, text: String, size: f32, color: Color32, animate: bool) {
pub fn animate_text(ui: &mut egui::Ui,
text: impl Into<String>,
size: f32,
color: Color32,
animate: bool) {
// Setup text color animation if needed.
let (dark, bright) = (0.3, 1.0);
let color_factor = if animate {
@@ -160,7 +162,7 @@ impl View {
}
}
/// Draw horizontally centered sub-title with space below.
/// Draw horizontally centered subtitle with space below.
pub fn sub_title(ui: &mut egui::Ui, text: String) {
ui.vertical_centered_justified(|ui| {
ui.label(RichText::new(text.to_uppercase()).size(16.0).color(Colors::text(false)));
@@ -168,19 +170,6 @@ impl View {
ui.add_space(4.0);
}
/// Temporary click optimization for touch screens, return `true` if it was clicked.
fn touched(ui: &mut egui::Ui, resp: Response) -> bool {
let drag_resp = resp.interact(Sense::click_and_drag());
// Clear pointer event if dragging is out of button area
if drag_resp.dragged() && !ui.rect_contains_pointer(drag_resp.rect) {
ui.input_mut(|i| i.pointer = PointerState::default());
}
if drag_resp.drag_stopped() || drag_resp.clicked() || drag_resp.secondary_clicked() {
return true;
}
false
}
/// Draw big size title button.
pub fn title_button_big(ui: &mut egui::Ui, icon: &str, action: impl FnOnce(&mut egui::Ui)) {
Self::title_button(ui, 22.0, icon, action);
@@ -194,23 +183,34 @@ impl View {
/// Draw title button with transparent background color, contains only icon.
fn title_button(ui: &mut egui::Ui, size: f32, icon: &str, action: impl FnOnce(&mut egui::Ui)) {
ui.scope(|ui| {
// Setup padding for title buttons.
if !View::is_desktop() {
ui.style_mut().spacing.button_padding = egui::vec2(20.0, 8.0);
} else {
ui.style_mut().spacing.button_padding = egui::vec2(16.0, 8.0);
}
// Disable strokes.
ui.style_mut().visuals.widgets.inactive.bg_stroke = Stroke::NONE;
ui.style_mut().visuals.widgets.hovered.bg_stroke = Stroke::NONE;
ui.style_mut().visuals.widgets.active.bg_stroke = Stroke::NONE;
ui.style_mut().visuals.widgets.active.rounding = Rounding::default();
ui.style_mut().visuals.widgets.active.corner_radius = CornerRadius::default();
ui.style_mut().visuals.widgets.active.expansion = 0.0;
// Setup text.
let wt = RichText::new(icon.to_string()).size(size).color(Colors::title(true));
// Draw button.
let br = Button::new(wt)
.sense(if View::is_desktop() {
Sense::click()
} else {
Sense::click_and_drag()
})
.fill(Colors::TRANSPARENT)
.ui(ui)
.on_hover_cursor(CursorIcon::PointingHand);
br.surrender_focus();
if Self::touched(ui, br) {
(action)(ui);
if br.clicked() || (!View::is_desktop() && br.drag_stopped()) {
action(ui);
}
});
}
@@ -221,17 +221,31 @@ impl View {
/// Tab button with white background fill color, contains only icon.
pub fn tab_button(ui: &mut egui::Ui,
icon: &str,
active: bool,
color: Option<Color32>,
selected: Option<bool>,
action: impl FnOnce(&mut egui::Ui)) {
ui.scope(|ui| {
let text_color = match active {
true => Colors::title(false),
false => Colors::text(false)
let text_color = if let Some(c) = color {
if selected.is_none() {
Colors::inactive_text()
} else {
c
}
} else {
if let Some(active) = selected {
match active {
true => Colors::gray(),
false => Colors::item_button_text()
}
} else {
Colors::inactive_text()
}
};
let mut button = Button::new(RichText::new(icon).size(22.0).color(text_color));
if !active {
let active_not_selected = selected.is_some() && !selected.unwrap();
if active_not_selected {
// Disable expansion on click/hover.
ui.style_mut().visuals.widgets.hovered.expansion = 0.0;
ui.style_mut().visuals.widgets.active.expansion = 0.0;
@@ -247,10 +261,23 @@ impl View {
button = button.fill(Colors::fill()).stroke(Stroke::NONE);
}
let br = button.ui(ui).on_hover_cursor(CursorIcon::PointingHand);
// Setup paddings for tab buttons.
if Self::is_desktop() {
ui.style_mut().spacing.button_padding = egui::vec2(10.0, 4.0);
} else {
ui.style_mut().spacing.button_padding = egui::vec2(14.0, 8.0);
};
// Setup pointer style.
let br = if active_not_selected {
button.ui(ui).on_hover_cursor(CursorIcon::PointingHand)
} else {
button.ui(ui)
};
br.surrender_focus();
if Self::touched(ui, br) {
(action)(ui);
if br.clicked() && active_not_selected {
action(ui);
}
});
}
@@ -266,10 +293,10 @@ impl View {
}
/// Draw [`Button`] with specified background fill color and default text color.
pub fn button(ui: &mut egui::Ui, text: String, fill: Color32, action: impl FnOnce()) {
let br = Self::button_resp(ui, text, Colors::text_button(), fill);
if Self::touched(ui, br) {
(action)();
pub fn button(ui: &mut egui::Ui, text: impl Into<String>, fill: Color32, cb: impl FnOnce()) {
let br = Self::button_resp(ui, text.into(), Colors::text_button(), fill);
if br.clicked() {
cb();
}
}
@@ -280,8 +307,8 @@ impl View {
fill: Color32,
action: impl FnOnce()) {
let br = Self::button_resp(ui, text, text_color, fill);
if Self::touched(ui, br) {
(action)();
if br.clicked() {
action();
}
}
@@ -292,36 +319,36 @@ impl View {
fill: Color32,
action: impl FnOnce(&mut egui::Ui)) {
let br = Self::button_resp(ui, text, text_color, fill);
if Self::touched(ui, br) {
(action)(ui);
if br.clicked() {
action(ui);
}
}
/// Draw gold action [`Button`].
pub fn action_button(ui: &mut egui::Ui,
text: String, action: impl FnOnce()) {
Self::colored_text_button(ui, text, Colors::title(true), Colors::gold(), action);
text: impl Into<String>, action: impl FnOnce()) {
Self::colored_text_button(ui, text.into(), Colors::title(true), Colors::gold(), action);
}
/// Draw [`Button`] with specified background fill color and ui at callback.
pub fn button_ui(ui: &mut egui::Ui,
text: String,
text: impl Into<String>,
fill: Color32,
action: impl FnOnce(&mut egui::Ui)) {
let button_text = Self::ellipsize(text.to_uppercase(), 17.0, Colors::text_button());
let button_text = Self::ellipsize(text.into().to_uppercase(), 17.0, Colors::text_button());
let br = Button::new(button_text)
.stroke(Self::default_stroke())
.fill(fill)
.ui(ui)
.on_hover_cursor(CursorIcon::PointingHand);
if Self::touched(ui, br) {
(action)(ui);
if br.clicked() {
action(ui);
}
}
/// Draw list item [`Button`] with provided rounding.
pub fn item_button(ui: &mut egui::Ui,
rounding: Rounding,
rounding: CornerRadius,
text: &'static str,
color: Option<Color32>,
action: impl FnOnce()) {
@@ -332,7 +359,12 @@ impl View {
ui.scope(|ui| {
// Setup padding for item buttons.
ui.style_mut().spacing.button_padding = egui::vec2(14.0, 0.0);
let padding = if Self::is_desktop() {
15.0
} else {
18.0
};
ui.style_mut().spacing.button_padding = egui::vec2(padding, 0.0);
// Disable expansion on click/hover.
ui.style_mut().visuals.widgets.hovered.expansion = 0.0;
ui.style_mut().visuals.widgets.active.expansion = 0.0;
@@ -346,17 +378,17 @@ impl View {
ui.visuals_mut().widgets.active.bg_stroke = Stroke::NONE;
// Setup button text color.
let text_color = if let Some(c) = color { c } else { Colors::item_button() };
let text_color = if let Some(c) = color { c } else { Colors::item_button_text() };
// Show button.
let br = Button::new(RichText::new(text).size(20.0).color(text_color))
.rounding(rounding)
.corner_radius(rounding)
.min_size(button_size)
.ui(ui)
.on_hover_cursor(CursorIcon::PointingHand);
br.surrender_focus();
if Self::touched(ui, br.clone()) {
(action)();
if br.clicked() {
action();
}
// Draw stroke.
@@ -370,143 +402,8 @@ impl View {
});
}
/// Default height of [`egui::TextEdit`] view.
const TEXT_EDIT_HEIGHT: f32 = 37.0;
/// Draw [`egui::TextEdit`] widget.
pub fn text_edit(ui: &mut egui::Ui,
cb: &dyn PlatformCallbacks,
value: &mut String,
options: &mut TextEditOptions) {
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::Center), |ui| {
// Setup password button.
let mut show_pass = false;
if options.password {
// Set password button state value.
let show_pass_id = egui::Id::new(options.id).with("_show_pass");
show_pass = ui.data(|data| {
data.get_temp(show_pass_id)
}).unwrap_or(true);
// Draw button to show/hide current password.
let eye_icon = if show_pass { EYE } else { EYE_SLASH };
let mut changed = false;
View::button(ui, eye_icon.to_string(), Colors::white_or_black(false), || {
show_pass = !show_pass;
changed = true;
});
// Save state if changed.
if changed {
ui.data_mut(|data| {
data.insert_temp(show_pass_id, show_pass);
});
}
ui.add_space(8.0);
}
// Setup copy button.
if options.copy {
let copy_icon = COPY.to_string();
View::button(ui, copy_icon, Colors::white_or_black(false), || {
cb.copy_string_to_buffer(value.clone());
});
ui.add_space(8.0);
}
// Setup paste button.
if options.paste {
let paste_icon = CLIPBOARD_TEXT.to_string();
View::button(ui, paste_icon, Colors::white_or_black(false), || {
*value = cb.get_string_from_buffer();
});
ui.add_space(8.0);
}
// Setup scan QR code button.
if options.scan_qr {
let scan_icon = SCAN.to_string();
View::button(ui, scan_icon, Colors::white_or_black(false), || {
cb.start_camera();
options.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::Center), |ui| {
// Setup text edit size.
let mut edit_rect = ui.available_rect_before_wrap();
edit_rect.set_height(Self::TEXT_EDIT_HEIGHT);
// Show text edit.
let text_edit_resp = egui::TextEdit::singleline(value)
.id(options.id)
.margin(egui::Vec2::new(2.0, 0.0))
.font(TextStyle::Heading)
.min_size(edit_rect.size())
.horizontal_align(if options.h_center { Align::Center } else { Align::Min })
.vertical_align(Align::Center)
.password(show_pass)
.cursor_at_end(true)
.ui(ui);
// Show keyboard on click.
if text_edit_resp.clicked() {
text_edit_resp.request_focus();
cb.show_keyboard();
}
// Setup focus on input field.
if options.focus {
text_edit_resp.request_focus();
cb.show_keyboard();
}
// Apply text from input on Android as temporary fix for egui.
if text_edit_resp.has_focus() {
Self::on_soft_input(ui, options.id, value);
}
});
});
}
/// Apply soft keyboard input data to provided String.
pub fn on_soft_input(ui: &mut egui::Ui, id: egui::Id, value: &mut String) {
let os = OperatingSystem::from_target_os();
if os == OperatingSystem::Android {
let mut w_input = LAST_SOFT_KEYBOARD_INPUT.write();
if !w_input.is_empty() {
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;
value.insert_text(w_input.as_str(), index);
index = index + 1;
if index == 0 {
r.primary.index = value.len();
r.secondary.index = r.primary.index;
} else {
r.primary.index = index;
r.secondary.index = r.primary.index;
}
state.cursor.set_char_range(Some(r));
TextEditState::store(state, ui.ctx(), id);
}
}
}
*w_input = "".to_string();
ui.ctx().request_repaint();
}
}
/// Calculate item background/button rounding based on item index.
pub fn item_rounding(index: usize, len: usize, is_button: bool) -> Rounding {
pub fn item_rounding(index: usize, len: usize, is_button: bool) -> CornerRadius {
let corners = if is_button {
if len == 1 {
[false, true, true, false]
@@ -528,32 +425,44 @@ impl View {
[false, false, false, false]
}
};
Rounding {
nw: if corners[0] { 8.0 } else { 0.0 },
ne: if corners[1] { 8.0 } else { 0.0 },
sw: if corners[3] { 8.0 } else { 0.0 },
se: if corners[2] { 8.0 } else { 0.0 },
CornerRadius {
nw: if corners[0] { 8.0 as u8 } else { 0.0 as u8 },
ne: if corners[1] { 8.0 as u8 } else { 0.0 as u8 },
sw: if corners[3] { 8.0 as u8 } else { 0.0 as u8 },
se: if corners[2] { 8.0 as u8 } else { 0.0 as u8 },
}
}
/// Draw selected item check.
pub fn selected_item_check(ui: &mut egui::Ui) {
let padding = if View::is_desktop() {
14.0
} else {
18.0
};
ui.add_space(padding);
ui.label(RichText::new(CHECK_FAT).size(20.0).color(Colors::green()));
ui.add_space(padding);
}
/// Draw rounded box with some value and label in the middle,
/// where is r = (top_left, top_right, bottom_left, bottom_right).
/// | VALUE |
/// | label |
pub fn label_box(ui: &mut egui::Ui, text: String, label: String, r: [bool; 4]) {
pub fn label_box(ui: &mut egui::Ui, v: impl Into<String>, l: impl Into<String>, r: [bool; 4]) {
let rect = ui.available_rect_before_wrap();
// Create background shape.
let mut bg_shape = RectShape::new(rect, Rounding {
nw: if r[0] { 8.0 } else { 0.0 },
ne: if r[1] { 8.0 } else { 0.0 },
sw: if r[2] { 8.0 } else { 0.0 },
se: if r[3] { 8.0 } else { 0.0 },
}, Colors::fill_lite(), Self::item_stroke());
let bg_idx = ui.painter().add(bg_shape);
let mut bg_shape = RectShape::new(rect, CornerRadius {
nw: if r[0] { 8.0 as u8 } else { 0.0 as u8 },
ne: if r[1] { 8.0 as u8 } else { 0.0 as u8 },
sw: if r[2] { 8.0 as u8 } else { 0.0 as u8 },
se: if r[3] { 8.0 as u8 } else { 0.0 as u8 },
}, Colors::fill(), Self::item_stroke(), StrokeKind::Outside);
let bg_idx = ui.painter().add(bg_shape.clone());
// Draw box content.
let content_resp = ui.allocate_new_ui(UiBuilder::new().max_rect(rect), |ui| {
let content_resp = ui.scope_builder(UiBuilder::new().max_rect(rect), |ui| {
ui.vertical_centered_justified(|ui| {
ui.add_space(4.0);
ui.scope(|ui| {
@@ -561,7 +470,7 @@ impl View {
ui.style_mut().spacing.item_spacing.y = -3.0;
// Draw box value.
let mut job = LayoutJob::single_section(text, TextFormat {
let mut job = LayoutJob::single_section(v.into(), TextFormat {
font_id: FontId::proportional(17.0),
color: Colors::white_or_black(true),
..Default::default()
@@ -575,7 +484,7 @@ impl View {
ui.label(job);
// Draw box label.
ui.label(RichText::new(label).color(Colors::gray()).size(15.0));
ui.label(RichText::new(l).color(Colors::gray()).size(15.0));
});
ui.add_space(2.0);
});
@@ -593,30 +502,38 @@ impl View {
let side_margin = 28.0;
rect.min += egui::emath::vec2(side_margin, ui.available_height() / 2.0 - height / 2.0);
rect.max -= egui::emath::vec2(side_margin, 0.0);
ui.allocate_new_ui(UiBuilder::new().max_rect(rect), |ui| {
(content)(ui);
ui.scope_builder(UiBuilder::new().max_rect(rect), |ui| {
content(ui);
});
});
}
/// Draw loading spinner.
pub fn loading_spinner(ui: &mut egui::Ui, size: f32) {
Spinner::new().size(size).color(Colors::gold()).ui(ui);
}
/// Size of big loading spinner.
pub const BIG_SPINNER_SIZE: f32 = 104.0;
/// Draw big gold loading spinner.
pub fn big_loading_spinner(ui: &mut egui::Ui) {
Spinner::new().size(Self::BIG_SPINNER_SIZE).color(Colors::gold()).ui(ui);
View::loading_spinner(ui, View::BIG_SPINNER_SIZE);
}
/// Size of big loading spinner.
pub const SMALL_SPINNER_SIZE: f32 = 32.0;
/// Draw small gold loading spinner.
pub fn small_loading_spinner(ui: &mut egui::Ui) {
Spinner::new().size(38.0).color(Colors::gold()).ui(ui);
View::loading_spinner(ui, View::SMALL_SPINNER_SIZE);
}
/// Draw the button that looks like checkbox with callback on check.
pub fn checkbox(ui: &mut egui::Ui, checked: bool, text: String, callback: impl FnOnce()) {
pub fn checkbox(ui: &mut egui::Ui, checked: bool, text: impl Into<String>, cb: impl FnOnce()) {
let (text_value, color) = match checked {
true => (format!("{} {}", CHECK_SQUARE, text), Colors::text_button()),
false => (format!("{} {}", SQUARE, text), Colors::checkbox())
true => (format!("{} {}", CHECK_SQUARE, text.into()), Colors::text_button()),
false => (format!("{} {}", SQUARE, text.into()), Colors::checkbox())
};
let br = Button::new(RichText::new(text_value).size(17.0).color(color))
@@ -625,21 +542,24 @@ impl View {
.fill(Colors::TRANSPARENT)
.ui(ui)
.on_hover_cursor(CursorIcon::PointingHand);
if Self::touched(ui, br) {
(callback)();
if br.clicked() {
cb();
}
}
/// Show a [`RadioButton`]. It is selected if `*current_value == selected_value`.
/// If clicked, `selected_value` is assigned to `*current_value`.
pub fn radio_value<T: PartialEq>(ui: &mut egui::Ui, current: &mut T, value: T, text: String) {
pub fn radio_value<T: PartialEq>(ui: &mut egui::Ui,
current: &mut T,
value: T,
text: impl Into<String>) {
ui.scope(|ui| {
// Setup background color.
ui.visuals_mut().widgets.inactive.bg_fill = Colors::fill_deep();
// Draw radio button.
let mut response = ui.radio(*current == value, text)
let mut response = ui.radio(*current == value, text.into())
.on_hover_cursor(CursorIcon::PointingHand);
if Self::touched(ui, response.clone()) && *current != value {
if response.clicked() && *current != value {
*current = value;
response.mark_changed();
}
@@ -652,7 +572,7 @@ impl View {
let (line_rect, _) = ui.allocate_exact_size(line_size, Sense::hover());
let painter = ui.painter();
painter.hline(line_rect.x_range(),
painter.round_to_pixel(line_rect.center().y),
line_rect.center().y.round_to_pixels(painter.pixels_per_point()),
Stroke { width: 1.0, color });
}
@@ -701,8 +621,8 @@ impl View {
pub fn svg_image(ui: &mut egui::Ui,
name: &str,
svg: &[u8],
size: Option<SizeHint>) -> TextureHandle {
let color_img = load_svg_bytes_with_size(svg, size).unwrap();
size: SizeHint) -> TextureHandle {
let color_img = load_svg_bytes_with_size(svg, size, &usvg::Options::default()).unwrap();
// Create image texture.
let texture_handle = ui.ctx().load_texture(name,
color_img.clone(),
@@ -751,7 +671,9 @@ impl View {
if resp.clicked() || resp.dragged() {
on_click();
}
let shape = RectShape::filled(resp.rect, Rounding::ZERO, Colors::semi_transparent());
let shape = RectShape::filled(resp.rect,
CornerRadius::ZERO,
Colors::semi_transparent().gamma_multiply(0.7));
ui.painter().add(shape);
}
@@ -787,7 +709,7 @@ lazy_static! {
#[allow(dead_code)]
#[cfg(target_os = "android")]
#[allow(non_snake_case)]
#[no_mangle]
#[unsafe(no_mangle)]
/// Callback from Java code to update display insets (cutouts).
pub extern "C" fn Java_mw_gri_android_MainActivity_onDisplayInsets(
_env: jni::JNIEnv,
@@ -806,34 +728,4 @@ pub extern "C" fn Java_mw_gri_android_MainActivity_onDisplayInsets(
RIGHT_DISPLAY_INSET.store(array[1], Ordering::Relaxed);
BOTTOM_DISPLAY_INSET.store(array[2], Ordering::Relaxed);
LEFT_DISPLAY_INSET.store(array[3], Ordering::Relaxed);
}
lazy_static! {
static ref LAST_SOFT_KEYBOARD_INPUT: Arc<RwLock<String>> = Arc::new(RwLock::new("".into()));
}
#[allow(dead_code)]
#[cfg(target_os = "android")]
#[allow(non_snake_case)]
#[no_mangle]
/// Callback from Java code with last entered character from soft keyboard.
pub extern "C" fn Java_mw_gri_android_MainActivity_onInput(
_env: jni::JNIEnv,
_class: jni::objects::JObject,
char: jni::sys::jstring
) {
use jni::objects::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_input = LAST_SOFT_KEYBOARD_INPUT.write();
*w_input = w_input.clone().add(str);
}
Err(_) => {}
}
}
}
File diff suppressed because it is too large Load Diff
@@ -19,8 +19,8 @@ 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::{Modal, Content, View, CameraScanModal};
use crate::gui::views::types::{LinePosition, ModalContainer, ModalPosition, QrScanResult};
use crate::gui::views::{Modal, Content, View, CameraScanContent};
use crate::gui::views::types::{LinePosition, ContentContainer, ModalPosition, QrScanResult};
use crate::gui::views::wallets::creation::MnemonicSetup;
use crate::gui::views::wallets::creation::types::Step;
use crate::gui::views::wallets::ConnectionSettings;
@@ -29,7 +29,7 @@ use crate::wallet::{ExternalConnection, Wallet};
use crate::wallet::types::PhraseMode;
/// Wallet creation content.
pub struct WalletCreation {
pub struct WalletCreationContent {
/// Wallet name.
pub name: String,
/// Wallet password.
@@ -39,7 +39,7 @@ pub struct WalletCreation {
step: Step,
/// QR code scanning [`Modal`] content.
scan_modal_content: Option<CameraScanModal>,
scan_modal_content: Option<CameraScanContent>,
/// Mnemonic phrase setup content.
mnemonic_setup: MnemonicSetup,
@@ -48,16 +48,15 @@ pub struct WalletCreation {
/// Flag to check if an error occurred during wallet creation.
creation_error: Option<String>,
/// [`Modal`] identifiers allowed at this ui container.
modal_ids: Vec<&'static str>
}
const QR_CODE_PHRASE_SCAN_MODAL: &'static str = "qr_code_rec_phrase_scan_modal";
impl ModalContainer for WalletCreation {
fn modal_ids(&self) -> &Vec<&'static str> {
&self.modal_ids
impl ContentContainer for WalletCreationContent {
fn modal_ids(&self) -> Vec<&'static str> {
vec![
QR_CODE_PHRASE_SCAN_MODAL
]
}
fn modal_ui(&mut self,
@@ -67,15 +66,15 @@ impl ModalContainer for WalletCreation {
match modal.id {
QR_CODE_PHRASE_SCAN_MODAL => {
if let Some(content) = self.scan_modal_content.as_mut() {
content.ui(ui, modal, cb, |result| {
content.modal_ui(ui, cb, |result| {
match result {
QrScanResult::Text(text) => {
self.mnemonic_setup.mnemonic.import(&text);
modal.close();
Modal::close();
}
QrScanResult::SeedQR(text) => {
self.mnemonic_setup.mnemonic.import(&text);
modal.close();
Modal::close();
}
_ => {}
}
@@ -86,10 +85,13 @@ impl ModalContainer for WalletCreation {
_ => {}
}
}
fn container_ui(&mut self, _: &mut egui::Ui, _: &dyn PlatformCallbacks) {
}
}
impl WalletCreation {
/// Create new wallet creation instance from name and password.
impl WalletCreationContent {
/// Create new wallet creation content from name and password.
pub fn new(name: String, pass: ZeroingString) -> Self {
Self {
name,
@@ -99,28 +101,24 @@ impl WalletCreation {
mnemonic_setup: MnemonicSetup::default(),
network_setup: ConnectionSettings::default(),
creation_error: None,
modal_ids: vec![
QR_CODE_PHRASE_SCAN_MODAL
],
}
}
/// Draw wallet creation content.
pub fn ui(&mut self,
ui: &mut egui::Ui,
cb: &dyn PlatformCallbacks,
on_create: impl FnMut(Wallet)) {
self.current_modal_ui(ui, cb);
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,
right: View::get_right_inset() + View::TAB_ITEMS_PADDING,
top: View::TAB_ITEMS_PADDING,
bottom: View::get_bottom_inset() + View::TAB_ITEMS_PADDING,
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_deep(),
fill: Colors::fill(),
..Default::default()
})
.show_inside(ui, |ui| {
@@ -143,11 +141,12 @@ impl WalletCreation {
egui::CentralPanel::default()
.frame(egui::Frame {
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,
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| {
@@ -214,7 +213,7 @@ impl WalletCreation {
.color(Colors::red()));
ui.add_space(10.0);
} else {
ui.label(RichText::new(&t!("wallets.not_valid_phrase"))
ui.label(RichText::new(t!("wallets.not_valid_phrase"))
.size(16.0)
.color(Colors::red()));
ui.add_space(4.0);
@@ -233,9 +232,7 @@ impl WalletCreation {
columns[0].vertical_centered_justified(|ui| {
match self.mnemonic_setup.mnemonic.mode() {
PhraseMode::Generate => {
let c_t = format!("{} {}",
COPY,
t!("copy").to_uppercase());
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
@@ -243,9 +240,7 @@ impl WalletCreation {
});
}
PhraseMode::Import => {
let p_t = format!("{} {}",
CLIPBOARD_TEXT,
t!("paste").to_uppercase());
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);
@@ -258,11 +253,9 @@ impl WalletCreation {
if next {
self.next_step_button_ui(ui, on_create);
} else {
let scan_text = format!("{} {}",
SCAN,
t!("scan").to_uppercase());
let scan_text = format!("{} {}", SCAN, t!("scan"));
View::button(ui, scan_text, Colors::white_or_black(false), || {
self.scan_modal_content = Some(CameraScanModal::default());
self.scan_modal_content = Some(CameraScanContent::default());
// Show QR code scan modal.
Modal::new(QR_CODE_PHRASE_SCAN_MODAL)
.position(ModalPosition::CenterTop)
@@ -280,7 +273,7 @@ impl WalletCreation {
if next {
self.next_step_button_ui(ui, on_create);
} else {
let paste_text = format!("{} {}", CLIPBOARD_TEXT, t!("paste").to_uppercase());
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);
@@ -304,7 +297,7 @@ impl WalletCreation {
let (next_text, text_color, bg_color) = if self.step == Step::SetupConnection {
(format!("{} {}", CHECK, t!("complete")), Colors::title(true), Colors::gold())
} else {
(t!("continue"), Colors::green(), Colors::white_or_black(false))
(t!("continue").into(), Colors::green(), Colors::white_or_black(false))
};
// Show next step button.
@@ -350,14 +343,14 @@ impl WalletCreation {
/// 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.ui(ui, cb),
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.create_ui(ui, cb)
self.network_setup.ui(ui, cb);
}
}
}
+68 -84
View File
@@ -17,8 +17,8 @@ use egui::{Id, RichText};
use crate::gui::Colors;
use crate::gui::icons::PENCIL;
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Modal, Content, View};
use crate::gui::views::types::{ModalContainer, ModalPosition, TextEditOptions};
use crate::gui::views::{Modal, Content, View, TextEdit};
use crate::gui::views::types::{ContentContainer, ModalPosition};
use crate::wallet::Mnemonic;
use crate::wallet::types::{PhraseMode, PhraseSize, PhraseWord};
@@ -33,9 +33,6 @@ pub struct MnemonicSetup {
word_edit: String,
/// Flag to check if entered word is valid at [`Modal`].
valid_word_edit: bool,
/// [`Modal`] identifiers allowed at this ui container.
modal_ids: Vec<&'static str>
}
/// Identifier for word input [`Modal`].
@@ -48,16 +45,15 @@ impl Default for MnemonicSetup {
word_index_edit: 0,
word_edit: String::from(""),
valid_word_edit: true,
modal_ids: vec![
WORD_INPUT_MODAL
]
}
}
}
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,
@@ -69,43 +65,17 @@ impl ModalContainer for MnemonicSetup {
_ => {}
}
}
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);
// Show mode and type setup.
self.mode_type_ui(ui);
ui.add_space(12.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(6.0);
// Show words setup.
self.word_list_ui(ui, self.mnemonic.mode() == PhraseMode::Import, cb);
}
/// 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);
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);
}
/// Draw mode and size setup.
fn mode_type_ui(&mut self, ui: &mut egui::Ui) {
// Show mode setup.
let mut mode = self.mnemonic.mode();
ui.columns(2, |columns| {
@@ -146,10 +116,29 @@ impl MnemonicSetup {
if size != self.mnemonic.size() {
self.mnemonic.set_size(size);
}
ui.add_space(12.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(6.0);
// 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);
}
/// Draw grid of words for mnemonic phrase.
fn word_list_ui(&mut self, ui: &mut egui::Ui, edit: bool, cb: &dyn PlatformCallbacks) {
fn word_list_ui(&mut self, ui: &mut egui::Ui, edit: bool) {
ui.add_space(6.0);
ui.scope(|ui| {
// Setup spacing between columns.
@@ -167,25 +156,25 @@ impl MnemonicSetup {
ui.columns(cols, |columns| {
columns[0].horizontal(|ui| {
let word = chunk.get(0).unwrap();
self.word_item_ui(ui, word_number, word, edit, cb);
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, cb);
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, cb);
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, cb);
self.word_item_ui(ui, word_number, word, edit);
});
}
});
@@ -193,7 +182,7 @@ impl MnemonicSetup {
ui.columns(cols, |columns| {
columns[0].horizontal(|ui| {
let word = chunk.get(0).unwrap();
self.word_item_ui(ui, word_number, word, edit, cb);
self.word_item_ui(ui, word_number, word, edit);
});
});
}
@@ -207,8 +196,7 @@ impl MnemonicSetup {
ui: &mut egui::Ui,
num: usize,
word: &PhraseWord,
edit: bool,
cb: &dyn PlatformCallbacks) {
edit: bool) {
let color = if !word.valid || (word.text.is_empty() && !self.mnemonic.valid()) {
Colors::red()
} else {
@@ -225,7 +213,6 @@ impl MnemonicSetup {
.position(ModalPosition::CenterTop)
.title(t!("wallets.recovery_phrase"))
.show();
cb.show_keyboard();
});
ui.label(RichText::new(format!("#{} {}", num, word.text))
.size(17.0)
@@ -244,6 +231,26 @@ impl MnemonicSetup {
/// 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("");
}
};
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("wallets.enter_word", "number" => self.word_index_edit + 1))
@@ -252,10 +259,11 @@ impl MnemonicSetup {
ui.add_space(8.0);
// Draw word value text edit.
let mut text_edit_opts = TextEditOptions::new(
Id::from(modal.id).with(self.word_index_edit)
);
View::text_edit(ui, cb, &mut self.word_edit, &mut text_edit_opts);
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);
}
// Show error when specified word is not valid.
if !self.valid_word_edit {
@@ -276,38 +284,14 @@ impl MnemonicSetup {
columns[0].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
// Close modal.
cb.hide_keyboard();
modal.close();
Modal::close();
});
});
columns[1].vertical_centered_justified(|ui| {
// Callback to save the word.
let mut save = || {
// Insert word checking validity.
let word = &self.word_edit.trim().to_string();
self.valid_word_edit = self.mnemonic.insert(self.word_index_edit, word);
if !self.valid_word_edit {
return;
}
// Close modal or go to next word to edit.
let next_word = self.mnemonic.get(self.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 {
cb.hide_keyboard();
modal.close();
} else {
self.word_index_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);
View::button(ui, t!("continue"), Colors::white_or_black(false), || {
on_save(self);
});
});
});
ui.add_space(6.0);
+2 -2
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;
+29 -37
View File
@@ -17,13 +17,10 @@ use grin_util::ZeroingString;
use crate::gui::Colors;
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Modal, View};
use crate::gui::views::types::TextEditOptions;
use crate::gui::views::{Modal, TextEdit, View};
/// Initial wallet creation [`Modal`] content.
pub struct AddWalletModal {
/// Flag to check if it's first draw to focus on first field.
first_draw: bool,
/// Wallet name.
pub name_edit: String,
/// Password to encrypt created wallet.
@@ -33,8 +30,7 @@ pub struct AddWalletModal {
impl Default for AddWalletModal {
fn default() -> Self {
Self {
first_draw: true,
name_edit: t!("wallets.default_wallet"),
name_edit: t!("wallets.default_wallet").into(),
pass_edit: "".to_string(),
}
}
@@ -47,33 +43,44 @@ impl AddWalletModal {
modal: &Modal,
cb: &dyn PlatformCallbacks,
mut on_input: impl FnMut(String, ZeroingString)) {
ui.add_space(6.0);
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_edit_opts = TextEditOptions::new(Id::from(modal.id).with("name"))
.no_focus();
if self.first_draw {
self.first_draw = false;
name_edit_opts.focus = true;
}
View::text_edit(ui, cb, &mut self.name_edit, &mut name_edit_opts);
ui.add_space(8.0);
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);
// Draw wallet password text edit.
let mut pass_text_edit_opts = TextEditOptions::new(Id::from(modal.id).with("pass"))
// Show wallet password text edit.
let mut pass_input = TextEdit::new(Id::from(modal.id).with("pass"))
.password()
.no_focus();
View::text_edit(ui, cb, &mut self.pass_edit, &mut pass_text_edit_opts);
.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);
});
@@ -86,28 +93,13 @@ impl AddWalletModal {
columns[0].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
// Close modal.
cb.hide_keyboard();
modal.close();
Modal::close();
});
});
columns[1].vertical_centered_justified(|ui| {
let mut on_next = || {
let name = self.name_edit.clone();
let pass = self.pass_edit.clone();
if name.is_empty() || pass.is_empty() {
return;
}
cb.hide_keyboard();
modal.close();
on_input(name, ZeroingString::from(pass));
};
// 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(self);
});
View::button(ui, t!("continue"), Colors::white_or_black(false), on_next);
});
});
ui.add_space(6.0);
+137
View File
@@ -0,0 +1,137 @@
// 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::views::{Modal, View};
use crate::gui::Colors;
use crate::gui::icons::{BRACKETS_CURLY, GITHUB_LOGO, TELEGRAM_LOGO};
/// 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);
}
}
@@ -13,20 +13,17 @@
// limitations under the License.
use egui::scroll_area::ScrollBarVisibility;
use egui::{Align, Layout, RichText, ScrollArea};
use egui::{Align, Layout, RichText, ScrollArea, StrokeKind};
use crate::gui::Colors;
use crate::gui::icons::{CHECK, CHECK_FAT, COMPUTER_TOWER, FOLDER_OPEN, GLOBE_SIMPLE, PLUGS_CONNECTED};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Modal, View};
use crate::gui::views::types::ModalPosition;
use crate::gui::views::wallets::modals::OpenWalletModal;
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::wallet::{Wallet, WalletList};
use crate::gui::views::{Modal, View};
use crate::gui::Colors;
use crate::wallet::types::ConnectionMethod;
use crate::wallet::{Wallet, WalletList};
/// Wallet list [`Modal`] content
pub struct WalletsModal {
pub struct WalletListModal {
/// Selected wallet id.
selected_id: Option<i64>,
@@ -35,32 +32,19 @@ pub struct WalletsModal {
/// Flag to check if wallet can be opened from the list.
can_open: bool,
/// Wallet opening content.
open_wallet_content: Option<OpenWalletModal>,
}
impl WalletsModal {
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, open_wallet_content: None }
Self { selected_id, data, can_open }
}
/// Draw content.
pub fn ui(&mut self,
ui: &mut egui::Ui,
modal: &Modal,
wallets: &WalletList,
cb: &dyn PlatformCallbacks,
mut on_select: impl FnMut(Wallet, Option<String>)) {
// Draw wallet opening modal content.
if let Some(open_content) = self.open_wallet_content.as_mut() {
open_content.ui(ui, modal, cb, |wallet, data| {
on_select(wallet, data);
self.data = None;
});
return;
}
ui.add_space(4.0);
ScrollArea::vertical()
.max_height(373.0)
@@ -74,7 +58,7 @@ impl WalletsModal {
for wallet in wallets.list() {
// Draw wallet list item.
self.wallet_item_ui(ui, wallet, || {
modal.close();
Modal::close();
on_select(wallet.clone(), data.clone());
});
ui.add_space(5.0);
@@ -90,7 +74,7 @@ impl WalletsModal {
ui.vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
self.data = None;
modal.close();
Modal::close();
});
});
ui.add_space(6.0);
@@ -108,7 +92,11 @@ impl WalletsModal {
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());
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 {
@@ -119,21 +107,12 @@ impl WalletsModal {
FOLDER_OPEN
};
View::item_button(ui, View::item_rounding(0, 1, true), icon, None, || {
if wallet.is_open() {
on_select();
} else {
Modal::change_position(ModalPosition::CenterTop);
self.open_wallet_content = Some(
OpenWalletModal::new(wallet.clone(), self.data.clone())
);
}
on_select();
});
} else {
// Draw button to select wallet.
let current = self.selected_id.unwrap_or(0) == id;
if current {
ui.add_space(12.0);
ui.label(RichText::new(CHECK_FAT).size(20.0).color(Colors::green()));
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();
+8 -5
View File
@@ -12,14 +12,17 @@
// See the License for the specific language governing permissions and
// limitations under the License.
mod conn;
pub use conn::*;
mod settings;
pub use settings::*;
mod wallets;
pub use wallets::*;
mod list;
pub use list::*;
mod open;
pub use open::*;
mod add;
pub use add::*;
pub use add::*;
mod changelog;
pub use changelog::*;
+25 -40
View File
@@ -17,32 +17,22 @@ use grin_util::ZeroingString;
use crate::gui::Colors;
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Modal, View};
use crate::gui::views::types::TextEditOptions;
use crate::wallet::Wallet;
use crate::gui::views::{Modal, TextEdit, View};
/// Wallet opening [`Modal`] content.
pub struct OpenWalletModal {
/// Wallet to open.
wallet: Wallet,
/// Password to open wallet.
pass_edit: String,
/// Flag to check if wrong password was entered.
wrong_pass: bool,
/// Optional data to pass after wallet opening.
data: Option<String>,
}
impl OpenWalletModal {
/// Create new content instance.
pub fn new(wallet: Wallet, data: Option<String>) -> Self {
pub fn new() -> Self {
Self {
wallet,
pass_edit: "".to_string(),
wrong_pass: false,
data,
}
}
/// Draw [`Modal`] content.
@@ -50,17 +40,33 @@ impl OpenWalletModal {
ui: &mut egui::Ui,
modal: &Modal,
cb: &dyn PlatformCallbacks,
mut on_continue: impl FnMut(Wallet, Option<String>)) {
ui.add_space(6.0);
mut on_continue: impl FnMut(ZeroingString) -> bool) {
// Callback for button to continue.
let mut on_continue = |m: &mut OpenWalletModal| {
let pass = m.pass_edit.clone();
if pass.is_empty() {
return;
}
m.wrong_pass = !on_continue(ZeroingString::from(pass));
if !m.wrong_pass {
m.pass_edit = "".to_string();
Modal::close();
}
};
ui.vertical_centered(|ui| {
ui.add_space(6.0);
ui.label(RichText::new(t!("wallets.pass"))
.size(17.0)
.color(Colors::gray()));
ui.add_space(8.0);
// Show password input.
let mut pass_edit_opts = TextEditOptions::new(Id::from(modal.id)).password();
View::text_edit(ui, cb, &mut self.pass_edit, &mut pass_edit_opts);
let mut pass_edit = TextEdit::new(Id::from(modal.id).with("pass_edit")).password();
pass_edit.ui(ui, &mut self.pass_edit, cb);
if pass_edit.enter_pressed {
(on_continue)(self);
}
// Show information when password is empty.
if self.pass_edit.is_empty() {
@@ -87,34 +93,13 @@ impl OpenWalletModal {
columns[0].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
// Close modal.
cb.hide_keyboard();
modal.close();
Modal::close();
});
});
columns[1].vertical_centered_justified(|ui| {
// Callback for button to continue.
let mut on_continue = || {
let pass = self.pass_edit.clone();
if pass.is_empty() {
return;
}
match self.wallet.open(ZeroingString::from(pass)) {
Ok(_) => {
self.pass_edit = "".to_string();
cb.hide_keyboard();
modal.close();
on_continue(self.wallet.clone(), self.data.clone());
}
Err(_) => self.wrong_pass = true
}
};
// Continue on Enter key press.
View::on_enter_key(ui, || {
(on_continue)();
View::button(ui, t!("continue"), Colors::white_or_black(false), || {
(on_continue)(self);
});
View::button(ui, t!("continue"), Colors::white_or_black(false), on_continue);
});
});
ui.add_space(6.0);
@@ -12,33 +12,35 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use egui::{RichText, ScrollArea};
use egui::scroll_area::ScrollBarVisibility;
use egui::{RichText, ScrollArea};
use crate::gui::Colors;
use crate::gui::icons::{CHECK, CHECK_FAT, PLUS_CIRCLE};
use crate::gui::icons::{PLUS_CIRCLE, TRASH};
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;
use crate::gui::views::network::ConnectionsContent;
use crate::gui::views::types::ModalPosition;
use crate::gui::views::wallets::WalletsContent;
use crate::gui::views::{Modal, View};
use crate::gui::Colors;
use crate::wallet::types::ConnectionMethod;
use crate::wallet::{ConnectionsConfig, ExternalConnection};
/// Wallet connection selection [`Modal`] content.
pub struct WalletConnectionModal {
pub struct WalletSettingsModal {
/// Current connection method.
pub conn: ConnectionMethod,
/// External connection content.
ext_conn_content: Option<ExternalConnectionModal>
/// External connection creation content.
new_ext_conn_content: Option<ExternalConnectionModal>
}
impl WalletConnectionModal {
impl WalletSettingsModal {
/// Create from provided wallet connection.
pub fn new(conn: ConnectionMethod) -> Self {
Self {
conn,
ext_conn_content: None,
new_ext_conn_content: None,
}
}
@@ -48,14 +50,19 @@ impl WalletConnectionModal {
modal: &Modal,
cb: &dyn PlatformCallbacks,
on_select: impl Fn(ConnectionMethod)) {
// Draw external connection content.
if let Some(ext_content) = self.ext_conn_content.as_mut() {
// Draw external connection creation content.
if let Some(ext_content) = self.new_ext_conn_content.as_mut() {
ext_content.ui(ui, cb, modal, |conn| {
on_select(ConnectionMethod::External(conn.id, conn.url));
});
return;
}
// Check connections state on first draw.
if Modal::first_draw() {
ExternalConnection::check(None, ui.ctx());
}
ui.add_space(4.0);
let ext_conn_list = ConnectionsConfig::ext_conn_list();
@@ -72,20 +79,20 @@ impl WalletConnectionModal {
ui.add_space(2.0);
// Show integrated node selection.
ConnectionsContent::integrated_node_item_ui(ui, |ui| {
match self.conn {
ConnectionMethod::Integrated => {
ui.add_space(14.0);
ui.label(RichText::new(CHECK_FAT).size(20.0).color(Colors::green()));
ui.add_space(14.0);
}
_ => {
View::item_button(ui, View::item_rounding(0, 1, true), CHECK, None, || {
on_select(ConnectionMethod::Integrated);
modal.close();
});
}
let cur_integrated = self.conn == ConnectionMethod::Integrated;
let bg = if cur_integrated {
Colors::fill()
} else {
Colors::fill_lite()
};
ConnectionsContent::integrated_node_item_ui(ui, bg, (!cur_integrated, || {
on_select(ConnectionMethod::Integrated);
Modal::close();
}), |ui| {
if cur_integrated {
View::selected_item_check(ui);
}
cur_integrated
});
ui.add_space(8.0);
@@ -97,53 +104,64 @@ impl WalletConnectionModal {
// 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.ext_conn_content = Some(ExternalConnectionModal::new(None));
self.new_ext_conn_content = Some(ExternalConnectionModal::new(None));
});
});
ui.add_space(4.0);
if !ext_conn_list.is_empty() {
ui.add_space(8.0);
for (index, conn) in ext_conn_list.iter().filter(|c| !c.deleted).enumerate() {
if conn.deleted {
continue;
}
ui.horizontal_wrapped(|ui| {
let len = ext_conn_list.len();
ConnectionsContent::ext_conn_item_ui(ui, conn, index, len, |ui| {
let current_ext_conn = match self.conn {
ConnectionMethod::Integrated => false,
ConnectionMethod::External(id, _) => id == conn.id
};
if !current_ext_conn {
let button_rounding = View::item_rounding(index, len, true);
View::item_button(ui, button_rounding, CHECK, None, || {
on_select(
ConnectionMethod::External(conn.id, conn.url.clone())
);
modal.close();
});
} else {
ui.add_space(12.0);
ui.label(RichText::new(CHECK_FAT)
.size(20.0)
.color(Colors::green()));
}
});
});
}
}
for (i, c) in ext_conn_list.iter().enumerate() {
ui.horizontal_wrapped(|ui| {
let len = ext_conn_list.len();
let is_current = match self.conn {
ConnectionMethod::External(id, _) => id == c.id,
_ => false
};
let bg = if is_current {
Colors::fill()
} else {
Colors::fill_lite()
};
ConnectionsContent::ext_conn_item_ui(ui, bg, c, i, len, (!is_current, || {
on_select(
ConnectionMethod::External(c.id, c.url.clone())
);
Modal::close();
}), |ui| {
if is_current {
View::selected_item_check(ui);
}
});
});
}
ui.add_space(4.0);
});
ui.add_space(2.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(6.0);
ui.vertical_centered(|ui| {
// Draw button to delete the wallet.
View::colored_text_button(ui,
format!("{} {}", TRASH, t!("wallets.delete")),
Colors::red(),
Colors::white_or_black(false), || {
Modal::new(WalletsContent::DELETE_CONFIRMATION_MODAL)
.position(ModalPosition::Center)
.title(t!("confirmation"))
.show();
});
});
ui.add_space(6.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), || {
modal.close();
Modal::close();
});
});
ui.add_space(6.0);
@@ -0,0 +1,413 @@
// 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, Layout, RichText, ScrollArea, StrokeKind};
use egui::scroll_area::ScrollBarVisibility;
use grin_core::core::amount_to_hr_string;
use crate::gui::icons::{CHECK, FOLDER_USER, PACKAGE, PATH, SCAN, SPINNER, USERS_THREE, USER_PLUS};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::types::{ModalPosition, QrScanResult};
use crate::gui::views::wallets::wallet::account::create::CreateAccountContent;
use crate::gui::views::wallets::wallet::types::{WalletContentContainer, GRIN};
use crate::gui::views::{CameraContent, CameraScanContent, Content, Modal, View};
use crate::gui::Colors;
use crate::gui::views::wallets::wallet::request::SendRequestContent;
use crate::wallet::{Wallet, WalletConfig};
use crate::wallet::types::{WalletAccount, WalletTask};
/// Wallet account panel content.
pub struct WalletAccountContent {
/// Flag to show account list content.
pub show_list: bool,
/// Account creation [`Modal`] content.
create_account_content: CreateAccountContent,
/// QR code scan content.
qr_scan_content: Option<CameraContent>,
/// QR code scan result
qr_scan_result: Option<QrScanResult>,
/// Send request creation [`Modal`] content.
send_content: Option<SendRequestContent>,
}
/// Account creation [`Modal`] identifier.
const CREATE_MODAL_ID: &'static str = "create_account_modal";
/// Identifier for sending request creation [`Modal`].
const SEND_MODAL_ID: &'static str = "account_send_request_modal";
impl WalletContentContainer for WalletAccountContent {
fn modal_ids(&self) -> Vec<&'static str> {
vec![
CREATE_MODAL_ID,
SEND_MODAL_ID
]
}
fn modal_ui(&mut self,
ui: &mut egui::Ui,
wallet: &Wallet,
modal: &Modal,
cb: &dyn PlatformCallbacks) {
match modal.id {
CREATE_MODAL_ID => self.create_account_content.ui(ui, wallet, modal, cb),
SEND_MODAL_ID => {
if let Some(c) = self.send_content.as_mut() {
c.modal_ui(ui, wallet, modal, cb);
}
}
_ => {}
}
}
fn container_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) {
if self.qr_scan_showing() {
self.qr_scan_ui(ui, wallet, cb);
} else {
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
if self.show_list {
self.list_ui(ui, wallet);
} else {
// Show account content.
self.account_ui(ui, wallet, cb);
}
});
}
}
}
impl Default for WalletAccountContent {
fn default() -> Self {
Self {
show_list: false,
create_account_content: CreateAccountContent::default(),
qr_scan_content: None,
qr_scan_result: None,
send_content: None,
}
}
}
const ACCOUNT_ITEM_HEIGHT: f32 = 75.0;
impl WalletAccountContent {
/// Check if QR code scanner was opened.
pub fn qr_scan_showing(&self) -> bool {
self.qr_scan_content.is_some() || self.qr_scan_result.is_some()
}
/// Close QR code scanner.
pub fn close_qr_scan(&mut self, cb: &dyn PlatformCallbacks) {
if !self.qr_scan_showing() {
return;
}
cb.stop_camera();
self.qr_scan_content = None;
self.qr_scan_result = None;
}
/// Check if it's possible to go back at navigation stack.
pub fn can_back(&self) -> bool {
self.qr_scan_showing() || self.show_list
}
/// Navigate back on navigation stack.
pub fn back(&mut self, cb: &dyn PlatformCallbacks) {
if self.qr_scan_showing() {
self.close_qr_scan(cb);
} else if self.show_list {
self.show_list = false;
}
}
/// Draw wallet account content.
fn account_ui(&mut self,
ui: &mut egui::Ui,
wallet: &Wallet,
cb: &dyn PlatformCallbacks) {
// Check wallet data.
if wallet.get_data().is_none() {
return;
}
let data = wallet.get_data().unwrap();
let mut rect = ui.available_rect_before_wrap();
rect.set_height(75.0);
// Draw round background.
let rounding = View::item_rounding(0, 2, false);
ui.painter().rect(rect,
rounding,
Colors::fill(),
View::item_stroke(),
StrokeKind::Outside);
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
// Draw button to show QR code scanner.
View::item_button(ui, View::item_rounding(0, 2, true), SCAN, None, || {
self.qr_scan_content = Some(CameraContent::default());
cb.start_camera();
});
// Draw button to show list of accounts.
let accounts = wallet.accounts();
let accounts_icon = if accounts.len() > 1 {
USERS_THREE
} else {
USER_PLUS
};
let rounding = View::item_rounding(1, 3, true);
View::item_button(ui, rounding, accounts_icon, None, || {
if accounts.len() == 1 {
self.create_account_content = CreateAccountContent::default();
Modal::new(CREATE_MODAL_ID)
.position(ModalPosition::CenterTop)
.title(t!("wallets.accounts"))
.show();
} else {
self.show_list = true;
}
});
let layout_size = ui.available_size();
ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| {
ui.add_space(8.0);
ui.vertical(|ui| {
ui.add_space(3.0);
// Show spendable amount.
let amount = amount_to_hr_string(data.info.amount_currently_spendable, true);
let amount_text = format!("{} {}", amount, GRIN);
ui.with_layout(Layout::left_to_right(Align::Min), |ui| {
ui.add_space(1.0);
ui.label(RichText::new(amount_text)
.size(18.0)
.color(Colors::white_or_black(true)));
});
ui.add_space(-2.0);
// Show account label.
let account = wallet.get_config().account;
let default_acc_label = WalletConfig::DEFAULT_ACCOUNT_LABEL.to_string();
let acc_label = if account == default_acc_label {
t!("wallets.default_account").into()
} else {
account.to_owned()
};
let acc_text = format!("{} {}", FOLDER_USER, acc_label);
View::ellipsize_text(ui, acc_text, 15.0, Colors::text(false));
// Show confirmed height or sync progress.
let status_text = if wallet.message_opening() {
format!("{} {}", SPINNER, t!("wallets.loading"))
} else if !wallet.syncing() {
format!("{} {}", PACKAGE, data.info.last_confirmed_height)
} else {
let info_progress = wallet.info_sync_progress();
if info_progress == 100 || info_progress == 0 {
format!("{} {}", SPINNER, t!("wallets.wallet_loading"))
} else {
if wallet.is_repairing() {
let rep_progress = wallet.repairing_progress();
if rep_progress == 0 {
format!("{} {}", SPINNER, t!("wallets.wallet_checking"))
} else {
format!("{} {}: {}%",
SPINNER,
t!("wallets.wallet_checking"),
rep_progress)
}
} else {
format!("{} {}: {}%",
SPINNER,
t!("wallets.wallet_loading"),
info_progress)
}
}
};
let animate = wallet.syncing() || wallet.message_opening();
View::animate_text(ui, status_text, 15.0, Colors::gray(), animate);
})
});
});
}
/// Draw account list content.
fn list_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet) {
let accounts = wallet.accounts();
let size = accounts.len();
ScrollArea::vertical()
.id_salt("account_list_scroll")
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.max_height(411.0)
.auto_shrink([true; 2])
.show_rows(ui, ACCOUNT_ITEM_HEIGHT, size, |ui, row_range| {
for index in row_range {
let acc = accounts.get(index).unwrap().clone();
let current = wallet.get_config().account == acc.label;
account_item_ui(ui, &acc, current, index, size, || {
let _ = wallet.set_active_account(&acc.label);
self.show_list = false;
});
if index == size - 1 {
ui.add_space(4.0);
}
}
});
ui.add_space(2.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);
// Show modal buttons.
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
self.show_list = false;
});
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.add"), Colors::white_or_black(false), || {
self.show_list = false;
self.create_account_content = CreateAccountContent::default();
Modal::new(CREATE_MODAL_ID)
.position(ModalPosition::CenterTop)
.title(t!("wallets.accounts"))
.show();
});
});
});
ui.add_space(6.0);
}
/// Draw QR code scanner content.
fn qr_scan_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) {
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH, |ui| {
if self.qr_scan_content.is_some() {
if let Some(result) = self.qr_scan_content.as_ref().unwrap().qr_scan_result() {
cb.stop_camera();
self.qr_scan_content = None;
match result {
QrScanResult::Address(a) => {
if let Some(data) = wallet.get_data() {
if data.info.amount_currently_spendable > 0 {
let address = Some(a.to_string());
self.send_content = Some(SendRequestContent::new(address));
Modal::new(SEND_MODAL_ID)
.position(ModalPosition::CenterTop)
.title(t!("wallets.send"))
.show();
}
}
}
QrScanResult::Slatepack(m) => {
wallet.task(WalletTask::OpenMessage(m));
}
_ => {
self.qr_scan_result = Some(result);
}
}
} else {
// Draw QR code scan content.
self.qr_scan_content.as_mut().unwrap().ui(ui, cb);
ui.add_space(6.0);
ui.vertical_centered_justified(|ui| {
View::button(ui, t!("close"), Colors::white_or_black(false), || {
self.close_qr_scan(cb);
});
});
}
} else if let Some(res) = &self.qr_scan_result.clone() {
CameraScanContent::result_ui(ui, res, cb, || {
self.qr_scan_result = None;
}, || {
self.qr_scan_content = Some(CameraContent::default());
cb.start_camera();
});
}
ui.add_space(6.0);
});
}
}
/// Draw account item.
fn account_item_ui(ui: &mut egui::Ui,
acc: &WalletAccount,
current: bool,
index: usize,
size: usize,
mut on_select: impl FnMut()) {
// Setup layout size.
let mut rect = ui.available_rect_before_wrap();
rect.set_height(ACCOUNT_ITEM_HEIGHT);
// Draw round background.
let bg_rect = rect.clone();
let item_rounding = View::item_rounding(index, size, false);
ui.painter().rect(bg_rect,
item_rounding,
Colors::fill(),
View::item_stroke(),
StrokeKind::Outside);
ui.vertical(|ui| {
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
// Draw button to select account.
if current {
View::selected_item_check(ui);
} else {
let button_rounding = View::item_rounding(index, size, true);
View::item_button(ui, button_rounding, 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(8.0);
ui.vertical(|ui| {
ui.add_space(3.0);
// Show spendable amount.
let amount = amount_to_hr_string(acc.spendable_amount, true);
let amount_text = format!("{} {}", amount, GRIN);
ui.with_layout(Layout::left_to_right(Align::Min), |ui| {
ui.add_space(1.0);
ui.label(RichText::new(amount_text)
.size(18.0)
.color(Colors::white_or_black(true)));
});
ui.add_space(-2.0);
// Show account name.
let default_acc_label = WalletConfig::DEFAULT_ACCOUNT_LABEL.to_string();
let acc_label = if acc.label == default_acc_label {
t!("wallets.default_account").into()
} else {
acc.label.to_owned()
};
let acc_name = format!("{} {}", FOLDER_USER, acc_label);
View::ellipsize_text(ui, acc_name, 15.0, Colors::text(false));
// Show account BIP32 derivation path.
let acc_path = format!("{} {}", PATH, acc.path);
ui.label(RichText::new(acc_path).size(15.0).color(Colors::gray()));
ui.add_space(3.0);
});
});
});
});
}
@@ -0,0 +1,103 @@
// 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::{Id, RichText};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Modal, TextEdit, View};
use crate::gui::Colors;
use crate::wallet::Wallet;
/// Account creation [`Modal`] content.
pub struct CreateAccountContent {
/// Account label value.
account_label_edit: String,
/// Flag to check if error occurred during account creation.
account_creation_error: bool,
}
impl Default for CreateAccountContent {
fn default() -> Self {
Self {
account_label_edit: "".to_string(),
account_creation_error: false,
}
}
}
impl CreateAccountContent {
/// Draw account creation [`Modal`] content.
pub fn ui(&mut self,
ui: &mut egui::Ui,
wallet: &Wallet,
modal: &Modal,
cb: &dyn PlatformCallbacks) {
let on_create = |m: &mut CreateAccountContent| {
if m.account_label_edit.is_empty() {
return;
}
let label = &m.account_label_edit;
match wallet.create_account(label) {
Ok(_) => {
let _ = wallet.set_active_account(label);
Modal::close();
},
Err(_) => m.account_creation_error = true
};
};
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("wallets.new_account_desc"))
.size(17.0)
.color(Colors::gray()));
ui.add_space(8.0);
// Draw account name edit.
let mut name_edit = TextEdit::new(Id::from(modal.id).with(wallet.get_config().id));
name_edit.ui(ui, &mut self.account_label_edit, cb);
if name_edit.enter_pressed {
on_create(self);
}
// Show error occurred during account creation.
if self.account_creation_error {
ui.add_space(12.0);
ui.label(RichText::new(t!("error"))
.size(17.0)
.color(Colors::red()));
}
ui.add_space(12.0);
});
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
// Show modal buttons.
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!("create"), Colors::white_or_black(false), || {
on_create(self);
});
});
});
ui.add_space(6.0);
}
}
@@ -1,4 +1,4 @@
// Copyright 2024 The Grim Developers
// 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.
@@ -15,4 +15,4 @@
mod content;
pub use content::*;
mod request;
mod create;
+417 -292
View File
@@ -12,234 +12,289 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use std::time::Duration;
use egui::{Align, Id, Layout, Margin, RichText, ScrollArea};
use egui::scroll_area::ScrollBarVisibility;
use egui::{Id, Margin, RichText, ScrollArea};
use grin_chain::SyncStatus;
use grin_core::core::amount_to_hr_string;
use crate::AppConfig;
use crate::gui::Colors;
use crate::gui::icons::{ARROWS_CLOCKWISE, BRIDGE, CAMERA_ROTATE, CHAT_CIRCLE_TEXT, FOLDER_USER, GEAR_FINE, GRAPH, PACKAGE, POWER, SCAN, SPINNER, USERS_THREE};
use crate::gui::icons::{ARROWS_CLOCKWISE, FILE_ARROW_DOWN, FILE_ARROW_UP, FILE_TEXT, GEAR_FINE, POWER, STACK};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Modal, Content, View, CameraContent};
use crate::gui::views::types::{LinePosition, ModalContainer, ModalPosition};
use crate::gui::views::wallets::{WalletTransactions, WalletMessages, WalletTransport};
use crate::gui::views::wallets::types::{GRIN, WalletTab, WalletTabType};
use crate::gui::views::wallets::wallet::modals::WalletAccountsModal;
use crate::gui::views::wallets::wallet::WalletSettings;
use crate::gui::views::types::{LinePosition, ModalPosition};
use crate::gui::views::wallets::wallet::account::WalletAccountContent;
use crate::gui::views::wallets::wallet::message::MessageInputContent;
use crate::gui::views::wallets::wallet::request::{InvoiceRequestContent, SendRequestContent};
use crate::gui::views::wallets::wallet::transport::WalletTransportContent;
use crate::gui::views::wallets::wallet::types::WalletContentContainer;
use crate::gui::views::wallets::wallet::{WalletSettingsContent, WalletTransactionsContent};
use crate::gui::views::{Content, Modal, View};
use crate::gui::Colors;
use crate::node::Node;
use crate::wallet::{ExternalConnection, Wallet, WalletConfig};
use crate::wallet::types::{ConnectionMethod, WalletData};
use crate::wallet::types::{ConnectionMethod, WalletTask};
use crate::wallet::{ExternalConnection, Wallet};
use crate::AppConfig;
use crate::gui::views::wallets::wallet::proof::PaymentProofContent;
/// Wallet content.
pub struct WalletContent {
/// Selected and opened wallet.
pub wallet: Wallet,
/// Current tab content to show.
pub current_tab: Box<dyn WalletTab>,
/// Transactions content.
pub txs_content: Option<WalletTransactionsContent>,
/// Wallet accounts [`Modal`] content.
accounts_modal_content: Option<WalletAccountsModal>,
/// Settings content.
pub settings_content: Option<WalletSettingsContent>,
/// QR code scan content.
pub qr_scan_content: Option<CameraContent>,
/// Account panel content.
pub account_content: WalletAccountContent,
/// Transport panel content.
pub transport_content: WalletTransportContent,
/// List of allowed [`Modal`] ids for this [`ModalContainer`].
allowed_modal_ids: Vec<&'static str>
/// Invoice request creation [`Modal`] content.
invoice_content: Option<InvoiceRequestContent>,
/// Send request creation [`Modal`] content.
send_content: Option<SendRequestContent>,
/// Slatepack message input [`Modal`] content.
message_content: Option<MessageInputContent>
}
/// Identifier for account list [`Modal`].
const ACCOUNT_LIST_MODAL: &'static str = "account_list_modal";
/// Identifier for invoice creation [`Modal`].
const INVOICE_MODAL_ID: &'static str = "invoice_request_modal";
/// Identifier for sending request creation [`Modal`].
const SEND_MODAL_ID: &'static str = "send_request_modal";
/// Identifier for Slatepack message input [`Modal`].
pub const MESSAGE_MODAL_ID: &'static str = "input_message_modal";
impl ModalContainer for WalletContent {
fn modal_ids(&self) -> &Vec<&'static str> {
&self.allowed_modal_ids
impl WalletContentContainer for WalletContent {
fn modal_ids(&self) -> Vec<&'static str> {
vec![
INVOICE_MODAL_ID,
SEND_MODAL_ID,
MESSAGE_MODAL_ID
]
}
fn modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
match modal.id {
ACCOUNT_LIST_MODAL => {
if let Some(content) = self.accounts_modal_content.as_mut() {
Modal::ui(ui.ctx(), |ui, modal| {
content.ui(ui, &self.wallet, modal, cb);
});
fn modal_ui(&mut self, ui: &mut egui::Ui, w: &Wallet, m: &Modal, cb: &dyn PlatformCallbacks) {
match m.id {
INVOICE_MODAL_ID => {
if let Some(c) = self.invoice_content.as_mut() {
c.modal_ui(ui, w, m, cb);
}
}
SEND_MODAL_ID => {
if let Some(c) = self.send_content.as_mut() {
c.modal_ui(ui, w, m, cb);
}
}
MESSAGE_MODAL_ID => {
if let Some(c) = self.message_content.as_mut() {
c.ui(ui, w, m, cb);
}
}
_ => {}
}
}
}
impl WalletContent {
/// Create new instance with optional data.
pub fn new(wallet: Wallet, data: Option<String>) -> Self {
let mut content = Self {
wallet,
accounts_modal_content: None,
qr_scan_content: None,
current_tab: Box::new(WalletTransactions::default()),
allowed_modal_ids: vec![
ACCOUNT_LIST_MODAL,
],
};
if data.is_some() {
content.on_data(data);
}
content
}
/// Handle data from deeplink or opened file.
pub fn on_data(&mut self, data: Option<String>) {
self.current_tab = Box::new(WalletMessages::new(data));
}
/// Draw wallet content.
pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
ui.ctx().request_repaint_after(Duration::from_millis(1000));
self.current_modal_ui(ui, cb);
fn container_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) {
let dual_panel = Content::is_dual_panel_mode(ui.ctx());
let show_wallets_dual = AppConfig::show_wallets_at_dual_panel();
let wallet = &self.wallet;
let wallet_id = wallet.identifier();
let data = wallet.get_data();
let show_qr_scan = self.qr_scan_content.is_some();
let hide_tabs = Self::block_navigation_on_sync(wallet);
let block_nav = self.block_navigation_on_sync(wallet);
// Show wallet account panel not on settings tab when navigation is not blocked and QR code
// scanner is not showing and wallet data is not empty.
let mut show_account = self.current_tab.get_type() != WalletTabType::Settings && !hide_tabs
let mut show_account = self.settings_content.is_none() && !block_nav
&& !wallet.sync_error() && data.is_some();
if wallet.get_current_connection() == ConnectionMethod::Integrated && !Node::is_running() {
if wallet.get_current_connection() == ConnectionMethod::Integrated &&
!Node::is_running() {
show_account = false;
}
// Close scanner when balance got hidden.
if !show_account && show_qr_scan {
cb.stop_camera();
self.qr_scan_content = None;
}
egui::TopBottomPanel::top(Id::from("wallet_account").with(wallet.identifier()))
// Show wallet tabs.
let side_padding = View::TAB_ITEMS_PADDING + if View::is_desktop() {
0.0
} else {
4.0
};
let tabs_margin = Margin {
left: (View::far_left_inset_margin(ui) + side_padding) as i8,
right: (View::get_right_inset() + 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("wallet_tabs")
.frame(egui::Frame {
inner_margin: Margin {
left: View::far_left_inset_margin(ui) + 4.0,
right: View::get_right_inset() + 4.0,
top: 4.0,
bottom: 0.0,
},
inner_margin: tabs_margin,
fill: Colors::fill(),
..Default::default()
})
.show_animated_inside(ui, show_account, |ui| {
let rect = ui.available_rect_before_wrap();
if show_qr_scan {
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH, |ui| {
self.qr_scan_content.as_mut().unwrap().ui(ui, cb);
ui.add_space(6.0);
ui.vertical_centered_justified(|ui| {
View::button(ui, t!("close"), Colors::white_or_black(false), || {
cb.stop_camera();
self.qr_scan_content = None;
});
});
ui.add_space(6.0);
});
} else {
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
self.account_ui(ui, data.unwrap(), cb);
});
}
// Draw content divider lines.
let r = {
let mut r = rect.clone();
r.min.x -= 4.0 + View::far_left_inset_margin(ui);
r.min.y -= 4.0;
r.max.x += 4.0 + View::get_right_inset();
.show_animated_inside(ui, !block_nav, |ui| {
let r = ui.available_rect_before_wrap();
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
self.tabs_ui(wallet, ui);
});
let rect = {
let mut r = r.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.max.y += tabs_margin.bottom as f32;
r
};
View::line(ui, LinePosition::BOTTOM, &r, Colors::item_stroke());
if dual_panel && show_wallets_dual && !show_qr_scan {
View::line(ui, LinePosition::LEFT, &r, Colors::item_stroke());
// Draw cover for content below opened panel.
if self.can_back() && show_account {
View::content_cover_ui(ui, rect, "wallet_tabs_content_cover", || {
self.back(cb);
});
} else {
// Draw content divider line.
View::line(ui, LinePosition::TOP, &rect, Colors::stroke());
}
});
// Show wallet tabs.
let show_tabs = !hide_tabs && self.qr_scan_content.is_none();
egui::TopBottomPanel::bottom("wallet_tabs")
.frame(egui::Frame {
inner_margin: Margin {
left: View::far_left_inset_margin(ui) + View::TAB_ITEMS_PADDING,
right: View::get_right_inset() + View::TAB_ITEMS_PADDING,
top: View::TAB_ITEMS_PADDING,
bottom: View::get_bottom_inset() + View::TAB_ITEMS_PADDING,
},
fill: Colors::fill(),
..Default::default()
})
.show_animated_inside(ui, show_tabs, |ui| {
let rect = ui.available_rect_before_wrap();
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
self.tabs_ui(ui, cb);
// Close scanner or account list when account panel got hidden.
if !show_account {
if self.account_content.qr_scan_showing() {
self.account_content.close_qr_scan(cb);
} else {
self.account_content.show_list = false;
}
}
// Flag to check if account panel is opened.
let top_panel_expanded = self.account_content.can_back() ||
self.transport_content.can_back();
// Show wallet account content.
if !self.transport_content.can_back() && show_account {
egui::TopBottomPanel::top(Id::from("wallet_account").with(wallet.identifier()))
.frame(egui::Frame {
inner_margin: Margin {
left: (View::far_left_inset_margin(ui) + View::content_padding()) as i8,
right: (View::get_right_inset() + View::content_padding()) as i8,
top: View::content_padding() as i8,
bottom: 0.0 as i8,
},
fill: if top_panel_expanded {
Colors::fill_lite()
} else {
Colors::TRANSPARENT
},
..Default::default()
})
.show_inside(ui, |ui| {
let rect = ui.available_rect_before_wrap();
self.account_content.ui(ui, &wallet, cb);
// Draw content divider lines.
let r = {
let mut r = rect.clone();
r.min.x -= View::content_padding() + View::far_left_inset_margin(ui);
r.min.y -= View::content_padding();
r.max.x += View::content_padding() + View::get_right_inset();
r
};
if dual_panel && show_wallets_dual {
View::line(ui, LinePosition::LEFT, &r, Colors::item_stroke());
}
});
let rect = {
let mut r = rect.clone();
r.min.x -= View::far_left_inset_margin(ui) + View::TAB_ITEMS_PADDING;
r.min.y -= View::TAB_ITEMS_PADDING;
r.max.x += View::get_right_inset() + View::TAB_ITEMS_PADDING;
r.max.y += View::get_bottom_inset() + View::TAB_ITEMS_PADDING;
r
};
// Draw content divider line.
View::line(ui, LinePosition::TOP, &rect, Colors::stroke());
});
}
// Show wallet transport content.
if !self.account_content.can_back() && show_account {
egui::TopBottomPanel::top(Id::from("wallet_transport").with(wallet.identifier()))
.frame(egui::Frame {
inner_margin: Margin {
left: (View::far_left_inset_margin(ui) + View::content_padding()) as i8,
right: (View::get_right_inset() + View::content_padding()) as i8,
top: 1.0 as i8,
bottom: 1.0 as i8,
},
fill: if top_panel_expanded {
if self.transport_content.qr_address_content.is_some() {
Colors::FILL_DEEP
} else {
Colors::fill_lite()
}
} else {
Colors::TRANSPARENT
},
..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.transport_content.ui(ui, &wallet, cb);
});
// Draw content divider lines.
let r = {
let mut r = rect.clone();
r.min.x -= View::content_padding() + View::far_left_inset_margin(ui);
r.min.y -= 1.0;
r.max.x += View::content_padding() + View::get_right_inset();
r
};
if dual_panel && show_wallets_dual {
View::line(ui, LinePosition::LEFT, &r, Colors::item_stroke());
}
});
}
// Show tab content.
egui::CentralPanel::default()
.frame(egui::Frame {
inner_margin: Margin {
left: View::far_left_inset_margin(ui) + 4.0,
right: View::get_right_inset() + 4.0,
top: 0.0,
bottom: 4.0,
inner_margin: Margin {
left: (View::far_left_inset_margin(ui) + View::content_padding()) as i8,
right: (View::get_right_inset() + View::content_padding()) as i8,
top: 0.0 as i8,
bottom: 4.0 as i8,
},
fill: if self.settings_content.is_some() {
Colors::fill_lite()
} else {
Colors::TRANSPARENT
},
..Default::default()
})
.show_inside(ui, |ui| {
let rect = ui.available_rect_before_wrap();
let tab_type = self.current_tab.get_type();
let show_sync = (tab_type != WalletTabType::Settings || hide_tabs) &&
sync_ui(ui, &self.wallet);
let show_settings = self.settings_content.is_some();
let show_txs = self.txs_content.is_some() && !top_panel_expanded;
let show_sync = (!show_settings || block_nav) &&
sync_ui(ui, &wallet);
if !show_sync {
if tab_type != WalletTabType::Txs {
if show_settings {
ui.add_space(3.0);
ScrollArea::vertical()
.id_salt(Id::from("wallet_scroll")
.with(tab_type.name())
.with(wallet_id))
.id_salt(Id::from("wallet_tab_content_scroll").with(wallet_id))
.auto_shrink([false; 2])
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.show(ui, |ui| {
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
self.current_tab.ui(ui, &self.wallet, cb);
self.settings_content
.as_mut()
.unwrap()
.ui(ui, &wallet, cb);
});
});
} else {
self.current_tab.ui(ui, &self.wallet, cb);
} else if show_txs {
self.txs_content
.as_mut()
.unwrap()
.ui(ui, &wallet, cb);
}
// Handle wallet task result.
self.handle_task_result(wallet);
}
let rect = {
let mut r = rect.clone();
r.min.x -= View::far_left_inset_margin(ui) + 4.0;
r.max.x += View::get_right_inset() + 4.0;
r.min.x -= View::far_left_inset_margin(ui) + View::content_padding();
r.max.x += View::get_right_inset() + View::content_padding();
r.max.y += 4.0;
r
};
// Draw cover when QR code scanner is active.
if show_qr_scan {
View::content_cover_ui(ui, rect, "wallet_tab", || {
cb.stop_camera();
self.qr_scan_content = None;
// Draw cover for content below opened panel.
if !show_sync && self.can_back() {
View::content_cover_ui(ui, rect, "wallet_panel_content_cover", || {
self.back(cb);
});
}
// Draw content divider line.
@@ -248,9 +303,56 @@ impl WalletContent {
}
});
}
}
impl Default for WalletContent {
fn default() -> Self {
Self {
txs_content: Some(WalletTransactionsContent::new(None)),
settings_content: None,
account_content: WalletAccountContent::default(),
transport_content: WalletTransportContent::default(),
invoice_content: None,
send_content: None,
message_content: None,
}
}
}
impl WalletContent {
/// Get title based on current navigation state.
pub fn title(&self) -> impl Into<String> {
if self.account_content.qr_scan_showing() {
t!("scan_qr")
} else if self.account_content.show_list {
t!("wallets.accounts")
} else if self.transport_content.settings_content.is_some() {
t!("wallets.transport")
} else if self.transport_content.qr_address_content.is_some() {
t!("network_mining.address")
} else if self.settings_content.is_some() {
t!("wallets.settings")
} else {
t!("wallets.txs")
}
}
/// Check if it's possible to go back at navigation stack.
pub fn can_back(&self) -> bool {
self.account_content.can_back() || self.transport_content.can_back()
}
/// Navigate back on navigation stack.
pub fn back(&mut self, cb: &dyn PlatformCallbacks) {
if self.account_content.can_back() {
self.account_content.back(cb);
} else if self.transport_content.can_back() {
self.transport_content.back();
}
}
/// Check when to block tabs navigation on sync progress.
pub fn block_navigation_on_sync(wallet: &Wallet) -> bool {
fn block_navigation_on_sync(&self, wallet: &Wallet) -> bool {
let sync_error = wallet.sync_error();
let integrated_node = wallet.get_current_connection() == ConnectionMethod::Integrated;
let integrated_node_ready = Node::get_sync_status() == Some(SyncStatus::NoSync);
@@ -262,149 +364,170 @@ impl WalletContent {
(!integrated_node || integrated_node_ready))
}
/// Draw wallet account content.
fn account_ui(&mut self,
ui: &mut egui::Ui,
data: WalletData,
cb: &dyn PlatformCallbacks) {
let mut rect = ui.available_rect_before_wrap();
rect.set_height(75.0);
// Draw round background.
let rounding = View::item_rounding(0, 2, false);
ui.painter().rect(rect, rounding, Colors::fill_lite(), View::item_stroke());
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
// Draw button to show QR code scanner.
View::item_button(ui, View::item_rounding(0, 2, true), SCAN, None, || {
self.qr_scan_content = Some(CameraContent::default());
cb.start_camera();
});
// Draw button to show list of accounts.
View::item_button(ui, View::item_rounding(1, 3, true), USERS_THREE, None, || {
self.accounts_modal_content = Some(
WalletAccountsModal::new(self.wallet.accounts())
);
Modal::new(ACCOUNT_LIST_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("wallets.accounts"))
.show();
});
let layout_size = ui.available_size();
ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| {
ui.add_space(8.0);
ui.vertical(|ui| {
ui.add_space(3.0);
// Show spendable amount.
let amount = amount_to_hr_string(data.info.amount_currently_spendable, true);
let amount_text = format!("{} {}", amount, GRIN);
ui.with_layout(Layout::left_to_right(Align::Min), |ui| {
ui.add_space(1.0);
ui.label(RichText::new(amount_text)
.size(18.0)
.color(Colors::white_or_black(true)));
});
ui.add_space(-2.0);
// Show account label.
let account = self.wallet.get_config().account;
let default_acc_label = WalletConfig::DEFAULT_ACCOUNT_LABEL.to_string();
let acc_label = if account == default_acc_label {
t!("wallets.default_account")
} else {
account.to_owned()
};
let acc_text = format!("{} {}", FOLDER_USER, acc_label);
View::ellipsize_text(ui, acc_text, 15.0, Colors::text(false));
// Show confirmed height or sync progress.
let status_text = if !self.wallet.syncing() {
format!("{} {}", PACKAGE, data.info.last_confirmed_height)
} else {
let info_progress = self.wallet.info_sync_progress();
if info_progress == 100 || info_progress == 0 {
format!("{} {}", SPINNER, t!("wallets.wallet_loading"))
} else {
if self.wallet.is_repairing() {
let rep_progress = self.wallet.repairing_progress();
if rep_progress == 0 {
format!("{} {}", SPINNER, t!("wallets.wallet_checking"))
} else {
format!("{} {}: {}%",
SPINNER,
t!("wallets.wallet_checking"),
rep_progress)
}
} else {
format!("{} {}: {}%",
SPINNER,
t!("wallets.wallet_loading"),
info_progress)
}
}
};
View::animate_text(ui,
status_text,
15.0,
Colors::gray(),
self.wallet.syncing());
})
});
});
}
/// Draw tab buttons at the bottom of the screen.
fn tabs_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
fn tabs_ui(&mut self, wallet: &Wallet, ui: &mut egui::Ui) {
ui.scope(|ui| {
// Setup spacing between tabs.
ui.style_mut().spacing.item_spacing = egui::vec2(View::TAB_ITEMS_PADDING, 0.0);
// Show camera switch button at QR code scan.
if self.qr_scan_content.is_some() && cb.can_switch_camera() {
// Setup vertical padding inside tab button.
ui.style_mut().spacing.button_padding = egui::vec2(10.0, 4.0);
let has_wallet_data = wallet.get_data().is_some();
let can_send = if has_wallet_data {
wallet.get_data().unwrap().info.amount_currently_spendable > 0
} else {
false
};
ui.vertical_centered(|ui| {
View::tab_button(ui, CAMERA_ROTATE, false, |_| {
cb.switch_camera();
});
});
return;
}
// Setup vertical padding inside buttons.
ui.style_mut().spacing.button_padding = egui::vec2(0.0, 4.0);
let current_type = self.current_tab.get_type();
ui.columns(4, |columns| {
let tabs_amount = if can_send { 5 } else { 4 };
ui.columns(tabs_amount, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::tab_button(ui, GRAPH, current_type == WalletTabType::Txs, |_| {
self.current_tab = Box::new(WalletTransactions::default());
let active = self.settings_content.is_none() && self.txs_content.is_some();
View::tab_button(ui, STACK, None, Some(active), |_| {
self.txs_content = Some(WalletTransactionsContent::new(None));
self.settings_content = None;
});
});
let active = if has_wallet_data { Some(false) } else { None };
columns[1].vertical_centered_justified(|ui| {
let is_messages = current_type == WalletTabType::Messages;
View::tab_button(ui, CHAT_CIRCLE_TEXT, is_messages, |_| {
self.current_tab = Box::new(
WalletMessages::new(None)
);
});
if wallet.invoice_creating() {
ui.add_space(4.0);
View::small_loading_spinner(ui);
} else {
let (icon, color) = (FILE_ARROW_DOWN, Some(Colors::green()));
View::tab_button(ui, icon, color, active, |_| {
self.invoice_content = Some(InvoiceRequestContent::default());
Modal::new(INVOICE_MODAL_ID)
.position(ModalPosition::CenterTop)
.title(t!("wallets.receive"))
.show();
});
}
});
columns[2].vertical_centered_justified(|ui| {
View::tab_button(ui, BRIDGE, current_type == WalletTabType::Transport, |_| {
self.current_tab = Box::new(WalletTransport::default());
});
if wallet.message_opening() {
ui.add_space(4.0);
View::small_loading_spinner(ui);
} else {
let (icon, color) = (FILE_TEXT, Some(Colors::gold_dark()));
View::tab_button(ui, icon, color, active, |_| {
self.message_content = Some(MessageInputContent::default());
Modal::new(MESSAGE_MODAL_ID)
.position(ModalPosition::Center)
.title(t!("wallets.messages"))
.show();
});
}
});
columns[3].vertical_centered_justified(|ui| {
View::tab_button(ui, GEAR_FINE, current_type == WalletTabType::Settings, |ui| {
if can_send {
columns[3].vertical_centered_justified(|ui| {
if wallet.send_creating() {
ui.add_space(4.0);
View::small_loading_spinner(ui);
} else {
let (icon, color) = (FILE_ARROW_UP, Some(Colors::red()));
View::tab_button(ui, icon, color, active, |_| {
self.send_content = Some(SendRequestContent::new(None));
Modal::new(SEND_MODAL_ID)
.position(ModalPosition::CenterTop)
.title(t!("wallets.send"))
.show();
});
}
});
}
columns[tabs_amount - 1].vertical_centered_justified(|ui| {
let active = self.settings_content.is_some();
View::tab_button(ui, GEAR_FINE, None, Some(active), |ui| {
ExternalConnection::check(None, ui.ctx());
self.current_tab = Box::new(WalletSettings::default());
self.txs_content = None;
self.settings_content = Some(WalletSettingsContent::default());
});
});
});
});
}
/// Handle wallet task result.
fn handle_task_result(&mut self, wallet: &Wallet) {
let res = wallet.consume_task_result();
if res.is_none() || wallet.get_data().is_none() {
return;
}
let (id, t) = res.unwrap();
match Modal::opened() {
None => {
// Show transaction modal on wallet task result.
if let Some(id) = id {
let tx = wallet.get_data().unwrap().tx_by_id(id);
if tx.is_some() {
self.txs_content = Some(WalletTransactionsContent::new(tx));
self.settings_content = None;
}
}
}
Some(modal_id) => {
match modal_id {
SEND_MODAL_ID => {
match t {
WalletTask::CalculateFee(_, f) => {
// Setup calculated tx fee at modal.
if let Some(m) = self.send_content.as_mut() {
if m.max_calculating {
let data = wallet.get_data().unwrap();
let a = data.info.amount_currently_spendable;
let max = if f > a {
0
} else {
a - f
};
m.on_max_amount_calculated(max, f);
} else {
m.on_fee_calculated(f);
}
}
}
_ => {}
}
}
MESSAGE_MODAL_ID => {
match t {
WalletTask::VerifyProof(proof, res) => {
if let Some(res) = res {
// Update message content on validation error
// or when tx not belongs to current wallet.
if res.is_err() || (!res.as_ref().unwrap().1 &&
!res.as_ref().unwrap().2) {
if let Some(m) = self.message_content.as_mut() {
if let Ok(p) = serde_json::to_string_pretty(&proof) {
let mut c = PaymentProofContent::new(Some(p));
c.validation_result = Some(res);
m.proof_content = Some(c);
}
}
} else if let Ok((id, _, _)) = res {
let tx = wallet.get_data().unwrap().tx_by_id(id);
if let Some(tx) = tx {
let mut tx_c = WalletTransactionsContent::new(Some(tx));
if let Ok(p) = serde_json::to_string_pretty(&proof) {
let proof_c = PaymentProofContent::new(Some(p));
tx_c.tx_info_content
.as_mut()
.unwrap()
.proof_content = Some(proof_c);
self.txs_content = Some(tx_c);
self.settings_content = None;
}
}
}
}
}
_ => {}
}
}
_ => {}
}
}
}
}
}
/// Draw content when wallet is syncing and not ready to use, returns `true` at this case.
@@ -412,7 +535,7 @@ fn sync_ui(ui: &mut egui::Ui, wallet: &Wallet) -> bool {
if wallet.is_repairing() && !wallet.sync_error() {
sync_progress_ui(ui, wallet);
return true;
} else if wallet.is_closing() {
} else if wallet.is_closing() || wallet.files_moving() {
sync_progress_ui(ui, wallet);
return true;
} else if wallet.get_current_connection() == ConnectionMethod::Integrated {
@@ -478,25 +601,27 @@ fn sync_progress_ui(ui: &mut egui::Ui, wallet: &Wallet) {
let int_ready = Node::get_sync_status() == Some(SyncStatus::NoSync);
let info_progress = wallet.info_sync_progress();
if wallet.is_closing() {
t!("wallets.wallet_closing")
if wallet.files_moving() {
t!("moving_files").into()
} else if wallet.is_closing() {
t!("wallets.wallet_closing").into()
} else if int_node && !int_ready {
t!("wallets.node_loading", "settings" => GEAR_FINE)
t!("wallets.node_loading", "settings" => GEAR_FINE).into()
} else if wallet.is_repairing() {
let repair_progress = wallet.repairing_progress();
if repair_progress == 0 {
t!("wallets.wallet_checking")
t!("wallets.wallet_checking").into()
} else {
format!("{}: {}%", t!("wallets.wallet_checking"), repair_progress)
}
} else if info_progress != 100 {
if info_progress == 0 {
t!("wallets.wallet_loading")
t!("wallets.wallet_loading").into()
} else {
format!("{}: {}%", t!("wallets.wallet_loading"), info_progress)
}
} else {
t!("wallets.tx_loading")
t!("wallets.tx_loading").into()
}
};
ui.label(RichText::new(text).size(16.0).color(Colors::inactive_text()));
+250
View File
@@ -0,0 +1,250 @@
// 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, RichText, ScrollArea};
use crate::gui::icons::{BROOM, CLIPBOARD_TEXT, SCAN, SEAL_CHECK};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{CameraContent, FilePickContent, FilePickContentType, Modal, View};
use crate::gui::Colors;
use crate::gui::views::wallets::wallet::proof::PaymentProofContent;
use crate::wallet::types::WalletTask;
use crate::wallet::Wallet;
pub struct MessageInputContent {
/// Slatepack input text.
message_edit: String,
/// Flag to check if error happened at Slatepack message parsing.
parse_error: bool,
/// Button to parse picked file content.
file_pick_button: FilePickContent,
/// QR code scanner content.
scan_qr_content: Option<CameraContent>,
/// Payment proof input content.
pub proof_content: Option<PaymentProofContent>,
}
/// Hint for Slatepack message input.
const SLATEPACK_MESSAGE_HINT: &'static str = "BEGINSLATEPACK.\n...\n...\n...\nENDSLATEPACK.";
impl Default for MessageInputContent {
fn default() -> Self {
Self {
message_edit: "".to_string(),
parse_error: false,
file_pick_button: FilePickContent::new(
FilePickContentType::Button(t!("choose_file").into())
),
scan_qr_content: None,
proof_content: None,
}
}
}
impl MessageInputContent {
/// Draw [`Modal`] content.
pub fn ui(&mut self,
ui: &mut egui::Ui,
wallet: &Wallet,
modal: &Modal,
cb: &dyn PlatformCallbacks) {
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.
self.on_message_input(result.text(), wallet);
} else {
scan_content.ui(ui, cb);
}
ui.add_space(8.0);
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.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();
});
});
});
} else if let Some(proof_content) = self.proof_content.as_mut() {
proof_content.input_ui(ui, wallet, cb);
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), || {
self.message_edit = "".to_string();
Modal::close();
});
});
} else {
ui.add_space(6.0);
ui.vertical_centered(|ui| {
let (text, color) = if self.parse_error {
(t!("wallets.parse_slatepack_err"), Colors::red())
} else {
(t!("wallets.input_slatepack_desc"), Colors::gray())
};
ui.label(RichText::new(text).size(16.0).color(color));
});
ui.add_space(6.0);
// Draw slatepack message content.
let message_before = self.message_edit.clone();
ui.vertical_centered(|ui| {
let scroll_id = Id::from("message_input").with(wallet.identifier());
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");
let resp = egui::TextEdit::multiline(&mut self.message_edit)
.id(input_id)
.font(egui::TextStyle::Small)
.desired_rows(5)
.interactive(true)
.hint_text(SLATEPACK_MESSAGE_HINT)
.desired_width(f32::INFINITY)
.show(ui)
.response;
if View::is_desktop() {
resp.request_focus();
}
ui.add_space(6.0);
});
});
// Parse message on input change.
if message_before != self.message_edit {
self.on_message_input(self.message_edit.clone(), wallet);
}
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| {
if self.parse_error {
// Draw button to clear message input.
let clear_text = format!("{} {}", BROOM, t!("clear"));
View::button(ui, clear_text, Colors::white_or_black(false), || {
self.message_edit = "".to_string();
self.parse_error = false;
});
} else {
// Draw button to scan Slatepack message QR code.
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();
});
}
});
columns[1].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.on_message_input(cb.get_string_from_buffer(), wallet);
});
});
});
// Draw button to pick Slatepack message file.
ui.add_space(8.0);
ui.vertical_centered(|ui| {
let mut picked_data = None;
self.file_pick_button.ui(ui, cb, |data| {
picked_data = Some(data);
});
if let Some(data) = picked_data {
self.on_message_input(data, wallet);
}
});
ui.add_space(8.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(8.0);
ui.vertical_centered(|ui| {
let proof_label = format!("{} {}", SEAL_CHECK, t!("wallets.payment_proof"));
View::colored_text_button(ui,
proof_label,
Colors::gold_dark(),
Colors::white_or_black(false), || {
Modal::set_title(t!("wallets.payment_proof"));
self.proof_content = Some(PaymentProofContent::new(None));
});
});
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), || {
self.message_edit = "".to_string();
Modal::close();
});
});
}
ui.add_space(6.0);
}
/// Parse Slatepack message on input change.
fn on_message_input(&mut self, text: String, wallet: &Wallet) {
self.parse_error = false;
self.message_edit = text;
if self.message_edit.is_empty() {
return;
}
match wallet.parse_slatepack(&self.message_edit) {
Ok(_) => {
wallet.task(WalletTask::OpenMessage(self.message_edit.to_string()));
self.message_edit = "".to_string();
Modal::close();
}
Err(_) => {
self.parse_error = true;
}
}
}
}
@@ -1,483 +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::thread;
use egui::{Id, RichText, ScrollArea};
use egui::scroll_area::ScrollBarVisibility;
use grin_core::core::amount_to_hr_string;
use grin_wallet_libwallet::{Error, Slate, SlateState};
use parking_lot::RwLock;
use crate::gui::Colors;
use crate::gui::icons::{BROOM, CLIPBOARD_TEXT, DOWNLOAD_SIMPLE, SCAN, UPLOAD_SIMPLE};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{FilePickButton, Modal, View, CameraScanModal};
use crate::gui::views::types::{ModalPosition, QrScanResult};
use crate::gui::views::wallets::wallet::messages::request::MessageRequestModal;
use crate::gui::views::wallets::wallet::types::{SLATEPACK_MESSAGE_HINT, WalletTab, WalletTabType};
use crate::gui::views::wallets::wallet::WalletTransactionModal;
use crate::wallet::types::WalletTransaction;
use crate::wallet::Wallet;
/// Slatepack messages interaction tab content.
pub struct WalletMessages {
/// Flag to check if it's first content draw.
first_draw: bool,
/// Invoice or sending request creation [`Modal`] content.
request_modal_content: Option<MessageRequestModal>,
/// Wallet transaction [`Modal`] content.
tx_info_content: Option<WalletTransactionModal>,
/// Slatepacks message input text.
message_edit: String,
/// Flag to check if message request is loading.
message_loading: bool,
/// Error on finalization, parse or response creation.
message_error: String,
/// Parsed message result.
message_result: Arc<RwLock<Option<(Slate, Result<WalletTransaction, Error>)>>>,
/// QR code scanner [`Modal`] content.
scan_modal_content: Option<CameraScanModal>,
/// Button to parse picked file content.
file_pick_button: FilePickButton,
}
/// Identifier for amount input [`Modal`] to create invoice or sending request.
const REQUEST_MODAL: &'static str = "messages_request_modal";
/// Identifier for [`Modal`] modal to show transaction information.
const TX_INFO_MODAL: &'static str = "messages_tx_info_modal";
/// Identifier for [`Modal`] to scan Slatepack message from QR code.
const SCAN_QR_MODAL: &'static str = "messages_scan_qr_modal";
impl WalletTab for WalletMessages {
fn get_type(&self) -> WalletTabType {
WalletTabType::Messages
}
fn ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) {
self.modal_content_ui(ui, wallet, cb);
self.messages_ui(ui, wallet, cb);
}
}
impl WalletMessages {
/// Create new content instance, put message into input if provided.
pub fn new(message: Option<String>) -> Self {
Self {
first_draw: true,
message_edit: message.unwrap_or("".to_string()),
message_loading: false,
message_error: "".to_string(),
message_result: Arc::new(Default::default()),
tx_info_content: None,
request_modal_content: None,
file_pick_button: FilePickButton::default(),
scan_modal_content: None,
}
}
/// Draw messages content.
fn messages_ui(&mut self,
ui: &mut egui::Ui,
wallet: &Wallet,
cb: &dyn PlatformCallbacks) {
if self.first_draw {
// Parse provided message on first draw.
if !self.message_edit.is_empty() {
self.parse_message(wallet);
}
self.first_draw = false;
}
ui.add_space(3.0);
// Show creation of request to send or receive funds.
self.request_ui(ui, wallet, cb);
ui.add_space(12.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(6.0);
// Show Slatepack message input field.
self.input_slatepack_ui(ui, wallet, cb);
ui.add_space(6.0);
}
/// Draw [`Modal`] content for this ui container.
fn modal_content_ui(&mut self,
ui: &mut egui::Ui,
wallet: &Wallet,
cb: &dyn PlatformCallbacks) {
match Modal::opened() {
None => {}
Some(id) => {
match id {
REQUEST_MODAL => {
if let Some(content) = self.request_modal_content.as_mut() {
Modal::ui(ui.ctx(), |ui, modal| {
content.ui(ui, wallet, modal, cb);
});
}
}
TX_INFO_MODAL => {
if let Some(content) = self.tx_info_content.as_mut() {
Modal::ui(ui.ctx(), |ui, modal| {
content.ui(ui, wallet, modal, cb);
});
}
}
SCAN_QR_MODAL => {
let mut result = None;
if let Some(content) = self.scan_modal_content.as_mut() {
Modal::ui(ui.ctx(), |ui, modal| {
content.ui(ui, modal, cb, |res| {
result = Some(res.clone());
modal.close();
});
});
}
if let Some(res) = result {
self.scan_modal_content = None;
match &res {
QrScanResult::Slatepack(text) => {
self.message_edit = text.to_string();
self.parse_message(wallet);
}
_ => {
self.message_edit = res.text();
self.message_error = t!("wallets.parse_slatepack_err");
}
}
}
}
_ => {}
}
}
}
}
/// Draw creation of request to send or receive funds.
fn request_ui(&mut self,
ui: &mut egui::Ui,
wallet: &Wallet,
cb: &dyn PlatformCallbacks) {
ui.label(RichText::new(t!("wallets.create_request_desc"))
.size(16.0)
.color(Colors::inactive_text()));
ui.add_space(7.0);
// Show send button only if balance is not empty.
let data = wallet.get_data().unwrap();
if data.info.amount_currently_spendable > 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| {
let send_text = format!("{} {}", UPLOAD_SIMPLE, t!("wallets.send"));
View::colored_text_button(ui,
send_text,
Colors::red(),
Colors::white_or_black(false), || {
self.show_request_modal(false, cb);
});
});
columns[1].vertical_centered_justified(|ui| {
self.receive_button_ui(ui, cb);
});
});
} else {
self.receive_button_ui(ui, cb);
}
}
/// Draw invoice request creation button.
fn receive_button_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
let receive_text = format!("{} {}", DOWNLOAD_SIMPLE, t!("wallets.receive"));
View::colored_text_button(ui,
receive_text,
Colors::green(),
Colors::white_or_black(false), || {
self.show_request_modal(true, cb);
});
}
/// Show [`Modal`] to create invoice or sending request.
fn show_request_modal(&mut self, invoice: bool, cb: &dyn PlatformCallbacks) {
self.request_modal_content = Some(MessageRequestModal::new(invoice));
let title = if invoice {
t!("wallets.receive")
} else {
t!("wallets.send")
};
Modal::new(REQUEST_MODAL)
.position(ModalPosition::CenterTop)
.title(title)
.show();
cb.show_keyboard();
}
/// Draw Slatepack message input content.
fn input_slatepack_ui(&mut self,
ui: &mut egui::Ui,
wallet: &Wallet,
cb: &dyn PlatformCallbacks) {
// Setup description text.
if !self.message_error.is_empty() {
ui.label(RichText::new(&self.message_error)
.size(16.0)
.color(Colors::red()));
} else {
ui.label(RichText::new(t!("wallets.input_slatepack_desc"))
.size(16.0)
.color(Colors::inactive_text()));
}
ui.add_space(6.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(3.0);
// Save message to check for changes.
let message_before = self.message_edit.clone();
let scroll_id = Id::from("message_input_scroll").with(wallet.get_config().id);
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");
let resp = egui::TextEdit::multiline(&mut self.message_edit)
.id(input_id)
.font(egui::TextStyle::Small)
.desired_rows(5)
.interactive(!self.message_loading)
.hint_text(SLATEPACK_MESSAGE_HINT)
.desired_width(f32::INFINITY)
.show(ui)
.response;
// Show soft keyboard on click.
if resp.clicked() {
resp.request_focus();
cb.show_keyboard();
}
if resp.has_focus() {
// Apply text from input on Android as temporary fix for egui.
View::on_soft_input(ui, input_id, &mut self.message_edit);
}
ui.add_space(6.0);
});
ui.add_space(2.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(10.0);
// Parse message if input field was changed.
if message_before != self.message_edit {
self.parse_message(wallet);
}
if self.message_loading {
View::small_loading_spinner(ui);
// Check loading result.
let has_tx = {
let r_res = self.message_result.read();
r_res.is_some()
};
if has_tx {
let mut w_res = self.message_result.write();
let tx_res = w_res.as_ref().unwrap();
let slate = &tx_res.0;
match &tx_res.1 {
Ok(tx) => {
self.message_edit.clear();
// Show transaction modal on success.
self.tx_info_content = Some(WalletTransactionModal::new(wallet, tx, false));
Modal::new(TX_INFO_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("wallets.tx"))
.show();
*w_res = None;
}
Err(err) => {
match err {
Error::TransactionWasCancelled {..} => {
self.message_error = t!("wallets.resp_canceled_err");
}
Error::NotEnoughFunds {..} => {
let m = t!(
"wallets.pay_balance_error",
"amount" => amount_to_hr_string(slate.amount, true)
);
self.message_error = m;
}
_ => {
// Show tx modal or show default error message.
if let Some(tx) = wallet.tx_by_slate(&slate).as_ref() {
self.message_edit.clear();
self.tx_info_content = Some(
WalletTransactionModal::new(wallet, tx, false)
);
Modal::new(TX_INFO_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("wallets.tx"))
.show();
} else {
let finalize = slate.state == SlateState::Standard2 ||
slate.state == SlateState::Invoice2;
self.message_error = if finalize {
t!("wallets.finalize_slatepack_err")
} else {
t!("wallets.resp_slatepack_err")
};
}
}
}
}
}
self.message_loading = false;
}
return;
}
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| {
let scan_text = format!("{} {}", SCAN, t!("scan"));
View::button(ui, scan_text, Colors::white_or_black(false), || {
self.message_edit.clear();
self.message_error.clear();
self.scan_modal_content = Some(CameraScanModal::default());
// Show QR code scan modal.
Modal::new(SCAN_QR_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("scan_qr"))
.closeable(false)
.show();
cb.start_camera();
});
});
columns[1].vertical_centered_justified(|ui| {
// Draw button to paste text from clipboard.
let paste = format!("{} {}", CLIPBOARD_TEXT, t!("paste"));
View::button(ui, paste, Colors::white_or_black(false), || {
let buf = cb.get_string_from_buffer();
let previous = self.message_edit.clone();
self.message_edit = buf.clone().trim().to_string();
// Parse Slatepack message resetting message error.
if buf != previous {
self.parse_message(wallet);
}
});
});
});
ui.add_space(10.0);
});
if self.message_edit.is_empty() {
// Draw button to choose file.
let mut parsed_text = "".to_string();
self.file_pick_button.ui(ui, cb, |text| {
parsed_text = text;
});
self.message_edit = parsed_text;
self.parse_message(wallet);
} else {
// Draw button to clear message input.
let clear_text = format!("{} {}", BROOM, t!("clear"));
View::button(ui, clear_text, Colors::white_or_black(false), || {
self.message_edit.clear();
self.message_error.clear();
});
}
}
/// Parse message input making operation based on incoming status.
fn parse_message(&mut self, wallet: &Wallet) {
self.message_error.clear();
self.message_edit = self.message_edit.trim().to_string();
if self.message_edit.is_empty() {
return;
}
if let Ok(mut slate) = wallet.parse_slatepack(&self.message_edit) {
// Try to setup empty amount from transaction by id.
if slate.amount == 0 {
let _ = wallet.get_data().unwrap().txs.as_ref().unwrap().iter().map(|tx| {
if tx.data.tx_slate_id == Some(slate.id) {
if slate.amount == 0 {
slate.amount = tx.amount;
}
}
tx
}).collect::<Vec<&WalletTransaction>>();
}
// Check if message with same id and state already exists to show tx modal.
let exists = wallet.read_slatepack(&slate).is_some();
if exists {
if let Some(tx) = wallet.tx_by_slate(&slate).as_ref() {
self.message_edit.clear();
self.tx_info_content = Some(WalletTransactionModal::new(wallet, tx, false));
Modal::new(TX_INFO_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("wallets.tx"))
.show();
return;
}
}
// Create response or finalize at separate thread.
let sl = slate.clone();
let message = self.message_edit.clone();
let message_result = self.message_result.clone();
let wallet = wallet.clone();
self.message_loading = true;
thread::spawn(move || {
let result = match slate.state {
SlateState::Standard1 | SlateState::Invoice1 => {
if sl.state != SlateState::Standard1 {
wallet.pay(&message)
} else {
wallet.receive(&message)
}
}
SlateState::Standard2 | SlateState::Invoice2 => {
wallet.finalize(&message)
}
_ => {
if let Some(tx) = wallet.tx_by_slate(&slate) {
Ok(tx)
} else {
Err(Error::GenericError(t!("wallets.parse_slatepack_err")))
}
}
};
let mut w_res = message_result.write();
*w_res = Some((slate, result));
});
} else {
self.message_error = t!("wallets.parse_slatepack_err");
}
}
}
@@ -1,260 +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::thread;
use parking_lot::RwLock;
use egui::{Id, RichText};
use grin_core::core::{amount_from_hr_string, amount_to_hr_string};
use grin_wallet_libwallet::Error;
use crate::gui::Colors;
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Modal, View};
use crate::gui::views::types::TextEditOptions;
use crate::gui::views::wallets::wallet::WalletTransactionModal;
use crate::wallet::types::WalletTransaction;
use crate::wallet::Wallet;
/// Invoice or sending request creation [`Modal`] content.
pub struct MessageRequestModal {
/// Flag to check if invoice or sending request was opened.
invoice: bool,
/// Amount to send or receive.
amount_edit: String,
/// Flag to check if request is loading.
request_loading: bool,
/// Request result if there is no error.
request_result: Arc<RwLock<Option<Result<WalletTransaction, Error>>>>,
/// Flag to check if there is an error happened on request creation.
request_error: Option<String>,
/// Request result transaction content.
result_tx_content: Option<WalletTransactionModal>,
}
impl MessageRequestModal {
/// Create new content instance.
pub fn new(invoice: bool) -> Self {
Self {
invoice,
amount_edit: "".to_string(),
request_loading: false,
request_result: Arc::new(RwLock::new(None)),
request_error: None,
result_tx_content: None,
}
}
/// Draw [`Modal`] content.
pub fn ui(&mut self,
ui: &mut egui::Ui,
wallet: &Wallet,
modal: &Modal,
cb: &dyn PlatformCallbacks) {
// Draw transaction information on request result.
if let Some(tx) = self.result_tx_content.as_mut() {
tx.ui(ui, wallet, modal, cb);
return;
}
ui.add_space(6.0);
// Draw content on request loading.
if self.request_loading {
self.loading_request_ui(ui, wallet, modal);
return;
}
// Draw amount input content.
self.amount_input_ui(ui, wallet, modal, cb);
// Show request creation error.
if let Some(err) = &self.request_error {
ui.add_space(12.0);
ui.vertical_centered(|ui| {
ui.label(RichText::new(err)
.size(17.0)
.color(Colors::red()));
});
}
ui.add_space(12.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), || {
self.amount_edit = "".to_string();
self.request_error = None;
cb.hide_keyboard();
modal.close();
});
});
columns[1].vertical_centered_justified(|ui| {
// Button to create Slatepack message request.
View::button(ui, t!("continue"), Colors::white_or_black(false), || {
if self.amount_edit.is_empty() {
return;
}
if let Ok(a) = amount_from_hr_string(self.amount_edit.as_str()) {
cb.hide_keyboard();
modal.disable_closing();
// Setup data for request.
let wallet = wallet.clone();
let invoice = self.invoice.clone();
let result = self.request_result.clone();
// Send request at another thread.
self.request_loading = true;
thread::spawn(move || {
let res = if invoice {
wallet.issue_invoice(a)
} else {
wallet.send(a, None)
};
let mut w_result = result.write();
*w_result = Some(res);
});
} else {
let err = if self.invoice {
t!("wallets.invoice_slatepack_err")
} else {
t!("wallets.send_slatepack_err")
};
self.request_error = Some(err);
}
});
});
});
ui.add_space(6.0);
}
/// Draw amount input content.
fn amount_input_ui(&mut self,
ui: &mut egui::Ui,
wallet: &Wallet,
modal: &Modal,
cb: &dyn PlatformCallbacks) {
ui.vertical_centered(|ui| {
let enter_text = if self.invoice {
t!("wallets.enter_amount_receive")
} else {
let data = wallet.get_data().unwrap();
let amount = amount_to_hr_string(data.info.amount_currently_spendable, true);
t!("wallets.enter_amount_send","amount" => amount)
};
ui.label(RichText::new(enter_text)
.size(17.0)
.color(Colors::gray()));
});
ui.add_space(8.0);
// Draw request amount text input.
let amount_edit_id = Id::from(modal.id).with(wallet.get_config().id);
let mut amount_edit_opts = TextEditOptions::new(amount_edit_id).h_center();
let amount_edit_before = self.amount_edit.clone();
View::text_edit(ui, cb, &mut self.amount_edit, &mut amount_edit_opts);
// Check value if input was changed.
if amount_edit_before != self.amount_edit {
self.request_error = None;
if !self.amount_edit.is_empty() {
self.amount_edit = self.amount_edit.trim().replace(",", ".");
match amount_from_hr_string(self.amount_edit.as_str()) {
Ok(a) => {
if !self.amount_edit.contains(".") {
// To avoid input of several "0".
if a == 0 {
self.amount_edit = "0".to_string();
return;
}
} else {
// Check input after ".".
let parts = self.amount_edit
.split(".")
.collect::<Vec<&str>>();
if parts.len() == 2 && parts[1].len() > 9 {
self.amount_edit = amount_edit_before;
return;
}
}
// Do not input amount more than balance in sending.
if !self.invoice {
let b = wallet.get_data().unwrap().info.amount_currently_spendable;
if b < a {
self.amount_edit = amount_edit_before;
}
}
}
Err(_) => {
self.amount_edit = amount_edit_before;
}
}
}
}
}
/// Draw loading request content.
fn loading_request_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, modal: &Modal) {
ui.add_space(34.0);
ui.vertical_centered(|ui| {
View::big_loading_spinner(ui);
});
ui.add_space(50.0);
// Check if there is request result error.
if self.request_error.is_some() {
modal.enable_closing();
self.request_loading = false;
return;
}
// Update data on request result.
let r_request = self.request_result.read();
if r_request.is_some() {
modal.enable_closing();
let result = r_request.as_ref().unwrap();
match result {
Ok(tx) => {
self.result_tx_content = Some(WalletTransactionModal::new(wallet, tx, false));
}
Err(err) => {
match err {
Error::NotEnoughFunds { .. } => {
let m = t!(
"wallets.pay_balance_error",
"amount" => self.amount_edit
);
self.request_error = Some(m);
}
_ => {
let m = if self.invoice {
t!("wallets.invoice_slatepack_err")
} else {
t!("wallets.send_slatepack_err")
};
self.request_error = Some(m);
}
}
self.request_loading = false;
}
}
}
}
}
+5 -7
View File
@@ -20,13 +20,11 @@ pub use settings::*;
mod txs;
pub use txs::*;
mod messages;
pub use messages::WalletMessages;
mod transport;
pub use transport::WalletTransport;
mod content;
pub use content::WalletContent;
mod modals;
mod account;
mod transport;
mod request;
mod message;
mod proof;
@@ -1,240 +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::{Align, Id, Layout, RichText, ScrollArea};
use egui::scroll_area::ScrollBarVisibility;
use grin_core::core::amount_to_hr_string;
use crate::gui::Colors;
use crate::gui::icons::{CHECK, CHECK_FAT, FOLDER_USER, PATH};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Modal, View};
use crate::gui::views::types::TextEditOptions;
use crate::gui::views::wallets::wallet::types::GRIN;
use crate::wallet::types::WalletAccount;
use crate::wallet::{Wallet, WalletConfig};
/// Wallet accounts [`Modal`] content.
pub struct WalletAccountsModal {
/// List of wallet accounts.
accounts: Vec<WalletAccount>,
/// Flag to check if account is creating.
account_creating: bool,
/// Account label value.
account_label_edit: String,
/// Flag to check if error occurred during account creation.
account_creation_error: bool,
}
impl Default for WalletAccountsModal {
fn default() -> Self {
Self {
accounts: vec![],
account_creating: false,
account_label_edit: "".to_string(),
account_creation_error: false,
}
}
}
impl WalletAccountsModal {
/// Create new instance from wallet accounts.
pub fn new(accounts: Vec<WalletAccount>) -> Self {
Self {
accounts,
account_creating: false,
account_label_edit: "".to_string(),
account_creation_error: false,
}
}
/// Draw [`Modal`] content.
pub fn ui(&mut self,
ui: &mut egui::Ui,
wallet: &Wallet,
modal: &Modal,
cb: &dyn PlatformCallbacks) {
if self.account_creating {
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("wallets.new_account_desc"))
.size(17.0)
.color(Colors::gray()));
ui.add_space(8.0);
// Draw account name edit.
let text_edit_id = Id::from(modal.id).with(wallet.get_config().id);
let mut text_edit_opts = TextEditOptions::new(text_edit_id);
View::text_edit(ui, cb, &mut self.account_label_edit, &mut text_edit_opts);
// Show error occurred during account creation..
if self.account_creation_error {
ui.add_space(12.0);
ui.label(RichText::new(t!("error"))
.size(17.0)
.color(Colors::red()));
}
ui.add_space(12.0);
});
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
// Show modal buttons.
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| {
// Create button callback.
let mut on_create = || {
if !self.account_label_edit.is_empty() {
let label = &self.account_label_edit;
match wallet.create_account(label) {
Ok(_) => {
let _ = wallet.set_active_account(label);
cb.hide_keyboard();
modal.close();
},
Err(_) => self.account_creation_error = true
};
}
};
View::on_enter_key(ui, || {
(on_create)();
});
View::button(ui, t!("create"), Colors::white_or_black(false), on_create);
});
});
ui.add_space(6.0);
} else {
ui.add_space(3.0);
// Show list of accounts.
let size = self.accounts.len();
ScrollArea::vertical()
.id_salt("account_list_modal_scroll")
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.max_height(266.0)
.auto_shrink([true; 2])
.show_rows(ui, ACCOUNT_ITEM_HEIGHT, size, |ui, row_range| {
for index in row_range {
// Add space before the first item.
if index == 0 {
ui.add_space(4.0);
}
let acc = self.accounts.get(index).unwrap();
account_item_ui(ui, modal, wallet, acc, index, size);
if index == size - 1 {
ui.add_space(4.0);
}
}
});
ui.add_space(2.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);
// Show modal buttons.
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!("create"), Colors::white_or_black(false), || {
self.account_creating = true;
cb.show_keyboard();
});
});
});
ui.add_space(6.0);
}
}
}
const ACCOUNT_ITEM_HEIGHT: f32 = 75.0;
/// Draw account item.
fn account_item_ui(ui: &mut egui::Ui,
modal: &Modal,
wallet: &Wallet,
acc: &WalletAccount,
index: usize,
size: usize) {
// Setup layout size.
let mut rect = ui.available_rect_before_wrap();
rect.set_height(ACCOUNT_ITEM_HEIGHT);
// Draw round background.
let bg_rect = rect.clone();
let item_rounding = View::item_rounding(index, size, 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 account.
let is_current_account = wallet.get_config().account == acc.label;
if !is_current_account {
let button_rounding = View::item_rounding(index, size, true);
View::item_button(ui, button_rounding, CHECK, None, || {
let _ = wallet.set_active_account(&acc.label);
modal.close();
});
} else {
ui.add_space(12.0);
ui.label(RichText::new(CHECK_FAT).size(20.0).color(Colors::green()));
}
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(4.0);
// Show spendable amount.
let amount = amount_to_hr_string(acc.spendable_amount, true);
let amount_text = format!("{} {}", amount, GRIN);
ui.label(RichText::new(amount_text).size(18.0).color(Colors::white_or_black(true)));
ui.add_space(-2.0);
// Show account name.
let default_acc_label = WalletConfig::DEFAULT_ACCOUNT_LABEL.to_string();
let acc_label = if acc.label == default_acc_label {
t!("wallets.default_account")
} else {
acc.label.to_owned()
};
let acc_name = format!("{} {}", FOLDER_USER, acc_label);
View::ellipsize_text(ui, acc_name, 15.0, Colors::text(false));
// Show account BIP32 derivation path.
let acc_path = format!("{} {}", PATH, acc.path);
ui.label(RichText::new(acc_path).size(15.0).color(Colors::gray()));
ui.add_space(3.0);
});
});
});
});
}
+223
View File
@@ -0,0 +1,223 @@
use egui::scroll_area::ScrollBarVisibility;
use egui::{Id, RichText, ScrollArea};
use grin_util::ToHex;
use grin_wallet_libwallet::{Error, PaymentProof, TxLogEntryType};
use crate::gui::icons::{BROOM, CLIPBOARD_TEXT, COPY, FILE_TEXT, SEAL_CHECK};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{FilePickContent, FilePickContentType, Modal, View};
use crate::gui::Colors;
use crate::wallet::types::{WalletTask, WalletTx};
use crate::wallet::Wallet;
pub struct PaymentProofContent {
/// Payment proof text.
input_edit: String,
/// Button to pick payment proof file.
pick_button: FilePickContent,
/// Flag to check if an error occurred during proof parsing.
parse_error: bool,
/// Proof validation result.
pub validation_result: Option<Result<(u32, bool, bool), Error>>,
}
impl PaymentProofContent {
/// Create new content to share or validate payment proof.
pub fn new(proof_text: Option<String>) -> Self {
Self {
input_edit: proof_text.unwrap_or("".to_string()),
pick_button: FilePickContent::new(FilePickContentType::Button(t!("file").into())),
parse_error: false,
validation_result: None,
}
}
/// Draw transaction payment proof input.
pub fn input_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) {
ui.add_space(6.0);
ui.vertical_centered(|ui| {
if self.parse_error {
let label_text = t!("wallets.payment_proof_error");
ui.label(RichText::new(label_text).size(16.0).color(Colors::red()));
} else if let Some(proof) = self.validation_result.as_ref() {
match proof {
Ok(_) => {
let label_text = t!("wallets.payment_proof_valid");
ui.label(RichText::new(label_text).size(16.0).color(Colors::green()));
}
Err(e) => {
let error_text = t!("wallets.payment_proof_error");
let label_text = format!("{} ({:?})", error_text, e);
ui.label(RichText::new(label_text).size(16.0).color(Colors::red()));
}
}
} else {
let desc_label = t!("wallets.payment_proof_desc");
ui.label(RichText::new(desc_label).size(16.0).color(Colors::inactive_text()));
}
});
ui.add_space(6.0);
ui.vertical_centered(|ui| {
let scroll_id = Id::from("tx_info_payment_proof_input");
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("edit");
let proof_input_before = self.input_edit.clone();
let resp = egui::TextEdit::multiline(&mut self.input_edit)
.id(input_id)
.font(egui::TextStyle::Small)
.desired_rows(5)
.interactive(!wallet.payment_proof_verifying())
.desired_width(f32::INFINITY)
.show(ui)
.response;
if View::is_desktop() {
resp.request_focus();
}
// Parse payment proof on input change.
if self.input_edit != proof_input_before {
self.on_proof_edit_change(wallet);
}
ui.add_space(6.0);
});
});
ui.add_space(2.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(8.0);
if wallet.payment_proof_verifying() {
ui.vertical_centered(|ui| {
View::small_loading_spinner(ui);
});
} else {
// 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| {
if self.parse_error || (self.validation_result.is_some() &&
self.validation_result.as_ref().unwrap().is_err()) {
// Draw button to clear message input.
let clear_text = format!("{} {}", BROOM, t!("clear"));
View::button(ui, clear_text, Colors::white_or_black(false), || {
self.input_edit = "".to_string();
self.parse_error = false;
self.validation_result = None;
});
} else {
// Draw button to paste proof text.
let paste_text = format!("{} {}", CLIPBOARD_TEXT, t!("paste"));
View::button(ui, paste_text, Colors::white_or_black(false), || {
self.input_edit = cb.get_string_from_buffer();
self.on_proof_edit_change(wallet);
});
}
});
columns[1].vertical_centered_justified(|ui| {
let mut changed = false;
self.pick_button.ui(ui, cb, |data| {
self.input_edit = data.clone();
changed = true;
});
if changed {
self.on_proof_edit_change(wallet);
}
});
});
}
}
/// Callback on payment proof input change.
fn on_proof_edit_change(&mut self, wallet: &Wallet) {
if wallet.payment_proof_verifying() {
return;
}
if self.input_edit.is_empty() {
self.parse_error = false;
return;
}
if let Ok(p) = serde_json::from_str::<PaymentProof>(self.input_edit.as_str()) {
wallet.task(WalletTask::VerifyProof(p, None));
} else {
self.parse_error = true;
}
}
/// Draw transaction payment proof content to share.
pub fn share_ui(&mut self,
ui: &mut egui::Ui,
tx: &WalletTx,
cb: &dyn PlatformCallbacks) {
ui.add_space(6.0);
ui.vertical_centered(|ui| {
let (desc_text, color) = if tx.data.tx_type == TxLogEntryType::TxReceived {
(t!("wallets.payment_proof_valid").into(), Colors::green())
} else {
(format!("{}:", t!("wallets.payment_proof")), Colors::inactive_text())
};
let desc = format!("{} {}", SEAL_CHECK, desc_text);
ui.label(RichText::new(desc).size(16.0).color(color));
});
ui.add_space(6.0);
ui.vertical_centered(|ui| {
let scroll_id = Id::from("tx_info_payment_proof_share");
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("edit");
egui::TextEdit::multiline(&mut self.input_edit)
.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(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.input_edit.clone());
Modal::close();
});
});
columns[1].vertical_centered_justified(|ui| {
let share_text = format!("{} {}", FILE_TEXT, t!("share"));
View::colored_text_button(ui,
share_text,
Colors::blue(),
Colors::white_or_black(false), || {
let file_name = format!("{}.txt", tx.data.kernel_excess.unwrap().to_hex());
let data = self.input_edit.as_bytes().to_vec();
cb.share_data(file_name, data).unwrap_or_default();
Modal::close();
});
});
});
}
}

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