Compare commits

...

109 commits

Author SHA1 Message Date
1d9b7d9698 wallet: do not lock whole balance on send
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-01-14 17:55:50 +03:00
82c05588bc readme: update title
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-01-13 21:59:22 +03:00
1cddd05bc0 readme: update img tag
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-01-13 21:58:29 +03:00
8ad0d1c461 readme: update images
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-01-13 21:56:48 +03:00
a22a75913c img: add grin logo
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-01-13 21:55:55 +03:00
e797da0ed8 img: add cover
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-01-13 21:26:00 +03:00
6936c14ed2 tor: remove macos tls fix
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-01-13 21:06:34 +03:00
c626ed5a48 tor: clear data on launch, update arti to 0.26.0
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-01-13 19:40:09 +03:00
d79d05ef5a android: debug build without keystore 2025-01-13 16:54:27 +03:00
ardocrat
094a5b8969 release: v0.2.3 2024-10-27 20:12:12 +03:00
ardocrat
12a75f8370 macos: future version update 2024-10-27 19:45:00 +03:00
ardocrat
1c14b9aa93 tx: fix confirmation status for new block, do not show Slatepack message after finalization 2024-10-27 19:02:17 +03:00
ardocrat
8ea388554a github: macos target 11.0 2024-10-27 18:07:22 +03:00
ardocrat
1531c201bb github: macos 12 2024-10-27 00:46:53 +03:00
ardocrat
ed522c56ae github: macos zig linker 2024-10-27 00:40:58 +03:00
ardocrat
4b454ab2f3 github: macos last os 2024-10-27 00:29:27 +03:00
ardocrat
f6fbf7226e fix: window size saving 2024-10-26 23:54:47 +03:00
ardocrat
ebd09ab1c8 camera: update nokhwa, eye for macos, ability to switch camera when another camera not loaded 2024-10-26 23:25:55 +03:00
ardocrat
75cf7edc96 fix: modal padding and window border on desktop 2024-10-26 23:23:39 +03:00
ardocrat
5c8b9c40be build: provide version for android release 2024-10-26 02:17:28 +03:00
ardocrat
dcaf9945c8 ui: wgpu renderer for macos, desktop content background fix, do not show left line for camera content at dual panel mode 2024-10-26 02:16:47 +03:00
ardocrat
f9426287d5 macos: release on darwin without zig, info.plist camera usage description and version update 2024-10-25 20:03:57 +03:00
ardocrat
77281e3ab9 github: fix macos arm sdk 2024-10-23 00:37:41 +03:00
ardocrat
64439ad3d3 github: fix macos deployment target 2024-10-23 00:11:45 +03:00
ardocrat
9494c1292e github: macos coreutils 2024-10-22 23:47:19 +03:00
ardocrat
accf123d49 github: macos build 2024-10-22 23:24:02 +03:00
ardocrat
d77598c259 github: fix macos sdk env 2024-10-22 04:39:08 +03:00
ardocrat
4e6dff52fe github: macos install zig 2024-10-22 04:14:21 +03:00
ardocrat
92d0aac250 github: fix macos sdk unzip 2024-10-22 04:08:26 +03:00
ardocrat
5ef310558a release: v0.2.2 2024-10-22 03:51:01 +03:00
ardocrat
683821b667 build: fix version script 2024-10-22 03:50:48 +03:00
ardocrat
da4cf71fac github: build macos on linux with SDK 10.15 2024-10-22 03:19:34 +03:00
ardocrat
f81ceae940 txs: new block confirmation time 2024-10-22 02:11:25 +03:00
ardocrat
fa6301a1db stratum: fix wallet name after selection, do not panic after stop 2024-10-22 00:12:13 +03:00
ardocrat
442fc425f7 ui: update to egui 0.29.1, wallet qr scan content, panels strokes and colors refactoring, check closeable modal at desktop title, fix app socket name 2024-10-21 12:03:09 +03:00
ardocrat
ea61588ede build: check android lib result 2024-10-12 19:58:14 +03:00
ardocrat
7f67aa134a build: increment on android development 2024-10-12 15:33:23 +03:00
ardocrat
d7d1c53c52 build: incremental release on desktop development 2024-10-12 15:26:01 +03:00
ardocrat
18f52f877a node: remove delay after server start 2024-10-12 15:24:15 +03:00
ardocrat
c13195bd61 stratum: prevent crash at connections thread 2024-10-10 21:51:28 +03:00
ardocrat
e40d5b6474 node: single function to get api secrets 2024-10-10 21:11:50 +03:00
ardocrat
92e5d38755 build: update grin 5.3.3, arti 0.23.0 (fork arti-hyper crate) and non-egui dependencies 2024-10-09 12:58:59 +03:00
ardocrat
ec7e795ba9 build: camera features 2024-10-09 10:13:55 +03:00
ardocrat
af220b2a09 camera: remove eye-rs to fix build for mac, horizontally flip image 2024-10-08 23:23:04 +03:00
ardocrat
846e30cb38 app: better panic handling, macos single app instance 2024-10-08 17:11:45 +03:00
ardocrat
d371d4368b wallet: disable tor listener by default 2024-10-08 14:59:51 +03:00
ardocrat
85fc8101e4 ui: show tx modal on error if exists 2024-10-08 02:37:51 +03:00
ardocrat
e2f58a8938 android: update gradle 2024-10-07 20:55:23 +03:00
ardocrat
7e6954afd9 fix: opened file data providing 2024-10-07 19:45:29 +03:00
ardocrat
bed041a1c3 git: ignore android release artifacts 2024-09-21 00:33:46 +03:00
ardocrat
f955f720d2 release: v0.2.1 2024-09-20 23:33:08 +03:00
ardocrat
b627ac1ca6 fix: mnemonic import 2024-09-20 23:30:41 +03:00
ardocrat
ac0b218376 fix: connection selection 2024-09-20 23:12:44 +03:00
ardocrat
04bf5a5349 github: coreutils for macos 2024-09-20 21:46:17 +03:00
ardocrat
9cce52a7d9 github: fix sha256sum 2024-09-20 20:38:05 +03:00
ardocrat
51e0d87d27 github: fix release 2024-09-20 15:17:41 +03:00
ardocrat
d6f7e2e976 github: release sha256sum 2024-09-20 15:15:18 +03:00
ardocrat
0bbf395a62 build: android warning fix 2024-09-20 15:03:56 +03:00
ardocrat
609d7ceb7a build: remove panic message dependency 2024-09-20 14:45:40 +03:00
ardocrat
b91605864d github: fix macos release 2024-09-20 14:42:37 +03:00
ardocrat
7857b708c9 release: v0.2.0 2024-09-20 14:17:03 +03:00
ardocrat
a0f85538e9 ui: tx modal height 2024-09-20 14:09:53 +03:00
ardocrat
c52da4f479 wallet: accounts balance calculating optimization, payment proof support on send, selection_strategy_is_use_all 2024-09-20 13:56:25 +03:00
ardocrat
af597df7b1 i18n: move confirmation word 2024-09-20 13:49:31 +03:00
ardocrat
2adb29f4ee ui: external connection check and ui repaint fix, tab button callback argument 2024-09-20 13:42:45 +03:00
ardocrat
2b83944f34 ui: show node error status on connection item 2024-09-20 11:10:05 +03:00
ardocrat
71e80f6df7 ui: reset node config from ui on error 2024-09-20 10:58:52 +03:00
ardocrat
0ead11ec6c tx: receiver address 2024-09-20 02:39:06 +03:00
ardocrat
3e249c5314 android: share file type 2024-09-20 00:16:12 +03:00
ardocrat
bacc87945c messages: qr scan modal 2024-09-20 00:09:08 +03:00
ardocrat
2cfd428c4c ui: do not clear qr state 2024-09-19 21:39:59 +03:00
ardocrat
c155deedb5 wallet: qr scan modal, connections content and default list, wallet creation and list refactoring, tx height 2024-09-19 15:56:53 +03:00
Ardocrat
3bc8c407b4
Merge pull request #13 from ardocrat/slatepack_ext_file
Open .slatepack file with the app
2024-09-16 16:08:27 +00:00
ardocrat
c3fae38d5c desktop: open camera check 2024-09-15 15:54:07 +03:00
ardocrat
d6ec4213ab ui: ability to finalize tx only when wallet is loaded 2024-09-14 21:21:03 +03:00
ardocrat
150a0de1c4 android: always build with release-apk profile 2024-09-14 21:17:43 +03:00
ardocrat
7cedebc70e ui: qr scan and accounts modals module, parsing messages fix 2024-09-14 21:11:52 +03:00
ardocrat
fe5aca6f0e build: remove debug from release profile 2024-09-14 16:08:40 +03:00
ardocrat
5d83710fed ui: dark colors fix 2024-09-14 16:02:20 +03:00
ardocrat
1431e307ee ui: separate wallet accounts modal 2024-09-14 15:21:08 +03:00
ardocrat
1934dc3377 desktop: args text 2024-09-14 15:04:11 +03:00
ardocrat
8af06d8860 build: android fix 2024-09-14 13:07:48 +03:00
ardocrat
9ea0da95b7 build: release sha256sum 2024-09-14 12:12:50 +03:00
ardocrat
d39e2ec21e build: android signed release 2024-09-14 02:06:35 +03:00
ardocrat
68c9c9df04 build: local android release 2024-09-14 01:47:06 +03:00
ardocrat
6f7156ef17 github: android secrets 2024-09-13 22:31:28 +03:00
ardocrat
50638ff54e github: android keystore 2024-09-13 22:00:59 +03:00
ardocrat
8594279b98 android: java call result fixes 2024-09-13 21:08:14 +03:00
ardocrat
0205e01b3c build: macos fix 2024-09-13 19:51:33 +03:00
ardocrat
17545c1b7c macos: platform build 2024-09-13 18:57:09 +03:00
ardocrat
bcf821c06a macos: initial file type association 2024-09-13 15:21:43 +03:00
ardocrat
34376d3490 build: fix macos 2024-09-13 14:56:04 +03:00
ardocrat
8ed2308340 macos: build, warn fix 2024-09-13 14:53:22 +03:00
ardocrat
c73cd58eed platform: android file opening, better exit 2024-09-13 14:22:15 +03:00
ardocrat
d78ec570b0 platform: passed data at lib, desktop user attention, check existing file on share at android 2024-09-12 21:27:37 +03:00
ardocrat
dd45f7ce38 desktop: platform socket fix, file extension association for windows 2024-09-12 18:02:02 +03:00
ardocrat
fb7312cb80 desktop: request window focus on data 2024-09-11 21:13:52 +03:00
ardocrat
dbc28205e8 desktop: parse file content from argument on launch, single app instance, wallets selection and opening modals refactoring 2024-09-11 17:01:05 +03:00
ardocrat
a3ed3bd234 build: linux release 2024-09-07 12:45:05 +03:00
ardocrat
21ecf200b8 wallet + ui: optimize sync after tx actions, remove tx repost, share message as file from tx modal, show tx info after tor sending and message creation or finalization, messages and transport modules refactoring, qr code text optimization, wallet dandelion setting, recovery phrase modal next step on enter 2024-09-07 00:11:17 +03:00
ardocrat
c8bca08bdc txs: share message as file from modal, module refactoring 2024-08-15 23:09:42 +03:00
ardocrat
68bd2b81ec peers: fix config edit and load, default mainnet dnsseed 2024-08-13 02:31:38 +03:00
ardocrat
09cfb84b94 fix: ellipsized sync status text at connections 2024-08-12 18:30:10 +03:00
ardocrat
5c1ffb5636 build: push version 2024-08-10 12:15:40 +03:00
ardocrat
7f79cc0708 release: v0.1.3 2024-08-10 12:08:20 +03:00
ardocrat
b0b4f9068a build: version release 2024-08-10 11:59:12 +03:00
ardocrat
cb9e86750c mnemonic: words import and errors check refactoring 2024-08-10 02:35:42 +03:00
ardocrat
86fbf2e14f github: fix android build 2024-08-08 03:08:10 +03:00
ardocrat
e0351cea84 fix: mnemonic words size on creation, wallet creation errors 2024-08-08 03:01:08 +03:00
107 changed files with 10174 additions and 9499 deletions

View file

@ -2,43 +2,6 @@ name: Build
on: [push, pull_request] on: [push, pull_request]
jobs: jobs:
android:
name: Android Build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
cache: gradle
- name: Setup build
run: |
cargo install cargo-ndk
rustup target add aarch64-linux-android
rustup target add armv7-linux-androideabi
rustup target add x86_64-linux-android
- name: Setup Java build
run: |
chmod +x android/gradlew
echo "${{ secrets.ANDROID_RELEASE_KEYSTORE }}" > release.keystore.asc
gpg -d --passphrase "${{ secrets.ANDROID_RELEASE_SECRET }}" --batch release.keystore.asc > android/keystore
echo -e "storePassword=${{ secrets.ANDROID_PASS }}\nkeyPassword=${{ secrets.ANDROID_PASS }}\nkeyAlias=grim\nstoreFile=../keystore" > android/keystore.properties
- name: Build lib 1/2
continue-on-error: true
run: |
sed -i -e 's/"rlib"/"cdylib","rlib"/g' Cargo.toml
export CPPFLAGS="-DMDB_USE_ROBUST=0" && export CFLAGS="-DMDB_USE_ROBUST=0" && cargo ndk -t arm64-v8a build --profile release-apk
- name: Build lib 2/2
run: |
unset CPPFLAGS && unset CFLAGS && cargo ndk -t arm64-v8a -o android/app/src/main/jniLibs build --profile release-apk
sed -i -e 's/"cdylib","rlib"/"rlib"/g' Cargo.toml
- name: Build APK
working-directory: android
run: |
./gradlew assembleRelease
linux: linux:
name: Linux Build name: Linux Build
runs-on: ubuntu-latest runs-on: ubuntu-latest

View file

@ -6,101 +6,6 @@ on:
- "v*.*.*" - "v*.*.*"
jobs: jobs:
android_release:
name: Android Release
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
cache: gradle
- name: Setup Rust build
run: |
cargo install cargo-ndk
rustup target add aarch64-linux-android
rustup target add armv7-linux-androideabi
rustup target add x86_64-linux-android
- name: Setup Java build
run: |
chmod +x android/gradlew
echo "${{ secrets.ANDROID_RELEASE_KEYSTORE }}" > release.keystore.asc
gpg -d --passphrase "${{ secrets.ANDROID_RELEASE_SECRET }}" --batch release.keystore.asc > android/keystore
echo -e "storePassword=${{ secrets.ANDROID_PASS }}\nkeyPassword=${{ secrets.ANDROID_PASS }}\nkeyAlias=grim\nstoreFile=../keystore" > android/keystore.properties
- name: Build lib ARMv8 1/2
continue-on-error: true
run: |
sed -i -e 's/"rlib"/"cdylib","rlib"/g' Cargo.toml
export CPPFLAGS="-DMDB_USE_ROBUST=0" && export CFLAGS="-DMDB_USE_ROBUST=0" && cargo ndk -t arm64-v8a build --profile release-apk
- name: Build lib ARMv8 2/2
run: |
unset CPPFLAGS && unset CFLAGS && cargo ndk -t arm64-v8a -o android/app/src/main/jniLibs build --profile release-apk
sed -i -e 's/"cdylib","rlib"/"rlib"/g' Cargo.toml
- name: Build APK ARMv8
working-directory: android
run: |
./gradlew assembleRelease
mv app/build/outputs/apk/release/app-release.apk grim-${{ github.ref_name }}-android-armv8.apk
- name: Checksum APK ARMv8
working-directory: android
shell: pwsh
run: get-filehash -algorithm sha256 grim-${{ github.ref_name }}-android-armv8.apk | Format-List | Out-String | ForEach-Object { $_.Trim() } > grim-${{ github.ref_name }}-android-armv8-sha256sum.txt
- name: Build lib ARMv7 1/2
continue-on-error: true
run: |
sed -i -e 's/"rlib"/"cdylib","rlib"/g' Cargo.toml
export CPPFLAGS="-DMDB_USE_ROBUST=0" && export CFLAGS="-DMDB_USE_ROBUST=0" && cargo ndk -t armeabi-v7a build --profile release-apk
- name: Build lib ARMv7 2/2
run: |
unset CPPFLAGS && unset CFLAGS && cargo ndk -t armeabi-v7a -o android/app/src/main/jniLibs build --profile release-apk
sed -i -e 's/"cdylib","rlib"/"rlib"/g' Cargo.toml
- name: Build APK ARMv7
working-directory: android
run: |
rm -rf app/build
rm -rf app/src/main/jniLibs/*
./gradlew assembleRelease
mv app/build/outputs/apk/release/app-release.apk grim-${{ github.ref_name }}-android-armv7.apk
- name: Checksum APK ARMv7
working-directory: android
shell: pwsh
run: get-filehash -algorithm sha256 grim-${{ github.ref_name }}-android-armv7.apk | Format-List | Out-String | ForEach-Object { $_.Trim() } > grim-${{ github.ref_name }}-android-armv7-sha256sum.txt
- name: Build lib x86 1/2
continue-on-error: true
run: |
sed -i -e 's/"rlib"/"cdylib","rlib"/g' Cargo.toml
export CPPFLAGS="-DMDB_USE_ROBUST=0" && export CFLAGS="-DMDB_USE_ROBUST=0" && cargo ndk -t x86_64 build --profile release-apk
- name: Build lib x86 2/2
run: |
unset CPPFLAGS && unset CFLAGS && cargo ndk -t x86_64 -o android/app/src/main/jniLibs build --profile release-apk
sed -i -e 's/"cdylib","rlib"/"rlib"/g' Cargo.toml
- name: Build APK x86
working-directory: android
run: |
rm -rf app/build
rm -rf app/src/main/jniLibs/*
./gradlew assembleRelease
mv app/build/outputs/apk/release/app-release.apk grim-${{ github.ref_name }}-android-x86_64.apk
- name: Checksum APK x86
working-directory: android
shell: pwsh
run: get-filehash -algorithm sha256 grim-${{ github.ref_name }}-android-x86_64.apk | Format-List | Out-String | ForEach-Object { $_.Trim() } > grim-${{ github.ref_name }}-android-x86_64-sha256sum.txt
- name: Release
uses: softprops/action-gh-release@v1
with:
files: |
android/grim-${{ github.ref_name }}-android-armv8.apk
android/grim-${{ github.ref_name }}-android-armv8-sha256sum.txt
android/grim-${{ github.ref_name }}-android-armv7.apk
android/grim-${{ github.ref_name }}-android-armv7-sha256sum.txt
android/grim-${{ github.ref_name }}-android-x86_64.apk
android/grim-${{ github.ref_name }}-android-x86_64-sha256sum.txt
linux_release: linux_release:
name: Linux Release name: Linux Release
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -132,16 +37,16 @@ jobs:
./appimagetool-x86_64.AppImage linux/Grim.AppDir target/x86_64-unknown-linux-gnu/release/grim-${{ github.ref_name }}-linux-x86_64.AppImage ./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 - name: Checksum AppImage x86
working-directory: target/x86_64-unknown-linux-gnu/release working-directory: target/x86_64-unknown-linux-gnu/release
shell: pwsh shell: bash
run: get-filehash -algorithm sha256 grim-${{ github.ref_name }}-linux-x86_64.AppImage | Format-List | Out-String | ForEach-Object { $_.Trim() } > grim-${{ github.ref_name }}-linux-x86_64-appimage-sha256sum.txt run: sha256sum grim-${{ github.ref_name }}-linux-x86_64.AppImage > grim-${{ github.ref_name }}-linux-x86_64-appimage-sha256sum.txt
- name: AppImage ARM - name: AppImage ARM
run: | run: |
cp target/aarch64-unknown-linux-gnu/release/grim linux/Grim.AppDir/AppRun 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 ./appimagetool-x86_64.AppImage linux/Grim.AppDir target/aarch64-unknown-linux-gnu/release/grim-${{ github.ref_name }}-linux-arm.AppImage
- name: Checksum AppImage ARM - name: Checksum AppImage ARM
working-directory: target/aarch64-unknown-linux-gnu/release working-directory: target/aarch64-unknown-linux-gnu/release
shell: pwsh shell: bash
run: get-filehash -algorithm sha256 grim-${{ github.ref_name }}-linux-arm.AppImage | Format-List | Out-String | ForEach-Object { $_.Trim() } > grim-${{ github.ref_name }}-linux-arm-appimage-sha256sum.txt run: sha256sum grim-${{ github.ref_name }}-linux-arm.AppImage > grim-${{ github.ref_name }}-linux-arm-appimage-sha256sum.txt
- name: Release - name: Release
uses: softprops/action-gh-release@v1 uses: softprops/action-gh-release@v1
with: with:
@ -168,16 +73,16 @@ jobs:
dest: target/release/grim-${{ github.ref_name }}-win-x86_64.zip dest: target/release/grim-${{ github.ref_name }}-win-x86_64.zip
- name: Checksum release - name: Checksum release
working-directory: target/release working-directory: target/release
shell: pwsh shell: bash
run: get-filehash -algorithm sha256 grim-${{ github.ref_name }}-win-x86_64.zip | Format-List | Out-String | ForEach-Object { $_.Trim() } > grim-${{ github.ref_name }}-win-x86_64-sha256sum.txt run: sha256sum grim-${{ github.ref_name }}-win-x86_64.zip > grim-${{ github.ref_name }}-win-x86_64-sha256sum.txt
- name: Install cargo-wix - name: Install cargo-wix
run: cargo install cargo-wix run: cargo install cargo-wix
- name: Run cargo-wix - name: Run cargo-wix
run: cargo wix -p grim -o ./target/wix/grim-${{ github.ref_name }}-win-x86_64.msi --nocapture run: cargo wix -p grim -o ./target/wix/grim-${{ github.ref_name }}-win-x86_64.msi --nocapture
- name: Checksum msi - name: Checksum msi
working-directory: target/wix working-directory: target/wix
shell: pwsh shell: bash
run: get-filehash -algorithm sha256 grim-${{ github.ref_name }}-win-x86_64.msi | Format-List | Out-String | ForEach-Object { $_.Trim() } > grim-${{ github.ref_name }}-win-x86_64-msi-sha256sum.txt run: sha256sum grim-${{ github.ref_name }}-win-x86_64.msi > grim-${{ github.ref_name }}-win-x86_64-msi-sha256sum.txt
- name: Release - name: Release
uses: softprops/action-gh-release@v1 uses: softprops/action-gh-release@v1
with: with:
@ -189,23 +94,30 @@ jobs:
macos_release: macos_release:
name: MacOS Release name: MacOS Release
runs-on: macos-latest runs-on: macos-12
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Install coreutils
run: brew install coreutils
- name: Zig Setup - name: Zig Setup
uses: goto-bus-stop/setup-zig@v2 uses: goto-bus-stop/setup-zig@v2
with: with:
version: 0.12.1 version: 0.12.1
- name: Install cargo-zigbuild - name: Install cargo-zigbuild
run: cargo 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 - name: Release x86
run: | run: |
rustup target add x86_64-apple-darwin rustup target add x86_64-apple-darwin
cargo zigbuild --release --target x86_64-apple-darwin cargo zigbuild --release --target x86_64-apple-darwin
mkdir macos/Grim.app/Contents/MacOS
yes | cp -rf target/x86_64-apple-darwin/release/grim macos/Grim.app/Contents/MacOS yes | cp -rf target/x86_64-apple-darwin/release/grim macos/Grim.app/Contents/MacOS
- name: Archive x86 - name: Archive x86
run: | run: |
@ -215,8 +127,8 @@ jobs:
cd .. cd ..
- name: Checksum Release x86 - name: Checksum Release x86
working-directory: target/x86_64-apple-darwin/release working-directory: target/x86_64-apple-darwin/release
shell: pwsh shell: bash
run: get-filehash -algorithm sha256 grim-${{ github.ref_name }}-macos-x86_64.zip | Format-List | Out-String | ForEach-Object { $_.Trim() } > grim-${{ github.ref_name }}-macos-x86_64-sha256sum.txt run: sha256sum grim-${{ github.ref_name }}-macos-x86_64.zip > grim-${{ github.ref_name }}-macos-x86_64-sha256sum.txt
- name: Release ARM - name: Release ARM
run: | run: |
rustup target add aarch64-apple-darwin rustup target add aarch64-apple-darwin
@ -230,12 +142,10 @@ jobs:
cd .. cd ..
- name: Checksum Release ARM - name: Checksum Release ARM
working-directory: target/aarch64-apple-darwin/release working-directory: target/aarch64-apple-darwin/release
shell: pwsh shell: bash
run: get-filehash -algorithm sha256 grim-${{ github.ref_name }}-macos-arm.zip | Format-List | Out-String | ForEach-Object { $_.Trim() } > grim-${{ github.ref_name }}-macos-arm-sha256sum.txt run: sha256sum grim-${{ github.ref_name }}-macos-arm.zip > grim-${{ github.ref_name }}-macos-arm-sha256sum.txt
- name: Release Universal - name: Release Universal
run: | run: |
rustup target add aarch64-apple-darwin
rustup target add x86_64-apple-darwin
cargo zigbuild --release --target universal2-apple-darwin cargo zigbuild --release --target universal2-apple-darwin
yes | cp -rf target/universal2-apple-darwin/release/grim macos/Grim.app/Contents/MacOS yes | cp -rf target/universal2-apple-darwin/release/grim macos/Grim.app/Contents/MacOS
- name: Archive Universal - name: Archive Universal
@ -247,7 +157,7 @@ jobs:
- name: Checksum Release Universal - name: Checksum Release Universal
working-directory: target/universal2-apple-darwin/release working-directory: target/universal2-apple-darwin/release
shell: pwsh shell: pwsh
run: get-filehash -algorithm sha256 grim-${{ github.ref_name }}-macos-universal.zip | Format-List | Out-String | ForEach-Object { $_.Trim() } > grim-${{ github.ref_name }}-macos-universal-sha256sum.txt run: sha256sum grim-${{ github.ref_name }}-macos-universal.zip > grim-${{ github.ref_name }}-macos-universal-sha256sum.txt
- name: Release - name: Release
uses: softprops/action-gh-release@v1 uses: softprops/action-gh-release@v1
with: with:

4
.gitignore vendored
View file

@ -1,9 +1,12 @@
*.iml *.iml
android/.idea
android/.gradle android/.gradle
android/local.properties android/local.properties
android/keystore android/keystore
android/keystore.asc android/keystore.asc
android/keystore.properties android/keystore.properties
android/*.apk
android/*sha256sum.txt
/.idea /.idea
.DS_Store .DS_Store
/captures /captures
@ -13,7 +16,6 @@ android/keystore.properties
target target
.cargo/ .cargo/
app/src/main/jniLibs app/src/main/jniLibs
macos/Grim.app/Contents/MacOS/grim
macos/cert.pem macos/cert.pem
linux/Grim.AppDir/AppRun linux/Grim.AppDir/AppRun
.intentionally-empty-file.o .intentionally-empty-file.o

4427
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
[package] [package]
name = "grim" name = "grim"
version = "0.1.0" version = "0.2.3"
authors = ["Ardocrat <ardocrat@proton.me>"] authors = ["Ardocrat <ardocrat@proton.me>"]
description = "Cross-platform GUI for Grin with focus on usability and availability to be used by anyone, anywhere." description = "Cross-platform GUI for Grin with focus on usability and availability to be used by anyone, anywhere."
license = "Apache-2.0" license = "Apache-2.0"
@ -16,9 +16,6 @@ path = "src/main.rs"
name="grim" name="grim"
crate-type = ["rlib"] crate-type = ["rlib"]
[profile.release]
debug = 1
[profile.release-apk] [profile.release-apk]
inherits = "release" inherits = "release"
strip = true strip = true
@ -28,108 +25,110 @@ codegen-units = 1
panic = "abort" panic = "abort"
[dependencies] [dependencies]
log = "0.4" log = "0.4.22"
## node ## node
openssl-sys = { version = "0.9.82", features = ["vendored"] } openssl-sys = { version = "0.9.103", features = ["vendored"] }
grin_api = "5.3.1" grin_api = "5.3.3"
grin_chain = "5.3.1" grin_chain = "5.3.3"
grin_config = "5.3.1" grin_config = "5.3.3"
grin_core = "5.3.1" grin_core = "5.3.3"
grin_p2p = "5.3.1" grin_p2p = "5.3.3"
grin_servers = "5.3.1" grin_servers = "5.3.3"
grin_keychain = "5.3.1" grin_keychain = "5.3.3"
grin_util = "5.3.1" grin_util = "5.3.3"
## wallet ## wallet
grin_wallet_impls = "5.3.1" grin_wallet_impls = "5.3.3"
grin_wallet_api = "5.3.1" grin_wallet_api = "5.3.3"
grin_wallet_libwallet = "5.3.1" grin_wallet_libwallet = "5.3.3"
grin_wallet_util = "5.3.1" grin_wallet_util = "5.3.3"
grin_wallet_controller = "5.3.1" grin_wallet_controller = "5.3.3"
## ui ## ui
egui = { version = "0.28.1", default-features = false } egui = { version = "0.29.1", default-features = false }
egui_extras = { version = "0.28.1", features = ["image", "svg"] } egui_extras = { version = "0.29.1", features = ["image", "svg"] }
rust-i18n = "2.3.1" rust-i18n = "2.3.1"
## other ## other
backtrace = "0.3" anyhow = "1.0.89"
panic-message = "0.3.0" pin-project = "1.1.6"
thiserror = "1.0.58" backtrace = "0.3.74"
futures = "0.3" thiserror = "1.0.64"
futures = "0.3.31"
dirs = "5.0.1" dirs = "5.0.1"
sys-locale = "0.3.0" sys-locale = "0.3.1"
chrono = "0.4.31" chrono = "0.4.38"
parking_lot = "0.12.1" parking_lot = "0.12.3"
lazy_static = "1.4.0" lazy_static = "1.5.0"
toml = "0.8.2" toml = "0.8.19"
serde = "1.0.170" serde = "1.0.210"
local-ip-address = "0.6.1" local-ip-address = "0.6.3"
url = "2.4.0" url = "2.5.2"
rand = "0.8.5" rand = "0.8.5"
serde_derive = "1.0.197" serde_derive = "1.0.210"
serde_json = "1.0.115" serde_json = "1.0.128"
tokio = { version = "1.37.0", features = ["full"] } tokio = { version = "1.40.0", features = ["full"] }
image = "0.25.1" image = "0.25.2"
rqrr = "0.7.1" rqrr = "0.8.0"
qrcodegen = "1.8.0" qrcodegen = "1.8.0"
qrcode = "0.14.0" qrcode = "0.14.1"
ur = "0.4.1" ur = "0.4.1"
gif = "0.13.1" gif = "0.13.1"
rkv = { version = "0.19.0", features = ["lmdb"] } rkv = { version = "0.19.0", features = ["lmdb"] }
## tor ## tor
arti-client = { version = "0.19.0", features = ["pt-client", "static", "onion-service-service", "onion-service-client"] } arti-client = { version = "0.26.0", features = ["pt-client", "static", "onion-service-service", "onion-service-client"] }
tor-rtcompat = { version = "0.19.0", features = ["static"] } tor-rtcompat = { version = "0.26.0", features = ["static"] }
tor-config = "0.19.0" tor-config = "0.26.0"
fs-mistrust = "0.7.9" fs-mistrust = "0.8.0"
tor-hsservice = "0.19.0" tor-hsservice = "0.26.0"
tor-hsrproxy = "0.19.0" tor-hsrproxy = "0.26.0"
tor-keymgr = "0.19.0" tor-keymgr = "0.26.0"
tor-llcrypto = "0.19.0" tor-llcrypto = "0.26.0"
tor-hscrypto = "0.19.0" tor-hscrypto = "0.26.0"
arti-hyper = "0.19.0" tor-error = "0.26.0"
sha2 = "0.10.0" sha2 = "0.10.8"
ed25519-dalek = "2.1.1" ed25519-dalek = "2.1.1"
curve25519-dalek = "4.1.2" curve25519-dalek = "4.1.3"
hyper = { version = "0.14.28", features = ["full"] } hyper = { version = "0.14.30", features = ["full"] }
hyper-tls = "0.5.0" hyper-tls = "0.5.0"
tls-api = "0.9.0" tls-api = "0.12.0"
tls-api-native-tls = "0.9.0" tls-api-native-tls = "0.12.1"
## stratum server ## 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" } tokio-util-old = { version = "0.2", features = ["codec"], package = "tokio-util" }
[target.'cfg(all(not(target_os = "windows"), not(target_os = "android")))'.dependencies] [target.'cfg(target_os = "linux")'.dependencies]
eye = { version = "0.5.0", default-features = false } nokhwa = { version = "0.10.5", default-features = false, features = ["input-v4l"] }
[target.'cfg(target_os = "windows")'.dependencies] [target.'cfg(target_os = "windows")'.dependencies]
nokhwa = { version = "0.10.4", default-features = false, features = ["input-msmf"] } nokhwa = { version = "0.10.5", default-features = false, features = ["input-msmf"] }
[target.'cfg(target_os = "macos")'.dependencies] [target.'cfg(target_os = "macos")'.dependencies]
tls-api-openssl = "0.9.0" eye = { git = "https://github.com/raymanfx/eye-rs", rev = "5b7e3f7a1e79966091692896c568aab042e449ef", default-features = false }
openpnp_capture_sys = "0.4.0"
[target.'cfg(not(target_os = "android"))'.dependencies] [target.'cfg(not(target_os = "android"))'.dependencies]
env_logger = "0.11.3" env_logger = "0.11.3"
winit = { version = "0.29.15" } winit = { version = "0.30.5" }
eframe = { version = "0.28.1", features = ["wgpu", "glow"] } eframe = { version = "0.29.1", features = ["wgpu", "glow"] }
arboard = "3.2.0" arboard = "3.2.0"
rfd = "0.14.1" rfd = "0.15.0"
dark-light = "1.1.1" interprocess = { version = "2.2.1", features = ["tokio"] }
[target.'cfg(target_os = "android")'.dependencies] [target.'cfg(target_os = "android")'.dependencies]
android_logger = "0.13.1" android_logger = "0.14.1"
jni = "0.21.1" jni = "0.21.1"
wgpu = "22.1.0"
android-activity = { version = "0.6.0", features = ["game-activity"] } android-activity = { version = "0.6.0", features = ["game-activity"] }
wgpu = "0.20.1" winit = { version = "0.30.5", features = ["android-game-activity"] }
winit = { version = "0.29.15", features = ["android-game-activity"] } eframe = { version = "0.29.1", features = ["wgpu", "android-game-activity"] }
eframe = { version = "0.28.1", features = ["wgpu", "android-game-activity"] }
[patch.crates-io] [patch.crates-io]
openpnp_capture = { git = "https://github.com/ardocrat/openpnp-capture-rs", rev = "f9b06f627c5e5d42c672d117650af700846ca6cf" }
egui_extras = { git = "https://github.com/emilk/egui", rev = "5b846b4554fe47269affb43efef2cad8710a8a47" }
egui = { git = "https://github.com/emilk/egui", rev = "5b846b4554fe47269affb43efef2cad8710a8a47" }
eframe = { git = "https://github.com/emilk/egui", rev = "5b846b4554fe47269affb43efef2cad8710a8a47" }
### patch grin store ### patch grin store
#grin_store = { path = "../grin-store" } #grin_store = { path = "../grin-store" }
### fix cross-compilation support for macos
openpnp_capture_sys = { git = "https://github.com/ardocrat/openpnp-capture-rs", branch = "cross_compilation_support" }

View file

@ -1,11 +1,11 @@
# <img height="22" src="https://github.com/ardocrat/grim/blob/master/android/app/src/main/ic_launcher-playstore.png?raw=true"> Grim <img height="20" src="https://github.com/mimblewimble/site/blob/master/assets/images/grin-logo.png?raw=true"> <img height="20" src="https://github.com/ardocrat/grim/blob/master/img/logo.png?raw=true"> # Grim <img height="20" src="https://gri.mw/code/GUI/grim/raw/branch/master/img/grin-logo.png"/> <img height="20" src="https://gri.mw/code/GUI/grim/raw/branch/master/img/logo.png"/>
Cross-platform GUI for [GRiN ツ](https://grin.mw) in [Rust](https://www.rust-lang.org/) Cross-platform GUI for [GRiN ツ](https://grin.mw) in [Rust](https://www.rust-lang.org/)
for maximum compatibility with original [Mimblewimble](https://github.com/mimblewimble/grin) implementation. for maximum compatibility with original [Mimblewimble](https://github.com/mimblewimble/grin) implementation.
Initially supported platforms are Linux, Mac, Windows, limited Android and possible web support with help of [egui](https://github.com/emilk/egui) - immediate mode GUI library in pure Rust. Initially supported platforms are Linux, Mac, Windows, limited Android and possible web support with help of [egui](https://github.com/emilk/egui) - immediate mode GUI library in pure Rust.
Named by the character [Grim](http://harrypotter.wikia.com/wiki/Grim) - the shape of a large, black, menacing, spectral giant dog. Named by the character [Grim](http://harrypotter.wikia.com/wiki/Grim) - the shape of a large, black, menacing, spectral giant dog.
![image](https://github.com/user-attachments/assets/a925b1c8-02c9-4b08-b888-0315d11138b6) ![image](https://gri.mw/code/GUI/grim/raw/branch/master/img/cover.png)
## Build instructions ## Build instructions

View file

@ -2,10 +2,6 @@ plugins {
id 'com.android.application' id 'com.android.application'
} }
def keystorePropertiesFile = rootProject.file("keystore.properties")
def keystoreProperties = new Properties()
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
android { android {
compileSdk 33 compileSdk 33
ndkVersion '26.0.10792818' ndkVersion '26.0.10792818'
@ -14,10 +10,15 @@ android {
applicationId "mw.gri.android" applicationId "mw.gri.android"
minSdk 24 minSdk 24
targetSdk 33 targetSdk 33
versionCode 1 versionCode 3
versionName "0.1.0" versionName "0.2.3"
} }
def keystorePropertiesFile = rootProject.file("keystore.properties")
def keystoreProperties = new Properties()
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
signingConfigs { signingConfigs {
release { release {
keyAlias keystoreProperties['keyAlias'] keyAlias keystoreProperties['keyAlias']
@ -27,12 +28,19 @@ android {
} }
} }
}
buildTypes { buildTypes {
release { release {
minifyEnabled false minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
if (keystorePropertiesFile.exists()) {
signedRelease {
initWith release
signingConfig signingConfigs.release signingConfig signingConfigs.release
} }
}
debug { debug {
minifyEnabled false minifyEnabled false
debuggable true debuggable true

View file

@ -1,15 +1,17 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:tools="http://schemas.android.com/tools" xmlns:android="http://schemas.android.com/apk/res/android"
> >
<uses-feature android:name="android.hardware.camera" android:required="false"/> <uses-feature android:name="android.hardware.camera" android:required="false"/>
<uses-permission android:name="android.permission.EXPAND_STATUS_BAR" /> <uses-permission android:name="android.permission.EXPAND_STATUS_BAR" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/> <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.CAMERA"/> <uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" tools:ignore="ScopedStorage"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" tools:ignore="ScopedStorage"/>
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" tools:ignore="ScopedStorage"/>
<application <application
android:hardwareAccelerated="true" android:hardwareAccelerated="true"
@ -18,7 +20,6 @@
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="Grim" android:label="Grim"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Main"> android:theme="@style/Theme.Main">
<receiver android:name=".NotificationActionsReceiver"/> <receiver android:name=".NotificationActionsReceiver"/>
@ -44,6 +45,22 @@
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
<intent-filter android:scheme="http" tools:ignore="AppLinkUrlError">
<action android:name="android.intent.action.SEND" />
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/*" />
</intent-filter>
<intent-filter android:scheme="http" tools:ignore="AppLinkUrlError">
<action android:name="android.intent.action.SEND" />
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="application/*" />
</intent-filter>
<meta-data android:name="android.app.lib_name" android:value="grim" /> <meta-data android:name="android.app.lib_name" android:value="grim" />
</activity> </activity>
<service android:name=".BackgroundService" android:stopWithTask="true" /> <service android:name=".BackgroundService" android:stopWithTask="true" />

View file

@ -152,6 +152,7 @@ public class BackgroundService extends Service {
// Show notification with sync status. // Show notification with sync status.
Intent i = getPackageManager().getLaunchIntentForPackage(this.getPackageName()); Intent i = getPackageManager().getLaunchIntentForPackage(this.getPackageName());
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, i, PendingIntent.FLAG_IMMUTABLE); PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, i, PendingIntent.FLAG_IMMUTABLE);
try {
mNotificationBuilder = new NotificationCompat.Builder(this, TAG) mNotificationBuilder = new NotificationCompat.Builder(this, TAG)
.setContentTitle(this.getSyncTitle()) .setContentTitle(this.getSyncTitle())
.setContentText(this.getSyncStatusText()) .setContentText(this.getSyncStatusText())
@ -159,6 +160,9 @@ public class BackgroundService extends Service {
.setSmallIcon(R.drawable.ic_stat_name) .setSmallIcon(R.drawable.ic_stat_name)
.setPriority(NotificationCompat.PRIORITY_MAX) .setPriority(NotificationCompat.PRIORITY_MAX)
.setContentIntent(pendingIntent); .setContentIntent(pendingIntent);
} catch (UnsatisfiedLinkError e) {
return;
}
Notification notification = mNotificationBuilder.build(); Notification notification = mNotificationBuilder.build();
// Start service at foreground state to prevent killing by system. // Start service at foreground state to prevent killing by system.

View file

@ -7,9 +7,9 @@ import android.content.*;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.content.res.Configuration; import android.content.res.Configuration;
import android.net.Uri; import android.net.Uri;
import android.os.Build; import android.os.*;
import android.os.Bundle;
import android.os.Process; import android.os.Process;
import android.provider.Settings;
import android.system.ErrnoException; import android.system.ErrnoException;
import android.system.Os; import android.system.Os;
import android.util.Size; import android.util.Size;
@ -51,8 +51,7 @@ public class MainActivity extends GameActivity {
@Override @Override
public void onReceive(Context ctx, Intent i) { public void onReceive(Context ctx, Intent i) {
if (i.getAction().equals(STOP_APP_ACTION)) { if (i.getAction().equals(STOP_APP_ACTION)) {
onExit(); exit();
Process.killProcess(Process.myPid());
} }
} }
}; };
@ -67,11 +66,19 @@ public class MainActivity extends GameActivity {
private ExecutorService mCameraExecutor = null; private ExecutorService mCameraExecutor = null;
private boolean mUseBackCamera = true; private boolean mUseBackCamera = true;
private ActivityResultLauncher<Intent> mFilePickResultLauncher = null; private ActivityResultLauncher<Intent> mFilePickResult = null;
private ActivityResultLauncher<Intent> mOpenFilePermissionsResult = null;
@SuppressLint("UnspecifiedRegisterReceiverFlag") @SuppressLint("UnspecifiedRegisterReceiverFlag")
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
// Check if activity was launched to exclude from recent apps on exit.
if ((getIntent().getFlags() & Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS) != 0) {
super.onCreate(null);
finish();
return;
}
// Clear cache on start. // Clear cache on start.
if (savedInstanceState == null) { if (savedInstanceState == null) {
Utils.deleteDirectoryContent(new File(getExternalCacheDir().getPath()), false); Utils.deleteDirectoryContent(new File(getExternalCacheDir().getPath()), false);
@ -91,8 +98,21 @@ public class MainActivity extends GameActivity {
// Register receiver to finish activity from the BackgroundService. // Register receiver to finish activity from the BackgroundService.
registerReceiver(mBroadcastReceiver, new IntentFilter(STOP_APP_ACTION)); registerReceiver(mBroadcastReceiver, new IntentFilter(STOP_APP_ACTION));
// Register file pick result launcher. // Register associated file opening result.
mFilePickResultLauncher = registerForActivityResult( 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(), new ActivityResultContracts.StartActivityForResult(),
result -> { result -> {
int resultCode = result.getResultCode(); int resultCode = result.getResultCode();
@ -124,7 +144,7 @@ public class MainActivity extends GameActivity {
// Listener for display insets (cutouts) to pass values into native code. // Listener for display insets (cutouts) to pass values into native code.
View content = getWindow().getDecorView().findViewById(android.R.id.content); View content = getWindow().getDecorView().findViewById(android.R.id.content);
ViewCompat.setOnApplyWindowInsetsListener(content, (v, insets) -> { ViewCompat.setOnApplyWindowInsetsListener(content, (v, insets) -> {
// Setup cutouts values. // Get display cutouts.
DisplayCutoutCompat dc = insets.getDisplayCutout(); DisplayCutoutCompat dc = insets.getDisplayCutout();
int cutoutTop = 0; int cutoutTop = 0;
int cutoutRight = 0; int cutoutRight = 0;
@ -140,7 +160,7 @@ public class MainActivity extends GameActivity {
// Get display insets. // Get display insets.
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()); Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
// Setup values to pass into native code. // Pass values into native code.
int[] values = new int[]{0, 0, 0, 0}; int[] values = new int[]{0, 0, 0, 0};
values[0] = Utils.pxToDp(Integer.max(cutoutTop, systemBars.top), this); values[0] = Utils.pxToDp(Integer.max(cutoutTop, systemBars.top), this);
values[1] = Utils.pxToDp(Integer.max(cutoutRight, systemBars.right), this); values[1] = Utils.pxToDp(Integer.max(cutoutRight, systemBars.right), this);
@ -166,7 +186,60 @@ public class MainActivity extends GameActivity {
BackgroundService.start(this); BackgroundService.start(this);
} }
}); });
// Check if intent has data on launch.
if (savedInstanceState == null) {
onNewIntent(getIntent());
} }
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
String action = intent.getAction();
// Check if file was open with the application.
if (action != null && action.equals(Intent.ACTION_VIEW)) {
Intent i = getIntent();
i.setData(intent.getData());
setIntent(i);
onFile();
}
}
// Callback when associated file was open.
private void onFile() {
Uri data = getIntent().getData();
if (data == null) {
return;
}
if (Build.VERSION.SDK_INT >= 30) {
if (!Environment.isExternalStorageManager()) {
Intent i = new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION);
mOpenFilePermissionsResult.launch(i);
return;
}
}
try {
ParcelFileDescriptor parcelFile = getContentResolver().openFileDescriptor(data, "r");
FileReader fileReader = new FileReader(parcelFile.getFileDescriptor());
BufferedReader reader = new BufferedReader(fileReader);
String line;
StringBuilder buff = new StringBuilder();
while ((line = reader.readLine()) != null) {
buff.append(line);
}
reader.close();
fileReader.close();
// Provide file content into native code.
onData(buff.toString());
} catch (Exception e) {
e.printStackTrace();
}
}
// Pass data into native code.
public native void onData(String data);
@Override @Override
public void onConfigurationChanged(Configuration newConfig) { public void onConfigurationChanged(Configuration newConfig) {
@ -232,17 +305,17 @@ public class MainActivity extends GameActivity {
// Implemented into native code to handle key code BACK event. // Implemented into native code to handle key code BACK event.
public native void onBack(); public native void onBack();
// Actions on app exit. // Called from native code to exit app.
private void onExit() { public void exit() {
unregisterReceiver(mBroadcastReceiver); finishAndRemoveTask();
BackgroundService.stop(this);
} }
@Override @Override
protected void onDestroy() { protected void onDestroy() {
onExit(); unregisterReceiver(mBroadcastReceiver);
BackgroundService.stop(this);
// Kill process after 3 seconds if app was terminated from recent apps to prevent app hanging. // Kill process after 3 secs if app was terminated from recent apps to prevent app hang.
new Thread(() -> { new Thread(() -> {
try { try {
onTermination(); onTermination();
@ -253,9 +326,7 @@ public class MainActivity extends GameActivity {
} }
}).start(); }).start();
// Destroy an app and kill process.
super.onDestroy(); super.onDestroy();
Process.killProcess(Process.myPid());
} }
// Notify native code to stop activity (e.g. node) if app was terminated from recent apps. // Notify native code to stop activity (e.g. node) if app was terminated from recent apps.
@ -298,18 +369,16 @@ public class MainActivity extends GameActivity {
// Called from native code to start camera. // Called from native code to start camera.
public void startCamera() { public void startCamera() {
// Check permissions.
String notificationsPermission = Manifest.permission.CAMERA; String notificationsPermission = Manifest.permission.CAMERA;
if (checkSelfPermission(notificationsPermission) != PackageManager.PERMISSION_GRANTED) { if (checkSelfPermission(notificationsPermission) != PackageManager.PERMISSION_GRANTED) {
requestPermissions(new String[] { notificationsPermission }, CAMERA_PERMISSION_CODE); requestPermissions(new String[] { notificationsPermission }, CAMERA_PERMISSION_CODE);
} else { } else {
// Start .
if (mCameraProviderFuture == null) { if (mCameraProviderFuture == null) {
mCameraProviderFuture = ProcessCameraProvider.getInstance(this); mCameraProviderFuture = ProcessCameraProvider.getInstance(this);
mCameraProviderFuture.addListener(() -> { mCameraProviderFuture.addListener(() -> {
try { try {
mCameraProvider = mCameraProviderFuture.get(); mCameraProvider = mCameraProviderFuture.get();
// Launch camera. // Start camera.
openCamera(); openCamera();
} catch (Exception e) { } catch (Exception e) {
View content = findViewById(android.R.id.content); View content = findViewById(android.R.id.content);
@ -381,14 +450,14 @@ public class MainActivity extends GameActivity {
// Pass image from camera into native code. // Pass image from camera into native code.
public native void onCameraImage(byte[] buff, int rotation); public native void onCameraImage(byte[] buff, int rotation);
// Called from native code to share image from provided path. // Called from native code to share data from provided path.
public void shareImage(String path) { public void shareData(String path) {
File file = new File(path); File file = new File(path);
Uri uri = FileProvider.getUriForFile(this, "mw.gri.android.fileprovider", file); Uri uri = FileProvider.getUriForFile(this, "mw.gri.android.fileprovider", file);
Intent intent = new Intent(Intent.ACTION_SEND); Intent intent = new Intent(Intent.ACTION_SEND);
intent.putExtra(Intent.EXTRA_STREAM, uri); intent.putExtra(Intent.EXTRA_STREAM, uri);
intent.setType("image/*"); intent.setType("*/*");
startActivity(Intent.createChooser(intent, "Share image")); startActivity(Intent.createChooser(intent, "Share data"));
} }
// Called from native code to check if device is using dark theme. // Called from native code to check if device is using dark theme.
@ -402,7 +471,7 @@ public class MainActivity extends GameActivity {
Intent intent = new Intent(Intent.ACTION_GET_CONTENT); Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.setType("*/*"); intent.setType("*/*");
try { try {
mFilePickResultLauncher.launch(Intent.createChooser(intent, "Pick file")); mFilePickResult.launch(Intent.createChooser(intent, "Pick file"));
} catch (android.content.ActivityNotFoundException ex) { } catch (android.content.ActivityNotFoundException ex) {
onFilePick(""); onFilePick("");
} }

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<paths> <paths>
<external-cache-path name="images" path="images/" /> <external-cache-path name="share" path="share/" />
</paths> </paths>

View file

@ -1,10 +1,6 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules. // Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins { plugins {
id 'com.android.application' version '8.1.1' apply false id 'com.android.application' version '8.6.1' apply false
id 'com.android.library' version '8.1.1' apply false id 'com.android.library' version '8.6.1' apply false
}
task clean(type: Delete) {
delete rootProject.buildDir
} }

View file

@ -19,5 +19,4 @@ android.useAndroidX=true
# resources declared in the library itself and none from the library's dependencies, # resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library # thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true android.nonTransitiveRClass=true
android.defaults.buildfeatures.buildconfig=true
android.nonFinalResIds=false android.nonFinalResIds=false

View file

@ -1,6 +1,6 @@
#Mon May 02 15:39:12 BST 2022 #Mon May 02 15:39:12 BST 2022
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME

BIN
img/cover.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 KiB

BIN
img/grin-logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

View file

@ -4,3 +4,4 @@ Exec=grim
Icon=grim Icon=grim
Type=Application Type=Application
Categories=Finance Categories=Finance
MimeType=application/x-slatepack;text/plain;

View file

@ -17,9 +17,7 @@ cd ..
[[ $2 == "x86_64" ]] && arch+=(x86_64-unknown-linux-gnu) [[ $2 == "x86_64" ]] && arch+=(x86_64-unknown-linux-gnu)
[[ $2 == "arm" ]] && arch+=(aarch64-unknown-linux-gnu) [[ $2 == "arm" ]] && arch+=(aarch64-unknown-linux-gnu)
# Start release build with zig linker for cross-compilation cargo build --release --target ${arch}
cargo install cargo-zigbuild
cargo zigbuild --release --target ${arch}
# Create AppImage with https://github.com/AppImage/appimagetool # Create AppImage with https://github.com/AppImage/appimagetool
cp target/${arch}/release/grim linux/Grim.AppDir/AppRun cp target/${arch}/release/grim linux/Grim.AppDir/AppRun

View file

@ -28,6 +28,7 @@ light: Hell
choose_file: Datei auswählen choose_file: Datei auswählen
crash_report: Absturzbericht crash_report: Absturzbericht
crash_report_warning: Anwendung wurde beim letzten Mal unerwartet geschlossen, Sie können den Absturzbericht mit Entwicklern teilen. crash_report_warning: Anwendung wurde beim letzten Mal unerwartet geschlossen, Sie können den Absturzbericht mit Entwicklern teilen.
confirmation: Bestätigung
wallets: wallets:
await_conf_amount: Erwarte Bestätigung await_conf_amount: Erwarte Bestätigung
await_fin_amount: Warten auf die Fertigstellung await_fin_amount: Warten auf die Fertigstellung
@ -287,7 +288,6 @@ network_settings:
modal: modal:
cancel: Abbrechen cancel: Abbrechen
save: Speichern save: Speichern
confirmation: Bestätigung
add: Hinzufügen add: Hinzufügen
modal_exit: modal_exit:
description: Sind Sie sicher, dass Sie die Anwendung beenden wollen? description: Sind Sie sicher, dass Sie die Anwendung beenden wollen?

View file

@ -28,6 +28,7 @@ light: Light
choose_file: Choose file choose_file: Choose file
crash_report: Crash report crash_report: Crash report
crash_report_warning: Application closed unexpectedly last time, you can share crash report with developers. crash_report_warning: Application closed unexpectedly last time, you can share crash report with developers.
confirmation: Confirmation
wallets: wallets:
await_conf_amount: Awaiting confirmation await_conf_amount: Awaiting confirmation
await_fin_amount: Awaiting finalization await_fin_amount: Awaiting finalization
@ -287,7 +288,6 @@ network_settings:
modal: modal:
cancel: Cancel cancel: Cancel
save: Save save: Save
confirmation: Confirmation
add: Add add: Add
modal_exit: modal_exit:
description: Are you sure you want to quit the application? description: Are you sure you want to quit the application?

View file

@ -28,6 +28,7 @@ light: Clair
choose_file: Choisir un fichier choose_file: Choisir un fichier
crash_report: Rapport d'échec 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. 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
wallets: wallets:
await_conf_amount: En attente de confirmation await_conf_amount: En attente de confirmation
await_fin_amount: En attente de finalisation await_fin_amount: En attente de finalisation
@ -287,7 +288,6 @@ network_settings:
modal: modal:
cancel: Annuler cancel: Annuler
save: Sauvegarder save: Sauvegarder
confirmation: Confirmation
add: Ajouter add: Ajouter
modal_exit: modal_exit:
description: "Êtes-vous sûr de vouloir quitter l'application ?" description: "Êtes-vous sûr de vouloir quitter l'application ?"

View file

@ -28,6 +28,7 @@ light: Светлая
choose_file: Выбрать файл choose_file: Выбрать файл
crash_report: Отчёт о сбое crash_report: Отчёт о сбое
crash_report_warning: В прошлый раз приложение неожиданно закрылось, вы можете поделиться отчетом о сбое с разработчиками. crash_report_warning: В прошлый раз приложение неожиданно закрылось, вы можете поделиться отчетом о сбое с разработчиками.
confirmation: Подтверждение
wallets: wallets:
await_conf_amount: Ожидает подтверждения await_conf_amount: Ожидает подтверждения
await_fin_amount: Ожидает завершения await_fin_amount: Ожидает завершения
@ -287,7 +288,6 @@ network_settings:
modal: modal:
cancel: Отмена cancel: Отмена
save: Сохранить save: Сохранить
confirmation: Подтверждение
add: Добавить add: Добавить
modal_exit: modal_exit:
description: Вы уверены, что хотите выйти из приложения? description: Вы уверены, что хотите выйти из приложения?

View file

@ -28,6 +28,7 @@ light: Isik
choose_file: Dosya seçin choose_file: Dosya seçin
crash_report: Ariza Raporu crash_report: Ariza Raporu
crash_report_warning: Uygulama beklenmedik bir sekilde kapandi son kez, kilitlenme raporunu gelistiricilerle paylasabilirsiniz. crash_report_warning: Uygulama beklenmedik bir sekilde kapandi son kez, kilitlenme raporunu gelistiricilerle paylasabilirsiniz.
confirmation: Onay
wallets: wallets:
await_conf_amount: Onay bekleniyor await_conf_amount: Onay bekleniyor
await_fin_amount: Tamamlanma bekleniyor await_fin_amount: Tamamlanma bekleniyor
@ -287,7 +288,6 @@ network_settings:
modal: modal:
cancel: Iptal cancel: Iptal
save: Kaydet save: Kaydet
confirmation: Onay
add: Ekle add: Ekle
modal_exit: modal_exit:
description: Uygulamadan cikmak için exit, emin misiniz? description: Uygulamadan cikmak için exit, emin misiniz?

View file

@ -1,49 +1,65 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>CFBundleDevelopmentRegion</key> <key>CFBundleDevelopmentRegion</key>
<string>en</string> <string>en</string>
<key>CFBundleDisplayName</key> <key>CFBundleDisplayName</key>
<string>Grim</string> <string>Grim</string>
<key>CFBundleExecutable</key> <key>CFBundleExecutable</key>
<string>grim</string> <string>grim</string>
<key>CFBundleIconFile</key> <key>CFBundleIconFile</key>
<string>AppIcon</string> <string>AppIcon</string>
<key>CFBundleIconName</key> <key>CFBundleIconName</key>
<string>AppIcon</string> <string>AppIcon</string>
<key>CFBundleIdentifier</key> <key>CFBundleIdentifier</key>
<string>mw.gri.macos</string> <string>mw.gri.macos</string>
<key>CFBundleInfoDictionaryVersion</key> <key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string> <string>6.0</string>
<key>CFBundleName</key> <key>CFBundleName</key>
<string>Grim</string> <string>Grim</string>
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>0.1.0</string> <string>0.2.3</string>
<key>CFBundleSupportedPlatforms</key> <key>CFBundleSupportedPlatforms</key>
<array> <array>
<string>MacOSX</string> <string>MacOSX</string>
</array> </array>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>1</string> <string>1</string>
<key>NSCameraUsageDescription</key>
<string>Grim needs an access to your camera to scan QR code.</string>
<key>CFBundleDocumentTypes</key>
<array>
<dict>
<key>CFBundleTypeName</key>
<string>Apple SimpleText document</string>
<key>CFBundleTypeRole</key>
<string>Viewer</string>
<key>LSItemContentTypes</key>
<array>
<string>com.apple.traditional-mac-plain-text</string>
</array>
<key>NSDocumentClass</key>
<string>Document</string>
</dict>
<dict>
<key>CFBundleTypeName</key>
<string>Unknown document</string>
<key>CFBundleTypeRole</key>
<string>Viewer</string>
<key>LSItemContentTypes</key>
<array>
<string>public.data</string>
</array>
<key>NSDocumentClass</key>
<string>Document</string>
</dict>
</array>
<key>LSApplicationCategoryType</key> <key>LSApplicationCategoryType</key>
<string>public.app-category.finance</string> <string>public.app-category.finance</string>
<key>NSHumanReadableCopyright</key> <key>NSHumanReadableCopyright</key>
<string>2024</string> <string>2024</string>
</dict> </dict>
</plist> </plist>

View file

@ -0,0 +1,2 @@
!.gitignore
grim

View file

@ -1,22 +1,21 @@
#!/bin/bash #!/bin/bash
set -e set -e
case $2 in case $1 in
x86_64|arm|universal) x86_64|arm|universal)
;; ;;
*) *)
echo "Usage: release_macos.sh [version] [platform]\n - platform: 'x86_64', 'arm', 'universal'" >&2 echo "Usage: release_macos.sh [platform] [version]\n - platform: 'x86_64', 'arm', 'universal'" >&2
exit 1 exit 1
esac esac
if [[ ! -v SDKROOT ]]; then if [[ "$OSTYPE" != "darwin"* ]]; then
if [ -z ${SDKROOT+x} ]; then
echo "MacOS SDKROOT is not set" echo "MacOS SDKROOT is not set"
exit 1 exit 1
elif [[ -z "SDKROOT" ]]; then else
echo "MacOS SDKROOT is set to the empty string"
exit 1
else
echo "Use MacOS SDK: ${SDKROOT}" echo "Use MacOS SDK: ${SDKROOT}"
fi
fi fi
# Setup build directory # Setup build directory
@ -25,31 +24,37 @@ cd ${BASEDIR}
cd .. cd ..
# Setup platform # Setup platform
rustup target add x86_64-apple-darwin [[ $1 == "x86_64" ]] && arch+=(x86_64-apple-darwin)
rustup target add aarch64-apple-darwin [[ $1 == "arm" ]] && arch+=(aarch64-apple-darwin)
rm -rf target/x86_64-apple-darwin if [[ "$OSTYPE" != "darwin"* ]]; then
rm -rf target/aarch64-apple-darwin # 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
[[ $2 == "x86_64" ]] && arch+=(x86_64-apple-darwin) rm -f .intentionally-empty-file.o
[[ $2 == "arm" ]] && arch+=(aarch64-apple-darwin)
[[ $2 == "universal" ]] && arch+=(universal2-apple-darwin)
# Start release build with zig linker for cross-compilation
# zig 0.12+ required
cargo install cargo-zigbuild
cargo zigbuild --release --target ${arch}
rm -rf .intentionally-empty-file.o
mkdir macos/Grim.app/Contents/MacOS
yes | cp -rf target/${arch}/release/grim macos/Grim.app/Contents/MacOS yes | cp -rf target/${arch}/release/grim macos/Grim.app/Contents/MacOS
### Sign .app resources on change: # Sign .app resources on change:
#rcodesign generate-self-signed-certificate #rcodesign generate-self-signed-certificate
#rcodesign sign --pem-file cert.pem macos/Grim.app #rcodesign sign --pem-file cert.pem macos/Grim.app
# Create release package # Create release package
FILE_NAME=grim-v$1-macos-$2.zip FILE_NAME=grim-v$2-macos-$1.zip
rm -rf target/${arch}/release/${FILE_NAME}
cd macos cd macos
zip -r ${FILE_NAME} Grim.app zip -r ${FILE_NAME} Grim.app
mv ${FILE_NAME} ../target/${arch}/release mv ${FILE_NAME} ../target/${arch}/release

View file

@ -1,81 +1,120 @@
#!/bin/bash #!/bin/bash
usage="Usage: build_run_android.sh [type] [platform]\n - type: 'debug', 'release'\n - platform: 'v7', 'v8'" usage="Usage: android.sh [type] [platform|version]\n - type: 'build', 'release'\n - platform, for 'build' type: 'v7', 'v8', 'x86'\n - optional version for 'release' (needed on MacOS), example: '0.2.2'"
case $1 in case $1 in
debug|release) build|release)
;; ;;
*) *)
printf "$usage" printf "$usage"
exit 1 exit 1
esac esac
case $2 in if [[ $1 == "build" ]]; then
v7|v8) case $2 in
v7|v8|x86)
;; ;;
*) *)
printf "$usage" printf "$usage"
exit 1 exit 1
esac esac
fi
# Setup build directory # Setup build directory
BASEDIR=$(cd $(dirname $0) && pwd) BASEDIR=$(cd "$(dirname "$0")" && pwd)
cd ${BASEDIR} cd "${BASEDIR}" || exit 1
cd .. cd ..
# Setup release argument # Install platforms and tools
type=$1 rustup target add armv7-linux-androideabi
[[ ${type} == "release" ]] && release_param="--profile release-apk" rustup target add aarch64-linux-android
rustup target add x86_64-linux-android
# Setup platform argument
[[ $2 == "v7" ]] && arch+=(armeabi-v7a)
[[ $2 == "v8" ]] && arch+=(arm64-v8a)
# Setup platform path
[[ $2 == "v7" ]] && platform+=(armv7-linux-androideabi)
[[ $2 == "v8" ]] && platform+=(aarch64-linux-android)
# Install platform
[[ $2 == "v7" ]] && rustup target install armv7-linux-androideabi
[[ $2 == "v8" ]] && rustup target install aarch64-linux-android
# Build native code
cargo install cargo-ndk cargo install cargo-ndk
sed -i -e 's/"rlib"/"cdylib","rlib"/g' Cargo.toml success=1
# temp fix for https://stackoverflow.com/questions/57193895/error-use-of-undeclared-identifier-pthread-mutex-robust-cargo-build-liblmdb-s ### Build native code
success=0 function build_lib() {
export CPPFLAGS="-DMDB_USE_ROBUST=0" && export CFLAGS="-DMDB_USE_ROBUST=0" [[ $1 == "v7" ]] && arch=armeabi-v7a
cargo ndk -t ${arch} build ${release_param} [[ $1 == "v8" ]] && arch=arm64-v8a
unset CPPFLAGS && unset CFLAGS [[ $1 == "x86" ]] && arch=x86_64
cargo ndk -t ${arch} -o android/app/src/main/jniLibs build ${release_param}
if [ $? -eq 0 ] sed -i -e 's/"rlib"/"cdylib","rlib"/g' Cargo.toml
then
# Fix for https://stackoverflow.com/questions/57193895/error-use-of-undeclared-identifier-pthread-mutex-robust-cargo-build-liblmdb-s
# Uncomment lines below for the 1st build:
#export CPPFLAGS="-DMDB_USE_ROBUST=0" && export CFLAGS="-DMDB_USE_ROBUST=0"
#cargo ndk -t ${arch} build --profile release-apk
#unset CPPFLAGS && unset CFLAGS
cargo ndk -t "${arch}" -o android/app/src/main/jniLibs build --profile release-apk
if [ $? -eq 0 ]
then
success=1 success=1
fi else
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
# Build Android application and launch at all connected devices }
if [ $success -eq 1 ]
then
cd android
# Setup gradle argument
[[ $1 == "release" ]] && gradle_param+=(assembleRelease)
[[ $1 == "debug" ]] && gradle_param+=(build)
### Build application
function build_apk() {
cd android || exit 1
./gradlew clean ./gradlew clean
./gradlew ${gradle_param} # Build signed apk if keystore exists
if [ ! -f keystore.properties ]; then
# Setup apk path ./gradlew assembleDebug
[[ $1 == "release" ]] && apk_path+=(app/build/outputs/apk/release/app-release.apk) apk_path=app/build/outputs/apk/debug/app-debug.apk
[[ $1 == "debug" ]] && apk_path+=(app/build/outputs/apk/debug/app-debug.apk) else
./gradlew assembleSignedRelease
apk_path=app/build/outputs/apk/signedRelease/app-signedRelease.apk
fi
if [[ $1 == "" ]]; then
# Launch application at all connected devices.
for SERIAL in $(adb devices | grep -v List | cut -f 1); for SERIAL in $(adb devices | grep -v List | cut -f 1);
do do
adb -s $SERIAL install ${apk_path} adb -s "$SERIAL" install ${apk_path}
sleep 1s sleep 1s
adb -s $SERIAL shell am start -n mw.gri.android/.MainActivity; adb -s "$SERIAL" shell am start -n mw.gri.android/.MainActivity;
done done
else
if [[ "$OSTYPE" != "darwin"* ]]; then
version=$(grep -m 1 -Po 'version = "\K[^"]*' Cargo.toml)
else
version=v$2
fi
# Setup release file name
name=grim-${version}-android-$1.apk
[[ $1 == "arm" ]] && name=grim-${version}-android.apk
rm -f "${name}"
mv ${apk_path} "${name}"
# Calculate checksum
checksum=grim-${version}-android-$1-sha256sum.txt
[[ $1 == "arm" ]] && checksum=grim-${version}-android-sha256sum.txt
rm -f "${checksum}"
sha256sum "${name}" > "${checksum}"
fi
cd ..
}
rm -rf android/app/src/main/jniLibs/*
if [[ $1 == "build" ]]; then
build_lib "$2"
[ $success -eq 1 ] && build_apk
else
rm -rf target/release-apk
rm -rf target/aarch64-linux-android
rm -rf target/x86_64-linux-android
rm -rf target/armv7-linux-androideabi
build_lib "v7"
[ $success -eq 1 ] && build_lib "v8"
[ $success -eq 1 ] && build_apk "arm" "$2"
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

@ -1,25 +1,27 @@
#!/bin/bash #!/bin/bash
case $1 in case $1 in
debug|release) debug|build)
;; ;;
*) *)
echo "Usage: build_run.sh [type] where is type is 'debug' or 'release'" >&2 echo "Usage: build_run.sh [type] where is type is 'debug' or 'build'" >&2
exit 1 exit 1
esac esac
# Setup build directory # Setup build directory
BASEDIR=$(cd $(dirname $0) && pwd) BASEDIR=$(cd "$(dirname $0)" && pwd)
cd ${BASEDIR} cd "${BASEDIR}" || return
cd .. cd ..
# Build application # Build application
type=$1 type=$1
[[ ${type} == "release" ]] && release_param+=(--release) [[ ${type} == "build" ]] && release_param+=(--release)
cargo build ${release_param[@]} cargo --config profile.release.incremental=true build "${release_param[@]}"
# Start application # Start application
if [ $? -eq 0 ] if [ $? -eq 0 ]
then then
./target/${type}/grim path=${type}
[[ ${type} == "build" ]] && path="release"
./target/"${path}"/grim
fi fi

101
scripts/version.sh Executable file
View file

@ -0,0 +1,101 @@
#!/bin/bash
# Usage to bump version
# ./version.sh patch
# ./version.sh minor
# ./version.sh major
# Setup base directory
BASEDIR=$(cd $(dirname $0) && pwd)
cd ${BASEDIR}
cd ..
# Exit script if command fails or uninitialized variables used
set -euo pipefail
# ==================================
# Verify repo is clean
# ==================================
# List uncommitted changes and
# check if the output is not empty
if [ -n "$(git status --porcelain)" ]; then
# Print error message
printf "\nError: repo has uncommitted changes\n\n"
# Exit with error code
exit 1
fi
# ==================================
# Get latest version from git tags
# ==================================
# List git tags sorted lexicographically
# so version numbers sorted correctly
GIT_TAGS=$(git tag --sort=version:refname)
# Get last line of output which returns the
# last tag (most recent version)
GIT_TAG_LATEST=$(echo "$GIT_TAGS" | tail -n 1)
# If no tag found, default to v0.1.0
if [ -z "$GIT_TAG_LATEST" ]; then
GIT_TAG_LATEST="v0.1.0"
fi
# Strip prefix 'v' from the tag to easily increment
GIT_TAG_LATEST=$(echo "$GIT_TAG_LATEST" | sed 's/^v//')
# ==================================
# Increment version number
# ==================================
# Get version type from first argument passed to script
VERSION_TYPE="${1-}"
VERSION_NEXT=""
if [ "$VERSION_TYPE" = "patch" ]; then
# Increment patch version
VERSION_NEXT="$(echo "$GIT_TAG_LATEST" | awk -F. '{$NF++; print $1"."$2"."$NF}')"
elif [ "$VERSION_TYPE" = "minor" ]; then
# Increment minor version
VERSION_NEXT="$(echo "$GIT_TAG_LATEST" | awk -F. '{$2++; $3=0; print $1"."$2"."$3}')"
elif [ "$VERSION_TYPE" = "major" ]; then
# Increment major version
VERSION_NEXT="$(echo "$GIT_TAG_LATEST" | awk -F. '{$1++; $2=0; $3=0; print $1"."$2"."$3}')"
else
# Print error for unknown versioning type
printf "\nError: invalid VERSION_TYPE arg passed, must be 'patch', 'minor' or 'major'\n\n"
# Exit with error code
exit 1
fi
# ==================================
# Update Android build.gradle file
# and package version at Cargo.toml
# ==================================
# Update version in build.gradle
sed -i'.bak' -e 's/versionName [0-9a-zA-Z -_]*/versionName "'"$VERSION_NEXT"'"/' android/app/build.gradle
rm -f android/app/build.gradle.bak
# Update version in Cargo.toml
sed -i'.bak' -e "s/^version = .*/version = \"$VERSION_NEXT\"/" Cargo.toml
rm -f Cargo.toml.bak
# Update Cargo.lock as this changes when
# updating the version in your manifest
cargo update -p grim
# Commit the changes
git add .
git commit -m "release: v$VERSION_NEXT"
# ==================================
# Create git tag for new version
# ==================================
# Create a tag and push to master branch
git tag "v$VERSION_NEXT" master
#git push origin master --follow-tags

View file

@ -14,7 +14,7 @@
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::{AtomicBool, Ordering};
use lazy_static::lazy_static; use lazy_static::lazy_static;
use egui::{Align, Context, CursorIcon, Layout, Modifiers, Rect, ResizeDirection, Rounding, Stroke, ViewportCommand}; use egui::{Align, Context, CursorIcon, Layout, Modifiers, ResizeDirection, Rounding, Stroke, UiBuilder, ViewportCommand};
use egui::epaint::{RectShape}; use egui::epaint::{RectShape};
use egui::os::OperatingSystem; use egui::os::OperatingSystem;
@ -22,7 +22,8 @@ use crate::AppConfig;
use crate::gui::Colors; use crate::gui::Colors;
use crate::gui::icons::{ARROWS_IN, ARROWS_OUT, CARET_DOWN, MOON, SUN, X}; use crate::gui::icons::{ARROWS_IN, ARROWS_OUT, CARET_DOWN, MOON, SUN, X};
use crate::gui::platform::PlatformCallbacks; use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Content, TitlePanel, View}; use crate::gui::views::{Content, Modal, TitlePanel, View};
use crate::wallet::ExternalConnection;
lazy_static! { lazy_static! {
/// State to check if platform Back button was pressed. /// State to check if platform Back button was pressed.
@ -31,27 +32,49 @@ lazy_static! {
/// Implements ui entry point and contains platform-specific callbacks. /// Implements ui entry point and contains platform-specific callbacks.
pub struct App<Platform> { pub struct App<Platform> {
/// Platform specific callbacks handler. /// Handles platform-specific functionality.
pub(crate) platform: Platform, pub platform: Platform,
/// Main content.
/// Main ui content.
content: Content, content: Content,
/// Last window resize direction. /// Last window resize direction.
resize_direction: Option<ResizeDirection> resize_direction: Option<ResizeDirection>,
/// Flag to check if it's first draw.
first_draw: bool
} }
impl<Platform: PlatformCallbacks> App<Platform> { impl<Platform: PlatformCallbacks> App<Platform> {
pub fn new(platform: Platform) -> Self { pub fn new(platform: Platform) -> Self {
Self { platform, content: Content::default(), resize_direction: None } Self {
platform,
content: Content::default(),
resize_direction: None,
first_draw: true
}
}
/// Called of first content draw.
fn on_first_draw(&mut self, ctx: &Context) {
// Set platform context.
if View::is_desktop() {
self.platform.set_context(ctx);
}
// Check connections availability.
ExternalConnection::check(None, ctx);
// Setup visuals.
crate::setup_visuals(ctx);
} }
/// Draw application content. /// Draw application content.
pub fn ui(&mut self, ctx: &Context) { pub fn ui(&mut self, ctx: &Context) {
if self.first_draw {
self.on_first_draw(ctx);
self.first_draw = false;
}
// Handle Esc keyboard key event and platform Back button key event. // Handle Esc keyboard key event and platform Back button key event.
let back_pressed = BACK_BUTTON_PRESSED.load(Ordering::Relaxed); let back_pressed = BACK_BUTTON_PRESSED.load(Ordering::Relaxed);
if ctx.input_mut(|i| i.consume_key(Modifiers::NONE, egui::Key::Escape)) || back_pressed { if back_pressed || ctx.input_mut(|i| i.consume_key(Modifiers::NONE, egui::Key::Escape)) {
self.content.on_back(); self.content.on_back(&self.platform);
if back_pressed { if back_pressed {
BACK_BUTTON_PRESSED.store(false, Ordering::Relaxed); BACK_BUTTON_PRESSED.store(false, Ordering::Relaxed);
} }
@ -59,16 +82,15 @@ impl<Platform: PlatformCallbacks> App<Platform> {
ctx.request_repaint(); ctx.request_repaint();
} }
// Handle Close event (on desktop). // Handle Close event on desktop.
if ctx.input(|i| i.viewport().close_requested()) { if View::is_desktop() && ctx.input(|i| i.viewport().close_requested()) {
if !self.content.exit_allowed { if !self.content.exit_allowed {
ctx.send_viewport_cmd(ViewportCommand::CancelClose); ctx.send_viewport_cmd(ViewportCommand::CancelClose);
Content::show_exit_modal(); Content::show_exit_modal();
} else { } else {
let (w, h) = View::window_size(ctx);
AppConfig::save_window_size(w, h);
ctx.input(|i| { ctx.input(|i| {
if let Some(rect) = i.viewport().inner_rect {
AppConfig::save_window_size(rect.width(), rect.height());
}
if let Some(rect) = i.viewport().outer_rect { if let Some(rect) = i.viewport().outer_rect {
AppConfig::save_window_pos(rect.left(), rect.top()); AppConfig::save_window_pos(rect.left(), rect.top());
} }
@ -76,95 +98,89 @@ impl<Platform: PlatformCallbacks> App<Platform> {
} }
} }
// Show main content with custom frame on desktop. // Show main content.
egui::CentralPanel::default() egui::CentralPanel::default()
.frame(egui::Frame { .frame(egui::Frame {
..Default::default() ..Default::default()
}) })
.show(ctx, |ui| { .show(ctx, |ui| {
let is_mac_os = OperatingSystem::from_target_os() == OperatingSystem::Mac; if View::is_desktop() {
if View::is_desktop() && !is_mac_os {
self.desktop_window_ui(ui);
} else {
if is_mac_os {
self.window_title_ui(ui);
ui.add_space(-1.0);
}
self.content.ui(ui, &self.platform);
}
});
}
/// Draw custom resizeable window content.
fn desktop_window_ui(&mut self, ui: &mut egui::Ui) {
let is_fullscreen = ui.ctx().input(|i| { let is_fullscreen = ui.ctx().input(|i| {
i.viewport().fullscreen.unwrap_or(false) i.viewport().fullscreen.unwrap_or(false)
}); });
if OperatingSystem::from_target_os() != OperatingSystem::Mac {
let title_stroke_rect = { self.desktop_window_ui(ui, is_fullscreen);
let mut rect = ui.max_rect();
if !is_fullscreen {
rect = rect.shrink(Content::WINDOW_FRAME_MARGIN);
}
rect.max.y = if !is_fullscreen {
Content::WINDOW_FRAME_MARGIN
} else { } else {
0.0 self.window_title_ui(ui, is_fullscreen);
} + Content::WINDOW_TITLE_HEIGHT + TitlePanel::DEFAULT_HEIGHT + 0.5; ui.add_space(-1.0);
rect Self::title_panel_bg(ui);
}; self.content.ui(ui, &self.platform);
let title_stroke = RectShape { }
rect: title_stroke_rect, } else {
rounding: Rounding { self.mobile_window_ui(ui);
nw: 8.0,
ne: 8.0,
sw: 0.0,
se: 0.0,
},
fill: Colors::yellow(),
stroke: Stroke {
width: 1.0,
color: egui::Color32::from_gray(200)
},
blur_width: 0.0,
fill_texture_id: Default::default(),
uv: Rect::ZERO
};
// Draw title stroke.
ui.painter().add(title_stroke);
let content_stroke_rect = {
let mut rect = ui.max_rect();
if !is_fullscreen {
rect = rect.shrink(Content::WINDOW_FRAME_MARGIN);
} }
let top = Content::WINDOW_TITLE_HEIGHT + TitlePanel::DEFAULT_HEIGHT + 0.5;
rect.min += egui::vec2(0.0, top);
rect
};
let content_stroke = RectShape {
rect: content_stroke_rect,
rounding: Rounding::ZERO,
fill: Colors::fill(),
stroke: Stroke {
width: 1.0,
color: Colors::stroke()
},
blur_width: 0.0,
fill_texture_id: Default::default(),
uv: Rect::ZERO
};
// Draw content stroke.
ui.painter().add(content_stroke);
// Draw window content. // 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.
if self.platform.user_attention_required() &&
ctx.input(|i| i.viewport().focused.unwrap_or(true)) {
self.platform.clear_user_attention();
}
}
/// Draw mobile platform window content.
fn mobile_window_ui(&mut self, ui: &mut egui::Ui) {
Self::title_panel_bg(ui);
self.content.ui(ui, &self.platform);
}
/// Draw desktop platform window content.
fn desktop_window_ui(&mut self, ui: &mut egui::Ui, is_fullscreen: bool) {
let content_bg_rect = {
let mut r = ui.max_rect();
if !is_fullscreen {
r = r.shrink(Content::WINDOW_FRAME_MARGIN);
}
r.min.y += Content::WINDOW_TITLE_HEIGHT + TitlePanel::HEIGHT;
r
};
let content_bg = RectShape::new(content_bg_rect,
Rounding::ZERO,
Colors::fill_lite(),
View::default_stroke());
// Draw content background.
ui.painter().add(content_bg);
let mut content_rect = ui.max_rect(); let mut content_rect = ui.max_rect();
if !is_fullscreen { if !is_fullscreen {
content_rect = content_rect.shrink(Content::WINDOW_FRAME_MARGIN); content_rect = content_rect.shrink(Content::WINDOW_FRAME_MARGIN);
} }
ui.allocate_ui_at_rect(content_rect, |ui| { // Draw window content.
self.window_title_ui(ui); ui.allocate_new_ui(UiBuilder::new().max_rect(content_rect), |ui| {
self.window_content(ui); // Draw window title.
self.window_title_ui(ui, is_fullscreen);
ui.add_space(-1.0);
// Draw title panel background.
Self::title_panel_bg(ui);
let content_rect = {
let mut rect = ui.max_rect();
rect.min.y += Content::WINDOW_TITLE_HEIGHT;
rect
};
let mut content_ui = ui.new_child(UiBuilder::new()
.max_rect(content_rect)
.layout(*ui.layout()));
// Draw main content.
self.content.ui(&mut content_ui, &self.platform);
}); });
// Setup resize areas. // Setup resize areas.
@ -180,35 +196,35 @@ impl<Platform: PlatformCallbacks> App<Platform> {
} }
} }
/// Draw window content for desktop. /// Draw title panel background.
fn window_content(&mut self, ui: &mut egui::Ui) { fn title_panel_bg(ui: &mut egui::Ui) {
let content_rect = { let title_rect = {
let mut rect = ui.max_rect(); let mut rect = ui.max_rect();
rect.min.y += Content::WINDOW_TITLE_HEIGHT; if View::is_desktop() {
rect.min.y += Content::WINDOW_TITLE_HEIGHT - 0.5;
}
rect.max.y = rect.min.y + View::get_top_inset() + TitlePanel::HEIGHT;
rect rect
}; };
// Draw main content. let title_bg = RectShape::filled(title_rect, Rounding::ZERO, Colors::yellow());
let mut content_ui = ui.child_ui(content_rect, *ui.layout(), None); ui.painter().add(title_bg);
self.content.ui(&mut content_ui, &self.platform);
} }
/// Draw custom window title content. /// Draw custom window title content.
fn window_title_ui(&self, ui: &mut egui::Ui) { fn window_title_ui(&self, ui: &mut egui::Ui, is_fullscreen: bool) {
let content_rect = ui.max_rect();
let title_rect = { let title_rect = {
let mut rect = content_rect; let mut rect = ui.max_rect();
rect.max.y = rect.min.y + Content::WINDOW_TITLE_HEIGHT; rect.max.y = rect.min.y + Content::WINDOW_TITLE_HEIGHT;
rect rect
}; };
let is_fullscreen = ui.ctx().input(|i| { let title_bg_rect = {
i.viewport().fullscreen.unwrap_or(false) let mut r = title_rect.clone();
}); r.max.y += TitlePanel::HEIGHT - 1.0;
r
let window_title_bg = RectShape { };
rect: title_rect, let is_mac = OperatingSystem::from_target_os() == OperatingSystem::Mac;
rounding: if is_fullscreen { let window_title_bg = RectShape::new(title_bg_rect, if is_fullscreen || is_mac {
Rounding::ZERO Rounding::ZERO
} else { } else {
Rounding { Rounding {
@ -217,20 +233,16 @@ impl<Platform: PlatformCallbacks> App<Platform> {
sw: 0.0, sw: 0.0,
se: 0.0, se: 0.0,
} }
}, }, Colors::yellow_dark(), Stroke::new(1.0, Colors::STROKE));
fill: Colors::yellow_dark(),
stroke: Stroke::NONE,
blur_width: 0.0,
fill_texture_id: Default::default(),
uv: Rect::ZERO
};
// Draw title background. // Draw title background.
ui.painter().add(window_title_bg); ui.painter().add(window_title_bg);
let painter = ui.painter(); let painter = ui.painter();
let interact_rect = { let interact_rect = {
let mut rect = title_rect; let mut rect = title_rect.clone();
rect.max.x -= 128.0;
rect.min.x += 85.0;
if !is_fullscreen { if !is_fullscreen {
rect.min.y += Content::WINDOW_FRAME_MARGIN; rect.min.y += Content::WINDOW_FRAME_MARGIN;
} }
@ -239,25 +251,29 @@ impl<Platform: PlatformCallbacks> App<Platform> {
let title_resp = ui.interact( let title_resp = ui.interact(
interact_rect, interact_rect,
egui::Id::new("window_title"), egui::Id::new("window_title"),
egui::Sense::click_and_drag(), egui::Sense::drag(),
); );
// Interact with the window title (drag to move window):
if !is_fullscreen && title_resp.drag_started_by(egui::PointerButton::Primary) {
ui.ctx().send_viewport_cmd(ViewportCommand::StartDrag);
}
// Paint the title. // Paint the title.
let dual_wallets_panel = let dual_wallets_panel = ui.available_width() >= (Content::SIDE_PANEL_WIDTH * 3.0) +
ui.available_width() >= (Content::SIDE_PANEL_WIDTH * 3.0) + View::get_right_inset(); View::get_right_inset() + View::get_left_inset();
let wallet_panel_opened = self.content.wallets.wallet_panel_opened(); let wallet_panel_opened = self.content.wallets.showing_wallet();
let hide_app_name = if dual_wallets_panel { let show_app_name = if dual_wallets_panel {
!wallet_panel_opened || (AppConfig::show_wallets_at_dual_panel() && wallet_panel_opened && !AppConfig::show_wallets_at_dual_panel()
self.content.wallets.showing_wallet() && !self.content.wallets.creating_wallet()) } else if Content::is_dual_panel_mode(ui.ctx()) {
} else if Content::is_dual_panel_mode(ui) { wallet_panel_opened
!wallet_panel_opened
} else { } else {
!Content::is_network_panel_open() && !wallet_panel_opened Content::is_network_panel_open() || wallet_panel_opened
}; };
let title_text = if hide_app_name { let creating_wallet = self.content.wallets.creating_wallet();
"".to_string() let title_text = if creating_wallet || show_app_name {
} else {
format!("Grim {}", crate::VERSION) format!("Grim {}", crate::VERSION)
} else {
"".to_string()
}; };
painter.text( painter.text(
title_rect.center(), title_rect.center(),
@ -267,20 +283,13 @@ impl<Platform: PlatformCallbacks> App<Platform> {
Colors::title(true), Colors::title(true),
); );
// Interact with the window title (drag to move window): ui.allocate_new_ui(UiBuilder::new().max_rect(title_rect), |ui| {
if !is_fullscreen && title_resp.double_clicked() {
ui.ctx().send_viewport_cmd(ViewportCommand::Fullscreen(!is_fullscreen));
}
if !is_fullscreen && title_resp.drag_started_by(egui::PointerButton::Primary) {
ui.ctx().send_viewport_cmd(ViewportCommand::StartDrag);
}
ui.allocate_ui_at_rect(title_rect, |ui| {
ui.with_layout(Layout::right_to_left(Align::Center), |ui| { ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
// Draw button to close window. // Draw button to close window.
View::title_button_small(ui, X, |_| { View::title_button_small(ui, X, |_| {
if Modal::opened().is_none() || Modal::opened_closeable() {
Content::show_exit_modal(); Content::show_exit_modal();
}
}); });
// Draw fullscreen button. // Draw fullscreen button.
@ -392,16 +401,11 @@ impl<Platform: PlatformCallbacks> eframe::App for App<Platform> {
} }
fn clear_color(&self, _visuals: &egui::Visuals) -> [f32; 4] { fn clear_color(&self, _visuals: &egui::Visuals) -> [f32; 4] {
if View::is_desktop() { let is_mac = OperatingSystem::from_target_os() == OperatingSystem::Mac;
let is_mac_os = OperatingSystem::from_target_os() == OperatingSystem::Mac; if !View::is_desktop() || is_mac {
if is_mac_os { return Colors::fill_lite().to_normalized_gamma_f32();
Colors::fill().to_normalized_gamma_f32()
} else {
egui::Rgba::TRANSPARENT.to_array()
}
} else {
Colors::fill().to_normalized_gamma_f32()
} }
Colors::TRANSPARENT.to_normalized_gamma_f32()
} }
} }

View file

@ -31,10 +31,14 @@ const YELLOW: Color32 = Color32::from_rgb(254, 241, 2);
const YELLOW_DARK: Color32 = Color32::from_rgb(239, 229, 3); const YELLOW_DARK: Color32 = Color32::from_rgb(239, 229, 3);
const GREEN: Color32 = Color32::from_rgb(0, 0x64, 0); 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: 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 BLUE: Color32 = Color32::from_rgb(0, 0x66, 0xE4); 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: Color32 = Color32::from_gray(244);
const FILL_DARK: Color32 = Color32::from_gray(24); const FILL_DARK: Color32 = Color32::from_gray(24);
@ -42,6 +46,9 @@ const FILL_DARK: Color32 = Color32::from_gray(24);
const FILL_DEEP: Color32 = Color32::from_gray(238); 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(18);
const FILL_LITE: Color32 = Color32::from_gray(249);
const FILL_LITE_DARK: Color32 = Color32::from_gray(16);
const TEXT: Color32 = Color32::from_gray(80); const TEXT: Color32 = Color32::from_gray(80);
const TEXT_DARK: Color32 = Color32::from_gray(185); const TEXT_DARK: Color32 = Color32::from_gray(185);
@ -54,13 +61,9 @@ const TEXT_BUTTON_DARK: Color32 = Color32::from_gray(195);
const TITLE: Color32 = Color32::from_gray(60); const TITLE: Color32 = Color32::from_gray(60);
const TITLE_DARK: Color32 = Color32::from_gray(205); const TITLE_DARK: Color32 = Color32::from_gray(205);
const BUTTON: Color32 = Color32::from_gray(249);
const BUTTON_DARK: Color32 = Color32::from_gray(16);
const GRAY: Color32 = Color32::from_gray(120); const GRAY: Color32 = Color32::from_gray(120);
const GRAY_DARK: Color32 = Color32::from_gray(145); const GRAY_DARK: Color32 = Color32::from_gray(145);
const STROKE: Color32 = Color32::from_gray(200);
const STROKE_DARK: Color32 = Color32::from_gray(50); const STROKE_DARK: Color32 = Color32::from_gray(50);
const INACTIVE_TEXT: Color32 = Color32::from_gray(150); const INACTIVE_TEXT: Color32 = Color32::from_gray(150);
@ -82,6 +85,7 @@ fn use_dark() -> bool {
impl Colors { impl Colors {
pub const TRANSPARENT: Color32 = Color32::from_rgba_premultiplied(0, 0, 0, 0); pub const TRANSPARENT: Color32 = Color32::from_rgba_premultiplied(0, 0, 0, 0);
pub const STROKE: Color32 = Color32::from_gray(200);
pub fn white_or_black(black_in_white: bool) -> Color32 { pub fn white_or_black(black_in_white: bool) -> Color32 {
if use_dark() { if use_dark() {
@ -125,7 +129,7 @@ impl Colors {
pub fn green() -> Color32 { pub fn green() -> Color32 {
if use_dark() { if use_dark() {
GREEN.gamma_multiply(1.3) GREEN_DARK
} else { } else {
GREEN GREEN
} }
@ -133,7 +137,7 @@ impl Colors {
pub fn red() -> Color32 { pub fn red() -> Color32 {
if use_dark() { if use_dark() {
RED.gamma_multiply(1.3) RED_DARK
} else { } else {
RED RED
} }
@ -141,7 +145,7 @@ impl Colors {
pub fn blue() -> Color32 { pub fn blue() -> Color32 {
if use_dark() { if use_dark() {
BLUE.gamma_multiply(1.3) BLUE_DARK
} else { } else {
BLUE BLUE
} }
@ -163,6 +167,14 @@ impl Colors {
} }
} }
pub fn fill_lite() -> Color32 {
if use_dark() {
FILL_LITE_DARK
} else {
FILL_LITE
}
}
pub fn checkbox() -> Color32 { pub fn checkbox() -> Color32 {
if use_dark() { if use_dark() {
CHECKBOX_DARK CHECKBOX_DARK
@ -195,14 +207,6 @@ impl Colors {
} }
} }
pub fn button() -> Color32 {
if use_dark() {
BUTTON_DARK
} else {
BUTTON
}
}
pub fn gray() -> Color32 { pub fn gray() -> Color32 {
if use_dark() { if use_dark() {
GRAY_DARK GRAY_DARK
@ -215,7 +219,7 @@ impl Colors {
if use_dark() { if use_dark() {
STROKE_DARK STROKE_DARK
} else { } else {
STROKE Self::STROKE
} }
} }

View file

@ -30,7 +30,11 @@ use crate::gui::platform::PlatformCallbacks;
/// Android platform implementation. /// Android platform implementation.
#[derive(Clone)] #[derive(Clone)]
pub struct Android { pub struct Android {
/// Android related state.
android_app: AndroidApp, android_app: AndroidApp,
/// Context to repaint content and handle viewport commands.
ctx: Arc<RwLock<Option<egui::Context>>>,
} }
impl Android { impl Android {
@ -38,6 +42,7 @@ impl Android {
pub fn new(app: AndroidApp) -> Self { pub fn new(app: AndroidApp) -> Self {
Self { Self {
android_app: app, android_app: app,
ctx: Arc::new(RwLock::new(None)),
} }
} }
@ -56,27 +61,36 @@ impl Android {
} }
impl PlatformCallbacks for Android { impl PlatformCallbacks for Android {
fn set_context(&mut self, ctx: &egui::Context) {
let mut w_ctx = self.ctx.write();
*w_ctx = Some(ctx.clone());
}
fn exit(&self) {
let _ = self.call_java_method("exit", "()V", &[]);
}
fn show_keyboard(&self) { fn show_keyboard(&self) {
// Disable NDK soft input show call before fix for egui. // Disable NDK soft input show call before fix for egui.
// self.android_app.show_soft_input(false); // self.android_app.show_soft_input(false);
self.call_java_method("showKeyboard", "()V", &[]).unwrap(); let _ = self.call_java_method("showKeyboard", "()V", &[]);
} }
fn hide_keyboard(&self) { fn hide_keyboard(&self) {
// Disable NDK soft input hide call before fix for egui. // Disable NDK soft input hide call before fix for egui.
// self.android_app.hide_soft_input(false); // self.android_app.hide_soft_input(false);
self.call_java_method("hideKeyboard", "()V", &[]).unwrap(); let _ = self.call_java_method("hideKeyboard", "()V", &[]);
} }
fn copy_string_to_buffer(&self, data: String) { fn copy_string_to_buffer(&self, data: String) {
let vm = unsafe { jni::JavaVM::from_raw(self.android_app.vm_as_ptr() as _) }.unwrap(); let vm = unsafe { jni::JavaVM::from_raw(self.android_app.vm_as_ptr() as _) }.unwrap();
let env = vm.attach_current_thread().unwrap(); let env = vm.attach_current_thread().unwrap();
let arg_value = env.new_string(data).unwrap(); let arg_value = env.new_string(data).unwrap();
self.call_java_method("copyText", let _ = self.call_java_method("copyText",
"(Ljava/lang/String;)V", "(Ljava/lang/String;)V",
&[JValue::Object(&JObject::from(arg_value))]).unwrap(); &[JValue::Object(&JObject::from(arg_value))]);
} }
fn get_string_from_buffer(&self) -> String { fn get_string_from_buffer(&self) -> String {
@ -95,12 +109,12 @@ impl PlatformCallbacks for Android {
let mut w_image = LAST_CAMERA_IMAGE.write(); let mut w_image = LAST_CAMERA_IMAGE.write();
*w_image = None; *w_image = None;
// Start camera. // Start camera.
self.call_java_method("startCamera", "()V", &[]).unwrap(); let _ = self.call_java_method("startCamera", "()V", &[]);
} }
fn stop_camera(&self) { fn stop_camera(&self) {
// Stop camera. // Stop camera.
self.call_java_method("stopCamera", "()V", &[]).unwrap(); let _ = self.call_java_method("stopCamera", "()V", &[]);
// Clear image. // Clear image.
let mut w_image = LAST_CAMERA_IMAGE.write(); let mut w_image = LAST_CAMERA_IMAGE.write();
*w_image = None; *w_image = None;
@ -115,32 +129,39 @@ impl PlatformCallbacks for Android {
} }
fn can_switch_camera(&self) -> bool { fn can_switch_camera(&self) -> bool {
let result = self.call_java_method("camerasAmount", "()I", &[]).unwrap(); if let Some(res) = self.call_java_method("camerasAmount", "()I", &[]) {
let amount = unsafe { result.i }; let amount = unsafe { res.i };
amount > 1 return amount > 1;
}
false
} }
fn switch_camera(&self) { fn switch_camera(&self) {
self.call_java_method("switchCamera", "()V", &[]).unwrap(); let _ = self.call_java_method("switchCamera", "()V", &[]);
} }
fn share_data(&self, name: String, data: Vec<u8>) -> Result<(), std::io::Error> { fn share_data(&self, name: String, data: Vec<u8>) -> Result<(), std::io::Error> {
// Create file at cache dir.
let default_cache = OsString::from(dirs::cache_dir().unwrap()); let default_cache = OsString::from(dirs::cache_dir().unwrap());
let mut cache = PathBuf::from(env::var_os("XDG_CACHE_HOME").unwrap_or(default_cache)); let mut file = PathBuf::from(env::var_os("XDG_CACHE_HOME").unwrap_or(default_cache));
cache.push("images"); // File path for Android provider.
std::fs::create_dir_all(cache.to_str().unwrap())?; file.push("share");
cache.push(name); if !file.exists() {
let mut image = File::create_new(cache.clone()).unwrap(); std::fs::create_dir(file.clone())?;
image.write_all(data.as_slice()).unwrap(); }
image.sync_all().unwrap(); file.push(name);
if file.exists() {
std::fs::remove_file(file.clone())?;
}
let mut image = File::create_new(file.clone())?;
image.write_all(data.as_slice())?;
image.sync_all()?;
// Call share modal at system. // Call share modal at system.
let vm = unsafe { jni::JavaVM::from_raw(self.android_app.vm_as_ptr() as _) }.unwrap(); let vm = unsafe { jni::JavaVM::from_raw(self.android_app.vm_as_ptr() as _) }.unwrap();
let env = vm.attach_current_thread().unwrap(); let env = vm.attach_current_thread().unwrap();
let arg_value = env.new_string(cache.to_str().unwrap()).unwrap(); let arg_value = env.new_string(file.to_str().unwrap()).unwrap();
self.call_java_method("shareImage", let _ = self.call_java_method("shareData",
"(Ljava/lang/String;)V", "(Ljava/lang/String;)V",
&[JValue::Object(&JObject::from(arg_value))]).unwrap(); &[JValue::Object(&JObject::from(arg_value))]);
Ok(()) Ok(())
} }
@ -149,7 +170,7 @@ impl PlatformCallbacks for Android {
let mut w_path = PICKED_FILE_PATH.write(); let mut w_path = PICKED_FILE_PATH.write();
*w_path = None; *w_path = None;
// Launch file picker. // Launch file picker.
let _ = self.call_java_method("pickFile", "()V", &[]).unwrap(); let _ = self.call_java_method("pickFile", "()V", &[]);
// Return empty string to identify async pick. // Return empty string to identify async pick.
Some("".to_string()) Some("".to_string())
} }
@ -167,6 +188,14 @@ impl PlatformCallbacks for Android {
} }
None None
} }
fn request_user_attention(&self) {}
fn user_attention_required(&self) -> bool {
false
}
fn clear_user_attention(&self) {}
} }
lazy_static! { lazy_static! {

View file

@ -13,12 +13,13 @@
// limitations under the License. // limitations under the License.
use std::fs::File; use std::fs::File;
use std::io:: Write; use std::io::Write;
use lazy_static::lazy_static;
use parking_lot::RwLock;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::thread; use std::thread;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::Arc;
use parking_lot::RwLock;
use lazy_static::lazy_static;
use egui::{UserAttentionType, ViewportCommand, WindowLevel};
use rfd::FileDialog; use rfd::FileDialog;
use crate::gui::platform::PlatformCallbacks; use crate::gui::platform::PlatformCallbacks;
@ -26,19 +27,155 @@ use crate::gui::platform::PlatformCallbacks;
/// Desktop platform related actions. /// Desktop platform related actions.
#[derive(Clone)] #[derive(Clone)]
pub struct Desktop { pub struct Desktop {
/// Context to repaint content and handle viewport commands.
ctx: Arc<RwLock<Option<egui::Context>>>,
/// Cameras amount.
cameras_amount: Arc<AtomicUsize>,
/// Camera index.
camera_index: Arc<AtomicUsize>,
/// Flag to check if camera stop is needed. /// Flag to check if camera stop is needed.
stop_camera: Arc<AtomicBool>, stop_camera: Arc<AtomicBool>,
/// Flag to check if attention required after window focusing.
attention_required: Arc<AtomicBool>,
} }
impl Default for Desktop { impl Desktop {
fn default() -> Self { pub fn new() -> Self {
Self { Self {
ctx: Arc::new(RwLock::new(None)),
cameras_amount: Arc::new(AtomicUsize::new(0)),
camera_index: Arc::new(AtomicUsize::new(0)),
stop_camera: Arc::new(AtomicBool::new(false)), stop_camera: Arc::new(AtomicBool::new(false)),
attention_required: Arc::new(AtomicBool::new(false)),
} }
} }
#[allow(dead_code)]
#[cfg(not(target_os = "macos"))]
fn start_camera_capture(cameras_amount: Arc<AtomicUsize>,
camera_index: Arc<AtomicUsize>,
stop_camera: Arc<AtomicBool>) {
use nokhwa::Camera;
use nokhwa::pixel_format::RgbFormat;
use nokhwa::utils::{CameraIndex, RequestedFormat, RequestedFormatType};
use nokhwa::utils::ApiBackend;
let devices = nokhwa::query(ApiBackend::Auto).unwrap();
cameras_amount.store(devices.len(), Ordering::Relaxed);
let index = camera_index.load(Ordering::Relaxed);
if devices.is_empty() || index >= devices.len() {
return;
}
thread::spawn(move || {
let index = CameraIndex::Index(camera_index.load(Ordering::Relaxed) as u32);
let requested = RequestedFormat::new::<RgbFormat>(
RequestedFormatType::AbsoluteHighestFrameRate
);
// Create and open camera.
if let Ok(mut camera) = Camera::new(index, requested) {
if let Ok(_) = camera.open_stream() {
loop {
// Stop if camera was stopped.
if stop_camera.load(Ordering::Relaxed) {
stop_camera.store(false, Ordering::Relaxed);
// Clear image.
let mut w_image = LAST_CAMERA_IMAGE.write();
*w_image = None;
break;
}
// Get a frame.
if let Ok(frame) = camera.frame() {
// Save image.
let mut w_image = LAST_CAMERA_IMAGE.write();
*w_image = Some((frame.buffer().to_vec(), 0));
} else {
// Clear image.
let mut w_image = LAST_CAMERA_IMAGE.write();
*w_image = None;
break;
}
}
camera.stop_stream().unwrap();
};
}
});
}
#[allow(dead_code)]
#[cfg(target_os = "macos")]
fn start_camera_capture(cameras_amount: Arc<AtomicUsize>,
camera_index: Arc<AtomicUsize>,
stop_camera: Arc<AtomicBool>) {
use image::{ExtendedColorType, ImageBuffer, ImageEncoder, Rgb};
use eye::hal::{traits::{Context, Device, Stream}, PlatformContext};
use image::codecs::jpeg::JpegEncoder;
let index = camera_index.load(Ordering::Relaxed);
let devices = PlatformContext::default().devices().unwrap_or(vec![]);
cameras_amount.store(devices.len(), Ordering::Relaxed);
if devices.is_empty() || index >= devices.len() {
return;
}
// Capture images at separate thread.
let uri = devices[camera_index.load(Ordering::Relaxed)].uri.clone();
thread::spawn(move || {
if let Ok(dev) = PlatformContext::default().open_device(&uri) {
let streams = dev.streams().unwrap_or(vec![]);
if streams.is_empty() {
return;
}
let stream_desc = streams[0].clone();
let w = stream_desc.width;
let h = stream_desc.height;
if let Ok(mut stream) = dev.start_stream(&stream_desc) {
loop {
// Stop if camera was stopped.
if stop_camera.load(Ordering::Relaxed) {
stop_camera.store(false, Ordering::Relaxed);
let mut w_image = LAST_CAMERA_IMAGE.write();
*w_image = None;
break;
}
// Get a frame.
let frame = stream.next()
.expect("Stream is dead")
.expect("Failed to capture a frame");
let mut out = vec![];
if let Some(buf) = ImageBuffer::<Rgb<u8>, &[u8]>::from_raw(w, h, &frame) {
JpegEncoder::new(&mut out)
.write_image(buf.as_raw(), w, h, ExtendedColorType::Rgb8)
.unwrap_or_default();
} else {
out = frame.to_vec();
}
// Save image.
let mut w_image = LAST_CAMERA_IMAGE.write();
*w_image = Some((out, 0));
}
}
}
});
}
} }
impl PlatformCallbacks for Desktop { impl PlatformCallbacks for Desktop {
fn set_context(&mut self, ctx: &egui::Context) {
let mut w_ctx = self.ctx.write();
*w_ctx = Some(ctx.clone());
}
fn exit(&self) {
let r_ctx = self.ctx.read();
if r_ctx.is_some() {
let ctx = r_ctx.as_ref().unwrap();
ctx.send_viewport_cmd(ViewportCommand::Close);
}
}
fn show_keyboard(&self) {} fn show_keyboard(&self) {}
fn hide_keyboard(&self) {} fn hide_keyboard(&self) {}
@ -59,15 +196,13 @@ impl PlatformCallbacks for Desktop {
let mut w_image = LAST_CAMERA_IMAGE.write(); let mut w_image = LAST_CAMERA_IMAGE.write();
*w_image = None; *w_image = None;
} }
// Setup stop camera flag. // Setup stop camera flag.
let stop_camera = self.stop_camera.clone(); let stop_camera = self.stop_camera.clone();
stop_camera.store(false, Ordering::Relaxed); stop_camera.store(false, Ordering::Relaxed);
// Capture images at separate thread. Self::start_camera_capture(self.cameras_amount.clone(),
thread::spawn(move || { self.camera_index.clone(),
Self::start_camera_capture(stop_camera); stop_camera);
});
} }
fn stop_camera(&self) { fn stop_camera(&self) {
@ -84,11 +219,20 @@ impl PlatformCallbacks for Desktop {
} }
fn can_switch_camera(&self) -> bool { fn can_switch_camera(&self) -> bool {
false let amount = self.cameras_amount.load(Ordering::Relaxed);
amount > 1
} }
fn switch_camera(&self) { fn switch_camera(&self) {
return; self.stop_camera();
let index = self.camera_index.load(Ordering::Relaxed);
let amount = self.cameras_amount.load(Ordering::Relaxed);
if index == amount - 1 {
self.camera_index.store(0, Ordering::Relaxed);
} else {
self.camera_index.store(index + 1, Ordering::Relaxed);
}
self.start_camera();
} }
fn share_data(&self, name: String, data: Vec<u8>) -> Result<(), std::io::Error> { fn share_data(&self, name: String, data: Vec<u8>) -> Result<(), std::io::Error> {
@ -119,86 +263,43 @@ impl PlatformCallbacks for Desktop {
fn picked_file(&self) -> Option<String> { fn picked_file(&self) -> Option<String> {
None None
} }
}
impl Desktop { fn request_user_attention(&self) {
#[allow(dead_code)] let r_ctx = self.ctx.read();
#[cfg(target_os = "windows")] if r_ctx.is_some() {
fn start_camera_capture(stop_camera: Arc<AtomicBool>) { let ctx = r_ctx.as_ref().unwrap();
use nokhwa::Camera; // Request attention on taskbar.
use nokhwa::pixel_format::RgbFormat; ctx.send_viewport_cmd(
use nokhwa::utils::{CameraIndex, RequestedFormat, RequestedFormatType}; ViewportCommand::RequestUserAttention(UserAttentionType::Informational)
let index = CameraIndex::Index(0);
let requested = RequestedFormat::new::<RgbFormat>(
RequestedFormatType::AbsoluteHighestFrameRate
); );
// Create and open camera. // Un-minimize window.
let mut camera = Camera::new(index, requested).unwrap(); if ctx.input(|i| i.viewport().minimized.unwrap_or(false)) {
if let Ok(_) = camera.open_stream() { ctx.send_viewport_cmd(ViewportCommand::Minimized(false));
loop {
// Stop if camera was stopped.
if stop_camera.load(Ordering::Relaxed) {
stop_camera.store(false, Ordering::Relaxed);
// Clear image.
let mut w_image = LAST_CAMERA_IMAGE.write();
*w_image = None;
break;
} }
// Get a frame. // Focus to window.
if let Ok(frame) = camera.frame() { if !ctx.input(|i| i.viewport().focused.unwrap_or(false)) {
// Save image. ctx.send_viewport_cmd(ViewportCommand::WindowLevel(WindowLevel::AlwaysOnTop));
let mut w_image = LAST_CAMERA_IMAGE.write(); ctx.send_viewport_cmd(ViewportCommand::Focus);
*w_image = Some((frame.buffer().to_vec(), 0));
} else {
// Clear image.
let mut w_image = LAST_CAMERA_IMAGE.write();
*w_image = None;
break;
} }
ctx.request_repaint();
} }
camera.stop_stream().unwrap(); self.attention_required.store(true, Ordering::Relaxed);
};
} }
#[allow(dead_code)] fn user_attention_required(&self) -> bool {
#[cfg(not(target_os = "windows"))] self.attention_required.load(Ordering::Relaxed)
fn start_camera_capture(stop_camera: Arc<AtomicBool>) {
use eye::hal::{traits::{Context, Device, Stream}, PlatformContext};
use image::ImageEncoder;
let ctx = PlatformContext::default();
let devices = ctx.devices().unwrap();
let dev = ctx.open_device(&devices[0].uri).unwrap();
let streams = dev.streams().unwrap();
let stream_desc = streams[0].clone();
let w = stream_desc.width;
let h = stream_desc.height;
let mut stream = dev.start_stream(&stream_desc).unwrap();
loop {
// Stop if camera was stopped.
if stop_camera.load(Ordering::Relaxed) {
stop_camera.store(false, Ordering::Relaxed);
// Clear image.
let mut w_image = LAST_CAMERA_IMAGE.write();
*w_image = None;
break;
} }
// Get a frame.
let frame = stream.next().expect("Stream is dead").expect("Failed to capture a frame"); fn clear_user_attention(&self) {
let mut out = vec![]; let r_ctx = self.ctx.read();
if let Some(buf) = image::ImageBuffer::<image::Rgb<u8>, &[u8]>::from_raw(w, h, &frame) { if r_ctx.is_some() {
image::codecs::jpeg::JpegEncoder::new(&mut out) let ctx = r_ctx.as_ref().unwrap();
.write_image(buf.as_raw(), w, h, image::ExtendedColorType::Rgb8).unwrap(); ctx.send_viewport_cmd(
} else { ViewportCommand::RequestUserAttention(UserAttentionType::Reset)
out = frame.to_vec(); );
} ctx.send_viewport_cmd(ViewportCommand::WindowLevel(WindowLevel::Normal));
// Save image.
let mut w_image = LAST_CAMERA_IMAGE.write();
*w_image = Some((out, 0));
} }
self.attention_required.store(false, Ordering::Relaxed);
} }
} }

View file

@ -22,6 +22,8 @@ pub mod platform;
pub mod platform; pub mod platform;
pub trait PlatformCallbacks { pub trait PlatformCallbacks {
fn set_context(&mut self, ctx: &egui::Context);
fn exit(&self);
fn show_keyboard(&self); fn show_keyboard(&self);
fn hide_keyboard(&self); fn hide_keyboard(&self);
fn copy_string_to_buffer(&self, data: String); fn copy_string_to_buffer(&self, data: String);
@ -34,4 +36,7 @@ pub trait PlatformCallbacks {
fn share_data(&self, name: String, data: Vec<u8>) -> Result<(), std::io::Error>; fn share_data(&self, name: String, data: Vec<u8>) -> Result<(), std::io::Error>;
fn pick_file(&self) -> Option<String>; fn pick_file(&self) -> Option<String>;
fn picked_file(&self) -> Option<String>; fn picked_file(&self) -> Option<String>;
fn request_user_attention(&self);
fn user_attention_required(&self) -> bool;
fn clear_user_attention(&self);
} }

View file

@ -15,11 +15,9 @@
use std::sync::Arc; use std::sync::Arc;
use parking_lot::RwLock; use parking_lot::RwLock;
use std::thread; use std::thread;
use eframe::emath::Align;
use egui::load::SizedTexture; use egui::load::SizedTexture;
use egui::{Layout, Pos2, Rect, RichText, TextureOptions, Widget}; use egui::{Pos2, Rect, RichText, TextureOptions, UiBuilder, Widget};
use image::{DynamicImage, EncodableLayout, ImageFormat}; use image::{DynamicImage, EncodableLayout};
use grin_util::ZeroingString; use grin_util::ZeroingString;
use grin_wallet_libwallet::SlatepackAddress; use grin_wallet_libwallet::SlatepackAddress;
use grin_keychain::mnemonic::WORDS; use grin_keychain::mnemonic::WORDS;
@ -36,16 +34,15 @@ use crate::wallet::WalletUtils;
pub struct CameraContent { pub struct CameraContent {
/// QR code scanning progress and result. /// QR code scanning progress and result.
qr_scan_state: Arc<RwLock<QrScanState>>, qr_scan_state: Arc<RwLock<QrScanState>>,
/// Uniform Resources URIs collected from QR code scanning. /// Uniform Resources URIs collected from QR code scanning.
ur_data: Arc<RwLock<Option<(Vec<String>, usize)>>>, ur_data: Arc<RwLock<Option<(Vec<String>, usize)>>>
} }
impl Default for CameraContent { impl Default for CameraContent {
fn default() -> Self { fn default() -> Self {
Self { Self {
qr_scan_state: Arc::new(RwLock::new(QrScanState::default())), qr_scan_state: Arc::new(RwLock::new(QrScanState::default())),
ur_data: Arc::new(RwLock::new(None)), ur_data: Arc::new(RwLock::new(None))
} }
} }
} }
@ -53,21 +50,56 @@ impl Default for CameraContent {
impl CameraContent { impl CameraContent {
/// Draw camera content. /// Draw camera content.
pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
// Draw last image from camera or loader. ui.ctx().request_repaint();
if let Some(img_data) = cb.camera_image() { let rect = if let Some(img_data) = cb.camera_image() {
// Load image to draw. if let Ok(img) =
if let Ok(mut img) = image::load_from_memory(&*img_data.0) {
image::load_from_memory_with_format(&*img_data.0, ImageFormat::Jpeg) {
// Process image to find QR code. // Process image to find QR code.
self.scan_qr(&img); self.scan_qr(&img);
// Draw image.
let img_rect = self.image_ui(ui, img, img_data.1);
// Show UR scan progress.
self.ur_progress_ui(ui);
img_rect
} else {
self.loading_ui(ui)
}
} else {
self.loading_ui(ui)
};
// Show button to switch cameras.
if cb.can_switch_camera() {
let r = {
let mut r = rect.clone();
r.min.y = r.max.y - 52.0;
r.min.x = r.max.x - 52.0;
r
};
ui.allocate_new_ui(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();
});
});
}
}
/// Draw camera image.
fn image_ui(&mut self, ui: &mut egui::Ui, mut img: DynamicImage, rotation: u32) -> Rect {
// Setup image rotation. // Setup image rotation.
img = match img_data.1 { img = match rotation {
90 => img.rotate90(), 90 => img.rotate90(),
180 => img.rotate180(), 180 => img.rotate180(),
270 => img.rotate270(), 270 => img.rotate270(),
_ => img _ => img
}; };
// Convert to ColorImage to add at content. if View::is_desktop() {
img = img.fliph();
}
// Convert to ColorImage.
let color_img = match &img { let color_img = match &img {
DynamicImage::ImageRgb8(image) => { DynamicImage::ImageRgb8(image) => {
egui::ColorImage::from_rgb( egui::ColorImage::from_rgb(
@ -90,8 +122,6 @@ impl CameraContent {
let img_size = egui::emath::vec2(color_img.width() as f32, let img_size = egui::emath::vec2(color_img.width() as f32,
color_img.height() as f32); color_img.height() as f32);
let sized_img = SizedTexture::new(texture.id(), img_size); let sized_img = SizedTexture::new(texture.id(), img_size);
// Add image to content.
ui.vertical_centered(|ui| {
egui::Image::from_texture(sized_img) egui::Image::from_texture(sized_img)
// Setup to crop image at square. // Setup to crop image at square.
.uv(Rect::from([ .uv(Rect::from([
@ -101,54 +131,31 @@ impl CameraContent {
.max_height(ui.available_width()) .max_height(ui.available_width())
.maintain_aspect_ratio(false) .maintain_aspect_ratio(false)
.shrink_to_fit() .shrink_to_fit()
.ui(ui); .ui(ui).rect
}); }
// Show UR scan progress. /// Draw animated QR code scanning progress.
fn ur_progress_ui(&self, ui: &mut egui::Ui) {
let show_ur_progress = { let show_ur_progress = {
self.ur_data.clone().read().is_some() self.ur_data.as_ref().read().is_some()
}; };
let ur_progress = self.ur_progress(); if show_ur_progress {
if show_ur_progress && ur_progress != 0 { ui.centered_and_justified(|ui| {
ui.add_space(-52.0); ui.label(RichText::new(format!("{}%", self.ur_progress()))
ui.vertical_centered(|ui| { .size(17.0)
ui.label(RichText::new(format!("{}%", ur_progress)) .color(Colors::green()));
.size(16.0)
.color(Colors::yellow()));
}); });
} }
// Show button to switch cameras.
if cb.can_switch_camera() {
ui.add_space(-52.0);
let mut size = ui.available_size();
size.y = 48.0;
ui.allocate_ui_with_layout(size, Layout::right_to_left(Align::Max), |ui| {
ui.add_space(4.0);
View::button(ui, CAMERA_ROTATE.to_string(), Colors::white_or_black(false), || {
cb.switch_camera();
});
});
}
} else {
self.loading_content_ui(ui);
}
} else {
self.loading_content_ui(ui);
}
// Request redraw.
ui.ctx().request_repaint();
} }
/// Draw camera loading progress content. /// Draw camera loading progress content.
fn loading_content_ui(&self, ui: &mut egui::Ui) { fn loading_ui(&self, ui: &mut egui::Ui) -> Rect {
let space = (ui.available_width() - View::BIG_SPINNER_SIZE) / 2.0; let space = (ui.available_width() - View::BIG_SPINNER_SIZE) / 2.0;
ui.vertical_centered(|ui| { ui.vertical_centered(|ui| {
ui.add_space(space); ui.add_space(space);
View::big_loading_spinner(ui); View::big_loading_spinner(ui);
ui.add_space(space); ui.add_space(space);
}); }).response.rect
} }
/// Check if image is processing to find QR code. /// Check if image is processing to find QR code.
@ -430,14 +437,4 @@ impl CameraContent {
} }
None None
} }
/// Reset camera content state to default.
pub fn clear_state(&mut self) {
// Clear QR code scanning state.
let mut w_scan = self.qr_scan_state.write();
*w_scan = QrScanState::default();
// Clear UR data.
let mut w_data = self.ur_data.write();
*w_data = None;
}
} }

View file

@ -25,7 +25,7 @@ use crate::gui::views::types::{ModalContainer, ModalPosition};
use crate::node::Node; use crate::node::Node;
use crate::{AppConfig, Settings}; use crate::{AppConfig, Settings};
use crate::gui::icons::{CHECK, CHECK_FAT, FILE_X}; use crate::gui::icons::{CHECK, CHECK_FAT, FILE_X};
use crate::gui::views::network::{NetworkContent, NodeSetup}; use crate::gui::views::network::NetworkContent;
use crate::gui::views::wallets::WalletsContent; use crate::gui::views::wallets::WalletsContent;
lazy_static! { lazy_static! {
@ -40,8 +40,8 @@ pub struct Content {
/// Central panel [`WalletsContent`] content. /// Central panel [`WalletsContent`] content.
pub wallets: WalletsContent, pub wallets: WalletsContent,
/// Check if app exit is allowed on close event of [`eframe::App`] implementation. /// Check if app exit is allowed on Desktop close event.
pub(crate) exit_allowed: bool, pub exit_allowed: bool,
/// Flag to show exit progress at [`Modal`]. /// Flag to show exit progress at [`Modal`].
show_exit_progress: bool, show_exit_progress: bool,
@ -52,6 +52,11 @@ pub struct Content {
allowed_modal_ids: Vec<&'static str> 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 { impl Default for Content {
fn default() -> Self { fn default() -> Self {
// Exit from eframe only for non-mobile platforms. // Exit from eframe only for non-mobile platforms.
@ -66,8 +71,8 @@ impl Default for Content {
allowed_modal_ids: vec![ allowed_modal_ids: vec![
Self::EXIT_CONFIRMATION_MODAL, Self::EXIT_CONFIRMATION_MODAL,
Self::SETTINGS_MODAL, Self::SETTINGS_MODAL,
Self::ANDROID_INTEGRATED_NODE_WARNING_MODAL, ANDROID_INTEGRATED_NODE_WARNING_MODAL,
Self::CRASH_REPORT_MODAL CRASH_REPORT_MODAL
], ],
} }
} }
@ -83,10 +88,10 @@ impl ModalContainer for Content {
modal: &Modal, modal: &Modal,
cb: &dyn PlatformCallbacks) { cb: &dyn PlatformCallbacks) {
match modal.id { match modal.id {
Self::EXIT_CONFIRMATION_MODAL => self.exit_modal_content(ui, modal), Self::EXIT_CONFIRMATION_MODAL => self.exit_modal_content(ui, modal, cb),
Self::SETTINGS_MODAL => self.settings_modal_ui(ui, modal), Self::SETTINGS_MODAL => self.settings_modal_ui(ui, modal),
Self::ANDROID_INTEGRATED_NODE_WARNING_MODAL => self.android_warning_modal_ui(ui, modal), ANDROID_INTEGRATED_NODE_WARNING_MODAL => self.android_warning_modal_ui(ui, modal),
Self::CRASH_REPORT_MODAL => self.crash_report_modal_ui(ui, modal, cb), CRASH_REPORT_MODAL => self.crash_report_modal_ui(ui, modal, cb),
_ => {} _ => {}
} }
} }
@ -97,10 +102,6 @@ impl Content {
pub const EXIT_CONFIRMATION_MODAL: &'static str = "exit_confirmation_modal"; pub const EXIT_CONFIRMATION_MODAL: &'static str = "exit_confirmation_modal";
/// Identifier for wallet opening [`Modal`]. /// Identifier for wallet opening [`Modal`].
pub const SETTINGS_MODAL: &'static str = "settings_modal"; pub const SETTINGS_MODAL: &'static str = "settings_modal";
/// Identifier for integrated node warning [`Modal`] on Android.
const ANDROID_INTEGRATED_NODE_WARNING_MODAL: &'static str = "android_node_warning_modal";
/// Identifier for crash report [`Modal`].
const CRASH_REPORT_MODAL: &'static str = "crash_report_modal";
/// Default width of side panel at application UI. /// Default width of side panel at application UI.
pub const SIDE_PANEL_WIDTH: f32 = 400.0; pub const SIDE_PANEL_WIDTH: f32 = 400.0;
@ -110,11 +111,10 @@ impl Content {
pub const WINDOW_FRAME_MARGIN: f32 = 6.0; pub const WINDOW_FRAME_MARGIN: f32 = 6.0;
pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { 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); self.current_modal_ui(ui, cb);
let dual_panel = Self::is_dual_panel_mode(ui); let dual_panel = Self::is_dual_panel_mode(ui.ctx());
let (is_panel_open, panel_width) = Self::network_panel_state_width(ui, dual_panel); let (is_panel_open, panel_width) = network_panel_state_width(ui.ctx(), dual_panel);
// Show network content. // Show network content.
egui::SidePanel::left("network_panel") egui::SidePanel::left("network_panel")
@ -137,48 +137,26 @@ impl Content {
}); });
if self.first_draw { if self.first_draw {
// Show crash report if needed. // Show crash report or integrated node Android warning.
if AppConfig::show_crash() { if Settings::crash_report_path().exists() {
Modal::new(Self::CRASH_REPORT_MODAL) Modal::new(CRASH_REPORT_MODAL)
.closeable(false) .closeable(false)
.position(ModalPosition::Center) .position(ModalPosition::Center)
.title(t!("crash_report")) .title(t!("crash_report"))
.show(); .show();
} else { } else if OperatingSystem::from_target_os() == OperatingSystem::Android &&
// Show integrated node warning on Android if needed.
if OperatingSystem::from_target_os() == OperatingSystem::Android &&
AppConfig::android_integrated_node_warning_needed() { AppConfig::android_integrated_node_warning_needed() {
Modal::new(Self::ANDROID_INTEGRATED_NODE_WARNING_MODAL) Modal::new(ANDROID_INTEGRATED_NODE_WARNING_MODAL)
.title(t!("network.node")) .title(t!("network.node"))
.show(); .show();
} }
}
self.first_draw = false; self.first_draw = false;
} }
} }
/// Get [`NetworkContent`] panel state and width.
fn network_panel_state_width(ui: &mut egui::Ui, dual_panel: bool) -> (bool, f32) {
let is_panel_open = dual_panel || Self::is_network_panel_open();
let panel_width = if dual_panel {
Self::SIDE_PANEL_WIDTH + View::get_left_inset()
} else {
let is_fullscreen = ui.ctx().input(|i| {
i.viewport().fullscreen.unwrap_or(false)
});
View::window_size(ui).0 - if View::is_desktop() && !is_fullscreen &&
OperatingSystem::from_target_os() != OperatingSystem::Mac {
Self::WINDOW_FRAME_MARGIN * 2.0
} else {
0.0
}
};
(is_panel_open, panel_width)
}
/// Check if ui can show [`NetworkContent`] and [`WalletsContent`] at same time. /// Check if ui can show [`NetworkContent`] and [`WalletsContent`] at same time.
pub fn is_dual_panel_mode(ui: &egui::Ui) -> bool { pub fn is_dual_panel_mode(ctx: &egui::Context) -> bool {
let (w, h) = View::window_size(ui); let (w, h) = View::window_size(ctx);
// Screen is wide if width is greater than height or just 20% smaller. // Screen is wide if width is greater than height or just 20% smaller.
let is_wide_screen = w > h || w + (w * 0.2) >= h; let is_wide_screen = w > h || w + (w * 0.2) >= h;
// Dual panel mode is available when window is wide and its width is at least 2 times // Dual panel mode is available when window is wide and its width is at least 2 times
@ -201,16 +179,16 @@ impl Content {
/// Show exit confirmation [`Modal`]. /// Show exit confirmation [`Modal`].
pub fn show_exit_modal() { pub fn show_exit_modal() {
Modal::new(Self::EXIT_CONFIRMATION_MODAL) Modal::new(Self::EXIT_CONFIRMATION_MODAL)
.title(t!("modal.confirmation")) .title(t!("confirmation"))
.show(); .show();
} }
/// Draw exit confirmation modal content. /// Draw exit confirmation modal content.
fn exit_modal_content(&mut self, ui: &mut egui::Ui, modal: &Modal) { fn exit_modal_content(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
if self.show_exit_progress { if self.show_exit_progress {
if !Node::is_running() { if !Node::is_running() {
self.exit_allowed = true; self.exit_allowed = true;
ui.ctx().send_viewport_cmd(egui::ViewportCommand::Close); cb.exit();
modal.close(); modal.close();
} }
ui.add_space(16.0); ui.add_space(16.0);
@ -241,10 +219,10 @@ impl Content {
}); });
}); });
columns[1].vertical_centered_justified(|ui| { columns[1].vertical_centered_justified(|ui| {
View::button_ui(ui, t!("modal_exit.exit"), Colors::white_or_black(false), |ui| { View::button_ui(ui, t!("modal_exit.exit"), Colors::white_or_black(false), |_| {
if !Node::is_running() { if !Node::is_running() {
self.exit_allowed = true; self.exit_allowed = true;
ui.ctx().send_viewport_cmd(egui::ViewportCommand::Close); cb.exit();
modal.close(); modal.close();
} else { } else {
Node::stop(true); Node::stop(true);
@ -260,9 +238,9 @@ impl Content {
} }
/// Handle Back key event. /// Handle Back key event.
pub fn on_back(&mut self) { pub fn on_back(&mut self, cb: &dyn PlatformCallbacks) {
if Modal::on_back() { if Modal::on_back() {
if self.wallets.on_back() { if self.wallets.on_back(cb) {
Self::show_exit_modal() Self::show_exit_modal()
} }
} }
@ -272,14 +250,7 @@ impl Content {
pub fn settings_modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal) { pub fn settings_modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal) {
ui.add_space(6.0); ui.add_space(6.0);
// Draw chain type selection. // Show theme selection.
NodeSetup::chain_type_ui(ui);
ui.add_space(8.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(8.0);
// Draw theme selection.
Self::theme_selection_ui(ui); Self::theme_selection_ui(ui);
ui.add_space(8.0); ui.add_space(8.0);
@ -348,7 +319,6 @@ impl Content {
let item_rounding = View::item_rounding(index, len, false); 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());
ui.vertical(|ui| {
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| { ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
// Draw button to select language. // Draw button to select language.
let is_current = if let Some(lang) = AppConfig::locale() { let is_current = if let Some(lang) = AppConfig::locale() {
@ -386,7 +356,6 @@ impl Content {
}); });
}); });
}); });
});
} }
/// Draw content for integrated node warning [`Modal`] on Android. /// Draw content for integrated node warning [`Modal`] on Android.
@ -422,10 +391,10 @@ impl Content {
let text = format!("{} {}", FILE_X, t!("share")); let text = format!("{} {}", FILE_X, t!("share"));
View::colored_text_button(ui, text, Colors::blue(), Colors::white_or_black(false), || { View::colored_text_button(ui, text, Colors::blue(), Colors::white_or_black(false), || {
if let Ok(data) = fs::read_to_string(Settings::crash_report_path()) { if let Ok(data) = fs::read_to_string(Settings::crash_report_path()) {
cb.share_data(Settings::CRASH_REPORT_FILE_NAME.to_string(), let name = Settings::CRASH_REPORT_FILE_NAME.to_string();
data.as_bytes().to_vec()).unwrap_or_default() let _ = cb.share_data(name, data.as_bytes().to_vec());
} }
AppConfig::set_show_crash(false); Settings::delete_crash_report();
modal.close(); modal.close();
}); });
}); });
@ -434,10 +403,29 @@ impl Content {
ui.add_space(8.0); ui.add_space(8.0);
ui.vertical_centered_justified(|ui| { ui.vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || { View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
AppConfig::set_show_crash(false); Settings::delete_crash_report();
modal.close(); modal.close();
}); });
}); });
ui.add_space(6.0); ui.add_space(6.0);
} }
} }
/// Get [`NetworkContent`] panel state and width.
fn network_panel_state_width(ctx: &egui::Context, dual_panel: bool) -> (bool, f32) {
let is_panel_open = dual_panel || Content::is_network_panel_open();
let panel_width = if dual_panel {
Content::SIDE_PANEL_WIDTH + View::get_left_inset()
} else {
let is_fullscreen = ctx.input(|i| {
i.viewport().fullscreen.unwrap_or(false)
});
View::window_size(ctx).0 - if View::is_desktop() && !is_fullscreen &&
OperatingSystem::from_target_os() != OperatingSystem::Mac {
Content::WINDOW_FRAME_MARGIN * 2.0
} else {
0.0
}
};
(is_panel_open, panel_width)
}

View file

@ -78,8 +78,8 @@ impl FilePickButton {
} }
} else { } else {
// Draw button to pick file. // Draw button to pick file.
let file_text = format!("{} {}", ARCHIVE_BOX, t!("choose_file")); let text = format!("{} {}", ARCHIVE_BOX, t!("choose_file"));
View::colored_text_button(ui, file_text, Colors::blue(), Colors::button(), || { View::colored_text_button(ui, text, Colors::blue(), Colors::white_or_black(false), || {
if let Some(path) = cb.pick_file() { if let Some(path) = cb.pick_file() {
self.on_file_pick(path); self.on_file_pick(path);
} }

View file

@ -36,8 +36,11 @@ pub use camera::*;
mod qr; mod qr;
pub use qr::*; pub use qr::*;
mod file; mod file_pick;
pub use file::*; pub use file_pick::*;
mod pull_to_refresh; mod pull_to_refresh;
pub use pull_to_refresh::*; pub use pull_to_refresh::*;
mod scan;
pub use scan::*;

View file

@ -16,7 +16,7 @@ use lazy_static::lazy_static;
use std::sync::Arc; use std::sync::Arc;
use parking_lot::RwLock; use parking_lot::RwLock;
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::{AtomicBool, Ordering};
use egui::{Align2, Rect, RichText, Rounding, Stroke, Vec2}; use egui::{Align2, RichText, Rounding, Stroke, UiBuilder, Vec2};
use egui::epaint::{RectShape, Shadow}; use egui::epaint::{RectShape, Shadow};
use egui::os::OperatingSystem; use egui::os::OperatingSystem;
@ -29,17 +29,17 @@ lazy_static! {
static ref MODAL_STATE: Arc<RwLock<ModalState>> = Arc::new(RwLock::new(ModalState::default())); static ref MODAL_STATE: Arc<RwLock<ModalState>> = Arc::new(RwLock::new(ModalState::default()));
} }
/// Stores data to draw modal [`egui::Window`] at ui. /// Modal [`egui::Window`] container.
#[derive(Clone)] #[derive(Clone)]
pub struct Modal { pub struct Modal {
/// Identifier for modal. /// Identifier for modal.
pub(crate) id: &'static str, pub(crate) id: &'static str,
/// Position on the screen. /// Position on the screen.
position: ModalPosition, pub position: ModalPosition,
/// To check if it can be closed. /// Flag to check if modal can be closed by keys.
closeable: Arc<AtomicBool>, closeable: Arc<AtomicBool>,
/// Title text /// Title text.
title: Option<String> title: Option<String>,
} }
impl Modal { impl Modal {
@ -54,7 +54,7 @@ impl Modal {
id, id,
position: ModalPosition::Center, position: ModalPosition::Center,
closeable: Arc::new(AtomicBool::new(true)), closeable: Arc::new(AtomicBool::new(true)),
title: None title: None,
} }
} }
@ -64,6 +64,12 @@ impl Modal {
self self
} }
/// Change [`Modal`] position on the screen.
pub fn change_position(position: ModalPosition) {
let mut w_state = MODAL_STATE.write();
w_state.modal.as_mut().unwrap().position = position;
}
/// Mark [`Modal`] closed. /// Mark [`Modal`] closed.
pub fn close(&self) { pub fn close(&self) {
let mut w_nav = MODAL_STATE.write(); let mut w_nav = MODAL_STATE.write();
@ -104,7 +110,7 @@ impl Modal {
} }
/// Remove [`Modal`] from [`ModalState`] if it's showing and can be closed. /// Remove [`Modal`] from [`ModalState`] if it's showing and can be closed.
/// Return `false` if Modal existed in [`ModalState`] before call. /// Return `false` if modal existed in state before call.
pub fn on_back() -> bool { pub fn on_back() -> bool {
let mut w_state = MODAL_STATE.write(); let mut w_state = MODAL_STATE.write();
@ -119,7 +125,7 @@ impl Modal {
true true
} }
/// Return id of opened [`Modal`]. /// Return identifier of opened [`Modal`].
pub fn opened() -> Option<&'static str> { pub fn opened() -> Option<&'static str> {
// Check if modal is showing. // Check if modal is showing.
{ {
@ -134,6 +140,19 @@ impl Modal {
Some(modal.id) Some(modal.id)
} }
/// Check if [`Modal`] is opened and can be closed.
pub fn opened_closeable() -> bool {
// Check if modal is showing.
{
if MODAL_STATE.read().modal.is_none() {
return false;
}
}
let r_state = MODAL_STATE.read();
let modal = r_state.modal.as_ref().unwrap();
modal.closeable.load(Ordering::Relaxed)
}
/// Set title text for current opened [`Modal`]. /// Set title text for current opened [`Modal`].
pub fn set_title(title: String) { pub fn set_title(title: String) {
// Save state. // Save state.
@ -164,40 +183,42 @@ impl Modal {
let is_fullscreen = ctx.input(|i| { let is_fullscreen = ctx.input(|i| {
i.viewport().fullscreen.unwrap_or(false) i.viewport().fullscreen.unwrap_or(false)
}); });
let is_mac_os = OperatingSystem::from_target_os() == OperatingSystem::Mac;
let mut rect = ctx.screen_rect(); // Setup background rect.
if View::is_desktop() && !is_mac_os { let bg_rect = if View::is_desktop() {
let margin = if !is_fullscreen { let mut r = ctx.screen_rect();
Content::WINDOW_FRAME_MARGIN let is_mac = OperatingSystem::Mac == OperatingSystem::from_target_os();
} else { if !is_mac && !is_fullscreen {
0.0 r = r.shrink(Content::WINDOW_FRAME_MARGIN - 1.0);
};
rect = rect.shrink(margin - 0.5);
rect.min += egui::vec2(0.0, Content::WINDOW_TITLE_HEIGHT + 0.5);
rect.max.x += 0.5;
} }
r.min.y += Content::WINDOW_TITLE_HEIGHT;
r
} else {
ctx.screen_rect()
};
// Draw modal background.
egui::Window::new("modal_bg_window") egui::Window::new("modal_bg_window")
.title_bar(false) .title_bar(false)
.resizable(false) .resizable(false)
.collapsible(false) .collapsible(false)
.fixed_rect(rect) .fixed_rect(bg_rect)
.frame(egui::Frame { .frame(egui::Frame {
fill: Colors::semi_transparent(), fill: Colors::semi_transparent(),
..Default::default() ..Default::default()
}) })
.show(ctx, |ui| { .show(ctx, |ui| {
ui.set_min_size(rect.size()); ui.set_min_size(bg_rect.size());
}); });
// Setup width of modal content. // Setup width of modal content.
let side_insets = View::get_left_inset() + View::get_right_inset(); let side_insets = View::get_left_inset() + View::get_right_inset();
let available_width = rect.width() - (side_insets + Self::DEFAULT_MARGIN); let available_width = ctx.screen_rect().width() - (side_insets + Self::DEFAULT_MARGIN);
let width = f32::min(available_width, Self::DEFAULT_WIDTH); let width = f32::min(available_width, Self::DEFAULT_WIDTH);
// Show main content Window at given position. // Show main content window at given position.
let (content_align, content_offset) = self.modal_position(is_fullscreen); let (content_align, content_offset) = self.modal_position();
let layer_id = egui::Window::new(format!("modal_window_{}", self.id)) let layer_id = egui::Window::new("modal_window")
.title_bar(false) .title_bar(false)
.resizable(false) .resizable(false)
.collapsible(false) .collapsible(false)
@ -212,33 +233,30 @@ impl Modal {
color: egui::Color32::from_black_alpha(32), color: egui::Color32::from_black_alpha(32),
}, },
rounding: Rounding::same(8.0), rounding: Rounding::same(8.0),
fill: Colors::fill(),
..Default::default() ..Default::default()
}) })
.show(ctx, |ui| { .show(ctx, |ui| {
if self.title.is_some() { if let Some(title) = &self.title {
self.title_ui(ui); title_ui(title, ui);
} }
self.content_ui(ui, add_content); self.content_ui(ui, add_content);
}).unwrap().response.layer_id; }).unwrap().response.layer_id;
// Always show main content Window above background Window. // Always show main content window above background window.
ctx.move_to_top(layer_id); ctx.move_to_top(layer_id);
} }
/// Get [`egui::Window`] position based on [`ModalPosition`]. /// Get [`egui::Window`] position based on [`ModalPosition`].
fn modal_position(&self, is_fullscreen: bool) -> (Align2, Vec2) { fn modal_position(&self) -> (Align2, Vec2) {
let align = match self.position { let align = match self.position {
ModalPosition::CenterTop => Align2::CENTER_TOP, ModalPosition::CenterTop => Align2::CENTER_TOP,
ModalPosition::Center => Align2::CENTER_CENTER ModalPosition::Center => Align2::CENTER_CENTER
}; };
let x_align = View::get_left_inset() - View::get_right_inset(); let x_align = View::get_left_inset() - View::get_right_inset();
let is_mac = OperatingSystem::Mac == OperatingSystem::from_target_os();
let is_mac_os = OperatingSystem::from_target_os() == OperatingSystem::Mac; let extra_y = if View::is_desktop() {
let extra_y = if View::is_desktop() && !is_mac_os { Content::WINDOW_TITLE_HEIGHT + if !is_mac {
Content::WINDOW_TITLE_HEIGHT + if !is_fullscreen {
Content::WINDOW_FRAME_MARGIN Content::WINDOW_FRAME_MARGIN
} else { } else {
0.0 0.0
@ -246,7 +264,7 @@ impl Modal {
} else { } else {
0.0 0.0
}; };
let y_align = View::get_top_inset() + Self::DEFAULT_MARGIN + extra_y; let y_align = View::get_top_inset() + Self::DEFAULT_MARGIN / 2.0 + extra_y;
let offset = match self.position { let offset = match self.position {
ModalPosition::CenterTop => Vec2::new(x_align, y_align), ModalPosition::CenterTop => Vec2::new(x_align, y_align),
@ -258,80 +276,64 @@ impl Modal {
/// Draw provided content. /// 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, add_content: impl FnOnce(&mut egui::Ui, &Modal)) {
let mut rect = ui.available_rect_before_wrap(); let mut rect = ui.available_rect_before_wrap();
rect.min += egui::emath::vec2(6.0, 0.0);
rect.max -= egui::emath::vec2(6.0, 0.0);
// Create background shape. // Create background shape.
let rounding = if self.title.is_some() { let mut bg_shape = RectShape::new(rect, if self.title.is_none() {
Rounding::same(8.0)
} else {
Rounding { Rounding {
nw: 0.0, nw: 0.0,
ne: 0.0, ne: 0.0,
sw: 8.0, sw: 8.0,
se: 8.0, se: 8.0,
} }
} else { }, Colors::fill(), Stroke::NONE);
Rounding::same(8.0)
};
let mut bg_shape = RectShape {
rect,
rounding,
fill: Colors::fill(),
stroke: Stroke::NONE,
blur_width: 0.0,
fill_texture_id: Default::default(),
uv: Rect::ZERO
};
let bg_idx = ui.painter().add(bg_shape); let bg_idx = ui.painter().add(bg_shape);
// Draw main content. rect.min += egui::emath::vec2(6.0, 0.0);
let mut content_rect = ui.allocate_ui_at_rect(rect, |ui| { 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); (add_content)(ui, self);
}).response.rect; }).response;
// Setup background shape to be painted behind main content. // Setup background size.
content_rect.min -= egui::emath::vec2(6.0, 0.0); let bg_rect = {
content_rect.max += egui::emath::vec2(6.0, 0.0); let mut r = resp.rect.clone();
bg_shape.rect = content_rect; r.min -= egui::emath::vec2(6.0, 0.0);
r.max += egui::emath::vec2(6.0, 0.0);
r
};
bg_shape.rect = bg_rect;
ui.painter().set(bg_idx, bg_shape); ui.painter().set(bg_idx, bg_shape);
} }
}
/// Draw title content. /// Draw title content.
fn title_ui(&self, ui: &mut egui::Ui) { fn title_ui(title: &String, ui: &mut egui::Ui) {
let rect = ui.available_rect_before_wrap(); let rect = ui.available_rect_before_wrap();
// Create background shape. // Create background shape.
let mut bg_shape = RectShape { let mut bg_shape = RectShape::new(rect, Rounding {
rect,
rounding: Rounding {
nw: 8.0, nw: 8.0,
ne: 8.0, ne: 8.0,
sw: 0.0, sw: 0.0,
se: 0.0, se: 0.0,
}, }, Colors::yellow(), Stroke::NONE);
fill: Colors::yellow(),
stroke: Stroke::NONE,
blur_width: 0.0,
fill_texture_id: Default::default(),
uv: Rect::ZERO
};
let bg_idx = ui.painter().add(bg_shape); let bg_idx = ui.painter().add(bg_shape);
// Draw title content. // Draw title content.
let title_resp = ui.allocate_ui_at_rect(rect, |ui| { let resp = ui.vertical_centered(|ui| {
ui.vertical_centered_justified(|ui| { ui.add_space(Modal::DEFAULT_MARGIN + 2.0);
ui.add_space(Self::DEFAULT_MARGIN + 1.0); ui.label(RichText::new(title)
ui.label(RichText::new(self.title.as_ref().unwrap())
.size(19.0) .size(19.0)
.color(Colors::title(true)) .color(Colors::title(true))
); );
ui.add_space(Self::DEFAULT_MARGIN); ui.add_space(Modal::DEFAULT_MARGIN + 1.0);
// Draw line below title. // Draw line below title.
View::horizontal_line(ui, Colors::item_stroke()); View::horizontal_line(ui, Colors::item_stroke());
});
}).response; }).response;
// Setup background shape to be painted behind title content. // Setup background size.
bg_shape.rect = title_resp.rect; bg_shape.rect = resp.rect;
ui.painter().set(bg_idx, bg_shape); ui.painter().set(bg_idx, bg_shape);
}
} }

View file

@ -16,7 +16,7 @@ use egui::{Align, Layout, RichText, Rounding};
use crate::AppConfig; use crate::AppConfig;
use crate::gui::Colors; use crate::gui::Colors;
use crate::gui::icons::{CARET_RIGHT, CHECK_CIRCLE, COMPUTER_TOWER, DOTS_THREE_CIRCLE, GLOBE_SIMPLE, PENCIL, PLUS_CIRCLE, POWER, TRASH, X_CIRCLE}; use crate::gui::icons::{CARET_RIGHT, CHECK_CIRCLE, COMPUTER_TOWER, DOTS_THREE_CIRCLE, GLOBE_SIMPLE, PENCIL, PLUS_CIRCLE, POWER, TRASH, WARNING_CIRCLE, X_CIRCLE};
use crate::gui::platform::PlatformCallbacks; use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Modal, View}; use crate::gui::views::{Modal, View};
use crate::gui::views::network::modals::ExternalConnectionModal; use crate::gui::views::network::modals::ExternalConnectionModal;
@ -36,7 +36,6 @@ pub struct ConnectionsContent {
impl Default for ConnectionsContent { impl Default for ConnectionsContent {
fn default() -> Self { fn default() -> Self {
ExternalConnection::check_ext_conn_availability(None);
Self { Self {
ext_conn_modal: ExternalConnectionModal::new(None), ext_conn_modal: ExternalConnectionModal::new(None),
modal_ids: vec![ modal_ids: vec![
@ -78,7 +77,7 @@ impl ConnectionsContent {
// Check connections availability. // Check connections availability.
if saved_chain_type != AppConfig::chain_type() { if saved_chain_type != AppConfig::chain_type() {
ExternalConnection::check_ext_conn_availability(None); ExternalConnection::check(None, ui.ctx());
} }
// Show integrated node info content. // Show integrated node info content.
@ -103,23 +102,20 @@ impl ConnectionsContent {
ui.add_space(4.0); ui.add_space(4.0);
let ext_conn_list = ConnectionsConfig::ext_conn_list(); let ext_conn_list = ConnectionsConfig::ext_conn_list();
if !ext_conn_list.is_empty() { let ext_conn_size = ext_conn_list.len();
if ext_conn_size != 0 {
ui.add_space(8.0); ui.add_space(8.0);
for (index, conn) in ext_conn_list.iter().enumerate() { for (index, conn) in ext_conn_list.iter().filter(|c| !c.deleted).enumerate() {
ui.horizontal_wrapped(|ui| { ui.horizontal_wrapped(|ui| {
// Draw connection list item. // Draw connection list item.
let len = ext_conn_list.len(); Self::ext_conn_item_ui(ui, conn, index, ext_conn_size, |ui| {
Self::ext_conn_item_ui(ui, conn, index, len, |ui| { let button_rounding = View::item_rounding(index, ext_conn_size, true);
// Draw buttons for non-default connections.
if conn.url != ExternalConnection::DEFAULT_MAIN_URL {
let button_rounding = View::item_rounding(index, len, true);
View::item_button(ui, button_rounding, TRASH, None, || { View::item_button(ui, button_rounding, TRASH, None, || {
ConnectionsConfig::remove_ext_conn(conn.id); ConnectionsConfig::remove_ext_conn(conn.id);
}); });
View::item_button(ui, Rounding::default(), PENCIL, None, || { View::item_button(ui, Rounding::default(), PENCIL, None, || {
self.show_add_ext_conn_modal(Some(conn.clone()), cb); self.show_add_ext_conn_modal(Some(conn.clone()), cb);
}); });
}
}); });
}); });
} }
@ -138,17 +134,18 @@ impl ConnectionsContent {
// Draw custom button. // Draw custom button.
custom_button(ui); custom_button(ui);
// Draw buttons to start/stop node.
if Node::get_error().is_none() {
if !Node::is_running() { if !Node::is_running() {
// Draw button to start integrated node.
View::item_button(ui, Rounding::default(), POWER, Some(Colors::green()), || { View::item_button(ui, Rounding::default(), POWER, Some(Colors::green()), || {
Node::start(); Node::start();
}); });
} else if !Node::is_starting() && !Node::is_stopping() && !Node::is_restarting() { } else if !Node::is_starting() && !Node::is_stopping() && !Node::is_restarting() {
// Draw button to stop integrated node.
View::item_button(ui, Rounding::default(), POWER, Some(Colors::red()), || { View::item_button(ui, Rounding::default(), POWER, Some(Colors::red()), || {
Node::stop(false); Node::stop(false);
}); });
} }
}
let layout_size = ui.available_size(); let layout_size = ui.available_size();
ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| { ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| {
@ -163,15 +160,22 @@ impl ConnectionsContent {
}); });
// Setup node status text. // Setup node status text.
let status_icon = if !Node::is_running() { let has_error = Node::get_error().is_some();
let status_icon = if has_error {
WARNING_CIRCLE
} else if !Node::is_running() {
X_CIRCLE X_CIRCLE
} else if Node::not_syncing() { } else if Node::not_syncing() {
CHECK_CIRCLE CHECK_CIRCLE
} else { } else {
DOTS_THREE_CIRCLE DOTS_THREE_CIRCLE
}; };
let status_text = format!("{} {}", status_icon, Node::get_sync_status_text()); let status_text = format!("{} {}", status_icon, if has_error {
ui.label(RichText::new(status_text).size(15.0).color(Colors::text(false))); t!("error")
} else {
Node::get_sync_status_text()
});
View::ellipsize_text(ui, status_text, 15.0, Colors::text(false));
ui.add_space(1.0); ui.add_space(1.0);
// Setup node API address text. // Setup node API address text.
@ -198,7 +202,6 @@ impl ConnectionsContent {
let item_rounding = View::item_rounding(index, len, false); 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());
ui.vertical(|ui| {
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| { ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
// Draw provided buttons. // Draw provided buttons.
buttons_ui(ui); buttons_ui(ui);
@ -228,7 +231,6 @@ impl ConnectionsContent {
}); });
}); });
}); });
});
} }
/// Show [`Modal`] to add external connection. /// Show [`Modal`] to add external connection.

View file

@ -12,7 +12,6 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
use std::time::Duration;
use egui::{Id, Margin, RichText, ScrollArea}; use egui::{Id, Margin, RichText, ScrollArea};
use egui::scroll_area::ScrollBarVisibility; use egui::scroll_area::ScrollBarVisibility;
@ -22,15 +21,15 @@ use crate::gui::icons::{ARROWS_COUNTER_CLOCKWISE, BRIEFCASE, DATABASE, DOTS_THRE
use crate::gui::platform::PlatformCallbacks; use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Content, TitlePanel, View}; use crate::gui::views::{Content, TitlePanel, View};
use crate::gui::views::network::{ConnectionsContent, NetworkMetrics, NetworkMining, NetworkNode, NetworkSettings}; use crate::gui::views::network::{ConnectionsContent, NetworkMetrics, NetworkMining, NetworkNode, NetworkSettings};
use crate::gui::views::network::types::{NetworkTab, NetworkTabType}; use crate::gui::views::network::types::{NodeTab, NodeTabType};
use crate::gui::views::types::{TitleContentType, TitleType}; use crate::gui::views::types::{LinePosition, TitleContentType, TitleType};
use crate::node::{Node, NodeError}; use crate::node::{Node, NodeConfig, NodeError};
use crate::wallet::ExternalConnection; use crate::wallet::ExternalConnection;
/// Network content. /// Network content.
pub struct NetworkContent { pub struct NetworkContent {
/// Current integrated node tab content. /// Current integrated node tab content.
node_tab_content: Box<dyn NetworkTab>, node_tab_content: Box<dyn NodeTab>,
/// Connections content. /// Connections content.
connections: ConnectionsContent, connections: ConnectionsContent,
} }
@ -47,14 +46,14 @@ impl Default for NetworkContent {
impl NetworkContent { impl NetworkContent {
pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
let show_connections = AppConfig::show_connections_network_panel(); let show_connections = AppConfig::show_connections_network_panel();
let dual_panel = Content::is_dual_panel_mode(ui); let dual_panel = Content::is_dual_panel_mode(ui.ctx());
// Show title panel. // Show title panel.
self.title_ui(ui, show_connections); self.title_ui(ui, dual_panel, show_connections);
// Show integrated node tabs content. // Show integrated node tabs content.
if !show_connections { if !show_connections {
egui::TopBottomPanel::bottom("node_tabs_content") egui::TopBottomPanel::bottom("node_tabs")
.min_height(0.5) .min_height(0.5)
.resizable(false) .resizable(false)
.frame(egui::Frame { .frame(egui::Frame {
@ -68,15 +67,23 @@ impl NetworkContent {
..Default::default() ..Default::default()
}) })
.show_inside(ui, |ui| { .show_inside(ui, |ui| {
ui.vertical_centered(|ui| { let rect = ui.available_rect_before_wrap();
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| { View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
self.tabs_ui(ui); self.tabs_ui(ui);
}); });
}); // Draw content divider line.
let r = {
let mut r = rect.clone();
r.min.x -= View::get_left_inset() + View::TAB_ITEMS_PADDING;
r.min.y -= View::TAB_ITEMS_PADDING;
r.max.x += View::far_right_inset_margin(ui) + View::TAB_ITEMS_PADDING;
r
};
View::line(ui, LinePosition::TOP, &r, Colors::stroke());
}); });
} }
// Show current node tab content. // Show integrated node tab content.
egui::SidePanel::right("node_tab_content") egui::SidePanel::right("node_tab_content")
.resizable(false) .resizable(false)
.exact_width(ui.available_width()) .exact_width(ui.available_width())
@ -86,8 +93,6 @@ impl NetworkContent {
.show_animated_inside(ui, !show_connections, |ui| { .show_animated_inside(ui, !show_connections, |ui| {
egui::CentralPanel::default() egui::CentralPanel::default()
.frame(egui::Frame { .frame(egui::Frame {
fill: Colors::white_or_black(false),
stroke: View::item_stroke(),
inner_margin: Margin { inner_margin: Margin {
left: View::get_left_inset() + 4.0, left: View::get_left_inset() + 4.0,
right: View::far_right_inset_margin(ui) + 4.0, right: View::far_right_inset_margin(ui) + 4.0,
@ -97,14 +102,42 @@ impl NetworkContent {
..Default::default() ..Default::default()
}) })
.show_inside(ui, |ui| { .show_inside(ui, |ui| {
let rect = ui.available_rect_before_wrap();
if self.node_tab_content.get_type() != NodeTabType::Settings {
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
let node_err = Node::get_error();
if let Some(err) = node_err {
node_error_ui(ui, err);
} else if !Node::is_running() {
disabled_node_ui(ui);
} else if Node::get_stats().is_none() || Node::is_restarting() ||
Node::is_stopping() {
NetworkContent::loading_ui(ui, None);
} else {
self.node_tab_content.ui(ui, cb); self.node_tab_content.ui(ui, cb);
}
});
} else {
self.node_tab_content.ui(ui, cb);
}
// Draw content divider line.
let r = {
let mut r = rect.clone();
r.min.y -= 3.0;
r.max.x += 4.0;
r.max.y += 4.0;
r
};
if dual_panel {
View::line(ui, LinePosition::RIGHT, &r, Colors::item_stroke());
}
}); });
}); });
// Show connections content. // Show connections content.
egui::CentralPanel::default() egui::CentralPanel::default()
.frame(egui::Frame { .frame(egui::Frame {
stroke: View::item_stroke(),
inner_margin: Margin { inner_margin: Margin {
left: if show_connections { left: if show_connections {
View::get_left_inset() + 4.0 View::get_left_inset() + 4.0
@ -117,18 +150,14 @@ impl NetworkContent {
0.0 0.0
}, },
top: 3.0, top: 3.0,
bottom: if View::is_desktop() && show_connections { bottom: 4.0 + View::get_bottom_inset(),
6.0
} else {
4.0
}, },
},
fill: Colors::button(),
..Default::default() ..Default::default()
}) })
.show_inside(ui, |ui| { .show_inside(ui, |ui| {
let rect = ui.available_rect_before_wrap();
ScrollArea::vertical() ScrollArea::vertical()
.id_source("connections_content") .id_salt("connections_scroll")
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden) .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.auto_shrink([false; 2]) .auto_shrink([false; 2])
.show(ui, |ui| { .show(ui, |ui| {
@ -144,17 +173,26 @@ impl NetworkContent {
}); });
}); });
}); });
// Draw content divider line.
let r = {
let mut r = rect.clone();
r.min.y -= 3.0;
r.max.x += 4.0;
r.max.y += 4.0 + View::get_bottom_inset();
r
};
if show_connections && dual_panel {
View::line(ui, LinePosition::RIGHT, &r, Colors::item_stroke());
}
}); });
// Redraw after delay. // Redraw after delay if node is running at non-dual-panel mode.
if Node::is_running() { if !dual_panel && Content::is_network_panel_open() && Node::is_running() {
ui.ctx().request_repaint_after(Node::STATS_UPDATE_DELAY); ui.ctx().request_repaint_after(Node::STATS_UPDATE_DELAY);
} else if show_connections {
ui.ctx().request_repaint_after(Duration::from_millis(1000));
} }
} }
/// Draw tab buttons in the bottom of the screen. /// Draw tab buttons at bottom of the screen.
fn tabs_ui(&mut self, ui: &mut egui::Ui) { fn tabs_ui(&mut self, ui: &mut egui::Ui) {
ui.vertical_centered(|ui| { ui.vertical_centered(|ui| {
// Setup spacing between tabs. // Setup spacing between tabs.
@ -166,22 +204,22 @@ impl NetworkContent {
let current_type = self.node_tab_content.get_type(); let current_type = self.node_tab_content.get_type();
ui.columns(4, |columns| { ui.columns(4, |columns| {
columns[0].vertical_centered_justified(|ui| { columns[0].vertical_centered_justified(|ui| {
View::tab_button(ui, DATABASE, current_type == NetworkTabType::Node, || { View::tab_button(ui, DATABASE, current_type == NodeTabType::Info, |_| {
self.node_tab_content = Box::new(NetworkNode::default()); self.node_tab_content = Box::new(NetworkNode::default());
}); });
}); });
columns[1].vertical_centered_justified(|ui| { columns[1].vertical_centered_justified(|ui| {
View::tab_button(ui, GAUGE, current_type == NetworkTabType::Metrics, || { View::tab_button(ui, GAUGE, current_type == NodeTabType::Metrics, |_| {
self.node_tab_content = Box::new(NetworkMetrics::default()); self.node_tab_content = Box::new(NetworkMetrics::default());
}); });
}); });
columns[2].vertical_centered_justified(|ui| { columns[2].vertical_centered_justified(|ui| {
View::tab_button(ui, FACTORY, current_type == NetworkTabType::Mining, || { View::tab_button(ui, FACTORY, current_type == NodeTabType::Mining, |_| {
self.node_tab_content = Box::new(NetworkMining::default()); self.node_tab_content = Box::new(NetworkMining::default());
}); });
}); });
columns[3].vertical_centered_justified(|ui| { columns[3].vertical_centered_justified(|ui| {
View::tab_button(ui, FADERS, current_type == NetworkTabType::Settings, || { View::tab_button(ui, FADERS, current_type == NodeTabType::Settings, |_| {
self.node_tab_content = Box::new(NetworkSettings::default()); self.node_tab_content = Box::new(NetworkSettings::default());
}); });
}); });
@ -190,29 +228,29 @@ impl NetworkContent {
} }
/// Draw title content. /// Draw title content.
fn title_ui(&mut self, ui: &mut egui::Ui, show_connections: bool) { fn title_ui(&mut self, ui: &mut egui::Ui, dual_panel: bool, show_connections: bool) {
// Setup values for title panel. // Setup values for title panel.
let title_text = self.node_tab_content.get_type().title().to_uppercase(); let title_text = self.node_tab_content.get_type().title();
let subtitle_text = Node::get_sync_status_text(); let subtitle_text = Node::get_sync_status_text();
let not_syncing = Node::not_syncing(); let not_syncing = Node::not_syncing();
let title_content = if !show_connections { let title_content = if !show_connections {
TitleContentType::WithSubTitle(title_text, subtitle_text, !not_syncing) TitleContentType::WithSubTitle(title_text, subtitle_text, !not_syncing)
} else { } else {
TitleContentType::Title(t!("network.connections").to_uppercase()) TitleContentType::Title(t!("network.connections"))
}; };
// Draw title panel. // Draw title panel.
TitlePanel::new(Id::from("network_title_panel")).ui(TitleType::Single(title_content), |ui| { TitlePanel::new(Id::from("network_title_panel")).ui(TitleType::Single(title_content), |ui| {
if !show_connections { if !show_connections {
View::title_button_big(ui, DOTS_THREE_OUTLINE_VERTICAL, |_| { View::title_button_big(ui, DOTS_THREE_OUTLINE_VERTICAL, |ui| {
AppConfig::toggle_show_connections_network_panel(); AppConfig::toggle_show_connections_network_panel();
if AppConfig::show_connections_network_panel() { if AppConfig::show_connections_network_panel() {
ExternalConnection::check_ext_conn_availability(None); ExternalConnection::check(None, ui.ctx());
} }
}); });
} }
}, |ui| { }, |ui| {
if !Content::is_dual_panel_mode(ui) { if !dual_panel {
View::title_button_big(ui, BRIEFCASE, |_| { View::title_button_big(ui, BRIEFCASE, |_| {
Content::toggle_network_panel(); Content::toggle_network_panel();
}); });
@ -220,23 +258,6 @@ impl NetworkContent {
}, ui); }, ui);
} }
/// Content to draw when node is disabled.
pub fn disabled_node_ui(ui: &mut egui::Ui) {
View::center_content(ui, 156.0, |ui| {
let text = t!("network.disabled_server", "dots" => DOTS_THREE_OUTLINE_VERTICAL);
ui.label(RichText::new(text)
.size(16.0)
.color(Colors::inactive_text())
);
ui.add_space(8.0);
View::action_button(ui, format!("{} {}", POWER, t!("network.enable_node")), || {
Node::start();
});
ui.add_space(2.0);
Self::autorun_node_ui(ui);
});
}
/// Content to draw on loading. /// Content to draw on loading.
pub fn loading_ui(ui: &mut egui::Ui, text: Option<String>) { pub fn loading_ui(ui: &mut egui::Ui, text: Option<String>) {
match text { match text {
@ -265,9 +286,27 @@ impl NetworkContent {
AppConfig::toggle_node_autostart(); AppConfig::toggle_node_autostart();
}); });
} }
}
/// Draw integrated node error content. /// Content to draw when node is disabled.
pub fn node_error_ui(ui: &mut egui::Ui, e: NodeError) { fn disabled_node_ui(ui: &mut egui::Ui) {
View::center_content(ui, 156.0, |ui| {
let text = t!("network.disabled_server", "dots" => DOTS_THREE_OUTLINE_VERTICAL);
ui.label(RichText::new(text)
.size(16.0)
.color(Colors::inactive_text())
);
ui.add_space(8.0);
View::action_button(ui, format!("{} {}", POWER, t!("network.enable_node")), || {
Node::start();
});
ui.add_space(2.0);
NetworkContent::autorun_node_ui(ui);
});
}
/// Draw integrated node error content.
pub fn node_error_ui(ui: &mut egui::Ui, e: NodeError) {
match e { match e {
NodeError::Storage => { NodeError::Storage => {
View::center_content(ui, 156.0, |ui| { View::center_content(ui, 156.0, |ui| {
@ -312,6 +351,14 @@ impl NetworkContent {
.size(16.0) .size(16.0)
.color(Colors::red()) .color(Colors::red())
); );
ui.add_space(8.0);
let btn_txt = format!("{} {}",
ARROWS_COUNTER_CLOCKWISE,
t!("network_settings.reset"));
View::action_button(ui, btn_txt, || {
NodeConfig::reset_to_default();
Node::start();
});
ui.add_space(2.0); ui.add_space(2.0);
}); });
} }
@ -333,5 +380,4 @@ impl NetworkContent {
}); });
} }
} }
}
} }

View file

@ -14,6 +14,7 @@
use egui::{RichText, Rounding, ScrollArea, vec2}; use egui::{RichText, Rounding, ScrollArea, vec2};
use egui::scroll_area::ScrollBarVisibility; use egui::scroll_area::ScrollBarVisibility;
use grin_core::consensus::{DAY_HEIGHT, GRIN_BASE, HOUR_SEC, REWARD};
use grin_servers::{DiffBlock, ServerStats}; use grin_servers::{DiffBlock, ServerStats};
use crate::gui::Colors; use crate::gui::Colors;
@ -21,87 +22,61 @@ use crate::gui::icons::{AT, COINS, CUBE_TRANSPARENT, HOURGLASS_LOW, HOURGLASS_ME
use crate::gui::platform::PlatformCallbacks; use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Content, View}; use crate::gui::views::{Content, View};
use crate::gui::views::network::NetworkContent; use crate::gui::views::network::NetworkContent;
use crate::gui::views::network::types::{NetworkTab, NetworkTabType}; use crate::gui::views::network::types::{NodeTab, NodeTabType};
use crate::node::Node; use crate::node::Node;
/// Chain metrics tab content. /// Chain metrics tab content.
#[derive(Default)] #[derive(Default)]
pub struct NetworkMetrics; pub struct NetworkMetrics;
const BLOCK_REWARD: f64 = 60.0; const BLOCK_REWARD: u64 = REWARD / GRIN_BASE;
// 1 year is calculated as 365 days and 6 hours (31557600). // 1 year as 365 days and 6 hours (31557600).
const YEARLY_SUPPLY: f64 = ((60 * 60 * 24 * 365) + 6 * 60 * 60) as f64; const YEARLY_SUPPLY: u64 = (BLOCK_REWARD * DAY_HEIGHT * 365) + 6 * HOUR_SEC;
impl NetworkTab for NetworkMetrics { impl NodeTab for NetworkMetrics {
fn get_type(&self) -> NetworkTabType { fn get_type(&self) -> NodeTabType {
NetworkTabType::Metrics NodeTabType::Metrics
} }
fn ui(&mut self, ui: &mut egui::Ui, _: &dyn PlatformCallbacks) { fn ui(&mut self, ui: &mut egui::Ui, _: &dyn PlatformCallbacks) {
// Show an error content when available.
let node_err = Node::get_error();
if node_err.is_some() {
NetworkContent::node_error_ui(ui, node_err.unwrap());
return;
}
// Show message to enable node when it's not running.
if !Node::is_running() {
NetworkContent::disabled_node_ui(ui);
return;
}
// Show loading spinner when node is stopping.
if Node::is_stopping() {
NetworkContent::loading_ui(ui, None);
return;
}
// Show message when metrics are not available.
let server_stats = Node::get_stats(); let server_stats = Node::get_stats();
if server_stats.is_none() || Node::is_restarting() let stats = server_stats.as_ref().unwrap();
|| server_stats.as_ref().unwrap().diff_stats.height == 0 { if stats.diff_stats.height == 0 {
NetworkContent::loading_ui(ui, Some(t!("network_metrics.loading"))); NetworkContent::loading_ui(ui, Some(t!("network_metrics.loading")));
return; return;
} }
ui.add_space(1.0); ui.add_space(1.0);
ui.vertical_centered(|ui| {
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| { View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
let stats = server_stats.as_ref().unwrap();
// Show emission and difficulty info. // Show emission and difficulty info.
info_ui(ui, stats); info_ui(ui, stats);
// Show difficulty adjustment window blocks. // Show difficulty adjustment window blocks.
blocks_ui(ui, stats); blocks_ui(ui, stats);
}); });
});
} }
} }
const BLOCK_ITEM_HEIGHT: f32 = 78.0;
/// Draw emission and difficulty info. /// Draw emission and difficulty info.
fn info_ui(ui: &mut egui::Ui, stats: &ServerStats) { fn info_ui(ui: &mut egui::Ui, stats: &ServerStats) {
// Show emission info. // Show emission info.
View::sub_title(ui, format!("{} {}", COINS, t!("network_metrics.emission"))); View::sub_title(ui, format!("{} {}", COINS, t!("network_metrics.emission")));
ui.columns(3, |columns| { ui.columns(3, |columns| {
let supply = stats.header_stats.height as f64 * BLOCK_REWARD; let supply = stats.header_stats.height * BLOCK_REWARD;
let rate = (YEARLY_SUPPLY * 100.0) / supply; let rate = (YEARLY_SUPPLY * 100) / supply;
columns[0].vertical_centered(|ui| { columns[0].vertical_centered(|ui| {
View::rounded_box(ui, View::label_box(ui,
format!("{}", BLOCK_REWARD), format!("{}", BLOCK_REWARD),
t!("network_metrics.reward"), t!("network_metrics.reward"),
[true, false, true, false]); [true, false, true, false]);
}); });
columns[1].vertical_centered(|ui| { columns[1].vertical_centered(|ui| {
View::rounded_box(ui, View::label_box(ui,
format!("{:.2}%", rate), format!("{:.2}%", rate),
t!("network_metrics.inflation"), t!("network_metrics.inflation"),
[false, false, false, false]); [false, false, false, false]);
}); });
columns[2].vertical_centered(|ui| { columns[2].vertical_centered(|ui| {
View::rounded_box(ui, View::label_box(ui,
supply.to_string(), supply.to_string(),
t!("network_metrics.supply"), t!("network_metrics.supply"),
[false, true, false, true]); [false, true, false, true]);
@ -117,19 +92,19 @@ fn info_ui(ui: &mut egui::Ui, stats: &ServerStats) {
View::sub_title(ui, format!("{} {}", HOURGLASS_MEDIUM, difficulty_title)); View::sub_title(ui, format!("{} {}", HOURGLASS_MEDIUM, difficulty_title));
ui.columns(3, |columns| { ui.columns(3, |columns| {
columns[0].vertical_centered(|ui| { columns[0].vertical_centered(|ui| {
View::rounded_box(ui, View::label_box(ui,
stats.diff_stats.height.to_string(), stats.diff_stats.height.to_string(),
t!("network_node.height"), t!("network_node.height"),
[true, false, true, false]); [true, false, true, false]);
}); });
columns[1].vertical_centered(|ui| { columns[1].vertical_centered(|ui| {
View::rounded_box(ui, View::label_box(ui,
format!("{}s", stats.diff_stats.average_block_time), format!("{}s", stats.diff_stats.average_block_time),
t!("network_metrics.block_time"), t!("network_metrics.block_time"),
[false, false, false, false]); [false, false, false, false]);
}); });
columns[2].vertical_centered(|ui| { columns[2].vertical_centered(|ui| {
View::rounded_box(ui, View::label_box(ui,
stats.diff_stats.average_difficulty.to_string(), stats.diff_stats.average_difficulty.to_string(),
t!("network_node.difficulty"), t!("network_node.difficulty"),
[false, true, false, true]); [false, true, false, true]);
@ -137,12 +112,14 @@ fn info_ui(ui: &mut egui::Ui, stats: &ServerStats) {
}); });
} }
const BLOCK_ITEM_HEIGHT: f32 = 77.0;
/// Draw difficulty adjustment window blocks content. /// Draw difficulty adjustment window blocks content.
fn blocks_ui(ui: &mut egui::Ui, stats: &ServerStats) { fn blocks_ui(ui: &mut egui::Ui, stats: &ServerStats) {
let blocks_size = stats.diff_stats.last_blocks.len(); let blocks_size = stats.diff_stats.last_blocks.len();
ui.add_space(4.0); ui.add_space(4.0);
ScrollArea::vertical() ScrollArea::vertical()
.id_source("difficulty_scroll") .id_salt("mining_difficulty_scroll")
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden) .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.auto_shrink([false; 2]) .auto_shrink([false; 2])
.stick_to_bottom(true) .stick_to_bottom(true)
@ -151,11 +128,8 @@ fn blocks_ui(ui: &mut egui::Ui, stats: &ServerStats) {
BLOCK_ITEM_HEIGHT, BLOCK_ITEM_HEIGHT,
blocks_size, blocks_size,
|ui, row_range| { |ui, row_range| {
for index in row_range {
// Add space before the first item.
if index == 0 {
ui.add_space(4.0); ui.add_space(4.0);
} for index in row_range {
let db = stats.diff_stats.last_blocks.get(index).unwrap(); let db = stats.diff_stats.last_blocks.get(index).unwrap();
block_item_ui(ui, db, View::item_rounding(index, blocks_size, false)); block_item_ui(ui, db, View::item_rounding(index, blocks_size, false));
} }
@ -167,11 +141,11 @@ fn blocks_ui(ui: &mut egui::Ui, stats: &ServerStats) {
fn block_item_ui(ui: &mut egui::Ui, db: &DiffBlock, rounding: Rounding) { fn block_item_ui(ui: &mut egui::Ui, db: &DiffBlock, rounding: Rounding) {
let mut rect = ui.available_rect_before_wrap(); let mut rect = ui.available_rect_before_wrap();
rect.set_height(BLOCK_ITEM_HEIGHT); rect.set_height(BLOCK_ITEM_HEIGHT);
ui.allocate_ui_at_rect(rect, |ui| { ui.allocate_ui(rect.size(), |ui| {
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.add_space(6.0); ui.add_space(6.0);
ui.vertical(|ui| { ui.vertical(|ui| {
ui.add_space(3.0); ui.add_space(4.0);
// Draw round background. // Draw round background.
rect.min += vec2(8.0, 0.0); rect.min += vec2(8.0, 0.0);
@ -180,24 +154,26 @@ fn block_item_ui(ui: &mut egui::Ui, db: &DiffBlock, rounding: Rounding) {
// Draw block hash. // Draw block hash.
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.add_space(7.0); ui.add_space(8.0);
ui.label(RichText::new(db.block_hash.to_string()) ui.label(RichText::new(db.block_hash.to_string())
.color(Colors::white_or_black(true)) .color(Colors::white_or_black(true))
.size(17.0)); .size(17.0));
}); });
// Draw block difficulty and height. // Draw block difficulty and height.
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.add_space(6.0); ui.add_space(7.0);
let diff_text = format!("{} {} {} {}", let diff_text = format!("{} {} {} {}",
CUBE_TRANSPARENT, CUBE_TRANSPARENT,
db.difficulty, db.difficulty,
AT, AT,
db.block_height); db.block_height);
ui.label(RichText::new(diff_text).color(Colors::title(false)).size(16.0)); ui.label(RichText::new(diff_text)
.color(Colors::title(false))
.size(15.0));
}); });
// Draw block date. // Draw block date.
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.add_space(6.0); ui.add_space(7.0);
let block_time = View::format_time(db.time as i64); let block_time = View::format_time(db.time as i64);
ui.label(RichText::new(format!("{} {}s {} {}", ui.label(RichText::new(format!("{} {}s {} {}",
TIMER, TIMER,
@ -205,7 +181,7 @@ fn block_item_ui(ui: &mut egui::Ui, db: &DiffBlock, rounding: Rounding) {
HOURGLASS_LOW, HOURGLASS_LOW,
block_time)) block_time))
.color(Colors::gray()) .color(Colors::gray())
.size(16.0)); .size(15.0));
}); });
ui.add_space(3.0); ui.add_space(3.0);
}); });

View file

@ -23,62 +23,30 @@ use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Content, View}; use crate::gui::views::{Content, View};
use crate::gui::views::network::NetworkContent; use crate::gui::views::network::NetworkContent;
use crate::gui::views::network::setup::StratumSetup; use crate::gui::views::network::setup::StratumSetup;
use crate::gui::views::network::types::{NetworkTab, NetworkTabType}; use crate::gui::views::network::types::{NodeTab, NodeTabType};
use crate::node::{Node, NodeConfig}; use crate::node::{Node, NodeConfig};
use crate::wallet::WalletConfig;
/// Mining tab content. /// Mining tab content.
pub struct NetworkMining { pub struct NetworkMining {
/// Stratum server setup content. /// Stratum server setup content.
stratum_server_setup: StratumSetup, stratum_server_setup: StratumSetup,
/// Wallet name for rewards.
wallet_name: String,
} }
impl Default for NetworkMining { impl Default for NetworkMining {
fn default() -> Self { fn default() -> Self {
let wallet_name = if let Some(id) = NodeConfig::get_stratum_wallet_id() {
WalletConfig::name_by_id(id).unwrap_or("-".to_string())
} else {
"-".to_string()
};
Self { Self {
stratum_server_setup: StratumSetup::default(), stratum_server_setup: StratumSetup::default(),
wallet_name,
} }
} }
} }
impl NetworkTab for NetworkMining { impl NodeTab for NetworkMining {
fn get_type(&self) -> NetworkTabType { fn get_type(&self) -> NodeTabType {
NetworkTabType::Mining NodeTabType::Mining
} }
fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
// Show an error content when available. if Node::is_stratum_starting() || Node::get_sync_status().unwrap() != SyncStatus::NoSync {
let node_err = Node::get_error();
if node_err.is_some() {
NetworkContent::node_error_ui(ui, node_err.unwrap());
return;
}
// Show message to enable node when it's not running.
if !Node::is_running() {
NetworkContent::disabled_node_ui(ui);
return;
}
// Show loading spinner when node is stopping or stratum server is starting.
if Node::is_stopping() || Node::is_stratum_starting() {
NetworkContent::loading_ui(ui, None);
return;
}
// Show message when mining is not available.
let server_stats = Node::get_stats();
if server_stats.is_none() || Node::is_restarting()
|| Node::get_sync_status().unwrap() != SyncStatus::NoSync {
NetworkContent::loading_ui(ui, Some(t!("network_mining.loading"))); NetworkContent::loading_ui(ui, Some(t!("network_mining.loading")));
return; return;
} }
@ -87,17 +55,15 @@ impl NetworkTab for NetworkMining {
let stratum_stats = Node::get_stratum_stats(); let stratum_stats = Node::get_stratum_stats();
if !stratum_stats.is_running { if !stratum_stats.is_running {
ScrollArea::vertical() ScrollArea::vertical()
.id_source("stratum_setup_scroll") .id_salt("stratum_setup_scroll")
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden) .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.auto_shrink([false; 2]) .auto_shrink([false; 2])
.show(ui, |ui| { .show(ui, |ui| {
ui.add_space(1.0); ui.add_space(1.0);
ui.vertical_centered(|ui| {
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| { View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
self.stratum_server_setup.ui(ui, cb); self.stratum_server_setup.ui(ui, cb);
}); });
}); });
});
return; return;
} }
@ -108,14 +74,17 @@ impl NetworkTab for NetworkMining {
ui.columns(2, |columns| { ui.columns(2, |columns| {
columns[0].vertical_centered(|ui| { columns[0].vertical_centered(|ui| {
let (stratum_addr, stratum_port) = NodeConfig::get_stratum_address(); let (stratum_addr, stratum_port) = NodeConfig::get_stratum_address();
View::rounded_box(ui, View::label_box(ui,
format!("{}:{}", stratum_addr, stratum_port), format!("{}:{}", stratum_addr, stratum_port),
t!("network_mining.address"), t!("network_mining.address"),
[true, false, true, false]); [true, false, true, false]);
}); });
columns[1].vertical_centered(|ui| { columns[1].vertical_centered(|ui| {
View::rounded_box(ui, View::label_box(ui,
self.wallet_name.clone(), self.stratum_server_setup
.wallet_name
.clone()
.unwrap_or("-".to_string()),
t!("network_mining.rewards_wallet"), t!("network_mining.rewards_wallet"),
[false, true, false, true]); [false, true, false, true]);
}); });
@ -131,7 +100,7 @@ impl NetworkTab for NetworkMining {
} else { } else {
"-".into() "-".into()
}; };
View::rounded_box(ui, View::label_box(ui,
difficulty, difficulty,
t!("network_node.difficulty"), t!("network_node.difficulty"),
[true, false, true, false]); [true, false, true, false]);
@ -142,7 +111,7 @@ impl NetworkTab for NetworkMining {
} else { } else {
"-".into() "-".into()
}; };
View::rounded_box(ui, View::label_box(ui,
block_height, block_height,
t!("network_node.header"), t!("network_node.header"),
[false, false, false, false]); [false, false, false, false]);
@ -153,7 +122,7 @@ impl NetworkTab for NetworkMining {
} else { } else {
"-".into() "-".into()
}; };
View::rounded_box(ui, View::label_box(ui,
hashrate, hashrate,
t!("network_mining.hashrate", "bits" => stratum_stats.edge_bits), t!("network_mining.hashrate", "bits" => stratum_stats.edge_bits),
[false, true, false, true]); [false, true, false, true]);
@ -165,14 +134,14 @@ impl NetworkTab for NetworkMining {
View::sub_title(ui, format!("{} {}", CPU, t!("network_mining.miners"))); View::sub_title(ui, format!("{} {}", CPU, t!("network_mining.miners")));
ui.columns(2, |columns| { ui.columns(2, |columns| {
columns[0].vertical_centered(|ui| { columns[0].vertical_centered(|ui| {
View::rounded_box(ui, View::label_box(ui,
stratum_stats.num_workers.to_string(), stratum_stats.num_workers.to_string(),
t!("network_mining.devices"), t!("network_mining.devices"),
[true, false, true, false]); [true, false, true, false]);
}); });
columns[1].vertical_centered(|ui| { columns[1].vertical_centered(|ui| {
View::rounded_box(ui, View::label_box(ui,
stratum_stats.blocks_found.to_string(), stratum_stats.blocks_found.to_string(),
t!("network_mining.blocks_found"), t!("network_mining.blocks_found"),
[false, true, false, true]); [false, true, false, true]);
@ -187,7 +156,7 @@ impl NetworkTab for NetworkMining {
View::horizontal_line(ui, Colors::item_stroke()); View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(4.0); ui.add_space(4.0);
ScrollArea::vertical() ScrollArea::vertical()
.id_source("stratum_workers_scroll") .id_salt("stratum_workers_scroll")
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden) .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.auto_shrink([false; 2]) .auto_shrink([false; 2])
.show_rows( .show_rows(

View file

@ -119,7 +119,7 @@ impl ExternalConnectionModal {
}); });
columns[1].vertical_centered_justified(|ui| { columns[1].vertical_centered_justified(|ui| {
// Add connection button callback. // Add connection button callback.
let mut on_add = || { let mut on_add = |ui: &mut egui::Ui| {
if !self.ext_node_url_edit.starts_with("http") { if !self.ext_node_url_edit.starts_with("http") {
self.ext_node_url_edit = format!("http://{}", self.ext_node_url_edit) self.ext_node_url_edit = format!("http://{}", self.ext_node_url_edit)
} }
@ -139,7 +139,7 @@ impl ExternalConnectionModal {
ext_conn.id = id; ext_conn.id = id;
} }
ConnectionsConfig::add_ext_conn(ext_conn.clone()); ConnectionsConfig::add_ext_conn(ext_conn.clone());
ExternalConnection::check_ext_conn_availability(Some(ext_conn.id)); ExternalConnection::check(Some(ext_conn.id), ui.ctx());
on_save(ext_conn); on_save(ext_conn);
// Close modal. // Close modal.
@ -150,10 +150,17 @@ impl ExternalConnectionModal {
modal.close(); modal.close();
} }
}; };
// Handle Enter key press.
let mut enter = false;
View::on_enter_key(ui, || { View::on_enter_key(ui, || {
(on_add)(); enter = true;
}); });
View::button(ui, if self.ext_conn_id.is_some() { if enter {
(on_add)(ui);
}
View::button_ui(ui, if self.ext_conn_id.is_some() {
t!("modal.save") t!("modal.save")
} else { } else {
t!("modal.add") t!("modal.add")

View file

@ -20,53 +20,30 @@ use crate::gui::Colors;
use crate::gui::icons::{AT, CUBE, DEVICES, FLOW_ARROW, HANDSHAKE, PACKAGE, SHARE_NETWORK}; use crate::gui::icons::{AT, CUBE, DEVICES, FLOW_ARROW, HANDSHAKE, PACKAGE, SHARE_NETWORK};
use crate::gui::platform::PlatformCallbacks; use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Content, View}; use crate::gui::views::{Content, View};
use crate::gui::views::network::NetworkContent; use crate::gui::views::network::types::{NodeTab, NodeTabType};
use crate::gui::views::network::types::{NetworkTab, NetworkTabType};
use crate::node::{Node, NodeConfig}; use crate::node::{Node, NodeConfig};
/// Integrated node tab content. /// Integrated node tab content.
#[derive(Default)] #[derive(Default)]
pub struct NetworkNode; pub struct NetworkNode;
impl NetworkTab for NetworkNode { impl NodeTab for NetworkNode {
fn get_type(&self) -> NetworkTabType { fn get_type(&self) -> NodeTabType {
NetworkTabType::Node NodeTabType::Info
} }
fn ui(&mut self, ui: &mut egui::Ui, _: &dyn PlatformCallbacks) { fn ui(&mut self, ui: &mut egui::Ui, _: &dyn PlatformCallbacks) {
// Show an error content when available.
let node_err = Node::get_error();
if node_err.is_some() {
NetworkContent::node_error_ui(ui, node_err.unwrap());
return;
}
// Show message to enable node when it's not running.
if !Node::is_running() {
NetworkContent::disabled_node_ui(ui);
return;
}
// Show loading spinner when stats are not available.
let server_stats = Node::get_stats();
if server_stats.is_none() || Node::is_restarting() || Node::is_stopping() {
NetworkContent::loading_ui(ui, None);
return;
}
ScrollArea::vertical() ScrollArea::vertical()
.id_source("integrated_node") .id_salt("integrated_node_info_scroll")
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden) .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.auto_shrink([false; 2]) .auto_shrink([false; 2])
.show(ui, |ui| { .show(ui, |ui| {
ui.add_space(2.0); ui.add_space(2.0);
ui.vertical_centered(|ui| {
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| { View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
// Show node stats content. // Show node stats content.
node_stats_ui(ui); node_stats_ui(ui);
}); });
}); });
});
} }
} }
@ -79,13 +56,13 @@ fn node_stats_ui(ui: &mut egui::Ui) {
View::sub_title(ui, format!("{} {}", FLOW_ARROW, t!("network_node.header"))); View::sub_title(ui, format!("{} {}", FLOW_ARROW, t!("network_node.header")));
ui.columns(2, |columns| { ui.columns(2, |columns| {
columns[0].vertical_centered(|ui| { columns[0].vertical_centered(|ui| {
View::rounded_box(ui, View::label_box(ui,
stats.header_stats.last_block_h.to_string(), stats.header_stats.last_block_h.to_string(),
t!("network_node.hash"), t!("network_node.hash"),
[true, false, false, false]); [true, false, false, false]);
}); });
columns[1].vertical_centered(|ui| { columns[1].vertical_centered(|ui| {
View::rounded_box(ui, View::label_box(ui,
stats.header_stats.height.to_string(), stats.header_stats.height.to_string(),
t!("network_node.height"), t!("network_node.height"),
[false, true, false, false]); [false, true, false, false]);
@ -93,7 +70,7 @@ fn node_stats_ui(ui: &mut egui::Ui) {
}); });
ui.columns(2, |columns| { ui.columns(2, |columns| {
columns[0].vertical_centered(|ui| { columns[0].vertical_centered(|ui| {
View::rounded_box(ui, View::label_box(ui,
stats.header_stats.total_difficulty.to_string(), stats.header_stats.total_difficulty.to_string(),
t!("network_node.difficulty"), t!("network_node.difficulty"),
[false, false, true, false]); [false, false, true, false]);
@ -101,7 +78,7 @@ fn node_stats_ui(ui: &mut egui::Ui) {
columns[1].vertical_centered(|ui| { columns[1].vertical_centered(|ui| {
let h_ts = stats.header_stats.latest_timestamp.timestamp(); let h_ts = stats.header_stats.latest_timestamp.timestamp();
let h_time = View::format_time(h_ts); let h_time = View::format_time(h_ts);
View::rounded_box(ui, View::label_box(ui,
h_time, h_time,
t!("network_node.time"), t!("network_node.time"),
[false, false, false, true]); [false, false, false, true]);
@ -113,13 +90,13 @@ fn node_stats_ui(ui: &mut egui::Ui) {
View::sub_title(ui, format!("{} {}", CUBE, t!("network_node.block"))); View::sub_title(ui, format!("{} {}", CUBE, t!("network_node.block")));
ui.columns(2, |columns| { ui.columns(2, |columns| {
columns[0].vertical_centered(|ui| { columns[0].vertical_centered(|ui| {
View::rounded_box(ui, View::label_box(ui,
stats.chain_stats.last_block_h.to_string(), stats.chain_stats.last_block_h.to_string(),
t!("network_node.hash"), t!("network_node.hash"),
[true, false, false, false]); [true, false, false, false]);
}); });
columns[1].vertical_centered(|ui| { columns[1].vertical_centered(|ui| {
View::rounded_box(ui, View::label_box(ui,
stats.chain_stats.height.to_string(), stats.chain_stats.height.to_string(),
t!("network_node.height"), t!("network_node.height"),
[false, true, false, false]); [false, true, false, false]);
@ -127,7 +104,7 @@ fn node_stats_ui(ui: &mut egui::Ui) {
}); });
ui.columns(2, |columns| { ui.columns(2, |columns| {
columns[0].vertical_centered(|ui| { columns[0].vertical_centered(|ui| {
View::rounded_box(ui, View::label_box(ui,
stats.chain_stats.total_difficulty.to_string(), stats.chain_stats.total_difficulty.to_string(),
t!("network_node.difficulty"), t!("network_node.difficulty"),
[false, false, true, false]); [false, false, true, false]);
@ -135,7 +112,7 @@ fn node_stats_ui(ui: &mut egui::Ui) {
columns[1].vertical_centered(|ui| { columns[1].vertical_centered(|ui| {
let b_ts = stats.chain_stats.latest_timestamp.timestamp(); let b_ts = stats.chain_stats.latest_timestamp.timestamp();
let b_time = View::format_time(b_ts); let b_time = View::format_time(b_ts);
View::rounded_box(ui, View::label_box(ui,
b_time, b_time,
t!("network_node.time"), t!("network_node.time"),
[false, false, false, true]); [false, false, false, true]);
@ -151,7 +128,7 @@ fn node_stats_ui(ui: &mut egui::Ui) {
None => "0 (0)".to_string(), None => "0 (0)".to_string(),
Some(tx) => format!("{} ({})", tx.tx_pool_size, tx.tx_pool_kernels) Some(tx) => format!("{} ({})", tx.tx_pool_size, tx.tx_pool_kernels)
}; };
View::rounded_box(ui, View::label_box(ui,
tx_stat, tx_stat,
t!("network_node.main_pool"), t!("network_node.main_pool"),
[true, false, false, false]); [true, false, false, false]);
@ -163,7 +140,7 @@ fn node_stats_ui(ui: &mut egui::Ui) {
stx.stem_pool_size, stx.stem_pool_size,
stx.stem_pool_kernels) stx.stem_pool_kernels)
}; };
View::rounded_box(ui, View::label_box(ui,
stem_tx_stat, stem_tx_stat,
t!("network_node.stem_pool"), t!("network_node.stem_pool"),
[false, true, false, false]); [false, true, false, false]);
@ -171,7 +148,7 @@ fn node_stats_ui(ui: &mut egui::Ui) {
}); });
ui.columns(2, |columns| { ui.columns(2, |columns| {
columns[0].vertical_centered(|ui| { columns[0].vertical_centered(|ui| {
View::rounded_box(ui, View::label_box(ui,
stats.disk_usage_gb.to_string(), stats.disk_usage_gb.to_string(),
t!("network_node.size"), t!("network_node.size"),
[false, false, true, false]); [false, false, true, false]);
@ -180,7 +157,7 @@ fn node_stats_ui(ui: &mut egui::Ui) {
let peers_txt = format!("{} ({})", let peers_txt = format!("{} ({})",
stats.peer_count, stats.peer_count,
NodeConfig::get_max_outbound_peers()); NodeConfig::get_max_outbound_peers());
View::rounded_box(ui, peers_txt, t!("network_node.peers"), [false, false, false, true]); View::label_box(ui, peers_txt, t!("network_node.peers"), [false, false, false, true]);
}); });
}); });
ui.add_space(5.0); ui.add_space(5.0);
@ -196,23 +173,27 @@ fn node_stats_ui(ui: &mut egui::Ui) {
} }
} }
const PEER_ITEM_HEIGHT: f32 = 77.0;
/// Draw connected peer info item. /// 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: Rounding) {
let mut rect = ui.available_rect_before_wrap(); let mut rect = ui.available_rect_before_wrap();
rect.set_height(79.0); rect.set_height(PEER_ITEM_HEIGHT);
ui.allocate_ui_at_rect(rect, |ui| { ui.allocate_ui(rect.size(), |ui| {
ui.vertical(|ui| { ui.vertical(|ui| {
ui.add_space(4.0); ui.add_space(4.0);
// Draw round background. // Draw round background.
ui.painter().rect(rect, rounding, Colors::white_or_black(false), View::item_stroke()); ui.painter().rect(rect, rounding, Colors::fill_lite(), View::item_stroke());
// Draw peer address // Draw IP address.
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.add_space(7.0); ui.add_space(7.0);
ui.label(RichText::new(&peer.addr).color(Colors::white_or_black(true)).size(17.0)); ui.label(RichText::new(&peer.addr)
.color(Colors::white_or_black(true))
.size(17.0));
}); });
// Draw peer difficulty and height // Draw difficulty and height.
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.add_space(6.0); ui.add_space(6.0);
let diff_text = format!("{} {} {} {}", let diff_text = format!("{} {} {} {}",
@ -220,13 +201,17 @@ fn peer_item_ui(ui: &mut egui::Ui, peer: &PeerStats, rounding: Rounding) {
peer.total_difficulty, peer.total_difficulty,
AT, AT,
peer.height); peer.height);
ui.label(RichText::new(diff_text).color(Colors::title(false)).size(16.0)); ui.label(RichText::new(diff_text)
.color(Colors::title(false))
.size(15.0));
}); });
// Draw peer user-agent // Draw user-agent.
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.add_space(6.0); ui.add_space(6.0);
let agent_text = format!("{} {}", DEVICES, &peer.user_agent); let agent_text = format!("{} {}", DEVICES, &peer.user_agent);
ui.label(RichText::new(agent_text).color(Colors::gray()).size(16.0)); ui.label(RichText::new(agent_text)
.color(Colors::gray())
.size(15.0));
}); });
ui.add_space(3.0); ui.add_space(3.0);

View file

@ -20,7 +20,7 @@ use crate::gui::icons::ARROW_COUNTER_CLOCKWISE;
use crate::gui::platform::PlatformCallbacks; use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Modal, Content, View}; use crate::gui::views::{Modal, Content, View};
use crate::gui::views::network::setup::{DandelionSetup, NodeSetup, P2PSetup, PoolSetup, StratumSetup}; use crate::gui::views::network::setup::{DandelionSetup, NodeSetup, P2PSetup, PoolSetup, StratumSetup};
use crate::gui::views::network::types::{NetworkTab, NetworkTabType}; use crate::gui::views::network::types::{NodeTab, NodeTabType};
use crate::gui::views::types::{ModalContainer, ModalPosition}; use crate::gui::views::types::{ModalContainer, ModalPosition};
use crate::node::{Node, NodeConfig}; use crate::node::{Node, NodeConfig};
@ -42,7 +42,7 @@ pub struct NetworkSettings {
} }
/// Identifier for settings reset confirmation [`Modal`]. /// Identifier for settings reset confirmation [`Modal`].
pub const RESET_SETTINGS_MODAL: &'static str = "reset_settings"; pub const RESET_SETTINGS_CONFIRMATION_MODAL: &'static str = "reset_settings_confirmation";
impl Default for NetworkSettings { impl Default for NetworkSettings {
fn default() -> Self { fn default() -> Self {
@ -53,7 +53,7 @@ impl Default for NetworkSettings {
pool: PoolSetup::default(), pool: PoolSetup::default(),
dandelion: DandelionSetup::default(), dandelion: DandelionSetup::default(),
modal_ids: vec![ modal_ids: vec![
RESET_SETTINGS_MODAL RESET_SETTINGS_CONFIRMATION_MODAL
] ]
} }
} }
@ -69,15 +69,15 @@ impl ModalContainer for NetworkSettings {
modal: &Modal, modal: &Modal,
_: &dyn PlatformCallbacks) { _: &dyn PlatformCallbacks) {
match modal.id { match modal.id {
RESET_SETTINGS_MODAL => reset_settings_confirmation_modal(ui, modal), RESET_SETTINGS_CONFIRMATION_MODAL => reset_settings_confirmation_modal(ui, modal),
_ => {} _ => {}
} }
} }
} }
impl NetworkTab for NetworkSettings { impl NodeTab for NetworkSettings {
fn get_type(&self) -> NetworkTabType { fn get_type(&self) -> NodeTabType {
NetworkTabType::Settings NodeTabType::Settings
} }
fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
@ -85,7 +85,7 @@ impl NetworkTab for NetworkSettings {
self.current_modal_ui(ui, cb); self.current_modal_ui(ui, cb);
ScrollArea::vertical() ScrollArea::vertical()
.id_source("network_settings") .id_salt("node_settings_scroll")
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden) .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.auto_shrink([false; 2]) .auto_shrink([false; 2])
.show(ui, |ui| { .show(ui, |ui| {
@ -210,9 +210,9 @@ fn reset_settings_ui(ui: &mut egui::Ui) {
t!("network_settings.reset_settings")); t!("network_settings.reset_settings"));
View::action_button(ui, button_text, || { View::action_button(ui, button_text, || {
// Show modal to confirm settings reset. // Show modal to confirm settings reset.
Modal::new(RESET_SETTINGS_MODAL) Modal::new(RESET_SETTINGS_CONFIRMATION_MODAL)
.position(ModalPosition::Center) .position(ModalPosition::Center)
.title(t!("modal.confirmation")) .title(t!("confirmation"))
.show(); .show();
}); });

View file

@ -141,7 +141,7 @@ impl DandelionSetup {
ui.add_space(6.0); ui.add_space(6.0);
let epoch = NodeConfig::get_dandelion_epoch(); let epoch = NodeConfig::get_dandelion_epoch();
View::button(ui, format!("{} {}", WATCH, epoch.clone()), Colors::button(), || { View::button(ui, format!("{} {}", WATCH, &epoch), Colors::white_or_black(false), || {
// Setup values for modal. // Setup values for modal.
self.epoch_edit = epoch; self.epoch_edit = epoch;
// Show epoch setup modal. // Show epoch setup modal.
@ -218,8 +218,7 @@ impl DandelionSetup {
ui.add_space(6.0); ui.add_space(6.0);
let embargo = NodeConfig::get_dandelion_embargo(); let embargo = NodeConfig::get_dandelion_embargo();
View::button(ui, format!("{} {}", TIMER, embargo.clone()), Colors::button(), || { View::button(ui, format!("{} {}", TIMER, &embargo), Colors::white_or_black(false), || {
// Setup values for modal.
self.embargo_edit = embargo; self.embargo_edit = embargo;
// Show embargo setup modal. // Show embargo setup modal.
Modal::new(EMBARGO_MODAL) Modal::new(EMBARGO_MODAL)
@ -294,10 +293,10 @@ impl DandelionSetup {
); );
ui.add_space(6.0); ui.add_space(6.0);
let agg = NodeConfig::get_dandelion_aggregation(); let ag = NodeConfig::get_dandelion_aggregation();
View::button(ui, format!("{} {}", CLOCK_COUNTDOWN, agg.clone()), Colors::button(), || { View::button(ui, format!("{} {}", CLOCK_COUNTDOWN, &ag), Colors::white_or_black(false), || {
// Setup values for modal. // Setup values for modal.
self.aggregation_edit = agg; self.aggregation_edit = ag;
// Show aggregation setup modal. // Show aggregation setup modal.
Modal::new(AGGREGATION_MODAL) Modal::new(AGGREGATION_MODAL)
.position(ModalPosition::CenterTop) .position(ModalPosition::CenterTop)
@ -372,7 +371,7 @@ impl DandelionSetup {
ui.add_space(6.0); ui.add_space(6.0);
let stem_prob = NodeConfig::get_stem_probability(); let stem_prob = NodeConfig::get_stem_probability();
View::button(ui, format!("{}%", stem_prob.clone()), Colors::button(), || { View::button(ui, format!("{}%", &stem_prob), Colors::white_or_black(false), || {
// Setup values for modal. // Setup values for modal.
self.stem_prob_edit = stem_prob; self.stem_prob_edit = stem_prob;
// Show stem probability setup modal. // Show stem probability setup modal.

View file

@ -255,7 +255,7 @@ impl NodeSetup {
ui.add_space(6.0); ui.add_space(6.0);
let (_, port) = NodeConfig::get_api_ip_port(); let (_, port) = NodeConfig::get_api_ip_port();
View::button(ui, format!("{} {}", PLUG, port.clone()), Colors::button(), || { View::button(ui, format!("{} {}", PLUG, &port), Colors::white_or_black(false), || {
// Setup values for modal. // Setup values for modal.
self.api_port_edit = port; self.api_port_edit = port;
self.api_port_available_edit = self.is_api_port_available; self.api_port_available_edit = self.is_api_port_available;
@ -283,7 +283,9 @@ impl NodeSetup {
fn api_port_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) { fn api_port_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
ui.add_space(6.0); ui.add_space(6.0);
ui.vertical_centered(|ui| { ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("network_settings.api_port")).size(17.0).color(Colors::gray())); ui.label(RichText::new(t!("network_settings.api_port"))
.size(17.0)
.color(Colors::gray()));
ui.add_space(6.0); ui.add_space(6.0);
// Draw API port text edit. // Draw API port text edit.
@ -356,8 +358,8 @@ impl NodeSetup {
ui.add_space(6.0); ui.add_space(6.0);
let secret_value = match modal_id { let secret_value = match modal_id {
API_SECRET_MODAL => NodeConfig::get_api_secret(), API_SECRET_MODAL => NodeConfig::get_api_secret(false),
_ => NodeConfig::get_foreign_api_secret() _ => NodeConfig::get_api_secret(true)
}; };
let secret_text = if secret_value.is_some() { let secret_text = if secret_value.is_some() {
@ -366,7 +368,7 @@ impl NodeSetup {
format!("{} {}", SHIELD_SLASH, t!("network_settings.disabled")) format!("{} {}", SHIELD_SLASH, t!("network_settings.disabled"))
}; };
View::button(ui, secret_text, Colors::button(), || { View::button(ui, secret_text, Colors::white_or_black(false), || {
// Setup values for modal. // Setup values for modal.
self.secret_edit = secret_value.unwrap_or("".to_string()); self.secret_edit = secret_value.unwrap_or("".to_string());
// Show secret edit modal. // Show secret edit modal.
@ -449,7 +451,9 @@ impl NodeSetup {
ui.add_space(6.0); ui.add_space(6.0);
let ftl = NodeConfig::get_ftl(); let ftl = NodeConfig::get_ftl();
View::button(ui, format!("{} {}", CLOCK_CLOCKWISE, ftl.clone()), Colors::button(), || { View::button(ui,
format!("{} {}", CLOCK_CLOCKWISE, &ftl),
Colors::white_or_black(false), || {
// Setup values for modal. // Setup values for modal.
self.ftl_edit = ftl; self.ftl_edit = ftl;
// Show ftl value setup modal. // Show ftl value setup modal.

View file

@ -91,7 +91,7 @@ impl Default for P2PSetup {
fn default() -> Self { fn default() -> Self {
let port = NodeConfig::get_p2p_port(); let port = NodeConfig::get_p2p_port();
let is_port_available = NodeConfig::is_p2p_port_available(&port); let is_port_available = NodeConfig::is_p2p_port_available(&port);
let default_main_seeds = grin_servers::MAINNET_DNS_SEEDS let default_main_seeds = Node::MAINNET_DNS_SEEDS
.iter() .iter()
.map(|s| s.to_string()) .map(|s| s.to_string())
.collect(); .collect();
@ -246,7 +246,9 @@ impl P2PSetup {
ui.add_space(6.0); ui.add_space(6.0);
let port = NodeConfig::get_p2p_port(); let port = NodeConfig::get_p2p_port();
View::button(ui, format!("{} {}", PLUG, port.clone()), Colors::button(), || { View::button(ui,
format!("{} {}", PLUG, &port),
Colors::white_or_black(false), || {
// Setup values for modal. // Setup values for modal.
self.port_edit = port; self.port_edit = port;
self.port_available_edit = self.is_port_available; self.port_available_edit = self.is_port_available;
@ -306,11 +308,9 @@ impl P2PSetup {
// Save port at config if it's available. // Save port at config if it's available.
if available { if available {
NodeConfig::save_p2p_port(self.port_edit.parse::<u16>().unwrap()); NodeConfig::save_p2p_port(self.port_edit.parse::<u16>().unwrap());
if Node::is_running() { if Node::is_running() {
Node::restart(); Node::restart();
} }
self.is_port_available = true; self.is_port_available = true;
cb.hide_keyboard(); cb.hide_keyboard();
modal.close(); modal.close();
@ -370,8 +370,8 @@ impl P2PSetup {
ui.label(RichText::new(desc) ui.label(RichText::new(desc)
.size(16.0) .size(16.0)
.color(Colors::inactive_text())); .color(Colors::inactive_text()));
ui.add_space(12.0);
} }
ui.add_space(12.0);
let add_text = if peer_type == &PeerType::CustomSeed { let add_text = if peer_type == &PeerType::CustomSeed {
format!("{} {}", PLUS_CIRCLE, t!("network_settings.add_seed")) format!("{} {}", PLUS_CIRCLE, t!("network_settings.add_seed"))
@ -379,7 +379,7 @@ impl P2PSetup {
format!("{} {}", PLUS_CIRCLE, t!("network_settings.add_peer")) format!("{} {}", PLUS_CIRCLE, t!("network_settings.add_peer"))
}; };
View::button(ui, add_text, Colors::button(), || { View::button(ui, add_text, Colors::white_or_black(false), || {
// Setup values for modal. // Setup values for modal.
self.is_correct_address_edit = true; self.is_correct_address_edit = true;
self.peer_edit = "".to_string(); self.peer_edit = "".to_string();
@ -506,7 +506,9 @@ impl P2PSetup {
ui.add_space(6.0); ui.add_space(6.0);
let ban_window = NodeConfig::get_p2p_ban_window(); let ban_window = NodeConfig::get_p2p_ban_window();
View::button(ui, format!("{} {}", PROHIBIT_INSET, ban_window.clone()), Colors::button(), || { View::button(ui,
format!("{} {}", PROHIBIT_INSET, &ban_window),
Colors::white_or_black(false), || {
// Setup values for modal. // Setup values for modal.
self.ban_window_edit = ban_window; self.ban_window_edit = ban_window;
// Show ban window period setup modal. // Show ban window period setup modal.
@ -588,8 +590,9 @@ impl P2PSetup {
ui.add_space(6.0); ui.add_space(6.0);
let max_inbound = NodeConfig::get_max_inbound_peers(); let max_inbound = NodeConfig::get_max_inbound_peers();
let button_text = format!("{} {}", ARROW_FAT_LINES_DOWN, max_inbound.clone()); View::button(ui,
View::button(ui, button_text, Colors::button(), || { format!("{} {}", ARROW_FAT_LINES_DOWN, &max_inbound),
Colors::white_or_black(false), || {
// Setup values for modal. // Setup values for modal.
self.max_inbound_count = max_inbound; self.max_inbound_count = max_inbound;
// Show maximum number of inbound peers setup modal. // Show maximum number of inbound peers setup modal.
@ -664,10 +667,10 @@ impl P2PSetup {
.color(Colors::gray()) .color(Colors::gray())
); );
ui.add_space(6.0); ui.add_space(6.0);
let max_outbound = NodeConfig::get_max_outbound_peers(); let max_outbound = NodeConfig::get_max_outbound_peers();
let button_text = format!("{} {}", ARROW_FAT_LINES_UP, max_outbound.clone()); View::button(ui,
View::button(ui, button_text, Colors::button(), || { format!("{} {}", ARROW_FAT_LINES_UP, &max_outbound),
Colors::white_or_black(false), || {
// Setup values for modal. // Setup values for modal.
self.max_outbound_count = max_outbound; self.max_outbound_count = max_outbound;
// Show maximum number of outbound peers setup modal. // Show maximum number of outbound peers setup modal.
@ -738,9 +741,10 @@ impl P2PSetup {
/// Draw content to reset peers data. /// Draw content to reset peers data.
fn reset_peers_ui(&mut self, ui: &mut egui::Ui) { fn reset_peers_ui(&mut self, ui: &mut egui::Ui) {
ui.add_space(4.0); ui.add_space(4.0);
View::colored_text_button(ui,
let button_text = format!("{} {}", TRASH, t!("network_settings.reset_peers")); format!("{} {}", TRASH, t!("network_settings.reset_peers")),
View::colored_text_button(ui, button_text, Colors::red(), Colors::button(), || { Colors::red(),
Colors::white_or_black(false), || {
Node::reset_peers(false); Node::reset_peers(false);
self.peers_reset = true; self.peers_reset = true;
}); });

View file

@ -145,7 +145,7 @@ impl PoolSetup {
ui.add_space(6.0); ui.add_space(6.0);
let fee = NodeConfig::get_base_fee(); let fee = NodeConfig::get_base_fee();
View::button(ui, format!("{} {}", HAND_COINS, fee.clone()), Colors::button(), || { View::button(ui, format!("{} {}", HAND_COINS, &fee), Colors::white_or_black(false), || {
// Setup values for modal. // Setup values for modal.
self.fee_base_edit = fee; self.fee_base_edit = fee;
// Show fee setup modal. // Show fee setup modal.
@ -195,7 +195,6 @@ impl PoolSetup {
modal.close(); modal.close();
} }
}; };
ui.columns(2, |columns| { ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| { columns[0].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || { View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
@ -220,9 +219,10 @@ impl PoolSetup {
.color(Colors::gray()) .color(Colors::gray())
); );
ui.add_space(6.0); ui.add_space(6.0);
let period = NodeConfig::get_reorg_cache_period(); let period = NodeConfig::get_reorg_cache_period();
View::button(ui, format!("{} {}", CLOCK_COUNTDOWN, period.clone()), Colors::button(), || { View::button(ui,
format!("{} {}", CLOCK_COUNTDOWN, &period),
Colors::white_or_black(false), || {
// Setup values for modal. // Setup values for modal.
self.reorg_period_edit = period; self.reorg_period_edit = period;
// Show reorg period setup modal. // Show reorg period setup modal.
@ -299,7 +299,7 @@ impl PoolSetup {
ui.add_space(6.0); ui.add_space(6.0);
let size = NodeConfig::get_max_pool_size(); let size = NodeConfig::get_max_pool_size();
View::button(ui, format!("{} {}", CIRCLES_THREE, size.clone()), Colors::button(), || { View::button(ui, format!("{} {}", CIRCLES_THREE, size), Colors::white_or_black(false), || {
// Setup values for modal. // Setup values for modal.
self.pool_size_edit = size; self.pool_size_edit = size;
// Show pool size setup modal. // Show pool size setup modal.
@ -376,7 +376,9 @@ impl PoolSetup {
ui.add_space(6.0); ui.add_space(6.0);
let size = NodeConfig::get_max_stempool_size(); let size = NodeConfig::get_max_stempool_size();
View::button(ui, format!("{} {}", BEZIER_CURVE, size.clone()), Colors::button(), || { View::button(ui,
format!("{} {}", BEZIER_CURVE, &size),
Colors::white_or_black(false), || {
// Setup values for modal. // Setup values for modal.
self.stempool_size_edit = size; self.stempool_size_edit = size;
// Show stempool size setup modal. // Show stempool size setup modal.
@ -453,7 +455,9 @@ impl PoolSetup {
ui.add_space(6.0); ui.add_space(6.0);
let weight = NodeConfig::get_mineable_max_weight(); let weight = NodeConfig::get_mineable_max_weight();
View::button(ui, format!("{} {}", BOUNDING_BOX, weight.clone()), Colors::button(), || { View::button(ui,
format!("{} {}", BOUNDING_BOX, &weight),
Colors::white_or_black(false), || {
// Setup values for modal. // Setup values for modal.
self.max_weight_edit = weight; self.max_weight_edit = weight;
// Show total tx weight setup modal. // Show total tx weight setup modal.

View file

@ -44,7 +44,7 @@ pub struct StratumSetup {
is_port_available: bool, is_port_available: bool,
/// Wallet name to receive rewards. /// Wallet name to receive rewards.
wallet_name: Option<String>, pub wallet_name: Option<String>,
/// Attempt time value in seconds to mine on a particular header. /// Attempt time value in seconds to mine on a particular header.
attempt_time_edit: String, attempt_time_edit: String,
@ -83,7 +83,7 @@ impl Default for StratumSetup {
Self { Self {
wallets: WalletList::default(), wallets: WalletList::default(),
wallets_modal: WalletsModal::new(wallet_id), wallets_modal: WalletsModal::new(wallet_id, None, false),
available_ips: NodeConfig::get_ip_addrs(), available_ips: NodeConfig::get_ip_addrs(),
stratum_port_edit: port, stratum_port_edit: port,
stratum_port_available_edit: is_port_available, stratum_port_available_edit: is_port_available,
@ -111,10 +111,13 @@ impl ModalContainer for StratumSetup {
modal: &Modal, modal: &Modal,
cb: &dyn PlatformCallbacks) { cb: &dyn PlatformCallbacks) {
match modal.id { match modal.id {
WALLET_SELECTION_MODAL => self.wallets_modal.ui(ui, modal, &self.wallets, |id| { WALLET_SELECTION_MODAL => {
self.wallets_modal.ui(ui, modal, &mut self.wallets, cb, |wallet, _| {
let id = wallet.get_config().id;
NodeConfig::save_stratum_wallet_id(id); NodeConfig::save_stratum_wallet_id(id);
self.wallet_name = WalletConfig::name_by_id(id); self.wallet_name = WalletConfig::name_by_id(id);
}), })
},
STRATUM_PORT_MODAL => self.port_modal(ui, modal, cb), STRATUM_PORT_MODAL => self.port_modal(ui, modal, cb),
ATTEMPT_TIME_MODAL => self.attempt_modal(ui, modal, cb), ATTEMPT_TIME_MODAL => self.attempt_modal(ui, modal, cb),
MIN_SHARE_DIFF_MODAL => self.min_diff_modal(ui, modal, cb), MIN_SHARE_DIFF_MODAL => self.min_diff_modal(ui, modal, cb),
@ -186,7 +189,9 @@ impl StratumSetup {
ui.add_space(8.0); ui.add_space(8.0);
// Show button to select wallet. // Show button to select wallet.
View::button(ui, t!("network_settings.choose_wallet"), Colors::button(), || { View::button(ui,
t!("network_settings.choose_wallet"),
Colors::white_or_black(false), || {
self.show_wallets_modal(); self.show_wallets_modal();
}); });
ui.add_space(12.0); ui.add_space(12.0);
@ -240,7 +245,7 @@ impl StratumSetup {
/// Show wallet selection [`Modal`]. /// Show wallet selection [`Modal`].
fn show_wallets_modal(&mut self) { fn show_wallets_modal(&mut self) {
self.wallets_modal = WalletsModal::new(NodeConfig::get_stratum_wallet_id()); self.wallets_modal = WalletsModal::new(NodeConfig::get_stratum_wallet_id(), None, false);
// Show modal. // Show modal.
Modal::new(WALLET_SELECTION_MODAL) Modal::new(WALLET_SELECTION_MODAL)
.position(ModalPosition::Center) .position(ModalPosition::Center)
@ -257,7 +262,7 @@ impl StratumSetup {
ui.add_space(6.0); ui.add_space(6.0);
let (_, port) = NodeConfig::get_stratum_address(); let (_, port) = NodeConfig::get_stratum_address();
View::button(ui, format!("{} {}", PLUG, port.clone()), Colors::button(), || { View::button(ui, format!("{} {}", PLUG, &port), Colors::white_or_black(false), || {
// Setup values for modal. // Setup values for modal.
self.stratum_port_edit = port; self.stratum_port_edit = port;
self.stratum_port_available_edit = self.is_port_available; self.stratum_port_available_edit = self.is_port_available;
@ -356,7 +361,7 @@ impl StratumSetup {
ui.add_space(6.0); ui.add_space(6.0);
let time = NodeConfig::get_stratum_attempt_time(); let time = NodeConfig::get_stratum_attempt_time();
View::button(ui, format!("{} {}", TIMER, time.clone()), Colors::button(), || { View::button(ui, format!("{} {}", TIMER, &time), Colors::white_or_black(false), || {
// Setup values for modal. // Setup values for modal.
self.attempt_time_edit = time; self.attempt_time_edit = time;
@ -439,7 +444,7 @@ impl StratumSetup {
ui.add_space(6.0); ui.add_space(6.0);
let diff = NodeConfig::get_stratum_min_share_diff(); let diff = NodeConfig::get_stratum_min_share_diff();
View::button(ui, format!("{} {}", BARBELL, diff.clone()), Colors::button(), || { View::button(ui, format!("{} {}", BARBELL, &diff), Colors::white_or_black(false), || {
// Setup values for modal. // Setup values for modal.
self.min_share_diff_edit = diff; self.min_share_diff_edit = diff;

View file

@ -14,28 +14,28 @@
use crate::gui::platform::PlatformCallbacks; use crate::gui::platform::PlatformCallbacks;
/// Network tab content interface. /// Integrated node tab content interface.
pub trait NetworkTab { pub trait NodeTab {
fn get_type(&self) -> NetworkTabType; fn get_type(&self) -> NodeTabType;
fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks); fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks);
} }
/// Type of [`NetworkTab`] content. /// Type of [`NodeTab`] content.
#[derive(PartialEq)] #[derive(PartialEq)]
pub enum NetworkTabType { pub enum NodeTabType {
Node, Info,
Metrics, Metrics,
Mining, Mining,
Settings Settings
} }
impl NetworkTabType { impl NodeTabType {
pub fn title(&self) -> String { pub fn title(&self) -> String {
match *self { match *self {
NetworkTabType::Node => { t!("network.node") } NodeTabType::Info => { t!("network.node") }
NetworkTabType::Metrics => { t!("network.metrics") } NodeTabType::Metrics => { t!("network.metrics") }
NetworkTabType::Mining => { t!("network.mining") } NodeTabType::Mining => { t!("network.mining") }
NetworkTabType::Settings => { t!("network.settings") } NodeTabType::Settings => { t!("network.settings") }
} }
} }
} }

View file

@ -13,7 +13,7 @@
// limitations under the License. // limitations under the License.
use egui::scroll_area::ScrollAreaOutput; use egui::scroll_area::ScrollAreaOutput;
use egui::{Sense, Align2, Area, Color32, Id, Rect, Response, Widget, Vec2}; use egui::{Sense, Align2, Area, Color32, Id, Rect, Response, Widget, Vec2, UiBuilder};
use egui::epaint::{emath::lerp, vec2, Pos2, Shape, Stroke}; use egui::epaint::{emath::lerp, vec2, Pos2, Shape, Stroke};
/// A spinner widget used to indicate loading. /// A spinner widget used to indicate loading.
@ -195,7 +195,9 @@ impl PullToRefresh {
ui: &mut egui::Ui, ui: &mut egui::Ui,
content: impl FnOnce(&mut egui::Ui) -> T, content: impl FnOnce(&mut egui::Ui) -> T,
) -> PullToRefreshResponse<T> { ) -> PullToRefreshResponse<T> {
let mut child = ui.child_ui(ui.available_rect_before_wrap(), *ui.layout(), None); let mut child = ui.new_child(UiBuilder::new()
.max_rect(ui.available_rect_before_wrap())
.layout(*ui.layout()));
let output = content(&mut child); let output = content(&mut child);

View file

@ -16,7 +16,7 @@ use std::mem::size_of;
use std::sync::Arc; use std::sync::Arc;
use parking_lot::RwLock; use parking_lot::RwLock;
use std::thread; use std::thread;
use egui::{SizeHint, TextureHandle}; use egui::{SizeHint, TextureHandle, UiBuilder};
use egui::epaint::RectShape; use egui::epaint::RectShape;
use image::{ExtendedColorType, ImageEncoder}; use image::{ExtendedColorType, ImageEncoder};
use image::codecs::png::{CompressionType, FilterType, PngEncoder}; use image::codecs::png::{CompressionType, FilterType, PngEncoder};
@ -30,8 +30,8 @@ use crate::gui::views::View;
/// QR code image from text. /// QR code image from text.
pub struct QrCodeContent { pub struct QrCodeContent {
/// Text to create QR code. /// QR code text.
pub(crate) text: String, text: String,
/// Flag to draw animated QR with Uniform Resources /// Flag to draw animated QR with Uniform Resources
/// https://github.com/BlockchainCommons/Research/blob/master/papers/bcr-2020-005-ur.md /// https://github.com/BlockchainCommons/Research/blob/master/papers/bcr-2020-005-ur.md
@ -62,18 +62,18 @@ impl QrCodeContent {
} }
/// Draw QR code. /// Draw QR code.
pub fn ui(&mut self, ui: &mut egui::Ui, text: String, cb: &dyn PlatformCallbacks) { pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
if self.animated { if self.animated {
// Show animated QR code. // Show animated QR code.
self.animated_ui(ui, text, cb); self.animated_ui(ui, cb);
} else { } else {
// Show static QR code. // Show static QR code.
self.static_ui(ui, text, cb); self.static_ui(ui, cb);
} }
} }
/// Draw animated QR code content. /// Draw animated QR code content.
fn animated_ui(&mut self, ui: &mut egui::Ui, text: String, cb: &dyn PlatformCallbacks) { fn animated_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
if !self.has_image() { if !self.has_image() {
let space = (ui.available_width() - View::BIG_SPINNER_SIZE) / 2.0; let space = (ui.available_width() - View::BIG_SPINNER_SIZE) / 2.0;
ui.vertical_centered(|ui| { ui.vertical_centered(|ui| {
@ -84,7 +84,7 @@ impl QrCodeContent {
// Create multiple vector images from text if not creating. // Create multiple vector images from text if not creating.
if !self.loading() { if !self.loading() {
self.create_svg_list(text); self.create_svg_list();
} }
} else { } else {
let svg_list = { let svg_list = {
@ -111,7 +111,7 @@ impl QrCodeContent {
// Show QR code text. // Show QR code text.
ui.add_space(6.0); ui.add_space(6.0);
View::ellipsize_text(ui, text.clone(), 16.0, Colors::inactive_text()); View::ellipsize_text(ui, self.text.clone(), 16.0, Colors::inactive_text());
ui.add_space(6.0); ui.add_space(6.0);
ui.vertical_centered(|ui| { ui.vertical_centered(|ui| {
@ -131,7 +131,7 @@ impl QrCodeContent {
w_state.exporting = true; w_state.exporting = true;
} }
// Create GIF to export. // Create GIF to export.
self.create_qr_gif(text, DEFAULT_QR_SIZE as usize); self.create_qr_gif();
}); });
} else { } else {
ui.vertical_centered(|ui| { ui.vertical_centered(|ui| {
@ -171,7 +171,7 @@ impl QrCodeContent {
} }
/// Draw static QR code content. /// Draw static QR code content.
fn static_ui(&mut self, ui: &mut egui::Ui, text: String, cb: &dyn PlatformCallbacks) { fn static_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
if !self.has_image() { if !self.has_image() {
let space = (ui.available_width() - View::BIG_SPINNER_SIZE) / 2.0; let space = (ui.available_width() - View::BIG_SPINNER_SIZE) / 2.0;
ui.vertical_centered(|ui| { ui.vertical_centered(|ui| {
@ -182,7 +182,7 @@ impl QrCodeContent {
// Create vector image from text if not creating. // Create vector image from text if not creating.
if !self.loading() { if !self.loading() {
self.create_svg(text); self.create_svg();
} }
} else { } else {
// Create image from SVG data. // Create image from SVG data.
@ -194,7 +194,7 @@ impl QrCodeContent {
// Show QR code text. // Show QR code text.
ui.add_space(6.0); ui.add_space(6.0);
View::ellipsize_text(ui, text.clone(), 16.0, Colors::inactive_text()); View::ellipsize_text(ui, self.text.clone(), 16.0, Colors::inactive_text());
ui.add_space(6.0); ui.add_space(6.0);
// Show button to share QR. // Show button to share QR.
@ -204,7 +204,8 @@ impl QrCodeContent {
share_text, share_text,
Colors::blue(), Colors::blue(),
Colors::white_or_black(false), || { Colors::white_or_black(false), || {
if let Ok(qr) = QrCode::encode_text(text.as_str(), qrcodegen::QrCodeEcc::Low) { let text = self.text.as_str();
if let Ok(qr) = QrCode::encode_text(text, qrcodegen::QrCodeEcc::Low) {
if let Some(data) = Self::qr_to_image_data(qr, DEFAULT_QR_SIZE as usize) { if let Some(data) = Self::qr_to_image_data(qr, DEFAULT_QR_SIZE as usize) {
let mut png = vec![]; let mut png = vec![];
let png_enc = PngEncoder::new_with_quality(&mut png, let png_enc = PngEncoder::new_with_quality(&mut png,
@ -234,26 +235,23 @@ impl QrCodeContent {
rect.max -= egui::emath::vec2(10.0, 0.0); rect.max -= egui::emath::vec2(10.0, 0.0);
// Create background shape. // Create background shape.
let mut bg_shape = RectShape { let mut bg_shape = RectShape::new(
rect, rect,
rounding: egui::Rounding::default(), egui::Rounding::default(),
fill: egui::Color32::WHITE, egui::Color32::WHITE,
stroke: egui::Stroke::NONE, egui::Stroke::NONE
blur_width: 0.0, );
fill_texture_id: Default::default(),
uv: egui::Rect::ZERO
};
let bg_idx = ui.painter().add(bg_shape); let bg_idx = ui.painter().add(bg_shape);
// Draw QR code image content. // Draw QR code image content.
let mut content_rect = ui.allocate_ui_at_rect(rect, |ui| { let mut content_rect = ui.allocate_new_ui(UiBuilder::new().max_rect(rect), |ui| {
ui.add_space(10.0); ui.add_space(10.0);
let size = SizeHint::Size(ui.available_width() as u32, ui.available_width() as u32); let size = SizeHint::Size(ui.available_width() as u32, ui.available_width() as u32);
self.texture_handle = Some(View::svg_image(ui, "qr_code", svg.as_slice(), Some(size))); self.texture_handle = Some(View::svg_image(ui, "qr_code", svg.as_slice(), Some(size)));
ui.add_space(10.0); ui.add_space(10.0);
}).response.rect; }).response.rect;
// Setup background shape to be painted behind content. // Setup background size.
content_rect.min -= egui::emath::vec2(10.0, 0.0); content_rect.min -= egui::emath::vec2(10.0, 0.0);
content_rect.max += egui::emath::vec2(10.0, 0.0); content_rect.max += egui::emath::vec2(10.0, 0.0);
bg_shape.rect = content_rect; bg_shape.rect = content_rect;
@ -267,8 +265,9 @@ impl QrCodeContent {
} }
/// Create multiple vector QR code images at separate thread. /// Create multiple vector QR code images at separate thread.
fn create_svg_list(&self, text: String) { fn create_svg_list(&self) {
let qr_state = self.qr_image_state.clone(); let qr_state = self.qr_image_state.clone();
let text = self.text.clone();
thread::spawn(move || { thread::spawn(move || {
let mut encoder = ur::Encoder::bytes(text.as_bytes(), 100).unwrap(); let mut encoder = ur::Encoder::bytes(text.as_bytes(), 100).unwrap();
let mut data = Vec::with_capacity(encoder.fragment_count()); let mut data = Vec::with_capacity(encoder.fragment_count());
@ -294,8 +293,9 @@ impl QrCodeContent {
} }
/// Create vector QR code image at separate thread. /// Create vector QR code image at separate thread.
fn create_svg(&self, text: String) { fn create_svg(&self) {
let qr_state = self.qr_image_state.clone(); let qr_state = self.qr_image_state.clone();
let text = self.text.clone();
thread::spawn(move || { thread::spawn(move || {
if let Ok(qr) = QrCode::encode_text(text.as_str(), qrcodegen::QrCodeEcc::Low) { if let Ok(qr) = QrCode::encode_text(text.as_str(), qrcodegen::QrCodeEcc::Low) {
let svg = Self::qr_to_svg(qr, 0); let svg = Self::qr_to_svg(qr, 0);
@ -332,13 +332,14 @@ impl QrCodeContent {
} }
/// Create GIF image at separate thread. /// Create GIF image at separate thread.
fn create_qr_gif(&self, text: String, size: usize) { fn create_qr_gif(&self) {
{ {
let mut w_state = self.qr_image_state.write(); let mut w_state = self.qr_image_state.write();
w_state.gif_creating = true; w_state.gif_creating = true;
} }
let qr_state = self.qr_image_state.clone(); let qr_state = self.qr_image_state.clone();
let text = self.text.clone();
thread::spawn(move || { thread::spawn(move || {
// Setup GIF image encoder. // Setup GIF image encoder.
let mut gif = vec![]; let mut gif = vec![];
@ -354,7 +355,7 @@ impl QrCodeContent {
) { ) {
// Create an image from QR data. // Create an image from QR data.
let image = qr.render() let image = qr.render()
.max_dimensions(size as u32, size as u32) .max_dimensions(DEFAULT_QR_SIZE, DEFAULT_QR_SIZE)
.dark_color(image::Rgb([0, 0, 0])) .dark_color(image::Rgb([0, 0, 0]))
.light_color(image::Rgb([255, 255, 255])) .light_color(image::Rgb([255, 255, 255]))
.build(); .build();
@ -428,10 +429,4 @@ impl QrCodeContent {
} }
Some(img_raw) Some(img_raw)
} }
/// Reset QR code image content state to default.
pub fn clear_state(&mut self) {
let mut w_create = self.qr_image_state.write();
*w_create = QrImageState::default();
}
} }

130
src/gui/views/scan.rs Normal file
View file

@ -0,0 +1,130 @@
// Copyright 2024 The Grim Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use egui::scroll_area::ScrollBarVisibility;
use egui::{Id, ScrollArea};
use crate::gui::Colors;
use crate::gui::icons::COPY;
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{CameraContent, Modal, View};
use crate::gui::views::types::QrScanResult;
/// QR code scan [`Modal`] content.
pub struct CameraScanModal {
/// Camera content for QR scan [`Modal`].
camera_content: Option<CameraContent>,
/// QR code scan result
qr_scan_result: Option<QrScanResult>,
}
impl Default for CameraScanModal {
fn default() -> Self {
Self {
camera_content: Some(CameraContent::default()),
qr_scan_result: None,
}
}
}
impl CameraScanModal {
/// Draw [`Modal`] content.
pub fn ui(&mut self,
ui: &mut egui::Ui,
modal: &Modal,
cb: &dyn PlatformCallbacks,
mut on_result: impl FnMut(&QrScanResult)) {
// Show scan result if exists or show camera content while scanning.
if let Some(result) = &self.qr_scan_result {
let mut result_text = result.text();
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(3.0);
ScrollArea::vertical()
.id_salt(Id::from("qr_scan_result_input"))
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.max_height(128.0)
.auto_shrink([false; 2])
.show(ui, |ui| {
ui.add_space(7.0);
egui::TextEdit::multiline(&mut result_text)
.font(egui::TextStyle::Small)
.desired_rows(5)
.interactive(false)
.desired_width(f32::INFINITY)
.show(ui);
ui.add_space(6.0);
});
ui.add_space(2.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(10.0);
// Show copy button.
ui.vertical_centered(|ui| {
let copy_text = format!("{} {}", COPY, t!("copy"));
View::button(ui, copy_text, Colors::white_or_black(false), || {
cb.copy_string_to_buffer(result_text.to_string());
self.qr_scan_result = None;
modal.close();
});
});
ui.add_space(10.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(6.0);
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(ui, t!("close"), Colors::white_or_black(false), || {
self.qr_scan_result = None;
self.camera_content = None;
modal.close();
});
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("repeat"), Colors::white_or_black(false), || {
Modal::set_title(t!("scan_qr"));
self.qr_scan_result = None;
self.camera_content = Some(CameraContent::default());
cb.start_camera();
});
});
});
} else if let Some(camera_content) = self.camera_content.as_mut() {
if let Some(result) = camera_content.qr_scan_result() {
cb.stop_camera();
self.camera_content = None;
on_result(&result);
// Set result and rename modal title.
self.qr_scan_result = Some(result);
Modal::set_title(t!("scan_result"));
} else {
// Draw camera content.
ui.add_space(6.0);
self.camera_content.as_mut().unwrap().ui(ui, cb);
ui.add_space(12.0);
ui.vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
cb.stop_camera();
self.camera_content = None;
modal.close();
});
});
}
}
ui.add_space(6.0);
}
}

View file

@ -12,11 +12,11 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
use egui::{Margin, Id, Layout, Align}; use egui::{Margin, Id, Layout, Align, UiBuilder};
use crate::gui::Colors; use crate::gui::Colors;
use crate::gui::views::{Content, View}; use crate::gui::views::{Content, View};
use crate::gui::views::types::{TitleContentType, TitleType}; use crate::gui::views::types::{LinePosition, TitleContentType, TitleType};
/// Title panel with left/right action buttons and text in the middle. /// Title panel with left/right action buttons and text in the middle.
pub struct TitlePanel { pub struct TitlePanel {
@ -25,8 +25,8 @@ pub struct TitlePanel {
} }
impl TitlePanel { impl TitlePanel {
/// Default [`TitlePanel`] content height. /// Content height.
pub const DEFAULT_HEIGHT: f32 = 54.0; pub const HEIGHT: f32 = 54.0;
/// Create new title panel with provided identifier. /// Create new title panel with provided identifier.
pub fn new(id: Id) -> Self { pub fn new(id: Id) -> Self {
@ -43,7 +43,7 @@ impl TitlePanel {
// Draw title panel. // Draw title panel.
egui::TopBottomPanel::top(self.id) egui::TopBottomPanel::top(self.id)
.resizable(false) .resizable(false)
.exact_height(Self::DEFAULT_HEIGHT + View::get_top_inset()) .exact_height(Self::HEIGHT + View::get_top_inset())
.frame(egui::Frame { .frame(egui::Frame {
inner_margin: Margin { inner_margin: Margin {
left: View::far_left_inset_margin(ui), left: View::far_left_inset_margin(ui),
@ -51,7 +51,6 @@ impl TitlePanel {
top: View::get_top_inset(), top: View::get_top_inset(),
bottom: 0.0, bottom: 0.0,
}, },
fill: Colors::yellow(),
..Default::default() ..Default::default()
}) })
.show_inside(ui, |ui| { .show_inside(ui, |ui| {
@ -68,40 +67,51 @@ impl TitlePanel {
match title { match title {
TitleType::Single(content) => { TitleType::Single(content) => {
let content_rect = { let content_rect = {
let mut r = rect; let mut r = rect.clone();
r.min.x += Self::DEFAULT_HEIGHT; r.min.x += Self::HEIGHT;
r.max.x -= Self::DEFAULT_HEIGHT; r.max.x -= Self::HEIGHT;
r r
}; };
ui.allocate_ui_at_rect(content_rect, |ui| { ui.allocate_new_ui(UiBuilder::new().max_rect(content_rect), |ui| {
Self::title_text_content(ui, content); Self::title_text_content(ui, content);
}); });
} }
TitleType::Dual(first, second) => { TitleType::Dual(first, second) => {
let first_rect = { let first_rect = {
let mut r = rect; let mut r = rect.clone();
r.max.x = r.min.x + Content::SIDE_PANEL_WIDTH - Self::DEFAULT_HEIGHT; r.max.x = r.min.x + Content::SIDE_PANEL_WIDTH - Self::HEIGHT;
r.min.x += Self::DEFAULT_HEIGHT; r.min.x += Self::HEIGHT;
r r
}; };
// Draw first title content. // Draw first title content.
ui.allocate_ui_at_rect(first_rect, |ui| { ui.allocate_new_ui(UiBuilder::new().max_rect(first_rect), |ui| {
Self::title_text_content(ui, first); Self::title_text_content(ui, first);
}); });
let second_rect = { let second_rect = {
let mut r = rect; let mut r = rect.clone();
r.min.x = first_rect.max.x + 2.0 * Self::DEFAULT_HEIGHT; r.min.x = first_rect.max.x + 2.0 * Self::HEIGHT;
r.max.x -= Self::DEFAULT_HEIGHT; r.max.x -= Self::HEIGHT;
r r
}; };
// Draw second title content. // Draw second title content.
ui.allocate_ui_at_rect(second_rect, |ui| { ui.allocate_new_ui(UiBuilder::new().max_rect(second_rect), |ui| {
Self::title_text_content(ui, second); Self::title_text_content(ui, second);
}); });
} }
} }
}); });
// Draw content divider line.
let r = {
let mut r = rect.clone();
r.min.x -= View::far_left_inset_margin(ui);
r.max.x += View::far_right_inset_margin(ui);
r
};
if Content::is_dual_panel_mode(ui.ctx()) {
View::line(ui, LinePosition::BOTTOM, &r, Colors::stroke());
}
}); });
} }
@ -115,11 +125,11 @@ impl TitlePanel {
} else { } else {
0.0 0.0
}); });
View::ellipsize_text(ui, text, 19.0, Colors::title(true)); View::ellipsize_text(ui, text.to_uppercase(), 19.0, Colors::title(true));
} }
TitleContentType::WithSubTitle(text, subtitle, animate) => { TitleContentType::WithSubTitle(text, subtitle, animate) => {
ui.add_space(4.0); ui.add_space(4.0);
View::ellipsize_text(ui, text, 18.0, Colors::title(true)); View::ellipsize_text(ui, text.to_uppercase(), 18.0, Colors::title(true));
ui.add_space(-2.0); ui.add_space(-2.0);
View::animate_text(ui, subtitle, 15.0, Colors::text(true), animate) View::animate_text(ui, subtitle, 15.0, Colors::text(true), animate)
} }

View file

@ -32,6 +32,11 @@ pub enum TitleContentType {
WithSubTitle(String, String, bool) WithSubTitle(String, String, bool)
} }
/// Stroke position against content.
pub enum LinePosition {
TOP, LEFT, RIGHT, BOTTOM
}
/// Position of [`Modal`] on the screen. /// Position of [`Modal`] on the screen.
#[derive(Clone)] #[derive(Clone)]
pub enum ModalPosition { pub enum ModalPosition {

View file

@ -17,8 +17,8 @@ use std::sync::Arc;
use parking_lot::RwLock; use parking_lot::RwLock;
use lazy_static::lazy_static; use lazy_static::lazy_static;
use egui::{Align, Button, CursorIcon, Layout, lerp, PointerState, Rect, Response, Rgba, RichText, Sense, SizeHint, Spinner, TextBuffer, TextStyle, TextureHandle, TextureOptions, Widget}; use egui::{Align, Button, CursorIcon, Layout, lerp, PointerState, Rect, Response, Rgba, RichText, Sense, SizeHint, Spinner, TextBuffer, TextStyle, TextureHandle, TextureOptions, Widget, UiBuilder};
use egui::epaint::{Color32, FontId, RectShape, Rounding, Stroke}; use egui::epaint::{Color32, FontId, PathShape, PathStroke, RectShape, Rounding, Stroke};
use egui::epaint::text::TextWrapping; use egui::epaint::text::TextWrapping;
use egui::load::SizedTexture; use egui::load::SizedTexture;
use egui::os::OperatingSystem; use egui::os::OperatingSystem;
@ -30,7 +30,7 @@ use crate::AppConfig;
use crate::gui::Colors; use crate::gui::Colors;
use crate::gui::icons::{CHECK_SQUARE, CLIPBOARD_TEXT, COPY, EYE, EYE_SLASH, SCAN, SQUARE}; use crate::gui::icons::{CHECK_SQUARE, CLIPBOARD_TEXT, COPY, EYE, EYE_SLASH, SCAN, SQUARE};
use crate::gui::platform::PlatformCallbacks; use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::types::TextEditOptions; use crate::gui::views::types::{LinePosition, TextEditOptions};
pub struct View; pub struct View;
@ -78,14 +78,16 @@ impl View {
rect.set_width(width); rect.set_width(width);
// Draw content. // Draw content.
ui.vertical_centered(|ui| {
ui.allocate_ui(rect.size(), |ui| { ui.allocate_ui(rect.size(), |ui| {
(add_content)(ui); (add_content)(ui);
}); });
});
} }
/// Get width and height of app window. /// Get width and height of app window.
pub fn window_size(ui: &egui::Ui) -> (f32, f32) { pub fn window_size(ctx: &egui::Context) -> (f32, f32) {
let rect = ui.ctx().screen_rect(); let rect = ctx.screen_rect();
(rect.width(), rect.height()) (rect.width(), rect.height())
} }
@ -108,7 +110,7 @@ impl View {
/// Calculate margin for far left view based on display insets (cutouts). /// Calculate margin for far left view based on display insets (cutouts).
pub fn far_right_inset_margin(ui: &mut egui::Ui) -> f32 { pub fn far_right_inset_margin(ui: &mut egui::Ui) -> f32 {
let container_width = ui.available_rect_before_wrap().max.x as i32; let container_width = ui.available_rect_before_wrap().max.x as i32;
let window_size = Self::window_size(ui); let window_size = Self::window_size(ui.ctx());
let display_width = window_size.0 as i32; let display_width = window_size.0 as i32;
// Means end of the screen. // Means end of the screen.
if container_width == display_width { if container_width == display_width {
@ -217,7 +219,10 @@ impl View {
pub const TAB_ITEMS_PADDING: f32 = 5.0; pub const TAB_ITEMS_PADDING: f32 = 5.0;
/// Tab button with white background fill color, contains only icon. /// Tab button with white background fill color, contains only icon.
pub fn tab_button(ui: &mut egui::Ui, icon: &str, active: bool, action: impl FnOnce()) { pub fn tab_button(ui: &mut egui::Ui,
icon: &str,
active: bool,
action: impl FnOnce(&mut egui::Ui)) {
ui.scope(|ui| { ui.scope(|ui| {
let text_color = match active { let text_color = match active {
true => Colors::title(false), true => Colors::title(false),
@ -232,7 +237,7 @@ impl View {
ui.style_mut().visuals.widgets.active.expansion = 0.0; ui.style_mut().visuals.widgets.active.expansion = 0.0;
// Setup fill colors. // Setup fill colors.
ui.visuals_mut().widgets.inactive.weak_bg_fill = Colors::white_or_black(false); ui.visuals_mut().widgets.inactive.weak_bg_fill = Colors::white_or_black(false);
ui.visuals_mut().widgets.hovered.weak_bg_fill = Colors::button(); ui.visuals_mut().widgets.hovered.weak_bg_fill = Colors::fill_lite();
ui.visuals_mut().widgets.active.weak_bg_fill = Colors::fill(); ui.visuals_mut().widgets.active.weak_bg_fill = Colors::fill();
// Setup stroke colors. // Setup stroke colors.
ui.visuals_mut().widgets.inactive.bg_stroke = Self::default_stroke(); ui.visuals_mut().widgets.inactive.bg_stroke = Self::default_stroke();
@ -245,7 +250,7 @@ impl View {
let br = button.ui(ui).on_hover_cursor(CursorIcon::PointingHand); let br = button.ui(ui).on_hover_cursor(CursorIcon::PointingHand);
br.surrender_focus(); br.surrender_focus();
if Self::touched(ui, br) { if Self::touched(ui, br) {
(action)(); (action)(ui);
} }
}); });
} }
@ -280,6 +285,18 @@ impl View {
} }
} }
/// Draw [`Button`] with specified background fill color and text color.
pub fn colored_text_button_ui(ui: &mut egui::Ui,
text: String,
text_color: Color32,
fill: Color32,
action: impl FnOnce(&mut egui::Ui)) {
let br = Self::button_resp(ui, text, text_color, fill);
if Self::touched(ui, br) {
(action)(ui);
}
}
/// Draw gold action [`Button`]. /// Draw gold action [`Button`].
pub fn action_button(ui: &mut egui::Ui, pub fn action_button(ui: &mut egui::Ui,
text: String, action: impl FnOnce()) { text: String, action: impl FnOnce()) {
@ -310,7 +327,7 @@ impl View {
action: impl FnOnce()) { action: impl FnOnce()) {
// Setup button size. // Setup button size.
let mut rect = ui.available_rect_before_wrap(); let mut rect = ui.available_rect_before_wrap();
rect.set_width(32.0); rect.set_width(42.0);
let button_size = rect.size(); let button_size = rect.size();
ui.scope(|ui| { ui.scope(|ui| {
@ -321,12 +338,12 @@ impl View {
ui.style_mut().visuals.widgets.active.expansion = 0.0; ui.style_mut().visuals.widgets.active.expansion = 0.0;
// Setup fill colors. // Setup fill colors.
ui.visuals_mut().widgets.inactive.weak_bg_fill = Colors::white_or_black(false); ui.visuals_mut().widgets.inactive.weak_bg_fill = Colors::white_or_black(false);
ui.visuals_mut().widgets.hovered.weak_bg_fill = Colors::button(); ui.visuals_mut().widgets.hovered.weak_bg_fill = Colors::fill_lite();
ui.visuals_mut().widgets.active.weak_bg_fill = Colors::fill(); ui.visuals_mut().widgets.active.weak_bg_fill = Colors::fill();
// Setup stroke colors. // Disable strokes.
ui.visuals_mut().widgets.inactive.bg_stroke = Self::default_stroke(); ui.visuals_mut().widgets.inactive.bg_stroke = Stroke::NONE;
ui.visuals_mut().widgets.hovered.bg_stroke = Self::hover_stroke(); ui.visuals_mut().widgets.hovered.bg_stroke = Stroke::NONE;
ui.visuals_mut().widgets.active.bg_stroke = Self::item_stroke(); ui.visuals_mut().widgets.active.bg_stroke = Stroke::NONE;
// Setup button text color. // Setup button text color.
let text_color = if let Some(c) = color { c } else { Colors::item_button() }; let text_color = if let Some(c) = color { c } else { Colors::item_button() };
@ -338,9 +355,18 @@ impl View {
.ui(ui) .ui(ui)
.on_hover_cursor(CursorIcon::PointingHand); .on_hover_cursor(CursorIcon::PointingHand);
br.surrender_focus(); br.surrender_focus();
if Self::touched(ui, br) { if Self::touched(ui, br.clone()) {
(action)(); (action)();
} }
// Draw stroke.
let r = {
let mut r = ui.available_rect_before_wrap();
r.min = br.rect.min;
r.min.x += 0.5;
r
};
Self::line(ui, LinePosition::LEFT, &r, Colors::item_stroke());
}); });
} }
@ -514,28 +540,20 @@ impl View {
/// where is r = (top_left, top_right, bottom_left, bottom_right). /// where is r = (top_left, top_right, bottom_left, bottom_right).
/// | VALUE | /// | VALUE |
/// | label | /// | label |
pub fn rounded_box(ui: &mut egui::Ui, value: String, label: String, r: [bool; 4]) { pub fn label_box(ui: &mut egui::Ui, text: String, label: String, r: [bool; 4]) {
let rect = ui.available_rect_before_wrap(); let rect = ui.available_rect_before_wrap();
// Create background shape. // Create background shape.
let mut bg_shape = RectShape { let mut bg_shape = RectShape::new(rect, Rounding {
rect,
rounding: Rounding {
nw: if r[0] { 8.0 } else { 0.0 }, nw: if r[0] { 8.0 } else { 0.0 },
ne: if r[1] { 8.0 } else { 0.0 }, ne: if r[1] { 8.0 } else { 0.0 },
sw: if r[2] { 8.0 } else { 0.0 }, sw: if r[2] { 8.0 } else { 0.0 },
se: if r[3] { 8.0 } else { 0.0 }, se: if r[3] { 8.0 } else { 0.0 },
}, }, Colors::fill_lite(), Self::item_stroke());
fill: Colors::TRANSPARENT,
stroke: Self::item_stroke(),
blur_width: 0.0,
fill_texture_id: Default::default(),
uv: Rect::ZERO
};
let bg_idx = ui.painter().add(bg_shape); let bg_idx = ui.painter().add(bg_shape);
// Draw box content. // Draw box content.
let content_resp = ui.allocate_ui_at_rect(rect, |ui| { let content_resp = ui.allocate_new_ui(UiBuilder::new().max_rect(rect), |ui| {
ui.vertical_centered_justified(|ui| { ui.vertical_centered_justified(|ui| {
ui.add_space(4.0); ui.add_space(4.0);
ui.scope(|ui| { ui.scope(|ui| {
@ -543,7 +561,7 @@ impl View {
ui.style_mut().spacing.item_spacing.y = -3.0; ui.style_mut().spacing.item_spacing.y = -3.0;
// Draw box value. // Draw box value.
let mut job = LayoutJob::single_section(value, TextFormat { let mut job = LayoutJob::single_section(text, TextFormat {
font_id: FontId::proportional(17.0), font_id: FontId::proportional(17.0),
color: Colors::white_or_black(true), color: Colors::white_or_black(true),
..Default::default() ..Default::default()
@ -563,7 +581,7 @@ impl View {
}); });
}).response; }).response;
// Setup background shape to be painted behind box content. // Setup background shape size.
bg_shape.rect = content_resp.rect; bg_shape.rect = content_resp.rect;
ui.painter().set(bg_idx, bg_shape); ui.painter().set(bg_idx, bg_shape);
} }
@ -575,7 +593,7 @@ impl View {
let side_margin = 28.0; let side_margin = 28.0;
rect.min += egui::emath::vec2(side_margin, ui.available_height() / 2.0 - height / 2.0); rect.min += egui::emath::vec2(side_margin, ui.available_height() / 2.0 - height / 2.0);
rect.max -= egui::emath::vec2(side_margin, 0.0); rect.max -= egui::emath::vec2(side_margin, 0.0);
ui.allocate_ui_at_rect(rect, |ui| { ui.allocate_new_ui(UiBuilder::new().max_rect(rect), |ui| {
(content)(ui); (content)(ui);
}); });
}); });
@ -638,6 +656,47 @@ impl View {
Stroke { width: 1.0, color }); Stroke { width: 1.0, color });
} }
/// Draw line for panel content.
pub fn line(ui: &mut egui::Ui, pos: LinePosition, rect: &Rect, color: Color32) {
let points = match pos {
LinePosition::RIGHT => {
vec![{
let mut r = rect.clone();
r.min.x = r.max.x;
r.min
}, rect.max]
}
LinePosition::BOTTOM => {
vec![{
let mut r = rect.clone();
r.min.y = r.max.y;
r.min
}, rect.max]
}
LinePosition::LEFT => {
vec![rect.min, {
let mut r = rect.clone();
r.max.x = r.min.x;
r.max
}]
}
LinePosition::TOP => {
vec![rect.min, {
let mut r = rect.clone();
r.max.y = r.min.y;
r.max
}]
}
};
let stroke = PathShape {
points,
closed: false,
fill: Default::default(),
stroke: PathStroke::new(1.0, color),
};
ui.painter().add(stroke);
}
/// Draw SVG image from provided data with optional provided size. /// Draw SVG image from provided data with optional provided size.
pub fn svg_image(ui: &mut egui::Ui, pub fn svg_image(ui: &mut egui::Ui,
name: &str, name: &str,
@ -683,6 +742,19 @@ impl View {
); );
} }
/// Draw semi-transparent cover at specified area.
pub fn content_cover_ui(ui: &mut egui::Ui,
rect: Rect,
id: impl std::hash::Hash,
mut on_click: impl FnMut()) {
let resp = ui.interact(rect, egui::Id::new(id), Sense::click_and_drag());
if resp.clicked() || resp.dragged() {
on_click();
}
let shape = RectShape::filled(resp.rect, Rounding::ZERO, Colors::semi_transparent());
ui.painter().add(shape);
}
/// Get top display inset (cutout) size. /// Get top display inset (cutout) size.
pub fn get_top_inset() -> f32 { pub fn get_top_inset() -> f32 {
TOP_DISPLAY_INSET.load(Ordering::Relaxed) as f32 TOP_DISPLAY_INSET.load(Ordering::Relaxed) as f32

View file

@ -18,33 +18,36 @@ use egui::scroll_area::ScrollBarVisibility;
use crate::AppConfig; use crate::AppConfig;
use crate::gui::Colors; use crate::gui::Colors;
use crate::gui::icons::{ARROW_LEFT, CARET_RIGHT, COMPUTER_TOWER, FOLDER_LOCK, FOLDER_OPEN, GEAR, GLOBE, GLOBE_SIMPLE, LOCK_KEY, PLUS, SIDEBAR_SIMPLE, SPINNER, SUITCASE, WARNING_CIRCLE}; use crate::gui::icons::{ARROW_LEFT, CARET_RIGHT, COMPUTER_TOWER, FOLDER_OPEN, FOLDER_PLUS, GEAR, GLOBE, GLOBE_SIMPLE, LOCK_KEY, PLUS, SIDEBAR_SIMPLE, SUITCASE};
use crate::gui::platform::PlatformCallbacks; use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Modal, Content, TitlePanel, View}; use crate::gui::views::{Modal, Content, TitlePanel, View};
use crate::gui::views::types::{ModalContainer, ModalPosition, TextEditOptions, TitleContentType, TitleType}; use crate::gui::views::types::{ModalContainer, ModalPosition, LinePosition, TitleContentType, TitleType};
use crate::gui::views::wallets::creation::WalletCreation; use crate::gui::views::wallets::creation::WalletCreation;
use crate::gui::views::wallets::modals::WalletConnectionModal; use crate::gui::views::wallets::modals::{AddWalletModal, OpenWalletModal, WalletConnectionModal, WalletsModal};
use crate::gui::views::wallets::types::WalletTabType; use crate::gui::views::wallets::types::WalletTabType;
use crate::gui::views::wallets::wallet::types::wallet_status_text;
use crate::gui::views::wallets::WalletContent; use crate::gui::views::wallets::WalletContent;
use crate::wallet::{Wallet, WalletList}; use crate::wallet::{ExternalConnection, Wallet, WalletList};
use crate::wallet::types::ConnectionMethod;
/// Wallets content. /// Wallets content.
pub struct WalletsContent { pub struct WalletsContent {
/// List of wallets. /// List of wallets.
wallets: WalletList, wallets: WalletList,
/// Password to open wallet for [`Modal`]. /// Initial wallet creation [`Modal`] content.
pass_edit: String, add_wallet_modal_content: Option<AddWalletModal>,
/// Flag to check if wrong password was entered at [`Modal`]. /// Wallet opening [`Modal`] content.
wrong_pass: bool, open_wallet_content: Option<OpenWalletModal>,
/// Wallet connection selection content. /// Wallet connection selection content.
conn_modal_content: Option<WalletConnectionModal>, conn_selection_content: Option<WalletConnectionModal>,
/// Wallet selection [`Modal`] content.
wallet_selection_content: Option<WalletsModal>,
/// Selected [`Wallet`] content. /// Selected [`Wallet`] content.
wallet_content: WalletContent, wallet_content: Option<WalletContent>,
/// Wallet creation content. /// Wallet creation content.
creation_content: WalletCreation, creation_content: Option<WalletCreation>,
/// Flag to show [`Wallet`] list at dual panel mode. /// Flag to show [`Wallet`] list at dual panel mode.
show_wallets_at_dual_panel: bool, show_wallets_at_dual_panel: bool,
@ -53,26 +56,28 @@ pub struct WalletsContent {
modal_ids: Vec<&'static str> modal_ids: Vec<&'static str>
} }
/// Identifier for connection selection [`Modal`]. const ADD_WALLET_MODAL: &'static str = "wallets_add_modal";
const CONNECTION_SELECTION_MODAL: &'static str = "wallets_connection_selection_modal"; const OPEN_WALLET_MODAL: &'static str = "wallets_open_wallet";
/// Identifier for wallet opening [`Modal`]. const SELECT_CONNECTION_MODAL: &'static str = "wallets_select_conn_modal";
const OPEN_WALLET_MODAL: &'static str = "open_wallet_modal"; const SELECT_WALLET_MODAL: &'static str = "wallets_select_modal";
impl Default for WalletsContent { impl Default for WalletsContent {
fn default() -> Self { fn default() -> Self {
Self { Self {
wallets: WalletList::default(), wallets: WalletList::default(),
pass_edit: "".to_string(), wallet_selection_content: None,
wrong_pass: false, open_wallet_content: None,
conn_modal_content: None, conn_selection_content: None,
wallet_content: WalletContent::default(), wallet_content: None,
creation_content: WalletCreation::default(), creation_content: None,
show_wallets_at_dual_panel: AppConfig::show_wallets_at_dual_panel(), show_wallets_at_dual_panel: AppConfig::show_wallets_at_dual_panel(),
modal_ids: vec![ modal_ids: vec![
ADD_WALLET_MODAL,
OPEN_WALLET_MODAL, OPEN_WALLET_MODAL,
WalletCreation::NAME_PASS_MODAL, SELECT_CONNECTION_MODAL,
CONNECTION_SELECTION_MODAL, SELECT_WALLET_MODAL,
] ],
add_wallet_modal_content: None,
} }
} }
} }
@ -87,208 +92,339 @@ impl ModalContainer for WalletsContent {
modal: &Modal, modal: &Modal,
cb: &dyn PlatformCallbacks) { cb: &dyn PlatformCallbacks) {
match modal.id { match modal.id {
OPEN_WALLET_MODAL => self.open_wallet_modal_ui(ui, modal, cb), ADD_WALLET_MODAL => {
WalletCreation::NAME_PASS_MODAL => { if let Some(content) = self.add_wallet_modal_content.as_mut() {
self.creation_content.name_pass_modal_ui(ui, modal, cb) content.ui(ui, modal, cb, |name, pass| {
}, self.creation_content = Some(
CONNECTION_SELECTION_MODAL => { WalletCreation::new(name.clone(), pass.clone())
if let Some(content) = self.conn_modal_content.as_mut() { );
content.ui(ui, modal, cb, |id| { });
let list = self.wallets.list();
for w in list {
if self.wallets.selected_id == Some(w.get_config().id) {
w.update_ext_conn_id(id);
} }
if self.creation_content.is_some() {
self.add_wallet_modal_content = None;
}
},
OPEN_WALLET_MODAL => {
let mut open = false;
if let Some(open_content) = self.open_wallet_content.as_mut() {
open_content.ui(ui, modal, cb, |wallet, data| {
self.wallet_content = Some(WalletContent::new(wallet, data));
open = true;
});
}
if open {
self.open_wallet_content = None;
}
},
SELECT_CONNECTION_MODAL => {
if let Some(content) = self.conn_selection_content.as_mut() {
content.ui(ui, modal, cb, |conn| {
if let Some(wallet_content) = &self.wallet_content {
wallet_content.wallet.update_connection(&conn);
} }
}); });
} }
} }
SELECT_WALLET_MODAL => {
let mut select = false;
if let Some(content) = self.wallet_selection_content.as_mut() {
content.ui(ui, modal, &mut self.wallets, cb, |wallet, data| {
self.wallet_content = Some(WalletContent::new(wallet, data));
select = true;
});
}
if select {
self.wallet_selection_content = None;
}
}
_ => {} _ => {}
} }
} }
} }
impl WalletsContent { impl WalletsContent {
/// Draw wallets content.
pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { 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); self.current_modal_ui(ui, cb);
// Setup wallet content flags. let creating_wallet = self.creating_wallet();
let empty_list = self.wallets.is_current_list_empty(); let showing_wallet = self.showing_wallet() && !creating_wallet;
let create_wallet = self.creation_content.can_go_back(); let dual_panel = Self::is_dual_panel_mode(ui);
let show_wallet = self.wallets.is_selected_open();
// Setup panels parameters.
let dual_panel = is_dual_panel_mode(ui);
let wallet_panel_width = self.wallet_panel_width(ui, empty_list, dual_panel, show_wallet);
let content_width = ui.available_width(); let content_width = ui.available_width();
let list_hidden = creating_wallet || self.wallets.list().is_empty()
let root_dual_panel = Content::is_dual_panel_mode(ui); || (showing_wallet && self.wallet_content.as_ref().unwrap().qr_scan_content.is_some())
|| (dual_panel && showing_wallet && !self.show_wallets_at_dual_panel)
// Flag to check if wallet list is hidden on the screen. || (!dual_panel && showing_wallet);
let list_hidden = content_width == 0.0 || empty_list || create_wallet
|| (dual_panel && show_wallet && !self.show_wallets_at_dual_panel)
|| (!dual_panel && show_wallet) ||
(!root_dual_panel && Content::is_network_panel_open());
// Show title panel. // Show title panel.
self.title_ui(ui, dual_panel, create_wallet, show_wallet); self.title_ui(ui, dual_panel, showing_wallet, cb);
// Show wallet panel content. if showing_wallet {
let wallet_panel_opened = self.wallet_panel_opened();
egui::SidePanel::right("wallet_panel") egui::SidePanel::right("wallet_panel")
.resizable(false) .resizable(false)
.exact_width(wallet_panel_width) .exact_width(if list_hidden {
content_width
} else {
content_width - Content::SIDE_PANEL_WIDTH
})
.frame(egui::Frame { .frame(egui::Frame {
fill: if empty_list && !create_wallet
|| (dual_panel && show_wallet && !self.show_wallets_at_dual_panel) {
Colors::fill_deep()
} else {
if create_wallet {
Colors::white_or_black(false)
} else {
Colors::button()
}
},
..Default::default() ..Default::default()
}) })
.show_animated_inside(ui, wallet_panel_opened, |ui| { .show_inside(ui, |ui| {
if create_wallet || !show_wallet { // Show opened wallet content.
// Show wallet creation content. if let Some(content) = self.wallet_content.as_mut() {
self.creation_content.ui(ui, cb, |wallet| { content.ui(ui, cb);
// Add created wallet to list.
self.wallets.add(wallet);
// Reset wallet content.
self.wallet_content = WalletContent::default();
});
} else {
let selected_id = self.wallets.selected_id.clone();
let list = self.wallets.mut_list();
for wallet in list {
// Show content for selected wallet.
if selected_id == Some(wallet.get_config().id) {
// Setup wallet content width.
let mut rect = ui.available_rect_before_wrap();
let mut width = ui.available_width();
if dual_panel && self.show_wallets_at_dual_panel {
width = content_width - Content::SIDE_PANEL_WIDTH;
}
rect.set_width(width);
// Show wallet content.
ui.allocate_ui_at_rect(rect, |ui| {
self.wallet_content.ui(ui, wallet, cb);
});
break;
}
}
} }
}); });
}
// Show wallets bottom panel. if !list_hidden {
let show_bottom_panel = !list_hidden || dual_panel;
if show_bottom_panel {
egui::TopBottomPanel::bottom("wallets_bottom_panel") egui::TopBottomPanel::bottom("wallets_bottom_panel")
.frame(egui::Frame { .frame(egui::Frame {
fill: Colors::fill(),
inner_margin: Margin { inner_margin: Margin {
left: View::get_left_inset() + View::TAB_ITEMS_PADDING, left: View::far_left_inset_margin(ui) + View::TAB_ITEMS_PADDING,
right: View::far_right_inset_margin(ui) + View::TAB_ITEMS_PADDING, right: View::far_right_inset_margin(ui) + View::TAB_ITEMS_PADDING,
top: View::TAB_ITEMS_PADDING, top: View::TAB_ITEMS_PADDING,
bottom: View::get_bottom_inset() + View::TAB_ITEMS_PADDING, bottom: View::get_bottom_inset() + View::TAB_ITEMS_PADDING,
}, },
fill: Colors::fill(),
..Default::default() ..Default::default()
}) })
.resizable(false)
.show_inside(ui, |ui| { .show_inside(ui, |ui| {
let rect = ui.available_rect_before_wrap();
// Setup spacing between tabs. // Setup spacing between tabs.
ui.style_mut().spacing.item_spacing = egui::vec2(View::TAB_ITEMS_PADDING, 0.0); ui.style_mut().spacing.item_spacing = egui::vec2(View::TAB_ITEMS_PADDING, 0.0);
// Setup vertical padding inside buttons. // Setup vertical padding inside buttons.
ui.style_mut().spacing.button_padding = egui::vec2(10.0, 4.0); ui.style_mut().spacing.button_padding = egui::vec2(10.0, 4.0);
ui.vertical_centered(|ui| { ui.vertical_centered(|ui| {
let pressed = Modal::opened() == Some(WalletCreation::NAME_PASS_MODAL); let pressed = Modal::opened() == Some(ADD_WALLET_MODAL);
View::tab_button(ui, PLUS, pressed, || { View::tab_button(ui, PLUS, pressed, |_| {
self.creation_content.show_name_pass_modal(cb); self.show_add_wallet_modal(cb);
}); });
}); });
});
}
// Show wallet list. // Draw content divider line.
egui::CentralPanel::default() let r = {
.frame(if list_hidden { let mut r = rect.clone();
egui::Frame::default() r.min.y -= View::TAB_ITEMS_PADDING;
r.min.x -= View::TAB_ITEMS_PADDING;
r.max.x += View::TAB_ITEMS_PADDING;
r
};
View::line(ui, LinePosition::TOP, &r, Colors::stroke());
});
egui::SidePanel::left("wallet_list_panel")
.exact_width(if dual_panel && showing_wallet {
Content::SIDE_PANEL_WIDTH
} else { } else {
egui::Frame { content_width
stroke: View::item_stroke(), })
fill: Colors::fill_deep(), .resizable(false)
.frame(egui::Frame {
inner_margin: Margin { inner_margin: Margin {
left: View::far_left_inset_margin(ui) + 4.0, left: View::far_left_inset_margin(ui) + 4.0,
right: View::far_right_inset_margin(ui) + 4.0, right: View::far_right_inset_margin(ui) + 4.0,
top: 3.0, top: 3.0,
bottom: 4.0, bottom: 4.0,
}, },
fill: Colors::fill_deep(),
..Default::default() ..Default::default()
}
}) })
.show_inside(ui, |ui| { .show_inside(ui, |ui| {
if !list_hidden && !dual_panel { if !dual_panel && !showing_wallet {
ui.ctx().request_repaint_after(Duration::from_millis(1000)); ui.ctx().request_repaint_after(Duration::from_millis(1000));
} }
// Show wallet list.
self.wallet_list_ui(ui, cb); self.wallet_list_ui(ui, cb);
}); });
} }
/// Check if wallet panel is showing. egui::CentralPanel::default()
pub fn wallet_panel_opened(&self) -> bool { .frame(egui::Frame {
let empty_list = self.wallets.is_current_list_empty(); fill: if creating_wallet {
empty_list || self.creating_wallet() || self.showing_wallet() Colors::TRANSPARENT
} else {
Colors::fill_deep()
},
..Default::default()
})
.show_inside(ui, |ui| {
if self.creation_content.is_some() {
let creation = self.creation_content.as_mut().unwrap();
let pass = creation.pass.clone();
let mut created = false;
// Show wallet creation content.
creation.ui(ui, cb, |wallet| {
self.wallets.add(wallet.clone());
if let Ok(_) = wallet.open(pass.clone()) {
self.wallet_content = Some(WalletContent::new(wallet, None));
}
created = true;
});
if created {
self.creation_content = None;
}
} else if self.wallets.list().is_empty() {
View::center_content(ui, 350.0 + View::get_bottom_inset(), |ui| {
View::app_logo_name_version(ui);
ui.add_space(4.0);
let text = t!("wallets.create_desc");
ui.label(RichText::new(text)
.size(16.0)
.color(Colors::gray())
);
ui.add_space(8.0);
// Show wallet creation button.
let add_text = format!("{} {}", FOLDER_PLUS, t!("wallets.add"));
View::button(ui, add_text, Colors::white_or_black(false), || {
self.show_add_wallet_modal(cb);
});
});
} else {
return;
}
});
} }
/// Check if opened wallet is showing. /// Check if opened wallet is showing.
pub fn showing_wallet(&self) -> bool { pub fn showing_wallet(&self) -> bool {
self.wallets.is_selected_open() if let Some(wallet_content) = &self.wallet_content {
let w = &wallet_content.wallet;
return w.is_open() && !w.is_deleted() &&
w.get_config().chain_type == AppConfig::chain_type();
}
false
} }
/// Check if wallet is creating. /// Check if wallet is creating.
pub fn creating_wallet(&self) -> bool { pub fn creating_wallet(&self) -> bool {
self.creation_content.can_go_back() self.creation_content.is_some()
}
/// Handle data from deeplink or opened file.
pub fn on_data(&mut self, ui: &mut egui::Ui, data: Option<String>, cb: &dyn PlatformCallbacks) {
let wallets_size = self.wallets.list().len();
if wallets_size == 0 {
return;
}
// Close network panel on single panel mode.
if !Content::is_dual_panel_mode(ui.ctx()) && Content::is_network_panel_open() {
Content::toggle_network_panel();
}
// Pass data to single wallet or show wallets selection.
if wallets_size == 1 {
let w = self.wallets.list()[0].clone();
if w.is_open() {
self.wallet_content = Some(WalletContent::new(w, data));
} else {
self.show_opening_modal(w, data, cb);
}
} else {
self.show_wallet_selection_modal(data);
}
}
/// Show initial wallet creation [`Modal`].
pub fn show_add_wallet_modal(&mut self, cb: &dyn PlatformCallbacks) {
self.add_wallet_modal_content = Some(AddWalletModal::default());
Modal::new(ADD_WALLET_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("wallets.add"))
.show();
cb.show_keyboard();
}
/// Show wallet selection with provided optional data.
fn show_wallet_selection_modal(&mut self, data: Option<String>) {
self.wallet_selection_content = Some(WalletsModal::new(None, data, true));
Modal::new(SELECT_WALLET_MODAL)
.position(ModalPosition::Center)
.title(t!("network_settings.choose_wallet"))
.show();
}
/// Handle Back key event returning `false` when event was handled.
pub fn on_back(&mut self, cb: &dyn PlatformCallbacks) -> bool {
if self.creation_content.is_some() {
// Close wallet creation.
let creation = self.creation_content.as_mut().unwrap();
if creation.on_back() {
self.creation_content = None;
}
return false;
} else {
if self.showing_wallet() {
let content = self.wallet_content.as_mut().unwrap();
// Close opened QR code scanner.
if content.qr_scan_content.is_some() {
cb.stop_camera();
content.qr_scan_content = None;
return false;
}
// Close opened wallet.
self.wallet_content = None;
return false;
}
}
true
} }
/// Draw [`TitlePanel`] content. /// Draw [`TitlePanel`] content.
fn title_ui(&mut self, fn title_ui(&mut self,
ui: &mut egui::Ui, ui: &mut egui::Ui,
dual_panel: bool, dual_panel: bool,
create_wallet: bool, show_wallet: bool,
show_wallet: bool) { cb: &dyn PlatformCallbacks) {
let show_list = self.show_wallets_at_dual_panel; let show_list = self.show_wallets_at_dual_panel;
let creating_wallet = self.creating_wallet();
let qr_scan = {
let mut scan = false;
if show_wallet {
scan = self.wallet_content.as_mut().unwrap().qr_scan_content.is_some();
}
scan
};
// Setup title. // Setup title.
let title_content = if self.wallets.is_selected_open() && (!dual_panel let title_content = if show_wallet && (!dual_panel
|| (dual_panel && !show_list)) && !create_wallet { || (dual_panel && !show_list)) && !creating_wallet {
let title_text = self.wallet_content.current_tab.get_type().name().to_uppercase(); let wallet_content = self.wallet_content.as_ref().unwrap();
if self.wallet_content.current_tab.get_type() == WalletTabType::Settings { let wallet_tab_type = wallet_content.current_tab.get_type();
let title_text = if qr_scan {
t!("scan_qr")
} else {
wallet_tab_type.name()
};
if wallet_tab_type == WalletTabType::Settings {
TitleType::Single(TitleContentType::Title(title_text)) TitleType::Single(TitleContentType::Title(title_text))
} else { } else {
let subtitle_text = self.wallets.selected_name(); let subtitle_text = wallet_content.wallet.get_config().name;
TitleType::Single(TitleContentType::WithSubTitle(title_text, subtitle_text, false)) TitleType::Single(TitleContentType::WithSubTitle(title_text, subtitle_text, false))
} }
} else { } else {
let title_text = if create_wallet { let title_text = if qr_scan {
t!("scan_qr")
} else if creating_wallet {
t!("wallets.add") t!("wallets.add")
} else { } else {
t!("wallets.title") t!("wallets.title")
}.to_uppercase();
let dual_title = !create_wallet && show_wallet && dual_panel;
if dual_title {
let wallet_tab_type = self.wallet_content.current_tab.get_type();
let wallet_tab_name = wallet_tab_type.name().to_uppercase();
let title_content = if wallet_tab_type == WalletTabType::Settings {
TitleContentType::Title(wallet_tab_name)
} else {
let subtitle_text = self.wallets.selected_name();
TitleContentType::WithSubTitle(wallet_tab_name, subtitle_text, false)
}; };
TitleType::Dual(TitleContentType::Title(title_text), title_content) let dual_title = !qr_scan && !creating_wallet && show_wallet && dual_panel;
if dual_title {
let wallet_content = self.wallet_content.as_ref().unwrap();
let wallet_tab_type = wallet_content.current_tab.get_type();
let wallet_title_text = wallet_tab_type.name();
let wallet_title_content = if wallet_tab_type == WalletTabType::Settings {
TitleContentType::Title(wallet_title_text)
} else {
let subtitle_text = wallet_content.wallet.get_config().name;
TitleContentType::WithSubTitle(wallet_title_text, subtitle_text, false)
};
TitleType::Dual(TitleContentType::Title(title_text), wallet_title_content)
} else { } else {
TitleType::Single(TitleContentType::Title(title_text)) TitleType::Single(TitleContentType::Title(title_text))
} }
@ -298,13 +434,37 @@ impl WalletsContent {
TitlePanel::new(Id::new("wallets_title_panel")).ui(title_content, |ui| { TitlePanel::new(Id::new("wallets_title_panel")).ui(title_content, |ui| {
if show_wallet && !dual_panel { if show_wallet && !dual_panel {
View::title_button_big(ui, ARROW_LEFT, |_| { View::title_button_big(ui, ARROW_LEFT, |_| {
self.wallets.select(None); let wallet_qr_scan = self.wallet_content
.as_ref()
.unwrap()
.qr_scan_content
.is_some();
if wallet_qr_scan {
cb.stop_camera();
self.wallet_content.as_mut().unwrap().qr_scan_content = None;
return;
}
self.wallet_content = None;
}); });
} else if create_wallet { } else if self.creation_content.is_some() {
let mut close = false;
if let Some(creation) = self.creation_content.as_mut() {
View::title_button_big(ui, ARROW_LEFT, |_| { View::title_button_big(ui, ARROW_LEFT, |_| {
self.creation_content.back(); if creation.on_back() {
close = true;
}
}); });
}
if close {
self.creation_content = None;
}
} else if show_wallet && dual_panel { } else if show_wallet && dual_panel {
if qr_scan {
View::title_button_big(ui, ARROW_LEFT, |_| {
cb.stop_camera();
self.wallet_content.as_mut().unwrap().qr_scan_content = None;
});
} else {
let list_icon = if show_list { let list_icon = if show_list {
SIDEBAR_SIMPLE SIDEBAR_SIMPLE
} else { } else {
@ -314,7 +474,8 @@ impl WalletsContent {
self.show_wallets_at_dual_panel = !show_list; self.show_wallets_at_dual_panel = !show_list;
AppConfig::toggle_show_wallets_at_dual_panel(); AppConfig::toggle_show_wallets_at_dual_panel();
}); });
} else if !Content::is_dual_panel_mode(ui) { }
} else if !Content::is_dual_panel_mode(ui.ctx()) {
View::title_button_big(ui, GLOBE, |_| { View::title_button_big(ui, GLOBE, |_| {
Content::toggle_network_panel(); Content::toggle_network_panel();
}); });
@ -330,69 +491,39 @@ impl WalletsContent {
}, ui); }, ui);
} }
/// Calculate [`WalletContent`] panel width.
fn wallet_panel_width(
&self,
ui:&mut egui::Ui,
list_empty: bool,
dual_panel: bool,
show_wallet: bool
) -> f32 {
let create_wallet = self.creation_content.can_go_back();
let available_width = if list_empty || create_wallet || (show_wallet && !dual_panel)
|| (show_wallet && !self.show_wallets_at_dual_panel) {
ui.available_width()
} else {
ui.available_width() - Content::SIDE_PANEL_WIDTH
};
if dual_panel && show_wallet && self.show_wallets_at_dual_panel {
let min_width = Content::SIDE_PANEL_WIDTH + View::get_right_inset();
f32::max(min_width, available_width)
} else {
available_width
}
}
/// Draw list of wallets. /// Draw list of wallets.
fn wallet_list_ui(&mut self, fn wallet_list_ui(&mut self,
ui: &mut egui::Ui, ui: &mut egui::Ui,
cb: &dyn PlatformCallbacks) { cb: &dyn PlatformCallbacks) {
ScrollArea::vertical() ScrollArea::vertical()
.id_source("wallet_list") .id_salt("wallet_list_scroll")
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden) .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.auto_shrink([false; 2]) .auto_shrink([false; 2])
.show(ui, |ui| { .show(ui, |ui| {
ui.vertical_centered(|ui| {
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| { View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
// Show application logo and name. // Show application logo and name.
View::app_logo_name_version(ui); View::app_logo_name_version(ui);
ui.add_space(15.0); ui.add_space(15.0);
let mut list = self.wallets.list().clone(); let list = self.wallets.list().clone();
// Remove deleted wallet from the list. for w in &list {
list.retain(|w| { // Remove deleted.
let deleted = w.is_deleted(); if w.is_deleted() {
if deleted { self.wallet_content = None;
self.wallets.select(None);
self.wallets.remove(w.get_config().id); self.wallets.remove(w.get_config().id);
ui.ctx().request_repaint(); ui.ctx().request_repaint();
continue;
} }
!deleted
});
for wallet in &list {
// Check if wallet reopen is needed. // Check if wallet reopen is needed.
if !wallet.is_open() && wallet.reopen_needed() { if w.reopen_needed() && !w.is_open() {
wallet.set_reopen(false); w.set_reopen(false);
self.wallets.select(Some(wallet.get_config().id)); self.show_opening_modal(w.clone(), None, cb);
self.show_open_wallet_modal(cb);
} }
// Draw wallet list item. self.wallet_item_ui(ui, w, cb);
self.wallet_item_ui(ui, wallet, cb);
ui.add_space(5.0); ui.add_space(5.0);
} }
}); });
}); });
});
} }
/// Draw wallet list item. /// Draw wallet list item.
@ -401,9 +532,11 @@ impl WalletsContent {
wallet: &Wallet, wallet: &Wallet,
cb: &dyn PlatformCallbacks) { cb: &dyn PlatformCallbacks) {
let config = wallet.get_config(); let config = wallet.get_config();
let id = config.id; let current = if let Some(content) = &self.wallet_content {
let is_selected = self.wallets.selected_id == Some(id); content.wallet.get_config().id == config.id && wallet.is_open()
let current = is_selected && wallet.is_open(); } else {
false
};
// Draw round background. // Draw round background.
let mut rect = ui.available_rect_before_wrap(); let mut rect = ui.available_rect_before_wrap();
@ -420,27 +553,36 @@ impl WalletsContent {
if !wallet.is_open() { if !wallet.is_open() {
// Show button to open closed wallet. // Show button to open closed wallet.
View::item_button(ui, View::item_rounding(0, 1, true), FOLDER_OPEN, None, || { View::item_button(ui, View::item_rounding(0, 1, true), FOLDER_OPEN, None, || {
self.wallets.select(Some(id)); self.show_opening_modal(wallet.clone(), None, cb);
self.show_open_wallet_modal(cb);
}); });
// Show button to select connection if not syncing.
if !wallet.syncing() { if !wallet.syncing() {
let mut show_selection = false;
View::item_button(ui, Rounding::default(), GLOBE, None, || { View::item_button(ui, Rounding::default(), GLOBE, None, || {
self.wallets.select(Some(id)); self.wallet_content = Some(WalletContent::new(wallet.clone(), None));
self.show_connection_selector_modal(wallet); self.conn_selection_content = Some(
WalletConnectionModal::new(wallet.get_current_connection())
);
// Show connection selection modal.
Modal::new(SELECT_CONNECTION_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("wallets.conn_method"))
.show();
show_selection = true;
}); });
if show_selection {
ExternalConnection::check(None, ui.ctx());
}
} }
} else { } else {
if !is_selected { if !current {
// Show button to select opened wallet. // Show button to select opened wallet.
View::item_button(ui, View::item_rounding(0, 1, true), CARET_RIGHT, None, || { View::item_button(ui, View::item_rounding(0, 1, true), CARET_RIGHT, None, || {
self.wallets.select(Some(id)); self.wallet_content = Some(WalletContent::new(wallet.clone(), None));
self.wallet_content = WalletContent::default();
}); });
} }
// Show button to close opened wallet. // Show button to close opened wallet.
if !wallet.is_closing() { if !wallet.is_closing() {
View::item_button(ui, if !is_selected { View::item_button(ui, if !current {
Rounding::default() Rounding::default()
} else { } else {
View::item_rounding(0, 1, true) View::item_rounding(0, 1, true)
@ -455,8 +597,8 @@ impl WalletsContent {
ui.add_space(6.0); ui.add_space(6.0);
ui.vertical(|ui| { ui.vertical(|ui| {
ui.add_space(3.0); ui.add_space(3.0);
// Setup wallet name text. // Show wallet name text.
let name_color = if is_selected { let name_color = if current {
Colors::white_or_black(true) Colors::white_or_black(true)
} else { } else {
Colors::title(false) Colors::title(false)
@ -466,46 +608,17 @@ impl WalletsContent {
View::ellipsize_text(ui, config.name, 18.0, name_color); View::ellipsize_text(ui, config.name, 18.0, name_color);
}); });
// Setup wallet status text. // Show wallet status text.
let status_text = if wallet.is_open() { View::ellipsize_text(ui, wallet_status_text(wallet), 15.0, Colors::text(false));
if wallet.sync_error() {
format!("{} {}", WARNING_CIRCLE, t!("error"))
} else if wallet.is_closing() {
format!("{} {}", SPINNER, t!("wallets.closing"))
} else if wallet.is_repairing() {
let repair_progress = wallet.repairing_progress();
if repair_progress == 0 {
format!("{} {}", SPINNER, t!("wallets.checking"))
} else {
format!("{} {}: {}%",
SPINNER,
t!("wallets.checking"),
repair_progress)
}
} else if wallet.syncing() {
let info_progress = wallet.info_sync_progress();
if info_progress == 100 || info_progress == 0 {
format!("{} {}", SPINNER, t!("wallets.loading"))
} else {
format!("{} {}: {}%",
SPINNER,
t!("wallets.loading"),
info_progress)
}
} else {
format!("{} {}", FOLDER_OPEN, t!("wallets.unlocked"))
}
} else {
format!("{} {}", FOLDER_LOCK, t!("wallets.locked"))
};
View::ellipsize_text(ui, status_text, 15.0, Colors::text(false));
ui.add_space(1.0); ui.add_space(1.0);
// Setup wallet connection text. // Show wallet connection text.
let conn_text = if let Some(conn) = wallet.get_current_ext_conn() { let connection = wallet.get_current_connection();
format!("{} {}", GLOBE_SIMPLE, conn.url) let conn_text = match connection {
} else { ConnectionMethod::Integrated => {
format!("{} {}", COMPUTER_TOWER, t!("network.node")) format!("{} {}", COMPUTER_TOWER, t!("network.node"))
}
ConnectionMethod::External(_, url) => format!("{} {}", GLOBE_SIMPLE, url)
}; };
ui.label(RichText::new(conn_text).size(15.0).color(Colors::gray())); ui.label(RichText::new(conn_text).size(15.0).color(Colors::gray()));
ui.add_space(3.0); ui.add_space(3.0);
@ -514,23 +627,13 @@ impl WalletsContent {
}); });
} }
/// Show [`Modal`] to select connection for the wallet. /// Show [`Modal`] to select and open wallet.
fn show_connection_selector_modal(&mut self, wallet: &Wallet) { fn show_opening_modal(&mut self,
let ext_conn = wallet.get_current_ext_conn(); wallet: Wallet,
self.conn_modal_content = Some(WalletConnectionModal::new(ext_conn)); data: Option<String>,
// Show modal. cb: &dyn PlatformCallbacks) {
Modal::new(CONNECTION_SELECTION_MODAL) self.wallet_content = Some(WalletContent::new(wallet.clone(), None));
.position(ModalPosition::CenterTop) self.open_wallet_content = Some(OpenWalletModal::new(wallet, data));
.title(t!("wallets.conn_method"))
.show();
}
/// Show [`Modal`] to open selected wallet.
fn show_open_wallet_modal(&mut self, cb: &dyn PlatformCallbacks) {
// Reset modal values.
self.pass_edit = String::from("");
self.wrong_pass = false;
// Show modal.
Modal::new(OPEN_WALLET_MODAL) Modal::new(OPEN_WALLET_MODAL)
.position(ModalPosition::CenterTop) .position(ModalPosition::CenterTop)
.title(t!("wallets.open")) .title(t!("wallets.open"))
@ -538,104 +641,10 @@ impl WalletsContent {
cb.show_keyboard(); cb.show_keyboard();
} }
/// Draw wallet opening [`Modal`] content. /// Check if it's possible to show [`WalletsContent`] and [`WalletContent`] panels at same time.
fn open_wallet_modal_ui(&mut self, fn is_dual_panel_mode(ui: &mut egui::Ui) -> bool {
ui: &mut egui::Ui, let dual_panel_root = Content::is_dual_panel_mode(ui.ctx());
modal: &Modal,
cb: &dyn PlatformCallbacks) {
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("wallets.pass"))
.size(17.0)
.color(Colors::gray()));
ui.add_space(8.0);
// Show password input.
let mut pass_edit_opts = TextEditOptions::new(Id::from(modal.id)).password();
View::text_edit(ui, cb, &mut self.pass_edit, &mut pass_edit_opts);
// Show information when password is empty.
if self.pass_edit.is_empty() {
self.wrong_pass = false;
ui.add_space(10.0);
ui.label(RichText::new(t!("wallets.pass_empty"))
.size(17.0)
.color(Colors::inactive_text()));
} else if self.wrong_pass {
ui.add_space(10.0);
ui.label(RichText::new(t!("wallets.wrong_pass"))
.size(17.0)
.color(Colors::red()));
}
ui.add_space(12.0);
});
// Show modal buttons.
ui.scope(|ui| {
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
// Close modal.
cb.hide_keyboard();
modal.close();
});
});
columns[1].vertical_centered_justified(|ui| {
// Callback for button to continue.
let mut on_continue = || {
if self.pass_edit.is_empty() {
return;
}
match self.wallets.open_selected(self.pass_edit.clone()) {
Ok(_) => {
// Clear values.
self.pass_edit = "".to_string();
self.wrong_pass = false;
// Close modal.
cb.hide_keyboard();
modal.close();
// Reset wallet content.
self.wallet_content = WalletContent::default();
}
Err(_) => self.wrong_pass = true
}
};
// Continue on Enter key press.
View::on_enter_key(ui, || {
(on_continue)();
});
View::button(ui, t!("continue"), Colors::white_or_black(false), on_continue);
});
});
ui.add_space(6.0);
});
}
/// Handle Back key event.
/// Return `false` when event was handled.
pub fn on_back(&mut self) -> bool {
let can_go_back = self.creation_content.can_go_back();
if can_go_back {
self.creation_content.back();
return false
} else {
if self.wallets.is_selected_open() {
self.wallets.select(None);
return false
}
}
true
}
}
/// Check if it's possible to show [`WalletsContent`] and [`WalletContent`] panels at same time.
fn is_dual_panel_mode(ui: &mut egui::Ui) -> bool {
let dual_panel_root = Content::is_dual_panel_mode(ui);
let max_width = ui.available_width(); let max_width = ui.available_width();
dual_panel_root && max_width >= (Content::SIDE_PANEL_WIDTH * 2.0) + View::get_right_inset() dual_panel_root && max_width >= (Content::SIDE_PANEL_WIDTH * 2.0) + View::get_right_inset()
}
} }

View file

@ -17,86 +17,131 @@ use egui::scroll_area::ScrollBarVisibility;
use grin_util::ZeroingString; use grin_util::ZeroingString;
use crate::gui::Colors; use crate::gui::Colors;
use crate::gui::icons::{CHECK, CLIPBOARD_TEXT, COPY, FOLDER_PLUS, SCAN, SHARE_FAT}; use crate::gui::icons::{CHECK, CLIPBOARD_TEXT, COPY, SCAN};
use crate::gui::platform::PlatformCallbacks; use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Modal, Content, View}; use crate::gui::views::{Modal, Content, View, CameraScanModal};
use crate::gui::views::types::{ModalPosition, TextEditOptions}; use crate::gui::views::types::{LinePosition, ModalContainer, ModalPosition, QrScanResult};
use crate::gui::views::wallets::creation::MnemonicSetup; use crate::gui::views::wallets::creation::MnemonicSetup;
use crate::gui::views::wallets::creation::types::Step; use crate::gui::views::wallets::creation::types::Step;
use crate::gui::views::wallets::settings::ConnectionSettings; use crate::gui::views::wallets::ConnectionSettings;
use crate::node::Node; use crate::node::Node;
use crate::wallet::{ExternalConnection, Wallet}; use crate::wallet::{ExternalConnection, Wallet};
use crate::wallet::types::PhraseMode; use crate::wallet::types::PhraseMode;
/// Wallet creation content. /// Wallet creation content.
pub struct WalletCreation { pub struct WalletCreation {
/// Wallet creation step. /// Wallet name.
step: Option<Step>, pub name: String,
/// Wallet password.
pub pass: ZeroingString,
/// Flag to check if wallet creation [`Modal`] was just opened to focus on first field. /// Wallet creation step.
modal_just_opened: bool, step: Step,
/// Wallet name value.
name_edit: String, /// QR code scanning [`Modal`] content.
/// Password to encrypt created wallet. scan_modal_content: Option<CameraScanModal>,
pass_edit: String,
/// Mnemonic phrase setup content. /// Mnemonic phrase setup content.
pub(crate) mnemonic_setup: MnemonicSetup, mnemonic_setup: MnemonicSetup,
/// Network setup content. /// Network setup content.
pub(crate) network_setup: ConnectionSettings network_setup: ConnectionSettings,
/// Flag to check if an error occurred during wallet creation.
creation_error: Option<String>,
/// [`Modal`] identifiers allowed at this ui container.
modal_ids: Vec<&'static str>
} }
impl Default for WalletCreation { const QR_CODE_PHRASE_SCAN_MODAL: &'static str = "qr_code_rec_phrase_scan_modal";
fn default() -> Self {
Self { impl ModalContainer for WalletCreation {
step: None, fn modal_ids(&self) -> &Vec<&'static str> {
modal_just_opened: true, &self.modal_ids
name_edit: String::from(""), }
pass_edit: String::from(""),
mnemonic_setup: MnemonicSetup::default(), fn modal_ui(&mut self,
network_setup: ConnectionSettings::default() ui: &mut egui::Ui,
modal: &Modal,
cb: &dyn PlatformCallbacks) {
match modal.id {
QR_CODE_PHRASE_SCAN_MODAL => {
if let Some(content) = self.scan_modal_content.as_mut() {
content.ui(ui, modal, cb, |result| {
match result {
QrScanResult::Text(text) => {
self.mnemonic_setup.mnemonic.import(&text);
modal.close();
}
QrScanResult::SeedQR(text) => {
self.mnemonic_setup.mnemonic.import(&text);
modal.close();
}
_ => {}
}
});
}
},
_ => {}
} }
} }
} }
impl WalletCreation { impl WalletCreation {
/// Wallet name/password input modal identifier. /// Create new wallet creation instance from name and password.
pub const NAME_PASS_MODAL: &'static str = "name_pass_modal"; pub fn new(name: String, pass: ZeroingString) -> Self {
Self {
name,
pass,
step: Step::EnterMnemonic,
scan_modal_content: None,
mnemonic_setup: MnemonicSetup::default(),
network_setup: ConnectionSettings::default(),
creation_error: None,
modal_ids: vec![
QR_CODE_PHRASE_SCAN_MODAL
],
}
}
/// Draw wallet creation content. /// Draw wallet creation content.
pub fn ui(&mut self, pub fn ui(&mut self,
ui: &mut egui::Ui, ui: &mut egui::Ui,
cb: &dyn PlatformCallbacks, cb: &dyn PlatformCallbacks,
on_create: impl FnOnce(Wallet)) { on_create: impl FnMut(Wallet)) {
// Show wallet creation step description and confirmation panel. self.current_modal_ui(ui, cb);
if self.step.is_some() {
egui::TopBottomPanel::bottom("wallet_creation_step_panel") egui::TopBottomPanel::bottom("wallet_creation_step_panel")
.frame(egui::Frame { .frame(egui::Frame {
fill: Colors::fill(),
inner_margin: Margin { inner_margin: Margin {
left: View::far_left_inset_margin(ui) + 8.0, left: View::far_left_inset_margin(ui) + View::TAB_ITEMS_PADDING,
right: View::get_right_inset() + 8.0, right: View::get_right_inset() + View::TAB_ITEMS_PADDING,
top: 4.0, top: View::TAB_ITEMS_PADDING,
bottom: View::get_bottom_inset(), bottom: View::get_bottom_inset() + View::TAB_ITEMS_PADDING,
}, },
fill: Colors::fill_deep(),
..Default::default() ..Default::default()
}) })
.show_inside(ui, |ui| { .show_inside(ui, |ui| {
ui.vertical_centered(|ui| { // Draw divider line.
ui.vertical_centered(|ui| { let rect = {
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 2.0, |ui| { let mut r = ui.available_rect_before_wrap();
r.min.y -= View::TAB_ITEMS_PADDING;
r.min.x -= View::far_left_inset_margin(ui) + View::TAB_ITEMS_PADDING;
r.max.x += View::get_right_inset() + View::TAB_ITEMS_PADDING;
r
};
View::line(ui, LinePosition::TOP, &rect, Colors::item_stroke());
// Show step control content.
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
self.step_control_ui(ui, on_create, cb); self.step_control_ui(ui, on_create, cb);
}); });
}); });
});
});
}
// Show wallet creation step content panel. // Show wallet creation step content panel.
egui::CentralPanel::default() egui::CentralPanel::default()
.frame(egui::Frame { .frame(egui::Frame {
stroke: View::item_stroke(),
inner_margin: Margin { inner_margin: Margin {
left: View::far_left_inset_margin(ui) + 4.0, left: View::far_left_inset_margin(ui) + 4.0,
right: View::get_right_inset() + 4.0, right: View::get_right_inset() + 4.0,
@ -106,18 +151,12 @@ impl WalletCreation {
..Default::default() ..Default::default()
}) })
.show_inside(ui, |ui| { .show_inside(ui, |ui| {
let id = if let Some(step) = &self.step {
format!("creation_step_scroll_{}", step.name())
} else {
"creation_step_scroll".to_owned()
};
ScrollArea::vertical() ScrollArea::vertical()
.id_source(id) .id_salt(Id::from(format!("creation_step_scroll_{}", self.step.name())))
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden) .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.auto_shrink([false; 2]) .auto_shrink([false; 2])
.show(ui, |ui| { .show(ui, |ui| {
ui.vertical_centered(|ui| { let max_width = if self.step == Step::SetupConnection {
let max_width = if self.step == Some(Step::SetupConnection) {
Content::SIDE_PANEL_WIDTH * 1.3 Content::SIDE_PANEL_WIDTH * 1.3
} else { } else {
Content::SIDE_PANEL_WIDTH * 2.0 Content::SIDE_PANEL_WIDTH * 2.0
@ -127,7 +166,6 @@ impl WalletCreation {
}); });
}); });
}); });
});
} }
/// Draw [`Step`] description and confirmation control. /// Draw [`Step`] description and confirmation control.
@ -135,176 +173,176 @@ impl WalletCreation {
ui: &mut egui::Ui, ui: &mut egui::Ui,
on_create: impl FnOnce(Wallet), on_create: impl FnOnce(Wallet),
cb: &dyn PlatformCallbacks) { cb: &dyn PlatformCallbacks) {
if let Some(step) = self.step.clone() { let step = &self.step;
// Setup step description text and availability. // Setup description and next step availability.
let (step_text, mut step_available) = match step { let (step_text, mut next) = match step {
Step::EnterMnemonic => { Step::EnterMnemonic => {
let mode = &self.mnemonic_setup.mnemonic.mode; let mode = &self.mnemonic_setup.mnemonic.mode();
let text = if mode == &PhraseMode::Generate { let (text, available) = match mode {
t!("wallets.create_phrase_desc") PhraseMode::Generate => (t!("wallets.create_phrase_desc"), true),
} else { PhraseMode::Import => {
t!("wallets.restore_phrase_desc") let available = !self.mnemonic_setup.mnemonic.has_empty_or_invalid();
(t!("wallets.restore_phrase_desc"), available)
}
}; };
let available = !self
.mnemonic_setup
.mnemonic
.words
.contains(&String::from(""));
(text, available) (text, available)
} }
Step::ConfirmMnemonic => { Step::ConfirmMnemonic => {
let text = t!("wallets.restore_phrase_desc"); let text = t!("wallets.restore_phrase_desc");
let available = !self let available = !self.mnemonic_setup.mnemonic.has_empty_or_invalid();
.mnemonic_setup
.mnemonic
.confirm_words
.contains(&String::from(""));
(text, available) (text, available)
}, }
Step::SetupConnection => (t!("wallets.setup_conn_desc"), true) Step::SetupConnection => {
(t!("wallets.setup_conn_desc"), self.creation_error.is_none())
}
}; };
// Show step description.
ui.add_space(2.0); // Show step description or error.
let generate_step = step == &Step::EnterMnemonic &&
self.mnemonic_setup.mnemonic.mode() == PhraseMode::Generate;
if (self.mnemonic_setup.mnemonic.valid() && self.creation_error.is_none()) ||
generate_step {
ui.label(RichText::new(step_text).size(16.0).color(Colors::gray())); ui.label(RichText::new(step_text).size(16.0).color(Colors::gray()));
ui.add_space(2.0); ui.add_space(6.0);
// Show error if entered phrase is not valid. } else {
if !self.mnemonic_setup.valid_phrase { next = false;
step_available = false; // Show error text.
ui.label(RichText::new(t!("wallets.not_valid_phrase")) if let Some(err) = &self.creation_error {
ui.add_space(10.0);
ui.label(RichText::new(err)
.size(16.0)
.color(Colors::red()));
ui.add_space(10.0);
} else {
ui.label(RichText::new(&t!("wallets.not_valid_phrase"))
.size(16.0) .size(16.0)
.color(Colors::red())); .color(Colors::red()));
ui.add_space(2.0);
}
if step == Step::EnterMnemonic {
ui.add_space(4.0); ui.add_space(4.0);
};
}
// Setup spacing between buttons. // Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); ui.style_mut().spacing.item_spacing = egui::vec2(8.0, 0.0);
// Setup vertical padding inside button.
ui.style_mut().spacing.button_padding = egui::vec2(10.0, 7.0);
match step {
Step::EnterMnemonic => {
ui.columns(2, |columns| { ui.columns(2, |columns| {
// Show copy or paste button for mnemonic phrase step. // Show copy or paste button for mnemonic phrase step.
columns[0].vertical_centered_justified(|ui| { columns[0].vertical_centered_justified(|ui| {
self.copy_or_paste_button_ui(ui, cb); match self.mnemonic_setup.mnemonic.mode() {
});
columns[1].vertical_centered_justified(|ui| {
if step_available {
// Show next step button if there are no empty words.
self.next_step_button_ui(ui, step, on_create);
} else {
// Show QR code scan button.
let scan_text = format!("{} {}", SCAN, t!("scan").to_uppercase());
View::button(ui, scan_text, Colors::white_or_black(false), || {
self.mnemonic_setup.show_qr_scan_modal(cb);
});
}
});
});
ui.add_space(4.0);
} else if step == Step::ConfirmMnemonic {
ui.add_space(4.0);
// Show next step or paste button.
if step_available {
self.next_step_button_ui(ui, step, on_create);
} else {
let paste_text = format!("{} {}", CLIPBOARD_TEXT, t!("paste").to_uppercase());
View::button(ui, paste_text, Colors::white_or_black(false), || {
let data = ZeroingString::from(cb.get_string_from_buffer().trim());
self.mnemonic_setup.mnemonic.import_text(&data, true);
});
}
ui.add_space(4.0);
} else if step_available {
ui.add_space(4.0);
self.next_step_button_ui(ui, step, on_create);
ui.add_space(4.0);
}
ui.add_space(4.0);
}
}
/// Draw copy or paste button at [`Step::EnterMnemonic`].
fn copy_or_paste_button_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
match self.mnemonic_setup.mnemonic.mode {
PhraseMode::Generate => { PhraseMode::Generate => {
// Show copy button. let c_t = format!("{} {}",
let c_t = format!("{} {}", COPY, t!("copy").to_uppercase()); COPY,
View::button(ui, c_t.to_uppercase(), Colors::white_or_black(false), || { t!("copy").to_uppercase());
cb.copy_string_to_buffer(self.mnemonic_setup.mnemonic.get_phrase()); View::button(ui, c_t, Colors::white_or_black(false), || {
cb.copy_string_to_buffer(self.mnemonic_setup
.mnemonic
.get_phrase());
}); });
} }
PhraseMode::Import => { PhraseMode::Import => {
// Show paste button. let p_t = format!("{} {}",
let p_t = format!("{} {}", CLIPBOARD_TEXT, t!("paste").to_uppercase()); CLIPBOARD_TEXT,
t!("paste").to_uppercase());
View::button(ui, p_t, Colors::white_or_black(false), || { View::button(ui, p_t, Colors::white_or_black(false), || {
let data = ZeroingString::from(cb.get_string_from_buffer().trim()); let data = ZeroingString::from(cb.get_string_from_buffer());
self.mnemonic_setup.mnemonic.import_text(&data, false); self.mnemonic_setup.mnemonic.import(&data);
}); });
} }
} }
});
// Show next step or QR code scan button.
columns[1].vertical_centered_justified(|ui| {
if next {
self.next_step_button_ui(ui, on_create);
} else {
let scan_text = format!("{} {}",
SCAN,
t!("scan").to_uppercase());
View::button(ui, scan_text, Colors::white_or_black(false), || {
self.scan_modal_content = Some(CameraScanModal::default());
// Show QR code scan modal.
Modal::new(QR_CODE_PHRASE_SCAN_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("scan_qr"))
.closeable(false)
.show();
cb.start_camera();
});
}
});
});
}
Step::ConfirmMnemonic => {
// Show next step or paste button.
if next {
self.next_step_button_ui(ui, on_create);
} else {
let paste_text = format!("{} {}", CLIPBOARD_TEXT, t!("paste").to_uppercase());
View::button(ui, paste_text, Colors::white_or_black(false), || {
let data = ZeroingString::from(cb.get_string_from_buffer());
self.mnemonic_setup.mnemonic.import(&data);
});
}
}
Step::SetupConnection => {
if next {
self.next_step_button_ui(ui, on_create);
ui.add_space(2.0);
}
}
}
} }
/// Draw button to go to next [`Step`]. /// Draw button to go to next [`Step`].
fn next_step_button_ui(&mut self, fn next_step_button_ui(&mut self,
ui: &mut egui::Ui, ui: &mut egui::Ui,
step: Step,
on_create: impl FnOnce(Wallet)) { on_create: impl FnOnce(Wallet)) {
// Setup button text. // Setup button text.
let (next_text, text_color, bg_color) = if step == Step::SetupConnection { let (next_text, text_color, bg_color) = if self.step == Step::SetupConnection {
(format!("{} {}", CHECK, t!("complete")), Colors::title(true), Colors::gold()) (format!("{} {}", CHECK, t!("complete")), Colors::title(true), Colors::gold())
} else { } else {
let text = format!("{} {}", SHARE_FAT, t!("continue")); (t!("continue"), Colors::green(), Colors::white_or_black(false))
(text, Colors::text_button(), Colors::white_or_black(false))
}; };
// Show next step button. // Show next step button.
View::colored_text_button(ui, next_text.to_uppercase(), text_color, bg_color, || { View::colored_text_button_ui(ui, next_text.to_uppercase(), text_color, bg_color, |ui| {
self.step = if let Some(step) = &self.step { self.step = match self.step {
match step {
Step::EnterMnemonic => { Step::EnterMnemonic => {
if self.mnemonic_setup.mnemonic.mode == PhraseMode::Generate { if self.mnemonic_setup.mnemonic.mode() == PhraseMode::Generate {
Some(Step::ConfirmMnemonic) Step::ConfirmMnemonic
} else { } else {
// Check if entered phrase was valid. Step::SetupConnection
if self.mnemonic_setup.valid_phrase {
Some(Step::SetupConnection)
} else {
Some(Step::EnterMnemonic)
}
} }
} }
Step::ConfirmMnemonic => { Step::ConfirmMnemonic => {
Some(Step::SetupConnection) Step::SetupConnection
}, },
Step::SetupConnection => { Step::SetupConnection => {
// Create wallet at last step. // Create wallet at last step.
let name = self.name_edit.clone(); match Wallet::create(&self.name,
let pass = self.pass_edit.clone(); &self.pass,
let phrase = self.mnemonic_setup.mnemonic.get_phrase(); &self.mnemonic_setup.mnemonic,
let conn_method = &self.network_setup.method; &self.network_setup.method) {
let mut wallet = Wallet::create(name, Ok(w) => {
pass.clone(),
phrase,
conn_method).unwrap();
// Open created wallet.
wallet.open(pass).unwrap();
// Pass created wallet to callback.
(on_create)(wallet);
// Reset input data.
self.step = None;
self.name_edit = String::from("");
self.pass_edit = String::from("");
self.mnemonic_setup.reset(); self.mnemonic_setup.reset();
None // Pass created wallet to callback.
(on_create)(w);
Step::EnterMnemonic
}
Err(e) => {
self.creation_error = Some(format!("{:?}", e));
Step::SetupConnection
}
} }
} }
} else {
Some(Step::EnterMnemonic)
}; };
// Check external connections availability on connection setup. // Check external connections availability on connection setup.
if self.step == Some(Step::SetupConnection) { if self.step == Step::SetupConnection {
ExternalConnection::check_ext_conn_availability(None); ExternalConnection::check(None, ui.ctx());
} }
}); });
} }
@ -312,148 +350,31 @@ impl WalletCreation {
/// Draw wallet creation [`Step`] content. /// Draw wallet creation [`Step`] content.
fn step_content_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { fn step_content_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
match &self.step { match &self.step {
None => {
// Show wallet creation message if step is empty.
View::center_content(ui, 350.0 + View::get_bottom_inset(), |ui| {
// Show app logo.
View::app_logo_name_version(ui);
ui.add_space(4.0);
let text = t!("wallets.create_desc");
ui.label(RichText::new(text)
.size(16.0)
.color(Colors::gray())
);
ui.add_space(8.0);
// Show wallet creation button.
let add_text = format!("{} {}", FOLDER_PLUS, t!("wallets.add"));
View::button(ui, add_text, Colors::white_or_black(false), || {
self.show_name_pass_modal(cb);
});
});
}
Some(step) => {
match step {
Step::EnterMnemonic => self.mnemonic_setup.ui(ui, cb), Step::EnterMnemonic => self.mnemonic_setup.ui(ui, cb),
Step::ConfirmMnemonic => self.mnemonic_setup.confirm_ui(ui, cb), Step::ConfirmMnemonic => self.mnemonic_setup.confirm_ui(ui, cb),
Step::SetupConnection => { Step::SetupConnection => {
// Redraw if node is running. // Redraw if node is running.
if Node::is_running() { if Node::is_running() && !Content::is_dual_panel_mode(ui.ctx()) {
ui.ctx().request_repaint_after(Node::STATS_UPDATE_DELAY); ui.ctx().request_repaint_after(Node::STATS_UPDATE_DELAY);
} }
self.network_setup.create_ui(ui, cb) self.network_setup.create_ui(ui, cb)
} }
} }
} }
}
}
/// Check if it's possible to go back for current step. /// Back to previous wallet creation [`Step`], return `true` to close creation.
pub fn can_go_back(&self) -> bool { pub fn on_back(&mut self) -> bool {
self.step.is_some()
}
/// Back to previous wallet creation [`Step`].
pub fn back(&mut self) {
match &self.step { match &self.step {
None => {} Step::ConfirmMnemonic => {
Some(step) => { self.step = Step::EnterMnemonic;
match step { false
Step::EnterMnemonic => {
self.step = None;
self.name_edit = String::from("");
self.pass_edit = String::from("");
self.mnemonic_setup.reset();
}, },
Step::ConfirmMnemonic => self.step = Some(Step::EnterMnemonic), Step::SetupConnection => {
Step::SetupConnection => self.step = Some(Step::EnterMnemonic) self.creation_error = None;
self.step = Step::EnterMnemonic;
false
} }
_ => true
} }
} }
}
/// Start wallet creation from showing [`Modal`] to enter name and password.
pub fn show_name_pass_modal(&mut self, cb: &dyn PlatformCallbacks) {
// Reset modal values.
self.modal_just_opened = true;
self.name_edit = t!("wallets.default_wallet");
self.pass_edit = String::from("");
// Show modal.
Modal::new(Self::NAME_PASS_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("wallets.add"))
.show();
cb.show_keyboard();
}
/// Draw creating wallet name/password input [`Modal`] content.
pub fn name_pass_modal_ui(&mut self,
ui: &mut egui::Ui,
modal: &Modal,
cb: &dyn PlatformCallbacks) {
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("wallets.name"))
.size(17.0)
.color(Colors::gray()));
ui.add_space(8.0);
// Show wallet name text edit.
let mut name_edit_opts = TextEditOptions::new(Id::from(modal.id).with("name"))
.no_focus();
if self.modal_just_opened {
self.modal_just_opened = false;
name_edit_opts.focus = true;
}
View::text_edit(ui, cb, &mut self.name_edit, &mut name_edit_opts);
ui.add_space(8.0);
ui.label(RichText::new(t!("wallets.pass"))
.size(17.0)
.color(Colors::gray()));
ui.add_space(8.0);
// Draw wallet password text edit.
let mut pass_text_edit_opts = TextEditOptions::new(Id::from(modal.id).with("pass"))
.password()
.no_focus();
View::text_edit(ui, cb, &mut self.pass_edit, &mut pass_text_edit_opts);
ui.add_space(12.0);
});
// Show modal buttons.
ui.scope(|ui| {
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
// Close modal.
cb.hide_keyboard();
modal.close();
});
});
columns[1].vertical_centered_justified(|ui| {
let mut on_next = || {
// Check if input values are not empty.
if self.name_edit.is_empty() || self.pass_edit.is_empty() {
return;
}
self.step = Some(Step::EnterMnemonic);
cb.hide_keyboard();
modal.close();
};
// Go to next creation step on Enter button press.
View::on_enter_key(ui, || {
(on_next)();
});
View::button(ui, t!("continue"), Colors::white_or_black(false), on_next);
});
});
ui.add_space(6.0);
});
}
} }

View file

@ -17,31 +17,23 @@ use egui::{Id, RichText};
use crate::gui::Colors; use crate::gui::Colors;
use crate::gui::icons::PENCIL; use crate::gui::icons::PENCIL;
use crate::gui::platform::PlatformCallbacks; use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{CameraContent, Modal, Content, View}; use crate::gui::views::{Modal, Content, View};
use crate::gui::views::types::{ModalContainer, ModalPosition, QrScanResult, TextEditOptions}; use crate::gui::views::types::{ModalContainer, ModalPosition, TextEditOptions};
use crate::wallet::Mnemonic; use crate::wallet::Mnemonic;
use crate::wallet::types::{PhraseMode, PhraseSize}; use crate::wallet::types::{PhraseMode, PhraseSize, PhraseWord};
/// Mnemonic phrase setup content. /// Mnemonic phrase setup content.
pub struct MnemonicSetup { pub struct MnemonicSetup {
/// Current mnemonic phrase. /// Current mnemonic phrase.
pub(crate) mnemonic: Mnemonic, pub mnemonic: Mnemonic,
/// Flag to check if entered phrase was valid.
pub(crate) valid_phrase: bool,
/// Current word number to edit at [`Modal`]. /// Current word number to edit at [`Modal`].
word_num_edit: usize, word_index_edit: usize,
/// Entered word value for [`Modal`]. /// Entered word value for [`Modal`].
word_edit: String, word_edit: String,
/// Flag to check if entered word is valid. /// Flag to check if entered word is valid at [`Modal`].
valid_word_edit: bool, valid_word_edit: bool,
/// Camera content for QR scan [`Modal`].
camera_content: CameraContent,
/// Flag to check if recovery phrase was found at QR code scanning [`Modal`].
scan_phrase_not_found: Option<bool>,
/// [`Modal`] identifiers allowed at this ui container. /// [`Modal`] identifiers allowed at this ui container.
modal_ids: Vec<&'static str> modal_ids: Vec<&'static str>
} }
@ -49,22 +41,15 @@ pub struct MnemonicSetup {
/// Identifier for word input [`Modal`]. /// Identifier for word input [`Modal`].
pub const WORD_INPUT_MODAL: &'static str = "word_input_modal"; pub const WORD_INPUT_MODAL: &'static str = "word_input_modal";
/// Identifier for QR code recovery phrase scan [`Modal`].
const QR_CODE_PHRASE_SCAN_MODAL: &'static str = "qr_code_rec_phrase_scan_modal";
impl Default for MnemonicSetup { impl Default for MnemonicSetup {
fn default() -> Self { fn default() -> Self {
Self { Self {
mnemonic: Mnemonic::default(), mnemonic: Mnemonic::default(),
valid_phrase: true, word_index_edit: 0,
word_num_edit: 0,
word_edit: String::from(""), word_edit: String::from(""),
valid_word_edit: true, valid_word_edit: true,
camera_content: CameraContent::default(),
scan_phrase_not_found: None,
modal_ids: vec![ modal_ids: vec![
WORD_INPUT_MODAL, WORD_INPUT_MODAL
QR_CODE_PHRASE_SCAN_MODAL
] ]
} }
} }
@ -81,7 +66,6 @@ impl ModalContainer for MnemonicSetup {
cb: &dyn PlatformCallbacks) { cb: &dyn PlatformCallbacks) {
match modal.id { match modal.id {
WORD_INPUT_MODAL => self.word_modal_ui(ui, modal, cb), WORD_INPUT_MODAL => self.word_modal_ui(ui, modal, cb),
QR_CODE_PHRASE_SCAN_MODAL => self.scan_qr_modal_ui(ui, modal, cb),
_ => {} _ => {}
} }
} }
@ -103,7 +87,7 @@ impl MnemonicSetup {
ui.add_space(6.0); ui.add_space(6.0);
// Show words setup. // Show words setup.
self.word_list_ui(ui, self.mnemonic.mode == PhraseMode::Import, cb); self.word_list_ui(ui, self.mnemonic.mode() == PhraseMode::Import, cb);
} }
/// Draw content for phrase confirmation step. /// Draw content for phrase confirmation step.
@ -123,7 +107,7 @@ impl MnemonicSetup {
/// Draw mode and size setup. /// Draw mode and size setup.
fn mode_type_ui(&mut self, ui: &mut egui::Ui) { fn mode_type_ui(&mut self, ui: &mut egui::Ui) {
// Show mode setup. // Show mode setup.
let mut mode = self.mnemonic.mode.clone(); let mut mode = self.mnemonic.mode();
ui.columns(2, |columns| { ui.columns(2, |columns| {
columns[0].vertical_centered(|ui| { columns[0].vertical_centered(|ui| {
let create_mode = PhraseMode::Generate; let create_mode = PhraseMode::Generate;
@ -136,8 +120,8 @@ impl MnemonicSetup {
View::radio_value(ui, &mut mode, import_mode, import_text); View::radio_value(ui, &mut mode, import_mode, import_text);
}); });
}); });
if mode != self.mnemonic.mode { if mode != self.mnemonic.mode() {
self.mnemonic.set_mode(mode) self.mnemonic.set_mode(mode);
} }
ui.add_space(10.0); ui.add_space(10.0);
@ -150,7 +134,7 @@ impl MnemonicSetup {
ui.add_space(6.0); ui.add_space(6.0);
// Show mnemonic phrase size setup. // Show mnemonic phrase size setup.
let mut size = self.mnemonic.size.clone(); let mut size = self.mnemonic.size();
ui.columns(5, |columns| { ui.columns(5, |columns| {
for (index, word) in PhraseSize::VALUES.iter().enumerate() { for (index, word) in PhraseSize::VALUES.iter().enumerate() {
columns[index].vertical_centered(|ui| { columns[index].vertical_centered(|ui| {
@ -159,29 +143,20 @@ impl MnemonicSetup {
}); });
} }
}); });
if size != self.mnemonic.size { if size != self.mnemonic.size() {
self.mnemonic.set_size(size); self.mnemonic.set_size(size);
} }
} }
/// Draw list of words for mnemonic phrase. /// Draw grid of words for mnemonic phrase.
fn word_list_ui(&mut self, ui: &mut egui::Ui, edit_words: bool, cb: &dyn PlatformCallbacks) { fn word_list_ui(&mut self, ui: &mut egui::Ui, edit: bool, cb: &dyn PlatformCallbacks) {
ui.add_space(6.0); ui.add_space(6.0);
ui.scope(|ui| { ui.scope(|ui| {
// Setup spacing between columns. // Setup spacing between columns.
ui.spacing_mut().item_spacing = egui::Vec2::new(6.0, 6.0); ui.spacing_mut().item_spacing = egui::Vec2::new(6.0, 6.0);
// Select list of words based on current mode and edit flag. // Select list of words based on current mode and edit flag.
let words = match self.mnemonic.mode { let words = self.mnemonic.words(edit);
PhraseMode::Generate => {
if edit_words {
&self.mnemonic.confirm_words
} else {
&self.mnemonic.words
}
}
PhraseMode::Import => &self.mnemonic.words
}.clone();
let mut word_number = 0; let mut word_number = 0;
let cols = list_columns_count(ui); let cols = list_columns_count(ui);
@ -192,25 +167,25 @@ impl MnemonicSetup {
ui.columns(cols, |columns| { ui.columns(cols, |columns| {
columns[0].horizontal(|ui| { columns[0].horizontal(|ui| {
let word = chunk.get(0).unwrap(); let word = chunk.get(0).unwrap();
self.word_item_ui(ui, word_number, word, edit_words, cb); self.word_item_ui(ui, word_number, word, edit, cb);
}); });
columns[1].horizontal(|ui| { columns[1].horizontal(|ui| {
word_number += 1; word_number += 1;
let word = chunk.get(1).unwrap(); let word = chunk.get(1).unwrap();
self.word_item_ui(ui, word_number, word, edit_words, cb); self.word_item_ui(ui, word_number, word, edit, cb);
}); });
if size > 2 { if size > 2 {
columns[2].horizontal(|ui| { columns[2].horizontal(|ui| {
word_number += 1; word_number += 1;
let word = chunk.get(2).unwrap(); let word = chunk.get(2).unwrap();
self.word_item_ui(ui, word_number, word, edit_words, cb); self.word_item_ui(ui, word_number, word, edit, cb);
}); });
} }
if size > 3 { if size > 3 {
columns[3].horizontal(|ui| { columns[3].horizontal(|ui| {
word_number += 1; word_number += 1;
let word = chunk.get(3).unwrap(); let word = chunk.get(3).unwrap();
self.word_item_ui(ui, word_number, word, edit_words, cb); self.word_item_ui(ui, word_number, word, edit, cb);
}); });
} }
}); });
@ -218,7 +193,7 @@ impl MnemonicSetup {
ui.columns(cols, |columns| { ui.columns(cols, |columns| {
columns[0].horizontal(|ui| { columns[0].horizontal(|ui| {
let word = chunk.get(0).unwrap(); let word = chunk.get(0).unwrap();
self.word_item_ui(ui, word_number, word, edit_words, cb); self.word_item_ui(ui, word_number, word, edit, cb);
}); });
}); });
} }
@ -227,20 +202,24 @@ impl MnemonicSetup {
ui.add_space(6.0); ui.add_space(6.0);
} }
/// Draw word list item for current mode. /// Draw word grid item.
fn word_item_ui(&mut self, fn word_item_ui(&mut self,
ui: &mut egui::Ui, ui: &mut egui::Ui,
num: usize, num: usize,
word: &String, word: &PhraseWord,
edit: bool, edit: bool,
cb: &dyn PlatformCallbacks) { cb: &dyn PlatformCallbacks) {
let color = if !word.valid || (word.text.is_empty() && !self.mnemonic.valid()) {
Colors::red()
} else {
Colors::white_or_black(true)
};
if edit { if edit {
ui.add_space(6.0); ui.add_space(6.0);
View::button(ui, PENCIL.to_string(), Colors::button(), || { View::button(ui, PENCIL.to_string(), Colors::white_or_black(false), || {
// Setup modal values. self.word_index_edit = num - 1;
self.word_num_edit = num; self.word_edit = word.text.clone();
self.word_edit = word.clone(); self.valid_word_edit = word.valid;
self.valid_word_edit = true;
// Show word edit modal. // Show word edit modal.
Modal::new(WORD_INPUT_MODAL) Modal::new(WORD_INPUT_MODAL)
.position(ModalPosition::CenterTop) .position(ModalPosition::CenterTop)
@ -248,17 +227,17 @@ impl MnemonicSetup {
.show(); .show();
cb.show_keyboard(); cb.show_keyboard();
}); });
ui.label(RichText::new(format!("#{} {}", num, word)) ui.label(RichText::new(format!("#{} {}", num, word.text))
.size(17.0) .size(17.0)
.color(Colors::white_or_black(true))); .color(color));
} else { } else {
ui.add_space(12.0); ui.add_space(12.0);
let text = format!("#{} {}", num, word); let text = format!("#{} {}", num, word.text);
ui.label(RichText::new(text).size(17.0).color(Colors::white_or_black(true))); ui.label(RichText::new(text).size(17.0).color(color));
} }
} }
/// Reset mnemonic phrase to default values. /// Reset mnemonic phrase state to default values.
pub fn reset(&mut self) { pub fn reset(&mut self) {
self.mnemonic = Mnemonic::default(); self.mnemonic = Mnemonic::default();
} }
@ -267,14 +246,14 @@ impl MnemonicSetup {
fn word_modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) { fn word_modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
ui.add_space(6.0); ui.add_space(6.0);
ui.vertical_centered(|ui| { ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("wallets.enter_word", "number" => self.word_num_edit)) ui.label(RichText::new(t!("wallets.enter_word", "number" => self.word_index_edit + 1))
.size(17.0) .size(17.0)
.color(Colors::gray())); .color(Colors::gray()));
ui.add_space(8.0); ui.add_space(8.0);
// Draw word value text edit. // Draw word value text edit.
let mut text_edit_opts = TextEditOptions::new( let mut text_edit_opts = TextEditOptions::new(
Id::from(modal.id).with(self.word_num_edit) Id::from(modal.id).with(self.word_index_edit)
); );
View::text_edit(ui, cb, &mut self.word_edit, &mut text_edit_opts); View::text_edit(ui, cb, &mut self.word_edit, &mut text_edit_opts);
@ -304,38 +283,22 @@ impl MnemonicSetup {
columns[1].vertical_centered_justified(|ui| { columns[1].vertical_centered_justified(|ui| {
// Callback to save the word. // Callback to save the word.
let mut save = || { let mut save = || {
self.word_edit = self.word_edit.trim().to_string(); // Insert word checking validity.
let word = &self.word_edit.trim().to_string();
// Check if word is valid. self.valid_word_edit = self.mnemonic.insert(self.word_index_edit, word);
let word_index = self.word_num_edit - 1; if !self.valid_word_edit {
if !self.mnemonic.is_valid_word(&self.word_edit, word_index) {
self.valid_word_edit = false;
return; return;
} }
self.valid_word_edit = true;
// Select list where to save word.
let words = match self.mnemonic.mode {
PhraseMode::Generate => &mut self.mnemonic.confirm_words,
PhraseMode::Import => &mut self.mnemonic.words
};
// Save word at list.
words.remove(word_index);
words.insert(word_index, self.word_edit.clone());
// Close modal or go to next word to edit. // Close modal or go to next word to edit.
let close_modal = words.len() == self.word_num_edit let next_word = self.mnemonic.get(self.word_index_edit + 1);
|| !words.get(self.word_num_edit).unwrap().is_empty(); let close_modal = next_word.is_none() ||
(!next_word.as_ref().unwrap().text.is_empty() &&
next_word.unwrap().valid);
if close_modal { if close_modal {
// Check if entered phrase was valid when all words were entered.
if !self.mnemonic.words.contains(&String::from("")) {
self.valid_phrase = self.mnemonic.is_valid_phrase();
}
cb.hide_keyboard(); cb.hide_keyboard();
modal.close(); modal.close();
} else { } else {
self.word_num_edit += 1; self.word_index_edit += 1;
self.word_edit = String::from(""); self.word_edit = String::from("");
} }
}; };
@ -350,85 +313,6 @@ impl MnemonicSetup {
ui.add_space(6.0); ui.add_space(6.0);
}); });
} }
/// Show QR code recovery phrase scanner [`Modal`].
pub fn show_qr_scan_modal(&mut self, cb: &dyn PlatformCallbacks) {
self.scan_phrase_not_found = None;
// Show QR code scan modal.
Modal::new(QR_CODE_PHRASE_SCAN_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("scan_qr"))
.closeable(false)
.show();
cb.start_camera();
}
/// Draw QR code scan [`Modal`] content.
fn scan_qr_modal_ui(&mut self,
ui: &mut egui::Ui,
modal: &Modal,
cb: &dyn PlatformCallbacks) {
// Show scan result if exists or show camera content while scanning.
if let Some(_) = &self.scan_phrase_not_found {
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("wallets.rec_phrase_not_found"))
.size(17.0)
.color(Colors::red()));
});
ui.add_space(6.0);
} else if let Some(result) = self.camera_content.qr_scan_result() {
cb.stop_camera();
self.camera_content.clear_state();
match &result {
QrScanResult::Text(text) => {
self.mnemonic.import_text(text, false);
if self.mnemonic.is_valid_phrase() {
modal.close();
return;
}
}
_ => {}
}
// Set an error when found phrase was not valid.
self.scan_phrase_not_found = Some(true);
Modal::set_title(t!("scan_result"));
} else {
ui.add_space(6.0);
self.camera_content.ui(ui, cb);
ui.add_space(6.0);
}
if self.scan_phrase_not_found.is_some() {
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(ui, t!("close"), Colors::white_or_black(false), || {
self.scan_phrase_not_found = None;
modal.close();
});
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("repeat"), Colors::white_or_black(false), || {
Modal::set_title(t!("scan_qr"));
self.scan_phrase_not_found = None;
cb.start_camera();
});
});
});
} else {
ui.vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
cb.stop_camera();
modal.close();
});
});
}
ui.add_space(6.0);
}
} }
/// Calculate word list columns count based on available ui width. /// Calculate word list columns count based on available ui width.

View file

@ -0,0 +1,116 @@
// Copyright 2024 The Grim Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use egui::{Id, RichText};
use grin_util::ZeroingString;
use crate::gui::Colors;
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Modal, View};
use crate::gui::views::types::TextEditOptions;
/// Initial wallet creation [`Modal`] content.
pub struct AddWalletModal {
/// Flag to check if it's first draw to focus on first field.
first_draw: bool,
/// Wallet name.
pub name_edit: String,
/// Password to encrypt created wallet.
pub pass_edit: String,
}
impl Default for AddWalletModal {
fn default() -> Self {
Self {
first_draw: true,
name_edit: t!("wallets.default_wallet"),
pass_edit: "".to_string(),
}
}
}
impl AddWalletModal {
/// Draw creating wallet name/password input [`Modal`] content.
pub fn ui(&mut self,
ui: &mut egui::Ui,
modal: &Modal,
cb: &dyn PlatformCallbacks,
mut on_input: impl FnMut(String, ZeroingString)) {
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("wallets.name"))
.size(17.0)
.color(Colors::gray()));
ui.add_space(8.0);
// Show wallet name text edit.
let mut name_edit_opts = TextEditOptions::new(Id::from(modal.id).with("name"))
.no_focus();
if self.first_draw {
self.first_draw = false;
name_edit_opts.focus = true;
}
View::text_edit(ui, cb, &mut self.name_edit, &mut name_edit_opts);
ui.add_space(8.0);
ui.label(RichText::new(t!("wallets.pass"))
.size(17.0)
.color(Colors::gray()));
ui.add_space(8.0);
// Draw wallet password text edit.
let mut pass_text_edit_opts = TextEditOptions::new(Id::from(modal.id).with("pass"))
.password()
.no_focus();
View::text_edit(ui, cb, &mut self.pass_edit, &mut pass_text_edit_opts);
ui.add_space(12.0);
});
// Show modal buttons.
ui.scope(|ui| {
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
// Close modal.
cb.hide_keyboard();
modal.close();
});
});
columns[1].vertical_centered_justified(|ui| {
let mut on_next = || {
let name = self.name_edit.clone();
let pass = self.pass_edit.clone();
if name.is_empty() || pass.is_empty() {
return;
}
cb.hide_keyboard();
modal.close();
on_input(name, ZeroingString::from(pass));
};
// Go to next creation step on Enter button press.
View::on_enter_key(ui, || {
(on_next)();
});
View::button(ui, t!("continue"), Colors::white_or_black(false), on_next);
});
});
ui.add_space(6.0);
});
}
}

View file

@ -21,28 +21,24 @@ use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Modal, View}; use crate::gui::views::{Modal, View};
use crate::gui::views::network::ConnectionsContent; use crate::gui::views::network::ConnectionsContent;
use crate::gui::views::network::modals::ExternalConnectionModal; use crate::gui::views::network::modals::ExternalConnectionModal;
use crate::wallet::{ConnectionsConfig, ExternalConnection}; use crate::wallet::ConnectionsConfig;
use crate::wallet::types::ConnectionMethod;
/// Wallet connection [`Modal`] content. /// Wallet connection selection [`Modal`] content.
pub struct WalletConnectionModal { pub struct WalletConnectionModal {
/// Current external connection. /// Current connection method.
pub ext_conn: Option<ExternalConnection>, pub conn: ConnectionMethod,
/// Flag to show connection creation. /// External connection content.
show_conn_creation: bool, ext_conn_content: Option<ExternalConnectionModal>
/// External connection creation content.
add_ext_conn_content: ExternalConnectionModal
} }
impl WalletConnectionModal { impl WalletConnectionModal {
/// Create from provided wallet connection. /// Create from provided wallet connection.
pub fn new(ext_conn: Option<ExternalConnection>) -> Self { pub fn new(conn: ConnectionMethod) -> Self {
ExternalConnection::check_ext_conn_availability(None);
Self { Self {
ext_conn, conn,
show_conn_creation: false, ext_conn_content: None,
add_ext_conn_content: ExternalConnectionModal::new(None),
} }
} }
@ -51,17 +47,17 @@ impl WalletConnectionModal {
ui: &mut egui::Ui, ui: &mut egui::Ui,
modal: &Modal, modal: &Modal,
cb: &dyn PlatformCallbacks, cb: &dyn PlatformCallbacks,
on_select: impl Fn(Option<i64>)) { on_select: impl Fn(ConnectionMethod)) {
ui.add_space(4.0); // Draw external connection content.
if let Some(ext_content) = self.ext_conn_content.as_mut() {
// Draw external connection creation content. ext_content.ui(ui, cb, modal, |conn| {
if self.show_conn_creation { on_select(ConnectionMethod::External(conn.id, conn.url));
self.add_ext_conn_content.ui(ui, cb, modal, |conn| {
on_select(Some(conn.id));
}); });
return; return;
} }
ui.add_space(4.0);
let ext_conn_list = ConnectionsConfig::ext_conn_list(); let ext_conn_list = ConnectionsConfig::ext_conn_list();
ScrollArea::vertical() ScrollArea::vertical()
.max_height(if ext_conn_list.len() < 4 { .max_height(if ext_conn_list.len() < 4 {
@ -69,7 +65,7 @@ impl WalletConnectionModal {
} else { } else {
350.0 350.0
}) })
.id_source("integrated_node") .id_salt("connections_scroll")
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden) .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.auto_shrink([true; 2]) .auto_shrink([true; 2])
.show(ui, |ui| { .show(ui, |ui| {
@ -77,52 +73,54 @@ impl WalletConnectionModal {
// Show integrated node selection. // Show integrated node selection.
ConnectionsContent::integrated_node_item_ui(ui, |ui| { ConnectionsContent::integrated_node_item_ui(ui, |ui| {
let is_current_method = self.ext_conn.is_none(); match self.conn {
if !is_current_method { ConnectionMethod::Integrated => {
View::item_button(ui, View::item_rounding(0, 1, true), CHECK, None, || {
self.ext_conn = None;
on_select(None);
modal.close();
});
} else {
ui.add_space(14.0); ui.add_space(14.0);
ui.label(RichText::new(CHECK_FAT).size(20.0).color(Colors::green())); ui.label(RichText::new(CHECK_FAT).size(20.0).color(Colors::green()));
ui.add_space(14.0); ui.add_space(14.0);
} }
_ => {
View::item_button(ui, View::item_rounding(0, 1, true), CHECK, None, || {
on_select(ConnectionMethod::Integrated);
modal.close();
});
}
}
}); });
// Show button to add new external node connection.
ui.add_space(8.0); ui.add_space(8.0);
ui.vertical_centered(|ui| { ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("wallets.ext_conn")) ui.label(RichText::new(t!("wallets.ext_conn"))
.size(16.0) .size(16.0)
.color(Colors::gray())); .color(Colors::gray()));
ui.add_space(6.0); ui.add_space(6.0);
// Show button to add new external node connection.
let add_node_text = format!("{} {}", PLUS_CIRCLE, t!("wallets.add_node")); let add_node_text = format!("{} {}", PLUS_CIRCLE, t!("wallets.add_node"));
View::button(ui, add_node_text, Colors::button(), || { View::button(ui, add_node_text, Colors::white_or_black(false), || {
self.show_conn_creation = true; self.ext_conn_content = Some(ExternalConnectionModal::new(None));
}); });
}); });
ui.add_space(4.0); ui.add_space(4.0);
if !ext_conn_list.is_empty() { if !ext_conn_list.is_empty() {
ui.add_space(8.0); ui.add_space(8.0);
for (index, conn) in ext_conn_list.iter().enumerate() { for (index, conn) in ext_conn_list.iter().filter(|c| !c.deleted).enumerate() {
if conn.deleted {
continue;
}
ui.horizontal_wrapped(|ui| { ui.horizontal_wrapped(|ui| {
// Draw external connection item.
let len = ext_conn_list.len(); let len = ext_conn_list.len();
ConnectionsContent::ext_conn_item_ui(ui, conn, index, len, |ui| { ConnectionsContent::ext_conn_item_ui(ui, conn, index, len, |ui| {
// Draw button to select connection. let current_ext_conn = match self.conn {
let is_current_method = if let Some(c) = self.ext_conn.as_ref() { ConnectionMethod::Integrated => false,
c.id == conn.id ConnectionMethod::External(id, _) => id == conn.id
} else {
false
}; };
if !is_current_method { if !current_ext_conn {
let button_rounding = View::item_rounding(index, len, true); let button_rounding = View::item_rounding(index, len, true);
View::item_button(ui, button_rounding, CHECK, None, || { View::item_button(ui, button_rounding, CHECK, None, || {
self.ext_conn = Some(conn.clone()); on_select(
on_select(Some(conn.id)); ConnectionMethod::External(conn.id, conn.url.clone())
);
modal.close(); modal.close();
}); });
} else { } else {
@ -139,7 +137,7 @@ impl WalletConnectionModal {
}); });
ui.add_space(2.0); ui.add_space(2.0);
View::horizontal_line(ui, Colors::stroke()); View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(6.0); ui.add_space(6.0);
// Show button to close modal. // Show button to close modal.

View file

@ -17,3 +17,9 @@ pub use conn::*;
mod wallets; mod wallets;
pub use wallets::*; pub use wallets::*;
mod open;
pub use open::*;
mod add;
pub use add::*;

View file

@ -0,0 +1,123 @@
// Copyright 2024 The Grim Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use egui::{Id, RichText};
use grin_util::ZeroingString;
use crate::gui::Colors;
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Modal, View};
use crate::gui::views::types::TextEditOptions;
use crate::wallet::Wallet;
/// Wallet opening [`Modal`] content.
pub struct OpenWalletModal {
/// Wallet to open.
wallet: Wallet,
/// Password to open wallet.
pass_edit: String,
/// Flag to check if wrong password was entered.
wrong_pass: bool,
/// Optional data to pass after wallet opening.
data: Option<String>,
}
impl OpenWalletModal {
/// Create new content instance.
pub fn new(wallet: Wallet, data: Option<String>) -> Self {
Self {
wallet,
pass_edit: "".to_string(),
wrong_pass: false,
data,
}
}
/// Draw [`Modal`] content.
pub fn ui(&mut self,
ui: &mut egui::Ui,
modal: &Modal,
cb: &dyn PlatformCallbacks,
mut on_continue: impl FnMut(Wallet, Option<String>)) {
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("wallets.pass"))
.size(17.0)
.color(Colors::gray()));
ui.add_space(8.0);
// Show password input.
let mut pass_edit_opts = TextEditOptions::new(Id::from(modal.id)).password();
View::text_edit(ui, cb, &mut self.pass_edit, &mut pass_edit_opts);
// Show information when password is empty.
if self.pass_edit.is_empty() {
self.wrong_pass = false;
ui.add_space(10.0);
ui.label(RichText::new(t!("wallets.pass_empty"))
.size(17.0)
.color(Colors::inactive_text()));
} else if self.wrong_pass {
ui.add_space(10.0);
ui.label(RichText::new(t!("wallets.wrong_pass"))
.size(17.0)
.color(Colors::red()));
}
ui.add_space(12.0);
});
// Show modal buttons.
ui.scope(|ui| {
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
// Close modal.
cb.hide_keyboard();
modal.close();
});
});
columns[1].vertical_centered_justified(|ui| {
// Callback for button to continue.
let mut on_continue = || {
let pass = self.pass_edit.clone();
if pass.is_empty() {
return;
}
match self.wallet.open(ZeroingString::from(pass)) {
Ok(_) => {
self.pass_edit = "".to_string();
cb.hide_keyboard();
modal.close();
on_continue(self.wallet.clone(), self.data.clone());
}
Err(_) => self.wrong_pass = true
}
};
// Continue on Enter key press.
View::on_enter_key(ui, || {
(on_continue)();
});
View::button(ui, t!("continue"), Colors::white_or_black(false), on_continue);
});
});
ui.add_space(6.0);
});
}
}

View file

@ -16,42 +16,66 @@ use egui::scroll_area::ScrollBarVisibility;
use egui::{Align, Layout, RichText, ScrollArea}; use egui::{Align, Layout, RichText, ScrollArea};
use crate::gui::Colors; use crate::gui::Colors;
use crate::gui::icons::{CHECK, CHECK_FAT, COMPUTER_TOWER, GLOBE_SIMPLE, PLUGS_CONNECTED}; use crate::gui::icons::{CHECK, CHECK_FAT, COMPUTER_TOWER, FOLDER_OPEN, GLOBE_SIMPLE, PLUGS_CONNECTED};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Modal, View}; use crate::gui::views::{Modal, View};
use crate::gui::views::types::ModalPosition;
use crate::gui::views::wallets::modals::OpenWalletModal;
use crate::gui::views::wallets::wallet::types::wallet_status_text;
use crate::wallet::{Wallet, WalletList}; use crate::wallet::{Wallet, WalletList};
use crate::wallet::types::ConnectionMethod;
/// Wallet list [`Modal`] content /// Wallet list [`Modal`] content
pub struct WalletsModal { pub struct WalletsModal {
/// Selected wallet id. /// Selected wallet id.
selected: Option<i64> selected_id: Option<i64>,
/// Optional data to pass after wallet selection.
data: Option<String>,
/// Flag to check if wallet can be opened from the list.
can_open: bool,
/// Wallet opening content.
open_wallet_content: Option<OpenWalletModal>,
} }
impl WalletsModal { impl WalletsModal {
pub fn new(selected: Option<i64>) -> Self { /// Create new content instance.
Self { pub fn new(selected_id: Option<i64>, data: Option<String>, can_open: bool) -> Self {
selected, Self { selected_id, data, can_open, open_wallet_content: None }
}
} }
/// Draw [`Modal`] content. /// Draw content.
pub fn ui(&mut self, pub fn ui(&mut self,
ui: &mut egui::Ui, ui: &mut egui::Ui,
modal: &Modal, modal: &Modal,
wallets: &WalletList, wallets: &WalletList,
mut on_select: impl FnMut(i64)) { cb: &dyn PlatformCallbacks,
mut on_select: impl FnMut(Wallet, Option<String>)) {
// Draw wallet opening modal content.
if let Some(open_content) = self.open_wallet_content.as_mut() {
open_content.ui(ui, modal, cb, |wallet, data| {
on_select(wallet, data);
self.data = None;
});
return;
}
ui.add_space(4.0); ui.add_space(4.0);
ScrollArea::vertical() ScrollArea::vertical()
.max_height(373.0) .max_height(373.0)
.id_source("select_wallet_list") .id_salt("select_wallet_list_scroll")
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden) .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.auto_shrink([true; 2]) .auto_shrink([true; 2])
.show(ui, |ui| { .show(ui, |ui| {
ui.add_space(2.0); ui.add_space(2.0);
ui.vertical_centered(|ui| { ui.vertical_centered(|ui| {
let data = self.data.clone();
for wallet in wallets.list() { for wallet in wallets.list() {
// Draw wallet list item. // Draw wallet list item.
self.wallet_item_ui(ui, wallet, modal, |id| { self.wallet_item_ui(ui, wallet, || {
on_select(id); modal.close();
on_select(wallet.clone(), data.clone());
}); });
ui.add_space(5.0); ui.add_space(5.0);
} }
@ -59,24 +83,24 @@ impl WalletsModal {
}); });
ui.add_space(2.0); ui.add_space(2.0);
View::horizontal_line(ui, Colors::stroke()); View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(6.0); ui.add_space(6.0);
// Show button to close modal. // Show button to close modal.
ui.vertical_centered_justified(|ui| { ui.vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || { View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
self.data = None;
modal.close(); modal.close();
}); });
}); });
ui.add_space(6.0); ui.add_space(6.0);
} }
/// Draw wallet list item. /// Draw wallet list item with provided callback on select.
fn wallet_item_ui(&mut self, fn wallet_item_ui(&mut self,
ui: &mut egui::Ui, ui: &mut egui::Ui,
wallet: &Wallet, wallet: &Wallet,
modal: &Modal, on_select: impl FnOnce()) {
mut on_select: impl FnMut(i64)) {
let config = wallet.get_config(); let config = wallet.get_config();
let id = config.id; let id = config.id;
@ -87,39 +111,64 @@ impl WalletsModal {
ui.painter().rect(rect, rounding, Colors::fill(), View::hover_stroke()); ui.painter().rect(rect, rounding, Colors::fill(), View::hover_stroke());
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| { ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
if self.can_open {
// Show button to select or open closed wallet.
let icon = if wallet.is_open() {
CHECK
} else {
FOLDER_OPEN
};
View::item_button(ui, View::item_rounding(0, 1, true), icon, None, || {
if wallet.is_open() {
on_select();
} else {
Modal::change_position(ModalPosition::CenterTop);
self.open_wallet_content = Some(
OpenWalletModal::new(wallet.clone(), self.data.clone())
);
}
});
} else {
// Draw button to select wallet. // Draw button to select wallet.
let current = self.selected.unwrap_or(0) == id; let current = self.selected_id.unwrap_or(0) == id;
if current { if current {
ui.add_space(12.0); ui.add_space(12.0);
ui.label(RichText::new(CHECK_FAT).size(20.0).color(Colors::green())); ui.label(RichText::new(CHECK_FAT).size(20.0).color(Colors::green()));
} else { } else {
View::item_button(ui, View::item_rounding(0, 1, true), CHECK, None, || { View::item_button(ui, View::item_rounding(0, 1, true), CHECK, None, || {
on_select(id); on_select();
modal.close();
}); });
} }
}
let layout_size = ui.available_size(); let layout_size = ui.available_size();
ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| { ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| {
ui.add_space(6.0); ui.add_space(6.0);
ui.vertical(|ui| { ui.vertical(|ui| {
ui.add_space(3.0); ui.add_space(3.0);
// Setup wallet name text. // Show wallet name text.
ui.with_layout(Layout::left_to_right(Align::Min), |ui| { ui.with_layout(Layout::left_to_right(Align::Min), |ui| {
ui.add_space(1.0); ui.add_space(1.0);
View::ellipsize_text(ui, config.name, 18.0, Colors::title(false)); View::ellipsize_text(ui, config.name, 18.0, Colors::title(false));
}); });
// Setup wallet connection text. // Show wallet connection text.
let conn = if let Some(conn) = wallet.get_current_ext_conn() { let connection = wallet.get_current_connection();
format!("{} {}", GLOBE_SIMPLE, conn.url) let conn_text = match connection {
} else { ConnectionMethod::Integrated => {
format!("{} {}", COMPUTER_TOWER, t!("network.node")) format!("{} {}", COMPUTER_TOWER, t!("network.node"))
}
ConnectionMethod::External(_, url) => format!("{} {}", GLOBE_SIMPLE, url)
}; };
View::ellipsize_text(ui, conn, 15.0, Colors::text(false)); ui.label(RichText::new(conn_text).size(15.0).color(Colors::text(false)));
ui.add_space(1.0); ui.add_space(1.0);
// Setup wallet API text. // Show wallet API text or open status.
if self.can_open {
ui.label(RichText::new(wallet_status_text(wallet))
.size(15.0)
.color(Colors::gray()));
} else {
let address = if let Some(port) = config.api_port { let address = if let Some(port) = config.api_port {
format!("127.0.0.1:{}", port) format!("127.0.0.1:{}", port)
} else { } else {
@ -127,6 +176,7 @@ impl WalletsModal {
}; };
let api_text = format!("{} {}", PLUGS_CONNECTED, address); let api_text = format!("{} {}", PLUGS_CONNECTED, address);
ui.label(RichText::new(api_text).size(15.0).color(Colors::gray())); ui.label(RichText::new(api_text).size(15.0).color(Colors::gray()));
}
ui.add_space(3.0); ui.add_space(3.0);
}); });
}); });

View file

@ -20,226 +20,271 @@ use grin_core::core::amount_to_hr_string;
use crate::AppConfig; use crate::AppConfig;
use crate::gui::Colors; use crate::gui::Colors;
use crate::gui::icons::{ARROWS_CLOCKWISE, BRIDGE, CHAT_CIRCLE_TEXT, CHECK, CHECK_FAT, COPY, FOLDER_USER, GEAR_FINE, GRAPH, PACKAGE, PATH, POWER, SCAN, SPINNER, USERS_THREE}; use crate::gui::icons::{ARROWS_CLOCKWISE, BRIDGE, CAMERA_ROTATE, CHAT_CIRCLE_TEXT, FOLDER_USER, GEAR_FINE, GRAPH, PACKAGE, POWER, SCAN, SPINNER, USERS_THREE};
use crate::gui::platform::PlatformCallbacks; use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{CameraContent, Modal, Content, View}; use crate::gui::views::{Modal, Content, View, CameraContent};
use crate::gui::views::types::{ModalPosition, QrScanResult, TextEditOptions}; use crate::gui::views::types::{LinePosition, ModalContainer, ModalPosition};
use crate::gui::views::wallets::{WalletTransactions, WalletMessages, WalletTransport}; use crate::gui::views::wallets::{WalletTransactions, WalletMessages, WalletTransport};
use crate::gui::views::wallets::types::{GRIN, WalletTab, WalletTabType}; use crate::gui::views::wallets::types::{GRIN, WalletTab, WalletTabType};
use crate::gui::views::wallets::settings::WalletSettings; use crate::gui::views::wallets::wallet::modals::WalletAccountsModal;
use crate::gui::views::wallets::wallet::WalletSettings;
use crate::node::Node; use crate::node::Node;
use crate::wallet::{Wallet, WalletConfig}; use crate::wallet::{ExternalConnection, Wallet, WalletConfig};
use crate::wallet::types::{WalletAccount, WalletData}; use crate::wallet::types::{ConnectionMethod, WalletData};
/// Selected and opened wallet content. /// Wallet content.
pub struct WalletContent { pub struct WalletContent {
/// List of wallet accounts for [`Modal`]. /// Selected and opened wallet.
accounts: Vec<WalletAccount>, pub wallet: Wallet,
/// Flag to check if account is creating.
account_creating: bool,
/// Account label [`Modal`] value.
account_label_edit: String,
/// Flag to check if error occurred during account creation at [`Modal`].
account_creation_error: bool,
/// Camera content for QR scan [`Modal`].
camera_content: CameraContent,
/// QR code scan result
qr_scan_result: Option<QrScanResult>,
/// Current tab content to show. /// Current tab content to show.
pub current_tab: Box<dyn WalletTab> pub current_tab: Box<dyn WalletTab>,
}
impl Default for WalletContent { /// Wallet accounts [`Modal`] content.
fn default() -> Self { accounts_modal_content: Option<WalletAccountsModal>,
Self {
accounts: vec![], /// QR code scan content.
account_creating: false, pub qr_scan_content: Option<CameraContent>,
account_label_edit: "".to_string(),
account_creation_error: false, /// List of allowed [`Modal`] ids for this [`ModalContainer`].
camera_content: CameraContent::default(), allowed_modal_ids: Vec<&'static str>
qr_scan_result: None,
current_tab: Box::new(WalletTransactions::default())
}
}
} }
/// Identifier for account list [`Modal`]. /// Identifier for account list [`Modal`].
const ACCOUNT_LIST_MODAL: &'static str = "account_list_modal"; const ACCOUNT_LIST_MODAL: &'static str = "account_list_modal";
/// Identifier for QR code scan [`Modal`]. impl ModalContainer for WalletContent {
const QR_CODE_SCAN_MODAL: &'static str = "qr_code_scan_modal"; fn modal_ids(&self) -> &Vec<&'static str> {
&self.allowed_modal_ids
}
fn modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
match modal.id {
ACCOUNT_LIST_MODAL => {
if let Some(content) = self.accounts_modal_content.as_mut() {
Modal::ui(ui.ctx(), |ui, modal| {
content.ui(ui, &self.wallet, modal, cb);
});
}
}
_ => {}
}
}
}
impl WalletContent { impl WalletContent {
/// Draw wallet content. /// Create new instance with optional data.
pub fn ui(&mut self, pub fn new(wallet: Wallet, data: Option<String>) -> Self {
ui: &mut egui::Ui, let mut content = Self {
wallet: &mut Wallet, wallet,
cb: &dyn PlatformCallbacks) { accounts_modal_content: None,
// Show modal content for this ui container. qr_scan_content: None,
self.modal_content_ui(ui, wallet, cb); current_tab: Box::new(WalletTransactions::default()),
allowed_modal_ids: vec![
let dual_panel = Content::is_dual_panel_mode(ui); ACCOUNT_LIST_MODAL,
],
let data = wallet.get_data(); };
let data_empty = data.is_none(); if data.is_some() {
content.on_data(data);
// Show wallet balance panel not on Settings tab with selected non-repairing
// wallet, when there is no error and data is not empty.
let mut show_balance = self.current_tab.get_type() != WalletTabType::Settings && !data_empty
&& !wallet.sync_error() && !wallet.is_repairing() && !wallet.is_closing();
if wallet.get_current_ext_conn().is_none() && !Node::is_running() {
show_balance = false;
} }
egui::TopBottomPanel::top(Id::from("wallet_balance").with(wallet.identifier())) content
}
/// Handle data from deeplink or opened file.
pub fn on_data(&mut self, data: Option<String>) {
self.current_tab = Box::new(WalletMessages::new(data));
}
/// Draw wallet content.
pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
ui.ctx().request_repaint_after(Duration::from_millis(1000));
self.current_modal_ui(ui, cb);
let dual_panel = Content::is_dual_panel_mode(ui.ctx());
let show_wallets_dual = AppConfig::show_wallets_at_dual_panel();
let wallet = &self.wallet;
let wallet_id = wallet.identifier();
let data = wallet.get_data();
let show_qr_scan = self.qr_scan_content.is_some();
let hide_tabs = Self::block_navigation_on_sync(wallet);
// Show wallet account panel not on settings tab when navigation is not blocked and QR code
// scanner is not showing and wallet data is not empty.
let mut show_account = self.current_tab.get_type() != WalletTabType::Settings && !hide_tabs
&& !wallet.sync_error() && data.is_some();
if wallet.get_current_connection() == ConnectionMethod::Integrated && !Node::is_running() {
show_account = false;
}
// Close scanner when balance got hidden.
if !show_account && show_qr_scan {
cb.stop_camera();
self.qr_scan_content = None;
}
egui::TopBottomPanel::top(Id::from("wallet_account").with(wallet.identifier()))
.frame(egui::Frame { .frame(egui::Frame {
fill: Colors::fill(),
stroke: View::item_stroke(),
inner_margin: Margin { inner_margin: Margin {
left: View::far_left_inset_margin(ui) + 4.0, left: View::far_left_inset_margin(ui) + 4.0,
right: View::get_right_inset() + 4.0, right: View::get_right_inset() + 4.0,
top: 4.0, top: 4.0,
bottom: 0.0, bottom: 0.0,
}, },
outer_margin: Margin { fill: Colors::fill(),
left: if dual_panel {
-0.5
} else {
0.0
},
right: 0.0,
top: 0.0,
bottom: if dual_panel {
-1.0
} else {
-0.5
},
},
..Default::default() ..Default::default()
}) })
.show_animated_inside(ui, show_balance, |ui| { .show_animated_inside(ui, show_account, |ui| {
ui.vertical_centered(|ui| { let rect = ui.available_rect_before_wrap();
if !dual_panel { if show_qr_scan {
ui.add_space(1.0); View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH, |ui| {
} self.qr_scan_content.as_mut().unwrap().ui(ui, cb);
// Draw account info. ui.add_space(6.0);
ui.vertical_centered_justified(|ui| {
View::button(ui, t!("close"), Colors::white_or_black(false), || {
cb.stop_camera();
self.qr_scan_content = None;
});
});
ui.add_space(6.0);
});
} else {
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| { View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
self.account_ui(ui, wallet, data.unwrap(), cb); self.account_ui(ui, data.unwrap(), cb);
});
}); });
}
// Draw content divider lines.
let r = {
let mut r = rect.clone();
r.min.x -= 4.0 + View::far_left_inset_margin(ui);
r.min.y -= 4.0;
r.max.x += 4.0 + View::get_right_inset();
r
};
View::line(ui, LinePosition::BOTTOM, &r, Colors::item_stroke());
if dual_panel && show_wallets_dual && !show_qr_scan {
View::line(ui, LinePosition::LEFT, &r, Colors::item_stroke());
}
}); });
// Show wallet tabs panel. // Show wallet tabs.
let show_tabs = !Self::block_navigation_on_sync(wallet); let show_tabs = !hide_tabs && self.qr_scan_content.is_none();
egui::TopBottomPanel::bottom("wallet_tabs_content") egui::TopBottomPanel::bottom("wallet_tabs")
.frame(egui::Frame { .frame(egui::Frame {
fill: Colors::fill(),
inner_margin: Margin { inner_margin: Margin {
left: View::far_left_inset_margin(ui) + View::TAB_ITEMS_PADDING, left: View::far_left_inset_margin(ui) + View::TAB_ITEMS_PADDING,
right: View::get_right_inset() + View::TAB_ITEMS_PADDING, right: View::get_right_inset() + View::TAB_ITEMS_PADDING,
top: View::TAB_ITEMS_PADDING, top: View::TAB_ITEMS_PADDING,
bottom: View::get_bottom_inset() + View::TAB_ITEMS_PADDING, bottom: View::get_bottom_inset() + View::TAB_ITEMS_PADDING,
}, },
fill: Colors::fill(),
..Default::default() ..Default::default()
}) })
.show_animated_inside(ui, show_tabs, |ui| { .show_animated_inside(ui, show_tabs, |ui| {
ui.vertical_centered(|ui| { let rect = ui.available_rect_before_wrap();
// Draw wallet tabs.
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| { View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
self.tabs_ui(ui, wallet); self.tabs_ui(ui, cb);
});
}); });
let rect = {
let mut r = rect.clone();
r.min.x -= View::far_left_inset_margin(ui) + View::TAB_ITEMS_PADDING;
r.min.y -= View::TAB_ITEMS_PADDING;
r.max.x += View::get_right_inset() + View::TAB_ITEMS_PADDING;
r.max.y += View::get_bottom_inset() + View::TAB_ITEMS_PADDING;
r
};
// Draw content divider line.
View::line(ui, LinePosition::TOP, &rect, Colors::stroke());
}); });
// Show tab content panel. // Show tab content.
egui::CentralPanel::default() egui::CentralPanel::default()
.frame(egui::Frame { .frame(egui::Frame {
outer_margin: Margin { inner_margin: Margin {
left: if dual_panel { left: View::far_left_inset_margin(ui) + 4.0,
-0.5 right: View::get_right_inset() + 4.0,
} else {
0.0
},
right: 0.0,
top: 0.0, top: 0.0,
bottom: 0.0, bottom: 4.0,
}, },
stroke: View::item_stroke(),
fill: Colors::white_or_black(false),
..Default::default() ..Default::default()
}) })
.show_inside(ui, |ui| { .show_inside(ui, |ui| {
self.current_tab.ui(ui, wallet, cb); let rect = ui.available_rect_before_wrap();
let tab_type = self.current_tab.get_type();
let show_sync = (tab_type != WalletTabType::Settings || hide_tabs) &&
sync_ui(ui, &self.wallet);
if !show_sync {
if tab_type != WalletTabType::Txs {
ui.add_space(3.0);
ScrollArea::vertical()
.id_salt(Id::from("wallet_scroll")
.with(tab_type.name())
.with(wallet_id))
.auto_shrink([false; 2])
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.show(ui, |ui| {
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
self.current_tab.ui(ui, &self.wallet, cb);
});
}); });
// Refresh content after 1 second for synced wallet.
if !data_empty {
ui.ctx().request_repaint_after(Duration::from_millis(1000));
} else { } else {
ui.ctx().request_repaint(); self.current_tab.ui(ui, &self.wallet, cb);
} }
} }
let rect = {
let mut r = rect.clone();
r.min.x -= View::far_left_inset_margin(ui) + 4.0;
r.max.x += View::get_right_inset() + 4.0;
r.max.y += 4.0;
r
};
// Draw cover when QR code scanner is active.
if show_qr_scan {
View::content_cover_ui(ui, rect, "wallet_tab", || {
cb.stop_camera();
self.qr_scan_content = None;
});
}
// Draw content divider line.
if dual_panel && show_wallets_dual {
View::line(ui, LinePosition::LEFT, &rect, Colors::item_stroke());
}
});
}
/// Draw [`Modal`] content for this ui container. /// Check when to block tabs navigation on sync progress.
fn modal_content_ui(&mut self, pub fn block_navigation_on_sync(wallet: &Wallet) -> bool {
ui: &mut egui::Ui, let sync_error = wallet.sync_error();
wallet: &mut Wallet, let integrated_node = wallet.get_current_connection() == ConnectionMethod::Integrated;
cb: &dyn PlatformCallbacks) { let integrated_node_ready = Node::get_sync_status() == Some(SyncStatus::NoSync);
match Modal::opened() { let sync_after_opening = wallet.get_data().is_none() && !wallet.sync_error();
None => {} // Block navigation if wallet is repairing and integrated node is not launching
Some(id) => { // and if wallet is closing or syncing after opening when there is no data to show.
match id { (wallet.is_repairing() && (integrated_node_ready || !integrated_node) && !sync_error)
ACCOUNT_LIST_MODAL => { || wallet.is_closing() || (sync_after_opening &&
Modal::ui(ui.ctx(), |ui, modal| { (!integrated_node || integrated_node_ready))
self.account_list_modal_ui(ui, wallet, modal, cb);
});
}
QR_CODE_SCAN_MODAL => {
Modal::ui(ui.ctx(), |ui, modal| {
self.scan_qr_modal_ui(ui, wallet, modal, cb);
});
}
_ => {}
}
}
}
} }
/// Draw wallet account content. /// Draw wallet account content.
fn account_ui(&mut self, fn account_ui(&mut self,
ui: &mut egui::Ui, ui: &mut egui::Ui,
wallet: &Wallet,
data: WalletData, data: WalletData,
cb: &dyn PlatformCallbacks) { cb: &dyn PlatformCallbacks) {
let mut rect = ui.available_rect_before_wrap(); let mut rect = ui.available_rect_before_wrap();
rect.set_height(75.0); rect.set_height(75.0);
// Draw round background. // Draw round background.
let rounding = View::item_rounding(0, 2, false); let rounding = View::item_rounding(0, 2, false);
ui.painter().rect(rect, rounding, Colors::button(), View::hover_stroke()); ui.painter().rect(rect, rounding, Colors::fill_lite(), View::item_stroke());
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| { ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
// Draw button to scan QR code. // Draw button to show QR code scanner.
View::item_button(ui, View::item_rounding(0, 2, true), SCAN, None, || { View::item_button(ui, View::item_rounding(0, 2, true), SCAN, None, || {
self.qr_scan_result = None; self.qr_scan_content = Some(CameraContent::default());
self.camera_content.clear_state();
// Show QR code scan modal.
Modal::new(QR_CODE_SCAN_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("scan_qr"))
.closeable(false)
.show();
cb.start_camera(); cb.start_camera();
}); });
// Draw button to show list of accounts. // Draw button to show list of accounts.
View::item_button(ui, View::item_rounding(1, 3, true), USERS_THREE, None, || { View::item_button(ui, View::item_rounding(1, 3, true), USERS_THREE, None, || {
// Load accounts. self.accounts_modal_content = Some(
self.account_label_edit = "".to_string(); WalletAccountsModal::new(self.wallet.accounts())
self.accounts = wallet.accounts(); );
self.account_creating = false;
// Show account list modal.
Modal::new(ACCOUNT_LIST_MODAL) Modal::new(ACCOUNT_LIST_MODAL)
.position(ModalPosition::CenterTop) .position(ModalPosition::CenterTop)
.title(t!("wallets.accounts")) .title(t!("wallets.accounts"))
@ -263,7 +308,7 @@ impl WalletContent {
ui.add_space(-2.0); ui.add_space(-2.0);
// Show account label. // Show account label.
let account = wallet.get_config().account; let account = self.wallet.get_config().account;
let default_acc_label = WalletConfig::DEFAULT_ACCOUNT_LABEL.to_string(); let default_acc_label = WalletConfig::DEFAULT_ACCOUNT_LABEL.to_string();
let acc_label = if account == default_acc_label { let acc_label = if account == default_acc_label {
t!("wallets.default_account") t!("wallets.default_account")
@ -274,15 +319,15 @@ impl WalletContent {
View::ellipsize_text(ui, acc_text, 15.0, Colors::text(false)); View::ellipsize_text(ui, acc_text, 15.0, Colors::text(false));
// Show confirmed height or sync progress. // Show confirmed height or sync progress.
let status_text = if !wallet.syncing() { let status_text = if !self.wallet.syncing() {
format!("{} {}", PACKAGE, data.info.last_confirmed_height) format!("{} {}", PACKAGE, data.info.last_confirmed_height)
} else { } else {
let info_progress = wallet.info_sync_progress(); let info_progress = self.wallet.info_sync_progress();
if info_progress == 100 || info_progress == 0 { if info_progress == 100 || info_progress == 0 {
format!("{} {}", SPINNER, t!("wallets.wallet_loading")) format!("{} {}", SPINNER, t!("wallets.wallet_loading"))
} else { } else {
if wallet.is_repairing() { if self.wallet.is_repairing() {
let rep_progress = wallet.repairing_progress(); let rep_progress = self.wallet.repairing_progress();
if rep_progress == 0 { if rep_progress == 0 {
format!("{} {}", SPINNER, t!("wallets.wallet_checking")) format!("{} {}", SPINNER, t!("wallets.wallet_checking"))
} else { } else {
@ -299,292 +344,88 @@ impl WalletContent {
} }
} }
}; };
View::animate_text(ui, status_text, 15.0, Colors::gray(), wallet.syncing()); View::animate_text(ui,
status_text,
15.0,
Colors::gray(),
self.wallet.syncing());
}) })
}); });
}); });
} }
/// Draw account list [`Modal`] content. /// Draw tab buttons at the bottom of the screen.
fn account_list_modal_ui(&mut self, fn tabs_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
ui: &mut egui::Ui,
wallet: &mut Wallet,
modal: &Modal,
cb: &dyn PlatformCallbacks) {
if self.account_creating {
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("wallets.new_account_desc"))
.size(17.0)
.color(Colors::gray()));
ui.add_space(8.0);
// Draw account name edit.
let text_edit_id = Id::from(modal.id).with(wallet.get_config().id);
let mut text_edit_opts = TextEditOptions::new(text_edit_id);
View::text_edit(ui, cb, &mut self.account_label_edit, &mut text_edit_opts);
// Show error occurred during account creation..
if self.account_creation_error {
ui.add_space(12.0);
ui.label(RichText::new(t!("error"))
.size(17.0)
.color(Colors::red()));
}
ui.add_space(12.0);
});
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
// Show modal buttons.
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
// Close modal.
cb.hide_keyboard();
modal.close();
});
});
columns[1].vertical_centered_justified(|ui| {
// Create button callback.
let mut on_create = || {
if !self.account_label_edit.is_empty() {
let label = &self.account_label_edit;
match wallet.create_account(label) {
Ok(_) => {
let _ = wallet.set_active_account(label);
cb.hide_keyboard();
modal.close();
},
Err(_) => self.account_creation_error = true
};
}
};
View::on_enter_key(ui, || {
(on_create)();
});
View::button(ui, t!("create"), Colors::white_or_black(false), on_create);
});
});
ui.add_space(6.0);
} else {
ui.add_space(3.0);
// Show list of accounts.
let size = self.accounts.len();
ScrollArea::vertical()
.id_source("account_list_modal_scroll")
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.max_height(266.0)
.auto_shrink([true; 2])
.show_rows(ui, ACCOUNT_ITEM_HEIGHT, size, |ui, row_range| {
for index in row_range {
// Add space before the first item.
if index == 0 {
ui.add_space(4.0);
}
let acc = self.accounts.get(index).unwrap();
account_item_ui(ui, modal, wallet, acc, index, size);
if index == size - 1 {
ui.add_space(4.0);
}
}
});
ui.add_space(2.0);
View::horizontal_line(ui, Colors::stroke());
ui.add_space(6.0);
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
// Show modal buttons.
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
modal.close();
});
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("create"), Colors::white_or_black(false), || {
self.account_creating = true;
cb.show_keyboard();
});
});
});
ui.add_space(6.0);
}
}
/// Draw QR code scan [`Modal`] content.
fn scan_qr_modal_ui(&mut self,
ui: &mut egui::Ui,
wallet: &mut Wallet,
modal: &Modal,
cb: &dyn PlatformCallbacks) {
// Show scan result if exists or show camera content while scanning.
if let Some(result) = &self.qr_scan_result {
let mut result_text = result.text();
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(3.0);
ScrollArea::vertical()
.id_source(Id::from("qr_scan_result_input").with(wallet.get_config().id))
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.max_height(128.0)
.auto_shrink([false; 2])
.show(ui, |ui| {
ui.add_space(7.0);
egui::TextEdit::multiline(&mut result_text)
.font(egui::TextStyle::Small)
.desired_rows(5)
.interactive(false)
.desired_width(f32::INFINITY)
.show(ui);
ui.add_space(6.0);
});
ui.add_space(2.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(10.0);
// Show copy button.
ui.vertical_centered(|ui| {
let copy_text = format!("{} {}", COPY, t!("copy"));
View::button(ui, copy_text, Colors::button(), || {
cb.copy_string_to_buffer(result_text.to_string());
self.qr_scan_result = None;
modal.close();
});
});
ui.add_space(10.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(6.0);
} else if let Some(result) = self.camera_content.qr_scan_result() {
cb.stop_camera();
self.camera_content.clear_state();
match &result {
QrScanResult::Slatepack(message) => {
// Redirect to messages to handle parsed message.
let mut messages =
WalletMessages::new(wallet.can_use_dandelion(), Some(message.to_string()));
messages.parse_message(wallet);
modal.close();
self.current_tab = Box::new(messages);
return;
}
QrScanResult::Address(receiver) => {
if wallet.get_data().unwrap().info.amount_currently_spendable > 0 {
// Redirect to send amount with Tor.
let addr = wallet.slatepack_address().unwrap();
let mut transport = WalletTransport::new(addr.clone());
modal.close();
transport.show_send_tor_modal(cb, Some(receiver.to_string()));
self.current_tab = Box::new(transport);
return;
}
}
_ => {}
}
// Set result and rename modal title.
self.qr_scan_result = Some(result);
Modal::set_title(t!("scan_result"));
} else {
ui.add_space(6.0);
self.camera_content.ui(ui, cb);
ui.add_space(6.0);
}
if self.qr_scan_result.is_some() {
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(ui, t!("close"), Colors::white_or_black(false), || {
self.qr_scan_result = None;
modal.close();
});
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("repeat"), Colors::white_or_black(false), || {
Modal::set_title(t!("scan_qr"));
self.qr_scan_result = None;
cb.start_camera();
});
});
});
} else {
ui.vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
cb.stop_camera();
modal.close();
});
});
}
ui.add_space(6.0);
}
/// Draw tab buttons in the bottom of the screen.
fn tabs_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet) {
ui.scope(|ui| { ui.scope(|ui| {
// Setup spacing between tabs. // Setup spacing between tabs.
ui.style_mut().spacing.item_spacing = egui::vec2(View::TAB_ITEMS_PADDING, 0.0); ui.style_mut().spacing.item_spacing = egui::vec2(View::TAB_ITEMS_PADDING, 0.0);
// Show camera switch button at QR code scan.
if self.qr_scan_content.is_some() && cb.can_switch_camera() {
// Setup vertical padding inside tab button. // Setup vertical padding inside tab button.
ui.style_mut().spacing.button_padding = egui::vec2(10.0, 4.0);
ui.vertical_centered(|ui| {
View::tab_button(ui, CAMERA_ROTATE, false, |_| {
cb.switch_camera();
});
});
return;
}
// Setup vertical padding inside buttons.
ui.style_mut().spacing.button_padding = egui::vec2(0.0, 4.0); ui.style_mut().spacing.button_padding = egui::vec2(0.0, 4.0);
// Draw tab buttons.
let current_type = self.current_tab.get_type(); let current_type = self.current_tab.get_type();
ui.columns(4, |columns| { ui.columns(4, |columns| {
columns[0].vertical_centered_justified(|ui| { columns[0].vertical_centered_justified(|ui| {
View::tab_button(ui, GRAPH, current_type == WalletTabType::Txs, || { View::tab_button(ui, GRAPH, current_type == WalletTabType::Txs, |_| {
self.current_tab = Box::new(WalletTransactions::default()); self.current_tab = Box::new(WalletTransactions::default());
}); });
}); });
columns[1].vertical_centered_justified(|ui| { columns[1].vertical_centered_justified(|ui| {
let is_messages = current_type == WalletTabType::Messages; let is_messages = current_type == WalletTabType::Messages;
View::tab_button(ui, CHAT_CIRCLE_TEXT, is_messages, || { View::tab_button(ui, CHAT_CIRCLE_TEXT, is_messages, |_| {
self.current_tab = Box::new( self.current_tab = Box::new(
WalletMessages::new(wallet.can_use_dandelion(), None) WalletMessages::new(None)
); );
}); });
}); });
columns[2].vertical_centered_justified(|ui| { columns[2].vertical_centered_justified(|ui| {
View::tab_button(ui, BRIDGE, current_type == WalletTabType::Transport, || { View::tab_button(ui, BRIDGE, current_type == WalletTabType::Transport, |_| {
let addr = wallet.slatepack_address().unwrap(); self.current_tab = Box::new(WalletTransport::default());
self.current_tab = Box::new(WalletTransport::new(addr));
}); });
}); });
columns[3].vertical_centered_justified(|ui| { columns[3].vertical_centered_justified(|ui| {
View::tab_button(ui, GEAR_FINE, current_type == WalletTabType::Settings, || { View::tab_button(ui, GEAR_FINE, current_type == WalletTabType::Settings, |ui| {
ExternalConnection::check(None, ui.ctx());
self.current_tab = Box::new(WalletSettings::default()); self.current_tab = Box::new(WalletSettings::default());
}); });
}); });
}); });
}); });
} }
}
/// Draw content when wallet is syncing and not ready to use, returns `true` at this case. /// Draw content when wallet is syncing and not ready to use, returns `true` at this case.
pub fn sync_ui(ui: &mut egui::Ui, wallet: &Wallet) -> bool { fn sync_ui(ui: &mut egui::Ui, wallet: &Wallet) -> bool {
if wallet.is_repairing() && !wallet.sync_error() { if wallet.is_repairing() && !wallet.sync_error() {
Self::sync_progress_ui(ui, wallet); sync_progress_ui(ui, wallet);
return true; return true;
} else if wallet.is_closing() { } else if wallet.is_closing() {
Self::sync_progress_ui(ui, wallet); sync_progress_ui(ui, wallet);
return true; return true;
} else if wallet.get_current_ext_conn().is_none() { } else if wallet.get_current_connection() == ConnectionMethod::Integrated {
if !Node::is_running() || Node::is_stopping() { if !Node::is_running() || Node::is_stopping() {
View::center_content(ui, 108.0, |ui| { View::center_content(ui, 108.0, |ui| {
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.5, |ui| { View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
let text = t!("wallets.enable_node", "settings" => GEAR_FINE); let text = t!("wallets.enable_node", "settings" => GEAR_FINE);
ui.label(RichText::new(text).size(16.0).color(Colors::inactive_text())); ui.label(RichText::new(text).size(16.0).color(Colors::inactive_text()));
ui.add_space(8.0); ui.add_space(8.0);
// Show button to enable integrated node at non-dual root panel mode // Show button to enable integrated node at non-dual root panel mode
// or when network connections are not showing and node is not stopping // or when network connections are not showing and node is not stopping
let dual_panel_root = Content::is_dual_panel_mode(ui); let dual_panel = Content::is_dual_panel_mode(ui.ctx());
if (!dual_panel_root || AppConfig::show_connections_network_panel()) if (!dual_panel || AppConfig::show_connections_network_panel())
&& !Node::is_stopping() { && !Node::is_stopping() {
let enable_text = format!("{} {}", POWER, t!("network.enable_node")); let enable_text = format!("{} {}", POWER, t!("network.enable_node"));
View::action_button(ui, enable_text, || { View::action_button(ui, enable_text, || {
@ -596,24 +437,24 @@ impl WalletContent {
return true return true
} else if wallet.sync_error() } else if wallet.sync_error()
&& Node::get_sync_status() == Some(SyncStatus::NoSync) { && Node::get_sync_status() == Some(SyncStatus::NoSync) {
Self::sync_error_ui(ui, wallet); sync_error_ui(ui, wallet);
return true; return true;
} else if wallet.get_data().is_none() { } else if wallet.get_data().is_none() {
Self::sync_progress_ui(ui, wallet); sync_progress_ui(ui, wallet);
return true; return true;
} }
} else if wallet.sync_error() { } else if wallet.sync_error() {
Self::sync_error_ui(ui, wallet); sync_error_ui(ui, wallet);
return true; return true;
} else if wallet.get_data().is_none() { } else if wallet.get_data().is_none() {
Self::sync_progress_ui(ui, wallet); sync_progress_ui(ui, wallet);
return true; return true;
} }
false false
} }
/// Draw wallet sync error content. /// Draw wallet sync error content.
fn sync_error_ui(ui: &mut egui::Ui, wallet: &Wallet) { fn sync_error_ui(ui: &mut egui::Ui, wallet: &Wallet) {
View::center_content(ui, 108.0, |ui| { View::center_content(ui, 108.0, |ui| {
let text = t!("wallets.wallet_loading_err", "settings" => GEAR_FINE); let text = t!("wallets.wallet_loading_err", "settings" => GEAR_FINE);
ui.label(RichText::new(text).size(16.0).color(Colors::inactive_text())); ui.label(RichText::new(text).size(16.0).color(Colors::inactive_text()));
@ -623,36 +464,23 @@ impl WalletContent {
wallet.set_sync_error(false); wallet.set_sync_error(false);
}); });
}); });
} }
/// Check when to block tabs navigation on sync progress. /// Draw wallet sync progress content.
pub fn block_navigation_on_sync(wallet: &Wallet) -> bool { fn sync_progress_ui(ui: &mut egui::Ui, wallet: &Wallet) {
let sync_error = wallet.sync_error();
let integrated_node = wallet.get_current_ext_conn().is_none();
let integrated_node_ready = Node::get_sync_status() == Some(SyncStatus::NoSync);
let sync_after_opening = wallet.get_data().is_none() && !wallet.sync_error();
// Block navigation if wallet is repairing and integrated node is not launching
// and if wallet is closing or syncing after opening when there is no data to show.
(wallet.is_repairing() && (integrated_node_ready || !integrated_node) && !sync_error)
|| wallet.is_closing() || (sync_after_opening &&
(!integrated_node || integrated_node_ready))
}
/// Draw wallet sync progress content.
pub fn sync_progress_ui(ui: &mut egui::Ui, wallet: &Wallet) {
View::center_content(ui, 162.0, |ui| { View::center_content(ui, 162.0, |ui| {
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.5, |ui| { View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
View::big_loading_spinner(ui); View::big_loading_spinner(ui);
ui.add_space(18.0); ui.add_space(18.0);
// Setup sync progress text. // Setup sync progress text.
let text = { let text = {
let integrated_node = wallet.get_current_ext_conn().is_none(); let int_node = wallet.get_current_connection() == ConnectionMethod::Integrated;
let integrated_node_ready = Node::get_sync_status() == Some(SyncStatus::NoSync); let int_ready = Node::get_sync_status() == Some(SyncStatus::NoSync);
let info_progress = wallet.info_sync_progress(); let info_progress = wallet.info_sync_progress();
if wallet.is_closing() { if wallet.is_closing() {
t!("wallets.wallet_closing") t!("wallets.wallet_closing")
} else if integrated_node && !integrated_node_ready { } else if int_node && !int_ready {
t!("wallets.node_loading", "settings" => GEAR_FINE) t!("wallets.node_loading", "settings" => GEAR_FINE)
} else if wallet.is_repairing() { } else if wallet.is_repairing() {
let repair_progress = wallet.repairing_progress(); let repair_progress = wallet.repairing_progress();
@ -674,69 +502,4 @@ impl WalletContent {
ui.label(RichText::new(text).size(16.0).color(Colors::inactive_text())); ui.label(RichText::new(text).size(16.0).color(Colors::inactive_text()));
}); });
}); });
}
}
const ACCOUNT_ITEM_HEIGHT: f32 = 75.0;
/// Draw account item.
fn account_item_ui(ui: &mut egui::Ui,
modal: &Modal,
wallet: &mut Wallet,
acc: &WalletAccount,
index: usize,
size: usize) {
// Setup layout size.
let mut rect = ui.available_rect_before_wrap();
rect.set_height(ACCOUNT_ITEM_HEIGHT);
// Draw round background.
let bg_rect = rect.clone();
let item_rounding = View::item_rounding(index, size, false);
ui.painter().rect(bg_rect, item_rounding, Colors::fill(), View::item_stroke());
ui.vertical(|ui| {
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
// Draw button to select account.
let is_current_account = wallet.get_config().account == acc.label;
if !is_current_account {
let button_rounding = View::item_rounding(index, size, true);
View::item_button(ui, button_rounding, CHECK, None, || {
let _ = wallet.set_active_account(&acc.label);
modal.close();
});
} else {
ui.add_space(12.0);
ui.label(RichText::new(CHECK_FAT).size(20.0).color(Colors::green()));
}
let layout_size = ui.available_size();
ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| {
ui.add_space(6.0);
ui.vertical(|ui| {
ui.add_space(4.0);
// Show spendable amount.
let amount = amount_to_hr_string(acc.spendable_amount, true);
let amount_text = format!("{} {}", amount, GRIN);
ui.label(RichText::new(amount_text).size(18.0).color(Colors::white_or_black(true)));
ui.add_space(-2.0);
// Show account name.
let default_acc_label = WalletConfig::DEFAULT_ACCOUNT_LABEL.to_string();
let acc_label = if acc.label == default_acc_label {
t!("wallets.default_account")
} else {
acc.label.to_owned()
};
let acc_name = format!("{} {}", FOLDER_USER, acc_label);
View::ellipsize_text(ui, acc_name, 15.0, Colors::text(false));
// Show account BIP32 derivation path.
let acc_path = format!("{} {}", PATH, acc.path);
ui.label(RichText::new(acc_path).size(15.0).color(Colors::gray()));
ui.add_space(3.0);
});
});
});
});
} }

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,483 @@
// Copyright 2024 The Grim Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::sync::Arc;
use std::thread;
use egui::{Id, RichText, ScrollArea};
use egui::scroll_area::ScrollBarVisibility;
use grin_core::core::amount_to_hr_string;
use grin_wallet_libwallet::{Error, Slate, SlateState};
use parking_lot::RwLock;
use crate::gui::Colors;
use crate::gui::icons::{BROOM, CLIPBOARD_TEXT, DOWNLOAD_SIMPLE, SCAN, UPLOAD_SIMPLE};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{FilePickButton, Modal, View, CameraScanModal};
use crate::gui::views::types::{ModalPosition, QrScanResult};
use crate::gui::views::wallets::wallet::messages::request::MessageRequestModal;
use crate::gui::views::wallets::wallet::types::{SLATEPACK_MESSAGE_HINT, WalletTab, WalletTabType};
use crate::gui::views::wallets::wallet::WalletTransactionModal;
use crate::wallet::types::WalletTransaction;
use crate::wallet::Wallet;
/// Slatepack messages interaction tab content.
pub struct WalletMessages {
/// Flag to check if it's first content draw.
first_draw: bool,
/// Invoice or sending request creation [`Modal`] content.
request_modal_content: Option<MessageRequestModal>,
/// Wallet transaction [`Modal`] content.
tx_info_content: Option<WalletTransactionModal>,
/// Slatepacks message input text.
message_edit: String,
/// Flag to check if message request is loading.
message_loading: bool,
/// Error on finalization, parse or response creation.
message_error: String,
/// Parsed message result.
message_result: Arc<RwLock<Option<(Slate, Result<WalletTransaction, Error>)>>>,
/// QR code scanner [`Modal`] content.
scan_modal_content: Option<CameraScanModal>,
/// Button to parse picked file content.
file_pick_button: FilePickButton,
}
/// Identifier for amount input [`Modal`] to create invoice or sending request.
const REQUEST_MODAL: &'static str = "messages_request_modal";
/// Identifier for [`Modal`] modal to show transaction information.
const TX_INFO_MODAL: &'static str = "messages_tx_info_modal";
/// Identifier for [`Modal`] to scan Slatepack message from QR code.
const SCAN_QR_MODAL: &'static str = "messages_scan_qr_modal";
impl WalletTab for WalletMessages {
fn get_type(&self) -> WalletTabType {
WalletTabType::Messages
}
fn ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) {
self.modal_content_ui(ui, wallet, cb);
self.messages_ui(ui, wallet, cb);
}
}
impl WalletMessages {
/// Create new content instance, put message into input if provided.
pub fn new(message: Option<String>) -> Self {
Self {
first_draw: true,
message_edit: message.unwrap_or("".to_string()),
message_loading: false,
message_error: "".to_string(),
message_result: Arc::new(Default::default()),
tx_info_content: None,
request_modal_content: None,
file_pick_button: FilePickButton::default(),
scan_modal_content: None,
}
}
/// Draw messages content.
fn messages_ui(&mut self,
ui: &mut egui::Ui,
wallet: &Wallet,
cb: &dyn PlatformCallbacks) {
if self.first_draw {
// Parse provided message on first draw.
if !self.message_edit.is_empty() {
self.parse_message(wallet);
}
self.first_draw = false;
}
ui.add_space(3.0);
// Show creation of request to send or receive funds.
self.request_ui(ui, wallet, cb);
ui.add_space(12.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(6.0);
// Show Slatepack message input field.
self.input_slatepack_ui(ui, wallet, cb);
ui.add_space(6.0);
}
/// Draw [`Modal`] content for this ui container.
fn modal_content_ui(&mut self,
ui: &mut egui::Ui,
wallet: &Wallet,
cb: &dyn PlatformCallbacks) {
match Modal::opened() {
None => {}
Some(id) => {
match id {
REQUEST_MODAL => {
if let Some(content) = self.request_modal_content.as_mut() {
Modal::ui(ui.ctx(), |ui, modal| {
content.ui(ui, wallet, modal, cb);
});
}
}
TX_INFO_MODAL => {
if let Some(content) = self.tx_info_content.as_mut() {
Modal::ui(ui.ctx(), |ui, modal| {
content.ui(ui, wallet, modal, cb);
});
}
}
SCAN_QR_MODAL => {
let mut result = None;
if let Some(content) = self.scan_modal_content.as_mut() {
Modal::ui(ui.ctx(), |ui, modal| {
content.ui(ui, modal, cb, |res| {
result = Some(res.clone());
modal.close();
});
});
}
if let Some(res) = result {
self.scan_modal_content = None;
match &res {
QrScanResult::Slatepack(text) => {
self.message_edit = text.to_string();
self.parse_message(wallet);
}
_ => {
self.message_edit = res.text();
self.message_error = t!("wallets.parse_slatepack_err");
}
}
}
}
_ => {}
}
}
}
}
/// Draw creation of request to send or receive funds.
fn request_ui(&mut self,
ui: &mut egui::Ui,
wallet: &Wallet,
cb: &dyn PlatformCallbacks) {
ui.label(RichText::new(t!("wallets.create_request_desc"))
.size(16.0)
.color(Colors::inactive_text()));
ui.add_space(7.0);
// Show send button only if balance is not empty.
let data = wallet.get_data().unwrap();
if data.info.amount_currently_spendable > 0 {
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
let send_text = format!("{} {}", UPLOAD_SIMPLE, t!("wallets.send"));
View::colored_text_button(ui,
send_text,
Colors::red(),
Colors::white_or_black(false), || {
self.show_request_modal(false, cb);
});
});
columns[1].vertical_centered_justified(|ui| {
self.receive_button_ui(ui, cb);
});
});
} else {
self.receive_button_ui(ui, cb);
}
}
/// Draw invoice request creation button.
fn receive_button_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
let receive_text = format!("{} {}", DOWNLOAD_SIMPLE, t!("wallets.receive"));
View::colored_text_button(ui,
receive_text,
Colors::green(),
Colors::white_or_black(false), || {
self.show_request_modal(true, cb);
});
}
/// Show [`Modal`] to create invoice or sending request.
fn show_request_modal(&mut self, invoice: bool, cb: &dyn PlatformCallbacks) {
self.request_modal_content = Some(MessageRequestModal::new(invoice));
let title = if invoice {
t!("wallets.receive")
} else {
t!("wallets.send")
};
Modal::new(REQUEST_MODAL)
.position(ModalPosition::CenterTop)
.title(title)
.show();
cb.show_keyboard();
}
/// Draw Slatepack message input content.
fn input_slatepack_ui(&mut self,
ui: &mut egui::Ui,
wallet: &Wallet,
cb: &dyn PlatformCallbacks) {
// Setup description text.
if !self.message_error.is_empty() {
ui.label(RichText::new(&self.message_error)
.size(16.0)
.color(Colors::red()));
} else {
ui.label(RichText::new(t!("wallets.input_slatepack_desc"))
.size(16.0)
.color(Colors::inactive_text()));
}
ui.add_space(6.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(3.0);
// Save message to check for changes.
let message_before = self.message_edit.clone();
let scroll_id = Id::from("message_input_scroll").with(wallet.get_config().id);
ScrollArea::vertical()
.id_salt(scroll_id)
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.max_height(128.0)
.auto_shrink([false; 2])
.show(ui, |ui| {
ui.add_space(7.0);
let input_id = scroll_id.with("_input");
let resp = egui::TextEdit::multiline(&mut self.message_edit)
.id(input_id)
.font(egui::TextStyle::Small)
.desired_rows(5)
.interactive(!self.message_loading)
.hint_text(SLATEPACK_MESSAGE_HINT)
.desired_width(f32::INFINITY)
.show(ui)
.response;
// Show soft keyboard on click.
if resp.clicked() {
resp.request_focus();
cb.show_keyboard();
}
if resp.has_focus() {
// Apply text from input on Android as temporary fix for egui.
View::on_soft_input(ui, input_id, &mut self.message_edit);
}
ui.add_space(6.0);
});
ui.add_space(2.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(10.0);
// Parse message if input field was changed.
if message_before != self.message_edit {
self.parse_message(wallet);
}
if self.message_loading {
View::small_loading_spinner(ui);
// Check loading result.
let has_tx = {
let r_res = self.message_result.read();
r_res.is_some()
};
if has_tx {
let mut w_res = self.message_result.write();
let tx_res = w_res.as_ref().unwrap();
let slate = &tx_res.0;
match &tx_res.1 {
Ok(tx) => {
self.message_edit.clear();
// Show transaction modal on success.
self.tx_info_content = Some(WalletTransactionModal::new(wallet, tx, false));
Modal::new(TX_INFO_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("wallets.tx"))
.show();
*w_res = None;
}
Err(err) => {
match err {
Error::TransactionWasCancelled {..} => {
self.message_error = t!("wallets.resp_canceled_err");
}
Error::NotEnoughFunds {..} => {
let m = t!(
"wallets.pay_balance_error",
"amount" => amount_to_hr_string(slate.amount, true)
);
self.message_error = m;
}
_ => {
// Show tx modal or show default error message.
if let Some(tx) = wallet.tx_by_slate(&slate).as_ref() {
self.message_edit.clear();
self.tx_info_content = Some(
WalletTransactionModal::new(wallet, tx, false)
);
Modal::new(TX_INFO_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("wallets.tx"))
.show();
} else {
let finalize = slate.state == SlateState::Standard2 ||
slate.state == SlateState::Invoice2;
self.message_error = if finalize {
t!("wallets.finalize_slatepack_err")
} else {
t!("wallets.resp_slatepack_err")
};
}
}
}
}
}
self.message_loading = false;
}
return;
}
ui.scope(|ui| {
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
let scan_text = format!("{} {}", SCAN, t!("scan"));
View::button(ui, scan_text, Colors::white_or_black(false), || {
self.message_edit.clear();
self.message_error.clear();
self.scan_modal_content = Some(CameraScanModal::default());
// Show QR code scan modal.
Modal::new(SCAN_QR_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("scan_qr"))
.closeable(false)
.show();
cb.start_camera();
});
});
columns[1].vertical_centered_justified(|ui| {
// Draw button to paste text from clipboard.
let paste = format!("{} {}", CLIPBOARD_TEXT, t!("paste"));
View::button(ui, paste, Colors::white_or_black(false), || {
let buf = cb.get_string_from_buffer();
let previous = self.message_edit.clone();
self.message_edit = buf.clone().trim().to_string();
// Parse Slatepack message resetting message error.
if buf != previous {
self.parse_message(wallet);
}
});
});
});
ui.add_space(10.0);
});
if self.message_edit.is_empty() {
// Draw button to choose file.
let mut parsed_text = "".to_string();
self.file_pick_button.ui(ui, cb, |text| {
parsed_text = text;
});
self.message_edit = parsed_text;
self.parse_message(wallet);
} else {
// Draw button to clear message input.
let clear_text = format!("{} {}", BROOM, t!("clear"));
View::button(ui, clear_text, Colors::white_or_black(false), || {
self.message_edit.clear();
self.message_error.clear();
});
}
}
/// Parse message input making operation based on incoming status.
fn parse_message(&mut self, wallet: &Wallet) {
self.message_error.clear();
self.message_edit = self.message_edit.trim().to_string();
if self.message_edit.is_empty() {
return;
}
if let Ok(mut slate) = wallet.parse_slatepack(&self.message_edit) {
// Try to setup empty amount from transaction by id.
if slate.amount == 0 {
let _ = wallet.get_data().unwrap().txs.as_ref().unwrap().iter().map(|tx| {
if tx.data.tx_slate_id == Some(slate.id) {
if slate.amount == 0 {
slate.amount = tx.amount;
}
}
tx
}).collect::<Vec<&WalletTransaction>>();
}
// Check if message with same id and state already exists to show tx modal.
let exists = wallet.read_slatepack(&slate).is_some();
if exists {
if let Some(tx) = wallet.tx_by_slate(&slate).as_ref() {
self.message_edit.clear();
self.tx_info_content = Some(WalletTransactionModal::new(wallet, tx, false));
Modal::new(TX_INFO_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("wallets.tx"))
.show();
return;
}
}
// Create response or finalize at separate thread.
let sl = slate.clone();
let message = self.message_edit.clone();
let message_result = self.message_result.clone();
let wallet = wallet.clone();
self.message_loading = true;
thread::spawn(move || {
let result = match slate.state {
SlateState::Standard1 | SlateState::Invoice1 => {
if sl.state != SlateState::Standard1 {
wallet.pay(&message)
} else {
wallet.receive(&message)
}
}
SlateState::Standard2 | SlateState::Invoice2 => {
wallet.finalize(&message)
}
_ => {
if let Some(tx) = wallet.tx_by_slate(&slate) {
Ok(tx)
} else {
Err(Error::GenericError(t!("wallets.parse_slatepack_err")))
}
}
};
let mut w_res = message_result.write();
*w_res = Some((slate, result));
});
} else {
self.message_error = t!("wallets.parse_slatepack_err");
}
}
}

View file

@ -0,0 +1,18 @@
// Copyright 2024 The Grim Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
mod content;
pub use content::*;
mod request;

View file

@ -0,0 +1,260 @@
// Copyright 2024 The Grim Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::sync::Arc;
use std::thread;
use parking_lot::RwLock;
use egui::{Id, RichText};
use grin_core::core::{amount_from_hr_string, amount_to_hr_string};
use grin_wallet_libwallet::Error;
use crate::gui::Colors;
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Modal, View};
use crate::gui::views::types::TextEditOptions;
use crate::gui::views::wallets::wallet::WalletTransactionModal;
use crate::wallet::types::WalletTransaction;
use crate::wallet::Wallet;
/// Invoice or sending request creation [`Modal`] content.
pub struct MessageRequestModal {
/// Flag to check if invoice or sending request was opened.
invoice: bool,
/// Amount to send or receive.
amount_edit: String,
/// Flag to check if request is loading.
request_loading: bool,
/// Request result if there is no error.
request_result: Arc<RwLock<Option<Result<WalletTransaction, Error>>>>,
/// Flag to check if there is an error happened on request creation.
request_error: Option<String>,
/// Request result transaction content.
result_tx_content: Option<WalletTransactionModal>,
}
impl MessageRequestModal {
/// Create new content instance.
pub fn new(invoice: bool) -> Self {
Self {
invoice,
amount_edit: "".to_string(),
request_loading: false,
request_result: Arc::new(RwLock::new(None)),
request_error: None,
result_tx_content: None,
}
}
/// Draw [`Modal`] content.
pub fn ui(&mut self,
ui: &mut egui::Ui,
wallet: &Wallet,
modal: &Modal,
cb: &dyn PlatformCallbacks) {
// Draw transaction information on request result.
if let Some(tx) = self.result_tx_content.as_mut() {
tx.ui(ui, wallet, modal, cb);
return;
}
ui.add_space(6.0);
// Draw content on request loading.
if self.request_loading {
self.loading_request_ui(ui, wallet, modal);
return;
}
// Draw amount input content.
self.amount_input_ui(ui, wallet, modal, cb);
// Show request creation error.
if let Some(err) = &self.request_error {
ui.add_space(12.0);
ui.vertical_centered(|ui| {
ui.label(RichText::new(err)
.size(17.0)
.color(Colors::red()));
});
}
ui.add_space(12.0);
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
self.amount_edit = "".to_string();
self.request_error = None;
cb.hide_keyboard();
modal.close();
});
});
columns[1].vertical_centered_justified(|ui| {
// Button to create Slatepack message request.
View::button(ui, t!("continue"), Colors::white_or_black(false), || {
if self.amount_edit.is_empty() {
return;
}
if let Ok(a) = amount_from_hr_string(self.amount_edit.as_str()) {
cb.hide_keyboard();
modal.disable_closing();
// Setup data for request.
let wallet = wallet.clone();
let invoice = self.invoice.clone();
let result = self.request_result.clone();
// Send request at another thread.
self.request_loading = true;
thread::spawn(move || {
let res = if invoice {
wallet.issue_invoice(a)
} else {
wallet.send(a, None)
};
let mut w_result = result.write();
*w_result = Some(res);
});
} else {
let err = if self.invoice {
t!("wallets.invoice_slatepack_err")
} else {
t!("wallets.send_slatepack_err")
};
self.request_error = Some(err);
}
});
});
});
ui.add_space(6.0);
}
/// Draw amount input content.
fn amount_input_ui(&mut self,
ui: &mut egui::Ui,
wallet: &Wallet,
modal: &Modal,
cb: &dyn PlatformCallbacks) {
ui.vertical_centered(|ui| {
let enter_text = if self.invoice {
t!("wallets.enter_amount_receive")
} else {
let data = wallet.get_data().unwrap();
let amount = amount_to_hr_string(data.info.amount_currently_spendable, true);
t!("wallets.enter_amount_send","amount" => amount)
};
ui.label(RichText::new(enter_text)
.size(17.0)
.color(Colors::gray()));
});
ui.add_space(8.0);
// Draw request amount text input.
let amount_edit_id = Id::from(modal.id).with(wallet.get_config().id);
let mut amount_edit_opts = TextEditOptions::new(amount_edit_id).h_center();
let amount_edit_before = self.amount_edit.clone();
View::text_edit(ui, cb, &mut self.amount_edit, &mut amount_edit_opts);
// Check value if input was changed.
if amount_edit_before != self.amount_edit {
self.request_error = None;
if !self.amount_edit.is_empty() {
self.amount_edit = self.amount_edit.trim().replace(",", ".");
match amount_from_hr_string(self.amount_edit.as_str()) {
Ok(a) => {
if !self.amount_edit.contains(".") {
// To avoid input of several "0".
if a == 0 {
self.amount_edit = "0".to_string();
return;
}
} else {
// Check input after ".".
let parts = self.amount_edit
.split(".")
.collect::<Vec<&str>>();
if parts.len() == 2 && parts[1].len() > 9 {
self.amount_edit = amount_edit_before;
return;
}
}
// Do not input amount more than balance in sending.
if !self.invoice {
let b = wallet.get_data().unwrap().info.amount_currently_spendable;
if b < a {
self.amount_edit = amount_edit_before;
}
}
}
Err(_) => {
self.amount_edit = amount_edit_before;
}
}
}
}
}
/// Draw loading request content.
fn loading_request_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, modal: &Modal) {
ui.add_space(34.0);
ui.vertical_centered(|ui| {
View::big_loading_spinner(ui);
});
ui.add_space(50.0);
// Check if there is request result error.
if self.request_error.is_some() {
modal.enable_closing();
self.request_loading = false;
return;
}
// Update data on request result.
let r_request = self.request_result.read();
if r_request.is_some() {
modal.enable_closing();
let result = r_request.as_ref().unwrap();
match result {
Ok(tx) => {
self.result_tx_content = Some(WalletTransactionModal::new(wallet, tx, false));
}
Err(err) => {
match err {
Error::NotEnoughFunds { .. } => {
let m = t!(
"wallets.pay_balance_error",
"amount" => self.amount_edit
);
self.request_error = Some(m);
}
_ => {
let m = if self.invoice {
t!("wallets.invoice_slatepack_err")
} else {
t!("wallets.send_slatepack_err")
};
self.request_error = Some(m);
}
}
self.request_loading = false;
}
}
}
}
}

View file

@ -13,10 +13,12 @@
// limitations under the License. // limitations under the License.
pub mod types; pub mod types;
pub mod settings;
mod settings;
pub use settings::*;
mod txs; mod txs;
pub use txs::WalletTransactions; pub use txs::*;
mod messages; mod messages;
pub use messages::WalletMessages; pub use messages::WalletMessages;
@ -26,3 +28,5 @@ pub use transport::WalletTransport;
mod content; mod content;
pub use content::WalletContent; pub use content::WalletContent;
mod modals;

View file

@ -0,0 +1,240 @@
// Copyright 2024 The Grim Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use egui::{Align, Id, Layout, RichText, ScrollArea};
use egui::scroll_area::ScrollBarVisibility;
use grin_core::core::amount_to_hr_string;
use crate::gui::Colors;
use crate::gui::icons::{CHECK, CHECK_FAT, FOLDER_USER, PATH};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Modal, View};
use crate::gui::views::types::TextEditOptions;
use crate::gui::views::wallets::wallet::types::GRIN;
use crate::wallet::types::WalletAccount;
use crate::wallet::{Wallet, WalletConfig};
/// Wallet accounts [`Modal`] content.
pub struct WalletAccountsModal {
/// List of wallet accounts.
accounts: Vec<WalletAccount>,
/// Flag to check if account is creating.
account_creating: bool,
/// Account label value.
account_label_edit: String,
/// Flag to check if error occurred during account creation.
account_creation_error: bool,
}
impl Default for WalletAccountsModal {
fn default() -> Self {
Self {
accounts: vec![],
account_creating: false,
account_label_edit: "".to_string(),
account_creation_error: false,
}
}
}
impl WalletAccountsModal {
/// Create new instance from wallet accounts.
pub fn new(accounts: Vec<WalletAccount>) -> Self {
Self {
accounts,
account_creating: false,
account_label_edit: "".to_string(),
account_creation_error: false,
}
}
/// Draw [`Modal`] content.
pub fn ui(&mut self,
ui: &mut egui::Ui,
wallet: &Wallet,
modal: &Modal,
cb: &dyn PlatformCallbacks) {
if self.account_creating {
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("wallets.new_account_desc"))
.size(17.0)
.color(Colors::gray()));
ui.add_space(8.0);
// Draw account name edit.
let text_edit_id = Id::from(modal.id).with(wallet.get_config().id);
let mut text_edit_opts = TextEditOptions::new(text_edit_id);
View::text_edit(ui, cb, &mut self.account_label_edit, &mut text_edit_opts);
// Show error occurred during account creation..
if self.account_creation_error {
ui.add_space(12.0);
ui.label(RichText::new(t!("error"))
.size(17.0)
.color(Colors::red()));
}
ui.add_space(12.0);
});
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
// Show modal buttons.
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
// Close modal.
cb.hide_keyboard();
modal.close();
});
});
columns[1].vertical_centered_justified(|ui| {
// Create button callback.
let mut on_create = || {
if !self.account_label_edit.is_empty() {
let label = &self.account_label_edit;
match wallet.create_account(label) {
Ok(_) => {
let _ = wallet.set_active_account(label);
cb.hide_keyboard();
modal.close();
},
Err(_) => self.account_creation_error = true
};
}
};
View::on_enter_key(ui, || {
(on_create)();
});
View::button(ui, t!("create"), Colors::white_or_black(false), on_create);
});
});
ui.add_space(6.0);
} else {
ui.add_space(3.0);
// Show list of accounts.
let size = self.accounts.len();
ScrollArea::vertical()
.id_salt("account_list_modal_scroll")
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.max_height(266.0)
.auto_shrink([true; 2])
.show_rows(ui, ACCOUNT_ITEM_HEIGHT, size, |ui, row_range| {
for index in row_range {
// Add space before the first item.
if index == 0 {
ui.add_space(4.0);
}
let acc = self.accounts.get(index).unwrap();
account_item_ui(ui, modal, wallet, acc, index, size);
if index == size - 1 {
ui.add_space(4.0);
}
}
});
ui.add_space(2.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(6.0);
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
// Show modal buttons.
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
modal.close();
});
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("create"), Colors::white_or_black(false), || {
self.account_creating = true;
cb.show_keyboard();
});
});
});
ui.add_space(6.0);
}
}
}
const ACCOUNT_ITEM_HEIGHT: f32 = 75.0;
/// Draw account item.
fn account_item_ui(ui: &mut egui::Ui,
modal: &Modal,
wallet: &Wallet,
acc: &WalletAccount,
index: usize,
size: usize) {
// Setup layout size.
let mut rect = ui.available_rect_before_wrap();
rect.set_height(ACCOUNT_ITEM_HEIGHT);
// Draw round background.
let bg_rect = rect.clone();
let item_rounding = View::item_rounding(index, size, false);
ui.painter().rect(bg_rect, item_rounding, Colors::fill(), View::item_stroke());
ui.vertical(|ui| {
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
// Draw button to select account.
let is_current_account = wallet.get_config().account == acc.label;
if !is_current_account {
let button_rounding = View::item_rounding(index, size, true);
View::item_button(ui, button_rounding, CHECK, None, || {
let _ = wallet.set_active_account(&acc.label);
modal.close();
});
} else {
ui.add_space(12.0);
ui.label(RichText::new(CHECK_FAT).size(20.0).color(Colors::green()));
}
let layout_size = ui.available_size();
ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| {
ui.add_space(6.0);
ui.vertical(|ui| {
ui.add_space(4.0);
// Show spendable amount.
let amount = amount_to_hr_string(acc.spendable_amount, true);
let amount_text = format!("{} {}", amount, GRIN);
ui.label(RichText::new(amount_text).size(18.0).color(Colors::white_or_black(true)));
ui.add_space(-2.0);
// Show account name.
let default_acc_label = WalletConfig::DEFAULT_ACCOUNT_LABEL.to_string();
let acc_label = if acc.label == default_acc_label {
t!("wallets.default_account")
} else {
acc.label.to_owned()
};
let acc_name = format!("{} {}", FOLDER_USER, acc_label);
View::ellipsize_text(ui, acc_name, 15.0, Colors::text(false));
// Show account BIP32 derivation path.
let acc_path = format!("{} {}", PATH, acc.path);
ui.label(RichText::new(acc_path).size(15.0).color(Colors::gray()));
ui.add_space(3.0);
});
});
});
});
}

View file

@ -0,0 +1,16 @@
// Copyright 2024 The Grim Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
mod accounts;
pub use accounts::*;

View file

@ -36,7 +36,7 @@ pub struct CommonSettings {
new_pass_edit: String, new_pass_edit: String,
/// Minimum confirmations number value. /// Minimum confirmations number value.
min_confirmations_edit: String min_confirmations_edit: String,
} }
/// Identifier for wallet name [`Modal`]. /// Identifier for wallet name [`Modal`].
@ -54,33 +54,34 @@ impl Default for CommonSettings {
wrong_pass: false, wrong_pass: false,
old_pass_edit: "".to_string(), old_pass_edit: "".to_string(),
new_pass_edit: "".to_string(), new_pass_edit: "".to_string(),
min_confirmations_edit: "".to_string() min_confirmations_edit: "".to_string(),
} }
} }
} }
impl CommonSettings { impl CommonSettings {
pub fn ui(&mut self, ui: &mut egui::Ui, wallet: &mut Wallet, cb: &dyn PlatformCallbacks) { /// Draw common wallet settings content.
// Show modal content for this ui container. pub fn ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) {
// Show modal content for this container.
self.modal_content_ui(ui, wallet, cb); self.modal_content_ui(ui, wallet, cb);
ui.vertical_centered(|ui| { ui.vertical_centered(|ui| {
let wallet_name = wallet.get_config().name; let config = wallet.get_config();
// Show wallet name. // Show wallet name.
ui.add_space(2.0); ui.add_space(2.0);
ui.label(RichText::new(t!("wallets.name")) ui.label(RichText::new(t!("wallets.name"))
.size(16.0) .size(16.0)
.color(Colors::gray())); .color(Colors::gray()));
ui.add_space(2.0); ui.add_space(2.0);
ui.label(RichText::new(wallet_name.clone()) ui.label(RichText::new(&config.name)
.size(16.0) .size(16.0)
.color(Colors::white_or_black(true))); .color(Colors::white_or_black(true)));
ui.add_space(8.0); ui.add_space(8.0);
// Show wallet name setup. // Show wallet name setup.
let name_text = format!("{} {}", PENCIL, t!("change")); let name_text = format!("{} {}", PENCIL, t!("change"));
View::button(ui, name_text, Colors::button(), || { View::button(ui, name_text, Colors::white_or_black(false), || {
self.name_edit = wallet_name; self.name_edit = config.name;
// Show wallet name modal. // Show wallet name modal.
Modal::new(NAME_EDIT_MODAL) Modal::new(NAME_EDIT_MODAL)
.position(ModalPosition::CenterTop) .position(ModalPosition::CenterTop)
@ -97,7 +98,7 @@ impl CommonSettings {
// Show wallet password setup. // Show wallet password setup.
let pass_text = format!("{} {}", PASSWORD, t!("change")); let pass_text = format!("{} {}", PASSWORD, t!("change"));
View::button(ui, pass_text, Colors::button(), || { View::button(ui, pass_text, Colors::white_or_black(false), || {
// Setup modal values. // Setup modal values.
self.first_edit_pass_opening = true; self.first_edit_pass_opening = true;
self.old_pass_edit = "".to_string(); self.old_pass_edit = "".to_string();
@ -118,10 +119,9 @@ impl CommonSettings {
ui.add_space(6.0); ui.add_space(6.0);
// Show minimum amount of confirmations value setup. // Show minimum amount of confirmations value setup.
let min_confirmations = wallet.get_config().min_confirmations; let min_conf_text = format!("{} {}", CLOCK_COUNTDOWN, config.min_confirmations);
let min_conf_text = format!("{} {}", CLOCK_COUNTDOWN, min_confirmations); View::button(ui, min_conf_text, Colors::white_or_black(false), || {
View::button(ui, min_conf_text, Colors::button(), || { self.min_confirmations_edit = config.min_confirmations.to_string();
self.min_confirmations_edit = min_confirmations.to_string();
// Show minimum amount of confirmations value modal. // Show minimum amount of confirmations value modal.
Modal::new(MIN_CONFIRMATIONS_EDIT_MODAL) Modal::new(MIN_CONFIRMATIONS_EDIT_MODAL)
.position(ModalPosition::CenterTop) .position(ModalPosition::CenterTop)
@ -131,15 +131,22 @@ impl CommonSettings {
}); });
ui.add_space(12.0); ui.add_space(12.0);
// Setup ability to post wallet transactions with Dandelion.
View::checkbox(ui, wallet.can_use_dandelion(), t!("wallets.use_dandelion"), || {
wallet.update_use_dandelion(!wallet.can_use_dandelion());
});
ui.add_space(6.0);
View::horizontal_line(ui, Colors::stroke()); View::horizontal_line(ui, Colors::stroke());
ui.add_space(4.0); ui.add_space(6.0);
}); });
} }
/// Draw [`Modal`] content for this ui container. /// Draw [`Modal`] content for this ui container.
fn modal_content_ui(&mut self, fn modal_content_ui(&mut self,
ui: &mut egui::Ui, ui: &mut egui::Ui,
wallet: &mut Wallet, wallet: &Wallet,
cb: &dyn PlatformCallbacks) { cb: &dyn PlatformCallbacks) {
match Modal::opened() { match Modal::opened() {
None => {} None => {}
@ -169,7 +176,7 @@ impl CommonSettings {
/// Draw wallet name [`Modal`] content. /// Draw wallet name [`Modal`] content.
fn name_modal_ui(&mut self, fn name_modal_ui(&mut self,
ui: &mut egui::Ui, ui: &mut egui::Ui,
wallet: &mut Wallet, wallet: &Wallet,
modal: &Modal, modal: &Modal,
cb: &dyn PlatformCallbacks) { cb: &dyn PlatformCallbacks) {
ui.add_space(6.0); ui.add_space(6.0);
@ -223,7 +230,7 @@ impl CommonSettings {
/// Draw wallet pass [`Modal`] content. /// Draw wallet pass [`Modal`] content.
fn pass_modal_ui(&mut self, fn pass_modal_ui(&mut self,
ui: &mut egui::Ui, ui: &mut egui::Ui,
wallet: &mut Wallet, wallet: &Wallet,
modal: &Modal, modal: &Modal,
cb: &dyn PlatformCallbacks) { cb: &dyn PlatformCallbacks) {
let wallet_id = wallet.get_config().id; let wallet_id = wallet.get_config().id;
@ -321,7 +328,7 @@ impl CommonSettings {
/// Draw wallet name [`Modal`] content. /// Draw wallet name [`Modal`] content.
fn min_conf_modal_ui(&mut self, fn min_conf_modal_ui(&mut self,
ui: &mut egui::Ui, ui: &mut egui::Ui,
wallet: &mut Wallet, wallet: &Wallet,
modal: &Modal, modal: &Modal,
cb: &dyn PlatformCallbacks) { cb: &dyn PlatformCallbacks) {
ui.add_space(6.0); ui.add_space(6.0);

View file

@ -29,9 +29,6 @@ pub struct ConnectionSettings {
/// Selected connection method. /// Selected connection method.
pub method: ConnectionMethod, pub method: ConnectionMethod,
/// Current wallet external connection.
curr_ext_conn: Option<ExternalConnection>,
/// External connection [`Modal`] content. /// External connection [`Modal`] content.
ext_conn_modal: ExternalConnectionModal, ext_conn_modal: ExternalConnectionModal,
@ -41,10 +38,8 @@ pub struct ConnectionSettings {
impl Default for ConnectionSettings { impl Default for ConnectionSettings {
fn default() -> Self { fn default() -> Self {
ExternalConnection::check_ext_conn_availability(None);
Self { Self {
method: ConnectionMethod::Integrated, method: ConnectionMethod::Integrated,
curr_ext_conn: None,
ext_conn_modal: ExternalConnectionModal::new(None), ext_conn_modal: ExternalConnectionModal::new(None),
modal_ids: vec![ modal_ids: vec![
ExternalConnectionModal::WALLET_ID ExternalConnectionModal::WALLET_ID
@ -74,52 +69,29 @@ impl ModalContainer for ConnectionSettings {
impl ConnectionSettings { impl ConnectionSettings {
/// Draw wallet creation setup content. /// Draw wallet creation setup content.
pub fn create_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { pub fn create_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
self.ui(ui, None, cb); self.ui(ui, cb);
} }
/// Draw existing wallet connection setup content. /// Draw existing wallet connection setup content.
pub fn wallet_ui(&mut self, ui: &mut egui::Ui, w: &mut Wallet, cb: &dyn PlatformCallbacks) { pub fn wallet_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) {
// Setup connection value from provided wallet. self.method = wallet.get_current_connection();
match w.get_config().ext_conn_id {
None => self.method = ConnectionMethod::Integrated,
Some(id) => self.method = ConnectionMethod::External(id)
}
// Draw setup content. // Draw setup content.
self.ui(ui, Some(w), cb); let changed = self.ui(ui, cb);
// Setup wallet connection value after change.
let changed = match self.method {
ConnectionMethod::Integrated => {
let changed = w.get_current_ext_conn().is_some();
if changed { if changed {
w.update_ext_conn_id(None); wallet.update_connection(&self.method);
}
changed
}
ConnectionMethod::External(id) => {
let changed = w.get_config().ext_conn_id != Some(id);
if changed {
w.update_ext_conn_id(Some(id));
}
changed
}
};
// Reopen wallet if connection changed. // Reopen wallet if connection changed.
if changed { if !wallet.reopen_needed() {
if !w.reopen_needed() { wallet.set_reopen(true);
w.set_reopen(true); wallet.close();
w.close();
} }
} }
} }
/// Draw connection setup content. /// Draw connection setup content, returning `true` if connection was changed.
fn ui(&mut self, fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) -> bool {
ui: &mut egui::Ui, let mut changed = false;
wallet: Option<&Wallet>,
cb: &dyn PlatformCallbacks) {
// Draw modal content for current ui container. // Draw modal content for current ui container.
self.current_modal_ui(ui, cb); self.current_modal_ui(ui, cb);
@ -129,14 +101,14 @@ impl ConnectionSettings {
ui.add_space(6.0); ui.add_space(6.0);
ui.vertical_centered(|ui| { ui.vertical_centered(|ui| {
// Show integrated node selection.
ui.add_space(6.0); ui.add_space(6.0);
// Show integrated node selection.
ConnectionsContent::integrated_node_item_ui(ui, |ui| { ConnectionsContent::integrated_node_item_ui(ui, |ui| {
// Draw button to select integrated node if it was not selected.
let is_current_method = self.method == ConnectionMethod::Integrated; let is_current_method = self.method == ConnectionMethod::Integrated;
if !is_current_method { if !is_current_method {
View::item_button(ui, View::item_rounding(0, 1, true), CHECK, None, || { View::item_button(ui, View::item_rounding(0, 1, true), CHECK, None, || {
self.method = ConnectionMethod::Integrated; self.method = ConnectionMethod::Integrated;
changed = true;
}); });
} else { } else {
ui.add_space(14.0); ui.add_space(14.0);
@ -145,53 +117,65 @@ impl ConnectionSettings {
} }
}); });
// Show external connections.
ui.add_space(8.0); ui.add_space(8.0);
ui.label(RichText::new(t!("wallets.ext_conn")).size(16.0).color(Colors::gray())); ui.label(RichText::new(t!("wallets.ext_conn")).size(16.0).color(Colors::gray()));
ui.add_space(6.0); ui.add_space(6.0);
// Show button to add new external node connection. // Show button to add new external node connection.
let add_node_text = format!("{} {}", PLUS_CIRCLE, t!("wallets.add_node")); let add_node_text = format!("{} {}", PLUS_CIRCLE, t!("wallets.add_node"));
View::button(ui, add_node_text, Colors::button(), || { View::button(ui, add_node_text, Colors::white_or_black(false), || {
self.show_add_ext_conn_modal(cb); self.ext_conn_modal = ExternalConnectionModal::new(None);
Modal::new(ExternalConnectionModal::WALLET_ID)
.position(ModalPosition::CenterTop)
.title(t!("wallets.add_node"))
.show();
cb.show_keyboard();
}); });
ui.add_space(4.0); ui.add_space(4.0);
let mut ext_conn_list = ConnectionsConfig::ext_conn_list(); // Check if it's current method.
let is_current = |m: &ConnectionMethod, c: &ExternalConnection| -> Option<bool> {
match m {
ConnectionMethod::External(id, _) => if c.deleted && *id == c.id {
None
} else {
Some(*id == c.id)
},
_ => Some(false)
}
};
// Check if current external connection was deleted to show at 1st place. let method = &self.method.clone();
if let Some(wallet) = wallet { let ext_conn_list = ConnectionsConfig::ext_conn_list();
if let Some(conn) = wallet.get_current_ext_conn() { let ext_list = ext_conn_list.iter().filter(|c| {
if ext_conn_list.iter() !c.deleted || is_current(method, c).unwrap_or(true)
.filter(|c| c.id == conn.id) }).collect::<Vec<&ExternalConnection>>();
.collect::<Vec<&ExternalConnection>>().is_empty() { let ext_size = ext_list.len();
if self.curr_ext_conn.is_none() { if ext_size != 0 {
self.curr_ext_conn = Some(conn);
}
ext_conn_list.insert(0, self.curr_ext_conn.as_ref().unwrap().clone());
}
}
}
if !ext_conn_list.is_empty() {
ui.add_space(8.0); ui.add_space(8.0);
for (index, conn) in ext_conn_list.iter().enumerate() {
for (i, c) in ext_list.iter().enumerate() {
ui.horizontal_wrapped(|ui| { ui.horizontal_wrapped(|ui| {
// Draw external connection item. // Draw external connection item.
self.ext_conn_item_ui(ui, wallet, conn, index, ext_conn_list.len()); let is_current = is_current(method, c);
Self::ext_conn_item_ui(ui, c, is_current, i, ext_size, || {
self.method = ConnectionMethod::External(c.id, c.url.clone());
changed = true;
});
}); });
} }
} }
}); });
changed
} }
/// Draw external connection item content. /// Draw external connection item content.
fn ext_conn_item_ui(&mut self, fn ext_conn_item_ui(ui: &mut egui::Ui,
ui: &mut egui::Ui,
wallet: Option<&Wallet>,
conn: &ExternalConnection, conn: &ExternalConnection,
is_current: Option<bool>,
index: usize, index: usize,
len: usize) { len: usize,
mut on_select: impl FnMut()) {
// Setup layout size. // Setup layout size.
let mut rect = ui.available_rect_before_wrap(); let mut rect = ui.available_rect_before_wrap();
rect.set_height(52.0); rect.set_height(52.0);
@ -203,24 +187,15 @@ impl ConnectionSettings {
ui.vertical(|ui| { ui.vertical(|ui| {
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| { ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
// Draw button to select connection. if is_current.unwrap_or(true) {
let is_current_method = if let Some(wallet) = wallet {
if let Some(cur) = wallet.get_config().ext_conn_id {
cur == conn.id
} else {
false
}
} else {
self.method == ConnectionMethod::External(conn.id)
};
if !is_current_method {
let button_rounding = View::item_rounding(index, len, true);
View::item_button(ui, button_rounding, CHECK, None, || {
self.method = ConnectionMethod::External(conn.id);
});
} else {
ui.add_space(12.0); ui.add_space(12.0);
ui.label(RichText::new(CHECK_FAT).size(20.0).color(Colors::green())); ui.label(RichText::new(CHECK_FAT).size(20.0).color(Colors::green()));
} else {
// Draw button to select connection.
let button_rounding = View::item_rounding(index, len, true);
View::item_button(ui, button_rounding, CHECK, None, || {
on_select();
});
} }
let layout_size = ui.available_size(); let layout_size = ui.available_size();
@ -235,7 +210,11 @@ impl ConnectionSettings {
// Setup connection status text. // Setup connection status text.
let status_text = if let Some(available) = conn.available { let status_text = if let Some(available) = conn.available {
if available { if available {
format!("{} {}", CHECK_CIRCLE, t!("network.available")) format!("{} {}", CHECK_CIRCLE, if is_current.is_none() {
t!("transport.connected")
} else {
t!("network.available")
})
} else { } else {
format!("{} {}", X_CIRCLE, t!("network.not_available")) format!("{} {}", X_CIRCLE, t!("network.not_available"))
} }
@ -249,15 +228,4 @@ impl ConnectionSettings {
}); });
}); });
} }
/// Show external connection adding [`Modal`].
fn show_add_ext_conn_modal(&mut self, cb: &dyn PlatformCallbacks) {
self.ext_conn_modal = ExternalConnectionModal::new(None);
// Show modal.
Modal::new(ExternalConnectionModal::WALLET_ID)
.position(ModalPosition::CenterTop)
.title(t!("wallets.add_node"))
.show();
cb.show_keyboard();
}
} }

View file

@ -12,15 +12,9 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
use egui::{Id, Margin, ScrollArea};
use egui::scroll_area::ScrollBarVisibility;
use crate::gui::Colors;
use crate::gui::platform::PlatformCallbacks; use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Content, View}; use crate::gui::views::wallets::{CommonSettings, ConnectionSettings, RecoverySettings};
use crate::gui::views::wallets::settings::{CommonSettings, ConnectionSettings, RecoverySettings};
use crate::gui::views::wallets::types::{WalletTab, WalletTabType}; use crate::gui::views::wallets::types::{WalletTab, WalletTabType};
use crate::gui::views::wallets::WalletContent;
use crate::wallet::Wallet; use crate::wallet::Wallet;
/// Wallet settings tab content. /// Wallet settings tab content.
@ -50,44 +44,13 @@ impl WalletTab for WalletSettings {
fn ui(&mut self, fn ui(&mut self,
ui: &mut egui::Ui, ui: &mut egui::Ui,
wallet: &mut Wallet, wallet: &Wallet,
cb: &dyn PlatformCallbacks) { cb: &dyn PlatformCallbacks) {
// Show loading progress if navigation is blocked.
if WalletContent::block_navigation_on_sync(wallet) {
WalletContent::sync_progress_ui(ui, wallet);
return;
}
// Show settings content panel.
egui::CentralPanel::default()
.frame(egui::Frame {
stroke: View::item_stroke(),
fill: Colors::white_or_black(false),
inner_margin: Margin {
left: View::far_left_inset_margin(ui) + 4.0,
right: View::get_right_inset() + 4.0,
top: 3.0,
bottom: 4.0,
},
..Default::default()
})
.show_inside(ui, |ui| {
ScrollArea::vertical()
.id_source(Id::from("wallet_settings_scroll").with(wallet.get_config().id))
.auto_shrink([false; 2])
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.show(ui, |ui| {
ui.vertical_centered(|ui| {
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
// Show common wallet setup. // Show common wallet setup.
self.common_setup.ui(ui, wallet, cb); self.common_setup.ui(ui, wallet, cb);
// Show wallet connections setup. // Show wallet connections setup.
self.conn_setup.wallet_ui(ui, wallet, cb); self.conn_setup.wallet_ui(ui, wallet, cb);
// Show wallet recovery setup. // Show wallet recovery setup.
self.recovery_setup.ui(ui, wallet, cb); self.recovery_setup.ui(ui, wallet, cb);
});
});
});
});
} }
} }

View file

@ -22,6 +22,7 @@ use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Modal, View}; use crate::gui::views::{Modal, View};
use crate::gui::views::types::{ModalPosition, TextEditOptions}; use crate::gui::views::types::{ModalPosition, TextEditOptions};
use crate::node::Node; use crate::node::Node;
use crate::wallet::types::ConnectionMethod;
use crate::wallet::Wallet; use crate::wallet::Wallet;
/// Wallet recovery settings content. /// Wallet recovery settings content.
@ -51,7 +52,7 @@ impl Default for RecoverySettings {
} }
impl RecoverySettings { impl RecoverySettings {
pub fn ui(&mut self, ui: &mut egui::Ui, wallet: &mut Wallet, cb: &dyn PlatformCallbacks) { pub fn ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) {
// Show modal content for this ui container. // Show modal content for this ui container.
self.modal_content_ui(ui, wallet, cb); self.modal_content_ui(ui, wallet, cb);
@ -63,7 +64,7 @@ impl RecoverySettings {
ui.add_space(4.0); ui.add_space(4.0);
ui.vertical_centered(|ui| { ui.vertical_centered(|ui| {
let integrated_node = wallet.get_current_ext_conn().is_none(); let integrated_node = wallet.get_current_connection() == ConnectionMethod::Integrated;
let integrated_node_ready = Node::get_sync_status() == Some(SyncStatus::NoSync); let integrated_node_ready = Node::get_sync_status() == Some(SyncStatus::NoSync);
if wallet.sync_error() || (integrated_node && !integrated_node_ready) { if wallet.sync_error() || (integrated_node && !integrated_node_ready) {
ui.add_space(2.0); ui.add_space(2.0);
@ -91,9 +92,11 @@ impl RecoverySettings {
ui.add_space(6.0); ui.add_space(6.0);
// Draw button to restore the wallet. // Draw button to restore the wallet.
let recover_text = format!("{} {}", LIFEBUOY, t!("wallets.recover"));
ui.add_space(4.0); ui.add_space(4.0);
View::colored_text_button(ui, recover_text, Colors::green(), Colors::button(), || { View::colored_text_button(ui,
format!("{} {}", LIFEBUOY, t!("wallets.recover")),
Colors::green(),
Colors::white_or_black(false), || {
wallet.delete_db(true); wallet.delete_db(true);
}); });
ui.add_space(6.0); ui.add_space(6.0);
@ -111,7 +114,7 @@ impl RecoverySettings {
// Draw button to show recovery phrase. // Draw button to show recovery phrase.
let show_text = format!("{} {}", EYE, t!("show")); let show_text = format!("{} {}", EYE, t!("show"));
View::button(ui, show_text, Colors::button(), || { View::button(ui, show_text, Colors::white_or_black(false), || {
self.show_recovery_phrase_modal(cb); self.show_recovery_phrase_modal(cb);
}); });
@ -122,11 +125,13 @@ impl RecoverySettings {
ui.add_space(6.0); ui.add_space(6.0);
// Draw button to delete the wallet. // Draw button to delete the wallet.
let delete_text = format!("{} {}", TRASH, t!("wallets.delete")); View::colored_text_button(ui,
View::colored_text_button(ui, delete_text, Colors::red(), Colors::button(), || { format!("{} {}", TRASH, t!("wallets.delete")),
Colors::red(),
Colors::white_or_black(false), || {
Modal::new(DELETE_CONFIRMATION_MODAL) Modal::new(DELETE_CONFIRMATION_MODAL)
.position(ModalPosition::Center) .position(ModalPosition::Center)
.title(t!("modal.confirmation")) .title(t!("confirmation"))
.show(); .show();
}); });
ui.add_space(8.0); ui.add_space(8.0);
@ -136,7 +141,7 @@ impl RecoverySettings {
/// Draw [`Modal`] content for this ui container. /// Draw [`Modal`] content for this ui container.
fn modal_content_ui(&mut self, fn modal_content_ui(&mut self,
ui: &mut egui::Ui, ui: &mut egui::Ui,
wallet: &mut Wallet, wallet: &Wallet,
cb: &dyn PlatformCallbacks) { cb: &dyn PlatformCallbacks) {
match Modal::opened() { match Modal::opened() {
None => {} None => {}
@ -175,7 +180,7 @@ impl RecoverySettings {
/// Draw recovery phrase [`Modal`] content. /// Draw recovery phrase [`Modal`] content.
fn recovery_phrase_modal_ui(&mut self, fn recovery_phrase_modal_ui(&mut self,
ui: &mut egui::Ui, ui: &mut egui::Ui,
wallet: &mut Wallet, wallet: &Wallet,
modal: &Modal, modal: &Modal,
cb: &dyn PlatformCallbacks) { cb: &dyn PlatformCallbacks) {
ui.add_space(6.0); ui.add_space(6.0);
@ -232,7 +237,7 @@ impl RecoverySettings {
}); });
}); });
columns[1].vertical_centered_justified(|ui| { columns[1].vertical_centered_justified(|ui| {
View::button(ui, "OK".to_owned(), Colors::white_or_black(false), || { let mut on_next = || {
match wallet.get_recovery(self.pass_edit.clone()) { match wallet.get_recovery(self.pass_edit.clone()) {
Ok(phrase) => { Ok(phrase) => {
self.wrong_pass = false; self.wrong_pass = false;
@ -243,6 +248,12 @@ impl RecoverySettings {
self.wrong_pass = true; self.wrong_pass = true;
} }
} }
};
View::on_enter_key(ui, || {
(on_next)();
});
View::button(ui, "OK".to_owned(), Colors::white_or_black(false), || {
on_next();
}); });
}); });
}); });
@ -254,7 +265,7 @@ impl RecoverySettings {
/// Draw wallet deletion [`Modal`] content. /// Draw wallet deletion [`Modal`] content.
fn deletion_modal_ui(&mut self, fn deletion_modal_ui(&mut self,
ui: &mut egui::Ui, ui: &mut egui::Ui,
wallet: &mut Wallet, wallet: &Wallet,
modal: &Modal) { modal: &Modal) {
ui.add_space(8.0); ui.add_space(8.0);
ui.vertical_centered(|ui| { ui.vertical_centered(|ui| {

View file

@ -1,944 +0,0 @@
// Copyright 2023 The Grim Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::sync::Arc;
use std::thread;
use egui::{Align, Id, Layout, Margin, RichText, Rounding, ScrollArea};
use egui::os::OperatingSystem;
use egui::scroll_area::ScrollBarVisibility;
use parking_lot::RwLock;
use tor_rtcompat::BlockOn;
use tor_rtcompat::tokio::TokioNativeTlsRuntime;
use grin_core::core::{amount_from_hr_string, amount_to_hr_string};
use grin_wallet_libwallet::SlatepackAddress;
use crate::gui::Colors;
use crate::gui::icons::{CHECK_CIRCLE, COPY, DOTS_THREE_CIRCLE, EXPORT, GEAR_SIX, GLOBE_SIMPLE, POWER, QR_CODE, SHIELD_CHECKERED, SHIELD_SLASH, WARNING_CIRCLE, X_CIRCLE};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{CameraContent, Modal, QrCodeContent, Content, View};
use crate::gui::views::types::{ModalPosition, TextEditOptions};
use crate::gui::views::wallets::wallet::types::{WalletTab, WalletTabType};
use crate::gui::views::wallets::wallet::WalletContent;
use crate::tor::{Tor, TorBridge, TorConfig};
use crate::wallet::types::WalletData;
use crate::wallet::Wallet;
/// Wallet transport tab content.
pub struct WalletTransport {
/// Flag to check if transaction is sending over Tor to show progress at [`Modal`].
tor_sending: Arc<RwLock<bool>>,
/// Flag to check if error occurred during sending of transaction over Tor at [`Modal`].
tor_send_error: Arc<RwLock<bool>>,
/// Flag to check if transaction sent successfully over Tor [`Modal`].
tor_success: Arc<RwLock<bool>>,
/// Entered amount value for [`Modal`].
amount_edit: String,
/// Entered address value for [`Modal`].
address_edit: String,
/// Flag to check if entered address is incorrect at [`Modal`].
address_error: bool,
/// Flag to check if QR code scanner is opened at address [`Modal`].
show_address_scan: bool,
/// Address QR code scanner [`Modal`] content.
address_scan_content: CameraContent,
/// Flag to check if [`Modal`] was just opened to focus on first field.
modal_just_opened: bool,
/// QR code address image [`Modal`] content.
qr_address_content: QrCodeContent,
/// Flag to check if Tor settings were changed.
tor_settings_changed: bool,
/// Tor bridge binary path edit text.
bridge_bin_path_edit: String,
/// Tor bridge connection line edit text.
bridge_conn_line_edit: String,
/// Flag to check if QR code scanner is opened at bridge [`Modal`].
show_bridge_scan: bool,
/// Address QR code scanner [`Modal`] content.
bridge_qr_scan_content: CameraContent,
}
impl WalletTab for WalletTransport {
fn get_type(&self) -> WalletTabType {
WalletTabType::Transport
}
fn ui(&mut self,
ui: &mut egui::Ui,
wallet: &mut Wallet,
cb: &dyn PlatformCallbacks) {
if WalletContent::sync_ui(ui, wallet) {
return;
}
// Show modal content for this ui container.
self.modal_content_ui(ui, wallet, cb);
// Show transport content panel.
egui::CentralPanel::default()
.frame(egui::Frame {
stroke: View::item_stroke(),
fill: Colors::white_or_black(false),
inner_margin: Margin {
left: View::far_left_inset_margin(ui) + 4.0,
right: View::get_right_inset() + 4.0,
top: 3.0,
bottom: 4.0,
},
..Default::default()
})
.show_inside(ui, |ui| {
ScrollArea::vertical()
.id_source(Id::from("wallet_transport").with(wallet.get_config().id))
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.auto_shrink([false; 2])
.show(ui, |ui| {
ui.vertical_centered(|ui| {
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
self.ui(ui, wallet, cb);
});
});
});
});
}
}
/// Identifier for [`Modal`] to send amount over Tor.
const SEND_TOR_MODAL: &'static str = "send_tor_modal";
/// Identifier for [`Modal`] to setup Tor service.
const TOR_SETTINGS_MODAL: &'static str = "tor_settings_modal";
/// Identifier for [`Modal`] to show QR code address image.
const QR_ADDRESS_MODAL: &'static str = "qr_address_modal";
impl WalletTransport {
/// Create new content instance from provided Slatepack address text.
pub fn new(addr: String) -> Self {
// Setup Tor bridge binary path edit text.
let bridge = TorConfig::get_bridge();
let (bin_path, conn_line) = if let Some(b) = bridge {
(b.binary_path(), b.connection_line())
} else {
("".to_string(), "".to_string())
};
Self {
tor_sending: Arc::new(RwLock::new(false)),
tor_send_error: Arc::new(RwLock::new(false)),
tor_success: Arc::new(RwLock::new(false)),
amount_edit: "".to_string(),
address_edit: "".to_string(),
address_error: false,
show_address_scan: false,
address_scan_content: CameraContent::default(),
modal_just_opened: false,
qr_address_content: QrCodeContent::new(addr, false),
tor_settings_changed: false,
bridge_bin_path_edit: bin_path,
bridge_conn_line_edit: conn_line,
show_bridge_scan: false,
bridge_qr_scan_content: CameraContent::default(),
}
}
/// Draw wallet transport content.
pub fn ui(&mut self, ui: &mut egui::Ui, wallet: &mut Wallet, cb: &dyn PlatformCallbacks) {
ui.add_space(3.0);
ui.label(RichText::new(t!("transport.desc"))
.size(16.0)
.color(Colors::inactive_text()));
ui.add_space(7.0);
// Draw Tor content.
self.tor_ui(ui, wallet, cb);
}
/// Draw [`Modal`] content for this ui container.
fn modal_content_ui(&mut self,
ui: &mut egui::Ui,
wallet: &mut Wallet,
cb: &dyn PlatformCallbacks) {
match Modal::opened() {
None => {}
Some(id) => {
match id {
SEND_TOR_MODAL => {
Modal::ui(ui.ctx(), |ui, modal| {
self.send_tor_modal_ui(ui, wallet, modal, cb);
});
}
TOR_SETTINGS_MODAL => {
Modal::ui(ui.ctx(), |ui, modal| {
self.tor_settings_modal_ui(ui, wallet, modal, cb);
});
}
QR_ADDRESS_MODAL => {
Modal::ui(ui.ctx(), |ui, modal| {
self.qr_address_modal_ui(ui, modal, cb);
});
}
_ => {}
}
}
}
}
/// Draw Tor transport content.
fn tor_ui(&mut self, ui: &mut egui::Ui, wallet: &mut Wallet, cb: &dyn PlatformCallbacks) {
let data = wallet.get_data().unwrap();
// Draw header content.
self.tor_header_ui(ui, wallet);
// Draw receive info content.
if wallet.slatepack_address().is_some() {
self.tor_receive_ui(ui, wallet, &data, cb);
}
// Draw send content.
if data.info.amount_currently_spendable > 0 && wallet.foreign_api_port().is_some() {
self.tor_send_ui(ui, cb);
}
}
/// Draw Tor transport header content.
fn tor_header_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet) {
// Setup layout size.
let mut rect = ui.available_rect_before_wrap();
rect.set_height(78.0);
// Draw round background.
let bg_rect = rect.clone();
let item_rounding = View::item_rounding(0, 2, false);
ui.painter().rect(bg_rect, item_rounding, Colors::button(), View::item_stroke());
ui.vertical(|ui| {
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
// Draw button to setup Tor transport.
let button_rounding = View::item_rounding(0, 2, true);
View::item_button(ui, button_rounding, GEAR_SIX, None, || {
self.show_tor_settings_modal();
});
// Draw button to enable/disable Tor listener for current wallet.
let service_id = &wallet.identifier();
if !Tor::is_service_starting(service_id) && wallet.foreign_api_port().is_some() {
if !Tor::is_service_running(service_id) {
View::item_button(ui, Rounding::default(), POWER, Some(Colors::green()), || {
if let Ok(key) = wallet.secret_key() {
let api_port = wallet.foreign_api_port().unwrap();
Tor::start_service(api_port, key, service_id);
}
});
} else {
View::item_button(ui, Rounding::default(), POWER, Some(Colors::red()), || {
Tor::stop_service(service_id);
});
}
}
let layout_size = ui.available_size();
ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| {
ui.add_space(6.0);
ui.vertical(|ui| {
ui.add_space(3.0);
ui.with_layout(Layout::left_to_right(Align::Min), |ui| {
ui.add_space(1.0);
ui.label(RichText::new(t!("transport.tor_network"))
.size(18.0)
.color(Colors::title(false)));
});
// Setup Tor status text.
let is_running = Tor::is_service_running(service_id);
let is_starting = Tor::is_service_starting(service_id);
let has_error = Tor::is_service_failed(service_id);
let (icon, text) = if wallet.foreign_api_port().is_none() {
(DOTS_THREE_CIRCLE, t!("wallets.loading"))
} else if is_starting {
(DOTS_THREE_CIRCLE, t!("transport.connecting"))
} else if has_error {
(WARNING_CIRCLE, t!("transport.conn_error"))
} else if is_running {
(CHECK_CIRCLE, t!("transport.connected"))
} else {
(X_CIRCLE, t!("transport.disconnected"))
};
let status_text = format!("{} {}", icon, text);
ui.label(RichText::new(status_text).size(15.0).color(Colors::text(false)));
ui.add_space(1.0);
// Setup bridges status text.
let bridge = TorConfig::get_bridge();
let bridges_text = match &bridge {
None => {
format!("{} {}", SHIELD_SLASH, t!("transport.bridges_disabled"))
}
Some(b) => {
let name = b.protocol_name().to_uppercase();
format!("{} {}",
SHIELD_CHECKERED,
t!("transport.bridge_name", "b" = name))
}
};
ui.label(RichText::new(bridges_text).size(15.0).color(Colors::gray()));
});
});
});
});
}
/// Show Tor transport settings [`Modal`].
fn show_tor_settings_modal(&mut self) {
self.tor_settings_changed = false;
// Show Tor settings modal.
Modal::new(TOR_SETTINGS_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("transport.tor_settings"))
.closeable(false)
.show();
}
/// Draw Tor transport settings [`Modal`] content.
fn tor_settings_modal_ui(&mut self,
ui: &mut egui::Ui,
wallet: &Wallet,
modal: &Modal,
cb: &dyn PlatformCallbacks) {
ui.add_space(6.0);
// Draw QR code scanner content if requested.
if self.show_bridge_scan {
let mut on_stop = |content: &mut CameraContent| {
cb.stop_camera();
content.clear_state();
modal.enable_closing();
self.show_bridge_scan = false;
};
if let Some(result) = self.bridge_qr_scan_content.qr_scan_result() {
self.bridge_conn_line_edit = result.text();
on_stop(&mut self.bridge_qr_scan_content);
cb.show_keyboard();
} else {
self.bridge_qr_scan_content.ui(ui, cb);
ui.add_space(12.0);
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
// Show buttons to close modal or come back to sending input.
ui.columns(2, |cols| {
cols[0].vertical_centered_justified(|ui| {
View::button(ui, t!("close"), Colors::white_or_black(false), || {
on_stop(&mut self.bridge_qr_scan_content);
modal.close();
});
});
cols[1].vertical_centered_justified(|ui| {
View::button(ui, t!("back"), Colors::white_or_black(false), || {
on_stop(&mut self.bridge_qr_scan_content);
});
});
});
ui.add_space(6.0);
}
return;
}
// Do not show bridges setup on Android.
let os = OperatingSystem::from_target_os();
let show_bridges = os != OperatingSystem::Android;
if show_bridges {
let bridge = TorConfig::get_bridge();
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("transport.bridges_desc"))
.size(17.0)
.color(Colors::inactive_text()));
// Draw checkbox to enable/disable bridges.
View::checkbox(ui, bridge.is_some(), t!("transport.bridges"), || {
// Save value.
let value = if bridge.is_some() {
None
} else {
let default_bridge = TorConfig::get_obfs4();
self.bridge_bin_path_edit = default_bridge.binary_path();
self.bridge_conn_line_edit = default_bridge.connection_line();
Some(default_bridge)
};
TorConfig::save_bridge(value);
self.tor_settings_changed = true;
});
});
// Draw bridges selection and path.
if bridge.is_some() {
let current_bridge = bridge.unwrap();
let mut bridge = current_bridge.clone();
ui.add_space(6.0);
ui.columns(2, |columns| {
columns[0].vertical_centered(|ui| {
// Draw Obfs4 bridge selector.
let obfs4 = TorConfig::get_obfs4();
let name = obfs4.protocol_name().to_uppercase();
View::radio_value(ui, &mut bridge, obfs4, name);
});
columns[1].vertical_centered(|ui| {
// Draw Snowflake bridge selector.
let snowflake = TorConfig::get_snowflake();
let name = snowflake.protocol_name().to_uppercase();
View::radio_value(ui, &mut bridge, snowflake, name);
});
});
ui.add_space(12.0);
// Check if bridge type was changed to save.
if current_bridge != bridge {
self.tor_settings_changed = true;
TorConfig::save_bridge(Some(bridge.clone()));
self.bridge_bin_path_edit = bridge.binary_path();
self.bridge_conn_line_edit = bridge.connection_line();
}
// Draw binary path text edit.
let bin_edit_id = Id::from(modal.id)
.with(wallet.get_config().id)
.with("_bin_edit");
let mut bin_edit_opts = TextEditOptions::new(bin_edit_id)
.paste()
.no_focus();
let bin_edit_before = self.bridge_bin_path_edit.clone();
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("transport.bin_file"))
.size(17.0)
.color(Colors::inactive_text()));
ui.add_space(6.0);
View::text_edit(ui, cb, &mut self.bridge_bin_path_edit, &mut bin_edit_opts);
ui.add_space(6.0);
});
// Draw connection line text edit.
let conn_edit_before = self.bridge_conn_line_edit.clone();
let conn_edit_id = Id::from(modal.id)
.with(wallet.get_config().id)
.with("_conn_edit");
let mut conn_edit_opts = TextEditOptions::new(conn_edit_id)
.paste()
.no_focus()
.scan_qr();
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("transport.conn_line"))
.size(17.0)
.color(Colors::inactive_text()));
ui.add_space(6.0);
View::text_edit(ui, cb, &mut self.bridge_conn_line_edit, &mut conn_edit_opts);
// Check if scan button was pressed.
if conn_edit_opts.scan_pressed {
cb.hide_keyboard();
modal.disable_closing();
conn_edit_opts.scan_pressed = false;
self.show_bridge_scan = true;
}
});
// Check if bin path or connection line text was changed to save bridge.
if conn_edit_before != self.bridge_conn_line_edit ||
bin_edit_before != self.bridge_bin_path_edit {
let bin_path = self.bridge_bin_path_edit.trim().to_string();
let conn_line = self.bridge_conn_line_edit.trim().to_string();
let b = match bridge {
TorBridge::Snowflake(_, _) => {
TorBridge::Snowflake(bin_path, conn_line)
},
TorBridge::Obfs4(_, _) => {
TorBridge::Obfs4(bin_path, conn_line)
}
};
TorConfig::save_bridge(Some(b));
self.tor_settings_changed = true;
}
ui.add_space(2.0);
}
ui.add_space(6.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(6.0);
}
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("transport.tor_autorun_desc"))
.size(17.0)
.color(Colors::inactive_text()));
// Show Tor service autorun checkbox.
let autorun = wallet.auto_start_tor_listener();
View::checkbox(ui, autorun, t!("network.autorun"), || {
wallet.update_auto_start_tor_listener(!autorun);
});
});
ui.add_space(6.0);
ui.vertical_centered_justified(|ui| {
View::button(ui, t!("close"), Colors::white_or_black(false), || {
if self.tor_settings_changed {
self.tor_settings_changed = false;
// Restart running service or rebuild client.
let service_id = &wallet.identifier();
if Tor::is_service_running(service_id) {
if let Ok(key) = wallet.secret_key() {
let api_port = wallet.foreign_api_port().unwrap();
Tor::restart_service(api_port, key, service_id);
}
} else {
Tor::rebuild_client();
}
}
modal.close();
});
});
ui.add_space(6.0);
}
/// Draw Tor receive content.
fn tor_receive_ui(&mut self,
ui: &mut egui::Ui,
wallet: &Wallet,
data: &WalletData,
cb: &dyn PlatformCallbacks) {
let slatepack_addr = wallet.slatepack_address().unwrap();
let service_id = &wallet.identifier();
let can_send = data.info.amount_currently_spendable > 0;
// Setup layout size.
let mut rect = ui.available_rect_before_wrap();
rect.set_height(52.0);
// Draw round background.
let bg_rect = rect.clone();
let item_rounding = if can_send {
View::item_rounding(1, 3, false)
} else {
View::item_rounding(1, 2, false)
};
ui.painter().rect(bg_rect, item_rounding, Colors::button(), View::item_stroke());
ui.vertical(|ui| {
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
// Draw button to setup Tor transport.
let button_rounding = if can_send {
View::item_rounding(1, 3, true)
} else {
View::item_rounding(1, 2, true)
};
View::item_button(ui, button_rounding, QR_CODE, None, || {
// Show QR code image address modal.
self.qr_address_content.clear_state();
Modal::new(QR_ADDRESS_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("network_mining.address"))
.show();
});
// Show button to enable/disable Tor listener for current wallet.
View::item_button(ui, Rounding::default(), COPY, None, || {
cb.copy_string_to_buffer(slatepack_addr.clone());
});
let layout_size = ui.available_size();
ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| {
ui.add_space(6.0);
ui.vertical(|ui| {
ui.add_space(3.0);
// Show wallet Slatepack address.
let address_color = if Tor::is_service_starting(service_id) ||
wallet.foreign_api_port().is_none() {
Colors::inactive_text()
} else if Tor::is_service_running(service_id) {
Colors::green()
} else {
Colors::red()
};
View::ellipsize_text(ui, slatepack_addr, 15.0, address_color);
let address_label = format!("{} {}",
GLOBE_SIMPLE,
t!("network_mining.address"));
ui.label(RichText::new(address_label).size(15.0).color(Colors::gray()));
});
});
});
});
}
/// Draw QR code image address [`Modal`] content.
fn qr_address_modal_ui(&mut self, ui: &mut egui::Ui, m: &Modal, cb: &dyn PlatformCallbacks) {
ui.add_space(6.0);
// Draw QR code content.
let text = self.qr_address_content.text.clone();
self.qr_address_content.ui(ui, text.clone(), cb);
ui.vertical_centered_justified(|ui| {
View::button(ui, t!("close"), Colors::white_or_black(false), || {
self.qr_address_content.clear_state();
m.close();
});
});
ui.add_space(6.0);
}
/// Draw Tor send content.
fn tor_send_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
// Setup layout size.
let mut rect = ui.available_rect_before_wrap();
rect.set_height(55.0);
// Draw round background.
let bg_rect = rect.clone();
let item_rounding = View::item_rounding(1, 2, false);
ui.painter().rect(bg_rect, item_rounding, Colors::fill(), View::item_stroke());
ui.vertical(|ui| {
ui.allocate_ui_with_layout(rect.size(), Layout::top_down(Align::Center), |ui| {
ui.add_space(7.0);
// Draw button to open sending modal.
let send_text = format!("{} {}", EXPORT, t!("wallets.send"));
View::button(ui, send_text, Colors::white_or_black(false), || {
self.show_send_tor_modal(cb, None);
});
});
});
}
/// Show [`Modal`] to send over Tor.
pub fn show_send_tor_modal(&mut self, cb: &dyn PlatformCallbacks, address: Option<String>) {
{
let mut w_send_err = self.tor_send_error.write();
*w_send_err = false;
let mut w_sending = self.tor_sending.write();
*w_sending = false;
let mut w_success = self.tor_success.write();
*w_success = false;
}
self.modal_just_opened = true;
self.amount_edit = "".to_string();
self.address_edit = address.unwrap_or("".to_string());
self.address_error = false;
// Show modal.
Modal::new(SEND_TOR_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("wallets.send"))
.show();
cb.show_keyboard();
}
/// Check if error occurred during sending over Tor at [`Modal`].
fn has_tor_send_error(&self) -> bool {
let r_send_err = self.tor_send_error.read();
r_send_err.clone()
}
/// Check if transaction is sending over Tor to show progress at [`Modal`].
fn tor_sending(&self) -> bool {
let r_sending = self.tor_sending.read();
r_sending.clone()
}
/// Check if transaction sent over Tor with success at [`Modal`].
fn tor_success(&self) -> bool {
let r_success = self.tor_success.read();
r_success.clone()
}
/// Draw amount input [`Modal`] content to send over Tor.
fn send_tor_modal_ui(&mut self,
ui: &mut egui::Ui,
wallet: &mut Wallet,
modal: &Modal,
cb: &dyn PlatformCallbacks) {
ui.add_space(6.0);
let has_send_err = self.has_tor_send_error();
let sending = self.tor_sending();
if !has_send_err && !sending {
// Draw QR code scanner content if requested.
if self.show_address_scan {
let mut on_stop = |content: &mut CameraContent| {
cb.stop_camera();
content.clear_state();
modal.enable_closing();
self.show_address_scan = false;
};
if let Some(result) = self.address_scan_content.qr_scan_result() {
self.address_edit = result.text();
self.modal_just_opened = true;
on_stop(&mut self.address_scan_content);
cb.show_keyboard();
} else {
self.address_scan_content.ui(ui, cb);
ui.add_space(6.0);
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
// Show buttons to close modal or come back to sending input.
ui.columns(2, |cols| {
cols[0].vertical_centered_justified(|ui| {
View::button(ui, t!("close"), Colors::white_or_black(false), || {
on_stop(&mut self.address_scan_content);
modal.close();
});
});
cols[1].vertical_centered_justified(|ui| {
View::button(ui, t!("back"), Colors::white_or_black(false), || {
self.modal_just_opened = true;
on_stop(&mut self.address_scan_content);
cb.show_keyboard();
});
});
});
ui.add_space(6.0);
}
return;
}
ui.vertical_centered(|ui| {
let data = wallet.get_data().unwrap();
let amount = amount_to_hr_string(data.info.amount_currently_spendable, true);
let enter_text = t!("wallets.enter_amount_send","amount" => amount);
ui.label(RichText::new(enter_text)
.size(17.0)
.color(Colors::gray()));
});
ui.add_space(8.0);
// Draw amount text edit.
let amount_edit_id = Id::from(modal.id).with("amount").with(wallet.get_config().id);
let mut amount_edit_opts = TextEditOptions::new(amount_edit_id).h_center().no_focus();
let amount_edit_before = self.amount_edit.clone();
if self.modal_just_opened {
self.modal_just_opened = false;
amount_edit_opts.focus = true;
}
View::text_edit(ui, cb, &mut self.amount_edit, &mut amount_edit_opts);
ui.add_space(8.0);
// Check value if input was changed.
if amount_edit_before != self.amount_edit {
if !self.amount_edit.is_empty() {
// Trim text, replace "," by "." and parse amount.
self.amount_edit = self.amount_edit.trim().replace(",", ".");
match amount_from_hr_string(self.amount_edit.as_str()) {
Ok(a) => {
if !self.amount_edit.contains(".") {
// To avoid input of several "0".
if a == 0 {
self.amount_edit = "0".to_string();
return;
}
} else {
// Check input after ".".
let parts = self.amount_edit.split(".").collect::<Vec<&str>>();
if parts.len() == 2 && parts[1].len() > 9 {
self.amount_edit = amount_edit_before;
return;
}
}
// Do not input amount more than balance in sending.
let b = wallet.get_data().unwrap().info.amount_currently_spendable;
if b < a {
self.amount_edit = amount_edit_before;
}
}
Err(_) => {
self.amount_edit = amount_edit_before;
}
}
}
}
// Show address error or input description.
ui.vertical_centered(|ui| {
if self.address_error {
ui.label(RichText::new(t!("transport.incorrect_addr_err"))
.size(17.0)
.color(Colors::red()));
} else {
ui.label(RichText::new(t!("transport.receiver_address"))
.size(17.0)
.color(Colors::gray()));
}
});
ui.add_space(6.0);
// Draw address text edit.
let addr_edit_before = self.address_edit.clone();
let address_edit_id = Id::from(modal.id).with("address").with(wallet.get_config().id);
let mut address_edit_opts = TextEditOptions::new(address_edit_id)
.paste()
.no_focus()
.scan_qr();
View::text_edit(ui, cb, &mut self.address_edit, &mut address_edit_opts);
// Check if scan button was pressed.
if address_edit_opts.scan_pressed {
cb.hide_keyboard();
modal.disable_closing();
address_edit_opts.scan_pressed = false;
self.show_address_scan = true;
}
ui.add_space(12.0);
// Check value if input was changed.
if addr_edit_before != self.address_edit {
self.address_error = false;
}
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
self.amount_edit = "".to_string();
self.address_edit = "".to_string();
cb.hide_keyboard();
modal.close();
});
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("continue"), Colors::white_or_black(false), || {
if self.amount_edit.is_empty() {
return;
}
// Check entered address.
let addr_str = self.address_edit.as_str();
if let Ok(addr) = SlatepackAddress::try_from(addr_str) {
// Parse amount and send over Tor.
if let Ok(a) = amount_from_hr_string(self.amount_edit.as_str()) {
cb.hide_keyboard();
modal.disable_closing();
let mut w_sending = self.tor_sending.write();
*w_sending = true;
{
let send_error = self.tor_send_error.clone();
let send_success = self.tor_success.clone();
let mut wallet = wallet.clone();
thread::spawn(move || {
let runtime = TokioNativeTlsRuntime::create().unwrap();
runtime
.block_on(async {
if wallet.send_tor(a, &addr)
.await
.is_some() {
let mut w_send_success = send_success.write();
*w_send_success = true;
} else {
let mut w_send_error = send_error.write();
*w_send_error = true;
}
});
});
}
}
} else {
self.address_error = true;
}
});
});
});
ui.add_space(6.0);
} else if has_send_err {
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("transport.tor_send_error"))
.size(17.0)
.color(Colors::red()));
});
ui.add_space(12.0);
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
self.amount_edit = "".to_string();
self.address_edit = "".to_string();
cb.hide_keyboard();
modal.close();
});
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("repeat"), Colors::white_or_black(false), || {
// Parse amount and send over Tor.
if let Ok(a) = amount_from_hr_string(self.amount_edit.as_str()) {
let mut w_send_error = self.tor_send_error.write();
*w_send_error = false;
let mut w_sending = self.tor_sending.write();
*w_sending = true;
{
let addr_text = self.address_edit.clone();
let send_error = self.tor_send_error.clone();
let send_success = self.tor_success.clone();
let mut wallet = wallet.clone();
thread::spawn(move || {
let runtime = TokioNativeTlsRuntime::create().unwrap();
runtime
.block_on(async {
let addr_str = addr_text.as_str();
let addr = &SlatepackAddress::try_from(addr_str)
.unwrap();
if wallet.send_tor(a, &addr)
.await
.is_some() {
let mut w_send_success = send_success.write();
*w_send_success = true;
} else {
let mut w_send_error = send_error.write();
*w_send_error = true;
}
});
});
}
}
});
});
});
ui.add_space(6.0);
} else {
ui.add_space(16.0);
ui.vertical_centered(|ui| {
View::small_loading_spinner(ui);
ui.add_space(12.0);
ui.label(RichText::new(t!("transport.tor_sending", "amount" => self.amount_edit))
.size(17.0)
.color(Colors::gray()));
});
ui.add_space(10.0);
// Close modal on success sending.
if self.tor_success() {
modal.close();
}
}
}
}

View file

@ -0,0 +1,364 @@
// Copyright 2023 The Grim Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use egui::{Align, Layout, RichText, Rounding};
use crate::gui::Colors;
use crate::gui::icons::{CHECK_CIRCLE, COPY, DOTS_THREE_CIRCLE, EXPORT, GEAR_SIX, GLOBE_SIMPLE, POWER, QR_CODE, SHIELD_CHECKERED, SHIELD_SLASH, WARNING_CIRCLE, X_CIRCLE};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Modal, QrCodeContent, View};
use crate::gui::views::types::ModalPosition;
use crate::gui::views::wallets::wallet::transport::send::TransportSendModal;
use crate::gui::views::wallets::wallet::transport::settings::TransportSettingsModal;
use crate::gui::views::wallets::wallet::types::{WalletTab, WalletTabType};
use crate::tor::{Tor, TorConfig};
use crate::wallet::types::WalletData;
use crate::wallet::Wallet;
/// Wallet transport tab content.
pub struct WalletTransport {
/// Sending [`Modal`] content.
send_modal_content: Option<TransportSendModal>,
/// QR code address image [`Modal`] content.
qr_address_content: Option<QrCodeContent>,
/// Tor settings [`Modal`] content.
settings_modal_content: Option<TransportSettingsModal>,
}
impl WalletTab for WalletTransport {
fn get_type(&self) -> WalletTabType {
WalletTabType::Transport
}
fn ui(&mut self,
ui: &mut egui::Ui,
wallet: &Wallet,
cb: &dyn PlatformCallbacks) {
self.modal_content_ui(ui, wallet, cb);
self.transport_ui(ui, wallet, cb);
}
}
/// Identifier for [`Modal`] to send amount over Tor.
const SEND_TOR_MODAL: &'static str = "send_tor_modal";
/// Identifier for [`Modal`] to setup Tor service.
const TOR_SETTINGS_MODAL: &'static str = "tor_settings_modal";
/// Identifier for [`Modal`] to show QR code address image.
const QR_ADDRESS_MODAL: &'static str = "qr_address_modal";
impl Default for WalletTransport {
fn default() -> Self {
Self {
send_modal_content: None,
qr_address_content: None,
settings_modal_content: None,
}
}
}
impl WalletTransport {
/// Draw wallet transport content.
fn transport_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) {
ui.add_space(3.0);
ui.label(RichText::new(t!("transport.desc"))
.size(16.0)
.color(Colors::inactive_text()));
ui.add_space(7.0);
// Draw Tor transport content.
self.tor_ui(ui, wallet, cb);
}
/// Draw [`Modal`] content for this ui container.
fn modal_content_ui(&mut self,
ui: &mut egui::Ui,
wallet: &Wallet,
cb: &dyn PlatformCallbacks) {
match Modal::opened() {
None => {}
Some(id) => {
match id {
SEND_TOR_MODAL => {
if let Some(content) = self.send_modal_content.as_mut() {
Modal::ui(ui.ctx(), |ui, modal| {
content.ui(ui, wallet, modal, cb);
});
}
}
TOR_SETTINGS_MODAL => {
if let Some(content) = self.settings_modal_content.as_mut() {
Modal::ui(ui.ctx(), |ui, modal| {
content.ui(ui, wallet, modal, cb);
});
}
}
QR_ADDRESS_MODAL => {
Modal::ui(ui.ctx(), |ui, modal| {
self.qr_address_modal_ui(ui, modal, cb);
});
}
_ => {}
}
}
}
}
/// Draw Tor transport content.
fn tor_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) {
let data = wallet.get_data().unwrap();
// Draw header content.
self.tor_header_ui(ui, wallet);
// Draw receive info content.
if wallet.slatepack_address().is_some() {
self.tor_receive_ui(ui, wallet, &data, cb);
}
// Draw send content.
let service_id = &wallet.identifier();
if data.info.amount_currently_spendable > 0 && wallet.foreign_api_port().is_some() &&
!Tor::is_service_starting(service_id) {
self.tor_send_ui(ui, cb);
}
}
/// Draw Tor transport header content.
fn tor_header_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet) {
// Setup layout size.
let mut rect = ui.available_rect_before_wrap();
rect.set_height(78.0);
// Draw round background.
let bg_rect = rect.clone();
let item_rounding = View::item_rounding(0, 2, false);
ui.painter().rect(bg_rect, item_rounding, Colors::fill_lite(), View::item_stroke());
ui.vertical(|ui| {
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
// Draw button to setup Tor transport.
let button_rounding = View::item_rounding(0, 2, true);
View::item_button(ui, button_rounding, GEAR_SIX, None, || {
self.settings_modal_content = Some(TransportSettingsModal::default());
// Show Tor settings modal.
Modal::new(TOR_SETTINGS_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("transport.tor_settings"))
.closeable(false)
.show();
});
// Draw button to enable/disable Tor listener for current wallet.
let service_id = &wallet.identifier();
if !Tor::is_service_starting(service_id) && wallet.foreign_api_port().is_some() {
if !Tor::is_service_running(service_id) {
View::item_button(ui, Rounding::default(), POWER, Some(Colors::green()), || {
if let Ok(key) = wallet.secret_key() {
let api_port = wallet.foreign_api_port().unwrap();
Tor::start_service(api_port, key, service_id);
}
});
} else {
View::item_button(ui, Rounding::default(), POWER, Some(Colors::red()), || {
Tor::stop_service(service_id);
});
}
}
let layout_size = ui.available_size();
ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| {
ui.add_space(6.0);
ui.vertical(|ui| {
ui.add_space(3.0);
ui.with_layout(Layout::left_to_right(Align::Min), |ui| {
ui.add_space(1.0);
ui.label(RichText::new(t!("transport.tor_network"))
.size(18.0)
.color(Colors::title(false)));
});
// Setup Tor status text.
let is_running = Tor::is_service_running(service_id);
let is_starting = Tor::is_service_starting(service_id);
let has_error = Tor::is_service_failed(service_id);
let (icon, text) = if wallet.foreign_api_port().is_none() {
(DOTS_THREE_CIRCLE, t!("wallets.loading"))
} else if is_starting {
(DOTS_THREE_CIRCLE, t!("transport.connecting"))
} else if has_error {
(WARNING_CIRCLE, t!("transport.conn_error"))
} else if is_running {
(CHECK_CIRCLE, t!("transport.connected"))
} else {
(X_CIRCLE, t!("transport.disconnected"))
};
let status_text = format!("{} {}", icon, text);
ui.label(RichText::new(status_text).size(15.0).color(Colors::text(false)));
ui.add_space(1.0);
// Setup bridges status text.
let bridge = TorConfig::get_bridge();
let bridges_text = match &bridge {
None => {
format!("{} {}", SHIELD_SLASH, t!("transport.bridges_disabled"))
}
Some(b) => {
let name = b.protocol_name().to_uppercase();
format!("{} {}",
SHIELD_CHECKERED,
t!("transport.bridge_name", "b" = name))
}
};
ui.label(RichText::new(bridges_text).size(15.0).color(Colors::gray()));
});
});
});
});
}
/// Draw Tor receive content.
fn tor_receive_ui(&mut self,
ui: &mut egui::Ui,
wallet: &Wallet,
data: &WalletData,
cb: &dyn PlatformCallbacks) {
let addr = wallet.slatepack_address().unwrap();
let service_id = &wallet.identifier();
let can_send = data.info.amount_currently_spendable > 0;
// Setup layout size.
let mut rect = ui.available_rect_before_wrap();
rect.set_height(52.0);
// Draw round background.
let bg_rect = rect.clone();
let item_rounding = if can_send {
View::item_rounding(1, 3, false)
} else {
View::item_rounding(1, 2, false)
};
ui.painter().rect(bg_rect, item_rounding, Colors::fill_lite(), View::item_stroke());
ui.vertical(|ui| {
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
// Draw button to setup Tor transport.
let button_rounding = if can_send {
View::item_rounding(1, 3, true)
} else {
View::item_rounding(1, 2, true)
};
View::item_button(ui, button_rounding, QR_CODE, None, || {
// Show QR code image address modal.
self.qr_address_content = Some(QrCodeContent::new(addr.clone(), false));
Modal::new(QR_ADDRESS_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("network_mining.address"))
.show();
});
// Show button to enable/disable Tor listener for current wallet.
View::item_button(ui, Rounding::default(), COPY, None, || {
cb.copy_string_to_buffer(addr.clone());
});
let layout_size = ui.available_size();
ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| {
ui.add_space(6.0);
ui.vertical(|ui| {
ui.add_space(3.0);
// Show wallet Slatepack address.
let address_color = if Tor::is_service_starting(service_id) ||
wallet.foreign_api_port().is_none() {
Colors::inactive_text()
} else if Tor::is_service_running(service_id) {
Colors::green()
} else {
Colors::red()
};
View::ellipsize_text(ui, addr, 15.0, address_color);
let address_label = format!("{} {}",
GLOBE_SIMPLE,
t!("network_mining.address"));
ui.label(RichText::new(address_label).size(15.0).color(Colors::gray()));
});
});
});
});
}
/// Draw QR code image address [`Modal`] content.
fn qr_address_modal_ui(&mut self,
ui: &mut egui::Ui,
modal: &Modal,
cb: &dyn PlatformCallbacks) {
ui.add_space(6.0);
// Draw QR code content.
if let Some(content) = self.qr_address_content.as_mut() {
content.ui(ui, cb);
} else {
modal.close();
return;
}
ui.vertical_centered_justified(|ui| {
View::button(ui, t!("close"), Colors::white_or_black(false), || {
self.qr_address_content = None;
modal.close();
});
});
ui.add_space(6.0);
}
/// Draw Tor send content.
fn tor_send_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
// Setup layout size.
let mut rect = ui.available_rect_before_wrap();
rect.set_height(55.0);
// Draw round background.
let bg_rect = rect.clone();
let item_rounding = View::item_rounding(1, 2, false);
ui.painter().rect(bg_rect, item_rounding, Colors::fill(), View::item_stroke());
ui.vertical(|ui| {
ui.allocate_ui_with_layout(rect.size(), Layout::top_down(Align::Center), |ui| {
ui.add_space(7.0);
// Draw button to open sending modal.
let send_text = format!("{} {}", EXPORT, t!("wallets.send"));
View::button(ui, send_text, Colors::white_or_black(false), || {
self.show_send_tor_modal(cb, None);
});
});
});
}
/// Show [`Modal`] to send over Tor.
pub fn show_send_tor_modal(&mut self, cb: &dyn PlatformCallbacks, address: Option<String>) {
self.send_modal_content = Some(TransportSendModal::new(address));
// Show modal.
Modal::new(SEND_TOR_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("wallets.send"))
.show();
cb.show_keyboard();
}
}

View file

@ -0,0 +1,19 @@
// Copyright 2024 The Grim Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
mod content;
pub use content::*;
mod send;
mod settings;

View file

@ -0,0 +1,363 @@
// Copyright 2024 The Grim Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::sync::Arc;
use std::thread;
use egui::{Id, RichText};
use grin_core::core::{amount_from_hr_string, amount_to_hr_string};
use grin_wallet_libwallet::{Error, SlatepackAddress};
use parking_lot::RwLock;
use tor_rtcompat::BlockOn;
use tor_rtcompat::tokio::TokioNativeTlsRuntime;
use crate::gui::Colors;
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{CameraContent, Modal, View};
use crate::gui::views::types::TextEditOptions;
use crate::gui::views::wallets::wallet::WalletTransactionModal;
use crate::wallet::types::WalletTransaction;
use crate::wallet::Wallet;
/// Transport sending [`Modal`] content.
pub struct TransportSendModal {
/// Flag to focus on first input field after opening.
first_draw: bool,
/// Flag to check if transaction is sending to show progress.
sending: bool,
/// Flag to check if there is an error to repeat.
error: bool,
/// Transaction result.
send_result: Arc<RwLock<Option<Result<WalletTransaction, Error>>>>,
/// Entered amount value.
amount_edit: String,
/// Entered address value.
address_edit: String,
/// Flag to check if entered address is incorrect.
address_error: bool,
/// Address QR code scanner content.
address_scan_content: Option<CameraContent>,
/// Transaction information content.
tx_info_content: Option<WalletTransactionModal>,
}
impl TransportSendModal {
/// Create new instance from provided address.
pub fn new(addr: Option<String>) -> Self {
Self {
first_draw: true,
sending: false,
error: false,
send_result: Arc::new(RwLock::new(None)),
amount_edit: "".to_string(),
address_edit: addr.unwrap_or("".to_string()),
address_error: false,
address_scan_content: None,
tx_info_content: None,
}
}
/// Draw [`Modal`] content.
pub fn ui(&mut self,
ui: &mut egui::Ui,
wallet: &Wallet,
modal: &Modal,
cb: &dyn PlatformCallbacks) {
// Draw transaction information on request result.
if let Some(tx) = self.tx_info_content.as_mut() {
tx.ui(ui, wallet, modal, cb);
return;
}
// Draw sending content, progress or an error.
if self.sending {
self.progress_ui(ui, wallet);
} else if self.error {
self.error_ui(ui, wallet, modal, cb);
} else {
self.content_ui(ui, wallet, modal, cb);
}
}
/// Draw content to send.
fn content_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, modal: &Modal,
cb: &dyn PlatformCallbacks) {
ui.add_space(6.0);
// Draw QR code scanner content if requested.
if let Some(scanner) = self.address_scan_content.as_mut() {
let mut on_stop = || {
self.first_draw = true;
cb.stop_camera();
modal.enable_closing();
};
if let Some(result) = scanner.qr_scan_result() {
self.address_edit = result.text();
on_stop();
self.address_scan_content = None;
cb.show_keyboard();
} else {
scanner.ui(ui, cb);
ui.add_space(6.0);
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
// Show buttons to close modal or come back to sending input.
ui.columns(2, |cols| {
cols[0].vertical_centered_justified(|ui| {
View::button(ui, t!("close"), Colors::white_or_black(false), || {
on_stop();
self.address_scan_content = None;
modal.close();
});
});
cols[1].vertical_centered_justified(|ui| {
View::button(ui, t!("back"), Colors::white_or_black(false), || {
on_stop();
self.address_scan_content = None;
cb.show_keyboard();
});
});
});
ui.add_space(6.0);
}
return;
}
ui.vertical_centered(|ui| {
let data = wallet.get_data().unwrap();
let amount = amount_to_hr_string(data.info.amount_currently_spendable, true);
let enter_text = t!("wallets.enter_amount_send","amount" => amount);
ui.label(RichText::new(enter_text)
.size(17.0)
.color(Colors::gray()));
});
ui.add_space(8.0);
// Draw amount text edit.
let amount_edit_id = Id::from(modal.id).with("amount").with(wallet.get_config().id);
let mut amount_edit_opts = TextEditOptions::new(amount_edit_id).h_center().no_focus();
let amount_edit_before = self.amount_edit.clone();
if self.first_draw {
self.first_draw = false;
amount_edit_opts.focus = true;
}
View::text_edit(ui, cb, &mut self.amount_edit, &mut amount_edit_opts);
ui.add_space(8.0);
// Check value if input was changed.
if amount_edit_before != self.amount_edit {
if !self.amount_edit.is_empty() {
// Trim text, replace "," by "." and parse amount.
self.amount_edit = self.amount_edit.trim().replace(",", ".");
match amount_from_hr_string(self.amount_edit.as_str()) {
Ok(a) => {
if !self.amount_edit.contains(".") {
// To avoid input of several "0".
if a == 0 {
self.amount_edit = "0".to_string();
return;
}
} else {
// Check input after ".".
let parts = self.amount_edit.split(".").collect::<Vec<&str>>();
if parts.len() == 2 && parts[1].len() > 9 {
self.amount_edit = amount_edit_before;
return;
}
}
// Do not input amount more than balance in sending.
let b = wallet.get_data().unwrap().info.amount_currently_spendable;
if b < a {
self.amount_edit = amount_edit_before;
}
}
Err(_) => {
self.amount_edit = amount_edit_before;
}
}
}
}
// Show address error or input description.
ui.vertical_centered(|ui| {
if self.address_error {
ui.label(RichText::new(t!("transport.incorrect_addr_err"))
.size(17.0)
.color(Colors::red()));
} else {
ui.label(RichText::new(t!("transport.receiver_address"))
.size(17.0)
.color(Colors::gray()));
}
});
ui.add_space(6.0);
// Draw address text edit.
let addr_edit_before = self.address_edit.clone();
let address_edit_id = Id::from(modal.id).with("_address").with(wallet.get_config().id);
let mut address_edit_opts = TextEditOptions::new(address_edit_id)
.paste()
.no_focus()
.scan_qr();
View::text_edit(ui, cb, &mut self.address_edit, &mut address_edit_opts);
// Check if scan button was pressed.
if address_edit_opts.scan_pressed {
cb.hide_keyboard();
modal.disable_closing();
address_edit_opts.scan_pressed = false;
self.address_scan_content = Some(CameraContent::default());
}
ui.add_space(12.0);
// Check value if input was changed.
if addr_edit_before != self.address_edit {
self.address_error = false;
}
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
self.close(modal, cb);
});
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("continue"), Colors::white_or_black(false), || {
self.send(wallet, modal, cb);
});
});
});
ui.add_space(6.0);
}
/// Draw error content.
fn error_ui(&mut self,
ui: &mut egui::Ui,
wallet: &Wallet,
modal: &Modal,
cb: &dyn PlatformCallbacks) {
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("transport.tor_send_error"))
.size(17.0)
.color(Colors::red()));
});
ui.add_space(12.0);
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
self.close(modal, cb);
});
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("repeat"), Colors::white_or_black(false), || {
self.send(wallet, modal, cb);
});
});
});
ui.add_space(6.0);
}
/// Close modal and clear data.
fn close(&mut self, modal: &Modal, cb: &dyn PlatformCallbacks) {
self.amount_edit = "".to_string();
self.address_edit = "".to_string();
let mut w_res = self.send_result.write();
*w_res = None;
self.tx_info_content = None;
self.address_scan_content = None;
cb.hide_keyboard();
modal.close();
}
/// Send entered amount to address.
fn send(&mut self, wallet: &Wallet, modal: &Modal, cb: &dyn PlatformCallbacks) {
if self.amount_edit.is_empty() {
return;
}
let addr_str = self.address_edit.as_str();
if let Ok(addr) = SlatepackAddress::try_from(addr_str) {
if let Ok(a) = amount_from_hr_string(self.amount_edit.as_str()) {
cb.hide_keyboard();
modal.disable_closing();
// Send amount over Tor.
let mut wallet = wallet.clone();
let res = self.send_result.clone();
self.sending = true;
thread::spawn(move || {
let runtime = TokioNativeTlsRuntime::create().unwrap();
runtime
.block_on(async {
let result = wallet.send_tor(a, &addr).await;
let mut w_res = res.write();
*w_res = Some(result);
});
});
}
} else {
self.address_error = true;
}
}
/// Draw sending progress content.
fn progress_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet) {
ui.add_space(16.0);
ui.vertical_centered(|ui| {
View::small_loading_spinner(ui);
ui.add_space(12.0);
ui.label(RichText::new(t!("transport.tor_sending", "amount" => self.amount_edit))
.size(17.0)
.color(Colors::gray()));
});
ui.add_space(10.0);
// Check sending result.
let has_result = {
let r_result = self.send_result.read();
r_result.is_some()
};
if has_result {
{
let res = self.send_result.read().clone().unwrap();
match res {
Ok(tx) => {
self.tx_info_content =
Some(WalletTransactionModal::new(wallet, &tx, false));
}
Err(_) => {
self.error = true;
}
}
}
let mut w_res = self.send_result.write();
*w_res = None;
self.sending = false;
}
}
}

View file

@ -0,0 +1,258 @@
// Copyright 2024 The Grim Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use egui::os::OperatingSystem;
use egui::{Id, RichText};
use crate::gui::Colors;
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{CameraContent, Modal, View};
use crate::gui::views::types::TextEditOptions;
use crate::tor::{Tor, TorBridge, TorConfig};
use crate::wallet::Wallet;
/// Transport settings [`Modal`] content.
pub struct TransportSettingsModal {
/// Flag to check if Tor settings were changed.
settings_changed: bool,
/// Tor bridge binary path edit text.
bridge_bin_path_edit: String,
/// Tor bridge connection line edit text.
bridge_conn_line_edit: String,
/// Address QR code scanner [`Modal`] content.
bridge_qr_scan_content: Option<CameraContent>,
}
impl Default for TransportSettingsModal {
fn default() -> Self {
// Setup Tor bridge binary path edit text.
let bridge = TorConfig::get_bridge();
let (bin_path, conn_line) = if let Some(b) = bridge {
(b.binary_path(), b.connection_line())
} else {
("".to_string(), "".to_string())
};
Self {
settings_changed: false,
bridge_bin_path_edit: bin_path,
bridge_conn_line_edit: conn_line,
bridge_qr_scan_content: None,
}
}
}
impl TransportSettingsModal {
pub fn ui(&mut self,
ui: &mut egui::Ui,
wallet: &Wallet,
modal: &Modal,
cb: &dyn PlatformCallbacks) {
ui.add_space(6.0);
// Draw QR code scanner content if requested.
if let Some(scanner) = self.bridge_qr_scan_content.as_mut() {
let on_stop = || {
cb.stop_camera();
modal.enable_closing();
};
if let Some(result) = scanner.qr_scan_result() {
self.bridge_conn_line_edit = result.text();
on_stop();
self.bridge_qr_scan_content = None;
cb.show_keyboard();
} else {
scanner.ui(ui, cb);
ui.add_space(12.0);
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
// Show buttons to close modal or come back to sending input.
ui.columns(2, |cols| {
cols[0].vertical_centered_justified(|ui| {
View::button(ui, t!("close"), Colors::white_or_black(false), || {
on_stop();
self.bridge_qr_scan_content = None;
modal.close();
});
});
cols[1].vertical_centered_justified(|ui| {
View::button(ui, t!("back"), Colors::white_or_black(false), || {
on_stop();
self.bridge_qr_scan_content = None;
});
});
});
ui.add_space(6.0);
}
return;
}
// Do not show bridges setup on Android.
let os = OperatingSystem::from_target_os();
let show_bridges = os != OperatingSystem::Android;
if show_bridges {
let bridge = TorConfig::get_bridge();
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("transport.bridges_desc"))
.size(17.0)
.color(Colors::inactive_text()));
// Draw checkbox to enable/disable bridges.
View::checkbox(ui, bridge.is_some(), t!("transport.bridges"), || {
// Save value.
let value = if bridge.is_some() {
None
} else {
let default_bridge = TorConfig::get_obfs4();
self.bridge_bin_path_edit = default_bridge.binary_path();
self.bridge_conn_line_edit = default_bridge.connection_line();
Some(default_bridge)
};
TorConfig::save_bridge(value);
self.settings_changed = true;
});
});
// Draw bridges selection and path.
if bridge.is_some() {
let current_bridge = bridge.unwrap();
let mut bridge = current_bridge.clone();
ui.add_space(6.0);
ui.columns(2, |columns| {
columns[0].vertical_centered(|ui| {
// Draw Obfs4 bridge selector.
let obfs4 = TorConfig::get_obfs4();
let name = obfs4.protocol_name().to_uppercase();
View::radio_value(ui, &mut bridge, obfs4, name);
});
columns[1].vertical_centered(|ui| {
// Draw Snowflake bridge selector.
let snowflake = TorConfig::get_snowflake();
let name = snowflake.protocol_name().to_uppercase();
View::radio_value(ui, &mut bridge, snowflake, name);
});
});
ui.add_space(12.0);
// Check if bridge type was changed to save.
if current_bridge != bridge {
self.settings_changed = true;
TorConfig::save_bridge(Some(bridge.clone()));
self.bridge_bin_path_edit = bridge.binary_path();
self.bridge_conn_line_edit = bridge.connection_line();
}
// Draw binary path text edit.
let bin_edit_id = Id::from(modal.id)
.with(wallet.get_config().id)
.with("_bin_edit");
let mut bin_edit_opts = TextEditOptions::new(bin_edit_id)
.paste()
.no_focus();
let bin_edit_before = self.bridge_bin_path_edit.clone();
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("transport.bin_file"))
.size(17.0)
.color(Colors::inactive_text()));
ui.add_space(6.0);
View::text_edit(ui, cb, &mut self.bridge_bin_path_edit, &mut bin_edit_opts);
ui.add_space(6.0);
});
// Draw connection line text edit.
let conn_edit_before = self.bridge_conn_line_edit.clone();
let conn_edit_id = Id::from(modal.id)
.with(wallet.get_config().id)
.with("_conn_edit");
let mut conn_edit_opts = TextEditOptions::new(conn_edit_id)
.paste()
.no_focus()
.scan_qr();
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("transport.conn_line"))
.size(17.0)
.color(Colors::inactive_text()));
ui.add_space(6.0);
View::text_edit(ui, cb, &mut self.bridge_conn_line_edit, &mut conn_edit_opts);
// Check if scan button was pressed.
if conn_edit_opts.scan_pressed {
cb.hide_keyboard();
modal.disable_closing();
conn_edit_opts.scan_pressed = false;
self.bridge_qr_scan_content = Some(CameraContent::default());
}
});
// Check if bin path or connection line text was changed to save bridge.
if conn_edit_before != self.bridge_conn_line_edit ||
bin_edit_before != self.bridge_bin_path_edit {
let bin_path = self.bridge_bin_path_edit.trim().to_string();
let conn_line = self.bridge_conn_line_edit.trim().to_string();
let b = match bridge {
TorBridge::Snowflake(_, _) => {
TorBridge::Snowflake(bin_path, conn_line)
},
TorBridge::Obfs4(_, _) => {
TorBridge::Obfs4(bin_path, conn_line)
}
};
TorConfig::save_bridge(Some(b));
self.settings_changed = true;
}
ui.add_space(2.0);
}
ui.add_space(6.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(6.0);
}
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("transport.tor_autorun_desc"))
.size(17.0)
.color(Colors::inactive_text()));
// Show Tor service autorun checkbox.
let autorun = wallet.auto_start_tor_listener();
View::checkbox(ui, autorun, t!("network.autorun"), || {
wallet.update_auto_start_tor_listener(!autorun);
});
});
ui.add_space(6.0);
ui.vertical_centered_justified(|ui| {
View::button(ui, t!("close"), Colors::white_or_black(false), || {
if self.settings_changed {
self.settings_changed = false;
// Restart running service or rebuild client.
let service_id = &wallet.identifier();
if Tor::is_service_running(service_id) {
if let Ok(key) = wallet.secret_key() {
let api_port = wallet.foreign_api_port().unwrap();
Tor::restart_service(api_port, key, service_id);
}
} else {
Tor::rebuild_client();
}
}
modal.close();
});
});
ui.add_space(6.0);
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,538 @@
// Copyright 2023 The Grim Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::ops::Range;
use std::time::{SystemTime, UNIX_EPOCH};
use egui::{Align, Id, Layout, Rect, RichText, Rounding, ScrollArea};
use egui::epaint::RectShape;
use egui::scroll_area::ScrollBarVisibility;
use grin_core::consensus::COINBASE_MATURITY;
use grin_core::core::amount_to_hr_string;
use grin_wallet_libwallet::TxLogEntryType;
use crate::gui::Colors;
use crate::gui::icons::{ARROW_CIRCLE_DOWN, ARROW_CIRCLE_UP, BRIDGE, CALENDAR_CHECK, CHAT_CIRCLE_TEXT, CHECK, DOTS_THREE_CIRCLE, FILE_TEXT, GEAR_FINE, PROHIBIT, X_CIRCLE};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Modal, PullToRefresh, Content, View};
use crate::gui::views::types::{LinePosition, ModalPosition};
use crate::gui::views::wallets::types::WalletTab;
use crate::gui::views::wallets::wallet::types::{GRIN, WalletTabType};
use crate::gui::views::wallets::wallet::WalletTransactionModal;
use crate::wallet::types::{WalletData, WalletTransaction};
use crate::wallet::Wallet;
/// Wallet transactions tab content.
pub struct WalletTransactions {
/// Transaction information [`Modal`] content.
tx_info_content: Option<WalletTransactionModal>,
/// Transaction identifier to use at confirmation [`Modal`].
confirm_cancel_tx_id: Option<u32>,
/// Flag to check if sync of wallet was initiated manually at time.
manual_sync: Option<u128>
}
impl Default for WalletTransactions {
fn default() -> Self {
Self {
tx_info_content: None,
confirm_cancel_tx_id: None,
manual_sync: None,
}
}
}
impl WalletTab for WalletTransactions {
fn get_type(&self) -> WalletTabType {
WalletTabType::Txs
}
fn ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) {
self.modal_content_ui(ui, wallet, cb);
self.txs_ui(ui, wallet, cb);
}
}
/// Identifier for transaction information [`Modal`].
const TX_INFO_MODAL: &'static str = "tx_info_modal";
/// Identifier for transaction cancellation confirmation [`Modal`].
const CANCEL_TX_CONFIRMATION_MODAL: &'static str = "cancel_tx_conf_modal";
impl WalletTransactions {
/// Height of transaction list item.
pub const TX_ITEM_HEIGHT: f32 = 75.0;
/// Draw transactions content.
fn txs_ui(&mut self,
ui: &mut egui::Ui,
wallet: &Wallet,
cb: &dyn PlatformCallbacks) {
let data = wallet.get_data().unwrap();
if data.txs.is_none() {
ui.centered_and_justified(|ui| {
View::big_loading_spinner(ui);
});
return;
}
let txs = data.txs.as_ref().unwrap();
let mut awaiting_amount = false;
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
// Show message when txs are empty.
if txs.is_empty() {
View::center_content(ui, 96.0, |ui| {
let empty_text = t!(
"wallets.txs_empty",
"message" => CHAT_CIRCLE_TEXT,
"transport" => BRIDGE,
"settings" => GEAR_FINE
);
ui.label(RichText::new(empty_text)
.size(16.0)
.color(Colors::inactive_text()));
});
return;
}
// Draw awaiting amount info if exists.
awaiting_amount = self.awaiting_info_ui(ui, &data);
});
ui.add_space(4.0);
// Show list of transactions.
let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis();
let refresh = self.manual_sync.unwrap_or(0) + 1600 > now;
let refresh_resp = PullToRefresh::new(refresh)
.can_refresh(!refresh && !wallet.syncing())
.min_refresh_distance(70.0)
.scroll_area_ui(ui, |ui| {
ScrollArea::vertical()
.id_salt(Id::from("wallet_tx_list_scroll").with(wallet.get_config().id))
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.auto_shrink([false; 2])
.show_rows(ui, Self::TX_ITEM_HEIGHT, txs.len(), |ui, row_range| {
ui.add_space(1.0);
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
self.tx_list_ui(ui, awaiting_amount, row_range, wallet, txs, cb);
});
})
});
// Sync wallet on refresh.
if refresh_resp.should_refresh() {
self.manual_sync = Some(now);
if !wallet.syncing() {
wallet.sync();
}
}
}
/// Draw transaction list content.
fn tx_list_ui(&mut self,
ui: &mut egui::Ui,
awaiting: bool,
row_range: Range<usize>,
wallet: &Wallet,
txs: &Vec<WalletTransaction>,
cb: &dyn PlatformCallbacks) {
for index in row_range {
let mut rect = if awaiting {
let mut rect = ui.available_rect_before_wrap();
rect.min += egui::emath::vec2(6.0, 0.0);
rect.max -= egui::emath::vec2(6.0, 0.0);
rect
} else {
ui.available_rect_before_wrap()
};
rect.set_height(Self::TX_ITEM_HEIGHT);
// Draw tx item background.
let mut r = View::item_rounding(index, txs.len(), false);
let p = ui.painter();
p.rect(rect, r, Colors::fill_lite(), View::item_stroke());
let tx = txs.get(index).unwrap();
let data = wallet.get_data().unwrap();
Self::tx_item_ui(ui, tx, rect, &data, |ui| {
// Draw button to show transaction info.
if tx.data.tx_slate_id.is_some() {
r.nw = 0.0;
r.sw = 0.0;
View::item_button(ui, r, FILE_TEXT, None, || {
self.show_tx_info_modal(wallet, tx, false);
});
}
let wallet_loaded = wallet.foreign_api_port().is_some();
// Draw button to show transaction finalization.
if wallet_loaded && tx.can_finalize {
let (icon, color) = (CHECK, Some(Colors::green()));
View::item_button(ui, Rounding::default(), icon, color, || {
cb.hide_keyboard();
self.show_tx_info_modal(wallet, tx, true);
});
}
// Draw button to cancel transaction.
if wallet_loaded && tx.can_cancel() {
let (icon, color) = (PROHIBIT, Some(Colors::red()));
View::item_button(ui, Rounding::default(), icon, color, || {
self.confirm_cancel_tx_id = Some(tx.data.id);
// Show transaction cancellation confirmation modal.
Modal::new(CANCEL_TX_CONFIRMATION_MODAL)
.position(ModalPosition::Center)
.title(t!("confirmation"))
.show();
});
}
});
}
}
/// Draw information about locked, finalizing or confirming balance, return `true` if exists.
fn awaiting_info_ui(&mut self, ui: &mut egui::Ui, data: &WalletData) -> bool {
let amount_conf = data.info.amount_awaiting_confirmation;
let amount_fin = data.info.amount_awaiting_finalization;
let amount_locked = data.info.amount_locked;
if amount_conf == 0 && amount_fin == 0 && amount_locked == 0 {
return false;
}
let rect = ui.available_rect_before_wrap();
// Draw background.
let mut bg = RectShape::new(rect, Rounding {
nw: 0.0,
ne: 0.0,
sw: 8.0,
se: 8.0,
}, Colors::TRANSPARENT, View::item_stroke());
let bg_idx = ui.painter().add(bg);
let resp = ui.allocate_ui(rect.size(), |ui| {
ui.vertical_centered_justified(|ui| {
// Correct vertical spacing between items.
ui.style_mut().spacing.item_spacing.y = -3.0;
if amount_conf != 0 {
// Draw awaiting confirmation amount.
awaiting_item_ui(ui, amount_conf, t!("wallets.await_conf_amount"));
}
if amount_fin != 0 {
// Draw awaiting confirmation amount.
awaiting_item_ui(ui, amount_fin, t!("wallets.await_fin_amount"));
}
if amount_locked != 0 {
// Draw locked amount.
awaiting_item_ui(ui, amount_locked, t!("wallets.locked_amount"));
}
});
}).response;
// Setup background size.
bg.rect = resp.rect;
ui.painter().set(bg_idx, bg);
true
}
/// Draw [`Modal`] content for this ui container.
fn modal_content_ui(&mut self,
ui: &mut egui::Ui,
wallet: &Wallet,
cb: &dyn PlatformCallbacks) {
match Modal::opened() {
None => {}
Some(id) => {
match id {
TX_INFO_MODAL => {
Modal::ui(ui.ctx(), |ui, modal| {
if let Some(content) = self.tx_info_content.as_mut() {
content.ui(ui, wallet, modal, cb);
}
});
}
CANCEL_TX_CONFIRMATION_MODAL => {
Modal::ui(ui.ctx(), |ui, modal| {
self.cancel_confirmation_modal(ui, wallet, modal);
});
}
_ => {}
}
}
}
}
/// Draw transaction item.
pub fn tx_item_ui(ui: &mut egui::Ui,
tx: &WalletTransaction,
rect: Rect,
data: &WalletData,
buttons_ui: impl FnOnce(&mut egui::Ui)) {
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Max), |ui| {
ui.horizontal_centered(|ui| {
// Draw buttons.
buttons_ui(ui);
});
ui.with_layout(Layout::left_to_right(Align::Min), |ui| {
ui.add_space(6.0);
ui.vertical(|ui| {
ui.add_space(3.0);
// Setup transaction amount.
let mut amount_text = if tx.data.tx_type == TxLogEntryType::TxSent ||
tx.data.tx_type == TxLogEntryType::TxSentCancelled {
"-"
} else if tx.data.tx_type == TxLogEntryType::TxReceived ||
tx.data.tx_type == TxLogEntryType::TxReceivedCancelled {
"+"
} else {
""
}.to_string();
amount_text = format!("{}{} {}",
amount_text,
amount_to_hr_string(tx.amount, true),
GRIN);
// Setup amount color.
let amount_color = match tx.data.tx_type {
TxLogEntryType::ConfirmedCoinbase => Colors::white_or_black(true),
TxLogEntryType::TxReceived => Colors::white_or_black(true),
TxLogEntryType::TxSent => Colors::white_or_black(true),
TxLogEntryType::TxReceivedCancelled => Colors::text(false),
TxLogEntryType::TxSentCancelled => Colors::text(false),
TxLogEntryType::TxReverted => Colors::text(false)
};
ui.with_layout(Layout::left_to_right(Align::Min), |ui| {
ui.add_space(1.0);
View::ellipsize_text(ui, amount_text, 18.0, amount_color);
});
ui.add_space(-2.0);
// Setup transaction status text.
let height = data.info.last_confirmed_height;
let status_text = if !tx.data.confirmed {
let is_canceled = tx.data.tx_type == TxLogEntryType::TxSentCancelled
|| tx.data.tx_type == TxLogEntryType::TxReceivedCancelled;
if is_canceled {
format!("{} {}", X_CIRCLE, t!("wallets.tx_canceled"))
} else if tx.finalizing {
format!("{} {}", DOTS_THREE_CIRCLE, t!("wallets.tx_finalizing"))
} else {
if tx.cancelling {
format!("{} {}", DOTS_THREE_CIRCLE, t!("wallets.tx_cancelling"))
} else {
match tx.data.tx_type {
TxLogEntryType::TxReceived => {
format!("{} {}",
DOTS_THREE_CIRCLE,
t!("wallets.tx_receiving"))
},
TxLogEntryType::TxSent => {
format!("{} {}",
DOTS_THREE_CIRCLE,
t!("wallets.tx_sending"))
},
TxLogEntryType::ConfirmedCoinbase => {
let tx_h = tx.height.unwrap_or(1) - 1;
if tx_h != 0 {
let left_conf = height - tx_h;
if height >= tx_h && left_conf < COINBASE_MATURITY {
let conf_info = format!("{}/{}",
left_conf,
COINBASE_MATURITY);
format!("{} {} {}",
DOTS_THREE_CIRCLE,
t!("wallets.tx_confirming"),
conf_info
)
} else {
format!("{} {}",
DOTS_THREE_CIRCLE,
t!("wallets.tx_confirming"))
}
} else {
format!("{} {}",
DOTS_THREE_CIRCLE,
t!("wallets.tx_confirming"))
}
},
_ => {
format!("{} {}",
DOTS_THREE_CIRCLE,
t!("wallets.tx_confirming"))
}
}
}
}
} else {
match tx.data.tx_type {
TxLogEntryType::ConfirmedCoinbase => {
let tx_h = tx.height.unwrap_or(1) - 1;
if tx_h != 0 {
let left_conf = height - tx_h;
if height >= tx_h && left_conf < COINBASE_MATURITY {
let conf_info = format!("{}/{}",
left_conf,
COINBASE_MATURITY);
format!("{} {} {}",
DOTS_THREE_CIRCLE,
t!("wallets.tx_confirming"),
conf_info
)
} else {
format!("{} {}",
DOTS_THREE_CIRCLE,
t!("wallets.tx_confirmed"))
}
} else {
format!("{} {}",
DOTS_THREE_CIRCLE,
t!("wallets.tx_confirmed"))
}
},
TxLogEntryType::TxSent | TxLogEntryType::TxReceived => {
let min_conf = data.info.minimum_confirmations;
if tx.height.is_none() || (tx.height.unwrap() != 0 &&
height - tx.height.unwrap() > min_conf - 1) {
let (i, t) = if tx.data.tx_type == TxLogEntryType::TxSent {
(ARROW_CIRCLE_UP, t!("wallets.tx_sent"))
} else {
(ARROW_CIRCLE_DOWN, t!("wallets.tx_received"))
};
format!("{} {}", i, t)
} else {
let tx_height = tx.height.unwrap() - 1;
let left_conf = height - tx_height;
let conf_info = if tx_height != 0 && height >= tx_height &&
left_conf < min_conf {
format!("{}/{}", left_conf, min_conf)
} else {
"".to_string()
};
format!("{} {} {}",
DOTS_THREE_CIRCLE,
t!("wallets.tx_confirming"),
conf_info
)
}
},
_ => format!("{} {}", X_CIRCLE, t!("wallets.canceled"))
}
};
// Setup status text color.
let status_color = match tx.data.tx_type {
TxLogEntryType::ConfirmedCoinbase => Colors::text(false),
TxLogEntryType::TxReceived => if tx.data.confirmed {
Colors::green()
} else {
Colors::text(false)
},
TxLogEntryType::TxSent => if tx.data.confirmed {
Colors::red()
} else {
Colors::text(false)
},
TxLogEntryType::TxReceivedCancelled => Colors::inactive_text(),
TxLogEntryType::TxSentCancelled => Colors::inactive_text(),
TxLogEntryType::TxReverted => Colors::inactive_text(),
};
ui.label(RichText::new(status_text).size(15.0).color(status_color));
// Setup transaction time.
let tx_time = View::format_time(tx.data.creation_ts.timestamp());
let tx_time_text = format!("{} {}", CALENDAR_CHECK, tx_time);
ui.label(RichText::new(tx_time_text).size(15.0).color(Colors::gray()));
ui.add_space(3.0);
});
});
});
}
/// Show transaction information [`Modal`].
fn show_tx_info_modal(&mut self, wallet: &Wallet, tx: &WalletTransaction, finalize: bool) {
let modal = WalletTransactionModal::new(wallet, tx, finalize);
self.tx_info_content = Some(modal);
Modal::new(TX_INFO_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("wallets.tx"))
.show();
}
/// Confirmation [`Modal`] to cancel transaction.
fn cancel_confirmation_modal(&mut self, ui: &mut egui::Ui, wallet: &Wallet, modal: &Modal) {
ui.add_space(6.0);
ui.vertical_centered(|ui| {
// Setup confirmation text.
let data = wallet.get_data().unwrap();
let data_txs = data.txs.unwrap();
let txs = data_txs.into_iter()
.filter(|tx| tx.data.id == self.confirm_cancel_tx_id.unwrap())
.collect::<Vec<WalletTransaction>>();
if txs.is_empty() {
modal.close();
return;
}
let tx = txs.get(0).unwrap();
let amount = amount_to_hr_string(tx.amount, true);
let text = match tx.data.tx_type {
TxLogEntryType::TxReceived => {
t!("wallets.tx_receive_cancel_conf", "amount" => amount)
},
_ => {
t!("wallets.tx_send_cancel_conf", "amount" => amount)
}
};
ui.label(RichText::new(text)
.size(17.0)
.color(Colors::text(false)));
ui.add_space(8.0);
});
// Show modal buttons.
ui.scope(|ui| {
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
self.confirm_cancel_tx_id = None;
modal.close();
});
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, "OK".to_string(), Colors::white_or_black(false), || {
wallet.cancel(self.confirm_cancel_tx_id.unwrap());
self.confirm_cancel_tx_id = None;
modal.close();
});
});
});
ui.add_space(6.0);
});
}
}
/// Draw awaiting balance item content.
fn awaiting_item_ui(ui: &mut egui::Ui, amount: u64, label: String) {
let rect = ui.available_rect_before_wrap();
View::line(ui, LinePosition::TOP, &rect, Colors::item_stroke());
ui.add_space(4.0);
let amount_format = amount_to_hr_string(amount, true);
ui.label(RichText::new(format!("{}", amount_format))
.color(Colors::white_or_black(true))
.size(17.0));
ui.label(RichText::new(label)
.color(Colors::gray())
.size(15.0));
ui.add_space(4.0);
}

View file

@ -0,0 +1,19 @@
// Copyright 2024 The Grim Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
mod content;
pub use content::*;
mod tx;
pub use tx::*;

View file

@ -0,0 +1,600 @@
// Copyright 2024 The Grim Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::thread;
use std::sync::Arc;
use parking_lot::RwLock;
use egui::scroll_area::ScrollBarVisibility;
use egui::{Align, Id, Layout, RichText, Rounding, ScrollArea};
use grin_util::ToHex;
use grin_core::core::amount_to_hr_string;
use grin_wallet_libwallet::{Error, Slate, SlateState, TxLogEntryType};
use crate::gui::Colors;
use crate::gui::icons::{BROOM, CHECK, CLIPBOARD_TEXT, COPY, CUBE, FILE_ARCHIVE, FILE_TEXT, HASH_STRAIGHT, PROHIBIT, QR_CODE, SCAN};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{CameraContent, FilePickButton, Modal, QrCodeContent, View};
use crate::gui::views::wallets::wallet::txs::WalletTransactions;
use crate::gui::views::wallets::wallet::types::SLATEPACK_MESSAGE_HINT;
use crate::wallet::types::WalletTransaction;
use crate::wallet::Wallet;
/// Transaction information [`Modal`] content.
pub struct WalletTransactionModal {
/// Transaction identifier.
tx_id: u32,
/// Response Slatepack message input value.
response_edit: String,
/// Flag to show transaction finalization input.
show_finalization: bool,
/// Finalization Slatepack message input value.
finalize_edit: String,
/// Flag to check if error happened during transaction finalization.
finalize_error: bool,
/// Flag to check if transaction is finalizing.
finalizing: bool,
/// Transaction finalization result.
final_result: Arc<RwLock<Option<Result<WalletTransaction, Error>>>>,
/// QR code Slatepack message image content.
qr_code_content: Option<QrCodeContent>,
/// QR code scanner content.
scan_qr_content: Option<CameraContent>,
/// Button to parse picked file content.
file_pick_button: FilePickButton,
}
impl WalletTransactionModal {
/// Create new content instance with [`Wallet`] from provided [`WalletTransaction`].
pub fn new(wallet: &Wallet, tx: &WalletTransaction, show_finalization: bool) -> Self {
Self {
tx_id: tx.data.id,
response_edit: if !tx.cancelling && !tx.finalizing && !tx.data.confirmed &&
tx.data.tx_slate_id.is_some() &&
(tx.data.tx_type == TxLogEntryType::TxSent ||
tx.data.tx_type == TxLogEntryType::TxReceived) {
let mut slate = Slate::blank(1, false);
slate.state = if tx.can_finalize {
if tx.data.tx_type == TxLogEntryType::TxSent {
SlateState::Standard1
} else {
SlateState::Invoice1
}
} else {
if tx.data.tx_type == TxLogEntryType::TxReceived {
SlateState::Standard2
} else {
SlateState::Invoice2
}
};
slate.id = tx.data.tx_slate_id.unwrap();
wallet.read_slatepack(&slate).unwrap_or("".to_string())
} else {
"".to_string()
},
finalize_edit: "".to_string(),
finalize_error: false,
show_finalization,
finalizing: false,
final_result: Arc::new(RwLock::new(None)),
qr_code_content: None,
scan_qr_content: None,
file_pick_button: FilePickButton::default(),
}
}
/// Draw [`Modal`] content.
pub fn ui(&mut self,
ui: &mut egui::Ui,
wallet: &Wallet,
modal: &Modal,
cb: &dyn PlatformCallbacks) {
// Check values and setup transaction data.
let wallet_data = wallet.get_data();
if wallet_data.is_none() {
modal.close();
return;
}
let data = wallet_data.unwrap();
let data_txs = data.txs.clone().unwrap();
let txs = data_txs.into_iter()
.filter(|tx| tx.data.id == self.tx_id)
.collect::<Vec<WalletTransaction>>();
if txs.is_empty() {
cb.hide_keyboard();
modal.close();
return;
}
let tx = txs.get(0).unwrap();
// Show transaction information.
if self.qr_code_content.is_none() && self.scan_qr_content.is_none() {
self.info_ui(ui, tx, wallet, cb);
}
// Show Slatepack message interaction.
if !self.response_edit.is_empty() {
self.message_ui(ui, tx, wallet, modal, cb);
}
if !self.finalizing {
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
if self.qr_code_content.is_some() {
// Show buttons to close modal or come back to text request content.
ui.columns(2, |cols| {
cols[0].vertical_centered_justified(|ui| {
View::button(ui, t!("close"), Colors::white_or_black(false), || {
self.qr_code_content = None;
modal.close();
});
});
cols[1].vertical_centered_justified(|ui| {
View::button(ui, t!("back"), Colors::white_or_black(false), || {
self.qr_code_content = None;
});
});
});
} else if self.scan_qr_content.is_some() {
ui.add_space(8.0);
// Show buttons to close modal or scanner.
ui.columns(2, |cols| {
cols[0].vertical_centered_justified(|ui| {
View::button(ui, t!("close"), Colors::white_or_black(false), || {
cb.stop_camera();
self.scan_qr_content = None;
modal.close();
});
});
cols[1].vertical_centered_justified(|ui| {
View::button(ui, t!("back"), Colors::white_or_black(false), || {
cb.stop_camera();
self.scan_qr_content = None;
modal.enable_closing();
});
});
});
} else {
ui.add_space(8.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(8.0);
// Show button to close modal.
ui.vertical_centered_justified(|ui| {
View::button(ui, t!("close"), Colors::white_or_black(false), || {
cb.hide_keyboard();
modal.close();
});
});
}
ui.add_space(6.0);
} else {
// Show loader on finalizing.
ui.vertical_centered(|ui| {
View::small_loading_spinner(ui);
ui.add_space(16.0);
});
// Check finalization result.
let has_res = {
let r_res = self.final_result.read();
r_res.is_some()
};
if has_res {
let res = {
let r_res = self.final_result.read();
r_res.as_ref().unwrap().clone()
};
if let Ok(_) = res {
self.show_finalization = false;
self.finalize_edit = "".to_string();
self.response_edit = "".to_string();
} else {
self.finalize_error = true;
}
// Clear status and result.
{
let mut w_res = self.final_result.write();
*w_res = None;
}
self.finalizing = false;
modal.enable_closing();
}
}
}
/// Draw transaction information content.
fn info_ui(&mut self,
ui: &mut egui::Ui,
tx: &WalletTransaction,
wallet: &Wallet,
cb: &dyn PlatformCallbacks) {
ui.add_space(6.0);
let mut rect = ui.available_rect_before_wrap();
rect.set_height(WalletTransactions::TX_ITEM_HEIGHT);
// Draw tx item background.
let p = ui.painter();
let r = View::item_rounding(0, 2, false);
p.rect(rect, r, Colors::TRANSPARENT, View::item_stroke());
// Show transaction amount status and time.
let data = wallet.get_data().unwrap();
WalletTransactions::tx_item_ui(ui, tx, rect, &data, |ui| {
if self.finalizing {
return;
}
// Show block height or buttons.
if let Some(h) = tx.height {
if h != 0 {
ui.add_space(6.0);
let height = format!("{} {}", CUBE, h.to_string());
ui.with_layout(Layout::bottom_up(Align::Max), |ui| {
ui.add_space(3.0);
ui.label(RichText::new(height)
.size(15.0)
.color(Colors::text(false)));
});
}
return;
}
let wallet_loaded = wallet.foreign_api_port().is_some();
// Draw button to show transaction finalization or request info.
if wallet_loaded && tx.can_finalize {
let (icon, color) = if self.show_finalization {
(FILE_TEXT, None)
} else {
(CHECK, Some(Colors::green()))
};
let r = View::item_rounding(0, 2, true);
View::item_button(ui, r, icon, color, || {
cb.hide_keyboard();
if self.show_finalization {
self.show_finalization = false;
return;
}
self.show_finalization = true;
});
}
// Draw button to cancel transaction.
if wallet_loaded && tx.can_cancel() {
let r = if tx.can_finalize {
Rounding::default()
} else {
View::item_rounding(0, 2, true)
};
View::item_button(ui, r, PROHIBIT, Some(Colors::red()), || {
cb.hide_keyboard();
wallet.cancel(tx.data.id);
});
}
});
// Show identifier.
if let Some(id) = tx.data.tx_slate_id {
let label = format!("{} {}", HASH_STRAIGHT, t!("id"));
info_item_ui(ui, id.to_string(), label, true, cb);
}
// Show kernel.
if let Some(kernel) = tx.data.kernel_excess {
let label = format!("{} {}", FILE_ARCHIVE, t!("kernel"));
info_item_ui(ui, kernel.0.to_hex(), label, true, cb);
}
// Show receiver address.
if let Some(rec) = tx.receiver() {
let label = format!("{} {}", CUBE, t!("network_mining.address"));
info_item_ui(ui, rec.to_string(), label, true, cb);
}
}
/// Draw Slatepack message content.
fn message_ui(&mut self,
ui: &mut egui::Ui,
tx: &WalletTransaction,
wallet: &Wallet,
modal: &Modal,
cb: &dyn PlatformCallbacks) {
ui.add_space(6.0);
// Draw QR code scanner content if requested.
if let Some(scan_content) = self.scan_qr_content.as_mut() {
if let Some(result) = scan_content.qr_scan_result() {
cb.stop_camera();
// Setup value to finalization input field.
self.finalize_edit = result.text();
self.on_finalization_input_change(tx, wallet, modal, cb);
modal.enable_closing();
self.scan_qr_content = None;
} else {
scan_content.ui(ui, cb);
}
return;
}
let amount = amount_to_hr_string(tx.amount, true);
// Draw Slatepack message description text.
ui.vertical_centered(|ui| {
if self.show_finalization {
let desc_text = if self.finalize_error {
t!("wallets.finalize_slatepack_err")
} else {
if tx.data.tx_type == TxLogEntryType::TxSent {
t!("wallets.parse_s2_slatepack_desc", "amount" => amount)
} else {
t!("wallets.parse_i2_slatepack_desc", "amount" => amount)
}
};
let desc_color = if self.finalize_error {
Colors::red()
} else {
Colors::gray()
};
ui.label(RichText::new(desc_text).size(16.0).color(desc_color));
} else {
let desc_text = if tx.can_finalize {
if tx.data.tx_type == TxLogEntryType::TxSent {
t!("wallets.send_request_desc", "amount" => amount)
} else {
t!("wallets.invoice_desc", "amount" => amount)
}
} else {
if tx.data.tx_type == TxLogEntryType::TxSent {
t!("wallets.parse_i1_slatepack_desc", "amount" => amount)
} else {
t!("wallets.parse_s1_slatepack_desc", "amount" => amount)
}
};
ui.label(RichText::new(desc_text).size(16.0).color(Colors::gray()));
}
});
ui.add_space(6.0);
// Setup message input value.
let message_edit = if self.show_finalization {
&mut self.finalize_edit
} else {
&mut self.response_edit
};
let message_before = message_edit.clone();
// Draw QR code content if requested.
if let Some(qr_content) = self.qr_code_content.as_mut() {
qr_content.ui(ui, cb);
return;
}
// Draw Slatepack message finalization input or request text.
ui.vertical_centered(|ui| {
let scroll_id = if self.show_finalization {
Id::from("tx_info_message_finalize")
} else {
Id::from("tx_info_message_request")
}.with(tx.data.id);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(3.0);
ScrollArea::vertical()
.id_salt(scroll_id)
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.max_height(128.0)
.auto_shrink([false; 2])
.show(ui, |ui| {
ui.add_space(7.0);
let input_id = scroll_id.with("_input");
let resp = egui::TextEdit::multiline(message_edit)
.id(input_id)
.font(egui::TextStyle::Small)
.desired_rows(5)
.interactive(self.show_finalization && !self.finalizing)
.hint_text(SLATEPACK_MESSAGE_HINT)
.desired_width(f32::INFINITY)
.show(ui).response;
// Show soft keyboard on click.
if self.show_finalization && resp.clicked() {
resp.request_focus();
cb.show_keyboard();
}
if self.show_finalization && resp.has_focus() {
// Apply text from input on Android as temporary fix for egui.
View::on_soft_input(ui, input_id, message_edit);
}
ui.add_space(6.0);
});
});
ui.add_space(2.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(8.0);
// Do not show buttons on finalization.
if self.finalizing {
return;
}
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
if self.show_finalization {
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
// Draw button to scan Slatepack message QR code.
let qr_text = format!("{} {}", SCAN, t!("scan"));
View::button(ui, qr_text, Colors::fill_lite(), || {
cb.hide_keyboard();
modal.disable_closing();
cb.start_camera();
self.scan_qr_content = Some(CameraContent::default());
});
});
columns[1].vertical_centered_justified(|ui| {
// Draw button to paste data from clipboard.
let paste_text = format!("{} {}", CLIPBOARD_TEXT, t!("paste"));
View::button(ui, paste_text, Colors::fill_lite(), || {
self.finalize_edit = cb.get_string_from_buffer();
});
});
});
ui.add_space(8.0);
ui.vertical_centered(|ui| {
if self.finalize_error {
// Draw button to clear message input.
let clear_text = format!("{} {}", BROOM, t!("clear"));
View::button(ui, clear_text, Colors::fill_lite(), || {
self.finalize_edit.clear();
self.finalize_error = false;
});
} else {
// Draw button to choose file.
self.file_pick_button.ui(ui, cb, |text| {
self.finalize_edit = text;
});
}
});
// Callback on finalization message input change.
if message_before != self.finalize_edit {
self.on_finalization_input_change(tx, wallet, modal, cb);
}
} else {
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
// Draw button to show Slatepack message as QR code.
let qr_text = format!("{} {}", QR_CODE, t!("qr_code"));
View::button(ui, qr_text.clone(), Colors::white_or_black(false), || {
cb.hide_keyboard();
let text = self.response_edit.clone();
self.qr_code_content = Some(QrCodeContent::new(text, true));
});
});
columns[1].vertical_centered_justified(|ui| {
// Draw copy button.
let copy_text = format!("{} {}", COPY, t!("copy"));
View::button(ui, copy_text, Colors::white_or_black(false), || {
cb.copy_string_to_buffer(self.response_edit.clone());
self.finalize_edit = "".to_string();
if tx.can_finalize {
self.show_finalization = true;
} else {
cb.hide_keyboard();
modal.close();
}
});
});
});
// Show button to share response as file.
ui.add_space(8.0);
ui.vertical_centered(|ui| {
let share_text = format!("{} {}", FILE_TEXT, t!("share"));
View::colored_text_button(ui,
share_text,
Colors::blue(),
Colors::white_or_black(false), || {
if let Some((s, _)) = wallet.read_slate_by_tx(tx) {
let name = format!("{}.{}.slatepack", s.id, s.state);
let data = self.response_edit.as_bytes().to_vec();
cb.share_data(name, data).unwrap_or_default();
}
});
});
}
}
/// Parse Slatepack message on transaction finalization input change.
fn on_finalization_input_change(&mut self,
tx: &WalletTransaction,
wallet: &Wallet,
modal: &Modal,
cb: &dyn PlatformCallbacks) {
let message = &self.finalize_edit;
if message.is_empty() {
self.finalize_error = false;
} else {
// Parse input message to finalize.
if let Ok(slate) = wallet.parse_slatepack(message) {
let send = slate.state == SlateState::Standard2 &&
tx.data.tx_type == TxLogEntryType::TxSent;
let receive = slate.state == SlateState::Invoice2 &&
tx.data.tx_type == TxLogEntryType::TxReceived;
if Some(slate.id) == tx.data.tx_slate_id && (send || receive) {
let message = message.clone();
let wallet = wallet.clone();
let final_res = self.final_result.clone();
// Finalize transaction at separate thread.
cb.hide_keyboard();
self.finalizing = true;
modal.disable_closing();
thread::spawn(move || {
let res = wallet.finalize(&message);
let mut w_res = final_res.write();
*w_res = Some(res);
});
} else {
self.finalize_error = true;
}
} else {
self.finalize_error = true;
}
}
}
}
/// Draw transaction information item content.
fn info_item_ui(ui: &mut egui::Ui,
value: String,
label: String,
copy: bool,
cb: &dyn PlatformCallbacks) {
// 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 mut rounding = View::item_rounding(1, 3, false);
ui.painter().rect(bg_rect, rounding, Colors::fill(), View::item_stroke());
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
// Draw button to copy transaction info value.
if copy {
rounding.nw = 0.0;
rounding.sw = 0.0;
View::item_button(ui, rounding, COPY, None, || {
cb.copy_string_to_buffer(value.clone());
});
}
// Draw value information.
let layout_size = ui.available_size();
ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| {
ui.add_space(6.0);
ui.vertical(|ui| {
ui.add_space(3.0);
View::ellipsize_text(ui, value, 15.0, Colors::title(false));
ui.label(RichText::new(label).size(15.0).color(Colors::gray()));
ui.add_space(3.0);
});
});
});
}

View file

@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
use crate::gui::icons::{FOLDER_LOCK, FOLDER_OPEN, SPINNER, WARNING_CIRCLE};
use crate::gui::platform::PlatformCallbacks; use crate::gui::platform::PlatformCallbacks;
use crate::wallet::Wallet; use crate::wallet::Wallet;
@ -25,7 +26,7 @@ pub trait WalletTab {
fn get_type(&self) -> WalletTabType; fn get_type(&self) -> WalletTabType;
fn ui(&mut self, fn ui(&mut self,
ui: &mut egui::Ui, ui: &mut egui::Ui,
wallet: &mut Wallet, wallet: &Wallet,
cb: &dyn PlatformCallbacks); cb: &dyn PlatformCallbacks);
} }
@ -49,3 +50,38 @@ impl WalletTabType {
} }
} }
} }
/// Get wallet status text.
pub fn wallet_status_text(wallet: &Wallet) -> String {
if wallet.is_open() {
if wallet.sync_error() {
format!("{} {}", WARNING_CIRCLE, t!("error"))
} else if wallet.is_closing() {
format!("{} {}", SPINNER, t!("wallets.closing"))
} else if wallet.is_repairing() {
let repair_progress = wallet.repairing_progress();
if repair_progress == 0 {
format!("{} {}", SPINNER, t!("wallets.checking"))
} else {
format!("{} {}: {}%",
SPINNER,
t!("wallets.checking"),
repair_progress)
}
} else if wallet.syncing() {
let info_progress = wallet.info_sync_progress();
if info_progress == 100 || info_progress == 0 {
format!("{} {}", SPINNER, t!("wallets.loading"))
} else {
format!("{} {}: {}%",
SPINNER,
t!("wallets.loading"),
info_progress)
}
} else {
format!("{} {}", FOLDER_OPEN, t!("wallets.unlocked"))
}
} else {
format!("{} {}", FOLDER_LOCK, t!("wallets.locked"))
}
}

View file

@ -16,7 +16,10 @@
extern crate rust_i18n; extern crate rust_i18n;
use eframe::NativeOptions; use eframe::NativeOptions;
use egui::{Context, Stroke}; use egui::{Context, Stroke, Theme};
use lazy_static::lazy_static;
use std::sync::Arc;
use parking_lot::RwLock;
#[cfg(target_os = "android")] #[cfg(target_os = "android")]
use winit::platform::android::activity::AndroidApp; use winit::platform::android::activity::AndroidApp;
@ -74,6 +77,7 @@ fn android_main(app: AndroidApp) {
options.wgpu_options.device_descriptor = std::sync::Arc::new(|_| { options.wgpu_options.device_descriptor = std::sync::Arc::new(|_| {
let base_limits = wgpu::Limits::downlevel_webgl2_defaults(); let base_limits = wgpu::Limits::downlevel_webgl2_defaults();
wgpu::DeviceDescriptor { wgpu::DeviceDescriptor {
memory_hints: wgpu::MemoryHints::default(),
label: Some("egui wgpu device"), label: Some("egui wgpu device"),
required_features: wgpu::Features::default(), required_features: wgpu::Features::default(),
required_limits: wgpu::Limits { required_limits: wgpu::Limits {
@ -99,31 +103,22 @@ fn use_dark_theme(platform: &gui::platform::Android) -> bool {
} }
/// [`App`] setup for [`eframe`]. /// [`App`] setup for [`eframe`].
pub fn app_creator<T: 'static>(app: App<T>) -> eframe::AppCreator pub fn app_creator<T: 'static>(app: App<T>) -> eframe::AppCreator<'static>
where App<T>: eframe::App, T: PlatformCallbacks { where App<T>: eframe::App, T: PlatformCallbacks {
Box::new(|cc| { Box::new(|cc| {
setup_fonts(&cc.egui_ctx);
// Setup images support. // Setup images support.
egui_extras::install_image_loaders(&cc.egui_ctx); egui_extras::install_image_loaders(&cc.egui_ctx);
// Setup visuals.
setup_visuals(&cc.egui_ctx);
// Setup fonts.
setup_fonts(&cc.egui_ctx);
// Return app instance.
Ok(Box::new(app)) Ok(Box::new(app))
}) })
} }
/// Entry point to start ui with [`eframe`]. /// Entry point to start ui with [`eframe`].
pub fn start(mut options: NativeOptions, app_creator: eframe::AppCreator) -> eframe::Result<()> { pub fn start(options: NativeOptions, app_creator: eframe::AppCreator) -> eframe::Result<()> {
options.default_theme = if AppConfig::dark_theme().unwrap_or(false) {
eframe::Theme::Dark
} else {
eframe::Theme::Light
};
// Setup translations. // Setup translations.
setup_i18n(); setup_i18n();
// Start integrated node if needed. // Start integrated node if needed.
if Settings::app_config_to_read().auto_start_node { if AppConfig::autostart_node() {
Node::start(); Node::start();
} }
// Launch graphical interface. // Launch graphical interface.
@ -132,6 +127,10 @@ pub fn start(mut options: NativeOptions, app_creator: eframe::AppCreator) -> efr
/// Setup application [`egui::Style`] and [`egui::Visuals`]. /// Setup application [`egui::Style`] and [`egui::Visuals`].
pub fn setup_visuals(ctx: &Context) { pub fn setup_visuals(ctx: &Context) {
let use_dark = AppConfig::dark_theme().unwrap_or_else(|| {
ctx.system_theme().unwrap_or(Theme::Dark) == Theme::Dark
});
let mut style = (*ctx.style()).clone(); let mut style = (*ctx.style()).clone();
// Setup selection. // Setup selection.
style.interaction.selectable_labels = false; style.interaction.selectable_labels = false;
@ -152,7 +151,6 @@ pub fn setup_visuals(ctx: &Context) {
ctx.set_style(style); ctx.set_style(style);
// Setup visuals based on app color theme. // Setup visuals based on app color theme.
let use_dark = AppConfig::dark_theme().unwrap_or(false);
let mut visuals = if use_dark { let mut visuals = if use_dark {
egui::Visuals::dark() egui::Visuals::dark()
} else { } else {
@ -188,9 +186,9 @@ pub fn setup_fonts(ctx: &Context) {
"../fonts/phosphor.ttf" "../fonts/phosphor.ttf"
)).tweak(egui::FontTweak { )).tweak(egui::FontTweak {
scale: 1.0, scale: 1.0,
y_offset_factor: -0.30, y_offset_factor: -0.20,
y_offset: 0.0, y_offset: 0.0,
baseline_offset_factor: 0.50, baseline_offset_factor: 0.16,
}), }),
); );
fonts fonts
@ -256,3 +254,54 @@ fn setup_i18n() {
} }
} }
} }
/// Get data from deeplink or opened file.
pub fn consume_incoming_data() -> Option<String> {
let has_data = {
let r_data = INCOMING_DATA.read();
r_data.is_some()
};
if has_data {
// Clear data.
let mut w_data = INCOMING_DATA.write();
let data = w_data.clone();
*w_data = None;
return data;
}
None
}
/// Provide data from deeplink or opened file.
pub fn on_data(data: String) {
let mut w_data = INCOMING_DATA.write();
*w_data = Some(data);
}
lazy_static! {
/// Data provided from deeplink or opened file.
pub static ref INCOMING_DATA: Arc<RwLock<Option<String>>> = Arc::new(RwLock::new(None));
}
/// Callback from Java code with with passed data.
#[allow(dead_code)]
#[allow(non_snake_case)]
#[cfg(target_os = "android")]
#[no_mangle]
pub extern "C" fn Java_mw_gri_android_MainActivity_onData(
_env: jni::JNIEnv,
_class: jni::objects::JObject,
char: jni::sys::jstring
) {
unsafe {
let j_obj = jni::objects::JString::from_raw(char);
if let Ok(j_str) = _env.get_string_unchecked(j_obj.as_ref()) {
match j_str.to_str() {
Ok(str) => {
let mut w_path = INCOMING_DATA.write();
*w_path = Some(str.to_string());
}
Err(_) => {}
}
};
}
}

View file

@ -29,57 +29,93 @@ fn real_main() {
.parse_default_env() .parse_default_env()
.init(); .init();
// Handle file path argument passing.
let args: Vec<_> = std::env::args().collect();
let mut data = None;
if args.len() > 1 {
let path = std::path::PathBuf::from(&args[1]);
let content = match std::fs::read_to_string(path) {
Ok(s) => Some(s),
Err(_) => Some(args[1].clone())
};
data = content
}
// Setup callback on panic crash. // Setup callback on panic crash.
std::panic::set_hook(Box::new(|info| { std::panic::set_hook(Box::new(|info| {
let backtrace = backtrace::Backtrace::new();
// Format error. // Format error.
let backtrace = backtrace::Backtrace::new();
let time = grim::gui::views::View::format_time(chrono::Utc::now().timestamp()); let time = grim::gui::views::View::format_time(chrono::Utc::now().timestamp());
let target = egui::os::OperatingSystem::from_target_os(); let os = egui::os::OperatingSystem::from_target_os();
let ver = grim::VERSION; let ver = grim::VERSION;
let msg = panic_message::panic_info_message(info); let msg = panic_info_message(info);
let err = format!("{} - {:?} - v{}\n\n{}\n\n{:?}", time, target, ver, msg, backtrace); let loc = if let Some(location) = info.location() {
format!("{}:{}:{}", location.file(), location.line(), location.column())
} else {
"no location found.".parse().unwrap()
};
let err = format!("{} - {:?} - v{}\n{}\n{}\n\n{:?}", time, os, ver, msg, loc, backtrace);
// Save backtrace to file. // Save backtrace to file.
let log = grim::Settings::crash_report_path(); let log = grim::Settings::crash_report_path();
if log.exists() { if log.exists() {
std::fs::remove_file(log.clone()).unwrap(); use std::io::{Seek, SeekFrom, Write};
let mut file = std::fs::OpenOptions::new()
.write(true)
.append(true)
.open(log)
.unwrap();
if file.seek(SeekFrom::End(0)).is_ok() {
file.write(err.as_bytes()).unwrap_or_default();
} }
std::fs::write(log, err.as_bytes()).unwrap(); } else {
// Setup flag to show crash after app restart. std::fs::write(log, err.as_bytes()).unwrap_or_default();
grim::AppConfig::set_show_crash(true); }
// Print message error.
println!("{}\n{}", msg, loc);
})); }));
// Start GUI. // Start GUI.
let _ = std::panic::catch_unwind(|| { let _ = std::panic::catch_unwind(|| {
start_desktop_gui(); if is_app_running(&data) {
return;
} else if let Some(data) = data {
grim::on_data(data);
}
let platform = grim::gui::platform::Desktop::new();
start_app_socket(platform.clone());
start_desktop_gui(platform);
}); });
} }
/// Start GUI with Desktop related setup. /// Get panic message from crash payload.
#[allow(dead_code)] #[allow(dead_code)]
#[cfg(not(target_os = "android"))] #[cfg(not(target_os = "android"))]
fn start_desktop_gui() { fn panic_info_message<'pi>(panic_info: &'pi std::panic::PanicHookInfo<'_>) -> &'pi str {
use grim::AppConfig; let payload = panic_info.payload();
use dark_light::Mode; // taken from: https://github.com/rust-lang/rust/blob/4b9f4b221b92193c7e95b1beb502c6eb32c3b613/library/std/src/panicking.rs#L194-L200
match payload.downcast_ref::<&'static str>() {
let platform = grim::gui::platform::Desktop::default(); Some(msg) => *msg,
None => match payload.downcast_ref::<String>() {
// Setup system theme if not set. Some(msg) => msg.as_str(),
if let None = AppConfig::dark_theme() { // Copy what rustc does in the default panic handler
let dark = match dark_light::detect() { None => "Box<dyn Any>",
Mode::Dark => true, },
Mode::Light => false,
Mode::Default => false
};
AppConfig::set_dark_theme(dark);
} }
}
// Setup window size. /// Start GUI with Desktop related setup passing data from opening.
#[allow(dead_code)]
#[cfg(not(target_os = "android"))]
fn start_desktop_gui(platform: grim::gui::platform::Desktop) {
use grim::AppConfig;
let os = egui::os::OperatingSystem::from_target_os();
let (width, height) = AppConfig::window_size(); let (width, height) = AppConfig::window_size();
let mut viewport = egui::ViewportBuilder::default() let mut viewport = egui::ViewportBuilder::default()
.with_min_inner_size([AppConfig::MIN_WIDTH, AppConfig::MIN_HEIGHT]) .with_min_inner_size([AppConfig::MIN_WIDTH, AppConfig::MIN_HEIGHT])
.with_inner_size([width, height]); .with_inner_size([width, height]);
// Setup an icon.
// Setup icon.
if let Ok(icon) = eframe::icon_data::from_png_bytes(include_bytes!("../img/icon.png")) { if let Ok(icon) = eframe::icon_data::from_png_bytes(include_bytes!("../img/icon.png")) {
viewport = viewport.with_icon(std::sync::Arc::new(icon)); viewport = viewport.with_icon(std::sync::Arc::new(icon));
} }
@ -88,9 +124,10 @@ fn start_desktop_gui() {
viewport = viewport.with_position(egui::pos2(x, y)); viewport = viewport.with_position(egui::pos2(x, y));
} }
// Setup window decorations. // Setup window decorations.
let is_mac = egui::os::OperatingSystem::from_target_os() == egui::os::OperatingSystem::Mac; let is_mac = os == egui::os::OperatingSystem::Mac;
viewport = viewport viewport = viewport
.with_fullsize_content_view(true) .with_fullsize_content_view(true)
.with_window_level(egui::WindowLevel::Normal)
.with_title_shown(false) .with_title_shown(false)
.with_titlebar_buttons_shown(false) .with_titlebar_buttons_shown(false)
.with_titlebar_shown(false) .with_titlebar_shown(false)
@ -102,23 +139,26 @@ fn start_desktop_gui() {
..Default::default() ..Default::default()
}; };
// Use Glow renderer for Windows. // Use Glow renderer for Windows.
let win = egui::os::OperatingSystem::from_target_os() == egui::os::OperatingSystem::Windows; let is_win = os == egui::os::OperatingSystem::Windows;
options.renderer = if win { options.renderer = if is_win {
eframe::Renderer::Glow eframe::Renderer::Glow
} else { } else {
eframe::Renderer::Wgpu eframe::Renderer::Wgpu
}; };
// Start GUI. // Start GUI.
match grim::start(options.clone(), grim::app_creator(grim::gui::App::new(platform.clone()))) { let app = grim::gui::App::new(platform.clone());
match grim::start(options.clone(), grim::app_creator(app)) {
Ok(_) => {} Ok(_) => {}
Err(e) => { Err(e) => {
if win { if is_win {
panic!("{}", e); panic!("{}", e);
} }
// Start with another renderer on error. // Start with another renderer on error.
options.renderer = eframe::Renderer::Glow; options.renderer = eframe::Renderer::Glow;
match grim::start(options, grim::app_creator(grim::gui::App::new(platform))) {
let app = grim::gui::App::new(platform);
match grim::start(options, grim::app_creator(app)) {
Ok(_) => {} Ok(_) => {}
Err(e) => { Err(e) => {
panic!("{}", e); panic!("{}", e);
@ -127,3 +167,123 @@ fn start_desktop_gui() {
} }
} }
} }
/// Check if application is already running to pass data.
#[allow(dead_code)]
#[cfg(not(target_os = "android"))]
fn is_app_running(data: &Option<String>) -> bool {
use tor_rtcompat::BlockOn;
let runtime = tor_rtcompat::tokio::TokioNativeTlsRuntime::create().unwrap();
let res: Result<(), Box<dyn std::error::Error>> = runtime
.block_on(async {
use interprocess::local_socket::{
tokio::{prelude::*, Stream}
};
use tokio::{
io::AsyncWriteExt,
};
let socket_path = grim::Settings::socket_path();
let name = socket_name(&socket_path)?;
// Connect to running application socket.
let conn = Stream::connect(name).await?;
let data = data.clone().unwrap_or("".to_string());
if data.is_empty() {
return Ok(());
}
let (rec, mut sen) = conn.split();
// Send data to socket.
let _ = sen.write_all(data.as_bytes()).await;
drop((rec, sen));
Ok(())
});
match res {
Ok(_) => true,
Err(_) => false
}
}
/// Start desktop socket that handles data for single application instance.
#[allow(dead_code)]
#[cfg(not(target_os = "android"))]
fn start_app_socket(platform: grim::gui::platform::Desktop) {
std::thread::spawn(move || {
use tor_rtcompat::BlockOn;
let runtime = tor_rtcompat::tokio::TokioNativeTlsRuntime::create().unwrap();
let _: Result<_, _> = runtime
.block_on(async {
use interprocess::local_socket::{
tokio::{prelude::*, Stream},
Listener, ListenerOptions,
};
use std::io;
use tokio::{
io::{AsyncBufReadExt, BufReader},
};
use grim::gui::platform::PlatformCallbacks;
// Handle incoming connection.
async fn handle_conn(conn: Stream)
-> io::Result<String> {
let mut read = BufReader::new(&conn);
let mut buffer = String::new();
// Read data.
let _ = read.read_line(&mut buffer).await;
Ok(buffer)
}
// Setup socket name.
let socket_path = grim::Settings::socket_path();
if socket_path.exists() {
let _ = std::fs::remove_file(&socket_path);
}
let name = socket_name(&socket_path)?;
// Create listener.
let opts = ListenerOptions::new().name(name);
let listener = match opts.create_tokio() {
Err(e) if e.kind() == io::ErrorKind::AddrInUse => {
eprintln!("Socket file is occupied.");
return Err::<Listener, io::Error>(e);
}
x => x?,
};
loop {
let conn = match listener.accept().await {
Ok(c) => c,
Err(e) => {
println!("{:?}", e);
continue
}
};
// Handle connection.
let res = handle_conn(conn).await;
match res {
Ok(data) => {
grim::on_data(data);
platform.request_user_attention();
},
Err(_) => {}
}
}
});
});
}
/// Get application socket name from provided path.
#[allow(dead_code)]
#[cfg(not(target_os = "android"))]
fn socket_name(path: &std::path::PathBuf) -> std::io::Result<interprocess::local_socket::Name> {
use interprocess::local_socket::{NameType, ToFsName, ToNsName};
let name = if egui::os::OperatingSystem::Mac != egui::os::OperatingSystem::from_target_os() &&
interprocess::local_socket::GenericNamespaced::is_supported() {
grim::Settings::SOCKET_NAME.to_ns_name::<interprocess::local_socket::GenericNamespaced>()?
} else {
path.clone().to_fs_name::<interprocess::local_socket::GenericFilePath>()?
};
Ok(name)
}

View file

@ -49,8 +49,6 @@ impl PeersConfig {
let chain_type = AppConfig::chain_type(); let chain_type = AppConfig::chain_type();
let config_path = Settings::config_path(Self::FILE_NAME, Some(chain_type.shortname())); let config_path = Settings::config_path(Self::FILE_NAME, Some(chain_type.shortname()));
Settings::write_to_file(self, config_path); Settings::write_to_file(self, config_path);
// Load changes to node server config.
Self::load_to_server_config();
} }
/// Convert string to [`PeerAddr`] if address is in correct format (`host:port`) and available. /// Convert string to [`PeerAddr`] if address is in correct format (`host:port`) and available.
@ -71,7 +69,7 @@ impl PeersConfig {
} }
/// Load saved peers to node server [`ConfigMembers`] config. /// Load saved peers to node server [`ConfigMembers`] config.
pub(crate) fn load_to_server_config() { pub fn load_to_server_config() {
let mut w_config = Settings::node_config_to_update(); let mut w_config = Settings::node_config_to_update();
// Load seeds. // Load seeds.
for seed in w_config.peers.seeds.clone() { for seed in w_config.peers.seeds.clone() {
@ -507,22 +505,29 @@ impl NodeConfig {
} }
/// Get API secret text. /// Get API secret text.
pub fn get_api_secret() -> Option<String> { pub fn get_api_secret(foreign: bool) -> Option<String> {
let r_config = Settings::node_config_to_read(); let r_config = Settings::node_config_to_read();
let api_secret_path = r_config let api_secret_path = if foreign {
&r_config
.node
.server
.foreign_api_secret_path
} else {
&r_config
.node .node
.server .server
.api_secret_path .api_secret_path
.clone(); }.clone();
return if let Some(secret_path) = api_secret_path { if let Some(secret_path) = api_secret_path {
let api_secret_file = File::open(secret_path).unwrap(); if let Ok(file) = File::open(secret_path) {
let buf_reader = BufReader::new(api_secret_file); let buf_reader = BufReader::new(file);
let mut lines_iter = buf_reader.lines(); let mut lines_iter = buf_reader.lines();
let first_line = lines_iter.next().unwrap(); if let Some(Ok(line)) = lines_iter.next() {
Some(first_line.unwrap()) return Some(line);
} else { }
}
}
None None
};
} }
/// Save API secret text. /// Save API secret text.
@ -530,25 +535,6 @@ impl NodeConfig {
Self::save_secret(api_secret, API_SECRET_FILE_NAME); Self::save_secret(api_secret, API_SECRET_FILE_NAME);
} }
/// Get Foreign API secret text.
pub fn get_foreign_api_secret() -> Option<String> {
let r_config = Settings::node_config_to_read();
let foreign_secret_path = r_config
.node
.server
.foreign_api_secret_path
.clone();
return if let Some(secret_path) = foreign_secret_path {
let foreign_secret_file = File::open(secret_path).unwrap();
let buf_reader = BufReader::new(foreign_secret_file);
let mut lines_iter = buf_reader.lines();
let first_line = lines_iter.next().unwrap();
Some(first_line.unwrap())
} else {
None
};
}
/// Update Foreign API secret. /// Update Foreign API secret.
pub fn save_foreign_api_secret(api_secret: &String) { pub fn save_foreign_api_secret(api_secret: &String) {
Self::save_secret(api_secret, FOREIGN_API_SECRET_FILE_NAME); Self::save_secret(api_secret, FOREIGN_API_SECRET_FILE_NAME);
@ -683,10 +669,9 @@ impl NodeConfig {
/// Toggle seeding type to use default or custom seed list. /// Toggle seeding type to use default or custom seed list.
pub fn toggle_seeding_type() { pub fn toggle_seeding_type() {
let seeding_type = if Self::is_default_seeding_type() { let seeding_type = match Self::is_default_seeding_type() {
Seeding::List true => Seeding::List,
} else { false => Seeding::DNSSeed
Seeding::DNSSeed
}; };
let mut w_config = Settings::node_config_to_update(); let mut w_config = Settings::node_config_to_update();
w_config.node.server.p2p_config.seeding_type = seeding_type; w_config.node.server.p2p_config.seeding_type = seeding_type;

View file

@ -15,7 +15,6 @@
//! Build a block to mine: gathers transactions from the pool, assembles //! Build a block to mine: gathers transactions from the pool, assembles
//! them into a block and returns it. //! them into a block and returns it.
use std::panic::panic_any;
use chrono::prelude::{DateTime, Utc}; use chrono::prelude::{DateTime, Utc};
use rand::{thread_rng, Rng}; use rand::{thread_rng, Rng};
use serde_json::{json, Value}; use serde_json::{json, Value};
@ -77,7 +76,7 @@ pub fn get_block(
key_id: Option<Identifier>, key_id: Option<Identifier>,
wallet_listener_url: Option<String>, wallet_listener_url: Option<String>,
stop_state: &Arc<StratumStopState> stop_state: &Arc<StratumStopState>
) -> (core::Block, BlockFees) { ) -> Option<(core::Block, BlockFees)> {
let wallet_retry_interval = 5; let wallet_retry_interval = 5;
// get the latest chain state and build a block on top of it // get the latest chain state and build a block on top of it
let mut result = build_block(chain, tx_pool, key_id.clone(), wallet_listener_url.clone()); let mut result = build_block(chain, tx_pool, key_id.clone(), wallet_listener_url.clone());
@ -116,12 +115,11 @@ pub fn get_block(
// Stop attempts to build a block on stop. // Stop attempts to build a block on stop.
if stop_state.is_stopped() { if stop_state.is_stopped() {
panic_any("Stopped"); return None;
} }
result = build_block(chain, tx_pool, new_key_id, wallet_listener_url.clone()); result = build_block(chain, tx_pool, new_key_id, wallet_listener_url.clone());
} }
return result.unwrap(); Some(result.unwrap())
} }
/// Builds a new block with the chain head as previous and eligible /// Builds a new block with the chain head as previous and eligible

View file

@ -24,10 +24,12 @@ use futures::channel::oneshot;
use grin_chain::SyncStatus; use grin_chain::SyncStatus;
use grin_core::global; use grin_core::global;
use grin_core::global::ChainTypes; use grin_core::global::ChainTypes;
use grin_p2p::msg::PeerAddrs;
use grin_p2p::Seeding;
use grin_servers::{Server, ServerStats, StratumServerConfig, StratumStats}; use grin_servers::{Server, ServerStats, StratumServerConfig, StratumStats};
use grin_servers::common::types::Error; use grin_servers::common::types::Error;
use crate::node::{NodeConfig, NodeError}; use crate::node::{NodeConfig, NodeError, PeersConfig};
use crate::node::stratum::{StratumStopState, StratumServer}; use crate::node::stratum::{StratumStopState, StratumServer};
lazy_static! { lazy_static! {
@ -83,6 +85,16 @@ impl Node {
/// Delay for thread to update the stats. /// Delay for thread to update the stats.
pub const STATS_UPDATE_DELAY: Duration = Duration::from_millis(1000); pub const STATS_UPDATE_DELAY: Duration = Duration::from_millis(1000);
/// Default Mainnet DNS Seeds
pub const MAINNET_DNS_SEEDS: &'static[&'static str] = &[
"mainnet.seed.grin.lesceller.com",
"grinseed.revcore.net",
"mainnet-seed.grinnode.live",
"mainnet.grin.punksec.de",
"grinnode.30-r.com",
"grincoin.org"
];
/// Stop the [`Server`] and setup exit flag after if needed. /// Stop the [`Server`] and setup exit flag after if needed.
pub fn stop(exit_after_stop: bool) { pub fn stop(exit_after_stop: bool) {
NODE_STATE.stop_needed.store(true, Ordering::Relaxed); NODE_STATE.stop_needed.store(true, Ordering::Relaxed);
@ -516,10 +528,29 @@ impl Node {
/// Start the node [`Server`]. /// Start the node [`Server`].
fn start_node_server() -> Result<Server, Error> { fn start_node_server() -> Result<Server, Error> {
// Get saved server config. // Setup server config.
PeersConfig::load_to_server_config();
let config = NodeConfig::node_server_config(); let config = NodeConfig::node_server_config();
let mut server_config = config.server.clone(); let mut server_config = config.server.clone();
// Setup Mainnet DNSSeed
if server_config.chain_type == ChainTypes::Mainnet && NodeConfig::is_default_seeding_type() {
server_config.p2p_config.seeding_type = Seeding::List;
server_config.p2p_config.seeds = Some(PeerAddrs::default());
for seed in Node::MAINNET_DNS_SEEDS {
let addr = format!("{}:3414", seed);
if let Some(p) = PeersConfig::peer_to_addr(addr) {
let mut seeds = server_config
.p2p_config
.seeds
.clone()
.unwrap_or(PeerAddrs::default());
seeds.peers.insert(seeds.peers.len(), p);
server_config.p2p_config.seeds = Some(seeds);
}
}
}
// Fix to avoid too many opened files. // Fix to avoid too many opened files.
server_config.p2p_config.peer_min_preferred_outbound_count = server_config.p2p_config.peer_min_preferred_outbound_count =
server_config.p2p_config.peer_max_outbound_count; server_config.p2p_config.peer_max_outbound_count;
@ -596,10 +627,6 @@ fn start_node_server() -> Result<Server, Error> {
let api_chan: &'static mut (oneshot::Sender<()>, oneshot::Receiver<()>) = let api_chan: &'static mut (oneshot::Sender<()>, oneshot::Receiver<()>) =
Box::leak(Box::new(oneshot::channel::<()>())); Box::leak(Box::new(oneshot::channel::<()>()));
let server_result = Server::new(server_config, None, api_chan); let server_result = Server::new(server_config, None, api_chan);
// Delay after server start.
thread::sleep(Duration::from_millis(5000));
server_result server_result
} }

View file

@ -604,14 +604,13 @@ impl Handler {
let clear_blocks = current_hash != latest_hash; let clear_blocks = current_hash != latest_hash;
// Build the new block (version) // Build the new block (version)
let (new_block, block_fees) = get_block( if let Some((new_block, block_fees)) = get_block(
&self.chain, &self.chain,
tx_pool, tx_pool,
state.current_key_id.clone(), state.current_key_id.clone(),
wallet_listener_url, wallet_listener_url,
&stop_state &stop_state
); ) {
// scaled difficulty // scaled difficulty
state.current_difficulty = state.current_difficulty =
(new_block.header.total_difficulty() - head.total_difficulty).to_num(); (new_block.header.total_difficulty() - head.total_difficulty).to_num();
@ -636,8 +635,12 @@ impl Handler {
self.workers.update_network_difficulty(difficulty.to_num()); self.workers.update_network_difficulty(difficulty.to_num());
self.workers.update_network_hashrate(); self.workers.update_network_hashrate();
// Add this new block candidate onto our list of block versions for this height // Add this new block candidate onto our list of block versions for height
state.current_block_versions.push(new_block); state.current_block_versions.push(new_block);
} else {
thread::sleep(Duration::from_millis(1500));
break;
}
} }
// Send this job to all connected workers // Send this job to all connected workers
self.broadcast_job(); self.broadcast_job();
@ -716,7 +719,7 @@ fn accept_connections(listen_addr: SocketAddr,
let mut rt = Runtime::new().unwrap(); let mut rt = Runtime::new().unwrap();
let (task, handle) = abortable(task); let (task, handle) = abortable(task);
rt.spawn(check_stop_state(stop_state, handle)); rt.spawn(check_stop_state(stop_state, handle));
rt.block_on(task).unwrap(); rt.block_on(task).unwrap_or_default();
} }
async fn check_stop_state(stop_state: Arc<StratumStopState>, handle: AbortHandle) { async fn check_stop_state(stop_state: Arc<StratumStopState>, handle: AbortHandle) {

View file

@ -16,7 +16,7 @@ use grin_core::global::ChainTypes;
use serde_derive::{Deserialize, Serialize}; use serde_derive::{Deserialize, Serialize};
use crate::gui::views::Content; use crate::gui::views::Content;
use crate::node::{NodeConfig, PeersConfig}; use crate::node::NodeConfig;
use crate::Settings; use crate::Settings;
use crate::wallet::ConnectionsConfig; use crate::wallet::ConnectionsConfig;
@ -49,9 +49,6 @@ pub struct AppConfig {
/// Flag to check if dark theme should be used, use system settings if not set. /// Flag to check if dark theme should be used, use system settings if not set.
use_dark_theme: Option<bool>, use_dark_theme: Option<bool>,
/// Flag to show crash report when happened.
show_crash: Option<bool>
} }
impl Default for AppConfig { impl Default for AppConfig {
@ -68,7 +65,6 @@ impl Default for AppConfig {
y: None, y: None,
lang: None, lang: None,
use_dark_theme: None, use_dark_theme: None,
show_crash: None,
} }
} }
} }
@ -113,10 +109,6 @@ impl AppConfig {
w_node_config.node = node_config.node; w_node_config.node = node_config.node;
w_node_config.peers = node_config.peers; w_node_config.peers = node_config.peers;
} }
// Load saved peers to node config.
{
PeersConfig::load_to_server_config();
}
// Load connections configuration // Load connections configuration
{ {
let mut w_conn_config = Settings::conn_config_to_update(); let mut w_conn_config = Settings::conn_config_to_update();
@ -245,17 +237,4 @@ impl AppConfig {
w_config.use_dark_theme = Some(use_dark); w_config.use_dark_theme = Some(use_dark);
w_config.save(); w_config.save();
} }
/// Check if crash report should be shown on application start.
pub fn show_crash() -> bool {
let r_config = Settings::app_config_to_read();
r_config.show_crash.unwrap_or(false)
}
/// Setup flag to show crash report on application start.
pub fn set_show_crash(show: bool) {
let mut w_config = Settings::app_config_to_update();
w_config.show_crash = Some(show);
w_config.save();
}
} }

View file

@ -20,7 +20,6 @@ use lazy_static::lazy_static;
use parking_lot::{RwLock, RwLockReadGuard, RwLockWriteGuard}; use parking_lot::{RwLock, RwLockReadGuard, RwLockWriteGuard};
use serde::de::DeserializeOwned; use serde::de::DeserializeOwned;
use serde::Serialize; use serde::Serialize;
use grin_config::ConfigError; use grin_config::ConfigError;
use crate::node::NodeConfig; use crate::node::NodeConfig;
@ -48,9 +47,10 @@ pub struct Settings {
impl Settings { impl Settings {
/// Main application directory name. /// Main application directory name.
pub const MAIN_DIR_NAME: &'static str = ".grim"; pub const MAIN_DIR_NAME: &'static str = ".grim";
/// Crash report file name. /// Crash report file name.
pub const CRASH_REPORT_FILE_NAME: &'static str = "crash.log"; pub const CRASH_REPORT_FILE_NAME: &'static str = "crash.log";
/// Application socket name.
pub const SOCKET_NAME: &'static str = "grim.sock";
/// Initialize settings with app and node configs. /// Initialize settings with app and node configs.
fn init() -> Self { fn init() -> Self {
@ -126,10 +126,7 @@ impl Settings {
/// Get base directory path for configuration. /// Get base directory path for configuration.
pub fn base_path(sub_dir: Option<String>) -> PathBuf { pub fn base_path(sub_dir: Option<String>) -> PathBuf {
// Check if dir exists. // Check if dir exists.
let mut path = match dirs::home_dir() { let mut path = dirs::home_dir().unwrap_or_else(|| PathBuf::new());
Some(p) => p,
None => PathBuf::new(),
};
path.push(Self::MAIN_DIR_NAME); path.push(Self::MAIN_DIR_NAME);
if sub_dir.is_some() { if sub_dir.is_some() {
path.push(sub_dir.unwrap()); path.push(sub_dir.unwrap());
@ -141,20 +138,35 @@ impl Settings {
path path
} }
/// Get configuration file path from provided name and sub-directory if needed. /// Get desktop application socket path.
pub fn socket_path() -> PathBuf {
let mut socket_path = Self::base_path(None);
socket_path.push(Self::SOCKET_NAME);
socket_path
}
/// Get configuration file path from provided name and subdirectory if needed.
pub fn config_path(config_name: &str, sub_dir: Option<String>) -> PathBuf { pub fn config_path(config_name: &str, sub_dir: Option<String>) -> PathBuf {
let mut path = Self::base_path(sub_dir); let mut path = Self::base_path(sub_dir);
path.push(config_name); path.push(config_name);
path path
} }
/// Get configuration file path from provided name and sub-directory if needed. /// Get configuration file path from provided name and subdirectory if needed.
pub fn crash_report_path() -> PathBuf { pub fn crash_report_path() -> PathBuf {
let mut path = Self::base_path(None); let mut path = Self::base_path(None);
path.push(Self::CRASH_REPORT_FILE_NAME); path.push(Self::CRASH_REPORT_FILE_NAME);
path path
} }
/// Delete crash report file.
pub fn delete_crash_report() {
let log = Self::crash_report_path();
if log.exists() {
let _ = fs::remove_file(log.clone());
}
}
/// Read configuration from the file. /// Read configuration from the file.
pub fn read_from_file<T: DeserializeOwned>(config_path: PathBuf) -> Result<T, ConfigError> { pub fn read_from_file<T: DeserializeOwned>(config_path: PathBuf) -> Result<T, ConfigError> {
let file_content = fs::read_to_string(config_path.clone())?; let file_content = fs::read_to_string(config_path.clone())?;

258
src/tor/http.rs Normal file
View file

@ -0,0 +1,258 @@
// Copyright 2024 The Grim Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::future::Future;
use std::io::Error;
use std::pin::Pin;
use std::sync::Arc;
use std::task::{Context, Poll};
use arti_client::{DataStream, IntoTorAddr, TorClient};
use hyper::client::connect::{Connected, Connection};
use hyper::http::uri::Scheme;
use hyper::http::Uri;
use hyper::service::Service;
use pin_project::pin_project;
use thiserror::Error;
use tls_api::TlsConnector as TlsConn; // This is different from tor_rtcompat::TlsConnector
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
use tor_config::deps::educe::Educe;
use tor_rtcompat::Runtime;
/// Error making or using http connection
///
/// This error ends up being passed to hyper and bundled up into a [`hyper::Error`]
#[derive(Error, Clone, Debug)]
#[non_exhaustive]
pub enum ConnectionError {
/// Unsupported URI scheme
#[error("unsupported URI scheme in {uri:?}")]
UnsupportedUriScheme {
/// URI
uri: Uri,
},
/// Missing hostname
#[error("Missing hostname in {uri:?}")]
MissingHostname {
/// URI
uri: Uri,
},
/// Tor connection failed
#[error("Tor connection failed")]
Arti(#[from] arti_client::Error),
/// TLS connection failed
#[error("TLS connection failed")]
TLS(#[source] Arc<anyhow::Error>),
}
/// We implement this for form's sake
impl tor_error::HasKind for ConnectionError {
#[rustfmt::skip]
fn kind(&self) -> tor_error::ErrorKind {
use ConnectionError as CE;
use tor_error::ErrorKind as EK;
match self {
CE::UnsupportedUriScheme{..} => EK::NotImplemented,
CE::MissingHostname{..} => EK::BadApiUsage,
CE::Arti(e) => e.kind(),
CE::TLS(_) => EK::RemoteProtocolViolation,
}
}
}
/// **Main entrypoint**: `hyper` connector to make HTTP\[S] connections via Tor, using Arti.
///
/// An `ArtiHttpConnector` combines an Arti Tor client, and a TLS implementation,
/// in a form that can be provided to hyper
/// (e.g. to [`hyper::client::Builder`]'s `build` method)
/// so that hyper can speak HTTP and HTTPS to origin servers via Tor.
///
/// TC is the TLS to used *across* Tor to connect to the origin server.
/// For example, it could be a [`tls_api_native_tls::TlsConnector`].
/// This is a different Rust type to the TLS used *by* Tor to connect to relays etc.
/// It might even be a different underlying TLS implementation
/// (although that is usually not a particularly good idea).
#[derive(Educe)]
#[educe(Clone)] // #[derive(Debug)] infers an unwanted bound TC: Clone
pub struct ArtiHttpConnector<R: Runtime, TC: TlsConn> {
/// The client
client: TorClient<R>,
/// TLS for using across Tor.
tls_conn: Arc<TC>,
}
// #[derive(Clone)] infers a TC: Clone bound
impl<R: Runtime, TC: TlsConn> ArtiHttpConnector<R, TC> {
/// Make a new `ArtiHttpConnector` using an Arti `TorClient` object.
pub fn new(client: TorClient<R>, tls_conn: TC) -> Self {
let tls_conn = tls_conn.into();
Self { client, tls_conn }
}
}
/// Wrapper type that makes an Arti `DataStream` implement necessary traits to be used as
/// a `hyper` connection object (mainly `Connection`).
///
/// This might represent a bare HTTP connection across Tor,
/// or it might represent an HTTPS connection through Tor to an origin server,
/// `TC::TlsStream` as the TLS layer.
///
/// An `ArtiHttpConnection` is constructed by hyper's use of the [`ArtiHttpConnector`]
/// implementation of [`hyper::service::Service`],
/// and then used by hyper as the transport for hyper's HTTP implementation.
#[pin_project]
pub struct ArtiHttpConnection<TC: TlsConn> {
/// The stream
#[pin]
inner: MaybeHttpsStream<TC>,
}
/// The actual stream; might be TLS, might not
#[pin_project(project = MaybeHttpsStreamProj)]
enum MaybeHttpsStream<TC: TlsConn> {
/// http
Http(Pin<Box<DataStream>>), // Tc:TlsStream is generally boxed; box this one too
/// https
Https(#[pin] TC::TlsStream),
}
impl<TC: TlsConn> Connection for ArtiHttpConnection<TC> {
fn connected(&self) -> Connected {
Connected::new()
}
}
// These trait implementations just defer to the inner `DataStream`; the wrapper type is just
// there to implement the `Connection` trait.
impl<TC: TlsConn> AsyncRead for ArtiHttpConnection<TC> {
fn poll_read(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut ReadBuf<'_>,
) -> Poll<Result<(), std::io::Error>> {
match self.project().inner.project() {
MaybeHttpsStreamProj::Http(ds) => ds.as_mut().poll_read(cx, buf),
MaybeHttpsStreamProj::Https(t) => t.poll_read(cx, buf),
}
}
}
impl<TC: TlsConn> AsyncWrite for ArtiHttpConnection<TC> {
fn poll_write(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<Result<usize, Error>> {
match self.project().inner.project() {
MaybeHttpsStreamProj::Http(ds) => ds.as_mut().poll_write(cx, buf),
MaybeHttpsStreamProj::Https(t) => t.poll_write(cx, buf),
}
}
fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Error>> {
match self.project().inner.project() {
MaybeHttpsStreamProj::Http(ds) => ds.as_mut().poll_flush(cx),
MaybeHttpsStreamProj::Https(t) => t.poll_flush(cx),
}
}
fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Error>> {
match self.project().inner.project() {
MaybeHttpsStreamProj::Http(ds) => ds.as_mut().poll_shutdown(cx),
MaybeHttpsStreamProj::Https(t) => t.poll_shutdown(cx),
}
}
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
/// Are we doing TLS?
enum UseTls {
/// No
Bare,
/// Yes
Tls,
}
/// Convert uri to http\[s\] host and port, and whether to do tls
fn uri_to_host_port_tls(uri: Uri) -> Result<(String, u16, UseTls), ConnectionError> {
let use_tls = {
// Scheme doesn't derive PartialEq so can't be matched on
let scheme = uri.scheme();
if scheme == Some(&Scheme::HTTP) {
UseTls::Bare
} else if scheme == Some(&Scheme::HTTPS) {
UseTls::Tls
} else {
return Err(ConnectionError::UnsupportedUriScheme { uri });
}
};
let host = match uri.host() {
Some(h) => h,
_ => return Err(ConnectionError::MissingHostname { uri }),
};
let port = uri.port().map(|x| x.as_u16()).unwrap_or(match use_tls {
UseTls::Tls => 443,
UseTls::Bare => 80,
});
Ok((host.to_owned(), port, use_tls))
}
impl<R: Runtime, TC: TlsConn> Service<Uri> for ArtiHttpConnector<R, TC> {
type Response = ArtiHttpConnection<TC>;
type Error = ConnectionError;
#[allow(clippy::type_complexity)]
type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
fn call(&mut self, req: Uri) -> Self::Future {
// `TorClient` objects can be cloned cheaply (the cloned objects refer to the same
// underlying handles required to make Tor connections internally).
// We use this to avoid the returned future having to borrow `self`.
let client = self.client.clone();
let tls_conn = self.tls_conn.clone();
Box::pin(async move {
// Extract the host and port to connect to from the URI.
let (host, port, use_tls) = uri_to_host_port_tls(req)?;
// Initiate a new Tor connection, producing a `DataStream` if successful.
let addr = (&host as &str, port)
.into_tor_addr()
.map_err(arti_client::Error::from)?;
let ds = client.connect(addr).await?;
let inner = match use_tls {
UseTls::Tls => {
let conn = tls_conn
.connect_impl_tls_stream(&host, ds)
.await
.map_err(|e| ConnectionError::TLS(e.into()))?;
MaybeHttpsStream::Https(conn)
}
UseTls::Bare => MaybeHttpsStream::Http(Box::new(ds).into()),
};
Ok(ArtiHttpConnection { inner })
})
}
}

View file

@ -20,3 +20,5 @@ pub use tor::Tor;
mod types; mod types;
pub use types::*; pub use types::*;
mod http;

View file

@ -24,7 +24,6 @@ use std::time::Duration;
use arti_client::config::{CfgPath, TorClientConfigBuilder}; use arti_client::config::{CfgPath, TorClientConfigBuilder};
use arti_client::{TorClient, TorClientConfig}; use arti_client::{TorClient, TorClientConfig};
use arti_hyper::ArtiHttpConnector;
use curve25519_dalek::digest::Digest; use curve25519_dalek::digest::Digest;
use ed25519_dalek::hazmat::ExpandedSecretKey; use ed25519_dalek::hazmat::ExpandedSecretKey;
use fs_mistrust::Mistrust; use fs_mistrust::Mistrust;
@ -32,6 +31,7 @@ use grin_util::secp::SecretKey;
use hyper::{Body, Uri}; use hyper::{Body, Uri};
use parking_lot::RwLock; use parking_lot::RwLock;
use sha2::Sha512; use sha2::Sha512;
use tls_api_native_tls::TlsConnector;
use tls_api::{TlsConnector as TlsConnectorTrait, TlsConnectorBuilder}; use tls_api::{TlsConnector as TlsConnectorTrait, TlsConnectorBuilder};
use tokio::time::sleep; use tokio::time::sleep;
use tor_hscrypto::pk::{HsIdKey, HsIdKeypair}; use tor_hscrypto::pk::{HsIdKey, HsIdKeypair};
@ -48,15 +48,7 @@ use tor_llcrypto::pk::ed25519::ExpandedKeypair;
use tor_rtcompat::tokio::TokioNativeTlsRuntime; use tor_rtcompat::tokio::TokioNativeTlsRuntime;
use tor_rtcompat::Runtime; use tor_rtcompat::Runtime;
// On aarch64-apple-darwin targets there is an issue with the native and rustls use crate::tor::http::ArtiHttpConnector;
// tls implementation so this makes it fall back to the openssl variant.
//
// https://gitlab.torproject.org/tpo/core/arti/-/issues/715
#[cfg(not(all(target_vendor = "apple", target_arch = "aarch64")))]
use tls_api_native_tls::TlsConnector;
#[cfg(all(target_vendor = "apple", target_arch = "aarch64"))]
use tls_api_openssl::TlsConnector;
use crate::tor::TorConfig; use crate::tor::TorConfig;
lazy_static! { lazy_static! {
@ -81,20 +73,16 @@ pub struct Tor {
impl Default for Tor { impl Default for Tor {
fn default() -> Self { fn default() -> Self {
// Cleanup keys, state and cache on start.
fs::remove_dir_all(TorConfig::keystore_path()).unwrap_or_default();
fs::remove_dir_all(TorConfig::state_path()).unwrap_or_default();
fs::remove_dir_all(TorConfig::cache_path()).unwrap_or_default();
// Create Tor client.
let runtime = TokioNativeTlsRuntime::create().unwrap(); let runtime = TokioNativeTlsRuntime::create().unwrap();
let config = Self::build_config(); let config = Self::build_config();
let client = if let Ok(c) = TorClient::with_runtime(runtime) let client = TorClient::with_runtime(runtime)
.config(config.clone()) .config(config.clone())
.create_unbootstrapped() { .create_unbootstrapped().unwrap();
c
} else {
fs::remove_dir_all(TorConfig::state_path()).unwrap();
fs::remove_dir_all(TorConfig::cache_path()).unwrap();
let runtime = TokioNativeTlsRuntime::create().unwrap();
TorClient::with_runtime(runtime)
.config(config.clone())
.create_unbootstrapped().unwrap()
};
Self { Self {
running_services: Arc::new(RwLock::new(BTreeMap::new())), running_services: Arc::new(RwLock::new(BTreeMap::new())),
starting_services: Arc::new(RwLock::new(BTreeSet::new())), starting_services: Arc::new(RwLock::new(BTreeSet::new())),
@ -314,8 +302,8 @@ impl Tor {
let mut w_services = let mut w_services =
TOR_SERVER_STATE.starting_services.write(); TOR_SERVER_STATE.starting_services.write();
w_services.remove(&service_id); w_services.remove(&service_id);
// Check again after 15 seconds. // Check again after 50 seconds.
Duration::from_millis(15000) Duration::from_millis(50000)
} }
Err(_) => { Err(_) => {
// Restart service on 3rd error. // Restart service on 3rd error.
@ -409,10 +397,10 @@ impl Tor {
hs_nickname: &HsNickname, hs_nickname: &HsNickname,
) -> tor_keymgr::Result<()> { ) -> tor_keymgr::Result<()> {
let arti_store = let arti_store =
ArtiNativeKeystore::from_path_and_mistrust(TorConfig::keystore_path(), &mistrust)?; ArtiNativeKeystore::from_path_and_mistrust(TorConfig::keystore_path(), mistrust)?;
let key_manager = KeyMgrBuilder::default() let key_manager = KeyMgrBuilder::default()
.default_store(Box::new(arti_store)) .primary_store(Box::new(arti_store))
.build() .build()
.unwrap(); .unwrap();
@ -427,13 +415,15 @@ impl Tor {
key_manager.insert( key_manager.insert(
HsIdKey::from(expanded_kp.public().clone()), HsIdKey::from(expanded_kp.public().clone()),
&HsIdPublicKeySpecifier::new(hs_nickname.clone()), &HsIdPublicKeySpecifier::new(hs_nickname.clone()),
KeystoreSelector::Default, KeystoreSelector::Primary,
true
)?; )?;
key_manager.insert( key_manager.insert(
HsIdKeypair::from(expanded_kp), HsIdKeypair::from(expanded_kp),
&HsIdKeypairSpecifier::new(hs_nickname.clone()), &HsIdKeypairSpecifier::new(hs_nickname.clone()),
KeystoreSelector::Default, KeystoreSelector::Primary,
true
)?; )?;
Ok(()) Ok(())
} }
@ -444,7 +434,7 @@ impl Tor {
builder.bridges().bridges().push(bridge); builder.bridges().bridges().push(bridge);
} }
// Now configure an snowflake transport. (Requires the "pt-client" feature) // Now configure a snowflake transport. (Requires the "pt-client" feature)
let mut transport = TransportConfigBuilder::default(); let mut transport = TransportConfigBuilder::default();
transport transport
.protocols(vec!["snowflake".parse().unwrap()]) .protocols(vec!["snowflake".parse().unwrap()])

View file

@ -22,6 +22,7 @@ use rand::Rng;
use serde_derive::{Deserialize, Serialize}; use serde_derive::{Deserialize, Serialize};
use crate::{AppConfig, Settings}; use crate::{AppConfig, Settings};
use crate::wallet::ConnectionsConfig;
use crate::wallet::types::ConnectionMethod; use crate::wallet::types::ConnectionMethod;
/// Wallet configuration. /// Wallet configuration.
@ -75,11 +76,11 @@ impl WalletConfig {
name, name,
ext_conn_id: match conn_method { ext_conn_id: match conn_method {
ConnectionMethod::Integrated => None, ConnectionMethod::Integrated => None,
ConnectionMethod::External(id) => Some(*id) ConnectionMethod::External(id, _) => Some(*id)
}, },
min_confirmations: MIN_CONFIRMATIONS_DEFAULT, min_confirmations: MIN_CONFIRMATIONS_DEFAULT,
use_dandelion: Some(true), use_dandelion: Some(true),
enable_tor_listener: Some(true), enable_tor_listener: Some(false),
api_port: Some(rand::thread_rng().gen_range(10000..30000)), api_port: Some(rand::thread_rng().gen_range(10000..30000)),
}; };
Settings::write_to_file(&config, config_path); Settings::write_to_file(&config, config_path);
@ -116,6 +117,18 @@ impl WalletConfig {
None None
} }
/// Get wallet connection method.
pub fn connection(&self) -> ConnectionMethod {
if let Some(ext_conn_id) = self.ext_conn_id {
if let Some(conn) = ConnectionsConfig::ext_conn(ext_conn_id) {
if !conn.deleted {
return ConnectionMethod::External(conn.id, conn.url);
}
}
}
ConnectionMethod::Integrated
}
/// Save wallet config. /// Save wallet config.
pub fn save(&self) { pub fn save(&self) {
let config_path = Self::get_config_file_path(self.chain_type, self.id); let config_path = Self::get_config_file_path(self.chain_type, self.id);

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