Compare commits

...

79 commits

Author SHA1 Message Date
cf4f0789a3 build: update egui to last github version
Some checks failed
Build / Linux Build (push) Has been cancelled
Build / Windows Build (push) Has been cancelled
Build / MacOS Build (push) Has been cancelled
2025-06-25 13:13:31 +03:00
1b78118f51 fix: action repeat tor tx, no resend for tor
Some checks are pending
Build / Linux Build (push) Waiting to run
Build / Windows Build (push) Waiting to run
Build / MacOS Build (push) Waiting to run
2025-06-25 12:51:29 +03:00
a89a9bcaed fix: message opening
Some checks are pending
Build / Linux Build (push) Waiting to run
Build / Windows Build (push) Waiting to run
Build / MacOS Build (push) Waiting to run
2025-06-25 11:50:49 +03:00
8528c33be5 fix: message opening when slate with previous state exists
Some checks are pending
Build / Linux Build (push) Waiting to run
Build / Windows Build (push) Waiting to run
Build / MacOS Build (push) Waiting to run
2025-06-25 11:45:26 +03:00
d1502e26b1 wallet: cleanup broadcasting delay on repost
Some checks are pending
Build / Windows Build (push) Waiting to run
Build / MacOS Build (push) Waiting to run
Build / Linux Build (push) Waiting to run
2025-06-25 11:32:26 +03:00
2f56defffa wallet: broadcasting delay, repeat tx action
Some checks are pending
Build / Windows Build (push) Waiting to run
Build / MacOS Build (push) Waiting to run
Build / Linux Build (push) Waiting to run
2025-06-25 11:08:57 +03:00
01af084568 build: update grin and tor deps
Some checks failed
Build / Linux Build (push) Has been cancelled
Build / Windows Build (push) Has been cancelled
Build / MacOS Build (push) Has been cancelled
2025-06-19 09:34:20 +03:00
cd0e3485c5 txs: async tasks for wallet
Some checks are pending
Build / Linux Build (push) Waiting to run
Build / Windows Build (push) Waiting to run
Build / MacOS Build (push) Waiting to run
2025-06-19 09:18:20 +03:00
b540fcbf19 tor: do not start already starting service 2025-06-11 15:16:33 +03:00
7d29b2af6d tx: qr padding, info buttons positions
Some checks failed
Build / Linux Build (push) Has been cancelled
Build / Windows Build (push) Has been cancelled
Build / MacOS Build (push) Has been cancelled
2025-06-10 22:25:10 +03:00
ad030fe811 fix: tx finalizing status setup
Some checks are pending
Build / Linux Build (push) Waiting to run
Build / Windows Build (push) Waiting to run
Build / MacOS Build (push) Waiting to run
2025-06-10 22:16:51 +03:00
fae1364f10 wallet: tx response flag to show sharing controls
Some checks are pending
Build / Linux Build (push) Waiting to run
Build / Windows Build (push) Waiting to run
Build / MacOS Build (push) Waiting to run
2025-06-10 22:03:49 +03:00
93297b5401 tx: do not show sharing content when can not finalize
Some checks are pending
Build / Linux Build (push) Waiting to run
Build / Windows Build (push) Waiting to run
Build / MacOS Build (push) Waiting to run
2025-06-10 21:20:27 +03:00
511611f994 wallet: show only txs with slate id
Some checks are pending
Build / Linux Build (push) Waiting to run
Build / Windows Build (push) Waiting to run
Build / MacOS Build (push) Waiting to run
2025-06-10 20:40:50 +03:00
e9e2a0a8e7 ui: fix tx description
Some checks are pending
Build / Linux Build (push) Waiting to run
Build / Windows Build (push) Waiting to run
Build / MacOS Build (push) Waiting to run
2025-06-10 20:25:23 +03:00
1222399926 tx: remove manual slatepack input, scan outputs after wallet db deletion
Some checks are pending
Build / Linux Build (push) Waiting to run
Build / Windows Build (push) Waiting to run
Build / MacOS Build (push) Waiting to run
2025-06-10 20:09:24 +03:00
845c1dc0ea i18n: file 2025-06-10 19:34:35 +03:00
3a21e60e19 ui: do not copy form animated qr 2025-06-10 19:30:36 +03:00
9622429180 build: remove unused module 2025-06-10 19:04:56 +03:00
d04b7a4e6a build: update version name
Some checks failed
Build / Linux Build (push) Has been cancelled
Build / Windows Build (push) Has been cancelled
Build / MacOS Build (push) Has been cancelled
2025-06-09 12:51:07 +03:00
8b369b6049 ui: refactoring of wallet screen, fix colors
Some checks are pending
Build / Linux Build (push) Waiting to run
Build / Windows Build (push) Waiting to run
Build / MacOS Build (push) Waiting to run
2025-06-09 12:34:07 +03:00
b54a573f61 tor: proxy settings 2025-06-09 12:27:36 +03:00
184326bfde wallet: open slatepack 2025-06-09 12:23:01 +03:00
b1f3c7d42b fix: mnemonic input
Some checks failed
Build / Linux Build (push) Has been cancelled
Build / Windows Build (push) Has been cancelled
Build / MacOS Build (push) Has been cancelled
2025-06-06 14:29:29 +03:00
53a96e567d wallet: sort accounts to show current first 2025-06-04 15:33:34 +03:00
20daa7b465 network: fix external connections check 2025-06-03 16:11:08 +03:00
0fa2ef4283 qr: smaller text 2025-06-02 21:54:13 +03:00
e067a0a900 qr: add max size support, ui copy button 2025-06-02 21:03:49 +03:00
31d8e2f012 eframe: glow renderer
Some checks failed
Build / Linux Build (push) Has been cancelled
Build / Windows Build (push) Has been cancelled
Build / MacOS Build (push) Has been cancelled
2025-06-02 12:10:41 +03:00
84d385ef1a macos: glow renderer
Some checks are pending
Build / Linux Build (push) Waiting to run
Build / Windows Build (push) Waiting to run
Build / MacOS Build (push) Waiting to run
2025-06-01 23:11:57 +03:00
fabef9492e proxy: tls support
Some checks are pending
Build / Linux Build (push) Waiting to run
Build / Windows Build (push) Waiting to run
Build / MacOS Build (push) Waiting to run
2025-06-01 00:05:48 +03:00
92f8386264 http: client, wallet to node communication with proxy
Some checks are pending
Build / Linux Build (push) Waiting to run
Build / Windows Build (push) Waiting to run
Build / MacOS Build (push) Waiting to run
2025-05-31 23:34:51 +03:00
1ef62a806b fix: show word list on wallet creation
Some checks are pending
Build / Linux Build (push) Waiting to run
Build / Windows Build (push) Waiting to run
Build / MacOS Build (push) Waiting to run
2025-05-31 22:34:12 +03:00
f8da3d0754 fix: hyper client import
Some checks are pending
Build / Linux Build (push) Waiting to run
Build / Windows Build (push) Waiting to run
Build / MacOS Build (push) Waiting to run
2025-05-31 17:44:37 +03:00
8165fab326 tor: update arti-client to 0.30.0
Some checks are pending
Build / Linux Build (push) Waiting to run
Build / Windows Build (push) Waiting to run
Build / MacOS Build (push) Waiting to run
2025-05-31 17:08:39 +03:00
918c5b4355 build: imports
Some checks are pending
Build / Linux Build (push) Waiting to run
Build / Windows Build (push) Waiting to run
Build / MacOS Build (push) Waiting to run
2025-05-31 15:47:03 +03:00
f930cd4ade config: node db path 2025-05-31 15:45:36 +03:00
3f3940e752 ui: remove storage settings 2025-05-31 15:45:15 +03:00
4ef5dd839d platform: pick folder 2025-05-31 15:44:24 +03:00
fd14700eae settings: network proxy
Some checks are pending
Build / Linux Build (push) Waiting to run
Build / Windows Build (push) Waiting to run
Build / MacOS Build (push) Waiting to run
2025-05-31 14:12:31 +03:00
e5548eb6f1 fix: current locale check at modal 2025-05-31 09:20:46 +03:00
a364daf52e ui: network and storage settings modules, language selection modal
Some checks are pending
Build / Linux Build (push) Waiting to run
Build / Windows Build (push) Waiting to run
Build / MacOS Build (push) Waiting to run
2025-05-31 09:11:07 +03:00
7089e6e1b2 ui: update app title 2025-05-31 09:09:01 +03:00
0621154902 ui: remove on_back callback from content container
Some checks are pending
Build / Linux Build (push) Waiting to run
Build / Windows Build (push) Waiting to run
Build / MacOS Build (push) Waiting to run
2025-05-30 22:11:16 +03:00
acfb5fec1a ui: wallet content container, accounts panel
Some checks are pending
Build / Linux Build (push) Waiting to run
Build / Windows Build (push) Waiting to run
Build / MacOS Build (push) Waiting to run
2025-05-30 21:25:29 +03:00
1a3df4619e ui: accounts module
Some checks are pending
Build / Linux Build (push) Waiting to run
Build / Windows Build (push) Waiting to run
Build / MacOS Build (push) Waiting to run
2025-05-30 16:13:27 +03:00
8994775be2 fix: keyboard focus
Some checks are pending
Build / Linux Build (push) Waiting to run
Build / Windows Build (push) Waiting to run
Build / MacOS Build (push) Waiting to run
2025-05-30 15:06:47 +03:00
81365dbe6a ui: reset keyboard window state on opening and inputs focus change
Some checks are pending
Build / Linux Build (push) Waiting to run
Build / Windows Build (push) Waiting to run
Build / MacOS Build (push) Waiting to run
2025-05-30 14:48:49 +03:00
7ae63b2b66 fix: modal window focus
Some checks are pending
Build / Linux Build (push) Waiting to run
Build / Windows Build (push) Waiting to run
Build / MacOS Build (push) Waiting to run
2025-05-30 14:14:58 +03:00
b8dd5911d4 ui: animate wallet list panels
Some checks are pending
Build / Linux Build (push) Waiting to run
Build / Windows Build (push) Waiting to run
Build / MacOS Build (push) Waiting to run
2025-05-30 13:01:12 +03:00
3fc4ffa179 fix: wallet and mnemonic modals for container
Some checks are pending
Build / Linux Build (push) Waiting to run
Build / Windows Build (push) Waiting to run
Build / MacOS Build (push) Waiting to run
2025-05-30 12:50:33 +03:00
b84f6480e7 ui: content container
Some checks are pending
Build / Linux Build (push) Waiting to run
Build / Windows Build (push) Waiting to run
Build / MacOS Build (push) Waiting to run
2025-05-30 12:33:13 +03:00
5dd8de7950 modal: move focus setup to the root content 2025-05-29 23:50:26 +03:00
78baaca4a3 fix: keyboard modal focus
Some checks are pending
Build / Linux Build (push) Waiting to run
Build / Windows Build (push) Waiting to run
Build / MacOS Build (push) Waiting to run
2025-05-29 15:37:00 +03:00
e597ac7e4b ui: ability to not show soft keyboard for input, move modal on top only at first draw
Some checks are pending
Build / Linux Build (push) Waiting to run
Build / Windows Build (push) Waiting to run
Build / MacOS Build (push) Waiting to run
2025-05-29 13:32:33 +03:00
4d5cc93a38 ui: settings content at separate panel
Some checks are pending
Build / Linux Build (push) Waiting to run
Build / Windows Build (push) Waiting to run
Build / MacOS Build (push) Waiting to run
2025-05-29 12:56:49 +03:00
ed50132d5e keyboard: long press clear
Some checks are pending
Build / Linux Build (push) Waiting to run
Build / Windows Build (push) Waiting to run
Build / MacOS Build (push) Waiting to run
2025-05-29 01:31:12 +03:00
fbb084f636 wallet: do not scan outputs for new wallet
Some checks are pending
Build / Linux Build (push) Waiting to run
Build / Windows Build (push) Waiting to run
Build / MacOS Build (push) Waiting to run
2025-05-29 00:38:45 +03:00
d42ef102b2 keyboard: layouts for languages
Some checks are pending
Build / Linux Build (push) Waiting to run
Build / Windows Build (push) Waiting to run
Build / MacOS Build (push) Waiting to run
2025-05-28 20:16:23 +03:00
9673c7d719 keyboard: show refactoring
Some checks are pending
Build / Linux Build (push) Waiting to run
Build / Windows Build (push) Waiting to run
Build / MacOS Build (push) Waiting to run
2025-05-28 13:46:44 +03:00
9b4623c558 keyboard: state refactoring
Some checks are pending
Build / Linux Build (push) Waiting to run
Build / Windows Build (push) Waiting to run
Build / MacOS Build (push) Waiting to run
2025-05-28 12:58:57 +03:00
b7563e63c1 ui: esc key handling for keyboard without modal 2025-05-28 11:11:22 +03:00
4d4b5eb007 keyboard: optimize buttons
Some checks are pending
Build / Linux Build (push) Waiting to run
Build / Windows Build (push) Waiting to run
Build / MacOS Build (push) Waiting to run
2025-05-28 10:23:17 +03:00
6c04eec026 modal: close refactoring
Some checks are pending
Build / Linux Build (push) Waiting to run
Build / Windows Build (push) Waiting to run
Build / MacOS Build (push) Waiting to run
2025-05-27 22:14:43 +03:00
1ff2b27edc android: release build script 2025-05-27 22:04:55 +03:00
6bce9ec071 android: switch to nativeactivity, fix clicks
Some checks are pending
Build / Linux Build (push) Waiting to run
Build / Windows Build (push) Waiting to run
Build / MacOS Build (push) Waiting to run
2025-05-27 21:01:00 +03:00
98619cc362 ui: update to egui 0.31
Some checks are pending
Build / Linux Build (push) Waiting to run
Build / Windows Build (push) Waiting to run
Build / MacOS Build (push) Waiting to run
2025-05-27 16:10:29 +03:00
1987d0553c ui: numeric keyboard input
Some checks are pending
Build / Linux Build (push) Waiting to run
Build / Windows Build (push) Waiting to run
Build / MacOS Build (push) Waiting to run
2025-05-27 14:28:23 +03:00
3f78095fe3 ui: keyboard language switch
Some checks are pending
Build / Linux Build (push) Waiting to run
Build / Windows Build (push) Waiting to run
Build / MacOS Build (push) Waiting to run
2025-05-27 13:08:32 +03:00
245766e1b5 fix: text width inside input content 2025-05-26 22:37:03 +03:00
2591653f66 ui: input refactoring
Some checks are pending
Build / Linux Build (push) Waiting to run
Build / Windows Build (push) Waiting to run
Build / MacOS Build (push) Waiting to run
2025-05-26 20:48:29 +03:00
d11e90226b feat: software keyboard (without language switch)
Some checks failed
Build / Linux Build (push) Has been cancelled
Build / Windows Build (push) Has been cancelled
Build / MacOS Build (push) Has been cancelled
2025-05-23 19:20:42 +03:00
fb159c17a0 i18n: chinese
Some checks failed
Build / Linux Build (push) Has been cancelled
Build / Windows Build (push) Has been cancelled
Build / MacOS Build (push) Has been cancelled
2025-04-27 21:22:08 +03:00
f7eb6580cc tor: trim address on send
Some checks are pending
Build / Linux Build (push) Waiting to run
Build / Windows Build (push) Waiting to run
Build / MacOS Build (push) Waiting to run
2025-04-27 19:51:06 +03:00
43720b34ba fix: external connection deletion
Some checks failed
Build / Linux Build (push) Has been cancelled
Build / Windows Build (push) Has been cancelled
Build / MacOS Build (push) Has been cancelled
2025-04-23 15:10:48 +03:00
f1f0f002ce fix: content redraw at connections 2025-04-23 13:09:31 +03:00
86afa21a60 node: do not remove lock file on cleanup
Some checks are pending
Build / Linux Build (push) Waiting to run
Build / Windows Build (push) Waiting to run
Build / MacOS Build (push) Waiting to run
2025-04-23 12:38:23 +03:00
0169acba81 build: use zig linker for macos and linux for arm on x86
Some checks failed
Build / Linux Build (push) Has been cancelled
Build / Windows Build (push) Has been cancelled
Build / MacOS Build (push) Has been cancelled
2025-04-02 22:30:59 +03:00
073d950d41 github: disable release build
Some checks are pending
Build / Linux Build (push) Waiting to run
Build / Windows Build (push) Waiting to run
Build / MacOS Build (push) Waiting to run
2025-04-02 21:10:45 +03:00
111 changed files with 10435 additions and 8410 deletions

