Compare commits
135 commits
Author | SHA1 | Date | |
---|---|---|---|
fb159c17a0 | |||
f7eb6580cc | |||
43720b34ba | |||
f1f0f002ce | |||
86afa21a60 | |||
0169acba81 | |||
073d950d41 | |||
4eaaebd739 | |||
a9e2106fda | |||
8b427989c5 | |||
f16ce3c69b | |||
a1b3330e5e | |||
3da8f5420b | |||
109e896506 | |||
8ad38f381e | |||
1e32315346 | |||
ef8c645a6a | |||
15ecdf1e57 | |||
587b00c93a | |||
aba2bead27 | |||
85ce58f69c | |||
bb7e00b0eb | |||
d60b35ebef | |||
eb60c52224 | |||
61828ea2db | |||
7e819e14d1 | |||
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 |
109 changed files with 11227 additions and 10062 deletions
37
.github/workflows/build.yml
vendored
37
.github/workflows/build.yml
vendored
|
@ -2,43 +2,6 @@ name: Build
|
|||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
android:
|
||||
name: Android Build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup JDK 17
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'temurin'
|
||||
cache: gradle
|
||||
- name: Setup build
|
||||
run: |
|
||||
cargo install cargo-ndk
|
||||
rustup target add aarch64-linux-android
|
||||
rustup target add armv7-linux-androideabi
|
||||
rustup target add x86_64-linux-android
|
||||
- name: Setup Java build
|
||||
run: |
|
||||
chmod +x android/gradlew
|
||||
echo "${{ secrets.ANDROID_RELEASE_KEYSTORE }}" > release.keystore.asc
|
||||
gpg -d --passphrase "${{ secrets.ANDROID_RELEASE_SECRET }}" --batch release.keystore.asc > android/keystore
|
||||
echo -e "storePassword=${{ secrets.ANDROID_PASS }}\nkeyPassword=${{ secrets.ANDROID_PASS }}\nkeyAlias=grim\nstoreFile=../keystore" > android/keystore.properties
|
||||
- name: Build lib 1/2
|
||||
continue-on-error: true
|
||||
run: |
|
||||
sed -i -e 's/"rlib"/"cdylib","rlib"/g' Cargo.toml
|
||||
export CPPFLAGS="-DMDB_USE_ROBUST=0" && export CFLAGS="-DMDB_USE_ROBUST=0" && cargo ndk -t arm64-v8a build --profile release-apk
|
||||
- name: Build lib 2/2
|
||||
run: |
|
||||
unset CPPFLAGS && unset CFLAGS && cargo ndk -t arm64-v8a -o android/app/src/main/jniLibs build --profile release-apk
|
||||
sed -i -e 's/"cdylib","rlib"/"rlib"/g' Cargo.toml
|
||||
- name: Build APK
|
||||
working-directory: android
|
||||
run: |
|
||||
./gradlew assembleRelease
|
||||
|
||||
linux:
|
||||
name: Linux Build
|
||||
runs-on: ubuntu-latest
|
||||
|
|
260
.github/workflows/release.yml
vendored
260
.github/workflows/release.yml
vendored
|
@ -1,260 +0,0 @@
|
|||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*.*.*"
|
||||
|
||||
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:
|
||||
name: Linux Release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Download appimagetools
|
||||
run: |
|
||||
wget https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage
|
||||
chmod +x appimagetool-x86_64.AppImage
|
||||
sudo apt install libfuse2
|
||||
- name: Zig Setup
|
||||
uses: goto-bus-stop/setup-zig@v2
|
||||
with:
|
||||
version: 0.12.1
|
||||
- name: Install cargo-zigbuild
|
||||
run: cargo install cargo-zigbuild
|
||||
- name: Release x86
|
||||
run: cargo zigbuild --release --target x86_64-unknown-linux-gnu
|
||||
- name: Release ARM
|
||||
run: |
|
||||
rustup target add aarch64-unknown-linux-gnu
|
||||
cargo zigbuild --release --target aarch64-unknown-linux-gnu
|
||||
- name: AppImage x86
|
||||
run: |
|
||||
cp target/x86_64-unknown-linux-gnu/release/grim linux/Grim.AppDir/AppRun
|
||||
./appimagetool-x86_64.AppImage linux/Grim.AppDir target/x86_64-unknown-linux-gnu/release/grim-${{ github.ref_name }}-linux-x86_64.AppImage
|
||||
- name: Checksum AppImage x86
|
||||
working-directory: target/x86_64-unknown-linux-gnu/release
|
||||
shell: pwsh
|
||||
run: get-filehash -algorithm sha256 grim-${{ github.ref_name }}-linux-x86_64.AppImage | Format-List | Out-String | ForEach-Object { $_.Trim() } > grim-${{ github.ref_name }}-linux-x86_64-appimage-sha256sum.txt
|
||||
- name: AppImage ARM
|
||||
run: |
|
||||
cp target/aarch64-unknown-linux-gnu/release/grim linux/Grim.AppDir/AppRun
|
||||
./appimagetool-x86_64.AppImage linux/Grim.AppDir target/aarch64-unknown-linux-gnu/release/grim-${{ github.ref_name }}-linux-arm.AppImage
|
||||
- name: Checksum AppImage ARM
|
||||
working-directory: target/aarch64-unknown-linux-gnu/release
|
||||
shell: pwsh
|
||||
run: get-filehash -algorithm sha256 grim-${{ github.ref_name }}-linux-arm.AppImage | Format-List | Out-String | ForEach-Object { $_.Trim() } > grim-${{ github.ref_name }}-linux-arm-appimage-sha256sum.txt
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
files: |
|
||||
target/x86_64-unknown-linux-gnu/release/grim-${{ github.ref_name }}-linux-x86_64.AppImage
|
||||
target/x86_64-unknown-linux-gnu/release/grim-${{ github.ref_name }}-linux-x86_64-appimage-sha256sum.txt
|
||||
target/aarch64-unknown-linux-gnu/release/grim-${{ github.ref_name }}-linux-arm.AppImage
|
||||
target/aarch64-unknown-linux-gnu/release/grim-${{ github.ref_name }}-linux-arm-appimage-sha256sum.txt
|
||||
|
||||
windows_release:
|
||||
name: Windows Release
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Build release
|
||||
run: cargo build --release
|
||||
- name: Archive release
|
||||
uses: vimtor/action-zip@v1
|
||||
with:
|
||||
files: target/release/grim.exe
|
||||
dest: target/release/grim-${{ github.ref_name }}-win-x86_64.zip
|
||||
- name: Checksum release
|
||||
working-directory: target/release
|
||||
shell: pwsh
|
||||
run: get-filehash -algorithm sha256 grim-${{ github.ref_name }}-win-x86_64.zip | Format-List | Out-String | ForEach-Object { $_.Trim() } > grim-${{ github.ref_name }}-win-x86_64-sha256sum.txt
|
||||
- name: Install cargo-wix
|
||||
run: cargo install cargo-wix
|
||||
- name: Run cargo-wix
|
||||
run: cargo wix -p grim -o ./target/wix/grim-${{ github.ref_name }}-win-x86_64.msi --nocapture
|
||||
- name: Checksum msi
|
||||
working-directory: target/wix
|
||||
shell: pwsh
|
||||
run: get-filehash -algorithm sha256 grim-${{ github.ref_name }}-win-x86_64.msi | Format-List | Out-String | ForEach-Object { $_.Trim() } > grim-${{ github.ref_name }}-win-x86_64-msi-sha256sum.txt
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
files: |
|
||||
target/release/grim-${{ github.ref_name }}-win-x86_64.zip
|
||||
target/release/grim-${{ github.ref_name }}-win-x86_64-sha256sum.txt
|
||||
target/wix/grim-${{ github.ref_name }}-win-x86_64.msi
|
||||
target/wix/grim-${{ github.ref_name }}-win-x86_64-msi-sha256sum.txt
|
||||
|
||||
macos_release:
|
||||
name: MacOS Release
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Zig Setup
|
||||
uses: goto-bus-stop/setup-zig@v2
|
||||
with:
|
||||
version: 0.12.1
|
||||
- name: Install cargo-zigbuild
|
||||
run: cargo install cargo-zigbuild
|
||||
- name: Release x86
|
||||
run: |
|
||||
rustup target add x86_64-apple-darwin
|
||||
cargo zigbuild --release --target x86_64-apple-darwin
|
||||
mkdir macos/Grim.app/Contents/MacOS
|
||||
yes | cp -rf target/x86_64-apple-darwin/release/grim macos/Grim.app/Contents/MacOS
|
||||
- name: Archive x86
|
||||
run: |
|
||||
cd macos
|
||||
zip -r grim-${{ github.ref_name }}-macos-x86_64.zip Grim.app
|
||||
mv grim-${{ github.ref_name }}-macos-x86_64.zip ../target/x86_64-apple-darwin/release
|
||||
cd ..
|
||||
- name: Checksum Release x86
|
||||
working-directory: target/x86_64-apple-darwin/release
|
||||
shell: pwsh
|
||||
run: get-filehash -algorithm sha256 grim-${{ github.ref_name }}-macos-x86_64.zip | Format-List | Out-String | ForEach-Object { $_.Trim() } > grim-${{ github.ref_name }}-macos-x86_64-sha256sum.txt
|
||||
- name: Release ARM
|
||||
run: |
|
||||
rustup target add aarch64-apple-darwin
|
||||
cargo zigbuild --release --target aarch64-apple-darwin
|
||||
yes | cp -rf target/aarch64-apple-darwin/release/grim macos/Grim.app/Contents/MacOS
|
||||
- name: Archive ARM
|
||||
run: |
|
||||
cd macos
|
||||
zip -r grim-${{ github.ref_name }}-macos-arm.zip Grim.app
|
||||
mv grim-${{ github.ref_name }}-macos-arm.zip ../target/aarch64-apple-darwin/release
|
||||
cd ..
|
||||
- name: Checksum Release ARM
|
||||
working-directory: target/aarch64-apple-darwin/release
|
||||
shell: pwsh
|
||||
run: get-filehash -algorithm sha256 grim-${{ github.ref_name }}-macos-arm.zip | Format-List | Out-String | ForEach-Object { $_.Trim() } > grim-${{ github.ref_name }}-macos-arm-sha256sum.txt
|
||||
- name: Release Universal
|
||||
run: |
|
||||
rustup target add aarch64-apple-darwin
|
||||
rustup target add x86_64-apple-darwin
|
||||
cargo zigbuild --release --target universal2-apple-darwin
|
||||
yes | cp -rf target/universal2-apple-darwin/release/grim macos/Grim.app/Contents/MacOS
|
||||
- name: Archive Universal
|
||||
run: |
|
||||
cd macos
|
||||
zip -r grim-${{ github.ref_name }}-macos-universal.zip Grim.app
|
||||
mv grim-${{ github.ref_name }}-macos-universal.zip ../target/universal2-apple-darwin/release
|
||||
cd ..
|
||||
- name: Checksum Release Universal
|
||||
working-directory: target/universal2-apple-darwin/release
|
||||
shell: pwsh
|
||||
run: get-filehash -algorithm sha256 grim-${{ github.ref_name }}-macos-universal.zip | Format-List | Out-String | ForEach-Object { $_.Trim() } > grim-${{ github.ref_name }}-macos-universal-sha256sum.txt
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
files: |
|
||||
target/x86_64-apple-darwin/release/grim-${{ github.ref_name }}-macos-x86_64.zip
|
||||
target/x86_64-apple-darwin/release/grim-${{ github.ref_name }}-macos-x86_64-sha256sum.txt
|
||||
target/aarch64-apple-darwin/release/grim-${{ github.ref_name }}-macos-arm.zip
|
||||
target/aarch64-apple-darwin/release/grim-${{ github.ref_name }}-macos-arm-sha256sum.txt
|
||||
target/universal2-apple-darwin/release/grim-${{ github.ref_name }}-macos-universal.zip
|
||||
target/universal2-apple-darwin/release/grim-${{ github.ref_name }}-macos-universal-sha256sum.txt
|
170
.github/workflows/release.yml.bak
vendored
Normal file
170
.github/workflows/release.yml.bak
vendored
Normal file
|
@ -0,0 +1,170 @@
|
|||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*.*.*"
|
||||
|
||||
jobs:
|
||||
linux_release:
|
||||
name: Linux Release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Download appimagetools
|
||||
run: |
|
||||
wget https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage
|
||||
chmod +x appimagetool-x86_64.AppImage
|
||||
sudo apt install libfuse2
|
||||
- name: Zig Setup
|
||||
uses: goto-bus-stop/setup-zig@v2
|
||||
with:
|
||||
version: 0.12.1
|
||||
- name: Install cargo-zigbuild
|
||||
run: cargo install cargo-zigbuild
|
||||
- name: Release x86
|
||||
run: cargo zigbuild --release --target x86_64-unknown-linux-gnu
|
||||
- name: Release ARM
|
||||
run: |
|
||||
rustup target add aarch64-unknown-linux-gnu
|
||||
cargo zigbuild --release --target aarch64-unknown-linux-gnu
|
||||
- name: AppImage x86
|
||||
run: |
|
||||
cp target/x86_64-unknown-linux-gnu/release/grim linux/Grim.AppDir/AppRun
|
||||
./appimagetool-x86_64.AppImage linux/Grim.AppDir target/x86_64-unknown-linux-gnu/release/grim-${{ github.ref_name }}-linux-x86_64.AppImage
|
||||
- name: Checksum AppImage x86
|
||||
working-directory: target/x86_64-unknown-linux-gnu/release
|
||||
shell: bash
|
||||
run: sha256sum grim-${{ github.ref_name }}-linux-x86_64.AppImage > grim-${{ github.ref_name }}-linux-x86_64-appimage-sha256sum.txt
|
||||
- name: AppImage ARM
|
||||
run: |
|
||||
cp target/aarch64-unknown-linux-gnu/release/grim linux/Grim.AppDir/AppRun
|
||||
./appimagetool-x86_64.AppImage linux/Grim.AppDir target/aarch64-unknown-linux-gnu/release/grim-${{ github.ref_name }}-linux-arm.AppImage
|
||||
- name: Checksum AppImage ARM
|
||||
working-directory: target/aarch64-unknown-linux-gnu/release
|
||||
shell: bash
|
||||
run: sha256sum grim-${{ github.ref_name }}-linux-arm.AppImage > grim-${{ github.ref_name }}-linux-arm-appimage-sha256sum.txt
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
files: |
|
||||
target/x86_64-unknown-linux-gnu/release/grim-${{ github.ref_name }}-linux-x86_64.AppImage
|
||||
target/x86_64-unknown-linux-gnu/release/grim-${{ github.ref_name }}-linux-x86_64-appimage-sha256sum.txt
|
||||
target/aarch64-unknown-linux-gnu/release/grim-${{ github.ref_name }}-linux-arm.AppImage
|
||||
target/aarch64-unknown-linux-gnu/release/grim-${{ github.ref_name }}-linux-arm-appimage-sha256sum.txt
|
||||
|
||||
windows_release:
|
||||
name: Windows Release
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Build release
|
||||
run: cargo build --release
|
||||
- name: Archive release
|
||||
uses: vimtor/action-zip@v1
|
||||
with:
|
||||
files: target/release/grim.exe
|
||||
dest: target/release/grim-${{ github.ref_name }}-win-x86_64.zip
|
||||
- name: Checksum release
|
||||
working-directory: target/release
|
||||
shell: bash
|
||||
run: sha256sum grim-${{ github.ref_name }}-win-x86_64.zip > grim-${{ github.ref_name }}-win-x86_64-sha256sum.txt
|
||||
- name: Install cargo-wix
|
||||
run: cargo install cargo-wix
|
||||
- name: Run cargo-wix
|
||||
run: cargo wix -p grim -o ./target/wix/grim-${{ github.ref_name }}-win-x86_64.msi --nocapture
|
||||
- name: Checksum msi
|
||||
working-directory: target/wix
|
||||
shell: bash
|
||||
run: sha256sum grim-${{ github.ref_name }}-win-x86_64.msi > grim-${{ github.ref_name }}-win-x86_64-msi-sha256sum.txt
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
files: |
|
||||
target/release/grim-${{ github.ref_name }}-win-x86_64.zip
|
||||
target/release/grim-${{ github.ref_name }}-win-x86_64-sha256sum.txt
|
||||
target/wix/grim-${{ github.ref_name }}-win-x86_64.msi
|
||||
target/wix/grim-${{ github.ref_name }}-win-x86_64-msi-sha256sum.txt
|
||||
|
||||
macos_release:
|
||||
name: MacOS Release
|
||||
runs-on: macos-12
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Install coreutils
|
||||
run: brew install coreutils
|
||||
- name: Zig Setup
|
||||
uses: goto-bus-stop/setup-zig@v2
|
||||
with:
|
||||
version: 0.12.1
|
||||
- name: Install cargo-zigbuild
|
||||
run: cargo install cargo-zigbuild
|
||||
- name: Download SDK
|
||||
run: wget https://github.com/phracker/MacOSX-SDKs/releases/download/11.3/MacOSX11.0.sdk.tar.xz
|
||||
- name: Setup SDK env
|
||||
run: tar xf ${{ github.workspace }}/MacOSX11.0.sdk.tar.xz && echo "SDKROOT=${{ github.workspace }}/MacOSX11.0.sdk" >> $GITHUB_ENV
|
||||
- name: Setup platform env
|
||||
run: echo "MACOSX_DEPLOYMENT_TARGET=11.0" >> $GITHUB_ENV
|
||||
- name: Release x86
|
||||
run: |
|
||||
rustup target add x86_64-apple-darwin
|
||||
cargo zigbuild --release --target x86_64-apple-darwin
|
||||
yes | cp -rf target/x86_64-apple-darwin/release/grim macos/Grim.app/Contents/MacOS
|
||||
- name: Archive x86
|
||||
run: |
|
||||
cd macos
|
||||
zip -r grim-${{ github.ref_name }}-macos-x86_64.zip Grim.app
|
||||
mv grim-${{ github.ref_name }}-macos-x86_64.zip ../target/x86_64-apple-darwin/release
|
||||
cd ..
|
||||
- name: Checksum Release x86
|
||||
working-directory: target/x86_64-apple-darwin/release
|
||||
shell: bash
|
||||
run: sha256sum grim-${{ github.ref_name }}-macos-x86_64.zip > grim-${{ github.ref_name }}-macos-x86_64-sha256sum.txt
|
||||
- name: Release ARM
|
||||
run: |
|
||||
rustup target add aarch64-apple-darwin
|
||||
cargo zigbuild --release --target aarch64-apple-darwin
|
||||
yes | cp -rf target/aarch64-apple-darwin/release/grim macos/Grim.app/Contents/MacOS
|
||||
- name: Archive ARM
|
||||
run: |
|
||||
cd macos
|
||||
zip -r grim-${{ github.ref_name }}-macos-arm.zip Grim.app
|
||||
mv grim-${{ github.ref_name }}-macos-arm.zip ../target/aarch64-apple-darwin/release
|
||||
cd ..
|
||||
- name: Checksum Release ARM
|
||||
working-directory: target/aarch64-apple-darwin/release
|
||||
shell: bash
|
||||
run: sha256sum grim-${{ github.ref_name }}-macos-arm.zip > grim-${{ github.ref_name }}-macos-arm-sha256sum.txt
|
||||
- name: Release Universal
|
||||
run: |
|
||||
cargo zigbuild --release --target universal2-apple-darwin
|
||||
yes | cp -rf target/universal2-apple-darwin/release/grim macos/Grim.app/Contents/MacOS
|
||||
- name: Archive Universal
|
||||
run: |
|
||||
cd macos
|
||||
zip -r grim-${{ github.ref_name }}-macos-universal.zip Grim.app
|
||||
mv grim-${{ github.ref_name }}-macos-universal.zip ../target/universal2-apple-darwin/release
|
||||
cd ..
|
||||
- name: Checksum Release Universal
|
||||
working-directory: target/universal2-apple-darwin/release
|
||||
shell: pwsh
|
||||
run: sha256sum grim-${{ github.ref_name }}-macos-universal.zip > grim-${{ github.ref_name }}-macos-universal-sha256sum.txt
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
files: |
|
||||
target/x86_64-apple-darwin/release/grim-${{ github.ref_name }}-macos-x86_64.zip
|
||||
target/x86_64-apple-darwin/release/grim-${{ github.ref_name }}-macos-x86_64-sha256sum.txt
|
||||
target/aarch64-apple-darwin/release/grim-${{ github.ref_name }}-macos-arm.zip
|
||||
target/aarch64-apple-darwin/release/grim-${{ github.ref_name }}-macos-arm-sha256sum.txt
|
||||
target/universal2-apple-darwin/release/grim-${{ github.ref_name }}-macos-universal.zip
|
||||
target/universal2-apple-darwin/release/grim-${{ github.ref_name }}-macos-universal-sha256sum.txt
|
7
.gitignore
vendored
7
.gitignore
vendored
|
@ -1,9 +1,12 @@
|
|||
*.iml
|
||||
android/.idea
|
||||
android/.gradle
|
||||
android/local.properties
|
||||
android/keystore
|
||||
android/keystore.asc
|
||||
android/keystore.properties
|
||||
android/*.apk
|
||||
android/*sha256sum.txt
|
||||
/.idea
|
||||
.DS_Store
|
||||
/captures
|
||||
|
@ -13,7 +16,7 @@ android/keystore.properties
|
|||
target
|
||||
.cargo/
|
||||
app/src/main/jniLibs
|
||||
macos/Grim.app/Contents/MacOS/grim
|
||||
macos/cert.pem
|
||||
linux/Grim.AppDir/AppRun
|
||||
.intentionally-empty-file.o
|
||||
.intentionally-empty-file.o
|
||||
Cargo.toml-e
|
5177
Cargo.lock
generated
5177
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
143
Cargo.toml
143
Cargo.toml
|
@ -1,10 +1,10 @@
|
|||
[package]
|
||||
name = "grim"
|
||||
version = "0.1.0"
|
||||
authors = ["Ardocrat <ardocrat@proton.me>"]
|
||||
version = "0.2.4"
|
||||
authors = ["Ardocrat <ardocrat@gri.mw>"]
|
||||
description = "Cross-platform GUI for Grin with focus on usability and availability to be used by anyone, anywhere."
|
||||
license = "Apache-2.0"
|
||||
repository = "https://github.com/ardocrat/grim"
|
||||
repository = "https://gri.mw/code/GUI/grim"
|
||||
keywords = [ "crypto", "grin", "mimblewimble" ]
|
||||
edition = "2021"
|
||||
|
||||
|
@ -16,9 +16,6 @@ path = "src/main.rs"
|
|||
name="grim"
|
||||
crate-type = ["rlib"]
|
||||
|
||||
[profile.release]
|
||||
debug = 1
|
||||
|
||||
[profile.release-apk]
|
||||
inherits = "release"
|
||||
strip = true
|
||||
|
@ -28,108 +25,108 @@ codegen-units = 1
|
|||
panic = "abort"
|
||||
|
||||
[dependencies]
|
||||
log = "0.4"
|
||||
log = "0.4.27"
|
||||
|
||||
## node
|
||||
openssl-sys = { version = "0.9.82", features = ["vendored"] }
|
||||
grin_api = "5.3.1"
|
||||
grin_chain = "5.3.1"
|
||||
grin_config = "5.3.1"
|
||||
grin_core = "5.3.1"
|
||||
grin_p2p = "5.3.1"
|
||||
grin_servers = "5.3.1"
|
||||
grin_keychain = "5.3.1"
|
||||
grin_util = "5.3.1"
|
||||
grin_api = "5.3.3"
|
||||
grin_chain = "5.3.3"
|
||||
grin_config = "5.3.3"
|
||||
grin_core = "5.3.3"
|
||||
grin_p2p = "5.3.3"
|
||||
grin_servers = "5.3.3"
|
||||
grin_keychain = "5.3.3"
|
||||
grin_util = "5.3.3"
|
||||
|
||||
## wallet
|
||||
grin_wallet_impls = "5.3.1"
|
||||
grin_wallet_api = "5.3.1"
|
||||
grin_wallet_libwallet = "5.3.1"
|
||||
grin_wallet_util = "5.3.1"
|
||||
grin_wallet_controller = "5.3.1"
|
||||
grin_wallet_impls = "5.3.3"
|
||||
grin_wallet_api = "5.3.3"
|
||||
grin_wallet_libwallet = "5.3.3"
|
||||
grin_wallet_util = "5.3.3"
|
||||
grin_wallet_controller = "5.3.3"
|
||||
|
||||
## ui
|
||||
egui = { version = "0.28.1", default-features = false }
|
||||
egui_extras = { version = "0.28.1", features = ["image", "svg"] }
|
||||
egui = { version = "0.29.1", default-features = false }
|
||||
egui_extras = { version = "0.29.1", features = ["image", "svg"] }
|
||||
rust-i18n = "2.3.1"
|
||||
|
||||
## other
|
||||
backtrace = "0.3"
|
||||
panic-message = "0.3.0"
|
||||
thiserror = "1.0.58"
|
||||
futures = "0.3"
|
||||
dirs = "5.0.1"
|
||||
sys-locale = "0.3.0"
|
||||
chrono = "0.4.31"
|
||||
parking_lot = "0.12.1"
|
||||
lazy_static = "1.4.0"
|
||||
toml = "0.8.2"
|
||||
serde = "1.0.170"
|
||||
local-ip-address = "0.6.1"
|
||||
url = "2.4.0"
|
||||
rand = "0.8.5"
|
||||
serde_derive = "1.0.197"
|
||||
serde_json = "1.0.115"
|
||||
tokio = { version = "1.37.0", features = ["full"] }
|
||||
image = "0.25.1"
|
||||
rqrr = "0.7.1"
|
||||
anyhow = "1.0.97"
|
||||
pin-project = "1.1.10"
|
||||
backtrace = "0.3.74"
|
||||
thiserror = "1.0.64"
|
||||
futures = "0.3.31"
|
||||
dirs = "6.0.0"
|
||||
sys-locale = "0.3.1"
|
||||
chrono = "0.4.38"
|
||||
parking_lot = "0.12.3"
|
||||
lazy_static = "1.5.0"
|
||||
toml = "0.8.19"
|
||||
serde = "1.0.210"
|
||||
local-ip-address = "0.6.3"
|
||||
url = "2.5.2"
|
||||
rand = "0.9.0"
|
||||
serde_derive = "1.0.219"
|
||||
serde_json = "1.0.140"
|
||||
tokio = { version = "1.44.1", features = ["full"] }
|
||||
image = "0.25.6"
|
||||
rqrr = "0.8.0"
|
||||
qrcodegen = "1.8.0"
|
||||
qrcode = "0.14.0"
|
||||
qrcode = "0.14.1"
|
||||
ur = "0.4.1"
|
||||
gif = "0.13.1"
|
||||
rkv = { version = "0.19.0", features = ["lmdb"] }
|
||||
|
||||
## tor
|
||||
arti-client = { version = "0.19.0", features = ["pt-client", "static", "onion-service-service", "onion-service-client"] }
|
||||
tor-rtcompat = { version = "0.19.0", features = ["static"] }
|
||||
tor-config = "0.19.0"
|
||||
fs-mistrust = "0.7.9"
|
||||
tor-hsservice = "0.19.0"
|
||||
tor-hsrproxy = "0.19.0"
|
||||
tor-keymgr = "0.19.0"
|
||||
tor-llcrypto = "0.19.0"
|
||||
tor-hscrypto = "0.19.0"
|
||||
arti-hyper = "0.19.0"
|
||||
sha2 = "0.10.0"
|
||||
arti-client = { version = "0.29.0", features = ["pt-client", "static", "onion-service-service", "onion-service-client"] }
|
||||
tor-rtcompat = { version = "0.29.0", features = ["static"] }
|
||||
tor-config = "0.29.0"
|
||||
fs-mistrust = "0.9.1"
|
||||
tor-hsservice = "0.29.0"
|
||||
tor-hsrproxy = "0.29.0"
|
||||
tor-keymgr = "0.29.0"
|
||||
tor-llcrypto = "0.29.0"
|
||||
tor-hscrypto = "0.29.0"
|
||||
tor-error = "0.29.0"
|
||||
sha2 = "0.10.8"
|
||||
ed25519-dalek = "2.1.1"
|
||||
curve25519-dalek = "4.1.2"
|
||||
hyper = { version = "0.14.28", features = ["full"] }
|
||||
curve25519-dalek = "4.1.3"
|
||||
hyper = { version = "0.14.31", features = ["full"] }
|
||||
hyper-tls = "0.5.0"
|
||||
tls-api = "0.9.0"
|
||||
tls-api-native-tls = "0.9.0"
|
||||
tls-api = "0.12.0"
|
||||
tls-api-native-tls = "0.12.1"
|
||||
|
||||
## stratum server
|
||||
tokio-old = {version = "0.2", features = ["full"], package = "tokio" }
|
||||
tokio-util-old = { version = "0.2", features = ["codec"], package = "tokio-util" }
|
||||
|
||||
[target.'cfg(all(not(target_os = "windows"), not(target_os = "android")))'.dependencies]
|
||||
eye = { version = "0.5.0", default-features = false }
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
nokhwa = { version = "0.10.5", default-features = false, features = ["input-v4l"] }
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
nokhwa = { version = "0.10.4", default-features = false, features = ["input-msmf"] }
|
||||
nokhwa = { version = "0.10.5", default-features = false, features = ["input-msmf"] }
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
tls-api-openssl = "0.9.0"
|
||||
openpnp_capture_sys = "0.4.0"
|
||||
nokhwa-mac = { git = "https://github.com/l1npengtul/nokhwa", rev = "612c861ef153cf0ee575d8dd1413b960e4e19dd6", features = ["input-avfoundation", "output-threaded"], package = "nokhwa" }
|
||||
|
||||
[target.'cfg(not(target_os = "android"))'.dependencies]
|
||||
env_logger = "0.11.3"
|
||||
winit = { version = "0.29.15" }
|
||||
eframe = { version = "0.28.1", features = ["wgpu", "glow"] }
|
||||
winit = { version = "0.30.5" }
|
||||
eframe = { version = "0.29.1", features = ["wgpu", "glow"] }
|
||||
arboard = "3.2.0"
|
||||
rfd = "0.14.1"
|
||||
dark-light = "1.1.1"
|
||||
rfd = "0.15.0"
|
||||
interprocess = { version = "2.2.1", features = ["tokio"] }
|
||||
|
||||
[target.'cfg(target_os = "android")'.dependencies]
|
||||
android_logger = "0.13.1"
|
||||
android_logger = "0.14.1"
|
||||
jni = "0.21.1"
|
||||
wgpu = "22.1.0"
|
||||
android-activity = { version = "0.6.0", features = ["game-activity"] }
|
||||
wgpu = "0.20.1"
|
||||
winit = { version = "0.29.15", features = ["android-game-activity"] }
|
||||
eframe = { version = "0.28.1", features = ["wgpu", "android-game-activity"] }
|
||||
winit = { version = "0.30.5", features = ["android-game-activity"] }
|
||||
eframe = { version = "0.29.1", features = ["wgpu", "android-game-activity"] }
|
||||
|
||||
[patch.crates-io]
|
||||
egui_extras = { git = "https://github.com/emilk/egui", rev = "5b846b4554fe47269affb43efef2cad8710a8a47" }
|
||||
egui = { git = "https://github.com/emilk/egui", rev = "5b846b4554fe47269affb43efef2cad8710a8a47" }
|
||||
eframe = { git = "https://github.com/emilk/egui", rev = "5b846b4554fe47269affb43efef2cad8710a8a47" }
|
||||
### patch grin store
|
||||
#grin_store = { path = "../grin-store" }
|
||||
### fix cross-compilation support for macos
|
||||
openpnp_capture_sys = { git = "https://github.com/ardocrat/openpnp-capture-rs", branch = "cross_compilation_support" }
|
|
@ -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/)
|
||||
for maximum compatibility with original [Mimblewimble](https://github.com/mimblewimble/grin) implementation.
|
||||
Initially supported platforms are Linux, Mac, Windows, limited Android and possible web support with help of [egui](https://github.com/emilk/egui) - immediate mode GUI library in pure Rust.
|
||||
|
||||
Named by the character [Grim](http://harrypotter.wikia.com/wiki/Grim) - the shape of a large, black, menacing, spectral giant dog.
|
||||
|
||||

|
||||

|
||||
|
||||
|
||||
## Build instructions
|
||||
|
|
|
@ -2,10 +2,6 @@ plugins {
|
|||
id 'com.android.application'
|
||||
}
|
||||
|
||||
def keystorePropertiesFile = rootProject.file("keystore.properties")
|
||||
def keystoreProperties = new Properties()
|
||||
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
|
||||
|
||||
android {
|
||||
compileSdk 33
|
||||
ndkVersion '26.0.10792818'
|
||||
|
@ -14,24 +10,36 @@ android {
|
|||
applicationId "mw.gri.android"
|
||||
minSdk 24
|
||||
targetSdk 33
|
||||
versionCode 1
|
||||
versionName "0.1.0"
|
||||
versionCode 3
|
||||
versionName "0.2.4"
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
release {
|
||||
keyAlias keystoreProperties['keyAlias']
|
||||
keyPassword keystoreProperties['keyPassword']
|
||||
storeFile file(keystoreProperties['storeFile'])
|
||||
storePassword keystoreProperties['storePassword']
|
||||
def keystorePropertiesFile = rootProject.file("keystore.properties")
|
||||
def keystoreProperties = new Properties()
|
||||
if (keystorePropertiesFile.exists()) {
|
||||
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
|
||||
|
||||
signingConfigs {
|
||||
release {
|
||||
keyAlias keystoreProperties['keyAlias']
|
||||
keyPassword keystoreProperties['keyPassword']
|
||||
storeFile file(keystoreProperties['storeFile'])
|
||||
storePassword keystoreProperties['storePassword']
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
signingConfig signingConfigs.release
|
||||
}
|
||||
if (keystorePropertiesFile.exists()) {
|
||||
signedRelease {
|
||||
initWith release
|
||||
signingConfig signingConfigs.release
|
||||
}
|
||||
}
|
||||
debug {
|
||||
minifyEnabled false
|
||||
|
|
|
@ -1,15 +1,17 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<manifest xmlns:tools="http://schemas.android.com/tools" xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
>
|
||||
|
||||
<uses-feature android:name="android.hardware.camera" android:required="false"/>
|
||||
|
||||
<uses-permission android:name="android.permission.EXPAND_STATUS_BAR" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.CAMERA"/>
|
||||
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" tools:ignore="ScopedStorage"/>
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" tools:ignore="ScopedStorage"/>
|
||||
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" tools:ignore="ScopedStorage"/>
|
||||
|
||||
<application
|
||||
android:hardwareAccelerated="true"
|
||||
|
@ -18,7 +20,6 @@
|
|||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="Grim"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.Main">
|
||||
|
||||
<receiver android:name=".NotificationActionsReceiver"/>
|
||||
|
@ -44,6 +45,22 @@
|
|||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter android:scheme="http" tools:ignore="AppLinkUrlError">
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="text/*" />
|
||||
</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" />
|
||||
</activity>
|
||||
<service android:name=".BackgroundService" android:stopWithTask="true" />
|
||||
|
|
|
@ -152,13 +152,17 @@ public class BackgroundService extends Service {
|
|||
// Show notification with sync status.
|
||||
Intent i = getPackageManager().getLaunchIntentForPackage(this.getPackageName());
|
||||
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, i, PendingIntent.FLAG_IMMUTABLE);
|
||||
mNotificationBuilder = new NotificationCompat.Builder(this, TAG)
|
||||
.setContentTitle(this.getSyncTitle())
|
||||
.setContentText(this.getSyncStatusText())
|
||||
.setStyle(new NotificationCompat.BigTextStyle().bigText(this.getSyncStatusText()))
|
||||
.setSmallIcon(R.drawable.ic_stat_name)
|
||||
.setPriority(NotificationCompat.PRIORITY_MAX)
|
||||
.setContentIntent(pendingIntent);
|
||||
try {
|
||||
mNotificationBuilder = new NotificationCompat.Builder(this, TAG)
|
||||
.setContentTitle(this.getSyncTitle())
|
||||
.setContentText(this.getSyncStatusText())
|
||||
.setStyle(new NotificationCompat.BigTextStyle().bigText(this.getSyncStatusText()))
|
||||
.setSmallIcon(R.drawable.ic_stat_name)
|
||||
.setPriority(NotificationCompat.PRIORITY_MAX)
|
||||
.setContentIntent(pendingIntent);
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
return;
|
||||
}
|
||||
Notification notification = mNotificationBuilder.build();
|
||||
|
||||
// Start service at foreground state to prevent killing by system.
|
||||
|
|
|
@ -7,9 +7,9 @@ import android.content.*;
|
|||
import android.content.pm.PackageManager;
|
||||
import android.content.res.Configuration;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.*;
|
||||
import android.os.Process;
|
||||
import android.provider.Settings;
|
||||
import android.system.ErrnoException;
|
||||
import android.system.Os;
|
||||
import android.util.Size;
|
||||
|
@ -51,8 +51,7 @@ public class MainActivity extends GameActivity {
|
|||
@Override
|
||||
public void onReceive(Context ctx, Intent i) {
|
||||
if (i.getAction().equals(STOP_APP_ACTION)) {
|
||||
onExit();
|
||||
Process.killProcess(Process.myPid());
|
||||
exit();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -67,11 +66,19 @@ public class MainActivity extends GameActivity {
|
|||
private ExecutorService mCameraExecutor = null;
|
||||
private boolean mUseBackCamera = true;
|
||||
|
||||
private ActivityResultLauncher<Intent> mFilePickResultLauncher = null;
|
||||
private ActivityResultLauncher<Intent> mFilePickResult = null;
|
||||
private ActivityResultLauncher<Intent> mOpenFilePermissionsResult = null;
|
||||
|
||||
@SuppressLint("UnspecifiedRegisterReceiverFlag")
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
// Check if activity was launched to exclude from recent apps on exit.
|
||||
if ((getIntent().getFlags() & Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS) != 0) {
|
||||
super.onCreate(null);
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear cache on start.
|
||||
if (savedInstanceState == null) {
|
||||
Utils.deleteDirectoryContent(new File(getExternalCacheDir().getPath()), false);
|
||||
|
@ -91,8 +98,21 @@ public class MainActivity extends GameActivity {
|
|||
// Register receiver to finish activity from the BackgroundService.
|
||||
registerReceiver(mBroadcastReceiver, new IntentFilter(STOP_APP_ACTION));
|
||||
|
||||
// Register file pick result launcher.
|
||||
mFilePickResultLauncher = registerForActivityResult(
|
||||
// Register associated file opening result.
|
||||
mOpenFilePermissionsResult = registerForActivityResult(
|
||||
new ActivityResultContracts.StartActivityForResult(),
|
||||
result -> {
|
||||
if (Build.VERSION.SDK_INT >= 30) {
|
||||
if (Environment.isExternalStorageManager()) {
|
||||
onFile();
|
||||
}
|
||||
} else if (result.getResultCode() == RESULT_OK) {
|
||||
onFile();
|
||||
}
|
||||
}
|
||||
);
|
||||
// Register file pick result.
|
||||
mFilePickResult = registerForActivityResult(
|
||||
new ActivityResultContracts.StartActivityForResult(),
|
||||
result -> {
|
||||
int resultCode = result.getResultCode();
|
||||
|
@ -105,11 +125,11 @@ public class MainActivity extends GameActivity {
|
|||
File file = new File(getExternalCacheDir(), name);
|
||||
try (InputStream is = getContentResolver().openInputStream(uri);
|
||||
OutputStream os = new FileOutputStream(file)) {
|
||||
byte[] buffer = new byte[1024];
|
||||
int length;
|
||||
while ((length = is.read(buffer)) > 0) {
|
||||
os.write(buffer, 0, length);
|
||||
}
|
||||
byte[] buffer = new byte[1024];
|
||||
int length;
|
||||
while ((length = is.read(buffer)) > 0) {
|
||||
os.write(buffer, 0, length);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
@ -124,7 +144,7 @@ public class MainActivity extends GameActivity {
|
|||
// Listener for display insets (cutouts) to pass values into native code.
|
||||
View content = getWindow().getDecorView().findViewById(android.R.id.content);
|
||||
ViewCompat.setOnApplyWindowInsetsListener(content, (v, insets) -> {
|
||||
// Setup cutouts values.
|
||||
// Get display cutouts.
|
||||
DisplayCutoutCompat dc = insets.getDisplayCutout();
|
||||
int cutoutTop = 0;
|
||||
int cutoutRight = 0;
|
||||
|
@ -140,7 +160,7 @@ public class MainActivity extends GameActivity {
|
|||
// Get display insets.
|
||||
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
|
||||
|
||||
// Setup values to pass into native code.
|
||||
// Pass values into native code.
|
||||
int[] values = new int[]{0, 0, 0, 0};
|
||||
values[0] = Utils.pxToDp(Integer.max(cutoutTop, systemBars.top), this);
|
||||
values[1] = Utils.pxToDp(Integer.max(cutoutRight, systemBars.right), this);
|
||||
|
@ -166,8 +186,61 @@ public class MainActivity extends GameActivity {
|
|||
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
|
||||
public void onConfigurationChanged(Configuration newConfig) {
|
||||
super.onConfigurationChanged(newConfig);
|
||||
|
@ -232,17 +305,17 @@ public class MainActivity extends GameActivity {
|
|||
// Implemented into native code to handle key code BACK event.
|
||||
public native void onBack();
|
||||
|
||||
// Actions on app exit.
|
||||
private void onExit() {
|
||||
unregisterReceiver(mBroadcastReceiver);
|
||||
BackgroundService.stop(this);
|
||||
// Called from native code to exit app.
|
||||
public void exit() {
|
||||
finishAndRemoveTask();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
onExit();
|
||||
unregisterReceiver(mBroadcastReceiver);
|
||||
BackgroundService.stop(this);
|
||||
|
||||
// Kill process after 3 seconds if app was terminated from recent apps to prevent app hanging.
|
||||
// Kill process after 3 secs if app was terminated from recent apps to prevent app hang.
|
||||
new Thread(() -> {
|
||||
try {
|
||||
onTermination();
|
||||
|
@ -253,9 +326,7 @@ public class MainActivity extends GameActivity {
|
|||
}
|
||||
}).start();
|
||||
|
||||
// Destroy an app and kill process.
|
||||
super.onDestroy();
|
||||
Process.killProcess(Process.myPid());
|
||||
}
|
||||
|
||||
// Notify native code to stop activity (e.g. node) if app was terminated from recent apps.
|
||||
|
@ -298,18 +369,16 @@ public class MainActivity extends GameActivity {
|
|||
|
||||
// Called from native code to start camera.
|
||||
public void startCamera() {
|
||||
// Check permissions.
|
||||
String notificationsPermission = Manifest.permission.CAMERA;
|
||||
if (checkSelfPermission(notificationsPermission) != PackageManager.PERMISSION_GRANTED) {
|
||||
requestPermissions(new String[] { notificationsPermission }, CAMERA_PERMISSION_CODE);
|
||||
} else {
|
||||
// Start .
|
||||
if (mCameraProviderFuture == null) {
|
||||
mCameraProviderFuture = ProcessCameraProvider.getInstance(this);
|
||||
mCameraProviderFuture.addListener(() -> {
|
||||
try {
|
||||
mCameraProvider = mCameraProviderFuture.get();
|
||||
// Launch camera.
|
||||
// Start camera.
|
||||
openCamera();
|
||||
} catch (Exception e) {
|
||||
View content = findViewById(android.R.id.content);
|
||||
|
@ -381,14 +450,14 @@ public class MainActivity extends GameActivity {
|
|||
// Pass image from camera into native code.
|
||||
public native void onCameraImage(byte[] buff, int rotation);
|
||||
|
||||
// Called from native code to share image from provided path.
|
||||
public void shareImage(String path) {
|
||||
// Called from native code to share data from provided path.
|
||||
public void shareData(String path) {
|
||||
File file = new File(path);
|
||||
Uri uri = FileProvider.getUriForFile(this, "mw.gri.android.fileprovider", file);
|
||||
Intent intent = new Intent(Intent.ACTION_SEND);
|
||||
intent.putExtra(Intent.EXTRA_STREAM, uri);
|
||||
intent.setType("image/*");
|
||||
startActivity(Intent.createChooser(intent, "Share image"));
|
||||
intent.setType("*/*");
|
||||
startActivity(Intent.createChooser(intent, "Share data"));
|
||||
}
|
||||
|
||||
// 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.setType("*/*");
|
||||
try {
|
||||
mFilePickResultLauncher.launch(Intent.createChooser(intent, "Pick file"));
|
||||
mFilePickResult.launch(Intent.createChooser(intent, "Pick file"));
|
||||
} catch (android.content.ActivityNotFoundException ex) {
|
||||
onFilePick("");
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<paths>
|
||||
<external-cache-path name="images" path="images/" />
|
||||
<external-cache-path name="share" path="share/" />
|
||||
</paths>
|
|
@ -1,10 +1,6 @@
|
|||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
plugins {
|
||||
id 'com.android.application' version '8.1.1' apply false
|
||||
id 'com.android.library' version '8.1.1' apply false
|
||||
}
|
||||
|
||||
task clean(type: Delete) {
|
||||
delete rootProject.buildDir
|
||||
id 'com.android.application' version '8.6.1' apply false
|
||||
id 'com.android.library' version '8.6.1' apply false
|
||||
}
|
||||
|
||||
|
|
|
@ -19,5 +19,4 @@ android.useAndroidX=true
|
|||
# resources declared in the library itself and none from the library's dependencies,
|
||||
# thereby reducing the size of the R class for that library
|
||||
android.nonTransitiveRClass=true
|
||||
android.defaults.buildfeatures.buildconfig=true
|
||||
android.nonFinalResIds=false
|
|
@ -1,6 +1,6 @@
|
|||
#Mon May 02 15:39:12 BST 2022
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
|
||||
distributionPath=wrapper/dists
|
||||
zipStorePath=wrapper/dists
|
||||
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
|
||||
Icon=grim
|
||||
Type=Application
|
||||
Categories=Finance
|
||||
Categories=Finance
|
||||
MimeType=application/x-slatepack;text/plain;
|
|
@ -4,7 +4,7 @@ case $2 in
|
|||
x86_64|arm)
|
||||
;;
|
||||
*)
|
||||
echo "Usage: release_linux.sh [version] [platform]\n - platform: 'x86_64', 'arm'" >&2
|
||||
echo "Usage: release_linux.sh [platform] [version]\n - platform: 'x86_64', 'arm'" >&2
|
||||
exit 1
|
||||
esac
|
||||
|
||||
|
@ -17,11 +17,11 @@ cd ..
|
|||
[[ $2 == "x86_64" ]] && arch+=(x86_64-unknown-linux-gnu)
|
||||
[[ $2 == "arm" ]] && arch+=(aarch64-unknown-linux-gnu)
|
||||
|
||||
# Start release build with zig linker for cross-compilation
|
||||
rustup target add ${arch}
|
||||
cargo install cargo-zigbuild
|
||||
cargo zigbuild --release --target ${arch}
|
||||
|
||||
# Create AppImage with https://github.com/AppImage/appimagetool
|
||||
cp target/${arch}/release/grim linux/Grim.AppDir/AppRun
|
||||
rm target/${arch}/release/*.AppImage
|
||||
appimagetool linux/Grim.AppDir target/${arch}/release/grim-v$1-linux-$2.AppImage
|
||||
appimagetool linux/Grim.AppDir target/${arch}/release/grim-v$2-linux-$1.AppImage
|
|
@ -28,6 +28,7 @@ light: Hell
|
|||
choose_file: Datei auswählen
|
||||
crash_report: Absturzbericht
|
||||
crash_report_warning: Anwendung wurde beim letzten Mal unerwartet geschlossen, Sie können den Absturzbericht mit Entwicklern teilen.
|
||||
confirmation: Bestätigung
|
||||
wallets:
|
||||
await_conf_amount: Erwarte Bestätigung
|
||||
await_fin_amount: Warten auf die Fertigstellung
|
||||
|
@ -287,7 +288,6 @@ network_settings:
|
|||
modal:
|
||||
cancel: Abbrechen
|
||||
save: Speichern
|
||||
confirmation: Bestätigung
|
||||
add: Hinzufügen
|
||||
modal_exit:
|
||||
description: Sind Sie sicher, dass Sie die Anwendung beenden wollen?
|
||||
|
|
|
@ -28,6 +28,7 @@ light: Light
|
|||
choose_file: Choose file
|
||||
crash_report: Crash report
|
||||
crash_report_warning: Application closed unexpectedly last time, you can share crash report with developers.
|
||||
confirmation: Confirmation
|
||||
wallets:
|
||||
await_conf_amount: Awaiting confirmation
|
||||
await_fin_amount: Awaiting finalization
|
||||
|
@ -287,7 +288,6 @@ network_settings:
|
|||
modal:
|
||||
cancel: Cancel
|
||||
save: Save
|
||||
confirmation: Confirmation
|
||||
add: Add
|
||||
modal_exit:
|
||||
description: Are you sure you want to quit the application?
|
||||
|
|
|
@ -28,6 +28,7 @@ light: Clair
|
|||
choose_file: Choisir un fichier
|
||||
crash_report: Rapport d'échec
|
||||
crash_report_warning: L'application s'est fermée de manière inattendue la dernière fois, vous pouvez partager un rapport d'incident avec les développeurs.
|
||||
confirmation: Confirmation
|
||||
wallets:
|
||||
await_conf_amount: En attente de confirmation
|
||||
await_fin_amount: En attente de finalisation
|
||||
|
@ -287,7 +288,6 @@ network_settings:
|
|||
modal:
|
||||
cancel: Annuler
|
||||
save: Sauvegarder
|
||||
confirmation: Confirmation
|
||||
add: Ajouter
|
||||
modal_exit:
|
||||
description: "Êtes-vous sûr de vouloir quitter l'application ?"
|
||||
|
|
|
@ -28,6 +28,7 @@ light: Светлая
|
|||
choose_file: Выбрать файл
|
||||
crash_report: Отчёт о сбое
|
||||
crash_report_warning: В прошлый раз приложение неожиданно закрылось, вы можете поделиться отчетом о сбое с разработчиками.
|
||||
confirmation: Подтверждение
|
||||
wallets:
|
||||
await_conf_amount: Ожидает подтверждения
|
||||
await_fin_amount: Ожидает завершения
|
||||
|
@ -287,7 +288,6 @@ network_settings:
|
|||
modal:
|
||||
cancel: Отмена
|
||||
save: Сохранить
|
||||
confirmation: Подтверждение
|
||||
add: Добавить
|
||||
modal_exit:
|
||||
description: Вы уверены, что хотите выйти из приложения?
|
||||
|
|
|
@ -28,6 +28,7 @@ light: Isik
|
|||
choose_file: Dosya seçin
|
||||
crash_report: Ariza Raporu
|
||||
crash_report_warning: Uygulama beklenmedik bir sekilde kapandi son kez, kilitlenme raporunu gelistiricilerle paylasabilirsiniz.
|
||||
confirmation: Onay
|
||||
wallets:
|
||||
await_conf_amount: Onay bekleniyor
|
||||
await_fin_amount: Tamamlanma bekleniyor
|
||||
|
@ -287,7 +288,6 @@ network_settings:
|
|||
modal:
|
||||
cancel: Iptal
|
||||
save: Kaydet
|
||||
confirmation: Onay
|
||||
add: Ekle
|
||||
modal_exit:
|
||||
description: Uygulamadan cikmak için exit, emin misiniz?
|
||||
|
|
294
locales/zh-CN.yml
Normal file
294
locales/zh-CN.yml
Normal file
|
@ -0,0 +1,294 @@
|
|||
lang_name: 英语
|
||||
copy: 复制
|
||||
paste: 粘贴
|
||||
continue: 继续
|
||||
complete: 完成
|
||||
error: 错误
|
||||
retry: 重试
|
||||
close: 关闭
|
||||
change: 更改
|
||||
show: 显示
|
||||
delete: 删除
|
||||
clear: 清楚
|
||||
create: 创建
|
||||
id: 标识
|
||||
kernel: 核心
|
||||
settings: 设置
|
||||
language: 语言
|
||||
scan: 扫描
|
||||
qr_code: 二维码
|
||||
scan_qr: 扫描二维码
|
||||
repeat: 重复
|
||||
scan_result: 扫描结果
|
||||
back: 返回
|
||||
share: 分享
|
||||
theme: '主题:'
|
||||
dark: 深色
|
||||
light: 淡色
|
||||
choose_file: 选择文件
|
||||
crash_report: 崩溃报告
|
||||
crash_report_warning: 上次应用程序意外关闭,您可以报告开发人员崩溃事件.
|
||||
confirmation: 确认
|
||||
wallets:
|
||||
await_conf_amount: 等待确认中
|
||||
await_fin_amount: 等待确定中
|
||||
locked_amount: 锁定帐户
|
||||
txs_empty: '手动接收资金或通过传输接收资金 %{message} or %{transport} 更改钱包设置, 请按屏幕底部的按钮 %{settings} 按钮.'
|
||||
title: 钱包
|
||||
create_desc: 创建或种子单词导入已有钱包.
|
||||
add: 添加钱包
|
||||
name: '用户名:'
|
||||
pass: '密码:'
|
||||
pass_empty: 输入钱包的密码
|
||||
current_pass: '目前密码:'
|
||||
new_pass: '新密码:'
|
||||
min_tx_conf_count: '确认交易的最低数量:'
|
||||
recover: 恢复
|
||||
recovery_phrase: 助记词
|
||||
words_count: '字数:'
|
||||
enter_word: '输入单词 #%{number}:'
|
||||
not_valid_word: 输入的单词无效
|
||||
not_valid_phrase: 输入的助记词无效
|
||||
create_phrase_desc: 已安全地写下并保存助记词.
|
||||
restore_phrase_desc: 从已保存的助记词中输入.
|
||||
setup_conn_desc: 选择钱包连接到网络的方式.
|
||||
conn_method: 连接方式
|
||||
ext_conn: '外部连接:'
|
||||
add_node: 添加节点
|
||||
node_url: '节点网址:'
|
||||
node_secret: 'API 密钥 (可选):'
|
||||
invalid_url: 输入的网址无效
|
||||
open: 打开钱包
|
||||
wrong_pass: 输入的密码错误
|
||||
locked: 已锁定
|
||||
unlocked: 解锁
|
||||
enable_node: '通过选择屏幕底部的按钮 %{settings} 启用集成节点以使用钱包或更改连接设置.'
|
||||
node_loading: '集成节点同步后钱包会加载,你可选择屏幕底部的按钮 %{settings} 更改连接.'
|
||||
loading: 正在加载
|
||||
closing: 正在关闭
|
||||
checking: 检查中
|
||||
default_wallet: 默认钱包
|
||||
new_account_desc: '输入新帐户的名称:'
|
||||
wallet_loading: 加载钱包
|
||||
wallet_closing: 关闭钱包
|
||||
wallet_checking: 检查钱包
|
||||
tx_loading: 加载事务
|
||||
default_account: 默认账户
|
||||
accounts: 账户
|
||||
tx_sent: 已发送
|
||||
tx_received: 已接收
|
||||
tx_sending: 发送中
|
||||
tx_receiving: 接收中
|
||||
tx_confirming: 等待确认
|
||||
tx_canceled: 已取消
|
||||
tx_cancelling: 取消
|
||||
tx_finalizing: 完成
|
||||
tx_confirmed: 已确认
|
||||
txs: 所有交易
|
||||
tx: 交易
|
||||
messages: 消息
|
||||
transport: 传输
|
||||
input_slatepack_desc: '输入收到的 Slatepack 消息创建响应或完成的请求:'
|
||||
parse_slatepack_err: '读取消息时出错,请检查输入:'
|
||||
pay_balance_error: '账户余额不足以支付 %{amount} ツ 和网络费用.'
|
||||
parse_i1_slatepack_desc: '要支付 %{amount} ツ 请将此消息发送给接收者:'
|
||||
parse_i2_slatepack_desc: '完成交易以接收 %{amount} ツ:'
|
||||
parse_i3_slatepack_desc: '发布交易以完成 %{amount} ツ的接收 ツ:'
|
||||
parse_s1_slatepack_desc: '要接收 %{amount} ツ 请将此消息发送给发件人:'
|
||||
parse_s2_slatepack_desc: '完成交易以发送 %{amount} ツ:'
|
||||
parse_s3_slatepack_desc: '发布交易以完成 %{amount} ツ的发送:'
|
||||
resp_slatepack_err: '创建响应时出错,请检查输入数据或重试:'
|
||||
resp_exists_err: 此交易已存在.
|
||||
resp_canceled_err: 此交易已被取消.
|
||||
create_request_desc: '创建发送或接收资金的请求:'
|
||||
send_request_desc: '您已创建发送请求 %{amount} ツ. 将此消息发送给接收者:'
|
||||
send_slatepack_err: 创建发送资金请求时出错,请检查输入数据或重试.
|
||||
invoice_desc: '您已创建接收请求 %{amount} ツ. 将此消息发送给发送者:'
|
||||
invoice_slatepack_err: 发票开具时出错,请检查输入数据或重试.
|
||||
finalize_slatepack_err: '完结时出错,请检查输入数据或重试:'
|
||||
finalize: 完成
|
||||
use_dandelion: 使用蒲公英
|
||||
enter_amount_send: '你有 %{amount} ツ. 输入要发送的金额:'
|
||||
enter_amount_receive: '输入要接收的金额:'
|
||||
recovery: 恢复
|
||||
repair_wallet: 修复钱包
|
||||
repair_desc: 检查钱包,必要时修复和恢复丢失的输出. 此操作需要时间.
|
||||
repair_unavailable: 您需要与节点建立有效连接并完成钱包同步.
|
||||
delete: 删除钱包
|
||||
delete_conf: 您确定要删除钱包吗?
|
||||
delete_desc: 确保您已保存恢复助记语,以便日后使用资金。.
|
||||
wallet_loading_err: '同步钱包时出错,你可以通过选择屏幕底部的按钮 %{settings} 来重试或更改连接设置.'
|
||||
wallet: 钱包
|
||||
send: 发送
|
||||
receive: 接收
|
||||
settings: 钱包设置
|
||||
tx_send_cancel_conf: '您确定要取消 %{amount} ツ的发送吗?'
|
||||
tx_receive_cancel_conf: '您确定要取消 %{amount} ツ的接收吗?'
|
||||
rec_phrase_not_found: 找不到恢复助记词.
|
||||
restore_wallet_desc: 如果常规修复没有帮助,通过删除所有文件来恢复钱包.您将需要重新打开您的钱包.
|
||||
transport:
|
||||
desc: '使用传输同步接收或发送消息:'
|
||||
tor_network: Tor 网络
|
||||
connected: 已连接
|
||||
connecting: 正在连接
|
||||
disconnecting: 断开连接
|
||||
conn_error: 连接错误
|
||||
disconnected: 已断开连接
|
||||
receiver_address: '接收者的地址:'
|
||||
incorrect_addr_err: '输入的地址不正确:'
|
||||
tor_send_error: 通过 Tor 发送时出错,请确保接收方在线, 交易已取消.
|
||||
tor_autorun_desc: 是否在开钱包时启动 Tor 服务以同步接收交易.
|
||||
tor_sending: '通过 Tor 发送%{amount} ツ'
|
||||
tor_settings: Tor 设置
|
||||
bridges: 桥梁
|
||||
bridges_desc: 如果常规连接不正常,设置网桥,可以绕过 Tor 网络审查.
|
||||
bin_file: '二进制文件:'
|
||||
conn_line: '连接线:'
|
||||
bridges_disabled: 网桥已禁用
|
||||
bridge_name: '网桥%{b}'
|
||||
network:
|
||||
self: 网络
|
||||
type: '网络类型:'
|
||||
mainnet: 主网
|
||||
testnet: 测试网
|
||||
connections: 连接
|
||||
node: 集成节点
|
||||
metrics: 指标
|
||||
mining: 挖矿
|
||||
settings: 节点设置
|
||||
enable_node: 启用节点
|
||||
autorun: 自动运行
|
||||
disabled_server: '按屏幕左上角的按钮 %{dots}启用集成节点或添加其他连接方法.'
|
||||
no_ips: T您的系统上没有可用的 IP 地址,服务器无法启动,请检查您的网络连接.
|
||||
available: 可用
|
||||
not_available: 不可用
|
||||
availability_check: 检查是否可用
|
||||
android_warning: Android 用户注意 .要成功同步集成节点,您必须在手机的系统设置中允许访问通知并取消 Grim 应用程序的电池使用限制.这是在后台正确运行应用程序的必要操作.
|
||||
sync_status:
|
||||
node_restarting: 节点正在重新启动
|
||||
node_down: 节点已关闭
|
||||
initial: 节点正在启动
|
||||
no_sync: 节点正在运行
|
||||
awaiting_peers: 等待网络对点
|
||||
header_sync: 正下载标题
|
||||
header_sync_percent: '正在下载标题: %{percent}%'
|
||||
tx_hashset_pibd: 下载状态 (PIBD)
|
||||
tx_hashset_pibd_percent: '下载状态 (PIBD): %{percent}%'
|
||||
tx_hashset_download: 正在下载状态
|
||||
tx_hashset_download_percent: '下载状态: %{percent}%'
|
||||
tx_hashset_setup_history: '正在准备状态(历史记录): %{percent}%'
|
||||
tx_hashset_setup_position: '正在准备状态(位置): %{percent}%'
|
||||
tx_hashset_setup: 正在准备状态
|
||||
tx_hashset_range_proofs_validation: '验证状态(范围证明): %{percent}%'
|
||||
tx_hashset_kernels_validation: '正在验证状态(核心): %{percent}%'
|
||||
tx_hashset_save: 最终确定链状态
|
||||
body_sync: 下载区块
|
||||
body_sync_percent: '下载区块中: %{percent}%'
|
||||
shutdown: 节点正在关闭
|
||||
network_node:
|
||||
header: 标题
|
||||
block: 区块
|
||||
hash: 哈希值
|
||||
height: 高度
|
||||
difficulty: 难度
|
||||
time: 时间
|
||||
main_pool: 主池
|
||||
stem_pool: stem池
|
||||
data: 数据
|
||||
size: 大小 (GB)
|
||||
peers: 网络对点
|
||||
error_clean: 点数据已损坏,需要重新同步.
|
||||
resync: 重新同步
|
||||
error_p2p_api: '%{p2p_api} 服务器初始化时出错,请选择屏幕底部的按钮 %{p2p_api} 来检查 %{settings}设置.'
|
||||
error_config: '配置初始化时出错,请选择屏幕底部的按钮 %{settings} 检查设置.'
|
||||
error_unknown: '初始化时出错,请选择屏幕底部的按钮 %{settings} 来检查集成节点设置,或者重新同步.'
|
||||
network_metrics:
|
||||
loading: 指标在同步后将可用
|
||||
emission: 发射
|
||||
inflation: 通货膨胀
|
||||
supply: 供应
|
||||
block_time: Block time
|
||||
reward: 奖励
|
||||
difficulty_window: '难度窗口 %{size}'
|
||||
network_mining:
|
||||
loading: 同步后即可挖矿
|
||||
info: '挖矿服务器已启用,您可以通过选择屏幕底部的按钮 %{settings} 来更改其设置。连接设备后,数据会更新.'
|
||||
restart_server_required: 需要重启服务器才能应用更改.
|
||||
rewards_wallet: 奖励钱包
|
||||
server: 阶层服务器
|
||||
address: 地址
|
||||
miners: 矿工
|
||||
devices: 设备
|
||||
blocks_found: 找到的区块
|
||||
hashrate: '哈希率 (C%{bits})'
|
||||
connected: 已连接
|
||||
disconnected: 已断开连接
|
||||
network_settings:
|
||||
change_value: 更改值
|
||||
stratum_ip: '层 IP 地址:'
|
||||
stratum_port: '层端口:'
|
||||
port_unavailable: 指定的端口不可用
|
||||
restart_node_required: 需要重启节点才能应用更改.
|
||||
choose_wallet: 选择钱包
|
||||
stratum_wallet_warning: 必须打开钱包才能获得奖励.
|
||||
enable: 启用
|
||||
disable: 禁用
|
||||
restart: 重新启动
|
||||
server: 服务器
|
||||
api_ip: 'API IP 地址:'
|
||||
api_port: 'API 端口:'
|
||||
api_secret: '其它API 和 V2 所有者 API 令牌:'
|
||||
foreign_api_secret: '外部 API 令牌:'
|
||||
disabled: 已禁用
|
||||
enabled: 已启用
|
||||
ftl: '未来时间限制 (FTL):'
|
||||
ftl_description: 限制未来多长时间, 相对于节点的本地时间,以秒为单位, 新区块的时间戳可以被接受.
|
||||
not_valid_value: 输入的值无效
|
||||
full_validation: 完全验证
|
||||
full_validation_description: 在处理每个区块时是否运行全链验证(同步期间除外).
|
||||
archive_mode: 存档模式
|
||||
archive_mode_desc: 以全部存档模式运行全节点(同步需要更多的磁盘空间和时间).
|
||||
attempt_time: '尝试挖矿时间 (秒):'
|
||||
attempt_time_desc: 在停止并从池中重新收集交易之前尝试对特定标题进行挖矿的时间
|
||||
min_share_diff: '可接受的最低份额难度:'
|
||||
reset_settings_desc: 将节点设置重置为默认值
|
||||
reset_settings: 重置设置
|
||||
reset: 重置
|
||||
tx_pool: 交易池
|
||||
pool_fee: '接受到矿池的基本费用:'
|
||||
reorg_period: '重组缓存保留期(以分钟为单位):'
|
||||
max_tx_pool: '池中的最大交易数:'
|
||||
max_tx_stempool: 'stem池中的最大交易数:'
|
||||
max_tx_weight: '可以选择构建区块交易的最大总权重:'
|
||||
epoch_duration: '纪元持续时间(以秒为单位):'
|
||||
embargo_timer: '禁止计时器(以秒为单位):'
|
||||
aggregation_period: '聚合周期(以秒为单位):'
|
||||
stem_probability: 'stem助记词概率:'
|
||||
stem_txs: stem交易
|
||||
p2p_server: P2P 服务器
|
||||
p2p_port: 'P2P 端口:'
|
||||
add_seed: 添加 DNS 种子
|
||||
seed_address: 'DNS 种子地址:'
|
||||
add_peer: 添加网络对点
|
||||
peer_address: '网络对点地址:'
|
||||
peer_address_error: '以正确的格式输入 IP 地址或 DNS 名称(确保指定的主机可用),例如:192.168.0.1:1234 或 example.com:5678'
|
||||
default: 默认
|
||||
allow_list: 允许列表
|
||||
allow_list_desc: 仅连接到此列表中的网络对点.
|
||||
deny_list: 拒绝列表
|
||||
deny_list_desc: 切勿连接到此列表中的网络对点.
|
||||
favourites: 收藏夹
|
||||
favourites_desc: 要连接的首选网络对点列表.
|
||||
ban_window: '被封禁的网络对点应该保持被封禁多长时间(以秒为单位):'
|
||||
ban_window_desc: 禁止的决定是由节点 根据从网络对点收到的数据的正确性做出的.
|
||||
max_inbound_count: '入站网络对点连接的最大数量:'
|
||||
max_outbound_count: '最大出站网络对点连接数:'
|
||||
reset_peers_desc: 重置网络对点数据。仅当查找网络对点出现问题时,才请谨慎使用它.
|
||||
reset_peers: 重置网络对点
|
||||
modal:
|
||||
cancel: 取消
|
||||
save: 保存
|
||||
add: 添加
|
||||
modal_exit:
|
||||
description: 您确定要退出应用程序吗?
|
||||
exit: 退出
|
|
@ -1,49 +1,65 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Grim</string>
|
||||
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>grim</string>
|
||||
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>AppIcon</string>
|
||||
|
||||
<key>CFBundleIconName</key>
|
||||
<string>AppIcon</string>
|
||||
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>mw.gri.macos</string>
|
||||
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
|
||||
<key>CFBundleName</key>
|
||||
<string>Grim</string>
|
||||
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>0.1.0</string>
|
||||
|
||||
<key>CFBundleSupportedPlatforms</key>
|
||||
<array>
|
||||
<string>MacOSX</string>
|
||||
</array>
|
||||
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
|
||||
<key>LSApplicationCategoryType</key>
|
||||
<string>public.app-category.finance</string>
|
||||
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>2024</string>
|
||||
</dict>
|
||||
</plist>
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Grim</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>grim</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>AppIcon</string>
|
||||
<key>CFBundleIconName</key>
|
||||
<string>AppIcon</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>mw.gri.macos</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>Grim</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>0.2.3</string>
|
||||
<key>CFBundleSupportedPlatforms</key>
|
||||
<array>
|
||||
<string>MacOSX</string>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>Grim needs an access to your camera to scan QR code.</string>
|
||||
<key>CFBundleDocumentTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>Apple SimpleText document</string>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Viewer</string>
|
||||
<key>LSItemContentTypes</key>
|
||||
<array>
|
||||
<string>com.apple.traditional-mac-plain-text</string>
|
||||
</array>
|
||||
<key>NSDocumentClass</key>
|
||||
<string>Document</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>Unknown document</string>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Viewer</string>
|
||||
<key>LSItemContentTypes</key>
|
||||
<array>
|
||||
<string>public.data</string>
|
||||
</array>
|
||||
<key>NSDocumentClass</key>
|
||||
<string>Document</string>
|
||||
</dict>
|
||||
</array>
|
||||
<key>LSApplicationCategoryType</key>
|
||||
<string>public.app-category.finance</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>2024</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
2
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
|
||||
set -e
|
||||
|
||||
case $2 in
|
||||
case $1 in
|
||||
x86_64|arm|universal)
|
||||
;;
|
||||
*)
|
||||
echo "Usage: release_macos.sh [version] [platform]\n - platform: 'x86_64', 'arm', 'universal'" >&2
|
||||
echo "Usage: release_macos.sh [platform] [version]\n - platform: 'x86_64', 'arm', 'universal'" >&2
|
||||
exit 1
|
||||
esac
|
||||
|
||||
if [[ ! -v SDKROOT ]]; then
|
||||
if [[ "$OSTYPE" != "darwin"* ]]; then
|
||||
if [ -z ${SDKROOT+x} ]; then
|
||||
echo "MacOS SDKROOT is not set"
|
||||
exit 1
|
||||
elif [[ -z "SDKROOT" ]]; then
|
||||
echo "MacOS SDKROOT is set to the empty string"
|
||||
exit 1
|
||||
else
|
||||
else
|
||||
echo "Use MacOS SDK: ${SDKROOT}"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Setup build directory
|
||||
|
@ -25,31 +24,25 @@ cd ${BASEDIR}
|
|||
cd ..
|
||||
|
||||
# Setup platform
|
||||
[[ $1 == "x86_64" ]] && arch+=(x86_64-apple-darwin)
|
||||
[[ $1 == "arm" ]] && arch+=(aarch64-apple-darwin)
|
||||
|
||||
rustup target add x86_64-apple-darwin
|
||||
rustup target add aarch64-apple-darwin
|
||||
|
||||
rm -rf target/x86_64-apple-darwin
|
||||
rm -rf target/aarch64-apple-darwin
|
||||
|
||||
[[ $2 == "x86_64" ]] && arch+=(x86_64-apple-darwin)
|
||||
[[ $2 == "arm" ]] && arch+=(aarch64-apple-darwin)
|
||||
[[ $2 == "universal" ]] && arch+=(universal2-apple-darwin)
|
||||
|
||||
# Start release build with zig linker for cross-compilation
|
||||
# zig 0.12+ required
|
||||
[[ $1 == "universal" ]]; arch+=(universal2-apple-darwin)
|
||||
cargo install cargo-zigbuild
|
||||
cargo zigbuild --release --target ${arch}
|
||||
rm -rf .intentionally-empty-file.o
|
||||
mkdir macos/Grim.app/Contents/MacOS
|
||||
|
||||
rm -f .intentionally-empty-file.o
|
||||
|
||||
yes | cp -rf target/${arch}/release/grim macos/Grim.app/Contents/MacOS
|
||||
|
||||
### Sign .app resources on change:
|
||||
# Sign .app resources on change:
|
||||
#rcodesign generate-self-signed-certificate
|
||||
#rcodesign sign --pem-file cert.pem macos/Grim.app
|
||||
|
||||
# Create release package
|
||||
FILE_NAME=grim-v$1-macos-$2.zip
|
||||
rm -rf target/${arch}/release/${FILE_NAME}
|
||||
FILE_NAME=grim-v$2-macos-$1.zip
|
||||
cd macos
|
||||
zip -r ${FILE_NAME} Grim.app
|
||||
mv ${FILE_NAME} ../target/${arch}/release
|
||||
|
|
|
@ -1,81 +1,120 @@
|
|||
#!/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
|
||||
debug|release)
|
||||
build|release)
|
||||
;;
|
||||
*)
|
||||
printf "$usage"
|
||||
exit 1
|
||||
esac
|
||||
|
||||
case $2 in
|
||||
v7|v8)
|
||||
;;
|
||||
*)
|
||||
printf "$usage"
|
||||
exit 1
|
||||
esac
|
||||
|
||||
# Setup build directory
|
||||
BASEDIR=$(cd $(dirname $0) && pwd)
|
||||
cd ${BASEDIR}
|
||||
cd ..
|
||||
|
||||
# Setup release argument
|
||||
type=$1
|
||||
[[ ${type} == "release" ]] && release_param="--profile release-apk"
|
||||
|
||||
# Setup platform argument
|
||||
[[ $2 == "v7" ]] && arch+=(armeabi-v7a)
|
||||
[[ $2 == "v8" ]] && arch+=(arm64-v8a)
|
||||
|
||||
# Setup platform path
|
||||
[[ $2 == "v7" ]] && platform+=(armv7-linux-androideabi)
|
||||
[[ $2 == "v8" ]] && platform+=(aarch64-linux-android)
|
||||
|
||||
# Install platform
|
||||
[[ $2 == "v7" ]] && rustup target install armv7-linux-androideabi
|
||||
[[ $2 == "v8" ]] && rustup target install aarch64-linux-android
|
||||
|
||||
# Build native code
|
||||
cargo install cargo-ndk
|
||||
|
||||
sed -i -e 's/"rlib"/"cdylib","rlib"/g' Cargo.toml
|
||||
|
||||
# temp fix for https://stackoverflow.com/questions/57193895/error-use-of-undeclared-identifier-pthread-mutex-robust-cargo-build-liblmdb-s
|
||||
success=0
|
||||
export CPPFLAGS="-DMDB_USE_ROBUST=0" && export CFLAGS="-DMDB_USE_ROBUST=0"
|
||||
cargo ndk -t ${arch} build ${release_param}
|
||||
unset CPPFLAGS && unset CFLAGS
|
||||
cargo ndk -t ${arch} -o android/app/src/main/jniLibs build ${release_param}
|
||||
if [ $? -eq 0 ]
|
||||
then
|
||||
success=1
|
||||
if [[ $1 == "build" ]]; then
|
||||
case $2 in
|
||||
v7|v8|x86)
|
||||
;;
|
||||
*)
|
||||
printf "$usage"
|
||||
exit 1
|
||||
esac
|
||||
fi
|
||||
|
||||
sed -i -e 's/"cdylib","rlib"/"rlib"/g' Cargo.toml
|
||||
# Setup build directory
|
||||
BASEDIR=$(cd "$(dirname "$0")" && pwd)
|
||||
cd "${BASEDIR}" || exit 1
|
||||
cd ..
|
||||
|
||||
# Build Android application and launch at all connected devices
|
||||
if [ $success -eq 1 ]
|
||||
then
|
||||
cd android
|
||||
# Install platforms and tools
|
||||
rustup target add armv7-linux-androideabi
|
||||
rustup target add aarch64-linux-android
|
||||
rustup target add x86_64-linux-android
|
||||
cargo install cargo-ndk
|
||||
|
||||
# Setup gradle argument
|
||||
[[ $1 == "release" ]] && gradle_param+=(assembleRelease)
|
||||
[[ $1 == "debug" ]] && gradle_param+=(build)
|
||||
success=1
|
||||
|
||||
### Build native code
|
||||
function build_lib() {
|
||||
[[ $1 == "v7" ]] && arch=armeabi-v7a
|
||||
[[ $1 == "v8" ]] && arch=arm64-v8a
|
||||
[[ $1 == "x86" ]] && arch=x86_64
|
||||
|
||||
sed -i -e 's/"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 ${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
|
||||
[[ $1 == "release" ]] && apk_path+=(app/build/outputs/apk/release/app-release.apk)
|
||||
[[ $1 == "debug" ]] && apk_path+=(app/build/outputs/apk/debug/app-debug.apk)
|
||||
if [[ $1 == "" ]]; then
|
||||
# Launch application at all connected devices.
|
||||
for SERIAL in $(adb devices | grep -v List | cut -f 1);
|
||||
do
|
||||
adb -s "$SERIAL" install ${apk_path}
|
||||
sleep 1s
|
||||
adb -s "$SERIAL" shell am start -n mw.gri.android/.MainActivity;
|
||||
done
|
||||
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);
|
||||
do
|
||||
adb -s $SERIAL install ${apk_path}
|
||||
sleep 1s
|
||||
adb -s $SERIAL shell am start -n mw.gri.android/.MainActivity;
|
||||
done
|
||||
# Calculate checksum
|
||||
checksum=grim-${version}-android-$1-sha256sum.txt
|
||||
[[ $1 == "arm" ]] && checksum=grim-${version}-android-sha256sum.txt
|
||||
rm -f "${checksum}"
|
||||
sha256sum "${name}" > "${checksum}"
|
||||
fi
|
||||
|
||||
cd ..
|
||||
}
|
||||
|
||||
rm -rf android/app/src/main/jniLibs/*
|
||||
|
||||
if [[ $1 == "build" ]]; then
|
||||
build_lib "$2"
|
||||
[ $success -eq 1 ] && build_apk
|
||||
else
|
||||
rm -rf target/release-apk
|
||||
rm -rf target/aarch64-linux-android
|
||||
rm -rf target/x86_64-linux-android
|
||||
rm -rf target/armv7-linux-androideabi
|
||||
|
||||
build_lib "v7"
|
||||
[ $success -eq 1 ] && build_lib "v8"
|
||||
[ $success -eq 1 ] && build_apk "arm" "$2"
|
||||
rm -rf android/app/src/main/jniLibs/*
|
||||
[ $success -eq 1 ] && build_lib "x86"
|
||||
[ $success -eq 1 ] && build_apk "x86_64" "$2"
|
||||
fi
|
|
@ -1,25 +1,27 @@
|
|||
#!/bin/bash
|
||||
|
||||
case $1 in
|
||||
debug|release)
|
||||
debug|build)
|
||||
;;
|
||||
*)
|
||||
echo "Usage: build_run.sh [type] where is type is 'debug' or 'release'" >&2
|
||||
echo "Usage: build_run.sh [type] where is type is 'debug' or 'build'" >&2
|
||||
exit 1
|
||||
esac
|
||||
|
||||
# Setup build directory
|
||||
BASEDIR=$(cd $(dirname $0) && pwd)
|
||||
cd ${BASEDIR}
|
||||
BASEDIR=$(cd "$(dirname $0)" && pwd)
|
||||
cd "${BASEDIR}" || return
|
||||
cd ..
|
||||
|
||||
# Build application
|
||||
type=$1
|
||||
[[ ${type} == "release" ]] && release_param+=(--release)
|
||||
cargo build ${release_param[@]}
|
||||
[[ ${type} == "build" ]] && release_param+=(--release)
|
||||
cargo --config profile.release.incremental=true build "${release_param[@]}"
|
||||
|
||||
# Start application
|
||||
if [ $? -eq 0 ]
|
||||
then
|
||||
./target/${type}/grim
|
||||
fi
|
||||
path=${type}
|
||||
[[ ${type} == "build" ]] && path="release"
|
||||
./target/"${path}"/grim
|
||||
fi
|
||||
|
|
99
scripts/version.sh
Executable file
99
scripts/version.sh
Executable file
|
@ -0,0 +1,99 @@
|
|||
#!/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 version for Windows installer.
|
||||
sed -i '' -e 's/" Version="[^\"]*"/" Version="'"$VERSION_NEXT"'"/g' wix/main.wxs
|
||||
sed -i '' -e 's/<Package Id="[^\"]*"/<Package Id="'"$(uuidgen)"'"/g' wix/main.wxs
|
||||
|
||||
# Update Android version in build.gradle
|
||||
sed -i'.bak' -e 's/versionName [0-9a-zA-Z -_]*/versionName "'"$VERSION_NEXT"'"/' android/app/build.gradle
|
||||
rm -f android/app/build.gradle.bak
|
||||
|
||||
# Update version in Cargo.toml
|
||||
sed -i'.bak' -e "s/^version = .*/version = \"$VERSION_NEXT\"/" Cargo.toml
|
||||
rm -f Cargo.toml.bak
|
||||
|
||||
# Update Cargo.lock as this changes when
|
||||
# updating the version in your manifest
|
||||
cargo update -p grim
|
||||
|
||||
# Commit the changes
|
||||
git add .
|
||||
git commit -m "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
|
314
src/gui/app.rs
Normal file → Executable file
314
src/gui/app.rs
Normal file → Executable file
|
@ -14,15 +14,15 @@
|
|||
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
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::os::OperatingSystem;
|
||||
|
||||
use crate::AppConfig;
|
||||
use crate::gui::Colors;
|
||||
use crate::gui::icons::{ARROWS_IN, ARROWS_OUT, CARET_DOWN, MOON, SUN, X};
|
||||
use crate::gui::platform::PlatformCallbacks;
|
||||
use crate::gui::views::{Content, TitlePanel, View};
|
||||
use crate::gui::views::{Content, Modal, TitlePanel, View};
|
||||
use crate::wallet::ExternalConnection;
|
||||
|
||||
lazy_static! {
|
||||
/// State to check if platform Back button was pressed.
|
||||
|
@ -31,27 +31,49 @@ lazy_static! {
|
|||
|
||||
/// Implements ui entry point and contains platform-specific callbacks.
|
||||
pub struct App<Platform> {
|
||||
/// Platform specific callbacks handler.
|
||||
pub(crate) platform: Platform,
|
||||
|
||||
/// Main ui content.
|
||||
/// Handles platform-specific functionality.
|
||||
pub platform: Platform,
|
||||
/// Main content.
|
||||
content: Content,
|
||||
|
||||
/// 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> {
|
||||
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.
|
||||
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.
|
||||
let back_pressed = BACK_BUTTON_PRESSED.load(Ordering::Relaxed);
|
||||
if ctx.input_mut(|i| i.consume_key(Modifiers::NONE, egui::Key::Escape)) || back_pressed {
|
||||
self.content.on_back();
|
||||
if back_pressed || ctx.input_mut(|i| i.consume_key(Modifiers::NONE, egui::Key::Escape)) {
|
||||
self.content.on_back(&self.platform);
|
||||
if back_pressed {
|
||||
BACK_BUTTON_PRESSED.store(false, Ordering::Relaxed);
|
||||
}
|
||||
|
@ -59,16 +81,15 @@ impl<Platform: PlatformCallbacks> App<Platform> {
|
|||
ctx.request_repaint();
|
||||
}
|
||||
|
||||
// Handle Close event (on desktop).
|
||||
if ctx.input(|i| i.viewport().close_requested()) {
|
||||
// Handle Close event on desktop.
|
||||
if View::is_desktop() && ctx.input(|i| i.viewport().close_requested()) {
|
||||
if !self.content.exit_allowed {
|
||||
ctx.send_viewport_cmd(ViewportCommand::CancelClose);
|
||||
Content::show_exit_modal();
|
||||
} else {
|
||||
let (w, h) = View::window_size(ctx);
|
||||
AppConfig::save_window_size(w, h);
|
||||
ctx.input(|i| {
|
||||
if let Some(rect) = i.viewport().inner_rect {
|
||||
AppConfig::save_window_size(rect.width(), rect.height());
|
||||
}
|
||||
if let Some(rect) = i.viewport().outer_rect {
|
||||
AppConfig::save_window_pos(rect.left(), rect.top());
|
||||
}
|
||||
|
@ -76,95 +97,92 @@ impl<Platform: PlatformCallbacks> App<Platform> {
|
|||
}
|
||||
}
|
||||
|
||||
// Show main content with custom frame on desktop.
|
||||
// Show main content.
|
||||
egui::CentralPanel::default()
|
||||
.frame(egui::Frame {
|
||||
..Default::default()
|
||||
})
|
||||
.show(ctx, |ui| {
|
||||
let is_mac_os = OperatingSystem::from_target_os() == OperatingSystem::Mac;
|
||||
if View::is_desktop() && !is_mac_os {
|
||||
self.desktop_window_ui(ui);
|
||||
} else {
|
||||
if is_mac_os {
|
||||
self.window_title_ui(ui);
|
||||
ui.add_space(-1.0);
|
||||
if View::is_desktop() {
|
||||
let is_fullscreen = ui.ctx().input(|i| {
|
||||
i.viewport().fullscreen.unwrap_or(false)
|
||||
});
|
||||
let os = egui::os::OperatingSystem::from_target_os();
|
||||
match os {
|
||||
egui::os::OperatingSystem::Mac => {
|
||||
self.window_title_ui(ui, is_fullscreen);
|
||||
ui.add_space(-1.0);
|
||||
Self::title_panel_bg(ui, true);
|
||||
self.content.ui(ui, &self.platform);
|
||||
}
|
||||
egui::os::OperatingSystem::Windows => {
|
||||
Self::title_panel_bg(ui, false);
|
||||
self.content.ui(ui, &self.platform);
|
||||
}
|
||||
_ => {
|
||||
self.custom_frame_ui(ui, is_fullscreen);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Self::title_panel_bg(ui, false);
|
||||
self.content.ui(ui, &self.platform);
|
||||
}
|
||||
|
||||
// Provide incoming data to wallets.
|
||||
if let Some(data) = crate::consume_incoming_data() {
|
||||
if !data.is_empty() {
|
||||
self.content.wallets.on_data(ui, Some(data), &self.platform);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Check if desktop window was focused after requested attention.
|
||||
if self.platform.user_attention_required() &&
|
||||
ctx.input(|i| i.viewport().focused.unwrap_or(true)) {
|
||||
self.platform.clear_user_attention();
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw custom resizeable window content.
|
||||
fn desktop_window_ui(&mut self, ui: &mut egui::Ui) {
|
||||
let is_fullscreen = ui.ctx().input(|i| {
|
||||
i.viewport().fullscreen.unwrap_or(false)
|
||||
});
|
||||
|
||||
let title_stroke_rect = {
|
||||
let mut rect = ui.max_rect();
|
||||
/// Draw custom desktop window frame content.
|
||||
fn custom_frame_ui(&mut self, ui: &mut egui::Ui, is_fullscreen: bool) {
|
||||
let content_bg_rect = {
|
||||
let mut r = ui.max_rect();
|
||||
if !is_fullscreen {
|
||||
rect = rect.shrink(Content::WINDOW_FRAME_MARGIN);
|
||||
r = r.shrink(Content::WINDOW_FRAME_MARGIN);
|
||||
}
|
||||
rect.max.y = if !is_fullscreen {
|
||||
Content::WINDOW_FRAME_MARGIN
|
||||
} else {
|
||||
0.0
|
||||
} + Content::WINDOW_TITLE_HEIGHT + TitlePanel::DEFAULT_HEIGHT + 0.5;
|
||||
rect
|
||||
r.min.y += Content::WINDOW_TITLE_HEIGHT + TitlePanel::HEIGHT;
|
||||
r
|
||||
};
|
||||
let title_stroke = RectShape {
|
||||
rect: title_stroke_rect,
|
||||
rounding: Rounding {
|
||||
nw: 8.0,
|
||||
ne: 8.0,
|
||||
sw: 0.0,
|
||||
se: 0.0,
|
||||
},
|
||||
fill: Colors::yellow(),
|
||||
stroke: Stroke {
|
||||
width: 1.0,
|
||||
color: egui::Color32::from_gray(200)
|
||||
},
|
||||
blur_width: 0.0,
|
||||
fill_texture_id: Default::default(),
|
||||
uv: Rect::ZERO
|
||||
};
|
||||
// Draw title stroke.
|
||||
ui.painter().add(title_stroke);
|
||||
let content_bg = RectShape::new(content_bg_rect,
|
||||
Rounding::ZERO,
|
||||
Colors::fill_lite(),
|
||||
View::default_stroke());
|
||||
// Draw content background.
|
||||
ui.painter().add(content_bg);
|
||||
|
||||
let 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();
|
||||
if !is_fullscreen {
|
||||
content_rect = content_rect.shrink(Content::WINDOW_FRAME_MARGIN);
|
||||
}
|
||||
ui.allocate_ui_at_rect(content_rect, |ui| {
|
||||
self.window_title_ui(ui);
|
||||
self.window_content(ui);
|
||||
// Draw window content.
|
||||
ui.allocate_new_ui(UiBuilder::new().max_rect(content_rect), |ui| {
|
||||
// Draw window title.
|
||||
self.window_title_ui(ui, is_fullscreen);
|
||||
ui.add_space(-1.0);
|
||||
|
||||
// Draw title panel background.
|
||||
Self::title_panel_bg(ui, true);
|
||||
|
||||
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.
|
||||
|
@ -180,57 +198,53 @@ impl<Platform: PlatformCallbacks> App<Platform> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Draw window content for desktop.
|
||||
fn window_content(&mut self, ui: &mut egui::Ui) {
|
||||
let content_rect = {
|
||||
/// Draw title panel background.
|
||||
fn title_panel_bg(ui: &mut egui::Ui, window_title: bool) {
|
||||
let title_rect = {
|
||||
let mut rect = ui.max_rect();
|
||||
rect.min.y += Content::WINDOW_TITLE_HEIGHT;
|
||||
if window_title {
|
||||
rect.min.y += Content::WINDOW_TITLE_HEIGHT - 0.5;
|
||||
}
|
||||
rect.max.y = rect.min.y + View::get_top_inset() + TitlePanel::HEIGHT;
|
||||
rect
|
||||
};
|
||||
// Draw main content.
|
||||
let mut content_ui = ui.child_ui(content_rect, *ui.layout(), None);
|
||||
self.content.ui(&mut content_ui, &self.platform);
|
||||
let title_bg = RectShape::filled(title_rect, Rounding::ZERO, Colors::yellow());
|
||||
ui.painter().add(title_bg);
|
||||
}
|
||||
|
||||
/// Draw custom window title content.
|
||||
fn window_title_ui(&self, ui: &mut egui::Ui) {
|
||||
let content_rect = ui.max_rect();
|
||||
|
||||
fn window_title_ui(&self, ui: &mut egui::Ui, is_fullscreen: bool) {
|
||||
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
|
||||
};
|
||||
|
||||
let is_fullscreen = ui.ctx().input(|i| {
|
||||
i.viewport().fullscreen.unwrap_or(false)
|
||||
});
|
||||
|
||||
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 title_bg_rect = {
|
||||
let mut r = title_rect.clone();
|
||||
r.max.y += TitlePanel::HEIGHT - 1.0;
|
||||
r
|
||||
};
|
||||
let is_mac = egui::os::OperatingSystem::from_target_os() == egui::os::OperatingSystem::Mac;
|
||||
let window_title_bg = RectShape::new(title_bg_rect, if is_fullscreen || is_mac {
|
||||
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.
|
||||
ui.painter().add(window_title_bg);
|
||||
|
||||
let painter = ui.painter();
|
||||
|
||||
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 {
|
||||
rect.min.y += Content::WINDOW_FRAME_MARGIN;
|
||||
}
|
||||
|
@ -239,25 +253,29 @@ impl<Platform: PlatformCallbacks> App<Platform> {
|
|||
let title_resp = ui.interact(
|
||||
interact_rect,
|
||||
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.
|
||||
let dual_wallets_panel =
|
||||
ui.available_width() >= (Content::SIDE_PANEL_WIDTH * 3.0) + View::get_right_inset();
|
||||
let wallet_panel_opened = self.content.wallets.wallet_panel_opened();
|
||||
let hide_app_name = if dual_wallets_panel {
|
||||
!wallet_panel_opened || (AppConfig::show_wallets_at_dual_panel() &&
|
||||
self.content.wallets.showing_wallet() && !self.content.wallets.creating_wallet())
|
||||
} else if Content::is_dual_panel_mode(ui) {
|
||||
!wallet_panel_opened
|
||||
let dual_wallets_panel = ui.available_width() >= (Content::SIDE_PANEL_WIDTH * 3.0) +
|
||||
View::get_right_inset() + View::get_left_inset();
|
||||
let wallet_panel_opened = self.content.wallets.showing_wallet();
|
||||
let show_app_name = if dual_wallets_panel {
|
||||
wallet_panel_opened && !AppConfig::show_wallets_at_dual_panel()
|
||||
} else if Content::is_dual_panel_mode(ui.ctx()) {
|
||||
wallet_panel_opened
|
||||
} else {
|
||||
!Content::is_network_panel_open() && !wallet_panel_opened
|
||||
Content::is_network_panel_open() || wallet_panel_opened
|
||||
};
|
||||
let title_text = if hide_app_name {
|
||||
"ツ".to_string()
|
||||
} else {
|
||||
let creating_wallet = self.content.wallets.creating_wallet();
|
||||
let title_text = if creating_wallet || show_app_name {
|
||||
format!("Grim {}", crate::VERSION)
|
||||
} else {
|
||||
"ツ".to_string()
|
||||
};
|
||||
painter.text(
|
||||
title_rect.center(),
|
||||
|
@ -267,20 +285,13 @@ impl<Platform: PlatformCallbacks> App<Platform> {
|
|||
Colors::title(true),
|
||||
);
|
||||
|
||||
// Interact with the window title (drag to move window):
|
||||
if !is_fullscreen && title_resp.double_clicked() {
|
||||
ui.ctx().send_viewport_cmd(ViewportCommand::Fullscreen(!is_fullscreen));
|
||||
}
|
||||
|
||||
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.allocate_new_ui(UiBuilder::new().max_rect(title_rect), |ui| {
|
||||
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
|
||||
// Draw button to close window.
|
||||
View::title_button_small(ui, X, |_| {
|
||||
Content::show_exit_modal();
|
||||
if Modal::opened().is_none() || Modal::opened_closeable() {
|
||||
Content::show_exit_modal();
|
||||
}
|
||||
});
|
||||
|
||||
// Draw fullscreen button.
|
||||
|
@ -392,16 +403,13 @@ impl<Platform: PlatformCallbacks> eframe::App for App<Platform> {
|
|||
}
|
||||
|
||||
fn clear_color(&self, _visuals: &egui::Visuals) -> [f32; 4] {
|
||||
if View::is_desktop() {
|
||||
let is_mac_os = OperatingSystem::from_target_os() == OperatingSystem::Mac;
|
||||
if is_mac_os {
|
||||
Colors::fill().to_normalized_gamma_f32()
|
||||
} else {
|
||||
egui::Rgba::TRANSPARENT.to_array()
|
||||
}
|
||||
} else {
|
||||
Colors::fill().to_normalized_gamma_f32()
|
||||
let os = egui::os::OperatingSystem::from_target_os();
|
||||
let is_win = os == egui::os::OperatingSystem::Windows;
|
||||
let is_mac = os == egui::os::OperatingSystem::Mac;
|
||||
if !View::is_desktop() || is_win || is_mac {
|
||||
return Colors::fill_lite().to_normalized_gamma_f32();
|
||||
}
|
||||
Colors::TRANSPARENT.to_normalized_gamma_f32()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -31,10 +31,14 @@ const YELLOW: Color32 = Color32::from_rgb(254, 241, 2);
|
|||
const YELLOW_DARK: Color32 = Color32::from_rgb(239, 229, 3);
|
||||
|
||||
const GREEN: Color32 = Color32::from_rgb(0, 0x64, 0);
|
||||
const GREEN_DARK: Color32 = Color32::from_rgb(0, (0x64 as f32 * 1.3 + 0.5) as u8, 0);
|
||||
|
||||
const RED: Color32 = Color32::from_rgb(0x8B, 0, 0);
|
||||
const RED_DARK: Color32 = Color32::from_rgb((0x8B as f32 * 1.3 + 0.5) as u8, 0, 0);
|
||||
|
||||
const BLUE: Color32 = Color32::from_rgb(0, 0x66, 0xE4);
|
||||
const BLUE_DARK: Color32 =
|
||||
Color32::from_rgb(0, (0x66 as f32 * 1.3 + 0.5) as u8, (0xE4 as f32 * 1.3 + 0.5) as u8);
|
||||
|
||||
const FILL: Color32 = Color32::from_gray(244);
|
||||
const FILL_DARK: Color32 = Color32::from_gray(24);
|
||||
|
@ -42,6 +46,9 @@ const FILL_DARK: Color32 = Color32::from_gray(24);
|
|||
const FILL_DEEP: Color32 = Color32::from_gray(238);
|
||||
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_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_DARK: Color32 = Color32::from_gray(205);
|
||||
|
||||
const BUTTON: Color32 = Color32::from_gray(249);
|
||||
const BUTTON_DARK: Color32 = Color32::from_gray(16);
|
||||
|
||||
const GRAY: Color32 = Color32::from_gray(120);
|
||||
const GRAY_DARK: Color32 = Color32::from_gray(145);
|
||||
|
||||
const STROKE: Color32 = Color32::from_gray(200);
|
||||
const STROKE_DARK: Color32 = Color32::from_gray(50);
|
||||
|
||||
const INACTIVE_TEXT: Color32 = Color32::from_gray(150);
|
||||
|
@ -82,6 +85,7 @@ fn use_dark() -> bool {
|
|||
|
||||
impl Colors {
|
||||
pub const TRANSPARENT: Color32 = Color32::from_rgba_premultiplied(0, 0, 0, 0);
|
||||
pub const STROKE: Color32 = Color32::from_gray(200);
|
||||
|
||||
pub fn white_or_black(black_in_white: bool) -> Color32 {
|
||||
if use_dark() {
|
||||
|
@ -125,7 +129,7 @@ impl Colors {
|
|||
|
||||
pub fn green() -> Color32 {
|
||||
if use_dark() {
|
||||
GREEN.gamma_multiply(1.3)
|
||||
GREEN_DARK
|
||||
} else {
|
||||
GREEN
|
||||
}
|
||||
|
@ -133,7 +137,7 @@ impl Colors {
|
|||
|
||||
pub fn red() -> Color32 {
|
||||
if use_dark() {
|
||||
RED.gamma_multiply(1.3)
|
||||
RED_DARK
|
||||
} else {
|
||||
RED
|
||||
}
|
||||
|
@ -141,7 +145,7 @@ impl Colors {
|
|||
|
||||
pub fn blue() -> Color32 {
|
||||
if use_dark() {
|
||||
BLUE.gamma_multiply(1.3)
|
||||
BLUE_DARK
|
||||
} else {
|
||||
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 {
|
||||
if use_dark() {
|
||||
CHECKBOX_DARK
|
||||
|
@ -195,14 +207,6 @@ impl Colors {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn button() -> Color32 {
|
||||
if use_dark() {
|
||||
BUTTON_DARK
|
||||
} else {
|
||||
BUTTON
|
||||
}
|
||||
}
|
||||
|
||||
pub fn gray() -> Color32 {
|
||||
if use_dark() {
|
||||
GRAY_DARK
|
||||
|
@ -215,7 +219,7 @@ impl Colors {
|
|||
if use_dark() {
|
||||
STROKE_DARK
|
||||
} else {
|
||||
STROKE
|
||||
Self::STROKE
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -30,7 +30,11 @@ use crate::gui::platform::PlatformCallbacks;
|
|||
/// Android platform implementation.
|
||||
#[derive(Clone)]
|
||||
pub struct Android {
|
||||
/// Android related state.
|
||||
android_app: AndroidApp,
|
||||
|
||||
/// Context to repaint content and handle viewport commands.
|
||||
ctx: Arc<RwLock<Option<egui::Context>>>,
|
||||
}
|
||||
|
||||
impl Android {
|
||||
|
@ -38,6 +42,7 @@ impl Android {
|
|||
pub fn new(app: AndroidApp) -> Self {
|
||||
Self {
|
||||
android_app: app,
|
||||
ctx: Arc::new(RwLock::new(None)),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -56,27 +61,36 @@ impl 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) {
|
||||
// Disable NDK soft input show call before fix for egui.
|
||||
// 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) {
|
||||
// Disable NDK soft input hide call before fix for egui.
|
||||
// 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) {
|
||||
let vm = unsafe { jni::JavaVM::from_raw(self.android_app.vm_as_ptr() as _) }.unwrap();
|
||||
let env = vm.attach_current_thread().unwrap();
|
||||
let arg_value = env.new_string(data).unwrap();
|
||||
self.call_java_method("copyText",
|
||||
"(Ljava/lang/String;)V",
|
||||
&[JValue::Object(&JObject::from(arg_value))]).unwrap();
|
||||
let _ = self.call_java_method("copyText",
|
||||
"(Ljava/lang/String;)V",
|
||||
&[JValue::Object(&JObject::from(arg_value))]);
|
||||
}
|
||||
|
||||
fn get_string_from_buffer(&self) -> String {
|
||||
|
@ -95,12 +109,12 @@ impl PlatformCallbacks for Android {
|
|||
let mut w_image = LAST_CAMERA_IMAGE.write();
|
||||
*w_image = None;
|
||||
// Start camera.
|
||||
self.call_java_method("startCamera", "()V", &[]).unwrap();
|
||||
let _ = self.call_java_method("startCamera", "()V", &[]);
|
||||
}
|
||||
|
||||
fn stop_camera(&self) {
|
||||
// Stop camera.
|
||||
self.call_java_method("stopCamera", "()V", &[]).unwrap();
|
||||
let _ = self.call_java_method("stopCamera", "()V", &[]);
|
||||
// Clear image.
|
||||
let mut w_image = LAST_CAMERA_IMAGE.write();
|
||||
*w_image = None;
|
||||
|
@ -115,32 +129,39 @@ impl PlatformCallbacks for Android {
|
|||
}
|
||||
|
||||
fn can_switch_camera(&self) -> bool {
|
||||
let result = self.call_java_method("camerasAmount", "()I", &[]).unwrap();
|
||||
let amount = unsafe { result.i };
|
||||
amount > 1
|
||||
if let Some(res) = self.call_java_method("camerasAmount", "()I", &[]) {
|
||||
let amount = unsafe { res.i };
|
||||
return amount > 1;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
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> {
|
||||
// Create file at cache dir.
|
||||
let default_cache = OsString::from(dirs::cache_dir().unwrap());
|
||||
let mut cache = PathBuf::from(env::var_os("XDG_CACHE_HOME").unwrap_or(default_cache));
|
||||
cache.push("images");
|
||||
std::fs::create_dir_all(cache.to_str().unwrap())?;
|
||||
cache.push(name);
|
||||
let mut image = File::create_new(cache.clone()).unwrap();
|
||||
image.write_all(data.as_slice()).unwrap();
|
||||
image.sync_all().unwrap();
|
||||
let mut file = PathBuf::from(env::var_os("XDG_CACHE_HOME").unwrap_or(default_cache));
|
||||
// File path for Android provider.
|
||||
file.push("share");
|
||||
if !file.exists() {
|
||||
std::fs::create_dir(file.clone())?;
|
||||
}
|
||||
file.push(name);
|
||||
if file.exists() {
|
||||
std::fs::remove_file(file.clone())?;
|
||||
}
|
||||
let mut image = File::create_new(file.clone())?;
|
||||
image.write_all(data.as_slice())?;
|
||||
image.sync_all()?;
|
||||
// Call share modal at system.
|
||||
let vm = unsafe { jni::JavaVM::from_raw(self.android_app.vm_as_ptr() as _) }.unwrap();
|
||||
let env = vm.attach_current_thread().unwrap();
|
||||
let arg_value = env.new_string(cache.to_str().unwrap()).unwrap();
|
||||
self.call_java_method("shareImage",
|
||||
"(Ljava/lang/String;)V",
|
||||
&[JValue::Object(&JObject::from(arg_value))]).unwrap();
|
||||
let arg_value = env.new_string(file.to_str().unwrap()).unwrap();
|
||||
let _ = self.call_java_method("shareData",
|
||||
"(Ljava/lang/String;)V",
|
||||
&[JValue::Object(&JObject::from(arg_value))]);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -149,7 +170,7 @@ impl PlatformCallbacks for Android {
|
|||
let mut w_path = PICKED_FILE_PATH.write();
|
||||
*w_path = None;
|
||||
// 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.
|
||||
Some("".to_string())
|
||||
}
|
||||
|
@ -167,6 +188,14 @@ impl PlatformCallbacks for Android {
|
|||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn request_user_attention(&self) {}
|
||||
|
||||
fn user_attention_required(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn clear_user_attention(&self) {}
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
|
|
|
@ -13,12 +13,13 @@
|
|||
// limitations under the License.
|
||||
|
||||
use std::fs::File;
|
||||
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::io::Write;
|
||||
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 crate::gui::platform::PlatformCallbacks;
|
||||
|
@ -26,19 +27,159 @@ use crate::gui::platform::PlatformCallbacks;
|
|||
/// Desktop platform related actions.
|
||||
#[derive(Clone)]
|
||||
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.
|
||||
stop_camera: Arc<AtomicBool>,
|
||||
|
||||
/// Flag to check if attention required after window focusing.
|
||||
attention_required: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl Default for Desktop {
|
||||
fn default() -> Self {
|
||||
impl Desktop {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
ctx: Arc::new(RwLock::new(None)),
|
||||
cameras_amount: Arc::new(AtomicUsize::new(0)),
|
||||
camera_index: Arc::new(AtomicUsize::new(0)),
|
||||
stop_camera: Arc::new(AtomicBool::new(false)),
|
||||
attention_required: Arc::new(AtomicBool::new(false)),
|
||||
}
|
||||
}
|
||||
|
||||
// #[allow(dead_code)]
|
||||
#[cfg(not(target_os = "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 nokhwa_mac::nokhwa_initialize;
|
||||
use nokhwa_mac::pixel_format::RgbFormat;
|
||||
use nokhwa_mac::utils::{CameraIndex, RequestedFormat, RequestedFormatType};
|
||||
use nokhwa_mac::utils::ApiBackend;
|
||||
use nokhwa_mac::query;
|
||||
use nokhwa_mac::CallbackCamera;
|
||||
|
||||
// Ask permission to open camera.
|
||||
nokhwa_initialize(|_| {});
|
||||
|
||||
thread::spawn(move || {
|
||||
let cameras = query(ApiBackend::Auto).unwrap();
|
||||
cameras_amount.store(cameras.len(), Ordering::Relaxed);
|
||||
let index = camera_index.load(Ordering::Relaxed);
|
||||
if cameras.is_empty() || index >= cameras.len() {
|
||||
return;
|
||||
}
|
||||
// Start camera.
|
||||
let camera_index = CameraIndex::Index(camera_index.load(Ordering::Relaxed) as u32);
|
||||
let camera_callback = CallbackCamera::new(
|
||||
camera_index,
|
||||
RequestedFormat::new::<RgbFormat>(RequestedFormatType::AbsoluteHighestFrameRate),
|
||||
|_| {}
|
||||
);
|
||||
if let Ok(mut cb) = camera_callback {
|
||||
if cb.open_stream().is_ok() {
|
||||
loop {
|
||||
// Stop if camera was stopped.
|
||||
if stop_camera.load(Ordering::Relaxed) {
|
||||
stop_camera.store(false, Ordering::Relaxed);
|
||||
// Clear image.
|
||||
let mut w_image = LAST_CAMERA_IMAGE.write();
|
||||
*w_image = None;
|
||||
break;
|
||||
}
|
||||
// Get image from camera.
|
||||
if let Ok(frame) = cb.poll_frame() {
|
||||
let image = frame.decode_image::<RgbFormat>().unwrap();
|
||||
let mut bytes: Vec<u8> = Vec::new();
|
||||
let format = image::ImageFormat::Jpeg;
|
||||
// Convert image to Jpeg format.
|
||||
image.write_to(&mut std::io::Cursor::new(&mut bytes), format).unwrap();
|
||||
let mut w_image = LAST_CAMERA_IMAGE.write();
|
||||
*w_image = Some((bytes, 0));
|
||||
} else {
|
||||
// Clear image.
|
||||
let mut w_image = LAST_CAMERA_IMAGE.write();
|
||||
*w_image = None;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl PlatformCallbacks for Desktop {
|
||||
fn set_context(&mut self, ctx: &egui::Context) {
|
||||
let mut w_ctx = self.ctx.write();
|
||||
*w_ctx = Some(ctx.clone());
|
||||
}
|
||||
|
||||
fn exit(&self) {
|
||||
let r_ctx = self.ctx.read();
|
||||
if r_ctx.is_some() {
|
||||
let ctx = r_ctx.as_ref().unwrap();
|
||||
ctx.send_viewport_cmd(ViewportCommand::Close);
|
||||
}
|
||||
}
|
||||
|
||||
fn show_keyboard(&self) {}
|
||||
|
||||
fn hide_keyboard(&self) {}
|
||||
|
@ -59,15 +200,13 @@ impl PlatformCallbacks for Desktop {
|
|||
let mut w_image = LAST_CAMERA_IMAGE.write();
|
||||
*w_image = None;
|
||||
}
|
||||
|
||||
// Setup stop camera flag.
|
||||
let stop_camera = self.stop_camera.clone();
|
||||
stop_camera.store(false, Ordering::Relaxed);
|
||||
|
||||
// Capture images at separate thread.
|
||||
thread::spawn(move || {
|
||||
Self::start_camera_capture(stop_camera);
|
||||
});
|
||||
Self::start_camera_capture(self.cameras_amount.clone(),
|
||||
self.camera_index.clone(),
|
||||
stop_camera);
|
||||
}
|
||||
|
||||
fn stop_camera(&self) {
|
||||
|
@ -84,11 +223,20 @@ impl PlatformCallbacks for Desktop {
|
|||
}
|
||||
|
||||
fn can_switch_camera(&self) -> bool {
|
||||
false
|
||||
let amount = self.cameras_amount.load(Ordering::Relaxed);
|
||||
amount > 1
|
||||
}
|
||||
|
||||
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> {
|
||||
|
@ -119,86 +267,43 @@ impl PlatformCallbacks for Desktop {
|
|||
fn picked_file(&self) -> Option<String> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl Desktop {
|
||||
#[allow(dead_code)]
|
||||
#[cfg(target_os = "windows")]
|
||||
fn start_camera_capture(stop_camera: Arc<AtomicBool>) {
|
||||
use nokhwa::Camera;
|
||||
use nokhwa::pixel_format::RgbFormat;
|
||||
use nokhwa::utils::{CameraIndex, RequestedFormat, RequestedFormatType};
|
||||
let index = CameraIndex::Index(0);
|
||||
let requested = RequestedFormat::new::<RgbFormat>(
|
||||
RequestedFormatType::AbsoluteHighestFrameRate
|
||||
);
|
||||
// Create and open camera.
|
||||
let mut camera = Camera::new(index, requested).unwrap();
|
||||
if let Ok(_) = camera.open_stream() {
|
||||
loop {
|
||||
// Stop if camera was stopped.
|
||||
if stop_camera.load(Ordering::Relaxed) {
|
||||
stop_camera.store(false, Ordering::Relaxed);
|
||||
// Clear image.
|
||||
let mut w_image = LAST_CAMERA_IMAGE.write();
|
||||
*w_image = None;
|
||||
break;
|
||||
}
|
||||
// Get a frame.
|
||||
if let Ok(frame) = camera.frame() {
|
||||
// Save image.
|
||||
let mut w_image = LAST_CAMERA_IMAGE.write();
|
||||
*w_image = Some((frame.buffer().to_vec(), 0));
|
||||
} else {
|
||||
// Clear image.
|
||||
let mut w_image = LAST_CAMERA_IMAGE.write();
|
||||
*w_image = None;
|
||||
break;
|
||||
}
|
||||
fn request_user_attention(&self) {
|
||||
let r_ctx = self.ctx.read();
|
||||
if r_ctx.is_some() {
|
||||
let ctx = r_ctx.as_ref().unwrap();
|
||||
// Request attention on taskbar.
|
||||
ctx.send_viewport_cmd(
|
||||
ViewportCommand::RequestUserAttention(UserAttentionType::Informational)
|
||||
);
|
||||
// Un-minimize window.
|
||||
if ctx.input(|i| i.viewport().minimized.unwrap_or(false)) {
|
||||
ctx.send_viewport_cmd(ViewportCommand::Minimized(false));
|
||||
}
|
||||
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)]
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
fn start_camera_capture(stop_camera: Arc<AtomicBool>) {
|
||||
use eye::hal::{traits::{Context, Device, Stream}, PlatformContext};
|
||||
use image::ImageEncoder;
|
||||
fn user_attention_required(&self) -> bool {
|
||||
self.attention_required.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
let ctx = PlatformContext::default();
|
||||
let devices = ctx.devices().unwrap();
|
||||
let dev = ctx.open_device(&devices[0].uri).unwrap();
|
||||
|
||||
let streams = dev.streams().unwrap();
|
||||
let stream_desc = streams[0].clone();
|
||||
let w = stream_desc.width;
|
||||
let h = stream_desc.height;
|
||||
|
||||
let mut stream = dev.start_stream(&stream_desc).unwrap();
|
||||
|
||||
loop {
|
||||
// Stop if camera was stopped.
|
||||
if stop_camera.load(Ordering::Relaxed) {
|
||||
stop_camera.store(false, Ordering::Relaxed);
|
||||
// Clear image.
|
||||
let mut w_image = LAST_CAMERA_IMAGE.write();
|
||||
*w_image = None;
|
||||
break;
|
||||
}
|
||||
// Get a frame.
|
||||
let frame = stream.next().expect("Stream is dead").expect("Failed to capture a frame");
|
||||
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));
|
||||
fn clear_user_attention(&self) {
|
||||
let r_ctx = self.ctx.read();
|
||||
if r_ctx.is_some() {
|
||||
let ctx = r_ctx.as_ref().unwrap();
|
||||
ctx.send_viewport_cmd(
|
||||
ViewportCommand::RequestUserAttention(UserAttentionType::Reset)
|
||||
);
|
||||
ctx.send_viewport_cmd(ViewportCommand::WindowLevel(WindowLevel::Normal));
|
||||
}
|
||||
self.attention_required.store(false, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -22,6 +22,8 @@ pub mod platform;
|
|||
pub mod platform;
|
||||
|
||||
pub trait PlatformCallbacks {
|
||||
fn set_context(&mut self, ctx: &egui::Context);
|
||||
fn exit(&self);
|
||||
fn show_keyboard(&self);
|
||||
fn hide_keyboard(&self);
|
||||
fn copy_string_to_buffer(&self, data: String);
|
||||
|
@ -34,4 +36,7 @@ pub trait PlatformCallbacks {
|
|||
fn share_data(&self, name: String, data: Vec<u8>) -> Result<(), std::io::Error>;
|
||||
fn pick_file(&self) -> Option<String>;
|
||||
fn picked_file(&self) -> Option<String>;
|
||||
fn request_user_attention(&self);
|
||||
fn user_attention_required(&self) -> bool;
|
||||
fn clear_user_attention(&self);
|
||||
}
|
|
@ -15,11 +15,9 @@
|
|||
use std::sync::Arc;
|
||||
use parking_lot::RwLock;
|
||||
use std::thread;
|
||||
use eframe::emath::Align;
|
||||
use egui::load::SizedTexture;
|
||||
use egui::{Layout, Pos2, Rect, RichText, TextureOptions, Widget};
|
||||
use image::{DynamicImage, EncodableLayout, ImageFormat};
|
||||
|
||||
use egui::{Pos2, Rect, RichText, TextureOptions, UiBuilder, Widget};
|
||||
use image::{DynamicImage, EncodableLayout};
|
||||
use grin_util::ZeroingString;
|
||||
use grin_wallet_libwallet::SlatepackAddress;
|
||||
use grin_keychain::mnemonic::WORDS;
|
||||
|
@ -36,16 +34,15 @@ use crate::wallet::WalletUtils;
|
|||
pub struct CameraContent {
|
||||
/// QR code scanning progress and result.
|
||||
qr_scan_state: Arc<RwLock<QrScanState>>,
|
||||
|
||||
/// Uniform Resources URIs collected from QR code scanning.
|
||||
ur_data: Arc<RwLock<Option<(Vec<String>, usize)>>>,
|
||||
ur_data: Arc<RwLock<Option<(Vec<String>, usize)>>>
|
||||
}
|
||||
|
||||
impl Default for CameraContent {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
qr_scan_state: Arc::new(RwLock::new(QrScanState::default())),
|
||||
ur_data: Arc::new(RwLock::new(None)),
|
||||
ur_data: Arc::new(RwLock::new(None))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -53,102 +50,112 @@ impl Default for CameraContent {
|
|||
impl CameraContent {
|
||||
/// Draw camera content.
|
||||
pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||
// Draw last image from camera or loader.
|
||||
if let Some(img_data) = cb.camera_image() {
|
||||
// Load image to draw.
|
||||
if let Ok(mut img) =
|
||||
image::load_from_memory_with_format(&*img_data.0, ImageFormat::Jpeg) {
|
||||
ui.ctx().request_repaint();
|
||||
let rect = if let Some(img_data) = cb.camera_image() {
|
||||
if let Ok(img) =
|
||||
image::load_from_memory(&*img_data.0) {
|
||||
// Process image to find QR code.
|
||||
self.scan_qr(&img);
|
||||
// Setup image rotation.
|
||||
img = match img_data.1 {
|
||||
90 => img.rotate90(),
|
||||
180 => img.rotate180(),
|
||||
270 => img.rotate270(),
|
||||
_ => img
|
||||
};
|
||||
// Convert to ColorImage to add at content.
|
||||
let color_img = match &img {
|
||||
DynamicImage::ImageRgb8(image) => {
|
||||
egui::ColorImage::from_rgb(
|
||||
[image.width() as usize, image.height() as usize],
|
||||
image.as_bytes(),
|
||||
)
|
||||
},
|
||||
other => {
|
||||
let image = other.to_rgba8();
|
||||
egui::ColorImage::from_rgba_unmultiplied(
|
||||
[image.width() as usize, image.height() as usize],
|
||||
image.as_bytes(),
|
||||
)
|
||||
},
|
||||
};
|
||||
// Create image texture.
|
||||
let texture = ui.ctx().load_texture("camera_image",
|
||||
color_img.clone(),
|
||||
TextureOptions::default());
|
||||
let img_size = egui::emath::vec2(color_img.width() as f32,
|
||||
color_img.height() as f32);
|
||||
let sized_img = SizedTexture::new(texture.id(), img_size);
|
||||
// Add image to content.
|
||||
ui.vertical_centered(|ui| {
|
||||
egui::Image::from_texture(sized_img)
|
||||
// Setup to crop image at square.
|
||||
.uv(Rect::from([
|
||||
Pos2::new(1.0 - (img_size.y / img_size.x), 0.0),
|
||||
Pos2::new(1.0, 1.0)
|
||||
]))
|
||||
.max_height(ui.available_width())
|
||||
.maintain_aspect_ratio(false)
|
||||
.shrink_to_fit()
|
||||
.ui(ui);
|
||||
});
|
||||
|
||||
// Draw image.
|
||||
let img_rect = self.image_ui(ui, img, img_data.1);
|
||||
|
||||
// Show UR scan progress.
|
||||
let show_ur_progress = {
|
||||
self.ur_data.clone().read().is_some()
|
||||
};
|
||||
let ur_progress = self.ur_progress();
|
||||
if show_ur_progress && ur_progress != 0 {
|
||||
ui.add_space(-52.0);
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.label(RichText::new(format!("{}%", ur_progress))
|
||||
.size(16.0)
|
||||
.color(Colors::yellow()));
|
||||
});
|
||||
}
|
||||
|
||||
// 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();
|
||||
});
|
||||
});
|
||||
}
|
||||
self.ur_progress_ui(ui);
|
||||
img_rect
|
||||
} else {
|
||||
self.loading_content_ui(ui);
|
||||
self.loading_ui(ui)
|
||||
}
|
||||
} else {
|
||||
self.loading_content_ui(ui);
|
||||
}
|
||||
self.loading_ui(ui)
|
||||
};
|
||||
|
||||
// Request redraw.
|
||||
ui.ctx().request_repaint();
|
||||
// Show button to switch cameras.
|
||||
if cb.can_switch_camera() {
|
||||
let r = {
|
||||
let mut r = rect.clone();
|
||||
r.min.y = r.max.y - 52.0;
|
||||
r.min.x = r.max.x - 52.0;
|
||||
r
|
||||
};
|
||||
ui.allocate_new_ui(UiBuilder::new().max_rect(r), |ui| {
|
||||
let rotate_img = CAMERA_ROTATE.to_string();
|
||||
View::button(ui, rotate_img, Colors::white_or_black(false), || {
|
||||
cb.switch_camera();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw camera image.
|
||||
fn image_ui(&mut self, ui: &mut egui::Ui, mut img: DynamicImage, rotation: u32) -> Rect {
|
||||
// Setup image rotation.
|
||||
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.
|
||||
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;
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.add_space(space);
|
||||
View::big_loading_spinner(ui);
|
||||
ui.add_space(space);
|
||||
});
|
||||
}).response.rect
|
||||
}
|
||||
|
||||
/// Check if image is processing to find QR code.
|
||||
|
@ -283,7 +290,7 @@ impl CameraContent {
|
|||
|
||||
// Launch scanner at separate thread.
|
||||
thread::spawn(move || {
|
||||
tokio::runtime::Builder::new_multi_thread()
|
||||
tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap()
|
||||
|
@ -430,14 +437,4 @@ impl CameraContent {
|
|||
}
|
||||
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::{AppConfig, Settings};
|
||||
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;
|
||||
|
||||
lazy_static! {
|
||||
|
@ -40,8 +40,8 @@ pub struct Content {
|
|||
/// Central panel [`WalletsContent`] content.
|
||||
pub wallets: WalletsContent,
|
||||
|
||||
/// Check if app exit is allowed on close event of [`eframe::App`] implementation.
|
||||
pub(crate) exit_allowed: bool,
|
||||
/// Check if app exit is allowed on Desktop close event.
|
||||
pub exit_allowed: bool,
|
||||
/// Flag to show exit progress at [`Modal`].
|
||||
show_exit_progress: bool,
|
||||
|
||||
|
@ -52,6 +52,11 @@ pub struct Content {
|
|||
allowed_modal_ids: Vec<&'static str>
|
||||
}
|
||||
|
||||
/// Identifier for integrated node warning [`Modal`] on Android.
|
||||
const ANDROID_INTEGRATED_NODE_WARNING_MODAL: &'static str = "android_node_warning_modal";
|
||||
/// Identifier for crash report [`Modal`].
|
||||
const CRASH_REPORT_MODAL: &'static str = "crash_report_modal";
|
||||
|
||||
impl Default for Content {
|
||||
fn default() -> Self {
|
||||
// Exit from eframe only for non-mobile platforms.
|
||||
|
@ -66,8 +71,8 @@ impl Default for Content {
|
|||
allowed_modal_ids: vec![
|
||||
Self::EXIT_CONFIRMATION_MODAL,
|
||||
Self::SETTINGS_MODAL,
|
||||
Self::ANDROID_INTEGRATED_NODE_WARNING_MODAL,
|
||||
Self::CRASH_REPORT_MODAL
|
||||
ANDROID_INTEGRATED_NODE_WARNING_MODAL,
|
||||
CRASH_REPORT_MODAL
|
||||
],
|
||||
}
|
||||
}
|
||||
|
@ -83,10 +88,10 @@ impl ModalContainer for Content {
|
|||
modal: &Modal,
|
||||
cb: &dyn PlatformCallbacks) {
|
||||
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::ANDROID_INTEGRATED_NODE_WARNING_MODAL => self.android_warning_modal_ui(ui, modal),
|
||||
Self::CRASH_REPORT_MODAL => self.crash_report_modal_ui(ui, modal, cb),
|
||||
ANDROID_INTEGRATED_NODE_WARNING_MODAL => self.android_warning_modal_ui(ui, modal),
|
||||
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";
|
||||
/// Identifier for wallet opening [`Modal`].
|
||||
pub const SETTINGS_MODAL: &'static str = "settings_modal";
|
||||
/// Identifier for integrated node warning [`Modal`] on Android.
|
||||
const ANDROID_INTEGRATED_NODE_WARNING_MODAL: &'static str = "android_node_warning_modal";
|
||||
/// Identifier for crash report [`Modal`].
|
||||
const CRASH_REPORT_MODAL: &'static str = "crash_report_modal";
|
||||
|
||||
/// Default width of side panel at application UI.
|
||||
pub const SIDE_PANEL_WIDTH: f32 = 400.0;
|
||||
|
@ -110,11 +111,10 @@ impl Content {
|
|||
pub const WINDOW_FRAME_MARGIN: f32 = 6.0;
|
||||
|
||||
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);
|
||||
|
||||
let dual_panel = Self::is_dual_panel_mode(ui);
|
||||
let (is_panel_open, panel_width) = Self::network_panel_state_width(ui, dual_panel);
|
||||
let dual_panel = Self::is_dual_panel_mode(ui.ctx());
|
||||
let (is_panel_open, panel_width) = network_panel_state_width(ui.ctx(), dual_panel);
|
||||
|
||||
// Show network content.
|
||||
egui::SidePanel::left("network_panel")
|
||||
|
@ -137,48 +137,26 @@ impl Content {
|
|||
});
|
||||
|
||||
if self.first_draw {
|
||||
// Show crash report if needed.
|
||||
if AppConfig::show_crash() {
|
||||
Modal::new(Self::CRASH_REPORT_MODAL)
|
||||
// Show crash report or integrated node Android warning.
|
||||
if Settings::crash_report_path().exists() {
|
||||
Modal::new(CRASH_REPORT_MODAL)
|
||||
.closeable(false)
|
||||
.position(ModalPosition::Center)
|
||||
.title(t!("crash_report"))
|
||||
.show();
|
||||
} else {
|
||||
// Show integrated node warning on Android if needed.
|
||||
if OperatingSystem::from_target_os() == OperatingSystem::Android &&
|
||||
} else if OperatingSystem::from_target_os() == OperatingSystem::Android &&
|
||||
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"))
|
||||
.show();
|
||||
}
|
||||
}
|
||||
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.
|
||||
pub fn is_dual_panel_mode(ui: &egui::Ui) -> bool {
|
||||
let (w, h) = View::window_size(ui);
|
||||
pub fn is_dual_panel_mode(ctx: &egui::Context) -> bool {
|
||||
let (w, h) = View::window_size(ctx);
|
||||
// Screen is wide if width is greater than height or just 20% smaller.
|
||||
let is_wide_screen = w > h || w + (w * 0.2) >= h;
|
||||
// Dual panel mode is available when window is wide and its width is at least 2 times
|
||||
|
@ -201,16 +179,16 @@ impl Content {
|
|||
/// Show exit confirmation [`Modal`].
|
||||
pub fn show_exit_modal() {
|
||||
Modal::new(Self::EXIT_CONFIRMATION_MODAL)
|
||||
.title(t!("modal.confirmation"))
|
||||
.title(t!("confirmation"))
|
||||
.show();
|
||||
}
|
||||
|
||||
/// 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 !Node::is_running() {
|
||||
self.exit_allowed = true;
|
||||
ui.ctx().send_viewport_cmd(egui::ViewportCommand::Close);
|
||||
cb.exit();
|
||||
modal.close();
|
||||
}
|
||||
ui.add_space(16.0);
|
||||
|
@ -241,10 +219,10 @@ impl Content {
|
|||
});
|
||||
});
|
||||
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() {
|
||||
self.exit_allowed = true;
|
||||
ui.ctx().send_viewport_cmd(egui::ViewportCommand::Close);
|
||||
cb.exit();
|
||||
modal.close();
|
||||
} else {
|
||||
Node::stop(true);
|
||||
|
@ -260,9 +238,9 @@ impl Content {
|
|||
}
|
||||
|
||||
/// Handle Back key event.
|
||||
pub fn on_back(&mut self) {
|
||||
pub fn on_back(&mut self, cb: &dyn PlatformCallbacks) {
|
||||
if Modal::on_back() {
|
||||
if self.wallets.on_back() {
|
||||
if self.wallets.on_back(cb) {
|
||||
Self::show_exit_modal()
|
||||
}
|
||||
}
|
||||
|
@ -272,14 +250,7 @@ impl Content {
|
|||
pub fn settings_modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal) {
|
||||
ui.add_space(6.0);
|
||||
|
||||
// Draw chain type selection.
|
||||
NodeSetup::chain_type_ui(ui);
|
||||
|
||||
ui.add_space(8.0);
|
||||
View::horizontal_line(ui, Colors::item_stroke());
|
||||
ui.add_space(8.0);
|
||||
|
||||
// Draw theme selection.
|
||||
// Show theme selection.
|
||||
Self::theme_selection_ui(ui);
|
||||
|
||||
ui.add_space(8.0);
|
||||
|
@ -348,42 +319,40 @@ impl Content {
|
|||
let item_rounding = View::item_rounding(index, len, false);
|
||||
ui.painter().rect(bg_rect, item_rounding, Colors::fill(), View::item_stroke());
|
||||
|
||||
ui.vertical(|ui| {
|
||||
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
|
||||
// Draw button to select language.
|
||||
let is_current = if let Some(lang) = AppConfig::locale() {
|
||||
lang == locale
|
||||
} else {
|
||||
rust_i18n::locale() == locale
|
||||
};
|
||||
if !is_current {
|
||||
View::item_button(ui, View::item_rounding(index, len, true), CHECK, None, || {
|
||||
rust_i18n::set_locale(locale);
|
||||
AppConfig::save_locale(locale);
|
||||
modal.close();
|
||||
});
|
||||
} else {
|
||||
ui.add_space(14.0);
|
||||
ui.label(RichText::new(CHECK_FAT).size(20.0).color(Colors::green()));
|
||||
ui.add_space(14.0);
|
||||
}
|
||||
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
|
||||
// Draw button to select language.
|
||||
let is_current = if let Some(lang) = AppConfig::locale() {
|
||||
lang == locale
|
||||
} else {
|
||||
rust_i18n::locale() == locale
|
||||
};
|
||||
if !is_current {
|
||||
View::item_button(ui, View::item_rounding(index, len, true), CHECK, None, || {
|
||||
rust_i18n::set_locale(locale);
|
||||
AppConfig::save_locale(locale);
|
||||
modal.close();
|
||||
});
|
||||
} else {
|
||||
ui.add_space(14.0);
|
||||
ui.label(RichText::new(CHECK_FAT).size(20.0).color(Colors::green()));
|
||||
ui.add_space(14.0);
|
||||
}
|
||||
|
||||
let layout_size = ui.available_size();
|
||||
ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| {
|
||||
let layout_size = ui.available_size();
|
||||
ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| {
|
||||
ui.add_space(12.0);
|
||||
ui.vertical(|ui| {
|
||||
// Draw language name.
|
||||
ui.add_space(12.0);
|
||||
ui.vertical(|ui| {
|
||||
// Draw language name.
|
||||
ui.add_space(12.0);
|
||||
let color = if is_current {
|
||||
Colors::title(false)
|
||||
} else {
|
||||
Colors::gray()
|
||||
};
|
||||
ui.label(RichText::new(t!("lang_name", locale = locale))
|
||||
.size(17.0)
|
||||
.color(color));
|
||||
ui.add_space(3.0);
|
||||
});
|
||||
let color = if is_current {
|
||||
Colors::title(false)
|
||||
} else {
|
||||
Colors::gray()
|
||||
};
|
||||
ui.label(RichText::new(t!("lang_name", locale = locale))
|
||||
.size(17.0)
|
||||
.color(color));
|
||||
ui.add_space(3.0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -422,10 +391,10 @@ impl Content {
|
|||
let text = format!("{} {}", FILE_X, t!("share"));
|
||||
View::colored_text_button(ui, text, Colors::blue(), Colors::white_or_black(false), || {
|
||||
if let Ok(data) = fs::read_to_string(Settings::crash_report_path()) {
|
||||
cb.share_data(Settings::CRASH_REPORT_FILE_NAME.to_string(),
|
||||
data.as_bytes().to_vec()).unwrap_or_default()
|
||||
let name = Settings::CRASH_REPORT_FILE_NAME.to_string();
|
||||
let _ = cb.share_data(name, data.as_bytes().to_vec());
|
||||
}
|
||||
AppConfig::set_show_crash(false);
|
||||
Settings::delete_crash_report();
|
||||
modal.close();
|
||||
});
|
||||
});
|
||||
|
@ -434,10 +403,29 @@ impl Content {
|
|||
ui.add_space(8.0);
|
||||
ui.vertical_centered_justified(|ui| {
|
||||
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
|
||||
AppConfig::set_show_crash(false);
|
||||
Settings::delete_crash_report();
|
||||
modal.close();
|
||||
});
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
}
|
||||
}
|
||||
|
||||
/// Get [`NetworkContent`] panel state and width.
|
||||
fn network_panel_state_width(ctx: &egui::Context, dual_panel: bool) -> (bool, f32) {
|
||||
let is_panel_open = dual_panel || Content::is_network_panel_open();
|
||||
let panel_width = if dual_panel {
|
||||
Content::SIDE_PANEL_WIDTH + View::get_left_inset()
|
||||
} else {
|
||||
let is_fullscreen = ctx.input(|i| {
|
||||
i.viewport().fullscreen.unwrap_or(false)
|
||||
});
|
||||
View::window_size(ctx).0 - if View::is_desktop() && !is_fullscreen &&
|
||||
OperatingSystem::from_target_os() != OperatingSystem::Mac {
|
||||
Content::WINDOW_FRAME_MARGIN * 2.0
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
};
|
||||
(is_panel_open, panel_width)
|
||||
}
|
|
@ -78,8 +78,8 @@ impl FilePickButton {
|
|||
}
|
||||
} else {
|
||||
// Draw button to pick file.
|
||||
let file_text = format!("{} {}", ARCHIVE_BOX, t!("choose_file"));
|
||||
View::colored_text_button(ui, file_text, Colors::blue(), Colors::button(), || {
|
||||
let text = format!("{} {}", ARCHIVE_BOX, t!("choose_file"));
|
||||
View::colored_text_button(ui, text, Colors::blue(), Colors::white_or_black(false), || {
|
||||
if let Some(path) = cb.pick_file() {
|
||||
self.on_file_pick(path);
|
||||
}
|
|
@ -36,8 +36,11 @@ pub use camera::*;
|
|||
mod qr;
|
||||
pub use qr::*;
|
||||
|
||||
mod file;
|
||||
pub use file::*;
|
||||
mod file_pick;
|
||||
pub use file_pick::*;
|
||||
|
||||
mod pull_to_refresh;
|
||||
pub use pull_to_refresh::*;
|
||||
pub use pull_to_refresh::*;
|
||||
|
||||
mod scan;
|
||||
pub use scan::*;
|
202
src/gui/views/modal.rs
Normal file → Executable file
202
src/gui/views/modal.rs
Normal file → Executable file
|
@ -16,7 +16,7 @@ use lazy_static::lazy_static;
|
|||
use std::sync::Arc;
|
||||
use parking_lot::RwLock;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use egui::{Align2, Rect, RichText, Rounding, Stroke, Vec2};
|
||||
use egui::{Align2, RichText, Rounding, Stroke, UiBuilder, Vec2};
|
||||
use egui::epaint::{RectShape, Shadow};
|
||||
use egui::os::OperatingSystem;
|
||||
|
||||
|
@ -29,17 +29,17 @@ lazy_static! {
|
|||
static ref MODAL_STATE: Arc<RwLock<ModalState>> = Arc::new(RwLock::new(ModalState::default()));
|
||||
}
|
||||
|
||||
/// Stores data to draw modal [`egui::Window`] at ui.
|
||||
/// Modal [`egui::Window`] container.
|
||||
#[derive(Clone)]
|
||||
pub struct Modal {
|
||||
/// Identifier for modal.
|
||||
pub(crate) id: &'static str,
|
||||
/// Position on the screen.
|
||||
position: ModalPosition,
|
||||
/// To check if it can be closed.
|
||||
pub position: ModalPosition,
|
||||
/// Flag to check if modal can be closed by keys.
|
||||
closeable: Arc<AtomicBool>,
|
||||
/// Title text
|
||||
title: Option<String>
|
||||
/// Title text.
|
||||
title: Option<String>,
|
||||
}
|
||||
|
||||
impl Modal {
|
||||
|
@ -54,7 +54,7 @@ impl Modal {
|
|||
id,
|
||||
position: ModalPosition::Center,
|
||||
closeable: Arc::new(AtomicBool::new(true)),
|
||||
title: None
|
||||
title: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -64,6 +64,12 @@ impl Modal {
|
|||
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.
|
||||
pub fn close(&self) {
|
||||
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.
|
||||
/// Return `false` if Modal existed in [`ModalState`] before call.
|
||||
/// Return `false` if modal existed in state before call.
|
||||
pub fn on_back() -> bool {
|
||||
let mut w_state = MODAL_STATE.write();
|
||||
|
||||
|
@ -119,7 +125,7 @@ impl Modal {
|
|||
true
|
||||
}
|
||||
|
||||
/// Return id of opened [`Modal`].
|
||||
/// Return identifier of opened [`Modal`].
|
||||
pub fn opened() -> Option<&'static str> {
|
||||
// Check if modal is showing.
|
||||
{
|
||||
|
@ -134,6 +140,19 @@ impl Modal {
|
|||
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`].
|
||||
pub fn set_title(title: String) {
|
||||
// Save state.
|
||||
|
@ -164,40 +183,43 @@ impl Modal {
|
|||
let is_fullscreen = ctx.input(|i| {
|
||||
i.viewport().fullscreen.unwrap_or(false)
|
||||
});
|
||||
let is_mac_os = OperatingSystem::from_target_os() == OperatingSystem::Mac;
|
||||
|
||||
let mut rect = ctx.screen_rect();
|
||||
if View::is_desktop() && !is_mac_os {
|
||||
let margin = if !is_fullscreen {
|
||||
Content::WINDOW_FRAME_MARGIN
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
rect = rect.shrink(margin - 0.5);
|
||||
rect.min += egui::vec2(0.0, Content::WINDOW_TITLE_HEIGHT + 0.5);
|
||||
rect.max.x += 0.5;
|
||||
}
|
||||
// Setup background rect.
|
||||
let is_win = OperatingSystem::Windows == OperatingSystem::from_target_os();
|
||||
let bg_rect = if View::is_desktop() && !is_win {
|
||||
let mut r = ctx.screen_rect();
|
||||
let is_mac = OperatingSystem::Mac == OperatingSystem::from_target_os();
|
||||
if !is_mac && !is_fullscreen {
|
||||
r = r.shrink(Content::WINDOW_FRAME_MARGIN - 1.0);
|
||||
}
|
||||
r.min.y += Content::WINDOW_TITLE_HEIGHT;
|
||||
r
|
||||
} else {
|
||||
ctx.screen_rect()
|
||||
};
|
||||
|
||||
// Draw modal background.
|
||||
egui::Window::new("modal_bg_window")
|
||||
.title_bar(false)
|
||||
.resizable(false)
|
||||
.collapsible(false)
|
||||
.fixed_rect(rect)
|
||||
.fixed_rect(bg_rect)
|
||||
.frame(egui::Frame {
|
||||
fill: Colors::semi_transparent(),
|
||||
..Default::default()
|
||||
})
|
||||
.show(ctx, |ui| {
|
||||
ui.set_min_size(rect.size());
|
||||
ui.set_min_size(bg_rect.size());
|
||||
});
|
||||
|
||||
// Setup width of modal content.
|
||||
let side_insets = View::get_left_inset() + View::get_right_inset();
|
||||
let available_width = 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);
|
||||
|
||||
// Show main content Window at given position.
|
||||
let (content_align, content_offset) = self.modal_position(is_fullscreen);
|
||||
let layer_id = egui::Window::new(format!("modal_window_{}", self.id))
|
||||
// Show main content window at given position.
|
||||
let (content_align, content_offset) = self.modal_position();
|
||||
let layer_id = egui::Window::new("modal_window")
|
||||
.title_bar(false)
|
||||
.resizable(false)
|
||||
.collapsible(false)
|
||||
|
@ -212,33 +234,31 @@ impl Modal {
|
|||
color: egui::Color32::from_black_alpha(32),
|
||||
},
|
||||
rounding: Rounding::same(8.0),
|
||||
fill: Colors::fill(),
|
||||
..Default::default()
|
||||
})
|
||||
.show(ctx, |ui| {
|
||||
if self.title.is_some() {
|
||||
self.title_ui(ui);
|
||||
if let Some(title) = &self.title {
|
||||
title_ui(title, ui);
|
||||
}
|
||||
self.content_ui(ui, add_content);
|
||||
}).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);
|
||||
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
ModalPosition::CenterTop => Align2::CENTER_TOP,
|
||||
ModalPosition::Center => Align2::CENTER_CENTER
|
||||
};
|
||||
|
||||
let x_align = View::get_left_inset() - View::get_right_inset();
|
||||
|
||||
let is_mac_os = OperatingSystem::from_target_os() == OperatingSystem::Mac;
|
||||
let extra_y = if View::is_desktop() && !is_mac_os {
|
||||
Content::WINDOW_TITLE_HEIGHT + if !is_fullscreen {
|
||||
let is_mac = OperatingSystem::Mac == OperatingSystem::from_target_os();
|
||||
let is_win = OperatingSystem::Windows == OperatingSystem::from_target_os();
|
||||
let extra_y = if View::is_desktop() && !is_win {
|
||||
Content::WINDOW_TITLE_HEIGHT + if !is_mac {
|
||||
Content::WINDOW_FRAME_MARGIN
|
||||
} else {
|
||||
0.0
|
||||
|
@ -246,7 +266,7 @@ impl Modal {
|
|||
} else {
|
||||
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 {
|
||||
ModalPosition::CenterTop => Vec2::new(x_align, y_align),
|
||||
|
@ -258,80 +278,64 @@ impl Modal {
|
|||
/// Draw provided content.
|
||||
fn content_ui(&self, ui: &mut egui::Ui, add_content: impl FnOnce(&mut egui::Ui, &Modal)) {
|
||||
let mut rect = ui.available_rect_before_wrap();
|
||||
rect.min += egui::emath::vec2(6.0, 0.0);
|
||||
rect.max -= egui::emath::vec2(6.0, 0.0);
|
||||
|
||||
// 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 {
|
||||
nw: 0.0,
|
||||
ne: 0.0,
|
||||
sw: 8.0,
|
||||
se: 8.0,
|
||||
}
|
||||
} else {
|
||||
Rounding::same(8.0)
|
||||
};
|
||||
let mut bg_shape = RectShape {
|
||||
rect,
|
||||
rounding,
|
||||
fill: Colors::fill(),
|
||||
stroke: Stroke::NONE,
|
||||
blur_width: 0.0,
|
||||
fill_texture_id: Default::default(),
|
||||
uv: Rect::ZERO
|
||||
};
|
||||
}, Colors::fill(), Stroke::NONE);
|
||||
let bg_idx = ui.painter().add(bg_shape);
|
||||
|
||||
// Draw main content.
|
||||
let mut content_rect = ui.allocate_ui_at_rect(rect, |ui| {
|
||||
rect.min += egui::emath::vec2(6.0, 0.0);
|
||||
rect.max -= egui::emath::vec2(6.0, 0.0);
|
||||
let resp = ui.allocate_new_ui(UiBuilder::new().max_rect(rect), |ui| {
|
||||
(add_content)(ui, self);
|
||||
}).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;
|
||||
|
||||
// Setup background shape to be painted behind title content.
|
||||
bg_shape.rect = title_resp.rect;
|
||||
// Setup background size.
|
||||
let bg_rect = {
|
||||
let mut r = resp.rect.clone();
|
||||
r.min -= egui::emath::vec2(6.0, 0.0);
|
||||
r.max += egui::emath::vec2(6.0, 0.0);
|
||||
r
|
||||
};
|
||||
bg_shape.rect = bg_rect;
|
||||
ui.painter().set(bg_idx, bg_shape);
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw title content.
|
||||
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::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::views::{Modal, View};
|
||||
use crate::gui::views::network::modals::ExternalConnectionModal;
|
||||
|
@ -36,7 +36,6 @@ pub struct ConnectionsContent {
|
|||
|
||||
impl Default for ConnectionsContent {
|
||||
fn default() -> Self {
|
||||
ExternalConnection::check_ext_conn_availability(None);
|
||||
Self {
|
||||
ext_conn_modal: ExternalConnectionModal::new(None),
|
||||
modal_ids: vec![
|
||||
|
@ -78,7 +77,7 @@ impl ConnectionsContent {
|
|||
|
||||
// Check connections availability.
|
||||
if saved_chain_type != AppConfig::chain_type() {
|
||||
ExternalConnection::check_ext_conn_availability(None);
|
||||
ExternalConnection::check(None, ui.ctx());
|
||||
}
|
||||
|
||||
// Show integrated node info content.
|
||||
|
@ -103,23 +102,20 @@ impl ConnectionsContent {
|
|||
ui.add_space(4.0);
|
||||
|
||||
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);
|
||||
for (index, conn) in ext_conn_list.iter().enumerate() {
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
// Draw connection list item.
|
||||
let len = ext_conn_list.len();
|
||||
Self::ext_conn_item_ui(ui, conn, index, len, |ui| {
|
||||
// Draw buttons for non-default connections.
|
||||
if conn.url != ExternalConnection::DEFAULT_MAIN_URL {
|
||||
let button_rounding = View::item_rounding(index, len, true);
|
||||
View::item_button(ui, button_rounding, TRASH, None, || {
|
||||
ConnectionsConfig::remove_ext_conn(conn.id);
|
||||
});
|
||||
View::item_button(ui, Rounding::default(), PENCIL, None, || {
|
||||
self.show_add_ext_conn_modal(Some(conn.clone()), cb);
|
||||
});
|
||||
}
|
||||
Self::ext_conn_item_ui(ui, conn, index, ext_conn_size, |ui| {
|
||||
let button_rounding = View::item_rounding(index, ext_conn_size, true);
|
||||
View::item_button(ui, button_rounding, TRASH, None, || {
|
||||
ConnectionsConfig::remove_ext_conn(conn.id);
|
||||
});
|
||||
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.
|
||||
custom_button(ui);
|
||||
|
||||
if !Node::is_running() {
|
||||
// Draw button to start integrated node.
|
||||
View::item_button(ui, Rounding::default(), POWER, Some(Colors::green()), || {
|
||||
Node::start();
|
||||
});
|
||||
} else if !Node::is_starting() && !Node::is_stopping() && !Node::is_restarting() {
|
||||
// Draw button to stop integrated node.
|
||||
View::item_button(ui, Rounding::default(), POWER, Some(Colors::red()), || {
|
||||
Node::stop(false);
|
||||
});
|
||||
// Draw buttons to start/stop node.
|
||||
if Node::get_error().is_none() {
|
||||
if !Node::is_running() {
|
||||
View::item_button(ui, Rounding::default(), POWER, Some(Colors::green()), || {
|
||||
Node::start();
|
||||
});
|
||||
} else if !Node::is_starting() && !Node::is_stopping() && !Node::is_restarting() {
|
||||
View::item_button(ui, Rounding::default(), POWER, Some(Colors::red()), || {
|
||||
Node::stop(false);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let layout_size = ui.available_size();
|
||||
|
@ -163,15 +160,22 @@ impl ConnectionsContent {
|
|||
});
|
||||
|
||||
// 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
|
||||
} else if Node::not_syncing() {
|
||||
CHECK_CIRCLE
|
||||
} else {
|
||||
DOTS_THREE_CIRCLE
|
||||
};
|
||||
let status_text = format!("{} {}", status_icon, Node::get_sync_status_text());
|
||||
ui.label(RichText::new(status_text).size(15.0).color(Colors::text(false)));
|
||||
let status_text = format!("{} {}", status_icon, if has_error {
|
||||
t!("error")
|
||||
} else {
|
||||
Node::get_sync_status_text()
|
||||
});
|
||||
View::ellipsize_text(ui, status_text, 15.0, Colors::text(false));
|
||||
ui.add_space(1.0);
|
||||
|
||||
// Setup node API address text.
|
||||
|
@ -198,34 +202,32 @@ impl ConnectionsContent {
|
|||
let item_rounding = View::item_rounding(index, len, false);
|
||||
ui.painter().rect(bg_rect, item_rounding, Colors::fill(), View::item_stroke());
|
||||
|
||||
ui.vertical(|ui| {
|
||||
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
|
||||
// Draw provided buttons.
|
||||
buttons_ui(ui);
|
||||
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
|
||||
// Draw provided buttons.
|
||||
buttons_ui(ui);
|
||||
|
||||
let layout_size = ui.available_size();
|
||||
ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| {
|
||||
ui.add_space(6.0);
|
||||
ui.vertical(|ui| {
|
||||
// Draw connections URL.
|
||||
ui.add_space(4.0);
|
||||
let conn_text = format!("{} {}", GLOBE_SIMPLE, conn.url);
|
||||
View::ellipsize_text(ui, conn_text, 15.0, Colors::title(false));
|
||||
ui.add_space(1.0);
|
||||
let layout_size = ui.available_size();
|
||||
ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| {
|
||||
ui.add_space(6.0);
|
||||
ui.vertical(|ui| {
|
||||
// Draw connections URL.
|
||||
ui.add_space(4.0);
|
||||
let conn_text = format!("{} {}", GLOBE_SIMPLE, conn.url);
|
||||
View::ellipsize_text(ui, conn_text, 15.0, Colors::title(false));
|
||||
ui.add_space(1.0);
|
||||
|
||||
// Setup connection status text.
|
||||
let status_text = if let Some(available) = conn.available {
|
||||
if available {
|
||||
format!("{} {}", CHECK_CIRCLE, t!("network.available"))
|
||||
} else {
|
||||
format!("{} {}", X_CIRCLE, t!("network.not_available"))
|
||||
}
|
||||
// Setup connection status text.
|
||||
let status_text = if let Some(available) = conn.available {
|
||||
if available {
|
||||
format!("{} {}", CHECK_CIRCLE, t!("network.available"))
|
||||
} else {
|
||||
format!("{} {}", DOTS_THREE_CIRCLE, t!("network.availability_check"))
|
||||
};
|
||||
ui.label(RichText::new(status_text).size(15.0).color(Colors::gray()));
|
||||
ui.add_space(3.0);
|
||||
});
|
||||
format!("{} {}", X_CIRCLE, t!("network.not_available"))
|
||||
}
|
||||
} else {
|
||||
format!("{} {}", DOTS_THREE_CIRCLE, t!("network.availability_check"))
|
||||
};
|
||||
ui.label(RichText::new(status_text).size(15.0).color(Colors::gray()));
|
||||
ui.add_space(3.0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -12,7 +12,6 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::time::Duration;
|
||||
use egui::{Id, Margin, RichText, ScrollArea};
|
||||
use egui::scroll_area::ScrollBarVisibility;
|
||||
|
||||
|
@ -22,15 +21,15 @@ use crate::gui::icons::{ARROWS_COUNTER_CLOCKWISE, BRIEFCASE, DATABASE, DOTS_THRE
|
|||
use crate::gui::platform::PlatformCallbacks;
|
||||
use crate::gui::views::{Content, TitlePanel, View};
|
||||
use crate::gui::views::network::{ConnectionsContent, NetworkMetrics, NetworkMining, NetworkNode, NetworkSettings};
|
||||
use crate::gui::views::network::types::{NetworkTab, NetworkTabType};
|
||||
use crate::gui::views::types::{TitleContentType, TitleType};
|
||||
use crate::node::{Node, NodeError};
|
||||
use crate::gui::views::network::types::{NodeTab, NodeTabType};
|
||||
use crate::gui::views::types::{LinePosition, TitleContentType, TitleType};
|
||||
use crate::node::{Node, NodeConfig, NodeError};
|
||||
use crate::wallet::ExternalConnection;
|
||||
|
||||
/// Network content.
|
||||
pub struct NetworkContent {
|
||||
/// Current integrated node tab content.
|
||||
node_tab_content: Box<dyn NetworkTab>,
|
||||
node_tab_content: Box<dyn NodeTab>,
|
||||
/// Connections content.
|
||||
connections: ConnectionsContent,
|
||||
}
|
||||
|
@ -47,14 +46,14 @@ impl Default for NetworkContent {
|
|||
impl NetworkContent {
|
||||
pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||
let show_connections = AppConfig::show_connections_network_panel();
|
||||
let dual_panel = Content::is_dual_panel_mode(ui);
|
||||
let dual_panel = Content::is_dual_panel_mode(ui.ctx());
|
||||
|
||||
// Show title panel.
|
||||
self.title_ui(ui, show_connections);
|
||||
self.title_ui(ui, dual_panel, show_connections);
|
||||
|
||||
// Show integrated node tabs content.
|
||||
if !show_connections {
|
||||
egui::TopBottomPanel::bottom("node_tabs_content")
|
||||
egui::TopBottomPanel::bottom("node_tabs")
|
||||
.min_height(0.5)
|
||||
.resizable(false)
|
||||
.frame(egui::Frame {
|
||||
|
@ -68,15 +67,23 @@ impl NetworkContent {
|
|||
..Default::default()
|
||||
})
|
||||
.show_inside(ui, |ui| {
|
||||
ui.vertical_centered(|ui| {
|
||||
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
|
||||
self.tabs_ui(ui);
|
||||
});
|
||||
let rect = ui.available_rect_before_wrap();
|
||||
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
|
||||
self.tabs_ui(ui);
|
||||
});
|
||||
// Draw content divider line.
|
||||
let r = {
|
||||
let mut r = rect.clone();
|
||||
r.min.x -= 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")
|
||||
.resizable(false)
|
||||
.exact_width(ui.available_width())
|
||||
|
@ -86,8 +93,6 @@ impl NetworkContent {
|
|||
.show_animated_inside(ui, !show_connections, |ui| {
|
||||
egui::CentralPanel::default()
|
||||
.frame(egui::Frame {
|
||||
fill: Colors::white_or_black(false),
|
||||
stroke: View::item_stroke(),
|
||||
inner_margin: Margin {
|
||||
left: View::get_left_inset() + 4.0,
|
||||
right: View::far_right_inset_margin(ui) + 4.0,
|
||||
|
@ -97,14 +102,42 @@ impl NetworkContent {
|
|||
..Default::default()
|
||||
})
|
||||
.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.
|
||||
egui::CentralPanel::default()
|
||||
.frame(egui::Frame {
|
||||
stroke: View::item_stroke(),
|
||||
inner_margin: Margin {
|
||||
left: if show_connections {
|
||||
View::get_left_inset() + 4.0
|
||||
|
@ -117,18 +150,14 @@ impl NetworkContent {
|
|||
0.0
|
||||
},
|
||||
top: 3.0,
|
||||
bottom: if View::is_desktop() && show_connections {
|
||||
6.0
|
||||
} else {
|
||||
4.0
|
||||
},
|
||||
bottom: 4.0 + View::get_bottom_inset(),
|
||||
},
|
||||
fill: Colors::button(),
|
||||
..Default::default()
|
||||
})
|
||||
.show_inside(ui, |ui| {
|
||||
let rect = ui.available_rect_before_wrap();
|
||||
ScrollArea::vertical()
|
||||
.id_source("connections_content")
|
||||
.id_salt("connections_scroll")
|
||||
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
|
||||
.auto_shrink([false; 2])
|
||||
.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.
|
||||
if Node::is_running() {
|
||||
// Redraw after delay if node is running at non-dual-panel mode.
|
||||
if ((!dual_panel && Content::is_network_panel_open()) || dual_panel) && Node::is_running() {
|
||||
ui.ctx().request_repaint_after(Node::STATS_UPDATE_DELAY);
|
||||
} 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) {
|
||||
ui.vertical_centered(|ui| {
|
||||
// Setup spacing between tabs.
|
||||
|
@ -166,22 +204,22 @@ impl NetworkContent {
|
|||
let current_type = self.node_tab_content.get_type();
|
||||
ui.columns(4, |columns| {
|
||||
columns[0].vertical_centered_justified(|ui| {
|
||||
View::tab_button(ui, DATABASE, current_type == NetworkTabType::Node, || {
|
||||
View::tab_button(ui, DATABASE, current_type == NodeTabType::Info, |_| {
|
||||
self.node_tab_content = Box::new(NetworkNode::default());
|
||||
});
|
||||
});
|
||||
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());
|
||||
});
|
||||
});
|
||||
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());
|
||||
});
|
||||
});
|
||||
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());
|
||||
});
|
||||
});
|
||||
|
@ -190,29 +228,29 @@ impl NetworkContent {
|
|||
}
|
||||
|
||||
/// 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.
|
||||
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 not_syncing = Node::not_syncing();
|
||||
let title_content = if !show_connections {
|
||||
TitleContentType::WithSubTitle(title_text, subtitle_text, !not_syncing)
|
||||
} else {
|
||||
TitleContentType::Title(t!("network.connections").to_uppercase())
|
||||
TitleContentType::Title(t!("network.connections"))
|
||||
};
|
||||
|
||||
// Draw title panel.
|
||||
TitlePanel::new(Id::from("network_title_panel")).ui(TitleType::Single(title_content), |ui| {
|
||||
if !show_connections {
|
||||
View::title_button_big(ui, DOTS_THREE_OUTLINE_VERTICAL, |_| {
|
||||
View::title_button_big(ui, DOTS_THREE_OUTLINE_VERTICAL, |ui| {
|
||||
AppConfig::toggle_show_connections_network_panel();
|
||||
if AppConfig::show_connections_network_panel() {
|
||||
ExternalConnection::check_ext_conn_availability(None);
|
||||
ExternalConnection::check(None, ui.ctx());
|
||||
}
|
||||
});
|
||||
}
|
||||
}, |ui| {
|
||||
if !Content::is_dual_panel_mode(ui) {
|
||||
if !dual_panel {
|
||||
View::title_button_big(ui, BRIEFCASE, |_| {
|
||||
Content::toggle_network_panel();
|
||||
});
|
||||
|
@ -220,23 +258,6 @@ impl NetworkContent {
|
|||
}, 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.
|
||||
pub fn loading_ui(ui: &mut egui::Ui, text: Option<String>) {
|
||||
match text {
|
||||
|
@ -265,73 +286,98 @@ impl NetworkContent {
|
|||
AppConfig::toggle_node_autostart();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw integrated node error content.
|
||||
pub fn node_error_ui(ui: &mut egui::Ui, e: NodeError) {
|
||||
match e {
|
||||
NodeError::Storage => {
|
||||
View::center_content(ui, 156.0, |ui| {
|
||||
ui.label(RichText::new(t!("network_node.error_clean"))
|
||||
.size(16.0)
|
||||
.color(Colors::red())
|
||||
);
|
||||
ui.add_space(8.0);
|
||||
let btn_txt = format!("{} {}",
|
||||
ARROWS_COUNTER_CLOCKWISE,
|
||||
t!("network_node.resync"));
|
||||
View::action_button(ui, btn_txt, || {
|
||||
Node::clean_up_data();
|
||||
Node::start();
|
||||
});
|
||||
ui.add_space(2.0);
|
||||
/// Content to draw when node is disabled.
|
||||
fn disabled_node_ui(ui: &mut egui::Ui) {
|
||||
View::center_content(ui, 156.0, |ui| {
|
||||
let text = t!("network.disabled_server", "dots" => DOTS_THREE_OUTLINE_VERTICAL);
|
||||
ui.label(RichText::new(text)
|
||||
.size(16.0)
|
||||
.color(Colors::inactive_text())
|
||||
);
|
||||
ui.add_space(8.0);
|
||||
View::action_button(ui, format!("{} {}", POWER, t!("network.enable_node")), || {
|
||||
Node::start();
|
||||
});
|
||||
ui.add_space(2.0);
|
||||
NetworkContent::autorun_node_ui(ui);
|
||||
});
|
||||
}
|
||||
|
||||
/// Draw integrated node error content.
|
||||
pub fn node_error_ui(ui: &mut egui::Ui, e: NodeError) {
|
||||
match e {
|
||||
NodeError::Storage => {
|
||||
View::center_content(ui, 156.0, |ui| {
|
||||
ui.label(RichText::new(t!("network_node.error_clean"))
|
||||
.size(16.0)
|
||||
.color(Colors::red())
|
||||
);
|
||||
ui.add_space(8.0);
|
||||
let btn_txt = format!("{} {}",
|
||||
ARROWS_COUNTER_CLOCKWISE,
|
||||
t!("network_node.resync"));
|
||||
View::action_button(ui, btn_txt, || {
|
||||
Node::clean_up_data();
|
||||
Node::start();
|
||||
});
|
||||
return;
|
||||
}
|
||||
NodeError::P2P | NodeError::API => {
|
||||
let msg_type = match e {
|
||||
NodeError::API => "API",
|
||||
_ => "P2P"
|
||||
};
|
||||
View::center_content(ui, 106.0, |ui| {
|
||||
let text = t!(
|
||||
ui.add_space(2.0);
|
||||
});
|
||||
return;
|
||||
}
|
||||
NodeError::P2P | NodeError::API => {
|
||||
let msg_type = match e {
|
||||
NodeError::API => "API",
|
||||
_ => "P2P"
|
||||
};
|
||||
View::center_content(ui, 106.0, |ui| {
|
||||
let text = t!(
|
||||
"network_node.error_p2p_api",
|
||||
"p2p_api" => msg_type,
|
||||
"settings" => FADERS
|
||||
);
|
||||
ui.label(RichText::new(text)
|
||||
.size(16.0)
|
||||
.color(Colors::red())
|
||||
);
|
||||
ui.add_space(2.0);
|
||||
ui.label(RichText::new(text)
|
||||
.size(16.0)
|
||||
.color(Colors::red())
|
||||
);
|
||||
ui.add_space(2.0);
|
||||
});
|
||||
return;
|
||||
}
|
||||
NodeError::Configuration => {
|
||||
View::center_content(ui, 106.0, |ui| {
|
||||
ui.label(RichText::new(t!("network_node.error_config", "settings" => FADERS))
|
||||
.size(16.0)
|
||||
.color(Colors::red())
|
||||
);
|
||||
ui.add_space(8.0);
|
||||
let btn_txt = format!("{} {}",
|
||||
ARROWS_COUNTER_CLOCKWISE,
|
||||
t!("network_settings.reset"));
|
||||
View::action_button(ui, btn_txt, || {
|
||||
NodeConfig::reset_to_default();
|
||||
Node::start();
|
||||
});
|
||||
return;
|
||||
}
|
||||
NodeError::Configuration => {
|
||||
View::center_content(ui, 106.0, |ui| {
|
||||
ui.label(RichText::new(t!("network_node.error_config", "settings" => FADERS))
|
||||
.size(16.0)
|
||||
.color(Colors::red())
|
||||
);
|
||||
ui.add_space(2.0);
|
||||
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();
|
||||
});
|
||||
}
|
||||
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);
|
||||
});
|
||||
}
|
||||
ui.add_space(2.0);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -14,6 +14,7 @@
|
|||
|
||||
use egui::{RichText, Rounding, ScrollArea, vec2};
|
||||
use egui::scroll_area::ScrollBarVisibility;
|
||||
use grin_core::consensus::{DAY_HEIGHT, GRIN_BASE, HOUR_SEC, REWARD};
|
||||
use grin_servers::{DiffBlock, ServerStats};
|
||||
|
||||
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::views::{Content, View};
|
||||
use crate::gui::views::network::NetworkContent;
|
||||
use crate::gui::views::network::types::{NetworkTab, NetworkTabType};
|
||||
use crate::gui::views::network::types::{NodeTab, NodeTabType};
|
||||
use crate::node::Node;
|
||||
|
||||
/// Chain metrics tab content.
|
||||
#[derive(Default)]
|
||||
pub struct NetworkMetrics;
|
||||
|
||||
const BLOCK_REWARD: f64 = 60.0;
|
||||
// 1 year is calculated as 365 days and 6 hours (31557600).
|
||||
const YEARLY_SUPPLY: f64 = ((60 * 60 * 24 * 365) + 6 * 60 * 60) as f64;
|
||||
const BLOCK_REWARD: u64 = REWARD / GRIN_BASE;
|
||||
// 1 year as 365 days and 6 hours (31557600).
|
||||
const YEARLY_SUPPLY: u64 = (BLOCK_REWARD * DAY_HEIGHT * 365) + 6 * HOUR_SEC;
|
||||
|
||||
impl NetworkTab for NetworkMetrics {
|
||||
fn get_type(&self) -> NetworkTabType {
|
||||
NetworkTabType::Metrics
|
||||
impl NodeTab for NetworkMetrics {
|
||||
fn get_type(&self) -> NodeTabType {
|
||||
NodeTabType::Metrics
|
||||
}
|
||||
|
||||
fn ui(&mut self, ui: &mut egui::Ui, _: &dyn PlatformCallbacks) {
|
||||
// Show an error content when available.
|
||||
let node_err = Node::get_error();
|
||||
if node_err.is_some() {
|
||||
NetworkContent::node_error_ui(ui, node_err.unwrap());
|
||||
return;
|
||||
}
|
||||
|
||||
// Show message to enable node when it's not running.
|
||||
if !Node::is_running() {
|
||||
NetworkContent::disabled_node_ui(ui);
|
||||
return;
|
||||
}
|
||||
|
||||
// Show loading spinner when node is stopping.
|
||||
if Node::is_stopping() {
|
||||
NetworkContent::loading_ui(ui, None);
|
||||
return;
|
||||
}
|
||||
|
||||
// Show message when metrics are not available.
|
||||
let server_stats = Node::get_stats();
|
||||
if server_stats.is_none() || Node::is_restarting()
|
||||
|| server_stats.as_ref().unwrap().diff_stats.height == 0 {
|
||||
let stats = server_stats.as_ref().unwrap();
|
||||
if stats.diff_stats.height == 0 {
|
||||
NetworkContent::loading_ui(ui, Some(t!("network_metrics.loading")));
|
||||
return;
|
||||
}
|
||||
|
||||
ui.add_space(1.0);
|
||||
ui.vertical_centered(|ui| {
|
||||
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
|
||||
let stats = server_stats.as_ref().unwrap();
|
||||
// Show emission and difficulty info.
|
||||
info_ui(ui, stats);
|
||||
// Show difficulty adjustment window blocks.
|
||||
blocks_ui(ui, stats);
|
||||
});
|
||||
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
|
||||
// Show emission and difficulty info.
|
||||
info_ui(ui, stats);
|
||||
// Show difficulty adjustment window blocks.
|
||||
blocks_ui(ui, stats);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const BLOCK_ITEM_HEIGHT: f32 = 78.0;
|
||||
|
||||
/// Draw emission and difficulty info.
|
||||
fn info_ui(ui: &mut egui::Ui, stats: &ServerStats) {
|
||||
// Show emission info.
|
||||
View::sub_title(ui, format!("{} {}", COINS, t!("network_metrics.emission")));
|
||||
ui.columns(3, |columns| {
|
||||
let supply = stats.header_stats.height as f64 * BLOCK_REWARD;
|
||||
let rate = (YEARLY_SUPPLY * 100.0) / supply;
|
||||
let supply = stats.header_stats.height * BLOCK_REWARD;
|
||||
let rate = (YEARLY_SUPPLY * 100) / supply;
|
||||
|
||||
columns[0].vertical_centered(|ui| {
|
||||
View::rounded_box(ui,
|
||||
format!("{}ツ", BLOCK_REWARD),
|
||||
t!("network_metrics.reward"),
|
||||
[true, false, true, false]);
|
||||
View::label_box(ui,
|
||||
format!("{}ツ", BLOCK_REWARD),
|
||||
t!("network_metrics.reward"),
|
||||
[true, false, true, false]);
|
||||
});
|
||||
columns[1].vertical_centered(|ui| {
|
||||
View::rounded_box(ui,
|
||||
format!("{:.2}%", rate),
|
||||
t!("network_metrics.inflation"),
|
||||
[false, false, false, false]);
|
||||
View::label_box(ui,
|
||||
format!("{:.2}%", rate),
|
||||
t!("network_metrics.inflation"),
|
||||
[false, false, false, false]);
|
||||
});
|
||||
columns[2].vertical_centered(|ui| {
|
||||
View::rounded_box(ui,
|
||||
supply.to_string(),
|
||||
t!("network_metrics.supply"),
|
||||
[false, true, false, true]);
|
||||
View::label_box(ui,
|
||||
supply.to_string(),
|
||||
t!("network_metrics.supply"),
|
||||
[false, true, false, true]);
|
||||
});
|
||||
});
|
||||
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));
|
||||
ui.columns(3, |columns| {
|
||||
columns[0].vertical_centered(|ui| {
|
||||
View::rounded_box(ui,
|
||||
stats.diff_stats.height.to_string(),
|
||||
t!("network_node.height"),
|
||||
[true, false, true, false]);
|
||||
View::label_box(ui,
|
||||
stats.diff_stats.height.to_string(),
|
||||
t!("network_node.height"),
|
||||
[true, false, true, false]);
|
||||
});
|
||||
columns[1].vertical_centered(|ui| {
|
||||
View::rounded_box(ui,
|
||||
format!("{}s", stats.diff_stats.average_block_time),
|
||||
t!("network_metrics.block_time"),
|
||||
[false, false, false, false]);
|
||||
View::label_box(ui,
|
||||
format!("{}s", stats.diff_stats.average_block_time),
|
||||
t!("network_metrics.block_time"),
|
||||
[false, false, false, false]);
|
||||
});
|
||||
columns[2].vertical_centered(|ui| {
|
||||
View::rounded_box(ui,
|
||||
stats.diff_stats.average_difficulty.to_string(),
|
||||
t!("network_node.difficulty"),
|
||||
[false, true, false, true]);
|
||||
View::label_box(ui,
|
||||
stats.diff_stats.average_difficulty.to_string(),
|
||||
t!("network_node.difficulty"),
|
||||
[false, true, false, true]);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const BLOCK_ITEM_HEIGHT: f32 = 77.0;
|
||||
|
||||
/// Draw difficulty adjustment window blocks content.
|
||||
fn blocks_ui(ui: &mut egui::Ui, stats: &ServerStats) {
|
||||
let blocks_size = stats.diff_stats.last_blocks.len();
|
||||
ui.add_space(4.0);
|
||||
ScrollArea::vertical()
|
||||
.id_source("difficulty_scroll")
|
||||
.id_salt("mining_difficulty_scroll")
|
||||
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
|
||||
.auto_shrink([false; 2])
|
||||
.stick_to_bottom(true)
|
||||
|
@ -151,11 +128,8 @@ fn blocks_ui(ui: &mut egui::Ui, stats: &ServerStats) {
|
|||
BLOCK_ITEM_HEIGHT,
|
||||
blocks_size,
|
||||
|ui, row_range| {
|
||||
ui.add_space(4.0);
|
||||
for index in row_range {
|
||||
// Add space before the first item.
|
||||
if index == 0 {
|
||||
ui.add_space(4.0);
|
||||
}
|
||||
let db = stats.diff_stats.last_blocks.get(index).unwrap();
|
||||
block_item_ui(ui, db, View::item_rounding(index, blocks_size, false));
|
||||
}
|
||||
|
@ -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) {
|
||||
let mut rect = ui.available_rect_before_wrap();
|
||||
rect.set_height(BLOCK_ITEM_HEIGHT);
|
||||
ui.allocate_ui_at_rect(rect, |ui| {
|
||||
ui.allocate_ui(rect.size(), |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.add_space(6.0);
|
||||
ui.vertical(|ui| {
|
||||
ui.add_space(3.0);
|
||||
ui.add_space(4.0);
|
||||
|
||||
// Draw round background.
|
||||
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.
|
||||
ui.horizontal(|ui| {
|
||||
ui.add_space(7.0);
|
||||
ui.add_space(8.0);
|
||||
ui.label(RichText::new(db.block_hash.to_string())
|
||||
.color(Colors::white_or_black(true))
|
||||
.size(17.0));
|
||||
});
|
||||
// Draw block difficulty and height.
|
||||
ui.horizontal(|ui| {
|
||||
ui.add_space(6.0);
|
||||
ui.add_space(7.0);
|
||||
let diff_text = format!("{} {} {} {}",
|
||||
CUBE_TRANSPARENT,
|
||||
db.difficulty,
|
||||
AT,
|
||||
db.block_height);
|
||||
ui.label(RichText::new(diff_text).color(Colors::title(false)).size(16.0));
|
||||
ui.label(RichText::new(diff_text)
|
||||
.color(Colors::title(false))
|
||||
.size(15.0));
|
||||
});
|
||||
// Draw block date.
|
||||
ui.horizontal(|ui| {
|
||||
ui.add_space(6.0);
|
||||
ui.add_space(7.0);
|
||||
let block_time = View::format_time(db.time as i64);
|
||||
ui.label(RichText::new(format!("{} {}s {} {}",
|
||||
TIMER,
|
||||
|
@ -205,7 +181,7 @@ fn block_item_ui(ui: &mut egui::Ui, db: &DiffBlock, rounding: Rounding) {
|
|||
HOURGLASS_LOW,
|
||||
block_time))
|
||||
.color(Colors::gray())
|
||||
.size(16.0));
|
||||
.size(15.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::network::NetworkContent;
|
||||
use crate::gui::views::network::setup::StratumSetup;
|
||||
use crate::gui::views::network::types::{NetworkTab, NetworkTabType};
|
||||
use crate::gui::views::network::types::{NodeTab, NodeTabType};
|
||||
use crate::node::{Node, NodeConfig};
|
||||
use crate::wallet::WalletConfig;
|
||||
|
||||
/// Mining tab content.
|
||||
pub struct NetworkMining {
|
||||
/// Stratum server setup content.
|
||||
stratum_server_setup: StratumSetup,
|
||||
|
||||
/// Wallet name for rewards.
|
||||
wallet_name: String,
|
||||
}
|
||||
|
||||
impl Default for NetworkMining {
|
||||
fn default() -> Self {
|
||||
let wallet_name = if let Some(id) = NodeConfig::get_stratum_wallet_id() {
|
||||
WalletConfig::name_by_id(id).unwrap_or("-".to_string())
|
||||
} else {
|
||||
"-".to_string()
|
||||
};
|
||||
Self {
|
||||
stratum_server_setup: StratumSetup::default(),
|
||||
wallet_name,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl NetworkTab for NetworkMining {
|
||||
fn get_type(&self) -> NetworkTabType {
|
||||
NetworkTabType::Mining
|
||||
impl NodeTab for NetworkMining {
|
||||
fn get_type(&self) -> NodeTabType {
|
||||
NodeTabType::Mining
|
||||
}
|
||||
|
||||
fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||
// Show an error content when available.
|
||||
let node_err = Node::get_error();
|
||||
if node_err.is_some() {
|
||||
NetworkContent::node_error_ui(ui, node_err.unwrap());
|
||||
return;
|
||||
}
|
||||
|
||||
// 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 {
|
||||
if Node::is_stratum_starting() || Node::get_sync_status().unwrap() != SyncStatus::NoSync {
|
||||
NetworkContent::loading_ui(ui, Some(t!("network_mining.loading")));
|
||||
return;
|
||||
}
|
||||
|
@ -87,15 +55,13 @@ impl NetworkTab for NetworkMining {
|
|||
let stratum_stats = Node::get_stratum_stats();
|
||||
if !stratum_stats.is_running {
|
||||
ScrollArea::vertical()
|
||||
.id_source("stratum_setup_scroll")
|
||||
.id_salt("stratum_setup_scroll")
|
||||
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
|
||||
.auto_shrink([false; 2])
|
||||
.show(ui, |ui| {
|
||||
ui.add_space(1.0);
|
||||
ui.vertical_centered(|ui| {
|
||||
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
|
||||
self.stratum_server_setup.ui(ui, cb);
|
||||
});
|
||||
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
|
||||
self.stratum_server_setup.ui(ui, cb);
|
||||
});
|
||||
});
|
||||
return;
|
||||
|
@ -108,16 +74,19 @@ impl NetworkTab for NetworkMining {
|
|||
ui.columns(2, |columns| {
|
||||
columns[0].vertical_centered(|ui| {
|
||||
let (stratum_addr, stratum_port) = NodeConfig::get_stratum_address();
|
||||
View::rounded_box(ui,
|
||||
format!("{}:{}", stratum_addr, stratum_port),
|
||||
t!("network_mining.address"),
|
||||
[true, false, true, false]);
|
||||
View::label_box(ui,
|
||||
format!("{}:{}", stratum_addr, stratum_port),
|
||||
t!("network_mining.address"),
|
||||
[true, false, true, false]);
|
||||
});
|
||||
columns[1].vertical_centered(|ui| {
|
||||
View::rounded_box(ui,
|
||||
self.wallet_name.clone(),
|
||||
t!("network_mining.rewards_wallet"),
|
||||
[false, true, false, true]);
|
||||
View::label_box(ui,
|
||||
self.stratum_server_setup
|
||||
.wallet_name
|
||||
.clone()
|
||||
.unwrap_or("-".to_string()),
|
||||
t!("network_mining.rewards_wallet"),
|
||||
[false, true, false, true]);
|
||||
});
|
||||
});
|
||||
ui.add_space(4.0);
|
||||
|
@ -131,10 +100,10 @@ impl NetworkTab for NetworkMining {
|
|||
} else {
|
||||
"-".into()
|
||||
};
|
||||
View::rounded_box(ui,
|
||||
difficulty,
|
||||
t!("network_node.difficulty"),
|
||||
[true, false, true, false]);
|
||||
View::label_box(ui,
|
||||
difficulty,
|
||||
t!("network_node.difficulty"),
|
||||
[true, false, true, false]);
|
||||
});
|
||||
columns[1].vertical_centered(|ui| {
|
||||
let block_height = if stratum_stats.block_height > 0 {
|
||||
|
@ -142,10 +111,10 @@ impl NetworkTab for NetworkMining {
|
|||
} else {
|
||||
"-".into()
|
||||
};
|
||||
View::rounded_box(ui,
|
||||
block_height,
|
||||
t!("network_node.header"),
|
||||
[false, false, false, false]);
|
||||
View::label_box(ui,
|
||||
block_height,
|
||||
t!("network_node.header"),
|
||||
[false, false, false, false]);
|
||||
});
|
||||
columns[2].vertical_centered(|ui| {
|
||||
let hashrate = if stratum_stats.network_hashrate > 0.0 {
|
||||
|
@ -153,10 +122,10 @@ impl NetworkTab for NetworkMining {
|
|||
} else {
|
||||
"-".into()
|
||||
};
|
||||
View::rounded_box(ui,
|
||||
hashrate,
|
||||
t!("network_mining.hashrate", "bits" => stratum_stats.edge_bits),
|
||||
[false, true, false, true]);
|
||||
View::label_box(ui,
|
||||
hashrate,
|
||||
t!("network_mining.hashrate", "bits" => stratum_stats.edge_bits),
|
||||
[false, true, false, true]);
|
||||
});
|
||||
});
|
||||
ui.add_space(4.0);
|
||||
|
@ -165,17 +134,17 @@ impl NetworkTab for NetworkMining {
|
|||
View::sub_title(ui, format!("{} {}", CPU, t!("network_mining.miners")));
|
||||
ui.columns(2, |columns| {
|
||||
columns[0].vertical_centered(|ui| {
|
||||
View::rounded_box(ui,
|
||||
stratum_stats.num_workers.to_string(),
|
||||
t!("network_mining.devices"),
|
||||
[true, false, true, false]);
|
||||
View::label_box(ui,
|
||||
stratum_stats.num_workers.to_string(),
|
||||
t!("network_mining.devices"),
|
||||
[true, false, true, false]);
|
||||
});
|
||||
|
||||
columns[1].vertical_centered(|ui| {
|
||||
View::rounded_box(ui,
|
||||
stratum_stats.blocks_found.to_string(),
|
||||
t!("network_mining.blocks_found"),
|
||||
[false, true, false, true]);
|
||||
View::label_box(ui,
|
||||
stratum_stats.blocks_found.to_string(),
|
||||
t!("network_mining.blocks_found"),
|
||||
[false, true, false, true]);
|
||||
});
|
||||
});
|
||||
ui.add_space(4.0);
|
||||
|
@ -187,7 +156,7 @@ impl NetworkTab for NetworkMining {
|
|||
View::horizontal_line(ui, Colors::item_stroke());
|
||||
ui.add_space(4.0);
|
||||
ScrollArea::vertical()
|
||||
.id_source("stratum_workers_scroll")
|
||||
.id_salt("stratum_workers_scroll")
|
||||
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
|
||||
.auto_shrink([false; 2])
|
||||
.show_rows(
|
||||
|
|
|
@ -119,7 +119,7 @@ impl ExternalConnectionModal {
|
|||
});
|
||||
columns[1].vertical_centered_justified(|ui| {
|
||||
// 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") {
|
||||
self.ext_node_url_edit = format!("http://{}", self.ext_node_url_edit)
|
||||
}
|
||||
|
@ -139,7 +139,7 @@ impl ExternalConnectionModal {
|
|||
ext_conn.id = id;
|
||||
}
|
||||
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);
|
||||
|
||||
// Close modal.
|
||||
|
@ -150,10 +150,17 @@ impl ExternalConnectionModal {
|
|||
modal.close();
|
||||
}
|
||||
};
|
||||
|
||||
// Handle Enter key press.
|
||||
let mut enter = false;
|
||||
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")
|
||||
} else {
|
||||
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::platform::PlatformCallbacks;
|
||||
use crate::gui::views::{Content, View};
|
||||
use crate::gui::views::network::NetworkContent;
|
||||
use crate::gui::views::network::types::{NetworkTab, NetworkTabType};
|
||||
use crate::gui::views::network::types::{NodeTab, NodeTabType};
|
||||
use crate::node::{Node, NodeConfig};
|
||||
|
||||
/// Integrated node tab content.
|
||||
#[derive(Default)]
|
||||
pub struct NetworkNode;
|
||||
|
||||
impl NetworkTab for NetworkNode {
|
||||
fn get_type(&self) -> NetworkTabType {
|
||||
NetworkTabType::Node
|
||||
impl NodeTab for NetworkNode {
|
||||
fn get_type(&self) -> NodeTabType {
|
||||
NodeTabType::Info
|
||||
}
|
||||
|
||||
fn ui(&mut self, ui: &mut egui::Ui, _: &dyn PlatformCallbacks) {
|
||||
// Show an error content when available.
|
||||
let node_err = Node::get_error();
|
||||
if node_err.is_some() {
|
||||
NetworkContent::node_error_ui(ui, node_err.unwrap());
|
||||
return;
|
||||
}
|
||||
|
||||
// Show message to enable node when it's not running.
|
||||
if !Node::is_running() {
|
||||
NetworkContent::disabled_node_ui(ui);
|
||||
return;
|
||||
}
|
||||
|
||||
// Show loading spinner when stats are not available.
|
||||
let server_stats = Node::get_stats();
|
||||
if server_stats.is_none() || Node::is_restarting() || Node::is_stopping() {
|
||||
NetworkContent::loading_ui(ui, None);
|
||||
return;
|
||||
}
|
||||
|
||||
ScrollArea::vertical()
|
||||
.id_source("integrated_node")
|
||||
.id_salt("integrated_node_info_scroll")
|
||||
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
|
||||
.auto_shrink([false; 2])
|
||||
.show(ui, |ui| {
|
||||
ui.add_space(2.0);
|
||||
ui.vertical_centered(|ui| {
|
||||
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
|
||||
// Show node stats content.
|
||||
node_stats_ui(ui);
|
||||
});
|
||||
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
|
||||
// Show node stats content.
|
||||
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")));
|
||||
ui.columns(2, |columns| {
|
||||
columns[0].vertical_centered(|ui| {
|
||||
View::rounded_box(ui,
|
||||
stats.header_stats.last_block_h.to_string(),
|
||||
t!("network_node.hash"),
|
||||
[true, false, false, false]);
|
||||
View::label_box(ui,
|
||||
stats.header_stats.last_block_h.to_string(),
|
||||
t!("network_node.hash"),
|
||||
[true, false, false, false]);
|
||||
});
|
||||
columns[1].vertical_centered(|ui| {
|
||||
View::rounded_box(ui,
|
||||
stats.header_stats.height.to_string(),
|
||||
t!("network_node.height"),
|
||||
[false, true, false, false]);
|
||||
View::label_box(ui,
|
||||
stats.header_stats.height.to_string(),
|
||||
t!("network_node.height"),
|
||||
[false, true, false, false]);
|
||||
});
|
||||
});
|
||||
ui.columns(2, |columns| {
|
||||
columns[0].vertical_centered(|ui| {
|
||||
View::rounded_box(ui,
|
||||
stats.header_stats.total_difficulty.to_string(),
|
||||
t!("network_node.difficulty"),
|
||||
[false, false, true, false]);
|
||||
View::label_box(ui,
|
||||
stats.header_stats.total_difficulty.to_string(),
|
||||
t!("network_node.difficulty"),
|
||||
[false, false, true, false]);
|
||||
});
|
||||
columns[1].vertical_centered(|ui| {
|
||||
let h_ts = stats.header_stats.latest_timestamp.timestamp();
|
||||
let h_time = View::format_time(h_ts);
|
||||
View::rounded_box(ui,
|
||||
h_time,
|
||||
t!("network_node.time"),
|
||||
[false, false, false, true]);
|
||||
View::label_box(ui,
|
||||
h_time,
|
||||
t!("network_node.time"),
|
||||
[false, false, false, true]);
|
||||
});
|
||||
});
|
||||
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")));
|
||||
ui.columns(2, |columns| {
|
||||
columns[0].vertical_centered(|ui| {
|
||||
View::rounded_box(ui,
|
||||
stats.chain_stats.last_block_h.to_string(),
|
||||
t!("network_node.hash"),
|
||||
[true, false, false, false]);
|
||||
View::label_box(ui,
|
||||
stats.chain_stats.last_block_h.to_string(),
|
||||
t!("network_node.hash"),
|
||||
[true, false, false, false]);
|
||||
});
|
||||
columns[1].vertical_centered(|ui| {
|
||||
View::rounded_box(ui,
|
||||
stats.chain_stats.height.to_string(),
|
||||
t!("network_node.height"),
|
||||
[false, true, false, false]);
|
||||
View::label_box(ui,
|
||||
stats.chain_stats.height.to_string(),
|
||||
t!("network_node.height"),
|
||||
[false, true, false, false]);
|
||||
});
|
||||
});
|
||||
ui.columns(2, |columns| {
|
||||
columns[0].vertical_centered(|ui| {
|
||||
View::rounded_box(ui,
|
||||
stats.chain_stats.total_difficulty.to_string(),
|
||||
t!("network_node.difficulty"),
|
||||
[false, false, true, false]);
|
||||
View::label_box(ui,
|
||||
stats.chain_stats.total_difficulty.to_string(),
|
||||
t!("network_node.difficulty"),
|
||||
[false, false, true, false]);
|
||||
});
|
||||
columns[1].vertical_centered(|ui| {
|
||||
let b_ts = stats.chain_stats.latest_timestamp.timestamp();
|
||||
let b_time = View::format_time(b_ts);
|
||||
View::rounded_box(ui,
|
||||
b_time,
|
||||
t!("network_node.time"),
|
||||
[false, false, false, true]);
|
||||
View::label_box(ui,
|
||||
b_time,
|
||||
t!("network_node.time"),
|
||||
[false, false, false, true]);
|
||||
});
|
||||
});
|
||||
ui.add_space(5.0);
|
||||
|
@ -151,10 +128,10 @@ fn node_stats_ui(ui: &mut egui::Ui) {
|
|||
None => "0 (0)".to_string(),
|
||||
Some(tx) => format!("{} ({})", tx.tx_pool_size, tx.tx_pool_kernels)
|
||||
};
|
||||
View::rounded_box(ui,
|
||||
tx_stat,
|
||||
t!("network_node.main_pool"),
|
||||
[true, false, false, false]);
|
||||
View::label_box(ui,
|
||||
tx_stat,
|
||||
t!("network_node.main_pool"),
|
||||
[true, false, false, false]);
|
||||
});
|
||||
columns[1].vertical_centered(|ui| {
|
||||
let stem_tx_stat = match &stats.tx_stats {
|
||||
|
@ -163,24 +140,24 @@ fn node_stats_ui(ui: &mut egui::Ui) {
|
|||
stx.stem_pool_size,
|
||||
stx.stem_pool_kernels)
|
||||
};
|
||||
View::rounded_box(ui,
|
||||
stem_tx_stat,
|
||||
t!("network_node.stem_pool"),
|
||||
[false, true, false, false]);
|
||||
View::label_box(ui,
|
||||
stem_tx_stat,
|
||||
t!("network_node.stem_pool"),
|
||||
[false, true, false, false]);
|
||||
});
|
||||
});
|
||||
ui.columns(2, |columns| {
|
||||
columns[0].vertical_centered(|ui| {
|
||||
View::rounded_box(ui,
|
||||
stats.disk_usage_gb.to_string(),
|
||||
t!("network_node.size"),
|
||||
[false, false, true, false]);
|
||||
View::label_box(ui,
|
||||
stats.disk_usage_gb.to_string(),
|
||||
t!("network_node.size"),
|
||||
[false, false, true, false]);
|
||||
});
|
||||
columns[1].vertical_centered(|ui| {
|
||||
let peers_txt = format!("{} ({})",
|
||||
stats.peer_count,
|
||||
NodeConfig::get_max_outbound_peers());
|
||||
View::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);
|
||||
|
@ -196,23 +173,27 @@ fn node_stats_ui(ui: &mut egui::Ui) {
|
|||
}
|
||||
}
|
||||
|
||||
const PEER_ITEM_HEIGHT: f32 = 77.0;
|
||||
|
||||
/// Draw connected peer info item.
|
||||
fn peer_item_ui(ui: &mut egui::Ui, peer: &PeerStats, rounding: Rounding) {
|
||||
let mut rect = ui.available_rect_before_wrap();
|
||||
rect.set_height(79.0);
|
||||
ui.allocate_ui_at_rect(rect, |ui| {
|
||||
rect.set_height(PEER_ITEM_HEIGHT);
|
||||
ui.allocate_ui(rect.size(), |ui| {
|
||||
ui.vertical(|ui| {
|
||||
ui.add_space(4.0);
|
||||
|
||||
// Draw round background.
|
||||
ui.painter().rect(rect, rounding, Colors::white_or_black(false), View::item_stroke());
|
||||
ui.painter().rect(rect, rounding, Colors::fill_lite(), View::item_stroke());
|
||||
|
||||
// Draw peer address
|
||||
// Draw IP address.
|
||||
ui.horizontal(|ui| {
|
||||
ui.add_space(7.0);
|
||||
ui.label(RichText::new(&peer.addr).color(Colors::white_or_black(true)).size(17.0));
|
||||
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.add_space(6.0);
|
||||
let diff_text = format!("{} {} {} {}",
|
||||
|
@ -220,13 +201,17 @@ fn peer_item_ui(ui: &mut egui::Ui, peer: &PeerStats, rounding: Rounding) {
|
|||
peer.total_difficulty,
|
||||
AT,
|
||||
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.add_space(6.0);
|
||||
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);
|
||||
|
|
|
@ -20,7 +20,7 @@ use crate::gui::icons::ARROW_COUNTER_CLOCKWISE;
|
|||
use crate::gui::platform::PlatformCallbacks;
|
||||
use crate::gui::views::{Modal, Content, View};
|
||||
use crate::gui::views::network::setup::{DandelionSetup, NodeSetup, P2PSetup, PoolSetup, StratumSetup};
|
||||
use crate::gui::views::network::types::{NetworkTab, NetworkTabType};
|
||||
use crate::gui::views::network::types::{NodeTab, NodeTabType};
|
||||
use crate::gui::views::types::{ModalContainer, ModalPosition};
|
||||
use crate::node::{Node, NodeConfig};
|
||||
|
||||
|
@ -42,7 +42,7 @@ pub struct NetworkSettings {
|
|||
}
|
||||
|
||||
/// Identifier for settings reset confirmation [`Modal`].
|
||||
pub const RESET_SETTINGS_MODAL: &'static str = "reset_settings";
|
||||
pub const RESET_SETTINGS_CONFIRMATION_MODAL: &'static str = "reset_settings_confirmation";
|
||||
|
||||
impl Default for NetworkSettings {
|
||||
fn default() -> Self {
|
||||
|
@ -53,7 +53,7 @@ impl Default for NetworkSettings {
|
|||
pool: PoolSetup::default(),
|
||||
dandelion: DandelionSetup::default(),
|
||||
modal_ids: vec![
|
||||
RESET_SETTINGS_MODAL
|
||||
RESET_SETTINGS_CONFIRMATION_MODAL
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -69,15 +69,15 @@ impl ModalContainer for NetworkSettings {
|
|||
modal: &Modal,
|
||||
_: &dyn PlatformCallbacks) {
|
||||
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 {
|
||||
fn get_type(&self) -> NetworkTabType {
|
||||
NetworkTabType::Settings
|
||||
impl NodeTab for NetworkSettings {
|
||||
fn get_type(&self) -> NodeTabType {
|
||||
NodeTabType::Settings
|
||||
}
|
||||
|
||||
fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||
|
@ -85,7 +85,7 @@ impl NetworkTab for NetworkSettings {
|
|||
self.current_modal_ui(ui, cb);
|
||||
|
||||
ScrollArea::vertical()
|
||||
.id_source("network_settings")
|
||||
.id_salt("node_settings_scroll")
|
||||
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
|
||||
.auto_shrink([false; 2])
|
||||
.show(ui, |ui| {
|
||||
|
@ -210,9 +210,9 @@ fn reset_settings_ui(ui: &mut egui::Ui) {
|
|||
t!("network_settings.reset_settings"));
|
||||
View::action_button(ui, button_text, || {
|
||||
// Show modal to confirm settings reset.
|
||||
Modal::new(RESET_SETTINGS_MODAL)
|
||||
Modal::new(RESET_SETTINGS_CONFIRMATION_MODAL)
|
||||
.position(ModalPosition::Center)
|
||||
.title(t!("modal.confirmation"))
|
||||
.title(t!("confirmation"))
|
||||
.show();
|
||||
});
|
||||
|
||||
|
|
|
@ -141,7 +141,7 @@ impl DandelionSetup {
|
|||
ui.add_space(6.0);
|
||||
|
||||
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.
|
||||
self.epoch_edit = epoch;
|
||||
// Show epoch setup modal.
|
||||
|
@ -193,6 +193,11 @@ impl DandelionSetup {
|
|||
}
|
||||
};
|
||||
|
||||
// Continue on Enter key press.
|
||||
View::on_enter_key(ui, || {
|
||||
on_save();
|
||||
});
|
||||
|
||||
ui.columns(2, |columns| {
|
||||
columns[0].vertical_centered_justified(|ui| {
|
||||
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
|
||||
|
@ -218,8 +223,7 @@ impl DandelionSetup {
|
|||
ui.add_space(6.0);
|
||||
|
||||
let embargo = NodeConfig::get_dandelion_embargo();
|
||||
View::button(ui, format!("{} {}", TIMER, embargo.clone()), Colors::button(), || {
|
||||
// Setup values for modal.
|
||||
View::button(ui, format!("{} {}", TIMER, &embargo), Colors::white_or_black(false), || {
|
||||
self.embargo_edit = embargo;
|
||||
// Show embargo setup modal.
|
||||
Modal::new(EMBARGO_MODAL)
|
||||
|
@ -270,6 +274,11 @@ impl DandelionSetup {
|
|||
}
|
||||
};
|
||||
|
||||
// Continue on Enter key press.
|
||||
View::on_enter_key(ui, || {
|
||||
on_save();
|
||||
});
|
||||
|
||||
ui.columns(2, |columns| {
|
||||
columns[0].vertical_centered_justified(|ui| {
|
||||
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
|
||||
|
@ -294,10 +303,10 @@ impl DandelionSetup {
|
|||
);
|
||||
ui.add_space(6.0);
|
||||
|
||||
let agg = NodeConfig::get_dandelion_aggregation();
|
||||
View::button(ui, format!("{} {}", CLOCK_COUNTDOWN, agg.clone()), Colors::button(), || {
|
||||
let ag = NodeConfig::get_dandelion_aggregation();
|
||||
View::button(ui, format!("{} {}", CLOCK_COUNTDOWN, &ag), Colors::white_or_black(false), || {
|
||||
// Setup values for modal.
|
||||
self.aggregation_edit = agg;
|
||||
self.aggregation_edit = ag;
|
||||
// Show aggregation setup modal.
|
||||
Modal::new(AGGREGATION_MODAL)
|
||||
.position(ModalPosition::CenterTop)
|
||||
|
@ -347,6 +356,11 @@ impl DandelionSetup {
|
|||
}
|
||||
};
|
||||
|
||||
// Continue on Enter key press.
|
||||
View::on_enter_key(ui, || {
|
||||
on_save();
|
||||
});
|
||||
|
||||
ui.columns(2, |columns| {
|
||||
columns[0].vertical_centered_justified(|ui| {
|
||||
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
|
||||
|
@ -372,7 +386,7 @@ impl DandelionSetup {
|
|||
ui.add_space(6.0);
|
||||
|
||||
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.
|
||||
self.stem_prob_edit = stem_prob;
|
||||
// Show stem probability setup modal.
|
||||
|
@ -424,6 +438,11 @@ impl DandelionSetup {
|
|||
}
|
||||
};
|
||||
|
||||
// Continue on Enter key press.
|
||||
View::on_enter_key(ui, || {
|
||||
on_save();
|
||||
});
|
||||
|
||||
ui.columns(2, |columns| {
|
||||
columns[0].vertical_centered_justified(|ui| {
|
||||
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
|
||||
|
|
|
@ -255,7 +255,7 @@ impl NodeSetup {
|
|||
ui.add_space(6.0);
|
||||
|
||||
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.
|
||||
self.api_port_edit = port;
|
||||
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) {
|
||||
ui.add_space(6.0);
|
||||
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);
|
||||
|
||||
// Draw API port text edit.
|
||||
|
@ -308,7 +310,7 @@ impl NodeSetup {
|
|||
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
|
||||
|
||||
// Save button callback.
|
||||
let on_save = || {
|
||||
let mut on_save = || {
|
||||
// Check if port is available.
|
||||
let (api_ip, _) = NodeConfig::get_api_ip_port();
|
||||
let available = NodeConfig::is_api_port_available(&api_ip, &self.api_port_edit);
|
||||
|
@ -328,6 +330,11 @@ impl NodeSetup {
|
|||
}
|
||||
};
|
||||
|
||||
// Continue on Enter key press.
|
||||
View::on_enter_key(ui, || {
|
||||
on_save();
|
||||
});
|
||||
|
||||
ui.columns(2, |columns| {
|
||||
columns[0].vertical_centered_justified(|ui| {
|
||||
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
|
||||
|
@ -356,8 +363,8 @@ impl NodeSetup {
|
|||
ui.add_space(6.0);
|
||||
|
||||
let secret_value = match modal_id {
|
||||
API_SECRET_MODAL => NodeConfig::get_api_secret(),
|
||||
_ => NodeConfig::get_foreign_api_secret()
|
||||
API_SECRET_MODAL => NodeConfig::get_api_secret(false),
|
||||
_ => NodeConfig::get_api_secret(true)
|
||||
};
|
||||
|
||||
let secret_text = if secret_value.is_some() {
|
||||
|
@ -366,7 +373,7 @@ impl NodeSetup {
|
|||
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.
|
||||
self.secret_edit = secret_value.unwrap_or("".to_string());
|
||||
// Show secret edit modal.
|
||||
|
@ -425,6 +432,11 @@ impl NodeSetup {
|
|||
modal.close();
|
||||
};
|
||||
|
||||
// Continue on Enter key press.
|
||||
View::on_enter_key(ui, || {
|
||||
on_save();
|
||||
});
|
||||
|
||||
ui.columns(2, |columns| {
|
||||
columns[0].vertical_centered_justified(|ui| {
|
||||
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
|
||||
|
@ -449,7 +461,9 @@ impl NodeSetup {
|
|||
ui.add_space(6.0);
|
||||
|
||||
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.
|
||||
self.ftl_edit = ftl;
|
||||
// Show ftl value setup modal.
|
||||
|
@ -505,6 +519,11 @@ impl NodeSetup {
|
|||
}
|
||||
};
|
||||
|
||||
// Continue on Enter key press.
|
||||
View::on_enter_key(ui, || {
|
||||
on_save();
|
||||
});
|
||||
|
||||
ui.columns(2, |columns| {
|
||||
columns[0].vertical_centered_justified(|ui| {
|
||||
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
|
||||
|
|
|
@ -91,7 +91,7 @@ impl Default for P2PSetup {
|
|||
fn default() -> Self {
|
||||
let port = NodeConfig::get_p2p_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()
|
||||
.map(|s| s.to_string())
|
||||
.collect();
|
||||
|
@ -246,17 +246,19 @@ impl P2PSetup {
|
|||
ui.add_space(6.0);
|
||||
|
||||
let port = NodeConfig::get_p2p_port();
|
||||
View::button(ui, format!("{} {}", PLUG, port.clone()), Colors::button(), || {
|
||||
// Setup values for modal.
|
||||
self.port_edit = port;
|
||||
self.port_available_edit = self.is_port_available;
|
||||
// Show p2p port modal.
|
||||
Modal::new(PORT_MODAL)
|
||||
.position(ModalPosition::CenterTop)
|
||||
.title(t!("network_settings.change_value"))
|
||||
.show();
|
||||
cb.show_keyboard();
|
||||
});
|
||||
View::button(ui,
|
||||
format!("{} {}", PLUG, &port),
|
||||
Colors::white_or_black(false), || {
|
||||
// Setup values for modal.
|
||||
self.port_edit = port;
|
||||
self.port_available_edit = self.is_port_available;
|
||||
// Show p2p port modal.
|
||||
Modal::new(PORT_MODAL)
|
||||
.position(ModalPosition::CenterTop)
|
||||
.title(t!("network_settings.change_value"))
|
||||
.show();
|
||||
cb.show_keyboard();
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
|
||||
// Show error when p2p port is unavailable.
|
||||
|
@ -298,7 +300,7 @@ impl P2PSetup {
|
|||
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
|
||||
|
||||
// Save button callback.
|
||||
let on_save = || {
|
||||
let mut on_save = || {
|
||||
// Check if port is available.
|
||||
let available = NodeConfig::is_p2p_port_available(&self.port_edit);
|
||||
self.port_available_edit = available;
|
||||
|
@ -306,17 +308,20 @@ impl P2PSetup {
|
|||
// Save port at config if it's available.
|
||||
if available {
|
||||
NodeConfig::save_p2p_port(self.port_edit.parse::<u16>().unwrap());
|
||||
|
||||
if Node::is_running() {
|
||||
Node::restart();
|
||||
}
|
||||
|
||||
self.is_port_available = true;
|
||||
cb.hide_keyboard();
|
||||
modal.close();
|
||||
}
|
||||
};
|
||||
|
||||
// Continue on Enter key press.
|
||||
View::on_enter_key(ui, || {
|
||||
on_save();
|
||||
});
|
||||
|
||||
ui.columns(2, |columns| {
|
||||
columns[0].vertical_centered_justified(|ui| {
|
||||
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
|
||||
|
@ -371,6 +376,8 @@ impl P2PSetup {
|
|||
.size(16.0)
|
||||
.color(Colors::inactive_text()));
|
||||
ui.add_space(12.0);
|
||||
} else if !peers.is_empty() {
|
||||
ui.add_space(12.0);
|
||||
}
|
||||
|
||||
let add_text = if peer_type == &PeerType::CustomSeed {
|
||||
|
@ -379,7 +386,7 @@ impl P2PSetup {
|
|||
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.
|
||||
self.is_correct_address_edit = true;
|
||||
self.peer_edit = "".to_string();
|
||||
|
@ -438,7 +445,7 @@ impl P2PSetup {
|
|||
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
|
||||
|
||||
// Save button callback.
|
||||
let on_save = || {
|
||||
let mut on_save = || {
|
||||
// Check if peer is correct and/or available.
|
||||
let peer = self.peer_edit.clone();
|
||||
let is_correct_address = PeersConfig::peer_to_addr(peer.clone()).is_some();
|
||||
|
@ -460,6 +467,11 @@ impl P2PSetup {
|
|||
}
|
||||
};
|
||||
|
||||
// Continue on Enter key press.
|
||||
View::on_enter_key(ui, || {
|
||||
on_save();
|
||||
});
|
||||
|
||||
ui.columns(2, |columns| {
|
||||
columns[0].vertical_centered_justified(|ui| {
|
||||
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
|
||||
|
@ -506,16 +518,18 @@ impl P2PSetup {
|
|||
ui.add_space(6.0);
|
||||
|
||||
let ban_window = NodeConfig::get_p2p_ban_window();
|
||||
View::button(ui, format!("{} {}", PROHIBIT_INSET, ban_window.clone()), Colors::button(), || {
|
||||
// Setup values for modal.
|
||||
self.ban_window_edit = ban_window;
|
||||
// Show ban window period setup modal.
|
||||
Modal::new(BAN_WINDOW_MODAL)
|
||||
.position(ModalPosition::CenterTop)
|
||||
.title(t!("network_settings.change_value"))
|
||||
.show();
|
||||
cb.show_keyboard();
|
||||
});
|
||||
View::button(ui,
|
||||
format!("{} {}", PROHIBIT_INSET, &ban_window),
|
||||
Colors::white_or_black(false), || {
|
||||
// Setup values for modal.
|
||||
self.ban_window_edit = ban_window;
|
||||
// Show ban window period setup modal.
|
||||
Modal::new(BAN_WINDOW_MODAL)
|
||||
.position(ModalPosition::CenterTop)
|
||||
.title(t!("network_settings.change_value"))
|
||||
.show();
|
||||
cb.show_keyboard();
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
ui.label(RichText::new(t!("network_settings.ban_window_desc"))
|
||||
.size(16.0)
|
||||
|
@ -563,6 +577,11 @@ impl P2PSetup {
|
|||
}
|
||||
};
|
||||
|
||||
// Continue on Enter key press.
|
||||
View::on_enter_key(ui, || {
|
||||
on_save();
|
||||
});
|
||||
|
||||
ui.columns(2, |columns| {
|
||||
columns[0].vertical_centered_justified(|ui| {
|
||||
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
|
||||
|
@ -588,17 +607,18 @@ impl P2PSetup {
|
|||
ui.add_space(6.0);
|
||||
|
||||
let max_inbound = NodeConfig::get_max_inbound_peers();
|
||||
let button_text = format!("{} {}", ARROW_FAT_LINES_DOWN, max_inbound.clone());
|
||||
View::button(ui, button_text, Colors::button(), || {
|
||||
// Setup values for modal.
|
||||
self.max_inbound_count = max_inbound;
|
||||
// Show maximum number of inbound peers setup modal.
|
||||
Modal::new(MAX_INBOUND_MODAL)
|
||||
.position(ModalPosition::CenterTop)
|
||||
.title(t!("network_settings.change_value"))
|
||||
.show();
|
||||
cb.show_keyboard();
|
||||
});
|
||||
View::button(ui,
|
||||
format!("{} {}", ARROW_FAT_LINES_DOWN, &max_inbound),
|
||||
Colors::white_or_black(false), || {
|
||||
// Setup values for modal.
|
||||
self.max_inbound_count = max_inbound;
|
||||
// Show maximum number of inbound peers setup modal.
|
||||
Modal::new(MAX_INBOUND_MODAL)
|
||||
.position(ModalPosition::CenterTop)
|
||||
.title(t!("network_settings.change_value"))
|
||||
.show();
|
||||
cb.show_keyboard();
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
}
|
||||
|
||||
|
@ -641,6 +661,11 @@ impl P2PSetup {
|
|||
}
|
||||
};
|
||||
|
||||
// Continue on Enter key press.
|
||||
View::on_enter_key(ui, || {
|
||||
on_save();
|
||||
});
|
||||
|
||||
ui.columns(2, |columns| {
|
||||
columns[0].vertical_centered_justified(|ui| {
|
||||
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
|
||||
|
@ -664,19 +689,19 @@ impl P2PSetup {
|
|||
.color(Colors::gray())
|
||||
);
|
||||
ui.add_space(6.0);
|
||||
|
||||
let max_outbound = NodeConfig::get_max_outbound_peers();
|
||||
let button_text = format!("{} {}", ARROW_FAT_LINES_UP, max_outbound.clone());
|
||||
View::button(ui, button_text, Colors::button(), || {
|
||||
// Setup values for modal.
|
||||
self.max_outbound_count = max_outbound;
|
||||
// Show maximum number of outbound peers setup modal.
|
||||
Modal::new(MAX_OUTBOUND_MODAL)
|
||||
.position(ModalPosition::CenterTop)
|
||||
.title(t!("network_settings.change_value"))
|
||||
.show();
|
||||
cb.show_keyboard();
|
||||
});
|
||||
View::button(ui,
|
||||
format!("{} {}", ARROW_FAT_LINES_UP, &max_outbound),
|
||||
Colors::white_or_black(false), || {
|
||||
// Setup values for modal.
|
||||
self.max_outbound_count = max_outbound;
|
||||
// Show maximum number of outbound peers setup modal.
|
||||
Modal::new(MAX_OUTBOUND_MODAL)
|
||||
.position(ModalPosition::CenterTop)
|
||||
.title(t!("network_settings.change_value"))
|
||||
.show();
|
||||
cb.show_keyboard();
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
}
|
||||
|
||||
|
@ -719,6 +744,11 @@ impl P2PSetup {
|
|||
}
|
||||
};
|
||||
|
||||
// Continue on Enter key press.
|
||||
View::on_enter_key(ui, || {
|
||||
on_save();
|
||||
});
|
||||
|
||||
ui.columns(2, |columns| {
|
||||
columns[0].vertical_centered_justified(|ui| {
|
||||
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
|
||||
|
@ -738,12 +768,13 @@ impl P2PSetup {
|
|||
/// Draw content to reset peers data.
|
||||
fn reset_peers_ui(&mut self, ui: &mut egui::Ui) {
|
||||
ui.add_space(4.0);
|
||||
|
||||
let button_text = format!("{} {}", TRASH, t!("network_settings.reset_peers"));
|
||||
View::colored_text_button(ui, button_text, Colors::red(), Colors::button(), || {
|
||||
Node::reset_peers(false);
|
||||
self.peers_reset = true;
|
||||
});
|
||||
View::colored_text_button(ui,
|
||||
format!("{} {}", TRASH, t!("network_settings.reset_peers")),
|
||||
Colors::red(),
|
||||
Colors::white_or_black(false), || {
|
||||
Node::reset_peers(false);
|
||||
self.peers_reset = true;
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
ui.label(RichText::new(t!("network_settings.reset_peers_desc"))
|
||||
.size(16.0)
|
||||
|
|
|
@ -145,7 +145,7 @@ impl PoolSetup {
|
|||
ui.add_space(6.0);
|
||||
|
||||
let fee = NodeConfig::get_base_fee();
|
||||
View::button(ui, format!("{} {}", HAND_COINS, fee.clone()), Colors::button(), |