Compare commits
109 commits
Author | SHA1 | Date | |
---|---|---|---|
1d9b7d9698 | |||
82c05588bc | |||
1cddd05bc0 | |||
8ad0d1c461 | |||
a22a75913c | |||
e797da0ed8 | |||
6936c14ed2 | |||
c626ed5a48 | |||
d79d05ef5a | |||
|
094a5b8969 | ||
|
12a75f8370 | ||
|
1c14b9aa93 | ||
|
8ea388554a | ||
|
1531c201bb | ||
|
ed522c56ae | ||
|
4b454ab2f3 | ||
|
f6fbf7226e | ||
|
ebd09ab1c8 | ||
|
75cf7edc96 | ||
|
5c8b9c40be | ||
|
dcaf9945c8 | ||
|
f9426287d5 | ||
|
77281e3ab9 | ||
|
64439ad3d3 | ||
|
9494c1292e | ||
|
accf123d49 | ||
|
d77598c259 | ||
|
4e6dff52fe | ||
|
92d0aac250 | ||
|
5ef310558a | ||
|
683821b667 | ||
|
da4cf71fac | ||
|
f81ceae940 | ||
|
fa6301a1db | ||
|
442fc425f7 | ||
|
ea61588ede | ||
|
7f67aa134a | ||
|
d7d1c53c52 | ||
|
18f52f877a | ||
|
c13195bd61 | ||
|
e40d5b6474 | ||
|
92e5d38755 | ||
|
ec7e795ba9 | ||
|
af220b2a09 | ||
|
846e30cb38 | ||
|
d371d4368b | ||
|
85fc8101e4 | ||
|
e2f58a8938 | ||
|
7e6954afd9 | ||
|
bed041a1c3 | ||
|
f955f720d2 | ||
|
b627ac1ca6 | ||
|
ac0b218376 | ||
|
04bf5a5349 | ||
|
9cce52a7d9 | ||
|
51e0d87d27 | ||
|
d6f7e2e976 | ||
|
0bbf395a62 | ||
|
609d7ceb7a | ||
|
b91605864d | ||
|
7857b708c9 | ||
|
a0f85538e9 | ||
|
c52da4f479 | ||
|
af597df7b1 | ||
|
2adb29f4ee | ||
|
2b83944f34 | ||
|
71e80f6df7 | ||
|
0ead11ec6c | ||
|
3e249c5314 | ||
|
bacc87945c | ||
|
2cfd428c4c | ||
|
c155deedb5 | ||
|
3bc8c407b4 | ||
|
c3fae38d5c | ||
|
d6ec4213ab | ||
|
150a0de1c4 | ||
|
7cedebc70e | ||
|
fe5aca6f0e | ||
|
5d83710fed | ||
|
1431e307ee | ||
|
1934dc3377 | ||
|
8af06d8860 | ||
|
9ea0da95b7 | ||
|
d39e2ec21e | ||
|
68c9c9df04 | ||
|
6f7156ef17 | ||
|
50638ff54e | ||
|
8594279b98 | ||
|
0205e01b3c | ||
|
17545c1b7c | ||
|
bcf821c06a | ||
|
34376d3490 | ||
|
8ed2308340 | ||
|
c73cd58eed | ||
|
d78ec570b0 | ||
|
dd45f7ce38 | ||
|
fb7312cb80 | ||
|
dbc28205e8 | ||
|
a3ed3bd234 | ||
|
21ecf200b8 | ||
|
c8bca08bdc | ||
|
68bd2b81ec | ||
|
09cfb84b94 | ||
|
5c1ffb5636 | ||
|
7f79cc0708 | ||
|
b0b4f9068a | ||
|
cb9e86750c | ||
|
86fbf2e14f | ||
|
e0351cea84 |
107 changed files with 10174 additions and 9499 deletions
37
.github/workflows/build.yml
vendored
37
.github/workflows/build.yml
vendored
|
@ -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
|
||||||
|
|
134
.github/workflows/release.yml
vendored
134
.github/workflows/release.yml
vendored
|
@ -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
4
.gitignore
vendored
|
@ -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
4427
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
137
Cargo.toml
137
Cargo.toml
|
@ -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" }
|
|
|
@ -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
|
||||||
|
|
|
@ -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,24 +10,36 @@ 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"
|
||||||
}
|
}
|
||||||
|
|
||||||
signingConfigs {
|
def keystorePropertiesFile = rootProject.file("keystore.properties")
|
||||||
release {
|
def keystoreProperties = new Properties()
|
||||||
keyAlias keystoreProperties['keyAlias']
|
if (keystorePropertiesFile.exists()) {
|
||||||
keyPassword keystoreProperties['keyPassword']
|
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
|
||||||
storeFile file(keystoreProperties['storeFile'])
|
|
||||||
storePassword keystoreProperties['storePassword']
|
signingConfigs {
|
||||||
|
release {
|
||||||
|
keyAlias keystoreProperties['keyAlias']
|
||||||
|
keyPassword keystoreProperties['keyPassword']
|
||||||
|
storeFile file(keystoreProperties['storeFile'])
|
||||||
|
storePassword keystoreProperties['storePassword']
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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'
|
||||||
signingConfig signingConfigs.release
|
}
|
||||||
|
if (keystorePropertiesFile.exists()) {
|
||||||
|
signedRelease {
|
||||||
|
initWith release
|
||||||
|
signingConfig signingConfigs.release
|
||||||
|
}
|
||||||
}
|
}
|
||||||
debug {
|
debug {
|
||||||
minifyEnabled false
|
minifyEnabled false
|
||||||
|
|
|
@ -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" />
|
||||||
|
|
|
@ -152,13 +152,17 @@ 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);
|
||||||
mNotificationBuilder = new NotificationCompat.Builder(this, TAG)
|
try {
|
||||||
.setContentTitle(this.getSyncTitle())
|
mNotificationBuilder = new NotificationCompat.Builder(this, TAG)
|
||||||
.setContentText(this.getSyncStatusText())
|
.setContentTitle(this.getSyncTitle())
|
||||||
.setStyle(new NotificationCompat.BigTextStyle().bigText(this.getSyncStatusText()))
|
.setContentText(this.getSyncStatusText())
|
||||||
.setSmallIcon(R.drawable.ic_stat_name)
|
.setStyle(new NotificationCompat.BigTextStyle().bigText(this.getSyncStatusText()))
|
||||||
.setPriority(NotificationCompat.PRIORITY_MAX)
|
.setSmallIcon(R.drawable.ic_stat_name)
|
||||||
.setContentIntent(pendingIntent);
|
.setPriority(NotificationCompat.PRIORITY_MAX)
|
||||||
|
.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.
|
||||||
|
|
|
@ -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();
|
||||||
|
@ -105,11 +125,11 @@ public class MainActivity extends GameActivity {
|
||||||
File file = new File(getExternalCacheDir(), name);
|
File file = new File(getExternalCacheDir(), name);
|
||||||
try (InputStream is = getContentResolver().openInputStream(uri);
|
try (InputStream is = getContentResolver().openInputStream(uri);
|
||||||
OutputStream os = new FileOutputStream(file)) {
|
OutputStream os = new FileOutputStream(file)) {
|
||||||
byte[] buffer = new byte[1024];
|
byte[] buffer = new byte[1024];
|
||||||
int length;
|
int length;
|
||||||
while ((length = is.read(buffer)) > 0) {
|
while ((length = is.read(buffer)) > 0) {
|
||||||
os.write(buffer, 0, length);
|
os.write(buffer, 0, length);
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
|
@ -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,8 +186,61 @@ 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) {
|
||||||
super.onConfigurationChanged(newConfig);
|
super.onConfigurationChanged(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("");
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
|
@ -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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
@ -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
BIN
img/cover.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 181 KiB |
BIN
img/grin-logo.png
Normal file
BIN
img/grin-logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 37 KiB |
|
@ -3,4 +3,5 @@ Name=Grim
|
||||||
Exec=grim
|
Exec=grim
|
||||||
Icon=grim
|
Icon=grim
|
||||||
Type=Application
|
Type=Application
|
||||||
Categories=Finance
|
Categories=Finance
|
||||||
|
MimeType=application/x-slatepack;text/plain;
|
|
@ -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
|
||||||
|
|
|
@ -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?
|
||||||
|
|
|
@ -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?
|
||||||
|
|
|
@ -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 ?"
|
||||||
|
|
|
@ -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: Вы уверены, что хотите выйти из приложения?
|
||||||
|
|
|
@ -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?
|
||||||
|
|
|
@ -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>
|
||||||
|
<string>grim</string>
|
||||||
<key>CFBundleExecutable</key>
|
<key>CFBundleIconFile</key>
|
||||||
<string>grim</string>
|
<string>AppIcon</string>
|
||||||
|
<key>CFBundleIconName</key>
|
||||||
<key>CFBundleIconFile</key>
|
<string>AppIcon</string>
|
||||||
<string>AppIcon</string>
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>mw.gri.macos</string>
|
||||||
<key>CFBundleIconName</key>
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
<string>AppIcon</string>
|
<string>6.0</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
<key>CFBundleIdentifier</key>
|
<string>Grim</string>
|
||||||
<string>mw.gri.macos</string>
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>APPL</string>
|
||||||
<key>CFBundleInfoDictionaryVersion</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>6.0</string>
|
<string>0.2.3</string>
|
||||||
|
<key>CFBundleSupportedPlatforms</key>
|
||||||
<key>CFBundleName</key>
|
<array>
|
||||||
<string>Grim</string>
|
<string>MacOSX</string>
|
||||||
|
</array>
|
||||||
<key>CFBundlePackageType</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>APPL</string>
|
<string>1</string>
|
||||||
|
<key>NSCameraUsageDescription</key>
|
||||||
<key>CFBundleShortVersionString</key>
|
<string>Grim needs an access to your camera to scan QR code.</string>
|
||||||
<string>0.1.0</string>
|
<key>CFBundleDocumentTypes</key>
|
||||||
|
<array>
|
||||||
<key>CFBundleSupportedPlatforms</key>
|
<dict>
|
||||||
<array>
|
<key>CFBundleTypeName</key>
|
||||||
<string>MacOSX</string>
|
<string>Apple SimpleText document</string>
|
||||||
</array>
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>Viewer</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>LSItemContentTypes</key>
|
||||||
<string>1</string>
|
<array>
|
||||||
|
<string>com.apple.traditional-mac-plain-text</string>
|
||||||
<key>LSApplicationCategoryType</key>
|
</array>
|
||||||
<string>public.app-category.finance</string>
|
<key>NSDocumentClass</key>
|
||||||
|
<string>Document</string>
|
||||||
<key>NSHumanReadableCopyright</key>
|
</dict>
|
||||||
<string>2024</string>
|
<dict>
|
||||||
</dict>
|
<key>CFBundleTypeName</key>
|
||||||
</plist>
|
<string>Unknown document</string>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>Viewer</string>
|
||||||
|
<key>LSItemContentTypes</key>
|
||||||
|
<array>
|
||||||
|
<string>public.data</string>
|
||||||
|
</array>
|
||||||
|
<key>NSDocumentClass</key>
|
||||||
|
<string>Document</string>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
|
<key>LSApplicationCategoryType</key>
|
||||||
|
<string>public.app-category.finance</string>
|
||||||
|
<key>NSHumanReadableCopyright</key>
|
||||||
|
<string>2024</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
|
|
2
macos/Grim.app/Contents/MacOS/.gitignore
vendored
Normal file
2
macos/Grim.app/Contents/MacOS/.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
!.gitignore
|
||||||
|
grim
|
|
@ -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
|
||||||
|
|
|
@ -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"
|
*)
|
||||||
exit 1
|
printf "$usage"
|
||||||
esac
|
exit 1
|
||||||
|
esac
|
||||||
# Setup build directory
|
|
||||||
BASEDIR=$(cd $(dirname $0) && pwd)
|
|
||||||
cd ${BASEDIR}
|
|
||||||
cd ..
|
|
||||||
|
|
||||||
# Setup release argument
|
|
||||||
type=$1
|
|
||||||
[[ ${type} == "release" ]] && release_param="--profile release-apk"
|
|
||||||
|
|
||||||
# Setup platform argument
|
|
||||||
[[ $2 == "v7" ]] && arch+=(armeabi-v7a)
|
|
||||||
[[ $2 == "v8" ]] && arch+=(arm64-v8a)
|
|
||||||
|
|
||||||
# Setup platform path
|
|
||||||
[[ $2 == "v7" ]] && platform+=(armv7-linux-androideabi)
|
|
||||||
[[ $2 == "v8" ]] && platform+=(aarch64-linux-android)
|
|
||||||
|
|
||||||
# Install platform
|
|
||||||
[[ $2 == "v7" ]] && rustup target install armv7-linux-androideabi
|
|
||||||
[[ $2 == "v8" ]] && rustup target install aarch64-linux-android
|
|
||||||
|
|
||||||
# Build native code
|
|
||||||
cargo install cargo-ndk
|
|
||||||
|
|
||||||
sed -i -e 's/"rlib"/"cdylib","rlib"/g' Cargo.toml
|
|
||||||
|
|
||||||
# temp fix for https://stackoverflow.com/questions/57193895/error-use-of-undeclared-identifier-pthread-mutex-robust-cargo-build-liblmdb-s
|
|
||||||
success=0
|
|
||||||
export CPPFLAGS="-DMDB_USE_ROBUST=0" && export CFLAGS="-DMDB_USE_ROBUST=0"
|
|
||||||
cargo ndk -t ${arch} build ${release_param}
|
|
||||||
unset CPPFLAGS && unset CFLAGS
|
|
||||||
cargo ndk -t ${arch} -o android/app/src/main/jniLibs build ${release_param}
|
|
||||||
if [ $? -eq 0 ]
|
|
||||||
then
|
|
||||||
success=1
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
sed -i -e 's/"cdylib","rlib"/"rlib"/g' Cargo.toml
|
# Setup build directory
|
||||||
|
BASEDIR=$(cd "$(dirname "$0")" && pwd)
|
||||||
|
cd "${BASEDIR}" || exit 1
|
||||||
|
cd ..
|
||||||
|
|
||||||
# Build Android application and launch at all connected devices
|
# Install platforms and tools
|
||||||
if [ $success -eq 1 ]
|
rustup target add armv7-linux-androideabi
|
||||||
then
|
rustup target add aarch64-linux-android
|
||||||
cd android
|
rustup target add x86_64-linux-android
|
||||||
|
cargo install cargo-ndk
|
||||||
|
|
||||||
# Setup gradle argument
|
success=1
|
||||||
[[ $1 == "release" ]] && gradle_param+=(assembleRelease)
|
|
||||||
[[ $1 == "debug" ]] && gradle_param+=(build)
|
|
||||||
|
|
||||||
|
### Build native code
|
||||||
|
function build_lib() {
|
||||||
|
[[ $1 == "v7" ]] && arch=armeabi-v7a
|
||||||
|
[[ $1 == "v8" ]] && arch=arm64-v8a
|
||||||
|
[[ $1 == "x86" ]] && arch=x86_64
|
||||||
|
|
||||||
|
sed -i -e 's/"rlib"/"cdylib","rlib"/g' Cargo.toml
|
||||||
|
|
||||||
|
# Fix for https://stackoverflow.com/questions/57193895/error-use-of-undeclared-identifier-pthread-mutex-robust-cargo-build-liblmdb-s
|
||||||
|
# Uncomment lines below for the 1st build:
|
||||||
|
#export CPPFLAGS="-DMDB_USE_ROBUST=0" && export CFLAGS="-DMDB_USE_ROBUST=0"
|
||||||
|
#cargo ndk -t ${arch} build --profile release-apk
|
||||||
|
#unset CPPFLAGS && unset CFLAGS
|
||||||
|
cargo ndk -t "${arch}" -o android/app/src/main/jniLibs build --profile release-apk
|
||||||
|
if [ $? -eq 0 ]
|
||||||
|
then
|
||||||
|
success=1
|
||||||
|
else
|
||||||
|
success=0
|
||||||
|
fi
|
||||||
|
|
||||||
|
sed -i -e 's/"cdylib","rlib"/"rlib"/g' Cargo.toml
|
||||||
|
rm -f Cargo.toml-e
|
||||||
|
}
|
||||||
|
|
||||||
|
### 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
|
||||||
|
./gradlew assembleDebug
|
||||||
|
apk_path=app/build/outputs/apk/debug/app-debug.apk
|
||||||
|
else
|
||||||
|
./gradlew assembleSignedRelease
|
||||||
|
apk_path=app/build/outputs/apk/signedRelease/app-signedRelease.apk
|
||||||
|
fi
|
||||||
|
|
||||||
# Setup apk path
|
if [[ $1 == "" ]]; then
|
||||||
[[ $1 == "release" ]] && apk_path+=(app/build/outputs/apk/release/app-release.apk)
|
# Launch application at all connected devices.
|
||||||
[[ $1 == "debug" ]] && apk_path+=(app/build/outputs/apk/debug/app-debug.apk)
|
for SERIAL in $(adb devices | grep -v List | cut -f 1);
|
||||||
|
do
|
||||||
|
adb -s "$SERIAL" install ${apk_path}
|
||||||
|
sleep 1s
|
||||||
|
adb -s "$SERIAL" shell am start -n mw.gri.android/.MainActivity;
|
||||||
|
done
|
||||||
|
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}"
|
||||||
|
|
||||||
for SERIAL in $(adb devices | grep -v List | cut -f 1);
|
# Calculate checksum
|
||||||
do
|
checksum=grim-${version}-android-$1-sha256sum.txt
|
||||||
adb -s $SERIAL install ${apk_path}
|
[[ $1 == "arm" ]] && checksum=grim-${version}-android-sha256sum.txt
|
||||||
sleep 1s
|
rm -f "${checksum}"
|
||||||
adb -s $SERIAL shell am start -n mw.gri.android/.MainActivity;
|
sha256sum "${name}" > "${checksum}"
|
||||||
done
|
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
|
|
@ -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}
|
||||||
fi
|
[[ ${type} == "build" ]] && path="release"
|
||||||
|
./target/"${path}"/grim
|
||||||
|
fi
|
||||||
|
|
101
scripts/version.sh
Executable file
101
scripts/version.sh
Executable 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
|
306
src/gui/app.rs
306
src/gui/app.rs
|
@ -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 {
|
let is_fullscreen = ui.ctx().input(|i| {
|
||||||
self.desktop_window_ui(ui);
|
i.viewport().fullscreen.unwrap_or(false)
|
||||||
} else {
|
});
|
||||||
if is_mac_os {
|
if OperatingSystem::from_target_os() != OperatingSystem::Mac {
|
||||||
self.window_title_ui(ui);
|
self.desktop_window_ui(ui, is_fullscreen);
|
||||||
|
} else {
|
||||||
|
self.window_title_ui(ui, is_fullscreen);
|
||||||
ui.add_space(-1.0);
|
ui.add_space(-1.0);
|
||||||
|
Self::title_panel_bg(ui);
|
||||||
|
self.content.ui(ui, &self.platform);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.mobile_window_ui(ui);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provide incoming data to wallets.
|
||||||
|
if let Some(data) = crate::consume_incoming_data() {
|
||||||
|
if !data.is_empty() {
|
||||||
|
self.content.wallets.on_data(ui, Some(data), &self.platform);
|
||||||
}
|
}
|
||||||
self.content.ui(ui, &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 custom resizeable window content.
|
/// Draw mobile platform window content.
|
||||||
fn desktop_window_ui(&mut self, ui: &mut egui::Ui) {
|
fn mobile_window_ui(&mut self, ui: &mut egui::Ui) {
|
||||||
let is_fullscreen = ui.ctx().input(|i| {
|
Self::title_panel_bg(ui);
|
||||||
i.viewport().fullscreen.unwrap_or(false)
|
self.content.ui(ui, &self.platform);
|
||||||
});
|
}
|
||||||
|
|
||||||
let title_stroke_rect = {
|
/// Draw desktop platform window content.
|
||||||
let mut rect = ui.max_rect();
|
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 {
|
if !is_fullscreen {
|
||||||
rect = rect.shrink(Content::WINDOW_FRAME_MARGIN);
|
r = r.shrink(Content::WINDOW_FRAME_MARGIN);
|
||||||
}
|
}
|
||||||
rect.max.y = if !is_fullscreen {
|
r.min.y += Content::WINDOW_TITLE_HEIGHT + TitlePanel::HEIGHT;
|
||||||
Content::WINDOW_FRAME_MARGIN
|
r
|
||||||
} else {
|
|
||||||
0.0
|
|
||||||
} + Content::WINDOW_TITLE_HEIGHT + TitlePanel::DEFAULT_HEIGHT + 0.5;
|
|
||||||
rect
|
|
||||||
};
|
};
|
||||||
let title_stroke = RectShape {
|
let content_bg = RectShape::new(content_bg_rect,
|
||||||
rect: title_stroke_rect,
|
Rounding::ZERO,
|
||||||
rounding: Rounding {
|
Colors::fill_lite(),
|
||||||
nw: 8.0,
|
View::default_stroke());
|
||||||
ne: 8.0,
|
// Draw content background.
|
||||||
sw: 0.0,
|
ui.painter().add(content_bg);
|
||||||
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.
|
|
||||||
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,57 +196,53 @@ 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,
|
|
||||||
rounding: if is_fullscreen {
|
|
||||||
Rounding::ZERO
|
|
||||||
} else {
|
|
||||||
Rounding {
|
|
||||||
nw: 8.0,
|
|
||||||
ne: 8.0,
|
|
||||||
sw: 0.0,
|
|
||||||
se: 0.0,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
fill: Colors::yellow_dark(),
|
|
||||||
stroke: Stroke::NONE,
|
|
||||||
blur_width: 0.0,
|
|
||||||
fill_texture_id: Default::default(),
|
|
||||||
uv: Rect::ZERO
|
|
||||||
};
|
};
|
||||||
|
let is_mac = OperatingSystem::from_target_os() == OperatingSystem::Mac;
|
||||||
|
let window_title_bg = RectShape::new(title_bg_rect, if is_fullscreen || is_mac {
|
||||||
|
Rounding::ZERO
|
||||||
|
} else {
|
||||||
|
Rounding {
|
||||||
|
nw: 8.0,
|
||||||
|
ne: 8.0,
|
||||||
|
sw: 0.0,
|
||||||
|
se: 0.0,
|
||||||
|
}
|
||||||
|
}, Colors::yellow_dark(), Stroke::new(1.0, Colors::STROKE));
|
||||||
// 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, |_| {
|
||||||
Content::show_exit_modal();
|
if Modal::opened().is_none() || Modal::opened_closeable() {
|
||||||
|
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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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! {
|
||||||
|
|
|
@ -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>(
|
// Un-minimize window.
|
||||||
RequestedFormatType::AbsoluteHighestFrameRate
|
if ctx.input(|i| i.viewport().minimized.unwrap_or(false)) {
|
||||||
);
|
ctx.send_viewport_cmd(ViewportCommand::Minimized(false));
|
||||||
// Create and open camera.
|
|
||||||
let mut camera = Camera::new(index, requested).unwrap();
|
|
||||||
if let Ok(_) = camera.open_stream() {
|
|
||||||
loop {
|
|
||||||
// Stop if camera was stopped.
|
|
||||||
if stop_camera.load(Ordering::Relaxed) {
|
|
||||||
stop_camera.store(false, Ordering::Relaxed);
|
|
||||||
// Clear image.
|
|
||||||
let mut w_image = LAST_CAMERA_IMAGE.write();
|
|
||||||
*w_image = None;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
// Get a frame.
|
|
||||||
if let Ok(frame) = camera.frame() {
|
|
||||||
// Save image.
|
|
||||||
let mut w_image = LAST_CAMERA_IMAGE.write();
|
|
||||||
*w_image = Some((frame.buffer().to_vec(), 0));
|
|
||||||
} else {
|
|
||||||
// Clear image.
|
|
||||||
let mut w_image = LAST_CAMERA_IMAGE.write();
|
|
||||||
*w_image = None;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
camera.stop_stream().unwrap();
|
// Focus to window.
|
||||||
};
|
if !ctx.input(|i| i.viewport().focused.unwrap_or(false)) {
|
||||||
|
ctx.send_viewport_cmd(ViewportCommand::WindowLevel(WindowLevel::AlwaysOnTop));
|
||||||
|
ctx.send_viewport_cmd(ViewportCommand::Focus);
|
||||||
|
}
|
||||||
|
ctx.request_repaint();
|
||||||
|
}
|
||||||
|
self.attention_required.store(true, Ordering::Relaxed);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[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();
|
fn clear_user_attention(&self) {
|
||||||
let devices = ctx.devices().unwrap();
|
let r_ctx = self.ctx.read();
|
||||||
let dev = ctx.open_device(&devices[0].uri).unwrap();
|
if r_ctx.is_some() {
|
||||||
|
let ctx = r_ctx.as_ref().unwrap();
|
||||||
let streams = dev.streams().unwrap();
|
ctx.send_viewport_cmd(
|
||||||
let stream_desc = streams[0].clone();
|
ViewportCommand::RequestUserAttention(UserAttentionType::Reset)
|
||||||
let w = stream_desc.width;
|
);
|
||||||
let h = stream_desc.height;
|
ctx.send_viewport_cmd(ViewportCommand::WindowLevel(WindowLevel::Normal));
|
||||||
|
|
||||||
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");
|
|
||||||
let mut out = vec![];
|
|
||||||
if let Some(buf) = image::ImageBuffer::<image::Rgb<u8>, &[u8]>::from_raw(w, h, &frame) {
|
|
||||||
image::codecs::jpeg::JpegEncoder::new(&mut out)
|
|
||||||
.write_image(buf.as_raw(), w, h, image::ExtendedColorType::Rgb8).unwrap();
|
|
||||||
} else {
|
|
||||||
out = frame.to_vec();
|
|
||||||
}
|
|
||||||
// Save image.
|
|
||||||
let mut w_image = LAST_CAMERA_IMAGE.write();
|
|
||||||
*w_image = Some((out, 0));
|
|
||||||
}
|
}
|
||||||
|
self.attention_required.store(false, Ordering::Relaxed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
|
@ -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,102 +50,112 @@ 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);
|
||||||
// Setup image rotation.
|
|
||||||
img = match img_data.1 {
|
// Draw image.
|
||||||
90 => img.rotate90(),
|
let img_rect = self.image_ui(ui, img, img_data.1);
|
||||||
180 => img.rotate180(),
|
|
||||||
270 => img.rotate270(),
|
|
||||||
_ => img
|
|
||||||
};
|
|
||||||
// Convert to ColorImage to add at content.
|
|
||||||
let color_img = match &img {
|
|
||||||
DynamicImage::ImageRgb8(image) => {
|
|
||||||
egui::ColorImage::from_rgb(
|
|
||||||
[image.width() as usize, image.height() as usize],
|
|
||||||
image.as_bytes(),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
other => {
|
|
||||||
let image = other.to_rgba8();
|
|
||||||
egui::ColorImage::from_rgba_unmultiplied(
|
|
||||||
[image.width() as usize, image.height() as usize],
|
|
||||||
image.as_bytes(),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
};
|
|
||||||
// Create image texture.
|
|
||||||
let texture = ui.ctx().load_texture("camera_image",
|
|
||||||
color_img.clone(),
|
|
||||||
TextureOptions::default());
|
|
||||||
let img_size = egui::emath::vec2(color_img.width() as f32,
|
|
||||||
color_img.height() as f32);
|
|
||||||
let sized_img = SizedTexture::new(texture.id(), img_size);
|
|
||||||
// Add image to content.
|
|
||||||
ui.vertical_centered(|ui| {
|
|
||||||
egui::Image::from_texture(sized_img)
|
|
||||||
// Setup to crop image at square.
|
|
||||||
.uv(Rect::from([
|
|
||||||
Pos2::new(1.0 - (img_size.y / img_size.x), 0.0),
|
|
||||||
Pos2::new(1.0, 1.0)
|
|
||||||
]))
|
|
||||||
.max_height(ui.available_width())
|
|
||||||
.maintain_aspect_ratio(false)
|
|
||||||
.shrink_to_fit()
|
|
||||||
.ui(ui);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Show UR scan progress.
|
// Show UR scan progress.
|
||||||
let show_ur_progress = {
|
self.ur_progress_ui(ui);
|
||||||
self.ur_data.clone().read().is_some()
|
img_rect
|
||||||
};
|
|
||||||
let ur_progress = self.ur_progress();
|
|
||||||
if show_ur_progress && ur_progress != 0 {
|
|
||||||
ui.add_space(-52.0);
|
|
||||||
ui.vertical_centered(|ui| {
|
|
||||||
ui.label(RichText::new(format!("{}%", ur_progress))
|
|
||||||
.size(16.0)
|
|
||||||
.color(Colors::yellow()));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 {
|
} else {
|
||||||
self.loading_content_ui(ui);
|
self.loading_ui(ui)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
self.loading_content_ui(ui);
|
self.loading_ui(ui)
|
||||||
}
|
};
|
||||||
|
|
||||||
// Request redraw.
|
// Show button to switch cameras.
|
||||||
ui.ctx().request_repaint();
|
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.
|
||||||
|
img = match rotation {
|
||||||
|
90 => img.rotate90(),
|
||||||
|
180 => img.rotate180(),
|
||||||
|
270 => img.rotate270(),
|
||||||
|
_ => img
|
||||||
|
};
|
||||||
|
if View::is_desktop() {
|
||||||
|
img = img.fliph();
|
||||||
|
}
|
||||||
|
// Convert to ColorImage.
|
||||||
|
let color_img = match &img {
|
||||||
|
DynamicImage::ImageRgb8(image) => {
|
||||||
|
egui::ColorImage::from_rgb(
|
||||||
|
[image.width() as usize, image.height() as usize],
|
||||||
|
image.as_bytes(),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
other => {
|
||||||
|
let image = other.to_rgba8();
|
||||||
|
egui::ColorImage::from_rgba_unmultiplied(
|
||||||
|
[image.width() as usize, image.height() as usize],
|
||||||
|
image.as_bytes(),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
};
|
||||||
|
// Create image texture.
|
||||||
|
let texture = ui.ctx().load_texture("camera_image",
|
||||||
|
color_img.clone(),
|
||||||
|
TextureOptions::default());
|
||||||
|
let img_size = egui::emath::vec2(color_img.width() as f32,
|
||||||
|
color_img.height() as f32);
|
||||||
|
let sized_img = SizedTexture::new(texture.id(), img_size);
|
||||||
|
egui::Image::from_texture(sized_img)
|
||||||
|
// Setup to crop image at square.
|
||||||
|
.uv(Rect::from([
|
||||||
|
Pos2::new(1.0 - (img_size.y / img_size.x), 0.0),
|
||||||
|
Pos2::new(1.0, 1.0)
|
||||||
|
]))
|
||||||
|
.max_height(ui.available_width())
|
||||||
|
.maintain_aspect_ratio(false)
|
||||||
|
.shrink_to_fit()
|
||||||
|
.ui(ui).rect
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw animated QR code scanning progress.
|
||||||
|
fn ur_progress_ui(&self, ui: &mut egui::Ui) {
|
||||||
|
let show_ur_progress = {
|
||||||
|
self.ur_data.as_ref().read().is_some()
|
||||||
|
};
|
||||||
|
if show_ur_progress {
|
||||||
|
ui.centered_and_justified(|ui| {
|
||||||
|
ui.label(RichText::new(format!("{}%", self.ur_progress()))
|
||||||
|
.size(17.0)
|
||||||
|
.color(Colors::green()));
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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;
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -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,42 +319,40 @@ 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() {
|
lang == locale
|
||||||
lang == locale
|
} else {
|
||||||
} else {
|
rust_i18n::locale() == locale
|
||||||
rust_i18n::locale() == locale
|
};
|
||||||
};
|
if !is_current {
|
||||||
if !is_current {
|
View::item_button(ui, View::item_rounding(index, len, true), CHECK, None, || {
|
||||||
View::item_button(ui, View::item_rounding(index, len, true), CHECK, None, || {
|
rust_i18n::set_locale(locale);
|
||||||
rust_i18n::set_locale(locale);
|
AppConfig::save_locale(locale);
|
||||||
AppConfig::save_locale(locale);
|
modal.close();
|
||||||
modal.close();
|
});
|
||||||
});
|
} else {
|
||||||
} 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);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
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(12.0);
|
||||||
|
ui.vertical(|ui| {
|
||||||
|
// Draw language name.
|
||||||
ui.add_space(12.0);
|
ui.add_space(12.0);
|
||||||
ui.vertical(|ui| {
|
let color = if is_current {
|
||||||
// Draw language name.
|
Colors::title(false)
|
||||||
ui.add_space(12.0);
|
} else {
|
||||||
let color = if is_current {
|
Colors::gray()
|
||||||
Colors::title(false)
|
};
|
||||||
} else {
|
ui.label(RichText::new(t!("lang_name", locale = locale))
|
||||||
Colors::gray()
|
.size(17.0)
|
||||||
};
|
.color(color));
|
||||||
ui.label(RichText::new(t!("lang_name", locale = locale))
|
ui.add_space(3.0);
|
||||||
.size(17.0)
|
|
||||||
.color(color));
|
|
||||||
ui.add_space(3.0);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -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)
|
||||||
}
|
}
|
|
@ -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);
|
||||||
}
|
}
|
|
@ -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::*;
|
|
@ -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);
|
r.min.y += Content::WINDOW_TITLE_HEIGHT;
|
||||||
rect.min += egui::vec2(0.0, Content::WINDOW_TITLE_HEIGHT + 0.5);
|
r
|
||||||
rect.max.x += 0.5;
|
} 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;
|
|
||||||
|
|
||||||
// Setup background shape to be painted behind main content.
|
|
||||||
content_rect.min -= egui::emath::vec2(6.0, 0.0);
|
|
||||||
content_rect.max += egui::emath::vec2(6.0, 0.0);
|
|
||||||
bg_shape.rect = content_rect;
|
|
||||||
ui.painter().set(bg_idx, bg_shape);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Draw title content.
|
|
||||||
fn title_ui(&self, ui: &mut egui::Ui) {
|
|
||||||
let rect = ui.available_rect_before_wrap();
|
|
||||||
|
|
||||||
// Create background shape.
|
|
||||||
let mut bg_shape = RectShape {
|
|
||||||
rect,
|
|
||||||
rounding: Rounding {
|
|
||||||
nw: 8.0,
|
|
||||||
ne: 8.0,
|
|
||||||
sw: 0.0,
|
|
||||||
se: 0.0,
|
|
||||||
},
|
|
||||||
fill: Colors::yellow(),
|
|
||||||
stroke: Stroke::NONE,
|
|
||||||
blur_width: 0.0,
|
|
||||||
fill_texture_id: Default::default(),
|
|
||||||
uv: Rect::ZERO
|
|
||||||
};
|
|
||||||
let bg_idx = ui.painter().add(bg_shape);
|
|
||||||
|
|
||||||
// Draw title content.
|
|
||||||
let title_resp = ui.allocate_ui_at_rect(rect, |ui| {
|
|
||||||
ui.vertical_centered_justified(|ui| {
|
|
||||||
ui.add_space(Self::DEFAULT_MARGIN + 1.0);
|
|
||||||
ui.label(RichText::new(self.title.as_ref().unwrap())
|
|
||||||
.size(19.0)
|
|
||||||
.color(Colors::title(true))
|
|
||||||
);
|
|
||||||
ui.add_space(Self::DEFAULT_MARGIN);
|
|
||||||
// Draw line below title.
|
|
||||||
View::horizontal_line(ui, Colors::item_stroke());
|
|
||||||
});
|
|
||||||
}).response;
|
}).response;
|
||||||
|
|
||||||
// Setup background shape to be painted behind title content.
|
// Setup background size.
|
||||||
bg_shape.rect = title_resp.rect;
|
let bg_rect = {
|
||||||
|
let mut r = resp.rect.clone();
|
||||||
|
r.min -= egui::emath::vec2(6.0, 0.0);
|
||||||
|
r.max += egui::emath::vec2(6.0, 0.0);
|
||||||
|
r
|
||||||
|
};
|
||||||
|
bg_shape.rect = bg_rect;
|
||||||
ui.painter().set(bg_idx, bg_shape);
|
ui.painter().set(bg_idx, bg_shape);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw title content.
|
||||||
|
fn title_ui(title: &String, ui: &mut egui::Ui) {
|
||||||
|
let rect = ui.available_rect_before_wrap();
|
||||||
|
|
||||||
|
// Create background shape.
|
||||||
|
let mut bg_shape = RectShape::new(rect, Rounding {
|
||||||
|
nw: 8.0,
|
||||||
|
ne: 8.0,
|
||||||
|
sw: 0.0,
|
||||||
|
se: 0.0,
|
||||||
|
}, Colors::yellow(), Stroke::NONE);
|
||||||
|
let bg_idx = ui.painter().add(bg_shape);
|
||||||
|
|
||||||
|
// Draw title content.
|
||||||
|
let resp = ui.vertical_centered(|ui| {
|
||||||
|
ui.add_space(Modal::DEFAULT_MARGIN + 2.0);
|
||||||
|
ui.label(RichText::new(title)
|
||||||
|
.size(19.0)
|
||||||
|
.color(Colors::title(true))
|
||||||
|
);
|
||||||
|
ui.add_space(Modal::DEFAULT_MARGIN + 1.0);
|
||||||
|
// Draw line below title.
|
||||||
|
View::horizontal_line(ui, Colors::item_stroke());
|
||||||
|
}).response;
|
||||||
|
|
||||||
|
// Setup background size.
|
||||||
|
bg_shape.rect = resp.rect;
|
||||||
|
ui.painter().set(bg_idx, bg_shape);
|
||||||
}
|
}
|
|
@ -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.
|
View::item_button(ui, button_rounding, TRASH, None, || {
|
||||||
if conn.url != ExternalConnection::DEFAULT_MAIN_URL {
|
ConnectionsConfig::remove_ext_conn(conn.id);
|
||||||
let button_rounding = View::item_rounding(index, len, true);
|
});
|
||||||
View::item_button(ui, button_rounding, TRASH, None, || {
|
View::item_button(ui, Rounding::default(), PENCIL, None, || {
|
||||||
ConnectionsConfig::remove_ext_conn(conn.id);
|
self.show_add_ext_conn_modal(Some(conn.clone()), cb);
|
||||||
});
|
});
|
||||||
View::item_button(ui, Rounding::default(), PENCIL, None, || {
|
|
||||||
self.show_add_ext_conn_modal(Some(conn.clone()), cb);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -138,16 +134,17 @@ impl ConnectionsContent {
|
||||||
// Draw custom button.
|
// Draw custom button.
|
||||||
custom_button(ui);
|
custom_button(ui);
|
||||||
|
|
||||||
if !Node::is_running() {
|
// Draw buttons to start/stop node.
|
||||||
// Draw button to start integrated node.
|
if Node::get_error().is_none() {
|
||||||
View::item_button(ui, Rounding::default(), POWER, Some(Colors::green()), || {
|
if !Node::is_running() {
|
||||||
Node::start();
|
View::item_button(ui, Rounding::default(), POWER, Some(Colors::green()), || {
|
||||||
});
|
Node::start();
|
||||||
} else if !Node::is_starting() && !Node::is_stopping() && !Node::is_restarting() {
|
});
|
||||||
// Draw button to stop integrated node.
|
} else if !Node::is_starting() && !Node::is_stopping() && !Node::is_restarting() {
|
||||||
View::item_button(ui, Rounding::default(), POWER, Some(Colors::red()), || {
|
View::item_button(ui, Rounding::default(), POWER, Some(Colors::red()), || {
|
||||||
Node::stop(false);
|
Node::stop(false);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let layout_size = ui.available_size();
|
let layout_size = ui.available_size();
|
||||||
|
@ -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,34 +202,32 @@ 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);
|
|
||||||
|
|
||||||
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| {
|
||||||
// Draw connections URL.
|
// Draw connections URL.
|
||||||
ui.add_space(4.0);
|
ui.add_space(4.0);
|
||||||
let conn_text = format!("{} {}", GLOBE_SIMPLE, conn.url);
|
let conn_text = format!("{} {}", GLOBE_SIMPLE, conn.url);
|
||||||
View::ellipsize_text(ui, conn_text, 15.0, Colors::title(false));
|
View::ellipsize_text(ui, conn_text, 15.0, Colors::title(false));
|
||||||
ui.add_space(1.0);
|
ui.add_space(1.0);
|
||||||
|
|
||||||
// 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, t!("network.available"))
|
||||||
} else {
|
|
||||||
format!("{} {}", X_CIRCLE, t!("network.not_available"))
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
format!("{} {}", DOTS_THREE_CIRCLE, t!("network.availability_check"))
|
format!("{} {}", X_CIRCLE, t!("network.not_available"))
|
||||||
};
|
}
|
||||||
ui.label(RichText::new(status_text).size(15.0).color(Colors::gray()));
|
} else {
|
||||||
ui.add_space(3.0);
|
format!("{} {}", DOTS_THREE_CIRCLE, t!("network.availability_check"))
|
||||||
});
|
};
|
||||||
|
ui.label(RichText::new(status_text).size(15.0).color(Colors::gray()));
|
||||||
|
ui.add_space(3.0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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| {
|
||||||
self.node_tab_content.ui(ui, cb);
|
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} 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,73 +286,98 @@ 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) {
|
||||||
match e {
|
View::center_content(ui, 156.0, |ui| {
|
||||||
NodeError::Storage => {
|
let text = t!("network.disabled_server", "dots" => DOTS_THREE_OUTLINE_VERTICAL);
|
||||||
View::center_content(ui, 156.0, |ui| {
|
ui.label(RichText::new(text)
|
||||||
ui.label(RichText::new(t!("network_node.error_clean"))
|
.size(16.0)
|
||||||
.size(16.0)
|
.color(Colors::inactive_text())
|
||||||
.color(Colors::red())
|
);
|
||||||
);
|
ui.add_space(8.0);
|
||||||
ui.add_space(8.0);
|
View::action_button(ui, format!("{} {}", POWER, t!("network.enable_node")), || {
|
||||||
let btn_txt = format!("{} {}",
|
Node::start();
|
||||||
ARROWS_COUNTER_CLOCKWISE,
|
});
|
||||||
t!("network_node.resync"));
|
ui.add_space(2.0);
|
||||||
View::action_button(ui, btn_txt, || {
|
NetworkContent::autorun_node_ui(ui);
|
||||||
Node::clean_up_data();
|
});
|
||||||
Node::start();
|
}
|
||||||
});
|
|
||||||
ui.add_space(2.0);
|
/// Draw integrated node error content.
|
||||||
|
pub fn node_error_ui(ui: &mut egui::Ui, e: NodeError) {
|
||||||
|
match e {
|
||||||
|
NodeError::Storage => {
|
||||||
|
View::center_content(ui, 156.0, |ui| {
|
||||||
|
ui.label(RichText::new(t!("network_node.error_clean"))
|
||||||
|
.size(16.0)
|
||||||
|
.color(Colors::red())
|
||||||
|
);
|
||||||
|
ui.add_space(8.0);
|
||||||
|
let btn_txt = format!("{} {}",
|
||||||
|
ARROWS_COUNTER_CLOCKWISE,
|
||||||
|
t!("network_node.resync"));
|
||||||
|
View::action_button(ui, btn_txt, || {
|
||||||
|
Node::clean_up_data();
|
||||||
|
Node::start();
|
||||||
});
|
});
|
||||||
return;
|
ui.add_space(2.0);
|
||||||
}
|
});
|
||||||
NodeError::P2P | NodeError::API => {
|
return;
|
||||||
let msg_type = match e {
|
}
|
||||||
NodeError::API => "API",
|
NodeError::P2P | NodeError::API => {
|
||||||
_ => "P2P"
|
let msg_type = match e {
|
||||||
};
|
NodeError::API => "API",
|
||||||
View::center_content(ui, 106.0, |ui| {
|
_ => "P2P"
|
||||||
let text = t!(
|
};
|
||||||
|
View::center_content(ui, 106.0, |ui| {
|
||||||
|
let text = t!(
|
||||||
"network_node.error_p2p_api",
|
"network_node.error_p2p_api",
|
||||||
"p2p_api" => msg_type,
|
"p2p_api" => msg_type,
|
||||||
"settings" => FADERS
|
"settings" => FADERS
|
||||||
);
|
);
|
||||||
ui.label(RichText::new(text)
|
ui.label(RichText::new(text)
|
||||||
.size(16.0)
|
.size(16.0)
|
||||||
.color(Colors::red())
|
.color(Colors::red())
|
||||||
);
|
);
|
||||||
ui.add_space(2.0);
|
ui.add_space(2.0);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
NodeError::Configuration => {
|
||||||
|
View::center_content(ui, 106.0, |ui| {
|
||||||
|
ui.label(RichText::new(t!("network_node.error_config", "settings" => FADERS))
|
||||||
|
.size(16.0)
|
||||||
|
.color(Colors::red())
|
||||||
|
);
|
||||||
|
ui.add_space(8.0);
|
||||||
|
let btn_txt = format!("{} {}",
|
||||||
|
ARROWS_COUNTER_CLOCKWISE,
|
||||||
|
t!("network_settings.reset"));
|
||||||
|
View::action_button(ui, btn_txt, || {
|
||||||
|
NodeConfig::reset_to_default();
|
||||||
|
Node::start();
|
||||||
});
|
});
|
||||||
return;
|
ui.add_space(2.0);
|
||||||
}
|
});
|
||||||
NodeError::Configuration => {
|
}
|
||||||
View::center_content(ui, 106.0, |ui| {
|
NodeError::Unknown => {
|
||||||
ui.label(RichText::new(t!("network_node.error_config", "settings" => FADERS))
|
View::center_content(ui, 156.0, |ui| {
|
||||||
.size(16.0)
|
ui.label(RichText::new(t!("network_node.error_unknown", "settings" => FADERS))
|
||||||
.color(Colors::red())
|
.size(16.0)
|
||||||
);
|
.color(Colors::red())
|
||||||
ui.add_space(2.0);
|
);
|
||||||
|
ui.add_space(8.0);
|
||||||
|
let btn_txt = format!("{} {}",
|
||||||
|
ARROWS_COUNTER_CLOCKWISE,
|
||||||
|
t!("network_node.resync"));
|
||||||
|
View::action_button(ui, btn_txt, || {
|
||||||
|
Node::clean_up_data();
|
||||||
|
Node::start();
|
||||||
});
|
});
|
||||||
}
|
ui.add_space(2.0);
|
||||||
NodeError::Unknown => {
|
});
|
||||||
View::center_content(ui, 156.0, |ui| {
|
|
||||||
ui.label(RichText::new(t!("network_node.error_unknown", "settings" => FADERS))
|
|
||||||
.size(16.0)
|
|
||||||
.color(Colors::red())
|
|
||||||
);
|
|
||||||
ui.add_space(8.0);
|
|
||||||
let btn_txt = format!("{} {}",
|
|
||||||
ARROWS_COUNTER_CLOCKWISE,
|
|
||||||
t!("network_node.resync"));
|
|
||||||
View::action_button(ui, btn_txt, || {
|
|
||||||
Node::clean_up_data();
|
|
||||||
Node::start();
|
|
||||||
});
|
|
||||||
ui.add_space(2.0);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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,90 +22,64 @@ 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| {
|
// Show emission and difficulty info.
|
||||||
let stats = server_stats.as_ref().unwrap();
|
info_ui(ui, stats);
|
||||||
// Show emission and difficulty info.
|
// Show difficulty adjustment window blocks.
|
||||||
info_ui(ui, stats);
|
blocks_ui(ui, stats);
|
||||||
// Show difficulty adjustment window blocks.
|
|
||||||
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]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
ui.add_space(5.0);
|
ui.add_space(5.0);
|
||||||
|
@ -117,32 +92,34 @@ 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]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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| {
|
||||||
|
ui.add_space(4.0);
|
||||||
for index in row_range {
|
for index in row_range {
|
||||||
// Add space before the first item.
|
|
||||||
if index == 0 {
|
|
||||||
ui.add_space(4.0);
|
|
||||||
}
|
|
||||||
let db = stats.diff_stats.last_blocks.get(index).unwrap();
|
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);
|
||||||
});
|
});
|
||||||
|
|
|
@ -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,15 +55,13 @@ 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,16 +74,19 @@ 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
|
||||||
t!("network_mining.rewards_wallet"),
|
.wallet_name
|
||||||
[false, true, false, true]);
|
.clone()
|
||||||
|
.unwrap_or("-".to_string()),
|
||||||
|
t!("network_mining.rewards_wallet"),
|
||||||
|
[false, true, false, true]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
ui.add_space(4.0);
|
ui.add_space(4.0);
|
||||||
|
@ -131,10 +100,10 @@ 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]);
|
||||||
});
|
});
|
||||||
columns[1].vertical_centered(|ui| {
|
columns[1].vertical_centered(|ui| {
|
||||||
let block_height = if stratum_stats.block_height > 0 {
|
let block_height = if stratum_stats.block_height > 0 {
|
||||||
|
@ -142,10 +111,10 @@ 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]);
|
||||||
});
|
});
|
||||||
columns[2].vertical_centered(|ui| {
|
columns[2].vertical_centered(|ui| {
|
||||||
let hashrate = if stratum_stats.network_hashrate > 0.0 {
|
let hashrate = if stratum_stats.network_hashrate > 0.0 {
|
||||||
|
@ -153,10 +122,10 @@ 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]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
ui.add_space(4.0);
|
ui.add_space(4.0);
|
||||||
|
@ -165,17 +134,17 @@ 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]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
ui.add_space(4.0);
|
ui.add_space(4.0);
|
||||||
|
@ -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(
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -20,51 +20,28 @@ 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,32 +56,32 @@ 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]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
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]);
|
||||||
});
|
});
|
||||||
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]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
ui.add_space(5.0);
|
ui.add_space(5.0);
|
||||||
|
@ -113,32 +90,32 @@ 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]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
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]);
|
||||||
});
|
});
|
||||||
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]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
ui.add_space(5.0);
|
ui.add_space(5.0);
|
||||||
|
@ -151,10 +128,10 @@ 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]);
|
||||||
});
|
});
|
||||||
columns[1].vertical_centered(|ui| {
|
columns[1].vertical_centered(|ui| {
|
||||||
let stem_tx_stat = match &stats.tx_stats {
|
let stem_tx_stat = match &stats.tx_stats {
|
||||||
|
@ -163,24 +140,24 @@ 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]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
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]);
|
||||||
});
|
});
|
||||||
columns[1].vertical_centered(|ui| {
|
columns[1].vertical_centered(|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);
|
||||||
|
|
|
@ -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();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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;
|
||||||
});
|
});
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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 => {
|
||||||
NodeConfig::save_stratum_wallet_id(id);
|
self.wallets_modal.ui(ui, modal, &mut self.wallets, cb, |wallet, _| {
|
||||||
self.wallet_name = WalletConfig::name_by_id(id);
|
let id = wallet.get_config().id;
|
||||||
}),
|
NodeConfig::save_stratum_wallet_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;
|
||||||
|
|
||||||
|
|
|
@ -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") }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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,21 +204,22 @@ 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 Some(data) = Self::qr_to_image_data(qr, DEFAULT_QR_SIZE as usize) {
|
if let Ok(qr) = QrCode::encode_text(text, qrcodegen::QrCodeEcc::Low) {
|
||||||
let mut png = vec![];
|
if let Some(data) = Self::qr_to_image_data(qr, DEFAULT_QR_SIZE as usize) {
|
||||||
let png_enc = PngEncoder::new_with_quality(&mut png,
|
let mut png = vec![];
|
||||||
CompressionType::Best,
|
let png_enc = PngEncoder::new_with_quality(&mut png,
|
||||||
FilterType::NoFilter);
|
CompressionType::Best,
|
||||||
if let Ok(()) = png_enc.write_image(data.as_slice(),
|
FilterType::NoFilter);
|
||||||
DEFAULT_QR_SIZE,
|
if let Ok(()) = png_enc.write_image(data.as_slice(),
|
||||||
DEFAULT_QR_SIZE,
|
DEFAULT_QR_SIZE,
|
||||||
ExtendedColorType::L8) {
|
DEFAULT_QR_SIZE,
|
||||||
let name = format!("{}.png", chrono::Utc::now().timestamp());
|
ExtendedColorType::L8) {
|
||||||
cb.share_data(name, png).unwrap_or_default();
|
let name = format!("{}.png", chrono::Utc::now().timestamp());
|
||||||
|
cb.share_data(name, png).unwrap_or_default();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
ui.add_space(8.0);
|
ui.add_space(8.0);
|
||||||
|
@ -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
130
src/gui/views/scan.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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.allocate_ui(rect.size(), |ui| {
|
ui.vertical_centered(|ui| {
|
||||||
(add_content)(ui);
|
ui.allocate_ui(rect.size(), |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,
|
nw: if r[0] { 8.0 } else { 0.0 },
|
||||||
rounding: Rounding {
|
ne: if r[1] { 8.0 } else { 0.0 },
|
||||||
nw: if r[0] { 8.0 } else { 0.0 },
|
sw: if r[2] { 8.0 } else { 0.0 },
|
||||||
ne: if r[1] { 8.0 } else { 0.0 },
|
se: if r[3] { 8.0 } else { 0.0 },
|
||||||
sw: if r[2] { 8.0 } else { 0.0 },
|
}, Colors::fill_lite(), Self::item_stroke());
|
||||||
se: if r[3] { 8.0 } else { 0.0 },
|
|
||||||
},
|
|
||||||
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
|
||||||
|
|
|
@ -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(
|
||||||
|
WalletCreation::new(name.clone(), pass.clone())
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if self.creation_content.is_some() {
|
||||||
|
self.add_wallet_modal_content = None;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
CONNECTION_SELECTION_MODAL => {
|
OPEN_WALLET_MODAL => {
|
||||||
if let Some(content) = self.conn_modal_content.as_mut() {
|
let mut open = false;
|
||||||
content.ui(ui, modal, cb, |id| {
|
if let Some(open_content) = self.open_wallet_content.as_mut() {
|
||||||
let list = self.wallets.list();
|
open_content.ui(ui, modal, cb, |wallet, data| {
|
||||||
for w in list {
|
self.wallet_content = Some(WalletContent::new(wallet, data));
|
||||||
if self.wallets.selected_id == Some(w.get_config().id) {
|
open = true;
|
||||||
w.update_ext_conn_id(id);
|
});
|
||||||
}
|
}
|
||||||
|
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(if list_hidden {
|
||||||
.exact_width(wallet_panel_width)
|
content_width
|
||||||
.frame(egui::Frame {
|
|
||||||
fill: if empty_list && !create_wallet
|
|
||||||
|| (dual_panel && show_wallet && !self.show_wallets_at_dual_panel) {
|
|
||||||
Colors::fill_deep()
|
|
||||||
} else {
|
} else {
|
||||||
if create_wallet {
|
content_width - Content::SIDE_PANEL_WIDTH
|
||||||
Colors::white_or_black(false)
|
})
|
||||||
} else {
|
.frame(egui::Frame {
|
||||||
Colors::button()
|
..Default::default()
|
||||||
|
})
|
||||||
|
.show_inside(ui, |ui| {
|
||||||
|
// Show opened wallet content.
|
||||||
|
if let Some(content) = self.wallet_content.as_mut() {
|
||||||
|
content.ui(ui, cb);
|
||||||
}
|
}
|
||||||
},
|
});
|
||||||
..Default::default()
|
}
|
||||||
})
|
|
||||||
.show_animated_inside(ui, wallet_panel_opened, |ui| {
|
|
||||||
if create_wallet || !show_wallet {
|
|
||||||
// Show wallet creation content.
|
|
||||||
self.creation_content.ui(ui, cb, |wallet| {
|
|
||||||
// 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;
|
||||||
} else {
|
r.min.x -= View::TAB_ITEMS_PADDING;
|
||||||
egui::Frame {
|
r.max.x += View::TAB_ITEMS_PADDING;
|
||||||
stroke: View::item_stroke(),
|
r
|
||||||
fill: Colors::fill_deep(),
|
};
|
||||||
|
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 {
|
||||||
|
content_width
|
||||||
|
})
|
||||||
|
.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| {
|
||||||
|
if !dual_panel && !showing_wallet {
|
||||||
|
ui.ctx().request_repaint_after(Duration::from_millis(1000));
|
||||||
|
}
|
||||||
|
// Show wallet list.
|
||||||
|
self.wallet_list_ui(ui, cb);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
egui::CentralPanel::default()
|
||||||
|
.frame(egui::Frame {
|
||||||
|
fill: if creating_wallet {
|
||||||
|
Colors::TRANSPARENT
|
||||||
|
} else {
|
||||||
|
Colors::fill_deep()
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
})
|
})
|
||||||
.show_inside(ui, |ui| {
|
.show_inside(ui, |ui| {
|
||||||
if !list_hidden && !dual_panel {
|
if self.creation_content.is_some() {
|
||||||
ui.ctx().request_repaint_after(Duration::from_millis(1000));
|
let creation = self.creation_content.as_mut().unwrap();
|
||||||
}
|
let pass = creation.pass.clone();
|
||||||
self.wallet_list_ui(ui, cb);
|
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);
|
||||||
|
|
||||||
/// Check if wallet panel is showing.
|
let text = t!("wallets.create_desc");
|
||||||
pub fn wallet_panel_opened(&self) -> bool {
|
ui.label(RichText::new(text)
|
||||||
let empty_list = self.wallets.is_current_list_empty();
|
.size(16.0)
|
||||||
empty_list || self.creating_wallet() || self.showing_wallet()
|
.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;
|
let dual_title = !qr_scan && !creating_wallet && show_wallet && dual_panel;
|
||||||
if dual_title {
|
if dual_title {
|
||||||
let wallet_tab_type = self.wallet_content.current_tab.get_type();
|
let wallet_content = self.wallet_content.as_ref().unwrap();
|
||||||
let wallet_tab_name = wallet_tab_type.name().to_uppercase();
|
let wallet_tab_type = wallet_content.current_tab.get_type();
|
||||||
let title_content = if wallet_tab_type == WalletTabType::Settings {
|
let wallet_title_text = wallet_tab_type.name();
|
||||||
TitleContentType::Title(wallet_tab_name)
|
let wallet_title_content = if wallet_tab_type == WalletTabType::Settings {
|
||||||
|
TitleContentType::Title(wallet_title_text)
|
||||||
} else {
|
} else {
|
||||||
let subtitle_text = self.wallets.selected_name();
|
let subtitle_text = wallet_content.wallet.get_config().name;
|
||||||
TitleContentType::WithSubTitle(wallet_tab_name, subtitle_text, false)
|
TitleContentType::WithSubTitle(wallet_title_text, subtitle_text, false)
|
||||||
};
|
};
|
||||||
TitleType::Dual(TitleContentType::Title(title_text), title_content)
|
TitleType::Dual(TitleContentType::Title(title_text), wallet_title_content)
|
||||||
} else {
|
} else {
|
||||||
TitleType::Single(TitleContentType::Title(title_text))
|
TitleType::Single(TitleContentType::Title(title_text))
|
||||||
}
|
}
|
||||||
|
@ -298,23 +434,48 @@ 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()
|
||||||
} else if create_wallet {
|
.unwrap()
|
||||||
View::title_button_big(ui, ARROW_LEFT, |_| {
|
.qr_scan_content
|
||||||
self.creation_content.back();
|
.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 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, |_| {
|
||||||
|
if creation.on_back() {
|
||||||
|
close = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if close {
|
||||||
|
self.creation_content = None;
|
||||||
|
}
|
||||||
} else if show_wallet && dual_panel {
|
} else if show_wallet && dual_panel {
|
||||||
let list_icon = if show_list {
|
if qr_scan {
|
||||||
SIDEBAR_SIMPLE
|
View::title_button_big(ui, ARROW_LEFT, |_| {
|
||||||
|
cb.stop_camera();
|
||||||
|
self.wallet_content.as_mut().unwrap().qr_scan_content = None;
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
SUITCASE
|
let list_icon = if show_list {
|
||||||
};
|
SIDEBAR_SIMPLE
|
||||||
View::title_button_big(ui, list_icon, |_| {
|
} else {
|
||||||
self.show_wallets_at_dual_panel = !show_list;
|
SUITCASE
|
||||||
AppConfig::toggle_show_wallets_at_dual_panel();
|
};
|
||||||
});
|
View::title_button_big(ui, list_icon, |_| {
|
||||||
} else if !Content::is_dual_panel_mode(ui) {
|
self.show_wallets_at_dual_panel = !show_list;
|
||||||
|
AppConfig::toggle_show_wallets_at_dual_panel();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} 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,67 +491,37 @@ 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.
|
|
||||||
if !wallet.is_open() && wallet.reopen_needed() {
|
|
||||||
wallet.set_reopen(false);
|
|
||||||
self.wallets.select(Some(wallet.get_config().id));
|
|
||||||
self.show_open_wallet_modal(cb);
|
|
||||||
}
|
|
||||||
// Draw wallet list item.
|
|
||||||
self.wallet_item_ui(ui, wallet, cb);
|
|
||||||
ui.add_space(5.0);
|
|
||||||
}
|
}
|
||||||
});
|
// Check if wallet reopen is needed.
|
||||||
|
if w.reopen_needed() && !w.is_open() {
|
||||||
|
w.set_reopen(false);
|
||||||
|
self.show_opening_modal(w.clone(), None, cb);
|
||||||
|
}
|
||||||
|
self.wallet_item_ui(ui, w, cb);
|
||||||
|
ui.add_space(5.0);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -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,
|
let max_width = ui.available_width();
|
||||||
cb: &dyn PlatformCallbacks) {
|
dual_panel_root && max_width >= (Content::SIDE_PANEL_WIDTH * 2.0) + View::get_right_inset()
|
||||||
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();
|
|
||||||
dual_panel_root && max_width >= (Content::SIDE_PANEL_WIDTH * 2.0) + View::get_right_inset()
|
|
||||||
}
|
}
|
|
@ -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")
|
|
||||||
.frame(egui::Frame {
|
|
||||||
fill: Colors::fill(),
|
|
||||||
inner_margin: Margin {
|
|
||||||
left: View::far_left_inset_margin(ui) + 8.0,
|
|
||||||
right: View::get_right_inset() + 8.0,
|
|
||||||
top: 4.0,
|
|
||||||
bottom: View::get_bottom_inset(),
|
|
||||||
},
|
|
||||||
..Default::default()
|
|
||||||
})
|
|
||||||
.show_inside(ui, |ui| {
|
|
||||||
ui.vertical_centered(|ui| {
|
|
||||||
ui.vertical_centered(|ui| {
|
|
||||||
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 2.0, |ui| {
|
|
||||||
self.step_control_ui(ui, on_create, cb);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
egui::TopBottomPanel::bottom("wallet_creation_step_panel")
|
||||||
|
.frame(egui::Frame {
|
||||||
|
inner_margin: Margin {
|
||||||
|
left: View::far_left_inset_margin(ui) + View::TAB_ITEMS_PADDING,
|
||||||
|
right: View::get_right_inset() + View::TAB_ITEMS_PADDING,
|
||||||
|
top: View::TAB_ITEMS_PADDING,
|
||||||
|
bottom: View::get_bottom_inset() + View::TAB_ITEMS_PADDING,
|
||||||
|
},
|
||||||
|
fill: Colors::fill_deep(),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.show_inside(ui, |ui| {
|
||||||
|
// Draw divider line.
|
||||||
|
let rect = {
|
||||||
|
let mut r = ui.available_rect_before_wrap();
|
||||||
|
r.min.y -= View::TAB_ITEMS_PADDING;
|
||||||
|
r.min.x -= View::far_left_inset_margin(ui) + View::TAB_ITEMS_PADDING;
|
||||||
|
r.max.x += View::get_right_inset() + View::TAB_ITEMS_PADDING;
|
||||||
|
r
|
||||||
|
};
|
||||||
|
View::line(ui, LinePosition::TOP, &rect, Colors::item_stroke());
|
||||||
|
// Show step control content.
|
||||||
|
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
|
||||||
|
self.step_control_ui(ui, on_create, cb);
|
||||||
});
|
});
|
||||||
}
|
});
|
||||||
|
|
||||||
// Show wallet creation step content panel.
|
// 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,25 +151,18 @@ 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
|
};
|
||||||
};
|
View::max_width_ui(ui, max_width, |ui| {
|
||||||
View::max_width_ui(ui, max_width, |ui| {
|
self.step_content_ui(ui, cb);
|
||||||
self.step_content_ui(ui, cb);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -135,111 +173,125 @@ 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
|
(text, available)
|
||||||
.words
|
}
|
||||||
.contains(&String::from(""));
|
Step::ConfirmMnemonic => {
|
||||||
(text, available)
|
let text = t!("wallets.restore_phrase_desc");
|
||||||
}
|
let available = !self.mnemonic_setup.mnemonic.has_empty_or_invalid();
|
||||||
Step::ConfirmMnemonic => {
|
(text, available)
|
||||||
let text = t!("wallets.restore_phrase_desc");
|
}
|
||||||
let available = !self
|
Step::SetupConnection => {
|
||||||
.mnemonic_setup
|
(t!("wallets.setup_conn_desc"), self.creation_error.is_none())
|
||||||
.mnemonic
|
}
|
||||||
.confirm_words
|
};
|
||||||
.contains(&String::from(""));
|
|
||||||
(text, available)
|
// Show step description or error.
|
||||||
},
|
let generate_step = step == &Step::EnterMnemonic &&
|
||||||
Step::SetupConnection => (t!("wallets.setup_conn_desc"), true)
|
self.mnemonic_setup.mnemonic.mode() == PhraseMode::Generate;
|
||||||
};
|
if (self.mnemonic_setup.mnemonic.valid() && self.creation_error.is_none()) ||
|
||||||
// Show step description.
|
generate_step {
|
||||||
ui.add_space(2.0);
|
|
||||||
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() {
|
||||||
|
PhraseMode::Generate => {
|
||||||
|
let c_t = format!("{} {}",
|
||||||
|
COPY,
|
||||||
|
t!("copy").to_uppercase());
|
||||||
|
View::button(ui, c_t, Colors::white_or_black(false), || {
|
||||||
|
cb.copy_string_to_buffer(self.mnemonic_setup
|
||||||
|
.mnemonic
|
||||||
|
.get_phrase());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
PhraseMode::Import => {
|
||||||
|
let p_t = format!("{} {}",
|
||||||
|
CLIPBOARD_TEXT,
|
||||||
|
t!("paste").to_uppercase());
|
||||||
|
View::button(ui, p_t, Colors::white_or_black(false), || {
|
||||||
|
let data = ZeroingString::from(cb.get_string_from_buffer());
|
||||||
|
self.mnemonic_setup.mnemonic.import(&data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
// Show next step or QR code scan button.
|
||||||
columns[1].vertical_centered_justified(|ui| {
|
columns[1].vertical_centered_justified(|ui| {
|
||||||
if step_available {
|
if next {
|
||||||
// Show next step button if there are no empty words.
|
self.next_step_button_ui(ui, on_create);
|
||||||
self.next_step_button_ui(ui, step, on_create);
|
|
||||||
} else {
|
} else {
|
||||||
// Show QR code scan button.
|
let scan_text = format!("{} {}",
|
||||||
let scan_text = format!("{} {}", SCAN, t!("scan").to_uppercase());
|
SCAN,
|
||||||
|
t!("scan").to_uppercase());
|
||||||
View::button(ui, scan_text, Colors::white_or_black(false), || {
|
View::button(ui, scan_text, Colors::white_or_black(false), || {
|
||||||
self.mnemonic_setup.show_qr_scan_modal(cb);
|
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();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
ui.add_space(4.0);
|
}
|
||||||
} else if step == Step::ConfirmMnemonic {
|
Step::ConfirmMnemonic => {
|
||||||
ui.add_space(4.0);
|
|
||||||
// Show next step or paste button.
|
// Show next step or paste button.
|
||||||
if step_available {
|
if next {
|
||||||
self.next_step_button_ui(ui, step, on_create);
|
self.next_step_button_ui(ui, on_create);
|
||||||
} else {
|
} else {
|
||||||
let paste_text = format!("{} {}", CLIPBOARD_TEXT, t!("paste").to_uppercase());
|
let paste_text = format!("{} {}", CLIPBOARD_TEXT, t!("paste").to_uppercase());
|
||||||
View::button(ui, paste_text, Colors::white_or_black(false), || {
|
View::button(ui, paste_text, 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, true);
|
self.mnemonic_setup.mnemonic.import(&data);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
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);
|
Step::SetupConnection => {
|
||||||
}
|
if next {
|
||||||
}
|
self.next_step_button_ui(ui, on_create);
|
||||||
|
ui.add_space(2.0);
|
||||||
/// Draw copy or paste button at [`Step::EnterMnemonic`].
|
}
|
||||||
fn copy_or_paste_button_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
|
||||||
match self.mnemonic_setup.mnemonic.mode {
|
|
||||||
PhraseMode::Generate => {
|
|
||||||
// Show copy button.
|
|
||||||
let c_t = format!("{} {}", COPY, t!("copy").to_uppercase());
|
|
||||||
View::button(ui, c_t.to_uppercase(), Colors::white_or_black(false), || {
|
|
||||||
cb.copy_string_to_buffer(self.mnemonic_setup.mnemonic.get_phrase());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
PhraseMode::Import => {
|
|
||||||
// Show paste button.
|
|
||||||
let p_t = format!("{} {}", CLIPBOARD_TEXT, t!("paste").to_uppercase());
|
|
||||||
View::button(ui, p_t, Colors::white_or_black(false), || {
|
|
||||||
let data = ZeroingString::from(cb.get_string_from_buffer().trim());
|
|
||||||
self.mnemonic_setup.mnemonic.import_text(&data, false);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -247,64 +299,50 @@ impl WalletCreation {
|
||||||
/// 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 {
|
Step::ConfirmMnemonic
|
||||||
Some(Step::ConfirmMnemonic)
|
} else {
|
||||||
} else {
|
Step::SetupConnection
|
||||||
// Check if entered phrase was valid.
|
}
|
||||||
if self.mnemonic_setup.valid_phrase {
|
}
|
||||||
Some(Step::SetupConnection)
|
Step::ConfirmMnemonic => {
|
||||||
} else {
|
Step::SetupConnection
|
||||||
Some(Step::EnterMnemonic)
|
},
|
||||||
}
|
Step::SetupConnection => {
|
||||||
}
|
// Create wallet at last step.
|
||||||
}
|
match Wallet::create(&self.name,
|
||||||
Step::ConfirmMnemonic => {
|
&self.pass,
|
||||||
Some(Step::SetupConnection)
|
&self.mnemonic_setup.mnemonic,
|
||||||
},
|
&self.network_setup.method) {
|
||||||
Step::SetupConnection => {
|
Ok(w) => {
|
||||||
// Create wallet at last step.
|
self.mnemonic_setup.reset();
|
||||||
let name = self.name_edit.clone();
|
// Pass created wallet to callback.
|
||||||
let pass = self.pass_edit.clone();
|
(on_create)(w);
|
||||||
let phrase = self.mnemonic_setup.mnemonic.get_phrase();
|
Step::EnterMnemonic
|
||||||
let conn_method = &self.network_setup.method;
|
}
|
||||||
let mut wallet = Wallet::create(name,
|
Err(e) => {
|
||||||
pass.clone(),
|
self.creation_error = Some(format!("{:?}", e));
|
||||||
phrase,
|
Step::SetupConnection
|
||||||
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();
|
|
||||||
None
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} 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 => {
|
Step::EnterMnemonic => self.mnemonic_setup.ui(ui, cb),
|
||||||
// Show wallet creation message if step is empty.
|
Step::ConfirmMnemonic => self.mnemonic_setup.confirm_ui(ui, cb),
|
||||||
View::center_content(ui, 350.0 + View::get_bottom_inset(), |ui| {
|
Step::SetupConnection => {
|
||||||
// Show app logo.
|
// Redraw if node is running.
|
||||||
View::app_logo_name_version(ui);
|
if Node::is_running() && !Content::is_dual_panel_mode(ui.ctx()) {
|
||||||
ui.add_space(4.0);
|
ui.ctx().request_repaint_after(Node::STATS_UPDATE_DELAY);
|
||||||
|
|
||||||
let text = t!("wallets.create_desc");
|
|
||||||
ui.label(RichText::new(text)
|
|
||||||
.size(16.0)
|
|
||||||
.color(Colors::gray())
|
|
||||||
);
|
|
||||||
ui.add_space(8.0);
|
|
||||||
// Show wallet creation button.
|
|
||||||
let add_text = format!("{} {}", FOLDER_PLUS, t!("wallets.add"));
|
|
||||||
View::button(ui, add_text, Colors::white_or_black(false), || {
|
|
||||||
self.show_name_pass_modal(cb);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Some(step) => {
|
|
||||||
match step {
|
|
||||||
Step::EnterMnemonic => self.mnemonic_setup.ui(ui, cb),
|
|
||||||
Step::ConfirmMnemonic => self.mnemonic_setup.confirm_ui(ui, cb),
|
|
||||||
Step::SetupConnection => {
|
|
||||||
// Redraw if node is running.
|
|
||||||
if Node::is_running() {
|
|
||||||
ui.ctx().request_repaint_after(Node::STATS_UPDATE_DELAY);
|
|
||||||
}
|
|
||||||
self.network_setup.create_ui(ui, cb)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
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;
|
Step::SetupConnection => {
|
||||||
self.name_edit = String::from("");
|
self.creation_error = None;
|
||||||
self.pass_edit = String::from("");
|
self.step = Step::EnterMnemonic;
|
||||||
self.mnemonic_setup.reset();
|
false
|
||||||
},
|
|
||||||
Step::ConfirmMnemonic => self.step = Some(Step::EnterMnemonic),
|
|
||||||
Step::SetupConnection => self.step = Some(Step::EnterMnemonic)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
_ => 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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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.
|
||||||
|
|
116
src/gui/views/wallets/modals/add.rs
Normal file
116
src/gui/views/wallets/modals/add.rs
Normal 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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -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, || {
|
ui.add_space(14.0);
|
||||||
self.ext_conn = None;
|
ui.label(RichText::new(CHECK_FAT).size(20.0).color(Colors::green()));
|
||||||
on_select(None);
|
ui.add_space(14.0);
|
||||||
modal.close();
|
}
|
||||||
});
|
_ => {
|
||||||
} else {
|
View::item_button(ui, View::item_rounding(0, 1, true), CHECK, None, || {
|
||||||
ui.add_space(14.0);
|
on_select(ConnectionMethod::Integrated);
|
||||||
ui.label(RichText::new(CHECK_FAT).size(20.0).color(Colors::green()));
|
modal.close();
|
||||||
ui.add_space(14.0);
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 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.
|
||||||
|
|
|
@ -16,4 +16,10 @@ mod conn;
|
||||||
pub use conn::*;
|
pub use conn::*;
|
||||||
|
|
||||||
mod wallets;
|
mod wallets;
|
||||||
pub use wallets::*;
|
pub use wallets::*;
|
||||||
|
|
||||||
|
mod open;
|
||||||
|
pub use open::*;
|
||||||
|
|
||||||
|
mod add;
|
||||||
|
pub use add::*;
|
123
src/gui/views/wallets/modals/open.rs
Normal file
123
src/gui/views/wallets/modals/open.rs
Normal 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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -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,16 +111,34 @@ 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| {
|
||||||
// Draw button to select wallet.
|
if self.can_open {
|
||||||
let current = self.selected.unwrap_or(0) == id;
|
// Show button to select or open closed wallet.
|
||||||
if current {
|
let icon = if wallet.is_open() {
|
||||||
ui.add_space(12.0);
|
CHECK
|
||||||
ui.label(RichText::new(CHECK_FAT).size(20.0).color(Colors::green()));
|
} else {
|
||||||
} else {
|
FOLDER_OPEN
|
||||||
View::item_button(ui, View::item_rounding(0, 1, true), CHECK, None, || {
|
};
|
||||||
on_select(id);
|
View::item_button(ui, View::item_rounding(0, 1, true), icon, None, || {
|
||||||
modal.close();
|
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.
|
||||||
|
let current = self.selected_id.unwrap_or(0) == id;
|
||||||
|
if current {
|
||||||
|
ui.add_space(12.0);
|
||||||
|
ui.label(RichText::new(CHECK_FAT).size(20.0).color(Colors::green()));
|
||||||
|
} else {
|
||||||
|
View::item_button(ui, View::item_rounding(0, 1, true), CHECK, None, || {
|
||||||
|
on_select();
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let layout_size = ui.available_size();
|
let layout_size = ui.available_size();
|
||||||
|
@ -104,29 +146,37 @@ impl WalletsModal {
|
||||||
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.
|
||||||
let address = if let Some(port) = config.api_port {
|
if self.can_open {
|
||||||
format!("127.0.0.1:{}", port)
|
ui.label(RichText::new(wallet_status_text(wallet))
|
||||||
|
.size(15.0)
|
||||||
|
.color(Colors::gray()));
|
||||||
} else {
|
} else {
|
||||||
"-".to_string()
|
let address = if let Some(port) = config.api_port {
|
||||||
};
|
format!("127.0.0.1:{}", port)
|
||||||
let api_text = format!("{} {}", PLUGS_CONNECTED, address);
|
} else {
|
||||||
ui.label(RichText::new(api_text).size(15.0).color(Colors::gray()));
|
"-".to_string()
|
||||||
|
};
|
||||||
|
let api_text = format!("{} {}", PLUGS_CONNECTED, address);
|
||||||
|
ui.label(RichText::new(api_text).size(15.0).color(Colors::gray()));
|
||||||
|
}
|
||||||
ui.add_space(3.0);
|
ui.add_space(3.0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
483
src/gui/views/wallets/wallet/messages/content.rs
Normal file
483
src/gui/views/wallets/wallet/messages/content.rs
Normal 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
18
src/gui/views/wallets/wallet/messages/mod.rs
Normal file
18
src/gui/views/wallets/wallet/messages/mod.rs
Normal 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;
|
260
src/gui/views/wallets/wallet/messages/request.rs
Normal file
260
src/gui/views/wallets/wallet/messages/request.rs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
@ -25,4 +27,6 @@ mod transport;
|
||||||
pub use transport::WalletTransport;
|
pub use transport::WalletTransport;
|
||||||
|
|
||||||
mod content;
|
mod content;
|
||||||
pub use content::WalletContent;
|
pub use content::WalletContent;
|
||||||
|
|
||||||
|
mod modals;
|
240
src/gui/views/wallets/wallet/modals/accounts.rs
Normal file
240
src/gui/views/wallets/wallet/modals/accounts.rs
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
16
src/gui/views/wallets/wallet/modals/mod.rs
Normal file
16
src/gui/views/wallets/wallet/modals/mod.rs
Normal 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::*;
|
|
@ -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);
|
||||||
|
|
|
@ -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 {
|
|
||||||
w.update_ext_conn_id(None);
|
|
||||||
}
|
|
||||||
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.
|
|
||||||
if changed {
|
if changed {
|
||||||
if !w.reopen_needed() {
|
wallet.update_connection(&self.method);
|
||||||
w.set_reopen(true);
|
// Reopen wallet if connection changed.
|
||||||
w.close();
|
if !wallet.reopen_needed() {
|
||||||
|
wallet.set_reopen(true);
|
||||||
|
wallet.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> {
|
||||||
// Check if current external connection was deleted to show at 1st place.
|
match m {
|
||||||
if let Some(wallet) = wallet {
|
ConnectionMethod::External(id, _) => if c.deleted && *id == c.id {
|
||||||
if let Some(conn) = wallet.get_current_ext_conn() {
|
None
|
||||||
if ext_conn_list.iter()
|
} else {
|
||||||
.filter(|c| c.id == conn.id)
|
Some(*id == c.id)
|
||||||
.collect::<Vec<&ExternalConnection>>().is_empty() {
|
},
|
||||||
if self.curr_ext_conn.is_none() {
|
_ => Some(false)
|
||||||
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() {
|
let method = &self.method.clone();
|
||||||
|
let ext_conn_list = ConnectionsConfig::ext_conn_list();
|
||||||
|
let ext_list = ext_conn_list.iter().filter(|c| {
|
||||||
|
!c.deleted || is_current(method, c).unwrap_or(true)
|
||||||
|
}).collect::<Vec<&ExternalConnection>>();
|
||||||
|
let ext_size = ext_list.len();
|
||||||
|
if ext_size != 0 {
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -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.
|
// Show common wallet setup.
|
||||||
if WalletContent::block_navigation_on_sync(wallet) {
|
self.common_setup.ui(ui, wallet, cb);
|
||||||
WalletContent::sync_progress_ui(ui, wallet);
|
// Show wallet connections setup.
|
||||||
return;
|
self.conn_setup.wallet_ui(ui, wallet, cb);
|
||||||
}
|
// Show wallet recovery setup.
|
||||||
|
self.recovery_setup.ui(ui, wallet, cb);
|
||||||
// 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.
|
|
||||||
self.common_setup.ui(ui, wallet, cb);
|
|
||||||
// Show wallet connections setup.
|
|
||||||
self.conn_setup.wallet_ui(ui, wallet, cb);
|
|
||||||
// Show wallet recovery setup.
|
|
||||||
self.recovery_setup.ui(ui, wallet, cb);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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| {
|
||||||
|
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
364
src/gui/views/wallets/wallet/transport/content.rs
Normal file
364
src/gui/views/wallets/wallet/transport/content.rs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
19
src/gui/views/wallets/wallet/transport/mod.rs
Normal file
19
src/gui/views/wallets/wallet/transport/mod.rs
Normal 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;
|
363
src/gui/views/wallets/wallet/transport/send.rs
Normal file
363
src/gui/views/wallets/wallet/transport/send.rs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
258
src/gui/views/wallets/wallet/transport/settings.rs
Normal file
258
src/gui/views/wallets/wallet/transport/settings.rs
Normal 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
538
src/gui/views/wallets/wallet/txs/content.rs
Normal file
538
src/gui/views/wallets/wallet/txs/content.rs
Normal 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);
|
||||||
|
}
|
19
src/gui/views/wallets/wallet/txs/mod.rs
Normal file
19
src/gui/views/wallets/wallet/txs/mod.rs
Normal 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::*;
|
600
src/gui/views/wallets/wallet/txs/tx.rs
Normal file
600
src/gui/views/wallets/wallet/txs/tx.rs
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -48,4 +49,39 @@ impl WalletTabType {
|
||||||
WalletTabType::Settings => t!("wallets.settings")
|
WalletTabType::Settings => t!("wallets.settings")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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"))
|
||||||
|
}
|
||||||
}
|
}
|
83
src/lib.rs
83
src/lib.rs
|
@ -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
|
||||||
|
@ -255,4 +253,55 @@ fn setup_i18n() {
|
||||||
rust_i18n::set_locale(AppConfig::DEFAULT_LOCALE);
|
rust_i18n::set_locale(AppConfig::DEFAULT_LOCALE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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(_) => {}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
226
src/main.rs
226
src/main.rs
|
@ -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();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
std::fs::write(log, err.as_bytes()).unwrap_or_default();
|
||||||
}
|
}
|
||||||
std::fs::write(log, err.as_bytes()).unwrap();
|
// Print message error.
|
||||||
// Setup flag to show crash after app restart.
|
println!("{}\n{}", msg, loc);
|
||||||
grim::AppConfig::set_show_crash(true);
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// 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);
|
||||||
|
@ -126,4 +166,124 @@ 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)
|
||||||
}
|
}
|
|
@ -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 {
|
||||||
.node
|
&r_config
|
||||||
.server
|
.node
|
||||||
.api_secret_path
|
.server
|
||||||
.clone();
|
.foreign_api_secret_path
|
||||||
return if let Some(secret_path) = api_secret_path {
|
|
||||||
let api_secret_file = File::open(secret_path).unwrap();
|
|
||||||
let buf_reader = BufReader::new(api_secret_file);
|
|
||||||
let mut lines_iter = buf_reader.lines();
|
|
||||||
let first_line = lines_iter.next().unwrap();
|
|
||||||
Some(first_line.unwrap())
|
|
||||||
} else {
|
} else {
|
||||||
None
|
&r_config
|
||||||
};
|
.node
|
||||||
|
.server
|
||||||
|
.api_secret_path
|
||||||
|
}.clone();
|
||||||
|
if let Some(secret_path) = api_secret_path {
|
||||||
|
if let Ok(file) = File::open(secret_path) {
|
||||||
|
let buf_reader = BufReader::new(file);
|
||||||
|
let mut lines_iter = buf_reader.lines();
|
||||||
|
if let Some(Ok(line)) = lines_iter.next() {
|
||||||
|
return Some(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -604,40 +604,43 @@ 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
|
||||||
|
state.current_difficulty =
|
||||||
|
(new_block.header.total_difficulty() - head.total_difficulty).to_num();
|
||||||
|
|
||||||
// scaled difficulty
|
state.current_key_id = block_fees.key_id();
|
||||||
state.current_difficulty =
|
|
||||||
(new_block.header.total_difficulty() - head.total_difficulty).to_num();
|
|
||||||
|
|
||||||
state.current_key_id = block_fees.key_id();
|
current_hash = latest_hash;
|
||||||
|
// set the minimum acceptable share unscaled difficulty for this block
|
||||||
|
state.minimum_share_difficulty = config.minimum_share_difficulty;
|
||||||
|
|
||||||
current_hash = latest_hash;
|
// set a new deadline for rebuilding with fresh transactions
|
||||||
// set the minimum acceptable share unscaled difficulty for this block
|
deadline = Utc::now().timestamp() + config.attempt_time_per_block as i64;
|
||||||
state.minimum_share_difficulty = config.minimum_share_difficulty;
|
|
||||||
|
|
||||||
// set a new deadline for rebuilding with fresh transactions
|
// If this is a new block we will clear the current_block version history
|
||||||
deadline = Utc::now().timestamp() + config.attempt_time_per_block as i64;
|
if clear_blocks {
|
||||||
|
state.current_block_versions.clear();
|
||||||
|
}
|
||||||
|
|
||||||
// If this is a new block we will clear the current_block version history
|
// Update the mining stats
|
||||||
if clear_blocks {
|
self.workers.update_block_height(new_block.header.height);
|
||||||
state.current_block_versions.clear();
|
let difficulty = new_block.header.total_difficulty() - head.total_difficulty;
|
||||||
|
self.workers.update_network_difficulty(difficulty.to_num());
|
||||||
|
self.workers.update_network_hashrate();
|
||||||
|
|
||||||
|
// Add this new block candidate onto our list of block versions for height
|
||||||
|
state.current_block_versions.push(new_block);
|
||||||
|
} else {
|
||||||
|
thread::sleep(Duration::from_millis(1500));
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the mining stats
|
|
||||||
self.workers.update_block_height(new_block.header.height);
|
|
||||||
let difficulty = new_block.header.total_difficulty() - head.total_difficulty;
|
|
||||||
self.workers.update_network_difficulty(difficulty.to_num());
|
|
||||||
self.workers.update_network_hashrate();
|
|
||||||
|
|
||||||
// Add this new block candidate onto our list of block versions for this height
|
|
||||||
state.current_block_versions.push(new_block);
|
|
||||||
}
|
}
|
||||||
// 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) {
|
||||||
|
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -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
258
src/tor/http.rs
Normal 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 })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -19,4 +19,6 @@ mod tor;
|
||||||
pub use tor::Tor;
|
pub use tor::Tor;
|
||||||
|
|
||||||
mod types;
|
mod types;
|
||||||
pub use types::*;
|
pub use types::*;
|
||||||
|
|
||||||
|
mod http;
|
|
@ -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()])
|
||||||
|
|
|
@ -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
Loading…
Reference in a new issue