View file

@ -1,170 +0,0 @@
#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

170
.github/workflows/release.yml.bak vendored Normal file
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

1
.gitignore vendored
View file

@ -1,4 +1,5 @@
*.iml
android/build
android/.idea
android/.gradle
android/local.properties

4126
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
[package]
name = "grim"
version = "0.2.4"
version = "0.3.0-alpha"
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"
@ -27,7 +27,7 @@ panic = "abort"
[dependencies]
log = "0.4.27"
## node
## grin
grin_api = "5.3.3"
grin_chain = "5.3.3"
grin_config = "5.3.3"
@ -37,16 +37,38 @@ grin_servers = "5.3.3"
grin_keychain = "5.3.3"
grin_util = "5.3.3"
## 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"
#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"
# local
#grin_api = { path = "../grin/api" }
#grin_chain = { path = "../grin/chain" }
#grin_config = { path = "../grin/config" }
#grin_core = { path = "../grin/core" }
#grin_p2p = { path = "../grin/p2p" }
#grin_servers = { path = "../grin/servers" }
#grin_keychain = { path = "../grin/keychain" }
#grin_util = { path = "../grin/util" }
#grin_wallet_impls = { path = "../grin-wallet/impls" }
#grin_wallet_api = { path = "../grin-wallet/api"}
#grin_wallet_libwallet = { path = "../grin-wallet/libwallet" }
#grin_wallet_util = { path = "../grin-wallet/util" }
#grin_wallet_controller = { path = "../grin-wallet/controller" }
# test
grin_wallet_impls = { git = "https://github.com/mimblewimble/grin-wallet", rev = "930a44d456b43172fc096eda0bbf6a3841f48c6a" }
grin_wallet_api = { git = "https://github.com/mimblewimble/grin-wallet", rev = "930a44d456b43172fc096eda0bbf6a3841f48c6a" }
grin_wallet_libwallet = { git = "https://github.com/mimblewimble/grin-wallet", rev = "930a44d456b43172fc096eda0bbf6a3841f48c6a" }
grin_wallet_util = { git = "https://github.com/mimblewimble/grin-wallet", rev = "930a44d456b43172fc096eda0bbf6a3841f48c6a" }
grin_wallet_controller = { git = "https://github.com/mimblewimble/grin-wallet", rev = "930a44d456b43172fc096eda0bbf6a3841f48c6a" }
## ui
egui = { version = "0.29.1", default-features = false }
egui_extras = { version = "0.29.1", features = ["image", "svg"] }
egui = { version = "0.31.1", default-features = false }
egui_extras = { version = "0.31.1", features = ["image", "svg"] }
rust-i18n = "2.3.1"
## other
@ -75,28 +97,36 @@ qrcode = "0.14.1"
ur = "0.4.1"
gif = "0.13.1"
rkv = { version = "0.19.0", features = ["lmdb"] }
usvg = "0.45.1"
ring = "0.16.20"
hyper = { version = "1.6.0", features = ["full"], package = "hyper" }
hyper-util = { version = "0.1.11", features = ["http1", "client", "client-legacy"] }
http-body-util = "0.1.3"
bytes = "1.10.1"
hyper-socks2 = "0.9.1"
hyper-proxy2 = "0.1.0"
hyper-tls = "0.6.0"
## tor
arti-client = { version = "0.29.0", features = ["pt-client", "static", "onion-service-service", "onion-service-client"] }
tor-rtcompat = { version = "0.29.0", features = ["static"] }
tor-config = "0.29.0"
arti-client = { version = "0.31.0", features = ["pt-client", "static", "onion-service-service", "onion-service-client"] }
tor-rtcompat = { version = "0.31.0", features = ["static"] }
tor-config = "0.31.0"
fs-mistrust = "0.9.1"
tor-hsservice = "0.29.0"
tor-hsrproxy = "0.29.0"
tor-keymgr = "0.29.0"
tor-llcrypto = "0.29.0"
tor-hscrypto = "0.29.0"
tor-error = "0.29.0"
tor-hsservice = "0.31.0"
tor-hsrproxy = "0.31.0"
tor-keymgr = "0.31.0"
tor-llcrypto = "0.31.0"
tor-hscrypto = "0.31.0"
tor-error = "0.31.0"
sha2 = "0.10.8"
ed25519-dalek = "2.1.1"
curve25519-dalek = "4.1.3"
hyper = { version = "0.14.31", features = ["full"] }
hyper-tls = "0.5.0"
hyper-tor = { version = "0.14.32", features = ["full"], package = "hyper" }
tls-api = "0.12.0"
tls-api-native-tls = "0.12.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]
@ -110,23 +140,22 @@ nokhwa-mac = { git = "https://github.com/l1npengtul/nokhwa", rev = "612c861ef153
[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.11" }
eframe = { version = "0.31.1", default-features = false, features = ["glow"] }
arboard = "3.2.0"
rfd = "0.15.0"
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"] }
android-activity = { version = "0.6.0", features = ["native-activity"] }
winit = { version = "0.30.11", features = ["android-native-activity"] }
eframe = { version = "0.31.1", default-features = false, features = ["glow", "android-native-activity"] }
[patch.crates-io]
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" }
egui_extras = { git = "https://github.com/emilk/egui", rev = "f11a3510ba07ae87747d744d952676476a88c24e" }
egui = { git = "https://github.com/emilk/egui", rev = "f11a3510ba07ae87747d744d952676476a88c24e" }
eframe = { git = "https://github.com/emilk/egui", rev = "f11a3510ba07ae87747d744d952676476a88c24e" }
### patch grin store
#grin_store = { path = "../grin-store" }

View file

@ -3,14 +3,14 @@ plugins {
}
android {
compileSdk 33
compileSdk 35
ndkVersion '26.0.10792818'
defaultConfig {
applicationId "mw.gri.android"
minSdk 24
targetSdk 33
versionCode 3
targetSdk 35
versionCode 4
versionName "0.2.4"
}
@ -27,7 +27,6 @@ android {
storePassword keystoreProperties['storePassword']
}
}
}
buildTypes {
@ -54,14 +53,11 @@ android {
}
dependencies {
implementation 'androidx.appcompat:appcompat:1.6.1'
// To use the Games Activity library
implementation "androidx.games:games-activity:2.0.2"
implementation 'androidx.appcompat:appcompat:1.7.0'
// 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.4.2'
implementation 'androidx.camera:camera-camera2:1.4.2'
implementation 'androidx.camera:camera-lifecycle:1.4.2'
}

View file

@ -12,6 +12,7 @@
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" tools:ignore="ScopedStorage"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" tools:ignore="ScopedStorage"/>
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" tools:ignore="ScopedStorage"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<application
android:hardwareAccelerated="true"
@ -63,7 +64,11 @@
<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>

View file

@ -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;
@ -32,25 +32,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")
@ -170,9 +151,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 +181,6 @@ public class BackgroundService extends Service {
// Stop updating the notification.
mHandler.removeCallbacks(mUpdateSyncStatus);
unregisterReceiver(mReceiver);
clearNotification();
// Remove service from foreground state.
@ -226,12 +203,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));
}
}
}

View file

@ -3,6 +3,7 @@ package mw.gri.android;
import android.Manifest;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.NativeActivity;
import android.content.*;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
@ -12,12 +13,9 @@ import android.os.Process;
import android.provider.Settings;
import android.system.ErrnoException;
import android.system.Os;
import android.util.Size;
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;
import androidx.camera.core.*;
import androidx.camera.lifecycle.ProcessCameraProvider;
@ -27,37 +25,41 @@ import androidx.core.graphics.Insets;
import androidx.core.view.DisplayCutoutCompat;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import com.google.androidgamesdk.GameActivity;
import com.google.common.util.concurrent.ListenableFuture;
import java.io.*;
import java.util.Objects;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import static android.content.ClipDescription.MIMETYPE_TEXT_HTML;
import static android.content.ClipDescription.MIMETYPE_TEXT_PLAIN;
public class MainActivity extends GameActivity {
public static String STOP_APP_ACTION = "STOP_APP";
public class MainActivity extends NativeActivity {
private static final int FILE_PICK_REQUEST = 1001;
private static final int FILE_PERMISSIONS_REQUEST = 1002;
private static final int NOTIFICATIONS_PERMISSION_CODE = 1;
private static final int CAMERA_PERMISSION_CODE = 2;
public static final String STOP_APP_ACTION = "STOP_APP_ACTION";
static {
System.loadLibrary("grim");
}
private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@SuppressLint("RestrictedApi")
@Override
public void onReceive(Context ctx, Intent i) {
if (i.getAction().equals(STOP_APP_ACTION)) {
public void onReceive(Context context, Intent intent) {
if (Objects.equals(intent.getAction(), MainActivity.STOP_APP_ACTION)) {
exit();
}
}
};
private final ImageAnalysis mImageAnalysis = new ImageAnalysis.Builder()
.setTargetResolution(new Size(640, 480))
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build();
@ -66,9 +68,6 @@ public class MainActivity extends GameActivity {
private ExecutorService mCameraExecutor = null;
private boolean mUseBackCamera = true;
private ActivityResultLauncher<Intent> mFilePickResult = null;
private ActivityResultLauncher<Intent> mOpenFilePermissionsResult = null;
@SuppressLint("UnspecifiedRegisterReceiverFlag")
@Override
protected void onCreate(Bundle savedInstanceState) {
@ -80,14 +79,15 @@ public class MainActivity extends GameActivity {
}
// Clear cache on start.
String cacheDir = Objects.requireNonNull(getExternalCacheDir()).getPath();
if (savedInstanceState == null) {
Utils.deleteDirectoryContent(new File(getExternalCacheDir().getPath()), false);
Utils.deleteDirectoryContent(new File(cacheDir), 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", cacheDir, true);
Os.setenv("ARTI_FS_DISABLE_PERMISSION_CHECKS", "true", true);
} catch (ErrnoException e) {
throw new RuntimeException(e);
@ -95,54 +95,10 @@ public class MainActivity extends GameActivity {
super.onCreate(null);
// Register receiver to finish activity from the BackgroundService.
registerReceiver(mBroadcastReceiver, new IntentFilter(STOP_APP_ACTION));
// Register associated file opening result.
mOpenFilePermissionsResult = registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(),
result -> {
if (Build.VERSION.SDK_INT >= 30) {
if (Environment.isExternalStorageManager()) {
onFile();
}
} else if (result.getResultCode() == RESULT_OK) {
onFile();
}
}
);
// Register file pick result.
mFilePickResult = registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(),
result -> {
int resultCode = result.getResultCode();
Intent data = result.getData();
if (resultCode == Activity.RESULT_OK) {
String path = "";
if (data != null) {
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);
}
} catch (Exception e) {
e.printStackTrace();
}
path = file.getPath();
}
onFilePick(path);
} else {
onFilePick("");
}
});
ContextCompat.registerReceiver(this, mReceiver, new IntentFilter(STOP_APP_ACTION), ContextCompat.RECEIVER_NOT_EXPORTED);
// Listener for display insets (cutouts) to pass values into native code.
View content = getWindow().getDecorView().findViewById(android.R.id.content);
View content = findViewById(android.R.id.content).getRootView();
ViewCompat.setOnApplyWindowInsetsListener(content, (v, insets) -> {
// Get display cutouts.
DisplayCutoutCompat dc = insets.getDisplayCutout();
@ -171,7 +127,7 @@ public class MainActivity extends GameActivity {
return insets;
});
findViewById(android.R.id.content).post(() -> {
content.post(() -> {
// Request notifications permissions if needed.
if (Build.VERSION.SDK_INT >= 33) {
String notificationsPermission = Manifest.permission.POST_NOTIFICATIONS;
@ -193,6 +149,44 @@ public class MainActivity extends GameActivity {
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
switch (requestCode) {
case FILE_PICK_REQUEST:
if (Build.VERSION.SDK_INT >= 30) {
if (Environment.isExternalStorageManager()) {
onFile();
}
} else if (resultCode == RESULT_OK) {
onFile();
}
case FILE_PERMISSIONS_REQUEST:
if (resultCode == Activity.RESULT_OK) {
String path = "";
if (data != 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);
}
} catch (Exception e) {
e.printStackTrace();
}
path = file.getPath();
}
onFilePick(path);
} else {
onFilePick("");
}
}
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
@ -215,7 +209,7 @@ public class MainActivity extends GameActivity {
if (Build.VERSION.SDK_INT >= 30) {
if (!Environment.isExternalStorageManager()) {
Intent i = new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION);
mOpenFilePermissionsResult.launch(i);
startActivityForResult(i, FILE_PERMISSIONS_REQUEST);
return;
}
}
@ -269,42 +263,9 @@ public class MainActivity extends GameActivity {
}
}
@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());
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()));
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.
public native void onBack();
// Called from native code to exit app.
public void exit() {
finishAndRemoveTask();
@ -312,7 +273,6 @@ public class MainActivity extends GameActivity {
@Override
protected void onDestroy() {
unregisterReceiver(mBroadcastReceiver);
BackgroundService.stop(this);
// Kill process after 3 secs if app was terminated from recent apps to prevent app hang.
@ -342,14 +302,16 @@ 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;
@ -417,7 +379,7 @@ public class MainActivity extends GameActivity {
}
// Apply declared configs to CameraX using the same lifecycle owner
mCameraProvider.unbindAll();
mCameraProvider.bindToLifecycle(this, cameraSelector, mImageAnalysis);
// mCameraProvider.bindToLifecycle(this, cameraSelector, mImageAnalysis);
}
// Called from native code to stop camera.
@ -471,8 +433,8 @@ public class MainActivity extends GameActivity {
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.setType("*/*");
try {
mFilePickResult.launch(Intent.createChooser(intent, "Pick file"));
} catch (android.content.ActivityNotFoundException ex) {
startActivityForResult(Intent.createChooser(intent, "Pick file"), FILE_PICK_REQUEST);
} catch (ActivityNotFoundException ex) {
onFilePick("");
}
}

View file

@ -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();
}

View file

@ -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>

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
}

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\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
distributionPath=wrapper/dists
zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME

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

View file

@ -25,10 +25,13 @@ 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
wallets:
await_conf_amount: Erwarte Bestätigung
await_fin_amount: Warten auf die Fertigstellung
@ -83,6 +86,7 @@ wallets:
tx_canceled: Abgebrochen
tx_cancelling: Abbrechen
tx_finalizing: Finalisierung
tx_posting: Buchungsvorgang
tx_confirmed: Bestätigt
txs: Transaktionen
tx: Transaktion
@ -138,7 +142,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 +295,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: '/'

View file

@ -25,10 +25,13 @@ 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
wallets:
await_conf_amount: Awaiting confirmation
await_fin_amount: Awaiting finalization
@ -83,6 +86,7 @@ wallets:
tx_canceled: Canceled
tx_cancelling: Cancelling
tx_finalizing: Finalizing
tx_posting: Posting
tx_confirmed: Confirmed
txs: Transactions
tx: Transaction
@ -138,7 +142,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 +295,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: /

View file

@ -25,10 +25,13 @@ 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
wallets:
await_conf_amount: En attente de confirmation
await_fin_amount: En attente de finalisation
@ -83,6 +86,7 @@ wallets:
tx_canceled: Annulé
tx_cancelling: Annulation
tx_finalizing: Finalisation
tx_posting: Publication
tx_confirmed: Confirmé
txs: Transactions
tx: Transaction
@ -138,7 +142,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 +295,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: /

View file

@ -25,10 +25,13 @@ share: Поделиться
theme: 'Тема:'
dark: Тёмная
light: Светлая
file: Файл
choose_file: Выбрать файл
choose_folder: Выбрать папку
crash_report: Отчёт о сбое
crash_report_warning: В прошлый раз приложение неожиданно закрылось, вы можете поделиться отчетом о сбое с разработчиками.
confirmation: Подтверждение
enter_url: Введите URL-адрес
wallets:
await_conf_amount: Ожидает подтверждения
await_fin_amount: Ожидает завершения
@ -83,6 +86,7 @@ wallets:
tx_canceled: Отменено
tx_cancelling: Отмена
tx_finalizing: Завершение
tx_posting: Публикация
tx_confirmed: Подтверждено
txs: Транзакции
tx: Транзакция
@ -138,7 +142,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 +295,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: ё

View file

@ -25,10 +25,13 @@ 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
wallets:
await_conf_amount: Onay bekleniyor
await_fin_amount: Tamamlanma bekleniyor
@ -83,6 +86,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
@ -138,7 +142,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 +295,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: /

345
locales/zh-CN.yml Normal file
View file

@ -0,0 +1,345 @@
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
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_sending_tor: 通过 Tor 发送
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: 如果常规修复没有帮助,通过删除所有文件来恢复钱包.您将需要重新打开您的钱包.
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_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: /

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

View file

@ -38,7 +38,8 @@ 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:
@ -53,7 +54,7 @@ function build_lib() {
success=0
fi
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
}
@ -117,4 +118,4 @@ else
rm -rf android/app/src/main/jniLibs/*
[ $success -eq 1 ] && build_lib "x86"
[ $success -eq 1 ] && build_apk "x86_64" "$2"
fi
fi

View file

@ -12,29 +12,24 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use std::sync::atomic::{AtomicBool, Ordering};
use lazy_static::lazy_static;
use egui::{Align, Context, CursorIcon, Layout, Modifiers, ResizeDirection, Rounding, Stroke, UiBuilder, ViewportCommand};
use egui::epaint::{RectShape};
use egui::epaint::RectShape;
use egui::{Align, Context, CornerRadius, CursorIcon, LayerId, Layout, Modifiers, Order, ResizeDirection, Stroke, StrokeKind, UiBuilder, ViewportCommand};
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;
lazy_static! {
/// State to check if platform Back button was pressed.
static ref BACK_BUTTON_PRESSED: AtomicBool = AtomicBool::new(false);
}
use crate::gui::views::types::ContentContainer;
use crate::gui::views::{Content, KeyboardContent, Modal, TitlePanel, View};
use crate::gui::Colors;
use crate::AppConfig;
/// Implements ui entry point and contains platform-specific callbacks.
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.
@ -57,8 +52,6 @@ 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_visuals(ctx);
}
@ -70,13 +63,9 @@ impl<Platform: PlatformCallbacks> App<Platform> {
self.first_draw = false;
}
// 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)) {
// Handle Esc keyboard key event.
if ctx.input_mut(|i| i.consume_key(Modifiers::NONE, egui::Key::Escape)) {
self.content.on_back(&self.platform);
if back_pressed {
BACK_BUTTON_PRESSED.store(false, Ordering::Relaxed);
}
// Request repaint to update previous content.
ctx.request_repaint();
}
@ -127,13 +116,6 @@ impl<Platform: PlatformCallbacks> App<Platform> {
Self::title_panel_bg(ui, false);
self.content.ui(ui, &self.platform);
}
// 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);
}
}
});
// Check if desktop window was focused after requested attention.
@ -141,6 +123,23 @@ 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 custom desktop window frame content.
@ -154,9 +153,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::Middle);
// Draw content background.
ui.painter().add(content_bg);
@ -165,7 +165,7 @@ 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);
@ -208,7 +208,7 @@ impl<Platform: PlatformCallbacks> App<Platform> {
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);
}
@ -227,15 +227,15 @@ impl<Platform: PlatformCallbacks> App<Platform> {
};
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::Middle);
// Draw title background.
ui.painter().add(window_title_bg);
@ -261,22 +261,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,
@ -285,7 +270,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, |_| {

View file

@ -34,20 +34,20 @@ 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);
@ -231,7 +231,7 @@ impl Colors {
}
}
pub fn item_button() -> Color32 {
pub fn item_button_text() -> Color32 {
if use_dark() {
ITEM_BUTTON_DARK
} else {

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();

View file

@ -180,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();
@ -264,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
}

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;

View file

@ -50,7 +50,6 @@ 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) {
@ -78,13 +77,14 @@ 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.ctx().request_repaint();
}
/// Draw camera image.
@ -311,7 +311,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.

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")
@ -145,14 +124,37 @@ impl Content {
.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, cb: &dyn PlatformCallbacks) -> bool {
if Modal::on_back() {
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 {
@ -189,7 +191,7 @@ impl Content {
if !Node::is_running() {
self.exit_allowed = true;
cb.exit();
modal.close();
Modal::close();
}
ui.add_space(16.0);
ui.vertical_centered(|ui| {
@ -215,7 +217,7 @@ 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| {
@ -223,7 +225,7 @@ impl Content {
if !Node::is_running() {
self.exit_allowed = true;
cb.exit();
modal.close();
Modal::close();
} else {
Node::stop(true);
modal.disable_closing();
@ -237,129 +239,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,17 +251,14 @@ 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"))
@ -395,7 +273,7 @@ impl Content {
let _ = cb.share_data(name, data.as_bytes().to_vec());
}
Settings::delete_crash_report();
modal.close();
Modal::close();
});
});
ui.add_space(8.0);
@ -404,7 +282,7 @@ impl Content {
ui.vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
Settings::delete_crash_report();
modal.close();
Modal::close();
});
});
ui.add_space(6.0);

View file

@ -12,42 +12,58 @@
// 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, 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 file is picking.
pub file_picking: Arc<AtomicBool>,
file_picking: Arc<AtomicBool>,
/// 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,
file_picking: Arc::new(AtomicBool::new(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)) {
/// Do not parse file content.
pub fn no_parse(mut self) -> Self {
self.parse_file = false;
self
}
/// Draw content with provided callback to return path of the file.
pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks, on_pick: impl FnOnce(String)) {
if self.file_picking.load(Ordering::Relaxed) {
View::small_loading_spinner(ui);
// Check file pick result.
@ -70,7 +86,7 @@ impl FilePickButton {
r_res.clone().unwrap()
};
// Callback on result.
on_result(text);
on_pick(text);
// Clear result.
let mut w_res = self.file_parsing_result.write();
*w_res = None;
@ -78,12 +94,48 @@ 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 => {
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() {
if !self.parse_file {
on_pick(path);
return;
}
self.on_file_pick(path);
}
});
}
});
FilePickContentType::ItemButton(r) => {
View::item_button(ui, r, ARCHIVE_BOX, Some(Colors::blue()), || {
if let Some(path) = cb.pick_file() {
if !self.parse_file {
on_pick(path);
return;
}
self.on_file_pick(path);
}
});
}
FilePickContentType::Tab => {
let active = self.file_parsing.load(Ordering::Relaxed) ||
self.file_picking.load(Ordering::Relaxed);
View::tab_button(ui, ARCHIVE_BOX, Some(Colors::blue()), Some(active), |_| {
if let Some(path) = cb.pick_file() {
if !self.parse_file {
on_pick(path);
return;
}
self.on_file_pick(path);
}
});
}
}
}
}
@ -94,6 +146,10 @@ impl FilePickButton {
self.file_picking.store(true, Ordering::Relaxed);
return;
}
// Do not parse result.
if !self.parse_file {
return;
}
self.file_parsing.store(true, Ordering::Relaxed);
let result = self.file_parsing_result.clone();
thread::spawn(move || {

321
src/gui/views/input/edit.rs Normal file
View file

@ -0,0 +1,321 @@
// 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::{Layout, TextBuffer, TextStyle, Widget, Align};
use egui::text_edit::TextEditState;
use crate::gui::Colors;
use crate::gui::icons::{CLIPBOARD_TEXT, COPY, EYE, EYE_SLASH, SCAN};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::input::keyboard::KeyboardContent;
use crate::gui::views::{KeyboardEvent, View};
/// Text input content.
pub struct TextEdit {
/// View identifier.
id: egui::Id,
/// Check if horizontal centering is needed.
h_center: bool,
/// Check if focus is needed.
focus: bool,
/// Check if 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,
/// Callback when scan button was pressed.
pub scan_pressed: bool,
/// Callback when Enter key was pressed.
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 = 41.0;
pub fn new(id: egui::Id) -> Self {
Self {
id,
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: false,
}
}
/// Draw text input content.
pub fn ui(&mut self, ui: &mut egui::Ui, input: &mut String, 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);
}
// 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)
.id(self.id)
.font(TextStyle::Heading)
.min_size(edit_rect.size())
.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 {
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 key input.
if !self.focus_request {
if ui.ctx().input(|i| i.key_pressed(egui::Key::Enter)) {
self.enter_pressed = true;
}
}
if self.enter_pressed {
KeyboardContent::unshift();
}
if !self.no_soft_keyboard {
KeyboardContent::default().window_ui(self.numeric, ui.ctx());
}
}
});
});
}
/// 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 {
if let Some(input) = KeyboardContent::consume_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 input {
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
}
/// 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
}
}

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.screen_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).as_str(), locale = Self::input_locale().as_str())
} 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();
}
}

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::*;

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)),
}
}
}

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::*;

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, 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,8 @@ pub struct Modal {
closeable: Arc<AtomicBool>,
/// Title text.
title: Option<String>,
/// Flag to check first content render.
first_draw: Arc<AtomicBool>,
}
impl Modal {
@ -47,6 +50,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 +60,7 @@ impl Modal {
position: ModalPosition::Center,
closeable: Arc::new(AtomicBool::new(true)),
title: None,
first_draw: Arc::new(AtomicBool::new(true)),
}
}
@ -70,8 +76,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;
}
@ -106,20 +112,15 @@ impl Modal {
/// 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() {
Self::close();
return false;
}
true
@ -155,7 +156,6 @@ impl Modal {
/// Set title text for current opened [`Modal`].
pub fn set_title(title: String) {
// Save state.
let mut w_state = MODAL_STATE.write();
if w_state.modal.is_some() {
let mut modal = w_state.modal.clone().unwrap();
@ -164,8 +164,19 @@ impl 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,12 +185,15 @@ 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)
});
@ -219,7 +233,7 @@ impl Modal {
// 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)
@ -229,22 +243,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`].
@ -276,26 +294,29 @@ impl Modal {
}
/// 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);
}, Colors::fill(), Stroke::NONE, StrokeKind::Middle);
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.
@ -315,13 +336,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::Middle);
let bg_idx = ui.painter().add(bg_shape.clone());
// Draw title content.
let resp = ui.vertical_centered(|ui| {

View file

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use egui::{Align, Layout, RichText, Rounding};
use egui::{Align, Layout, RichText, CornerRadius, StrokeKind};
use crate::AppConfig;
use crate::gui::Colors;
@ -21,33 +21,32 @@ use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Modal, View};
use crate::gui::views::network::modals::ExternalConnectionModal;
use crate::gui::views::network::NodeSetup;
use crate::gui::views::types::{ModalContainer, ModalPosition};
use crate::gui::views::types::{ContentContainer, ModalPosition};
use crate::node::{Node, NodeConfig};
use crate::wallet::{ConnectionsConfig, ExternalConnection};
/// Network connections content.
pub struct ConnectionsContent {
/// Flag to check connections state on first draw.
first_draw: bool,
/// External connection [`Modal`] content.
ext_conn_modal: ExternalConnectionModal,
/// [`Modal`] identifiers allowed at this ui container.
modal_ids: Vec<&'static str>
}
impl Default for ConnectionsContent {
fn default() -> Self {
Self {
first_draw: true,
ext_conn_modal: ExternalConnectionModal::new(None),
modal_ids: vec![
ExternalConnectionModal::NETWORK_ID
],
}
}
}
impl ModalContainer for ConnectionsContent {
fn modal_ids(&self) -> &Vec<&'static str> {
&self.modal_ids
impl ContentContainer for ConnectionsContent {
fn modal_ids(&self) -> Vec<&'static str> {
vec![
ExternalConnectionModal::NETWORK_ID
]
}
fn modal_ui(&mut self,
@ -61,12 +60,13 @@ impl ModalContainer for ConnectionsContent {
_ => {}
}
}
}
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);
@ -96,7 +96,7 @@ 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);
@ -105,7 +105,7 @@ impl ConnectionsContent {
let ext_conn_size = ext_conn_list.len();
if ext_conn_size != 0 {
ui.add_space(8.0);
for (index, conn) in ext_conn_list.iter().filter(|c| !c.deleted).enumerate() {
for (index, conn) 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| {
@ -113,22 +113,24 @@ impl ConnectionsContent {
View::item_button(ui, button_rounding, TRASH, None, || {
ConnectionsConfig::remove_ext_conn(conn.id);
});
View::item_button(ui, Rounding::default(), PENCIL, None, || {
self.show_add_ext_conn_modal(Some(conn.clone()), cb);
View::item_button(ui, CornerRadius::default(), PENCIL, None, || {
self.show_add_ext_conn_modal(Some(conn.clone()));
});
});
});
}
}
}
}
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)) {
// 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());
ui.painter().rect(rect, rounding, Colors::fill(), View::item_stroke(), StrokeKind::Middle);
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
// Draw custom button.
@ -137,11 +139,11 @@ impl ConnectionsContent {
// 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()), || {
View::item_button(ui, CornerRadius::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()), || {
View::item_button(ui, CornerRadius::default(), POWER, Some(Colors::red()), || {
Node::stop(false);
});
}
@ -200,7 +202,11 @@ impl ConnectionsContent {
// 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.painter().rect(bg_rect,
item_rounding,
Colors::fill(),
View::item_stroke(),
StrokeKind::Middle);
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
// Draw provided buttons.
@ -234,15 +240,12 @@ impl ConnectionsContent {
}
/// Show [`Modal`] to add external connection.
pub fn show_add_ext_conn_modal(&mut self,
conn: Option<ExternalConnection>,
cb: &dyn PlatformCallbacks) {
pub fn show_add_ext_conn_modal(&mut self, conn: Option<ExternalConnection>) {
self.ext_conn_modal = ExternalConnectionModal::new(conn);
// Show modal.
Modal::new(ExternalConnectionModal::NETWORK_ID)
.position(ModalPosition::CenterTop)
.title(t!("wallets.add_node"))
.show();
cb.show_keyboard();
}
}

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,16 +57,16 @@ 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 {
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,
left: (View::get_left_inset() + View::TAB_ITEMS_PADDING) as i8,
right: (View::far_right_inset_margin(ui) + View::TAB_ITEMS_PADDING) as i8,
top: View::TAB_ITEMS_PADDING as i8,
bottom: (View::get_bottom_inset() + View::TAB_ITEMS_PADDING) as i8,
},
fill: Colors::fill(),
..Default::default()
@ -83,27 +88,43 @@ impl NetworkContent {
});
}
// 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() + 4.0) as i8,
right: (View::far_right_inset_margin(ui) + 4.0) 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 {
@ -114,11 +135,11 @@ impl NetworkContent {
Node::is_stopping() {
NetworkContent::loading_ui(ui, None);
} 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.
@ -143,14 +164,14 @@ impl NetworkContent {
View::get_left_inset() + 4.0
} else {
0.0
},
} as i8,
right: if show_connections {
View::far_right_inset_margin(ui) + 4.0
} else {
0.0
},
top: 3.0,
bottom: 4.0 + View::get_bottom_inset(),
} as i8,
top: 3.0 as i8,
bottom:(4.0 + View::get_bottom_inset()) as i8,
},
..Default::default()
})
@ -187,11 +208,16 @@ 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);
}
}
/// 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| {
@ -204,22 +230,26 @@ impl NetworkContent {
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,11 +259,15 @@ 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 title_content = if show_settings {
TitleContentType::Title(t!("settings"))
} else if !show_connections {
TitleContentType::WithSubTitle(title_text, subtitle_text, !not_syncing)
} else {
TitleContentType::Title(t!("network.connections"))
@ -241,16 +275,21 @@ impl NetworkContent {
// 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();
});

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::white_or_black(false),
View::item_stroke(),
StrokeKind::Middle);
// Draw block hash.
ui.horizontal(|ui| {

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::Middle);
ui.add_space(2.0);
ui.horizontal(|ui| {

View file

@ -14,16 +14,17 @@
use egui::{Id, RichText};
use url::Url;
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};
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,
/// Flag to check if content was just rendered.
first_draw: bool,
/// External connection URL value for [`Modal`].
ext_node_url_edit: String,
/// External connection API secret value for [`Modal`].
@ -34,8 +35,6 @@ pub struct ExternalConnectionModal {
ext_conn_id: Option<i64>,
}
impl ExternalConnectionModal {
/// Network [`Modal`] identifier.
pub const NETWORK_ID: &'static str = "net_ext_conn_modal";
@ -50,7 +49,7 @@ impl ExternalConnectionModal {
("".to_string(), "".to_string(), None)
};
Self {
first_modal_launch: true,
first_draw: true,
ext_node_url_edit,
ext_node_secret_edit,
ext_node_url_error: false,
@ -64,23 +63,54 @@ impl ExternalConnectionModal {
cb: &dyn PlatformCallbacks,
modal: &Modal,
on_save: impl Fn(ExternalConnection)) {
ui.add_space(6.0);
// Add connection button callback.
let on_add = |ui: &mut egui::Ui, m: &mut ExternalConnectionModal| {
let url = if !m.ext_node_url_edit.starts_with("http") {
format!("http://{}", m.ext_node_url_edit)
} else {
m.ext_node_url_edit.clone()
};
let error = Url::parse(url.trim()).is_err();
m.ext_node_url_error = error;
if !error {
let secret = if m.ext_node_secret_edit.is_empty() {
None
} else {
Some(m.ext_node_secret_edit.clone())
};
// Update or create new connection.
let mut ext_conn = ExternalConnection::new(url, secret);
if let Some(id) = m.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.
m.ext_node_url_edit = "".to_string();
m.ext_node_secret_edit = "".to_string();
m.ext_node_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;
}
View::text_edit(ui, cb, &mut self.ext_node_url_edit, &mut url_edit_opts);
ui.add_space(8.0);
let url_edit_id = Id::from(modal.id).with(self.ext_conn_id).with("node_url");
let mut url_edit = TextEdit::new(url_edit_id)
.paste()
.focus(self.first_draw);
url_edit.ui(ui, &mut self.ext_node_url_edit, cb);
ui.add_space(8.0);
ui.label(RichText::new(t!("wallets.node_secret"))
.size(17.0)
.color(Colors::gray()));
@ -88,8 +118,17 @@ impl ExternalConnectionModal {
// 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 mut secret_edit = TextEdit::new(secret_edit_id)
.password()
.paste()
.focus(false);
if url_edit.enter_pressed {
secret_edit.focus_request();
}
secret_edit.ui(ui, &mut self.ext_node_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 {
@ -113,61 +152,22 @@ impl ExternalConnectionModal {
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();
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() {
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;
}
}

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, rounding: 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, rounding, Colors::fill_lite(), View::item_stroke(), StrokeKind::Middle);
// Draw IP address.
ui.horizontal(|ui| {

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();
});
});
});

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,30 +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();
}
};
// Continue on Enter key press.
View::on_enter_key(ui, || {
on_save();
});
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);
@ -215,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())
@ -230,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"))
@ -245,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() {
@ -265,30 +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();
}
};
// Continue on Enter key press.
View::on_enter_key(ui, || {
on_save();
});
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);
@ -296,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())
@ -312,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"))
@ -327,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() {
@ -347,30 +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();
}
};
// Continue on Enter key press.
View::on_enter_key(ui, || {
on_save();
});
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);
@ -378,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())
@ -394,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"))
@ -409,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() {
@ -429,30 +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();
}
};
// Continue on Enter key press.
View::on_enter_key(ui, || {
on_save();
});
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);

View file

@ -15,15 +15,15 @@
use egui::{Id, RichText};
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::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::{Modal, TextEdit, View};
use crate::gui::Colors;
use crate::node::{Node, NodeConfig};
use crate::AppConfig;
/// Integrated node general setup section content.
pub struct NodeSetup {
@ -43,9 +43,6 @@ pub struct NodeSetup {
/// Future Time Limit value.
ftl_edit: String,
/// [`Modal`] identifiers allowed at this ui container.
modal_ids: Vec<&'static str>
}
/// Identifier for API port value [`Modal`].
@ -68,19 +65,18 @@ impl Default for NodeSetup {
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![
API_PORT_MODAL,
API_SECRET_MODAL,
FOREIGN_API_SECRET_MODAL,
FTL_MODAL
]
}
fn modal_ui(&mut self,
@ -95,13 +91,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, _: &dyn PlatformCallbacks) {
View::sub_title(ui, format!("{} {}", COMPUTER_TOWER, t!("network_settings.server")));
View::horizontal_line(ui, Colors::stroke());
ui.add_space(6.0);
@ -185,12 +176,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 +192,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,7 +209,9 @@ impl NodeSetup {
self.archive_mode_ui(ui);
});
}
}
impl NodeSetup {
/// Draw [`ChainTypes`] setup content.
pub fn chain_type_ui(ui: &mut egui::Ui) {
ui.vertical_centered(|ui| {
@ -250,7 +243,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 +258,6 @@ impl NodeSetup {
.position(ModalPosition::CenterTop)
.title(t!("network_settings.change_value"))
.show();
cb.show_keyboard();
});
ui.add_space(6.0);
@ -281,6 +273,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 +297,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,41 +320,16 @@ impl NodeSetup {
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
// Save button callback.
let mut 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();
}
};
// Continue on Enter key press.
View::on_enter_key(ui, || {
on_save();
});
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);
@ -351,7 +337,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")
@ -381,12 +367,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 {
@ -397,8 +395,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.
@ -417,35 +421,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();
};
// Continue on Enter key press.
View::on_enter_key(ui, || {
on_save();
});
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);
@ -453,7 +438,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())
@ -471,7 +456,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"))
@ -482,6 +466,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"))
@ -490,8 +482,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() {
@ -510,30 +505,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();
}
};
// Continue on Enter key press.
View::on_enter_key(ui, || {
on_save();
});
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);

View file

@ -12,16 +12,16 @@
// 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 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.
@ -65,9 +65,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`].
@ -111,23 +108,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 +142,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 +168,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 +179,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,7 +190,7 @@ 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);
@ -210,21 +198,21 @@ impl P2PSetup {
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 +224,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())
@ -257,7 +250,6 @@ impl P2PSetup {
.position(ModalPosition::CenterTop)
.title(t!("network_settings.change_value"))
.show();
cb.show_keyboard();
});
ui.add_space(6.0);
@ -273,6 +265,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 +289,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,39 +310,17 @@ impl P2PSetup {
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
// Save button callback.
let mut 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();
}
};
// Continue on Enter key press.
View::on_enter_key(ui, || {
on_save();
});
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);
@ -340,7 +329,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 {
@ -402,14 +391,13 @@ impl P2PSetup {
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()
_ => 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);
@ -417,6 +405,27 @@ impl P2PSetup {
/// Draw peer creation [`Modal`] content.
fn peer_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
let on_save = |c: &mut P2PSetup| {
// Check if peer is correct and/or available.
let peer = c.peer_edit.clone();
let is_correct_address = PeersConfig::peer_to_addr(peer.clone()).is_some();
c.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),
&_ => {}
}
c.is_port_available = true;
Modal::close();
}
};
ui.add_space(6.0);
ui.vertical_centered(|ui| {
let label_text = match modal.id {
@ -427,8 +436,11 @@ 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();
edit.ui(ui, &mut self.peer_edit, cb);
if edit.enter_pressed {
on_save(self);
}
// Show error when specified address is incorrect.
if !self.is_correct_address_edit {
@ -444,44 +456,17 @@ impl P2PSetup {
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
// Save button callback.
let mut 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();
}
};
// Continue on Enter key press.
View::on_enter_key(ui, || {
on_save();
});
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);
@ -490,9 +475,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();
@ -506,11 +490,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())
@ -528,7 +512,6 @@ impl P2PSetup {
.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.ban_window_desc"))
@ -540,6 +523,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"))
@ -548,8 +538,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() {
@ -568,30 +561,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();
}
};
// Continue on Enter key press.
View::on_enter_key(ui, || {
on_save();
});
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);
@ -599,7 +578,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())
@ -617,13 +596,19 @@ impl P2PSetup {
.position(ModalPosition::CenterTop)
.title(t!("network_settings.change_value"))
.show();
cb.show_keyboard();
});
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();
}
};