Open .slatepack file with the app #13
60 changed files with 4667 additions and 4276 deletions
37
.github/workflows/build.yml
vendored
37
.github/workflows/build.yml
vendored
|
@ -2,43 +2,6 @@ name: Build
|
||||||
on: [push, pull_request]
|
on: [push, pull_request]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
android:
|
|
||||||
name: Android Build
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- name: Setup JDK 17
|
|
||||||
uses: actions/setup-java@v3
|
|
||||||
with:
|
|
||||||
java-version: '17'
|
|
||||||
distribution: 'temurin'
|
|
||||||
cache: gradle
|
|
||||||
- name: Setup build
|
|
||||||
run: |
|
|
||||||
cargo install cargo-ndk
|
|
||||||
rustup target add aarch64-linux-android
|
|
||||||
rustup target add armv7-linux-androideabi
|
|
||||||
rustup target add x86_64-linux-android
|
|
||||||
- name: Setup Java build
|
|
||||||
run: |
|
|
||||||
chmod +x android/gradlew
|
|
||||||
echo "${{ secrets.ANDROID_RELEASE_KEYSTORE }}" > release.keystore.asc
|
|
||||||
gpg -d --passphrase "${{ secrets.ANDROID_RELEASE_SECRET }}" --batch release.keystore.asc > android/keystore
|
|
||||||
echo -e "storePassword=${{ secrets.ANDROID_PASS }}\nkeyPassword=${{ secrets.ANDROID_PASS }}\nkeyAlias=grim\nstoreFile=../keystore" > android/keystore.properties
|
|
||||||
- name: Build lib 1/2
|
|
||||||
continue-on-error: true
|
|
||||||
run: |
|
|
||||||
sed -i -e 's/"rlib"/"cdylib","rlib"/g' Cargo.toml
|
|
||||||
export CPPFLAGS="-DMDB_USE_ROBUST=0" && export CFLAGS="-DMDB_USE_ROBUST=0" && cargo ndk -t arm64-v8a build --profile release-apk
|
|
||||||
- name: Build lib 2/2
|
|
||||||
run: |
|
|
||||||
unset CPPFLAGS && unset CFLAGS && cargo ndk -t arm64-v8a -o android/app/src/main/jniLibs build --profile release-apk
|
|
||||||
sed -i -e 's/"cdylib","rlib"/"rlib"/g' Cargo.toml
|
|
||||||
- name: Build APK
|
|
||||||
working-directory: android
|
|
||||||
run: |
|
|
||||||
./gradlew assembleRelease
|
|
||||||
|
|
||||||
linux:
|
linux:
|
||||||
name: Linux Build
|
name: Linux Build
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
97
.github/workflows/release.yml
vendored
97
.github/workflows/release.yml
vendored
|
@ -6,89 +6,6 @@ on:
|
||||||
- "v*.*.*"
|
- "v*.*.*"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
android_release:
|
|
||||||
name: Android Release
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
- name: Setup JDK 17
|
|
||||||
uses: actions/setup-java@v3
|
|
||||||
with:
|
|
||||||
java-version: '17'
|
|
||||||
distribution: 'temurin'
|
|
||||||
cache: gradle
|
|
||||||
- name: Setup Rust build
|
|
||||||
run: |
|
|
||||||
cargo install cargo-ndk
|
|
||||||
rustup target add aarch64-linux-android
|
|
||||||
rustup target add armv7-linux-androideabi
|
|
||||||
rustup target add x86_64-linux-android
|
|
||||||
- name: Setup Java build
|
|
||||||
run: |
|
|
||||||
chmod +x android/gradlew
|
|
||||||
echo "${{ secrets.ANDROID_RELEASE_KEYSTORE }}" > release.keystore.asc
|
|
||||||
gpg -d --passphrase "${{ secrets.ANDROID_RELEASE_SECRET }}" --batch release.keystore.asc > android/keystore
|
|
||||||
echo -e "storePassword=${{ secrets.ANDROID_PASS }}\nkeyPassword=${{ secrets.ANDROID_PASS }}\nkeyAlias=grim\nstoreFile=../keystore" > android/keystore.properties
|
|
||||||
- name: Build lib ARMv8 1/2
|
|
||||||
continue-on-error: true
|
|
||||||
run: |
|
|
||||||
sed -i -e 's/"rlib"/"cdylib","rlib"/g' Cargo.toml
|
|
||||||
export CPPFLAGS="-DMDB_USE_ROBUST=0" && export CFLAGS="-DMDB_USE_ROBUST=0" && cargo ndk -t arm64-v8a build --profile release-apk
|
|
||||||
- name: Build lib ARMv8 2/2
|
|
||||||
run: |
|
|
||||||
unset CPPFLAGS && unset CFLAGS && cargo ndk -t arm64-v8a -o android/app/src/main/jniLibs build --profile release-apk
|
|
||||||
sed -i -e 's/"cdylib","rlib"/"rlib"/g' Cargo.toml
|
|
||||||
- name: Build 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 ARM
|
|
||||||
working-directory: android
|
|
||||||
run: |
|
|
||||||
rm -rf app/build
|
|
||||||
./gradlew assembleRelease
|
|
||||||
mv app/build/outputs/apk/release/app-release.apk grim-${{ github.ref_name }}-android.apk
|
|
||||||
rm -rf app/src/main/jniLibs/*
|
|
||||||
- name: Checksum APK ARM
|
|
||||||
working-directory: android
|
|
||||||
shell: pwsh
|
|
||||||
run: get-filehash -algorithm sha256 grim-${{ github.ref_name }}-android.apk | Format-List | Out-String | ForEach-Object { $_.Trim() } > grim-${{ github.ref_name }}-android-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
|
|
||||||
./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.apk
|
|
||||||
android/grim-${{ github.ref_name }}-android-sha256sum.txt
|
|
||||||
android/grim-${{ github.ref_name }}-android-x86_64.apk
|
|
||||||
android/grim-${{ github.ref_name }}-android-x86_64-sha256sum.txt
|
|
||||||
|
|
||||||
linux_release:
|
linux_release:
|
||||||
name: Linux Release
|
name: Linux Release
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
@ -121,7 +38,7 @@ jobs:
|
||||||
- name: Checksum AppImage x86
|
- name: Checksum AppImage x86
|
||||||
working-directory: target/x86_64-unknown-linux-gnu/release
|
working-directory: target/x86_64-unknown-linux-gnu/release
|
||||||
shell: pwsh
|
shell: 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
|
run: sha256sum grim-${{ github.ref_name }}-linux-x86_64.AppImage > grim-${{ github.ref_name }}-linux-x86_64-appimage-sha256sum.txt
|
||||||
- name: AppImage ARM
|
- name: AppImage ARM
|
||||||
run: |
|
run: |
|
||||||
cp target/aarch64-unknown-linux-gnu/release/grim linux/Grim.AppDir/AppRun
|
cp target/aarch64-unknown-linux-gnu/release/grim linux/Grim.AppDir/AppRun
|
||||||
|
@ -129,7 +46,7 @@ jobs:
|
||||||
- name: Checksum AppImage ARM
|
- name: Checksum AppImage ARM
|
||||||
working-directory: target/aarch64-unknown-linux-gnu/release
|
working-directory: target/aarch64-unknown-linux-gnu/release
|
||||||
shell: pwsh
|
shell: 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
|
run: sha256sum grim-${{ github.ref_name }}-linux-arm.AppImage > grim-${{ github.ref_name }}-linux-arm-appimage-sha256sum.txt
|
||||||
- name: Release
|
- name: Release
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@v1
|
||||||
with:
|
with:
|
||||||
|
@ -157,7 +74,7 @@ jobs:
|
||||||
- name: Checksum release
|
- name: Checksum release
|
||||||
working-directory: target/release
|
working-directory: target/release
|
||||||
shell: pwsh
|
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
|
run: sha256sum grim-${{ github.ref_name }}-win-x86_64.zip > grim-${{ github.ref_name }}-win-x86_64-sha256sum.txt
|
||||||
- name: Install cargo-wix
|
- name: Install cargo-wix
|
||||||
run: cargo install cargo-wix
|
run: cargo install cargo-wix
|
||||||
- name: Run cargo-wix
|
- name: Run cargo-wix
|
||||||
|
@ -165,7 +82,7 @@ jobs:
|
||||||
- name: Checksum msi
|
- name: Checksum msi
|
||||||
working-directory: target/wix
|
working-directory: target/wix
|
||||||
shell: pwsh
|
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
|
run: sha256sum grim-${{ github.ref_name }}-win-x86_64.msi > grim-${{ github.ref_name }}-win-x86_64-msi-sha256sum.txt
|
||||||
- name: Release
|
- name: Release
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@v1
|
||||||
with:
|
with:
|
||||||
|
@ -204,7 +121,7 @@ jobs:
|
||||||
- name: Checksum Release x86
|
- name: Checksum Release x86
|
||||||
working-directory: target/x86_64-apple-darwin/release
|
working-directory: target/x86_64-apple-darwin/release
|
||||||
shell: pwsh
|
shell: 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
|
run: sha256sum grim-${{ github.ref_name }}-macos-x86_64.zip > grim-${{ github.ref_name }}-macos-x86_64-sha256sum.txt
|
||||||
- name: Release ARM
|
- name: Release ARM
|
||||||
run: |
|
run: |
|
||||||
rustup target add aarch64-apple-darwin
|
rustup target add aarch64-apple-darwin
|
||||||
|
@ -219,7 +136,7 @@ jobs:
|
||||||
- name: Checksum Release ARM
|
- name: Checksum Release ARM
|
||||||
working-directory: target/aarch64-apple-darwin/release
|
working-directory: target/aarch64-apple-darwin/release
|
||||||
shell: pwsh
|
shell: 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
|
run: sha256sum grim-${{ github.ref_name }}-macos-arm.zip > grim-${{ github.ref_name }}-macos-arm-sha256sum.txt
|
||||||
- name: Release Universal
|
- name: Release Universal
|
||||||
run: |
|
run: |
|
||||||
rustup target add aarch64-apple-darwin
|
rustup target add aarch64-apple-darwin
|
||||||
|
@ -235,7 +152,7 @@ jobs:
|
||||||
- name: Checksum Release Universal
|
- name: Checksum Release Universal
|
||||||
working-directory: target/universal2-apple-darwin/release
|
working-directory: target/universal2-apple-darwin/release
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
run: get-filehash -algorithm sha256 grim-${{ github.ref_name }}-macos-universal.zip | Format-List | Out-String | ForEach-Object { $_.Trim() } > grim-${{ github.ref_name }}-macos-universal-sha256sum.txt
|
run: sha256sum grim-${{ github.ref_name }}-macos-universal.zip > grim-${{ github.ref_name }}-macos-universal-sha256sum.txt
|
||||||
- name: Release
|
- name: Release
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@v1
|
||||||
with:
|
with:
|
||||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -13,7 +13,6 @@ android/keystore.properties
|
||||||
target
|
target
|
||||||
.cargo/
|
.cargo/
|
||||||
app/src/main/jniLibs
|
app/src/main/jniLibs
|
||||||
macos/Grim.app/Contents/MacOS/grim
|
|
||||||
macos/cert.pem
|
macos/cert.pem
|
||||||
linux/Grim.AppDir/AppRun
|
linux/Grim.AppDir/AppRun
|
||||||
.intentionally-empty-file.o
|
.intentionally-empty-file.o
|
28
Cargo.lock
generated
28
Cargo.lock
generated
|
@ -2483,6 +2483,12 @@ version = "0.3.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0688c2a7f92e427f44895cd63841bff7b29f8d7a1648b9e7e07a4a365b2e1257"
|
checksum = "0688c2a7f92e427f44895cd63841bff7b29f8d7a1648b9e7e07a4a365b2e1257"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "doctest-file"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "aac81fa3e28d21450aa4d2ac065992ba96a1d7303efbce51a95f4fd175b67562"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "document-features"
|
name = "document-features"
|
||||||
version = "0.2.8"
|
version = "0.2.8"
|
||||||
|
@ -3833,6 +3839,7 @@ dependencies = [
|
||||||
"hyper 0.14.29",
|
"hyper 0.14.29",
|
||||||
"hyper-tls 0.5.0",
|
"hyper-tls 0.5.0",
|
||||||
"image 0.25.1",
|
"image 0.25.1",
|
||||||
|
"interprocess",
|
||||||
"jni",
|
"jni",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"local-ip-address",
|
"local-ip-address",
|
||||||
|
@ -4976,6 +4983,21 @@ dependencies = [
|
||||||
"syn 2.0.66",
|
"syn 2.0.66",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "interprocess"
|
||||||
|
version = "2.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d2f4e4a06d42fab3e85ab1b419ad32b09eab58b901d40c57935ff92db3287a13"
|
||||||
|
dependencies = [
|
||||||
|
"doctest-file",
|
||||||
|
"futures-core",
|
||||||
|
"libc",
|
||||||
|
"recvmsg",
|
||||||
|
"tokio 1.38.0",
|
||||||
|
"widestring",
|
||||||
|
"windows-sys 0.52.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "intl-memoizer"
|
name = "intl-memoizer"
|
||||||
version = "0.5.2"
|
version = "0.5.2"
|
||||||
|
@ -7432,6 +7454,12 @@ dependencies = [
|
||||||
"rand_core 0.3.1",
|
"rand_core 0.3.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "recvmsg"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d3edd4d5d42c92f0a659926464d4cce56b562761267ecf0f469d85b7de384175"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "redox_syscall"
|
name = "redox_syscall"
|
||||||
version = "0.1.57"
|
version = "0.1.57"
|
||||||
|
|
|
@ -16,9 +16,6 @@ path = "src/main.rs"
|
||||||
name="grim"
|
name="grim"
|
||||||
crate-type = ["rlib"]
|
crate-type = ["rlib"]
|
||||||
|
|
||||||
[profile.release]
|
|
||||||
debug = 1
|
|
||||||
|
|
||||||
[profile.release-apk]
|
[profile.release-apk]
|
||||||
inherits = "release"
|
inherits = "release"
|
||||||
strip = true
|
strip = true
|
||||||
|
@ -119,6 +116,7 @@ eframe = { version = "0.28.1", features = ["wgpu", "glow"] }
|
||||||
arboard = "3.2.0"
|
arboard = "3.2.0"
|
||||||
rfd = "0.14.1"
|
rfd = "0.14.1"
|
||||||
dark-light = "1.1.1"
|
dark-light = "1.1.1"
|
||||||
|
interprocess = { version = "2.2.1", features = ["tokio"] }
|
||||||
|
|
||||||
[target.'cfg(target_os = "android")'.dependencies]
|
[target.'cfg(target_os = "android")'.dependencies]
|
||||||
android_logger = "0.13.1"
|
android_logger = "0.13.1"
|
||||||
|
|
|
@ -2,10 +2,6 @@ plugins {
|
||||||
id 'com.android.application'
|
id 'com.android.application'
|
||||||
}
|
}
|
||||||
|
|
||||||
def keystorePropertiesFile = rootProject.file("keystore.properties")
|
|
||||||
def keystoreProperties = new Properties()
|
|
||||||
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
|
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdk 33
|
compileSdk 33
|
||||||
ndkVersion '26.0.10792818'
|
ndkVersion '26.0.10792818'
|
||||||
|
@ -18,20 +14,32 @@ android {
|
||||||
versionName "0.1.3"
|
versionName "0.1.3"
|
||||||
}
|
}
|
||||||
|
|
||||||
signingConfigs {
|
def keystorePropertiesFile = rootProject.file("keystore.properties")
|
||||||
release {
|
def keystoreProperties = new Properties()
|
||||||
keyAlias keystoreProperties['keyAlias']
|
if (keystorePropertiesFile.exists()) {
|
||||||
keyPassword keystoreProperties['keyPassword']
|
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
|
||||||
storeFile file(keystoreProperties['storeFile'])
|
|
||||||
storePassword keystoreProperties['storePassword']
|
signingConfigs {
|
||||||
|
release {
|
||||||
|
keyAlias keystoreProperties['keyAlias']
|
||||||
|
keyPassword keystoreProperties['keyPassword']
|
||||||
|
storeFile file(keystoreProperties['storeFile'])
|
||||||
|
storePassword keystoreProperties['storePassword']
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
minifyEnabled false
|
minifyEnabled false
|
||||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||||
signingConfig signingConfigs.release
|
}
|
||||||
|
if (keystorePropertiesFile.exists()) {
|
||||||
|
signedRelease {
|
||||||
|
initWith release
|
||||||
|
signingConfig signingConfigs.release
|
||||||
|
}
|
||||||
}
|
}
|
||||||
debug {
|
debug {
|
||||||
minifyEnabled false
|
minifyEnabled false
|
||||||
|
|
|
@ -1,15 +1,17 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:tools="http://schemas.android.com/tools" xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
>
|
>
|
||||||
|
|
||||||
<uses-feature android:name="android.hardware.camera" android:required="false"/>
|
<uses-feature android:name="android.hardware.camera" android:required="false"/>
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.EXPAND_STATUS_BAR" />
|
<uses-permission android:name="android.permission.EXPAND_STATUS_BAR" />
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||||
<uses-permission android:name="android.permission.INTERNET"/>
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
<uses-permission android:name="android.permission.CAMERA"/>
|
<uses-permission android:name="android.permission.CAMERA"/>
|
||||||
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" tools:ignore="ScopedStorage"/>
|
||||||
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" tools:ignore="ScopedStorage"/>
|
||||||
|
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" tools:ignore="ScopedStorage"/>
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:hardwareAccelerated="true"
|
android:hardwareAccelerated="true"
|
||||||
|
@ -18,7 +20,6 @@
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="Grim"
|
android:label="Grim"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
|
||||||
android:theme="@style/Theme.Main">
|
android:theme="@style/Theme.Main">
|
||||||
|
|
||||||
<receiver android:name=".NotificationActionsReceiver"/>
|
<receiver android:name=".NotificationActionsReceiver"/>
|
||||||
|
@ -44,6 +45,22 @@
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
|
<intent-filter android:scheme="http" tools:ignore="AppLinkUrlError">
|
||||||
|
<action android:name="android.intent.action.SEND" />
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<data android:mimeType="text/*" />
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
|
<intent-filter android:scheme="http" tools:ignore="AppLinkUrlError">
|
||||||
|
<action android:name="android.intent.action.SEND" />
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<data android:mimeType="application/*" />
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
<meta-data android:name="android.app.lib_name" android:value="grim" />
|
<meta-data android:name="android.app.lib_name" android:value="grim" />
|
||||||
</activity>
|
</activity>
|
||||||
<service android:name=".BackgroundService" android:stopWithTask="true" />
|
<service android:name=".BackgroundService" android:stopWithTask="true" />
|
||||||
|
|
|
@ -152,13 +152,17 @@ public class BackgroundService extends Service {
|
||||||
// Show notification with sync status.
|
// Show notification with sync status.
|
||||||
Intent i = getPackageManager().getLaunchIntentForPackage(this.getPackageName());
|
Intent i = getPackageManager().getLaunchIntentForPackage(this.getPackageName());
|
||||||
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, i, PendingIntent.FLAG_IMMUTABLE);
|
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, i, PendingIntent.FLAG_IMMUTABLE);
|
||||||
mNotificationBuilder = new NotificationCompat.Builder(this, TAG)
|
try {
|
||||||
.setContentTitle(this.getSyncTitle())
|
mNotificationBuilder = new NotificationCompat.Builder(this, TAG)
|
||||||
.setContentText(this.getSyncStatusText())
|
.setContentTitle(this.getSyncTitle())
|
||||||
.setStyle(new NotificationCompat.BigTextStyle().bigText(this.getSyncStatusText()))
|
.setContentText(this.getSyncStatusText())
|
||||||
.setSmallIcon(R.drawable.ic_stat_name)
|
.setStyle(new NotificationCompat.BigTextStyle().bigText(this.getSyncStatusText()))
|
||||||
.setPriority(NotificationCompat.PRIORITY_MAX)
|
.setSmallIcon(R.drawable.ic_stat_name)
|
||||||
.setContentIntent(pendingIntent);
|
.setPriority(NotificationCompat.PRIORITY_MAX)
|
||||||
|
.setContentIntent(pendingIntent);
|
||||||
|
} catch (UnsatisfiedLinkError e) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
Notification notification = mNotificationBuilder.build();
|
Notification notification = mNotificationBuilder.build();
|
||||||
|
|
||||||
// Start service at foreground state to prevent killing by system.
|
// Start service at foreground state to prevent killing by system.
|
||||||
|
|
|
@ -7,9 +7,9 @@ import android.content.*;
|
||||||
import android.content.pm.PackageManager;
|
import android.content.pm.PackageManager;
|
||||||
import android.content.res.Configuration;
|
import android.content.res.Configuration;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Build;
|
import android.os.*;
|
||||||
import android.os.Bundle;
|
|
||||||
import android.os.Process;
|
import android.os.Process;
|
||||||
|
import android.provider.Settings;
|
||||||
import android.system.ErrnoException;
|
import android.system.ErrnoException;
|
||||||
import android.system.Os;
|
import android.system.Os;
|
||||||
import android.util.Size;
|
import android.util.Size;
|
||||||
|
@ -51,8 +51,7 @@ public class MainActivity extends GameActivity {
|
||||||
@Override
|
@Override
|
||||||
public void onReceive(Context ctx, Intent i) {
|
public void onReceive(Context ctx, Intent i) {
|
||||||
if (i.getAction().equals(STOP_APP_ACTION)) {
|
if (i.getAction().equals(STOP_APP_ACTION)) {
|
||||||
onExit();
|
exit();
|
||||||
Process.killProcess(Process.myPid());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -67,11 +66,19 @@ public class MainActivity extends GameActivity {
|
||||||
private ExecutorService mCameraExecutor = null;
|
private ExecutorService mCameraExecutor = null;
|
||||||
private boolean mUseBackCamera = true;
|
private boolean mUseBackCamera = true;
|
||||||
|
|
||||||
private ActivityResultLauncher<Intent> mFilePickResultLauncher = null;
|
private ActivityResultLauncher<Intent> mFilePickResult = null;
|
||||||
|
private ActivityResultLauncher<Intent> mOpenFilePermissionsResult = null;
|
||||||
|
|
||||||
@SuppressLint("UnspecifiedRegisterReceiverFlag")
|
@SuppressLint("UnspecifiedRegisterReceiverFlag")
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
// Check if activity was launched to exclude from recent apps on exit.
|
||||||
|
if ((getIntent().getFlags() & Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS) != 0) {
|
||||||
|
super.onCreate(null);
|
||||||
|
finish();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Clear cache on start.
|
// Clear cache on start.
|
||||||
if (savedInstanceState == null) {
|
if (savedInstanceState == null) {
|
||||||
Utils.deleteDirectoryContent(new File(getExternalCacheDir().getPath()), false);
|
Utils.deleteDirectoryContent(new File(getExternalCacheDir().getPath()), false);
|
||||||
|
@ -91,8 +98,21 @@ public class MainActivity extends GameActivity {
|
||||||
// Register receiver to finish activity from the BackgroundService.
|
// Register receiver to finish activity from the BackgroundService.
|
||||||
registerReceiver(mBroadcastReceiver, new IntentFilter(STOP_APP_ACTION));
|
registerReceiver(mBroadcastReceiver, new IntentFilter(STOP_APP_ACTION));
|
||||||
|
|
||||||
// Register file pick result launcher.
|
// Register associated file opening result.
|
||||||
mFilePickResultLauncher = registerForActivityResult(
|
mOpenFilePermissionsResult = registerForActivityResult(
|
||||||
|
new ActivityResultContracts.StartActivityForResult(),
|
||||||
|
result -> {
|
||||||
|
if (Build.VERSION.SDK_INT >= 30) {
|
||||||
|
if (Environment.isExternalStorageManager()) {
|
||||||
|
onFile();
|
||||||
|
}
|
||||||
|
} else if (result.getResultCode() == RESULT_OK) {
|
||||||
|
onFile();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
// Register file pick result.
|
||||||
|
mFilePickResult = registerForActivityResult(
|
||||||
new ActivityResultContracts.StartActivityForResult(),
|
new ActivityResultContracts.StartActivityForResult(),
|
||||||
result -> {
|
result -> {
|
||||||
int resultCode = result.getResultCode();
|
int resultCode = result.getResultCode();
|
||||||
|
@ -105,11 +125,11 @@ public class MainActivity extends GameActivity {
|
||||||
File file = new File(getExternalCacheDir(), name);
|
File file = new File(getExternalCacheDir(), name);
|
||||||
try (InputStream is = getContentResolver().openInputStream(uri);
|
try (InputStream is = getContentResolver().openInputStream(uri);
|
||||||
OutputStream os = new FileOutputStream(file)) {
|
OutputStream os = new FileOutputStream(file)) {
|
||||||
byte[] buffer = new byte[1024];
|
byte[] buffer = new byte[1024];
|
||||||
int length;
|
int length;
|
||||||
while ((length = is.read(buffer)) > 0) {
|
while ((length = is.read(buffer)) > 0) {
|
||||||
os.write(buffer, 0, length);
|
os.write(buffer, 0, length);
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
|
@ -124,7 +144,7 @@ public class MainActivity extends GameActivity {
|
||||||
// Listener for display insets (cutouts) to pass values into native code.
|
// Listener for display insets (cutouts) to pass values into native code.
|
||||||
View content = getWindow().getDecorView().findViewById(android.R.id.content);
|
View content = getWindow().getDecorView().findViewById(android.R.id.content);
|
||||||
ViewCompat.setOnApplyWindowInsetsListener(content, (v, insets) -> {
|
ViewCompat.setOnApplyWindowInsetsListener(content, (v, insets) -> {
|
||||||
// Setup cutouts values.
|
// Get display cutouts.
|
||||||
DisplayCutoutCompat dc = insets.getDisplayCutout();
|
DisplayCutoutCompat dc = insets.getDisplayCutout();
|
||||||
int cutoutTop = 0;
|
int cutoutTop = 0;
|
||||||
int cutoutRight = 0;
|
int cutoutRight = 0;
|
||||||
|
@ -140,7 +160,7 @@ public class MainActivity extends GameActivity {
|
||||||
// Get display insets.
|
// Get display insets.
|
||||||
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
|
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
|
||||||
|
|
||||||
// Setup values to pass into native code.
|
// Pass values into native code.
|
||||||
int[] values = new int[]{0, 0, 0, 0};
|
int[] values = new int[]{0, 0, 0, 0};
|
||||||
values[0] = Utils.pxToDp(Integer.max(cutoutTop, systemBars.top), this);
|
values[0] = Utils.pxToDp(Integer.max(cutoutTop, systemBars.top), this);
|
||||||
values[1] = Utils.pxToDp(Integer.max(cutoutRight, systemBars.right), this);
|
values[1] = Utils.pxToDp(Integer.max(cutoutRight, systemBars.right), this);
|
||||||
|
@ -166,8 +186,61 @@ public class MainActivity extends GameActivity {
|
||||||
BackgroundService.start(this);
|
BackgroundService.start(this);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Check if intent has data on launch.
|
||||||
|
if (savedInstanceState == null) {
|
||||||
|
onNewIntent(getIntent());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onNewIntent(Intent intent) {
|
||||||
|
super.onNewIntent(intent);
|
||||||
|
String action = intent.getAction();
|
||||||
|
// Check if file was open with the application.
|
||||||
|
if (action != null && action.equals(Intent.ACTION_VIEW)) {
|
||||||
|
Intent i = getIntent();
|
||||||
|
i.setData(intent.getData());
|
||||||
|
setIntent(i);
|
||||||
|
onFile();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Callback when associated file was open.
|
||||||
|
private void onFile() {
|
||||||
|
Uri data = getIntent().getData();
|
||||||
|
if (data == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (Build.VERSION.SDK_INT >= 30) {
|
||||||
|
if (!Environment.isExternalStorageManager()) {
|
||||||
|
Intent i = new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION);
|
||||||
|
mOpenFilePermissionsResult.launch(i);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
ParcelFileDescriptor parcelFile = getContentResolver().openFileDescriptor(data, "r");
|
||||||
|
FileReader fileReader = new FileReader(parcelFile.getFileDescriptor());
|
||||||
|
BufferedReader reader = new BufferedReader(fileReader);
|
||||||
|
String line;
|
||||||
|
StringBuilder buff = new StringBuilder();
|
||||||
|
while ((line = reader.readLine()) != null) {
|
||||||
|
buff.append(line);
|
||||||
|
}
|
||||||
|
reader.close();
|
||||||
|
fileReader.close();
|
||||||
|
|
||||||
|
// Provide file content into native code.
|
||||||
|
onData(buff.toString());
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pass data into native code.
|
||||||
|
public native void onData(String data);
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onConfigurationChanged(Configuration newConfig) {
|
public void onConfigurationChanged(Configuration newConfig) {
|
||||||
super.onConfigurationChanged(newConfig);
|
super.onConfigurationChanged(newConfig);
|
||||||
|
@ -232,17 +305,17 @@ public class MainActivity extends GameActivity {
|
||||||
// Implemented into native code to handle key code BACK event.
|
// Implemented into native code to handle key code BACK event.
|
||||||
public native void onBack();
|
public native void onBack();
|
||||||
|
|
||||||
// Actions on app exit.
|
// Called from native code to exit app.
|
||||||
private void onExit() {
|
public void exit() {
|
||||||
unregisterReceiver(mBroadcastReceiver);
|
finishAndRemoveTask();
|
||||||
BackgroundService.stop(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onDestroy() {
|
protected void onDestroy() {
|
||||||
onExit();
|
unregisterReceiver(mBroadcastReceiver);
|
||||||
|
BackgroundService.stop(this);
|
||||||
|
|
||||||
// Kill process after 3 seconds if app was terminated from recent apps to prevent app hanging.
|
// Kill process after 3 secs if app was terminated from recent apps to prevent app hang.
|
||||||
new Thread(() -> {
|
new Thread(() -> {
|
||||||
try {
|
try {
|
||||||
onTermination();
|
onTermination();
|
||||||
|
@ -253,9 +326,7 @@ public class MainActivity extends GameActivity {
|
||||||
}
|
}
|
||||||
}).start();
|
}).start();
|
||||||
|
|
||||||
// Destroy an app and kill process.
|
|
||||||
super.onDestroy();
|
super.onDestroy();
|
||||||
Process.killProcess(Process.myPid());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Notify native code to stop activity (e.g. node) if app was terminated from recent apps.
|
// Notify native code to stop activity (e.g. node) if app was terminated from recent apps.
|
||||||
|
@ -298,18 +369,16 @@ public class MainActivity extends GameActivity {
|
||||||
|
|
||||||
// Called from native code to start camera.
|
// Called from native code to start camera.
|
||||||
public void startCamera() {
|
public void startCamera() {
|
||||||
// Check permissions.
|
|
||||||
String notificationsPermission = Manifest.permission.CAMERA;
|
String notificationsPermission = Manifest.permission.CAMERA;
|
||||||
if (checkSelfPermission(notificationsPermission) != PackageManager.PERMISSION_GRANTED) {
|
if (checkSelfPermission(notificationsPermission) != PackageManager.PERMISSION_GRANTED) {
|
||||||
requestPermissions(new String[] { notificationsPermission }, CAMERA_PERMISSION_CODE);
|
requestPermissions(new String[] { notificationsPermission }, CAMERA_PERMISSION_CODE);
|
||||||
} else {
|
} else {
|
||||||
// Start .
|
|
||||||
if (mCameraProviderFuture == null) {
|
if (mCameraProviderFuture == null) {
|
||||||
mCameraProviderFuture = ProcessCameraProvider.getInstance(this);
|
mCameraProviderFuture = ProcessCameraProvider.getInstance(this);
|
||||||
mCameraProviderFuture.addListener(() -> {
|
mCameraProviderFuture.addListener(() -> {
|
||||||
try {
|
try {
|
||||||
mCameraProvider = mCameraProviderFuture.get();
|
mCameraProvider = mCameraProviderFuture.get();
|
||||||
// Launch camera.
|
// Start camera.
|
||||||
openCamera();
|
openCamera();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
View content = findViewById(android.R.id.content);
|
View content = findViewById(android.R.id.content);
|
||||||
|
@ -402,7 +471,7 @@ public class MainActivity extends GameActivity {
|
||||||
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
|
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
|
||||||
intent.setType("*/*");
|
intent.setType("*/*");
|
||||||
try {
|
try {
|
||||||
mFilePickResultLauncher.launch(Intent.createChooser(intent, "Pick file"));
|
mFilePickResult.launch(Intent.createChooser(intent, "Pick file"));
|
||||||
} catch (android.content.ActivityNotFoundException ex) {
|
} catch (android.content.ActivityNotFoundException ex) {
|
||||||
onFilePick("");
|
onFilePick("");
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,4 +3,5 @@ Name=Grim
|
||||||
Exec=grim
|
Exec=grim
|
||||||
Icon=grim
|
Icon=grim
|
||||||
Type=Application
|
Type=Application
|
||||||
Categories=Finance
|
Categories=Finance
|
||||||
|
MimeType=application/x-slatepack;text/plain;
|
|
@ -17,9 +17,7 @@ cd ..
|
||||||
[[ $2 == "x86_64" ]] && arch+=(x86_64-unknown-linux-gnu)
|
[[ $2 == "x86_64" ]] && arch+=(x86_64-unknown-linux-gnu)
|
||||||
[[ $2 == "arm" ]] && arch+=(aarch64-unknown-linux-gnu)
|
[[ $2 == "arm" ]] && arch+=(aarch64-unknown-linux-gnu)
|
||||||
|
|
||||||
# Start release build with zig linker for cross-compilation
|
cargo build --release --target ${arch}
|
||||||
cargo install cargo-zigbuild
|
|
||||||
cargo zigbuild --release --target ${arch}
|
|
||||||
|
|
||||||
# Create AppImage with https://github.com/AppImage/appimagetool
|
# Create AppImage with https://github.com/AppImage/appimagetool
|
||||||
cp target/${arch}/release/grim linux/Grim.AppDir/AppRun
|
cp target/${arch}/release/grim linux/Grim.AppDir/AppRun
|
||||||
|
|
|
@ -40,6 +40,34 @@
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>1</string>
|
<string>1</string>
|
||||||
|
|
||||||
|
<key>CFBundleDocumentTypes</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleTypeName</key>
|
||||||
|
<string>Apple SimpleText document</string>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>Viewer</string>
|
||||||
|
<key>LSItemContentTypes</key>
|
||||||
|
<array>
|
||||||
|
<string>com.apple.traditional-mac-plain-text</string>
|
||||||
|
</array>
|
||||||
|
<key>NSDocumentClass</key>
|
||||||
|
<string>Document</string>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleTypeName</key>
|
||||||
|
<string>Unknown document</string>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>Viewer</string>
|
||||||
|
<key>LSItemContentTypes</key>
|
||||||
|
<array>
|
||||||
|
<string>public.data</string>
|
||||||
|
</array>
|
||||||
|
<key>NSDocumentClass</key>
|
||||||
|
<string>Document</string>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
|
|
||||||
<key>LSApplicationCategoryType</key>
|
<key>LSApplicationCategoryType</key>
|
||||||
<string>public.app-category.finance</string>
|
<string>public.app-category.finance</string>
|
||||||
|
|
||||||
|
|
1
macos/Grim.app/Contents/MacOS/.gitignore
vendored
Normal file
1
macos/Grim.app/Contents/MacOS/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
!.gitignore
|
|
@ -9,14 +9,13 @@ case $2 in
|
||||||
exit 1
|
exit 1
|
||||||
esac
|
esac
|
||||||
|
|
||||||
if [[ ! -v SDKROOT ]]; then
|
if [[ "$OSTYPE" != "darwin"* ]]; then
|
||||||
|
if [ -z ${SDKROOT+x} ]; then
|
||||||
echo "MacOS SDKROOT is not set"
|
echo "MacOS SDKROOT is not set"
|
||||||
exit 1
|
exit 1
|
||||||
elif [[ -z "SDKROOT" ]]; then
|
else
|
||||||
echo "MacOS SDKROOT is set to the empty string"
|
|
||||||
exit 1
|
|
||||||
else
|
|
||||||
echo "Use MacOS SDK: ${SDKROOT}"
|
echo "Use MacOS SDK: ${SDKROOT}"
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Setup build directory
|
# Setup build directory
|
||||||
|
@ -28,22 +27,18 @@ cd ..
|
||||||
rustup target add x86_64-apple-darwin
|
rustup target add x86_64-apple-darwin
|
||||||
rustup target add aarch64-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 == "x86_64" ]] && arch+=(x86_64-apple-darwin)
|
||||||
[[ $2 == "arm" ]] && arch+=(aarch64-apple-darwin)
|
[[ $2 == "arm" ]] && arch+=(aarch64-apple-darwin)
|
||||||
[[ $2 == "universal" ]] && arch+=(universal2-apple-darwin)
|
[[ $2 == "universal" ]]; arch+=(universal2-apple-darwin)
|
||||||
|
|
||||||
# Start release build with zig linker for cross-compilation
|
# Start release build with zig linker, requires zig 0.12.1
|
||||||
# zig 0.12+ required
|
|
||||||
cargo install cargo-zigbuild
|
cargo install cargo-zigbuild
|
||||||
cargo zigbuild --release --target ${arch}
|
cargo zigbuild --release --target ${arch}
|
||||||
rm -rf .intentionally-empty-file.o
|
rm -rf .intentionally-empty-file.o
|
||||||
mkdir macos/Grim.app/Contents/MacOS
|
|
||||||
yes | cp -rf target/${arch}/release/grim macos/Grim.app/Contents/MacOS
|
yes | cp -rf target/${arch}/release/grim macos/Grim.app/Contents/MacOS
|
||||||
|
|
||||||
### Sign .app resources on change:
|
# Sign .app resources on change:
|
||||||
#rcodesign generate-self-signed-certificate
|
#rcodesign generate-self-signed-certificate
|
||||||
#rcodesign sign --pem-file cert.pem macos/Grim.app
|
#rcodesign sign --pem-file cert.pem macos/Grim.app
|
||||||
|
|
||||||
|
|
|
@ -1,81 +1,113 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
usage="Usage: build_run_android.sh [type] [platform]\n - type: 'debug', 'release'\n - platform: 'v7', 'v8'"
|
usage="Usage: android.sh [type] [platform]\n - type: 'build', 'release', ''\n - platform, for build type: 'v7', 'v8', 'x86'"
|
||||||
case $1 in
|
case $1 in
|
||||||
debug|release)
|
build|release)
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
printf "$usage"
|
printf "$usage"
|
||||||
exit 1
|
exit 1
|
||||||
esac
|
esac
|
||||||
|
|
||||||
case $2 in
|
if [[ $1 == "build" ]]; then
|
||||||
v7|v8)
|
case $2 in
|
||||||
;;
|
v7|v8|x86)
|
||||||
*)
|
;;
|
||||||
printf "$usage"
|
*)
|
||||||
exit 1
|
printf "$usage"
|
||||||
esac
|
exit 1
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
# Setup build directory
|
# Setup build directory
|
||||||
BASEDIR=$(cd $(dirname $0) && pwd)
|
BASEDIR=$(cd $(dirname $0) && pwd)
|
||||||
cd ${BASEDIR}
|
cd ${BASEDIR}
|
||||||
cd ..
|
cd ..
|
||||||
|
|
||||||
# Setup release argument
|
# Install platforms and tools
|
||||||
type=$1
|
rustup target add armv7-linux-androideabi
|
||||||
[[ ${type} == "release" ]] && release_param="--profile release-apk"
|
rustup target add aarch64-linux-android
|
||||||
|
rustup target add x86_64-linux-android
|
||||||
# Setup platform argument
|
|
||||||
[[ $2 == "v7" ]] && arch+=(armeabi-v7a)
|
|
||||||
[[ $2 == "v8" ]] && arch+=(arm64-v8a)
|
|
||||||
|
|
||||||
# Setup platform path
|
|
||||||
[[ $2 == "v7" ]] && platform+=(armv7-linux-androideabi)
|
|
||||||
[[ $2 == "v8" ]] && platform+=(aarch64-linux-android)
|
|
||||||
|
|
||||||
# Install platform
|
|
||||||
[[ $2 == "v7" ]] && rustup target install armv7-linux-androideabi
|
|
||||||
[[ $2 == "v8" ]] && rustup target install aarch64-linux-android
|
|
||||||
|
|
||||||
# Build native code
|
|
||||||
cargo install cargo-ndk
|
cargo install cargo-ndk
|
||||||
|
|
||||||
sed -i -e 's/"rlib"/"cdylib","rlib"/g' Cargo.toml
|
success=1
|
||||||
|
|
||||||
# temp fix for https://stackoverflow.com/questions/57193895/error-use-of-undeclared-identifier-pthread-mutex-robust-cargo-build-liblmdb-s
|
### Build native code
|
||||||
success=0
|
function build_lib() {
|
||||||
export CPPFLAGS="-DMDB_USE_ROBUST=0" && export CFLAGS="-DMDB_USE_ROBUST=0"
|
[[ $1 == "v7" ]] && arch=(armeabi-v7a)
|
||||||
cargo ndk -t ${arch} build ${release_param}
|
[[ $1 == "v8" ]] && arch=(arm64-v8a)
|
||||||
unset CPPFLAGS && unset CFLAGS
|
[[ $1 == "x86" ]] && arch=(x86_64)
|
||||||
cargo ndk -t ${arch} -o android/app/src/main/jniLibs build ${release_param}
|
|
||||||
if [ $? -eq 0 ]
|
|
||||||
then
|
|
||||||
success=1
|
|
||||||
fi
|
|
||||||
|
|
||||||
sed -i -e 's/"cdylib","rlib"/"rlib"/g' Cargo.toml
|
sed -i -e 's/"rlib"/"cdylib","rlib"/g' Cargo.toml
|
||||||
|
|
||||||
|
# Fix for https://stackoverflow.com/questions/57193895/error-use-of-undeclared-identifier-pthread-mutex-robust-cargo-build-liblmdb-s
|
||||||
|
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
|
||||||
|
fi
|
||||||
|
|
||||||
|
sed -i -e 's/"cdylib","rlib"/"rlib"/g' Cargo.toml
|
||||||
|
}
|
||||||
|
|
||||||
|
### Build application
|
||||||
|
function build_apk() {
|
||||||
|
version=$(grep -m 1 -Po 'version = "\K[^"]*' Cargo.toml)
|
||||||
|
|
||||||
# Build Android application and launch at all connected devices
|
|
||||||
if [ $success -eq 1 ]
|
|
||||||
then
|
|
||||||
cd android
|
cd android
|
||||||
|
|
||||||
# Setup gradle argument
|
|
||||||
[[ $1 == "release" ]] && gradle_param+=(assembleRelease)
|
|
||||||
[[ $1 == "debug" ]] && gradle_param+=(build)
|
|
||||||
|
|
||||||
./gradlew clean
|
./gradlew clean
|
||||||
./gradlew ${gradle_param}
|
# Build signed apk if keystore exists
|
||||||
|
if [ ! -f keystore.properties ]; then
|
||||||
|
./gradlew assembleRelease
|
||||||
|
apk_path=app/build/outputs/apk/release/app-release.apk
|
||||||
|
else
|
||||||
|
./gradlew assembleSignedRelease
|
||||||
|
apk_path=app/build/outputs/apk/signedRelease/app-signedRelease.apk
|
||||||
|
fi
|
||||||
|
|
||||||
# Setup apk path
|
if [[ $1 == "" ]]; then
|
||||||
[[ $1 == "release" ]] && apk_path+=(app/build/outputs/apk/release/app-release.apk)
|
# Launch application at all connected devices.
|
||||||
[[ $1 == "debug" ]] && apk_path+=(app/build/outputs/apk/debug/app-debug.apk)
|
for SERIAL in $(adb devices | grep -v List | cut -f 1);
|
||||||
|
do
|
||||||
|
adb -s $SERIAL install ${apk_path}
|
||||||
|
sleep 1s
|
||||||
|
adb -s $SERIAL shell am start -n mw.gri.android/.MainActivity;
|
||||||
|
done
|
||||||
|
else
|
||||||
|
# Setup release file name
|
||||||
|
name=grim-${version}-android-$1.apk
|
||||||
|
[[ $1 == "arm" ]] && name=grim-${version}-android.apk
|
||||||
|
rm -rf ${name}
|
||||||
|
mv ${apk_path} ${name}
|
||||||
|
|
||||||
for SERIAL in $(adb devices | grep -v List | cut -f 1);
|
# Calculate checksum
|
||||||
do
|
checksum=grim-${version}-android-$1-sha256sum.txt
|
||||||
adb -s $SERIAL install ${apk_path}
|
[[ $1 == "arm" ]] && checksum=grim-${version}-android-sha256sum.txt
|
||||||
sleep 1s
|
rm -rf ${checksum}
|
||||||
adb -s $SERIAL shell am start -n mw.gri.android/.MainActivity;
|
sha256sum ${name} > ${checksum}
|
||||||
done
|
fi
|
||||||
|
|
||||||
|
cd ..
|
||||||
|
}
|
||||||
|
|
||||||
|
rm -rf android/app/src/main/jniLibs/*
|
||||||
|
|
||||||
|
if [[ $1 == "build" ]]; then
|
||||||
|
build_lib $2
|
||||||
|
[ $success -eq 1 ] && build_apk
|
||||||
|
else
|
||||||
|
rm -rf target/release-apk
|
||||||
|
rm -rf target/aarch64-linux-android
|
||||||
|
rm -rf target/x86_64-linux-android
|
||||||
|
rm -rf target/armv7-linux-androideabi
|
||||||
|
|
||||||
|
build_lib "v7"
|
||||||
|
[ $success -eq 1 ] && build_lib "v8"
|
||||||
|
[ $success -eq 1 ] && build_apk "arm"
|
||||||
|
rm -rf android/app/src/main/jniLibs/*
|
||||||
|
[ $success -eq 1 ] && build_lib "x86"
|
||||||
|
[ $success -eq 1 ] && build_apk "x86_64"
|
||||||
fi
|
fi
|
|
@ -32,25 +32,41 @@ lazy_static! {
|
||||||
/// Implements ui entry point and contains platform-specific callbacks.
|
/// Implements ui entry point and contains platform-specific callbacks.
|
||||||
pub struct App<Platform> {
|
pub struct App<Platform> {
|
||||||
/// Platform specific callbacks handler.
|
/// Platform specific callbacks handler.
|
||||||
pub(crate) platform: Platform,
|
pub platform: Platform,
|
||||||
|
|
||||||
/// Main ui content.
|
/// Main content.
|
||||||
content: Content,
|
content: Content,
|
||||||
|
|
||||||
/// Last window resize direction.
|
/// Last window resize direction.
|
||||||
resize_direction: Option<ResizeDirection>
|
resize_direction: Option<ResizeDirection>,
|
||||||
|
|
||||||
|
/// Flag to check if it's first draw.
|
||||||
|
first_draw: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<Platform: PlatformCallbacks> App<Platform> {
|
impl<Platform: PlatformCallbacks> App<Platform> {
|
||||||
pub fn new(platform: Platform) -> Self {
|
pub fn new(platform: Platform) -> Self {
|
||||||
Self { platform, content: Content::default(), resize_direction: None }
|
Self {
|
||||||
|
platform,
|
||||||
|
content: Content::default(),
|
||||||
|
resize_direction: None,
|
||||||
|
first_draw: true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Draw application content.
|
/// Draw application content.
|
||||||
pub fn ui(&mut self, ctx: &Context) {
|
pub fn ui(&mut self, ctx: &Context) {
|
||||||
|
// Set platform context on first draw.
|
||||||
|
if self.first_draw {
|
||||||
|
if View::is_desktop() {
|
||||||
|
self.platform.set_context(ctx);
|
||||||
|
}
|
||||||
|
self.first_draw = false;
|
||||||
|
}
|
||||||
|
|
||||||
// Handle Esc keyboard key event and platform Back button key event.
|
// Handle Esc keyboard key event and platform Back button key event.
|
||||||
let back_pressed = BACK_BUTTON_PRESSED.load(Ordering::Relaxed);
|
let back_pressed = BACK_BUTTON_PRESSED.load(Ordering::Relaxed);
|
||||||
if ctx.input_mut(|i| i.consume_key(Modifiers::NONE, egui::Key::Escape)) || back_pressed {
|
if back_pressed || ctx.input_mut(|i| i.consume_key(Modifiers::NONE, egui::Key::Escape)) {
|
||||||
self.content.on_back();
|
self.content.on_back();
|
||||||
if back_pressed {
|
if back_pressed {
|
||||||
BACK_BUTTON_PRESSED.store(false, Ordering::Relaxed);
|
BACK_BUTTON_PRESSED.store(false, Ordering::Relaxed);
|
||||||
|
@ -59,8 +75,8 @@ impl<Platform: PlatformCallbacks> App<Platform> {
|
||||||
ctx.request_repaint();
|
ctx.request_repaint();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle Close event (on desktop).
|
// Handle Close event on desktop.
|
||||||
if ctx.input(|i| i.viewport().close_requested()) {
|
if View::is_desktop() && ctx.input(|i| i.viewport().close_requested()) {
|
||||||
if !self.content.exit_allowed {
|
if !self.content.exit_allowed {
|
||||||
ctx.send_viewport_cmd(ViewportCommand::CancelClose);
|
ctx.send_viewport_cmd(ViewportCommand::CancelClose);
|
||||||
Content::show_exit_modal();
|
Content::show_exit_modal();
|
||||||
|
@ -92,7 +108,20 @@ impl<Platform: PlatformCallbacks> App<Platform> {
|
||||||
}
|
}
|
||||||
self.content.ui(ui, &self.platform);
|
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.
|
/// Draw custom resizeable window content.
|
||||||
|
|
|
@ -31,10 +31,14 @@ const YELLOW: Color32 = Color32::from_rgb(254, 241, 2);
|
||||||
const YELLOW_DARK: Color32 = Color32::from_rgb(239, 229, 3);
|
const YELLOW_DARK: Color32 = Color32::from_rgb(239, 229, 3);
|
||||||
|
|
||||||
const GREEN: Color32 = Color32::from_rgb(0, 0x64, 0);
|
const GREEN: Color32 = Color32::from_rgb(0, 0x64, 0);
|
||||||
|
const GREEN_DARK: Color32 = Color32::from_rgb(0, (0x64 as f32 * 1.3 + 0.5) as u8, 0);
|
||||||
|
|
||||||
const RED: Color32 = Color32::from_rgb(0x8B, 0, 0);
|
const RED: Color32 = Color32::from_rgb(0x8B, 0, 0);
|
||||||
|
const RED_DARK: Color32 = Color32::from_rgb((0x8B as f32 * 1.3 + 0.5) as u8, 0, 0);
|
||||||
|
|
||||||
const BLUE: Color32 = Color32::from_rgb(0, 0x66, 0xE4);
|
const BLUE: Color32 = Color32::from_rgb(0, 0x66, 0xE4);
|
||||||
|
const BLUE_DARK: Color32 =
|
||||||
|
Color32::from_rgb(0, (0x66 as f32 * 1.3 + 0.5) as u8, (0xE4 as f32 * 1.3 + 0.5) as u8);
|
||||||
|
|
||||||
const FILL: Color32 = Color32::from_gray(244);
|
const FILL: Color32 = Color32::from_gray(244);
|
||||||
const FILL_DARK: Color32 = Color32::from_gray(24);
|
const FILL_DARK: Color32 = Color32::from_gray(24);
|
||||||
|
@ -125,7 +129,7 @@ impl Colors {
|
||||||
|
|
||||||
pub fn green() -> Color32 {
|
pub fn green() -> Color32 {
|
||||||
if use_dark() {
|
if use_dark() {
|
||||||
GREEN.gamma_multiply(1.3)
|
GREEN_DARK
|
||||||
} else {
|
} else {
|
||||||
GREEN
|
GREEN
|
||||||
}
|
}
|
||||||
|
@ -133,7 +137,7 @@ impl Colors {
|
||||||
|
|
||||||
pub fn red() -> Color32 {
|
pub fn red() -> Color32 {
|
||||||
if use_dark() {
|
if use_dark() {
|
||||||
RED.gamma_multiply(1.3)
|
RED_DARK
|
||||||
} else {
|
} else {
|
||||||
RED
|
RED
|
||||||
}
|
}
|
||||||
|
@ -141,7 +145,7 @@ impl Colors {
|
||||||
|
|
||||||
pub fn blue() -> Color32 {
|
pub fn blue() -> Color32 {
|
||||||
if use_dark() {
|
if use_dark() {
|
||||||
BLUE.gamma_multiply(1.3)
|
BLUE_DARK
|
||||||
} else {
|
} else {
|
||||||
BLUE
|
BLUE
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,7 +30,11 @@ use crate::gui::platform::PlatformCallbacks;
|
||||||
/// Android platform implementation.
|
/// Android platform implementation.
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Android {
|
pub struct Android {
|
||||||
|
/// Android related state.
|
||||||
android_app: AndroidApp,
|
android_app: AndroidApp,
|
||||||
|
|
||||||
|
/// Context to repaint content and handle viewport commands.
|
||||||
|
ctx: Arc<RwLock<Option<egui::Context>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Android {
|
impl Android {
|
||||||
|
@ -38,6 +42,7 @@ impl Android {
|
||||||
pub fn new(app: AndroidApp) -> Self {
|
pub fn new(app: AndroidApp) -> Self {
|
||||||
Self {
|
Self {
|
||||||
android_app: app,
|
android_app: app,
|
||||||
|
ctx: Arc::new(RwLock::new(None)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -56,27 +61,36 @@ impl Android {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PlatformCallbacks for Android {
|
impl PlatformCallbacks for Android {
|
||||||
|
fn set_context(&mut self, ctx: &egui::Context) {
|
||||||
|
let mut w_ctx = self.ctx.write();
|
||||||
|
*w_ctx = Some(ctx.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn exit(&self) {
|
||||||
|
let _ = self.call_java_method("exit", "()V", &[]);
|
||||||
|
}
|
||||||
|
|
||||||
fn show_keyboard(&self) {
|
fn show_keyboard(&self) {
|
||||||
// Disable NDK soft input show call before fix for egui.
|
// Disable NDK soft input show call before fix for egui.
|
||||||
// self.android_app.show_soft_input(false);
|
// self.android_app.show_soft_input(false);
|
||||||
|
|
||||||
self.call_java_method("showKeyboard", "()V", &[]).unwrap();
|
let _ = self.call_java_method("showKeyboard", "()V", &[]);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn hide_keyboard(&self) {
|
fn hide_keyboard(&self) {
|
||||||
// Disable NDK soft input hide call before fix for egui.
|
// Disable NDK soft input hide call before fix for egui.
|
||||||
// self.android_app.hide_soft_input(false);
|
// self.android_app.hide_soft_input(false);
|
||||||
|
|
||||||
self.call_java_method("hideKeyboard", "()V", &[]).unwrap();
|
let _ = self.call_java_method("hideKeyboard", "()V", &[]);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn copy_string_to_buffer(&self, data: String) {
|
fn copy_string_to_buffer(&self, data: String) {
|
||||||
let vm = unsafe { jni::JavaVM::from_raw(self.android_app.vm_as_ptr() as _) }.unwrap();
|
let vm = unsafe { jni::JavaVM::from_raw(self.android_app.vm_as_ptr() as _) }.unwrap();
|
||||||
let env = vm.attach_current_thread().unwrap();
|
let env = vm.attach_current_thread().unwrap();
|
||||||
let arg_value = env.new_string(data).unwrap();
|
let arg_value = env.new_string(data).unwrap();
|
||||||
self.call_java_method("copyText",
|
let _ = self.call_java_method("copyText",
|
||||||
"(Ljava/lang/String;)V",
|
"(Ljava/lang/String;)V",
|
||||||
&[JValue::Object(&JObject::from(arg_value))]).unwrap();
|
&[JValue::Object(&JObject::from(arg_value))]);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_string_from_buffer(&self) -> String {
|
fn get_string_from_buffer(&self) -> String {
|
||||||
|
@ -95,12 +109,12 @@ impl PlatformCallbacks for Android {
|
||||||
let mut w_image = LAST_CAMERA_IMAGE.write();
|
let mut w_image = LAST_CAMERA_IMAGE.write();
|
||||||
*w_image = None;
|
*w_image = None;
|
||||||
// Start camera.
|
// Start camera.
|
||||||
self.call_java_method("startCamera", "()V", &[]).unwrap();
|
let _ = self.call_java_method("startCamera", "()V", &[]);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn stop_camera(&self) {
|
fn stop_camera(&self) {
|
||||||
// Stop camera.
|
// Stop camera.
|
||||||
self.call_java_method("stopCamera", "()V", &[]).unwrap();
|
let _ = self.call_java_method("stopCamera", "()V", &[]);
|
||||||
// Clear image.
|
// Clear image.
|
||||||
let mut w_image = LAST_CAMERA_IMAGE.write();
|
let mut w_image = LAST_CAMERA_IMAGE.write();
|
||||||
*w_image = None;
|
*w_image = None;
|
||||||
|
@ -115,32 +129,39 @@ impl PlatformCallbacks for Android {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn can_switch_camera(&self) -> bool {
|
fn can_switch_camera(&self) -> bool {
|
||||||
let result = self.call_java_method("camerasAmount", "()I", &[]).unwrap();
|
if let Some(res) = self.call_java_method("camerasAmount", "()I", &[]) {
|
||||||
let amount = unsafe { result.i };
|
let amount = unsafe { res.i };
|
||||||
amount > 1
|
return amount > 1;
|
||||||
|
}
|
||||||
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
fn switch_camera(&self) {
|
fn switch_camera(&self) {
|
||||||
self.call_java_method("switchCamera", "()V", &[]).unwrap();
|
let _ = self.call_java_method("switchCamera", "()V", &[]);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn share_data(&self, name: String, data: Vec<u8>) -> Result<(), std::io::Error> {
|
fn share_data(&self, name: String, data: Vec<u8>) -> Result<(), std::io::Error> {
|
||||||
// Create file at cache dir.
|
|
||||||
let default_cache = OsString::from(dirs::cache_dir().unwrap());
|
let default_cache = OsString::from(dirs::cache_dir().unwrap());
|
||||||
let mut cache = PathBuf::from(env::var_os("XDG_CACHE_HOME").unwrap_or(default_cache));
|
let mut file = PathBuf::from(env::var_os("XDG_CACHE_HOME").unwrap_or(default_cache));
|
||||||
cache.push("images");
|
// File path for Android provider.
|
||||||
std::fs::create_dir_all(cache.to_str().unwrap())?;
|
file.push("images");
|
||||||
cache.push(name);
|
if !file.exists() {
|
||||||
let mut image = File::create_new(cache.clone()).unwrap();
|
std::fs::create_dir(file.clone())?;
|
||||||
image.write_all(data.as_slice()).unwrap();
|
}
|
||||||
image.sync_all().unwrap();
|
file.push(name);
|
||||||
|
if file.exists() {
|
||||||
|
std::fs::remove_file(file.clone())?;
|
||||||
|
}
|
||||||
|
let mut image = File::create_new(file.clone())?;
|
||||||
|
image.write_all(data.as_slice())?;
|
||||||
|
image.sync_all()?;
|
||||||
// Call share modal at system.
|
// Call share modal at system.
|
||||||
let vm = unsafe { jni::JavaVM::from_raw(self.android_app.vm_as_ptr() as _) }.unwrap();
|
let vm = unsafe { jni::JavaVM::from_raw(self.android_app.vm_as_ptr() as _) }.unwrap();
|
||||||
let env = vm.attach_current_thread().unwrap();
|
let env = vm.attach_current_thread().unwrap();
|
||||||
let arg_value = env.new_string(cache.to_str().unwrap()).unwrap();
|
let arg_value = env.new_string(file.to_str().unwrap()).unwrap();
|
||||||
self.call_java_method("shareImage",
|
let _ = self.call_java_method("shareImage",
|
||||||
"(Ljava/lang/String;)V",
|
"(Ljava/lang/String;)V",
|
||||||
&[JValue::Object(&JObject::from(arg_value))]).unwrap();
|
&[JValue::Object(&JObject::from(arg_value))]);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -149,7 +170,7 @@ impl PlatformCallbacks for Android {
|
||||||
let mut w_path = PICKED_FILE_PATH.write();
|
let mut w_path = PICKED_FILE_PATH.write();
|
||||||
*w_path = None;
|
*w_path = None;
|
||||||
// Launch file picker.
|
// Launch file picker.
|
||||||
let _ = self.call_java_method("pickFile", "()V", &[]).unwrap();
|
let _ = self.call_java_method("pickFile", "()V", &[]);
|
||||||
// Return empty string to identify async pick.
|
// Return empty string to identify async pick.
|
||||||
Some("".to_string())
|
Some("".to_string())
|
||||||
}
|
}
|
||||||
|
@ -167,6 +188,14 @@ impl PlatformCallbacks for Android {
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn request_user_attention(&self) {}
|
||||||
|
|
||||||
|
fn user_attention_required(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clear_user_attention(&self) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
|
|
|
@ -13,12 +13,13 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io:: Write;
|
use std::io::Write;
|
||||||
use lazy_static::lazy_static;
|
use std::thread;
|
||||||
use parking_lot::RwLock;
|
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::thread;
|
use parking_lot::RwLock;
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
use egui::{UserAttentionType, ViewportCommand, WindowLevel};
|
||||||
use rfd::FileDialog;
|
use rfd::FileDialog;
|
||||||
|
|
||||||
use crate::gui::platform::PlatformCallbacks;
|
use crate::gui::platform::PlatformCallbacks;
|
||||||
|
@ -26,19 +27,30 @@ use crate::gui::platform::PlatformCallbacks;
|
||||||
/// Desktop platform related actions.
|
/// Desktop platform related actions.
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Desktop {
|
pub struct Desktop {
|
||||||
|
/// Context to repaint content and handle viewport commands.
|
||||||
|
ctx: Arc<RwLock<Option<egui::Context>>>,
|
||||||
|
|
||||||
/// Flag to check if camera stop is needed.
|
/// Flag to check if camera stop is needed.
|
||||||
stop_camera: Arc<AtomicBool>,
|
stop_camera: Arc<AtomicBool>,
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Desktop {
|
/// Flag to check if attention required after window focusing.
|
||||||
fn default() -> Self {
|
attention_required: Arc<AtomicBool>,
|
||||||
Self {
|
|
||||||
stop_camera: Arc::new(AtomicBool::new(false)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PlatformCallbacks for Desktop {
|
impl PlatformCallbacks for Desktop {
|
||||||
|
fn set_context(&mut self, ctx: &egui::Context) {
|
||||||
|
let mut w_ctx = self.ctx.write();
|
||||||
|
*w_ctx = Some(ctx.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn exit(&self) {
|
||||||
|
let r_ctx = self.ctx.read();
|
||||||
|
if r_ctx.is_some() {
|
||||||
|
let ctx = r_ctx.as_ref().unwrap();
|
||||||
|
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn show_keyboard(&self) {}
|
fn show_keyboard(&self) {}
|
||||||
|
|
||||||
fn hide_keyboard(&self) {}
|
fn hide_keyboard(&self) {}
|
||||||
|
@ -119,9 +131,55 @@ impl PlatformCallbacks for Desktop {
|
||||||
fn picked_file(&self) -> Option<String> {
|
fn picked_file(&self) -> Option<String> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn user_attention_required(&self) -> bool {
|
||||||
|
self.attention_required.load(Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Desktop {
|
impl Desktop {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
stop_camera: Arc::new(AtomicBool::new(false)),
|
||||||
|
ctx: Arc::new(RwLock::new(None)),
|
||||||
|
attention_required: Arc::new(AtomicBool::new(false)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
fn start_camera_capture(stop_camera: Arc<AtomicBool>) {
|
fn start_camera_capture(stop_camera: Arc<AtomicBool>) {
|
||||||
|
@ -168,36 +226,35 @@ impl Desktop {
|
||||||
|
|
||||||
let ctx = PlatformContext::default();
|
let ctx = PlatformContext::default();
|
||||||
let devices = ctx.devices().unwrap();
|
let devices = ctx.devices().unwrap();
|
||||||
let dev = ctx.open_device(&devices[0].uri).unwrap();
|
if let Ok(dev) = ctx.open_device(&devices[0].uri) {
|
||||||
|
let streams = dev.streams().unwrap();
|
||||||
|
let stream_desc = streams[0].clone();
|
||||||
|
let w = stream_desc.width;
|
||||||
|
let h = stream_desc.height;
|
||||||
|
|
||||||
let streams = dev.streams().unwrap();
|
let mut stream = dev.start_stream(&stream_desc).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.
|
||||||
loop {
|
if stop_camera.load(Ordering::Relaxed) {
|
||||||
// Stop if camera was stopped.
|
stop_camera.store(false, Ordering::Relaxed);
|
||||||
if stop_camera.load(Ordering::Relaxed) {
|
let mut w_image = LAST_CAMERA_IMAGE.write();
|
||||||
stop_camera.store(false, Ordering::Relaxed);
|
*w_image = None;
|
||||||
// Clear image.
|
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();
|
let mut w_image = LAST_CAMERA_IMAGE.write();
|
||||||
*w_image = None;
|
*w_image = Some((out, 0));
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,8 @@ pub mod platform;
|
||||||
pub mod platform;
|
pub mod platform;
|
||||||
|
|
||||||
pub trait PlatformCallbacks {
|
pub trait PlatformCallbacks {
|
||||||
|
fn set_context(&mut self, ctx: &egui::Context);
|
||||||
|
fn exit(&self);
|
||||||
fn show_keyboard(&self);
|
fn show_keyboard(&self);
|
||||||
fn hide_keyboard(&self);
|
fn hide_keyboard(&self);
|
||||||
fn copy_string_to_buffer(&self, data: String);
|
fn copy_string_to_buffer(&self, data: String);
|
||||||
|
@ -34,4 +36,7 @@ pub trait PlatformCallbacks {
|
||||||
fn share_data(&self, name: String, data: Vec<u8>) -> Result<(), std::io::Error>;
|
fn share_data(&self, name: String, data: Vec<u8>) -> Result<(), std::io::Error>;
|
||||||
fn pick_file(&self) -> Option<String>;
|
fn pick_file(&self) -> Option<String>;
|
||||||
fn picked_file(&self) -> Option<String>;
|
fn picked_file(&self) -> Option<String>;
|
||||||
|
fn request_user_attention(&self);
|
||||||
|
fn user_attention_required(&self) -> bool;
|
||||||
|
fn clear_user_attention(&self);
|
||||||
}
|
}
|
|
@ -40,8 +40,8 @@ pub struct Content {
|
||||||
/// Central panel [`WalletsContent`] content.
|
/// Central panel [`WalletsContent`] content.
|
||||||
pub wallets: WalletsContent,
|
pub wallets: WalletsContent,
|
||||||
|
|
||||||
/// Check if app exit is allowed on close event of [`eframe::App`] implementation.
|
/// Check if app exit is allowed on Desktop close event.
|
||||||
pub(crate) exit_allowed: bool,
|
pub exit_allowed: bool,
|
||||||
/// Flag to show exit progress at [`Modal`].
|
/// Flag to show exit progress at [`Modal`].
|
||||||
show_exit_progress: bool,
|
show_exit_progress: bool,
|
||||||
|
|
||||||
|
@ -83,7 +83,7 @@ impl ModalContainer for Content {
|
||||||
modal: &Modal,
|
modal: &Modal,
|
||||||
cb: &dyn PlatformCallbacks) {
|
cb: &dyn PlatformCallbacks) {
|
||||||
match modal.id {
|
match modal.id {
|
||||||
Self::EXIT_CONFIRMATION_MODAL => self.exit_modal_content(ui, modal),
|
Self::EXIT_CONFIRMATION_MODAL => self.exit_modal_content(ui, modal, cb),
|
||||||
Self::SETTINGS_MODAL => self.settings_modal_ui(ui, modal),
|
Self::SETTINGS_MODAL => self.settings_modal_ui(ui, modal),
|
||||||
Self::ANDROID_INTEGRATED_NODE_WARNING_MODAL => self.android_warning_modal_ui(ui, modal),
|
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),
|
Self::CRASH_REPORT_MODAL => self.crash_report_modal_ui(ui, modal, cb),
|
||||||
|
@ -206,11 +206,11 @@ impl Content {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Draw exit confirmation modal content.
|
/// Draw exit confirmation modal content.
|
||||||
fn exit_modal_content(&mut self, ui: &mut egui::Ui, modal: &Modal) {
|
fn exit_modal_content(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
|
||||||
if self.show_exit_progress {
|
if self.show_exit_progress {
|
||||||
if !Node::is_running() {
|
if !Node::is_running() {
|
||||||
self.exit_allowed = true;
|
self.exit_allowed = true;
|
||||||
ui.ctx().send_viewport_cmd(egui::ViewportCommand::Close);
|
cb.exit();
|
||||||
modal.close();
|
modal.close();
|
||||||
}
|
}
|
||||||
ui.add_space(16.0);
|
ui.add_space(16.0);
|
||||||
|
@ -241,10 +241,10 @@ impl Content {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
columns[1].vertical_centered_justified(|ui| {
|
columns[1].vertical_centered_justified(|ui| {
|
||||||
View::button_ui(ui, t!("modal_exit.exit"), Colors::white_or_black(false), |ui| {
|
View::button_ui(ui, t!("modal_exit.exit"), Colors::white_or_black(false), |_| {
|
||||||
if !Node::is_running() {
|
if !Node::is_running() {
|
||||||
self.exit_allowed = true;
|
self.exit_allowed = true;
|
||||||
ui.ctx().send_viewport_cmd(egui::ViewportCommand::Close);
|
cb.exit();
|
||||||
modal.close();
|
modal.close();
|
||||||
} else {
|
} else {
|
||||||
Node::stop(true);
|
Node::stop(true);
|
||||||
|
|
|
@ -36,8 +36,8 @@ pub use camera::*;
|
||||||
mod qr;
|
mod qr;
|
||||||
pub use qr::*;
|
pub use qr::*;
|
||||||
|
|
||||||
mod file;
|
mod file_pick;
|
||||||
pub use file::*;
|
pub use file_pick::*;
|
||||||
|
|
||||||
mod pull_to_refresh;
|
mod pull_to_refresh;
|
||||||
pub use pull_to_refresh::*;
|
pub use pull_to_refresh::*;
|
|
@ -35,7 +35,7 @@ pub struct Modal {
|
||||||
/// Identifier for modal.
|
/// Identifier for modal.
|
||||||
pub(crate) id: &'static str,
|
pub(crate) id: &'static str,
|
||||||
/// Position on the screen.
|
/// Position on the screen.
|
||||||
position: ModalPosition,
|
pub position: ModalPosition,
|
||||||
/// To check if it can be closed.
|
/// To check if it can be closed.
|
||||||
closeable: Arc<AtomicBool>,
|
closeable: Arc<AtomicBool>,
|
||||||
/// Title text
|
/// Title text
|
||||||
|
@ -64,6 +64,12 @@ impl Modal {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Change [`Modal`] position on the screen.
|
||||||
|
pub fn change_position(position: ModalPosition) {
|
||||||
|
let mut w_state = MODAL_STATE.write();
|
||||||
|
w_state.modal.as_mut().unwrap().position = position;
|
||||||
|
}
|
||||||
|
|
||||||
/// Mark [`Modal`] closed.
|
/// Mark [`Modal`] closed.
|
||||||
pub fn close(&self) {
|
pub fn close(&self) {
|
||||||
let mut w_nav = MODAL_STATE.write();
|
let mut w_nav = MODAL_STATE.write();
|
||||||
|
|
|
@ -83,7 +83,7 @@ impl Default for StratumSetup {
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
wallets: WalletList::default(),
|
wallets: WalletList::default(),
|
||||||
wallets_modal: WalletsModal::new(wallet_id),
|
wallets_modal: WalletsModal::new(wallet_id, None, false),
|
||||||
available_ips: NodeConfig::get_ip_addrs(),
|
available_ips: NodeConfig::get_ip_addrs(),
|
||||||
stratum_port_edit: port,
|
stratum_port_edit: port,
|
||||||
stratum_port_available_edit: is_port_available,
|
stratum_port_available_edit: is_port_available,
|
||||||
|
@ -111,10 +111,12 @@ impl ModalContainer for StratumSetup {
|
||||||
modal: &Modal,
|
modal: &Modal,
|
||||||
cb: &dyn PlatformCallbacks) {
|
cb: &dyn PlatformCallbacks) {
|
||||||
match modal.id {
|
match modal.id {
|
||||||
WALLET_SELECTION_MODAL => self.wallets_modal.ui(ui, modal, &self.wallets, |id| {
|
WALLET_SELECTION_MODAL => {
|
||||||
NodeConfig::save_stratum_wallet_id(id);
|
self.wallets_modal.ui(ui, modal, &mut self.wallets, cb, |id, _| {
|
||||||
self.wallet_name = WalletConfig::name_by_id(id);
|
NodeConfig::save_stratum_wallet_id(id);
|
||||||
}),
|
self.wallet_name = WalletConfig::name_by_id(id);
|
||||||
|
})
|
||||||
|
},
|
||||||
STRATUM_PORT_MODAL => self.port_modal(ui, modal, cb),
|
STRATUM_PORT_MODAL => self.port_modal(ui, modal, cb),
|
||||||
ATTEMPT_TIME_MODAL => self.attempt_modal(ui, modal, cb),
|
ATTEMPT_TIME_MODAL => self.attempt_modal(ui, modal, cb),
|
||||||
MIN_SHARE_DIFF_MODAL => self.min_diff_modal(ui, modal, cb),
|
MIN_SHARE_DIFF_MODAL => self.min_diff_modal(ui, modal, cb),
|
||||||
|
@ -240,7 +242,7 @@ impl StratumSetup {
|
||||||
|
|
||||||
/// Show wallet selection [`Modal`].
|
/// Show wallet selection [`Modal`].
|
||||||
fn show_wallets_modal(&mut self) {
|
fn show_wallets_modal(&mut self) {
|
||||||
self.wallets_modal = WalletsModal::new(NodeConfig::get_stratum_wallet_id());
|
self.wallets_modal = WalletsModal::new(NodeConfig::get_stratum_wallet_id(), None, false);
|
||||||
// Show modal.
|
// Show modal.
|
||||||
Modal::new(WALLET_SELECTION_MODAL)
|
Modal::new(WALLET_SELECTION_MODAL)
|
||||||
.position(ModalPosition::Center)
|
.position(ModalPosition::Center)
|
||||||
|
|
|
@ -30,8 +30,8 @@ use crate::gui::views::View;
|
||||||
|
|
||||||
/// QR code image from text.
|
/// QR code image from text.
|
||||||
pub struct QrCodeContent {
|
pub struct QrCodeContent {
|
||||||
/// Text to create QR code.
|
/// QR code text.
|
||||||
pub(crate) text: String,
|
text: String,
|
||||||
|
|
||||||
/// Flag to draw animated QR with Uniform Resources
|
/// Flag to draw animated QR with Uniform Resources
|
||||||
/// https://github.com/BlockchainCommons/Research/blob/master/papers/bcr-2020-005-ur.md
|
/// https://github.com/BlockchainCommons/Research/blob/master/papers/bcr-2020-005-ur.md
|
||||||
|
@ -62,18 +62,18 @@ impl QrCodeContent {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Draw QR code.
|
/// Draw QR code.
|
||||||
pub fn ui(&mut self, ui: &mut egui::Ui, text: String, cb: &dyn PlatformCallbacks) {
|
pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||||
if self.animated {
|
if self.animated {
|
||||||
// Show animated QR code.
|
// Show animated QR code.
|
||||||
self.animated_ui(ui, text, cb);
|
self.animated_ui(ui, cb);
|
||||||
} else {
|
} else {
|
||||||
// Show static QR code.
|
// Show static QR code.
|
||||||
self.static_ui(ui, text, cb);
|
self.static_ui(ui, cb);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Draw animated QR code content.
|
/// Draw animated QR code content.
|
||||||
fn animated_ui(&mut self, ui: &mut egui::Ui, text: String, cb: &dyn PlatformCallbacks) {
|
fn animated_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||||
if !self.has_image() {
|
if !self.has_image() {
|
||||||
let space = (ui.available_width() - View::BIG_SPINNER_SIZE) / 2.0;
|
let space = (ui.available_width() - View::BIG_SPINNER_SIZE) / 2.0;
|
||||||
ui.vertical_centered(|ui| {
|
ui.vertical_centered(|ui| {
|
||||||
|
@ -84,7 +84,7 @@ impl QrCodeContent {
|
||||||
|
|
||||||
// Create multiple vector images from text if not creating.
|
// Create multiple vector images from text if not creating.
|
||||||
if !self.loading() {
|
if !self.loading() {
|
||||||
self.create_svg_list(text);
|
self.create_svg_list();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let svg_list = {
|
let svg_list = {
|
||||||
|
@ -111,7 +111,7 @@ impl QrCodeContent {
|
||||||
|
|
||||||
// Show QR code text.
|
// Show QR code text.
|
||||||
ui.add_space(6.0);
|
ui.add_space(6.0);
|
||||||
View::ellipsize_text(ui, text.clone(), 16.0, Colors::inactive_text());
|
View::ellipsize_text(ui, self.text.clone(), 16.0, Colors::inactive_text());
|
||||||
ui.add_space(6.0);
|
ui.add_space(6.0);
|
||||||
|
|
||||||
ui.vertical_centered(|ui| {
|
ui.vertical_centered(|ui| {
|
||||||
|
@ -131,7 +131,7 @@ impl QrCodeContent {
|
||||||
w_state.exporting = true;
|
w_state.exporting = true;
|
||||||
}
|
}
|
||||||
// Create GIF to export.
|
// Create GIF to export.
|
||||||
self.create_qr_gif(text, DEFAULT_QR_SIZE as usize);
|
self.create_qr_gif();
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
ui.vertical_centered(|ui| {
|
ui.vertical_centered(|ui| {
|
||||||
|
@ -171,7 +171,7 @@ impl QrCodeContent {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Draw static QR code content.
|
/// Draw static QR code content.
|
||||||
fn static_ui(&mut self, ui: &mut egui::Ui, text: String, cb: &dyn PlatformCallbacks) {
|
fn static_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||||
if !self.has_image() {
|
if !self.has_image() {
|
||||||
let space = (ui.available_width() - View::BIG_SPINNER_SIZE) / 2.0;
|
let space = (ui.available_width() - View::BIG_SPINNER_SIZE) / 2.0;
|
||||||
ui.vertical_centered(|ui| {
|
ui.vertical_centered(|ui| {
|
||||||
|
@ -182,7 +182,7 @@ impl QrCodeContent {
|
||||||
|
|
||||||
// Create vector image from text if not creating.
|
// Create vector image from text if not creating.
|
||||||
if !self.loading() {
|
if !self.loading() {
|
||||||
self.create_svg(text);
|
self.create_svg();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Create image from SVG data.
|
// Create image from SVG data.
|
||||||
|
@ -194,7 +194,7 @@ impl QrCodeContent {
|
||||||
|
|
||||||
// Show QR code text.
|
// Show QR code text.
|
||||||
ui.add_space(6.0);
|
ui.add_space(6.0);
|
||||||
View::ellipsize_text(ui, text.clone(), 16.0, Colors::inactive_text());
|
View::ellipsize_text(ui, self.text.clone(), 16.0, Colors::inactive_text());
|
||||||
ui.add_space(6.0);
|
ui.add_space(6.0);
|
||||||
|
|
||||||
// Show button to share QR.
|
// Show button to share QR.
|
||||||
|
@ -204,21 +204,22 @@ impl QrCodeContent {
|
||||||
share_text,
|
share_text,
|
||||||
Colors::blue(),
|
Colors::blue(),
|
||||||
Colors::white_or_black(false), || {
|
Colors::white_or_black(false), || {
|
||||||
if let Ok(qr) = QrCode::encode_text(text.as_str(), qrcodegen::QrCodeEcc::Low) {
|
let text = self.text.as_str();
|
||||||
if let Some(data) = Self::qr_to_image_data(qr, DEFAULT_QR_SIZE as usize) {
|
if let Ok(qr) = QrCode::encode_text(text, qrcodegen::QrCodeEcc::Low) {
|
||||||
let mut png = vec![];
|
if let Some(data) = Self::qr_to_image_data(qr, DEFAULT_QR_SIZE as usize) {
|
||||||
let png_enc = PngEncoder::new_with_quality(&mut png,
|
let mut png = vec![];
|
||||||
CompressionType::Best,
|
let png_enc = PngEncoder::new_with_quality(&mut png,
|
||||||
FilterType::NoFilter);
|
CompressionType::Best,
|
||||||
if let Ok(()) = png_enc.write_image(data.as_slice(),
|
FilterType::NoFilter);
|
||||||
DEFAULT_QR_SIZE,
|
if let Ok(()) = png_enc.write_image(data.as_slice(),
|
||||||
DEFAULT_QR_SIZE,
|
DEFAULT_QR_SIZE,
|
||||||
ExtendedColorType::L8) {
|
DEFAULT_QR_SIZE,
|
||||||
let name = format!("{}.png", chrono::Utc::now().timestamp());
|
ExtendedColorType::L8) {
|
||||||
cb.share_data(name, png).unwrap_or_default();
|
let name = format!("{}.png", chrono::Utc::now().timestamp());
|
||||||
|
cb.share_data(name, png).unwrap_or_default();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
ui.add_space(8.0);
|
ui.add_space(8.0);
|
||||||
|
@ -267,8 +268,9 @@ impl QrCodeContent {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create multiple vector QR code images at separate thread.
|
/// Create multiple vector QR code images at separate thread.
|
||||||
fn create_svg_list(&self, text: String) {
|
fn create_svg_list(&self) {
|
||||||
let qr_state = self.qr_image_state.clone();
|
let qr_state = self.qr_image_state.clone();
|
||||||
|
let text = self.text.clone();
|
||||||
thread::spawn(move || {
|
thread::spawn(move || {
|
||||||
let mut encoder = ur::Encoder::bytes(text.as_bytes(), 100).unwrap();
|
let mut encoder = ur::Encoder::bytes(text.as_bytes(), 100).unwrap();
|
||||||
let mut data = Vec::with_capacity(encoder.fragment_count());
|
let mut data = Vec::with_capacity(encoder.fragment_count());
|
||||||
|
@ -294,8 +296,9 @@ impl QrCodeContent {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create vector QR code image at separate thread.
|
/// Create vector QR code image at separate thread.
|
||||||
fn create_svg(&self, text: String) {
|
fn create_svg(&self) {
|
||||||
let qr_state = self.qr_image_state.clone();
|
let qr_state = self.qr_image_state.clone();
|
||||||
|
let text = self.text.clone();
|
||||||
thread::spawn(move || {
|
thread::spawn(move || {
|
||||||
if let Ok(qr) = QrCode::encode_text(text.as_str(), qrcodegen::QrCodeEcc::Low) {
|
if let Ok(qr) = QrCode::encode_text(text.as_str(), qrcodegen::QrCodeEcc::Low) {
|
||||||
let svg = Self::qr_to_svg(qr, 0);
|
let svg = Self::qr_to_svg(qr, 0);
|
||||||
|
@ -332,13 +335,14 @@ impl QrCodeContent {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create GIF image at separate thread.
|
/// Create GIF image at separate thread.
|
||||||
fn create_qr_gif(&self, text: String, size: usize) {
|
fn create_qr_gif(&self) {
|
||||||
{
|
{
|
||||||
let mut w_state = self.qr_image_state.write();
|
let mut w_state = self.qr_image_state.write();
|
||||||
w_state.gif_creating = true;
|
w_state.gif_creating = true;
|
||||||
|
|
||||||
}
|
}
|
||||||
let qr_state = self.qr_image_state.clone();
|
let qr_state = self.qr_image_state.clone();
|
||||||
|
let text = self.text.clone();
|
||||||
thread::spawn(move || {
|
thread::spawn(move || {
|
||||||
// Setup GIF image encoder.
|
// Setup GIF image encoder.
|
||||||
let mut gif = vec![];
|
let mut gif = vec![];
|
||||||
|
@ -354,7 +358,7 @@ impl QrCodeContent {
|
||||||
) {
|
) {
|
||||||
// Create an image from QR data.
|
// Create an image from QR data.
|
||||||
let image = qr.render()
|
let image = qr.render()
|
||||||
.max_dimensions(size as u32, size as u32)
|
.max_dimensions(DEFAULT_QR_SIZE, DEFAULT_QR_SIZE)
|
||||||
.dark_color(image::Rgb([0, 0, 0]))
|
.dark_color(image::Rgb([0, 0, 0]))
|
||||||
.light_color(image::Rgb([255, 255, 255]))
|
.light_color(image::Rgb([255, 255, 255]))
|
||||||
.build();
|
.build();
|
||||||
|
|
|
@ -18,13 +18,14 @@ use egui::scroll_area::ScrollBarVisibility;
|
||||||
|
|
||||||
use crate::AppConfig;
|
use crate::AppConfig;
|
||||||
use crate::gui::Colors;
|
use crate::gui::Colors;
|
||||||
use crate::gui::icons::{ARROW_LEFT, CARET_RIGHT, COMPUTER_TOWER, FOLDER_LOCK, FOLDER_OPEN, GEAR, GLOBE, GLOBE_SIMPLE, LOCK_KEY, PLUS, SIDEBAR_SIMPLE, SPINNER, SUITCASE, WARNING_CIRCLE};
|
use crate::gui::icons::{ARROW_LEFT, CARET_RIGHT, COMPUTER_TOWER, FOLDER_OPEN, GEAR, GLOBE, GLOBE_SIMPLE, LOCK_KEY, PLUS, SIDEBAR_SIMPLE, SUITCASE};
|
||||||
use crate::gui::platform::PlatformCallbacks;
|
use crate::gui::platform::PlatformCallbacks;
|
||||||
use crate::gui::views::{Modal, Content, TitlePanel, View};
|
use crate::gui::views::{Modal, Content, TitlePanel, View};
|
||||||
use crate::gui::views::types::{ModalContainer, ModalPosition, TextEditOptions, TitleContentType, TitleType};
|
use crate::gui::views::types::{ModalContainer, ModalPosition, TitleContentType, TitleType};
|
||||||
use crate::gui::views::wallets::creation::WalletCreation;
|
use crate::gui::views::wallets::creation::WalletCreation;
|
||||||
use crate::gui::views::wallets::modals::WalletConnectionModal;
|
use crate::gui::views::wallets::modals::{OpenWalletModal, WalletConnectionModal, WalletsModal};
|
||||||
use crate::gui::views::wallets::types::WalletTabType;
|
use crate::gui::views::wallets::types::WalletTabType;
|
||||||
|
use crate::gui::views::wallets::wallet::types::status_text;
|
||||||
use crate::gui::views::wallets::WalletContent;
|
use crate::gui::views::wallets::WalletContent;
|
||||||
use crate::wallet::{Wallet, WalletList};
|
use crate::wallet::{Wallet, WalletList};
|
||||||
|
|
||||||
|
@ -33,13 +34,12 @@ pub struct WalletsContent {
|
||||||
/// List of wallets.
|
/// List of wallets.
|
||||||
wallets: WalletList,
|
wallets: WalletList,
|
||||||
|
|
||||||
/// Password to open wallet for [`Modal`].
|
/// Wallet selection [`Modal`] content.
|
||||||
pass_edit: String,
|
wallet_selection_content: Option<WalletsModal>,
|
||||||
/// Flag to check if wrong password was entered at [`Modal`].
|
/// Wallet opening [`Modal`] content.
|
||||||
wrong_pass: bool,
|
open_wallet_content: Option<OpenWalletModal>,
|
||||||
|
|
||||||
/// Wallet connection selection content.
|
/// Wallet connection selection content.
|
||||||
conn_modal_content: Option<WalletConnectionModal>,
|
conn_selection_content: Option<WalletConnectionModal>,
|
||||||
|
|
||||||
/// Selected [`Wallet`] content.
|
/// Selected [`Wallet`] content.
|
||||||
wallet_content: WalletContent,
|
wallet_content: WalletContent,
|
||||||
|
@ -54,24 +54,29 @@ pub struct WalletsContent {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Identifier for connection selection [`Modal`].
|
/// Identifier for connection selection [`Modal`].
|
||||||
const CONNECTION_SELECTION_MODAL: &'static str = "wallets_connection_selection_modal";
|
const CONNECTION_SELECTION_MODAL: &'static str = "wallets_connection_selection";
|
||||||
|
|
||||||
/// Identifier for wallet opening [`Modal`].
|
/// Identifier for wallet opening [`Modal`].
|
||||||
const OPEN_WALLET_MODAL: &'static str = "open_wallet_modal";
|
const OPEN_WALLET_MODAL: &'static str = "wallets_open_wallet";
|
||||||
|
|
||||||
|
/// Identifier for wallet opening [`Modal`].
|
||||||
|
const SELECT_WALLET_MODAL: &'static str = "wallets_select_wallet";
|
||||||
|
|
||||||
impl Default for WalletsContent {
|
impl Default for WalletsContent {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
wallets: WalletList::default(),
|
wallets: WalletList::default(),
|
||||||
pass_edit: "".to_string(),
|
wallet_selection_content: None,
|
||||||
wrong_pass: false,
|
open_wallet_content: None,
|
||||||
conn_modal_content: None,
|
conn_selection_content: None,
|
||||||
wallet_content: WalletContent::default(),
|
wallet_content: WalletContent::new(None),
|
||||||
creation_content: WalletCreation::default(),
|
creation_content: WalletCreation::default(),
|
||||||
show_wallets_at_dual_panel: AppConfig::show_wallets_at_dual_panel(),
|
show_wallets_at_dual_panel: AppConfig::show_wallets_at_dual_panel(),
|
||||||
modal_ids: vec![
|
modal_ids: vec![
|
||||||
OPEN_WALLET_MODAL,
|
OPEN_WALLET_MODAL,
|
||||||
WalletCreation::NAME_PASS_MODAL,
|
WalletCreation::NAME_PASS_MODAL,
|
||||||
CONNECTION_SELECTION_MODAL,
|
CONNECTION_SELECTION_MODAL,
|
||||||
|
SELECT_WALLET_MODAL
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -87,13 +92,21 @@ impl ModalContainer for WalletsContent {
|
||||||
modal: &Modal,
|
modal: &Modal,
|
||||||
cb: &dyn PlatformCallbacks) {
|
cb: &dyn PlatformCallbacks) {
|
||||||
match modal.id {
|
match modal.id {
|
||||||
OPEN_WALLET_MODAL => self.open_wallet_modal_ui(ui, modal, cb),
|
OPEN_WALLET_MODAL => {
|
||||||
|
if let Some(content) = self.open_wallet_content.as_mut() {
|
||||||
|
content.ui(ui, modal, &mut self.wallets, cb, |data| {
|
||||||
|
// Setup wallet content.
|
||||||
|
self.wallet_content = WalletContent::new(data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
WalletCreation::NAME_PASS_MODAL => {
|
WalletCreation::NAME_PASS_MODAL => {
|
||||||
self.creation_content.name_pass_modal_ui(ui, modal, cb)
|
self.creation_content.name_pass_modal_ui(ui, modal, cb)
|
||||||
},
|
},
|
||||||
CONNECTION_SELECTION_MODAL => {
|
CONNECTION_SELECTION_MODAL => {
|
||||||
if let Some(content) = self.conn_modal_content.as_mut() {
|
if let Some(content) = self.conn_selection_content.as_mut() {
|
||||||
content.ui(ui, modal, cb, |id| {
|
content.ui(ui, modal, cb, |id| {
|
||||||
|
// Update wallet connection on select.
|
||||||
let list = self.wallets.list();
|
let list = self.wallets.list();
|
||||||
for w in list {
|
for w in list {
|
||||||
if self.wallets.selected_id == Some(w.get_config().id) {
|
if self.wallets.selected_id == Some(w.get_config().id) {
|
||||||
|
@ -103,12 +116,20 @@ impl ModalContainer for WalletsContent {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
SELECT_WALLET_MODAL => {
|
||||||
|
if let Some(content) = self.wallet_selection_content.as_mut() {
|
||||||
|
content.ui(ui, modal, &mut self.wallets, cb, |_, data| {
|
||||||
|
self.wallet_content = WalletContent::new(data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WalletsContent {
|
impl WalletsContent {
|
||||||
|
/// Draw wallets content.
|
||||||
pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||||
// Draw modal content for current ui container.
|
// Draw modal content for current ui container.
|
||||||
self.current_modal_ui(ui, cb);
|
self.current_modal_ui(ui, cb);
|
||||||
|
@ -159,7 +180,7 @@ impl WalletsContent {
|
||||||
// Add created wallet to list.
|
// Add created wallet to list.
|
||||||
self.wallets.add(wallet);
|
self.wallets.add(wallet);
|
||||||
// Reset wallet content.
|
// Reset wallet content.
|
||||||
self.wallet_content = WalletContent::default();
|
self.wallet_content = WalletContent::new(None);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
let selected_id = self.wallets.selected_id.clone();
|
let selected_id = self.wallets.selected_id.clone();
|
||||||
|
@ -254,6 +275,57 @@ impl WalletsContent {
|
||||||
self.creation_content.can_go_back()
|
self.creation_content.can_go_back()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Handle data from deeplink or opened file.
|
||||||
|
pub fn on_data(&mut self, ui: &mut egui::Ui, data: Option<String>, cb: &dyn PlatformCallbacks) {
|
||||||
|
let wallets_size = self.wallets.list().len();
|
||||||
|
if wallets_size == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Close network panel on single panel mode.
|
||||||
|
if !Content::is_dual_panel_mode(ui) && Content::is_network_panel_open() {
|
||||||
|
Content::toggle_network_panel();
|
||||||
|
}
|
||||||
|
// Pass data to opened selected wallet or show wallets selection.
|
||||||
|
if self.wallets.is_selected_open() {
|
||||||
|
if wallets_size == 1 {
|
||||||
|
self.wallet_content = WalletContent::new(data);
|
||||||
|
} else {
|
||||||
|
self.show_wallet_selection_modal(data);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if wallets_size == 1 {
|
||||||
|
self.show_opening_modal(self.wallets.list()[0].get_config().id, data, cb);
|
||||||
|
} else {
|
||||||
|
self.show_wallet_selection_modal(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Show wallet selection with provided optional data.
|
||||||
|
fn show_wallet_selection_modal(&mut self, data: Option<String>) {
|
||||||
|
self.wallet_selection_content = Some(WalletsModal::new(None, data, true));
|
||||||
|
// Show wallet selection modal.
|
||||||
|
Modal::new(SELECT_WALLET_MODAL)
|
||||||
|
.position(ModalPosition::Center)
|
||||||
|
.title(t!("network_settings.choose_wallet"))
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle Back key event returning `false` when event was handled.
|
||||||
|
pub fn on_back(&mut self) -> bool {
|
||||||
|
let can_go_back = self.creation_content.can_go_back();
|
||||||
|
if can_go_back {
|
||||||
|
self.creation_content.back();
|
||||||
|
return false
|
||||||
|
} else {
|
||||||
|
if self.wallets.is_selected_open() {
|
||||||
|
self.wallets.select(None);
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
/// Draw [`TitlePanel`] content.
|
/// Draw [`TitlePanel`] content.
|
||||||
fn title_ui(&mut self,
|
fn title_ui(&mut self,
|
||||||
ui: &mut egui::Ui,
|
ui: &mut egui::Ui,
|
||||||
|
@ -383,8 +455,7 @@ impl WalletsContent {
|
||||||
// Check if wallet reopen is needed.
|
// Check if wallet reopen is needed.
|
||||||
if !wallet.is_open() && wallet.reopen_needed() {
|
if !wallet.is_open() && wallet.reopen_needed() {
|
||||||
wallet.set_reopen(false);
|
wallet.set_reopen(false);
|
||||||
self.wallets.select(Some(wallet.get_config().id));
|
self.show_opening_modal(wallet.get_config().id, None, cb);
|
||||||
self.show_open_wallet_modal(cb);
|
|
||||||
}
|
}
|
||||||
// Draw wallet list item.
|
// Draw wallet list item.
|
||||||
self.wallet_item_ui(ui, wallet, cb);
|
self.wallet_item_ui(ui, wallet, cb);
|
||||||
|
@ -420,8 +491,7 @@ impl WalletsContent {
|
||||||
if !wallet.is_open() {
|
if !wallet.is_open() {
|
||||||
// Show button to open closed wallet.
|
// Show button to open closed wallet.
|
||||||
View::item_button(ui, View::item_rounding(0, 1, true), FOLDER_OPEN, None, || {
|
View::item_button(ui, View::item_rounding(0, 1, true), FOLDER_OPEN, None, || {
|
||||||
self.wallets.select(Some(id));
|
self.show_opening_modal(id, None, cb);
|
||||||
self.show_open_wallet_modal(cb);
|
|
||||||
});
|
});
|
||||||
// Show button to select connection if not syncing.
|
// Show button to select connection if not syncing.
|
||||||
if !wallet.syncing() {
|
if !wallet.syncing() {
|
||||||
|
@ -435,7 +505,7 @@ impl WalletsContent {
|
||||||
// Show button to select opened wallet.
|
// Show button to select opened wallet.
|
||||||
View::item_button(ui, View::item_rounding(0, 1, true), CARET_RIGHT, None, || {
|
View::item_button(ui, View::item_rounding(0, 1, true), CARET_RIGHT, None, || {
|
||||||
self.wallets.select(Some(id));
|
self.wallets.select(Some(id));
|
||||||
self.wallet_content = WalletContent::default();
|
self.wallet_content = WalletContent::new(None);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// Show button to close opened wallet.
|
// Show button to close opened wallet.
|
||||||
|
@ -455,7 +525,7 @@ impl WalletsContent {
|
||||||
ui.add_space(6.0);
|
ui.add_space(6.0);
|
||||||
ui.vertical(|ui| {
|
ui.vertical(|ui| {
|
||||||
ui.add_space(3.0);
|
ui.add_space(3.0);
|
||||||
// Setup wallet name text.
|
// Show wallet name text.
|
||||||
let name_color = if is_selected {
|
let name_color = if is_selected {
|
||||||
Colors::white_or_black(true)
|
Colors::white_or_black(true)
|
||||||
} else {
|
} else {
|
||||||
|
@ -466,42 +536,11 @@ impl WalletsContent {
|
||||||
View::ellipsize_text(ui, config.name, 18.0, name_color);
|
View::ellipsize_text(ui, config.name, 18.0, name_color);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Setup wallet status text.
|
// Show wallet status text.
|
||||||
let status_text = if wallet.is_open() {
|
View::ellipsize_text(ui, status_text(wallet), 15.0, Colors::text(false));
|
||||||
if wallet.sync_error() {
|
|
||||||
format!("{} {}", WARNING_CIRCLE, t!("error"))
|
|
||||||
} else if wallet.is_closing() {
|
|
||||||
format!("{} {}", SPINNER, t!("wallets.closing"))
|
|
||||||
} else if wallet.is_repairing() {
|
|
||||||
let repair_progress = wallet.repairing_progress();
|
|
||||||
if repair_progress == 0 {
|
|
||||||
format!("{} {}", SPINNER, t!("wallets.checking"))
|
|
||||||
} else {
|
|
||||||
format!("{} {}: {}%",
|
|
||||||
SPINNER,
|
|
||||||
t!("wallets.checking"),
|
|
||||||
repair_progress)
|
|
||||||
}
|
|
||||||
} else if wallet.syncing() {
|
|
||||||
let info_progress = wallet.info_sync_progress();
|
|
||||||
if info_progress == 100 || info_progress == 0 {
|
|
||||||
format!("{} {}", SPINNER, t!("wallets.loading"))
|
|
||||||
} else {
|
|
||||||
format!("{} {}: {}%",
|
|
||||||
SPINNER,
|
|
||||||
t!("wallets.loading"),
|
|
||||||
info_progress)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
format!("{} {}", FOLDER_OPEN, t!("wallets.unlocked"))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
format!("{} {}", FOLDER_LOCK, t!("wallets.locked"))
|
|
||||||
};
|
|
||||||
View::ellipsize_text(ui, status_text, 15.0, Colors::text(false));
|
|
||||||
ui.add_space(1.0);
|
ui.add_space(1.0);
|
||||||
|
|
||||||
// Setup wallet connection text.
|
// Show wallet connection text.
|
||||||
let conn_text = if let Some(conn) = wallet.get_current_ext_conn() {
|
let conn_text = if let Some(conn) = wallet.get_current_ext_conn() {
|
||||||
format!("{} {}", GLOBE_SIMPLE, conn.url)
|
format!("{} {}", GLOBE_SIMPLE, conn.url)
|
||||||
} else {
|
} else {
|
||||||
|
@ -517,7 +556,7 @@ impl WalletsContent {
|
||||||
/// Show [`Modal`] to select connection for the wallet.
|
/// Show [`Modal`] to select connection for the wallet.
|
||||||
fn show_connection_selector_modal(&mut self, wallet: &Wallet) {
|
fn show_connection_selector_modal(&mut self, wallet: &Wallet) {
|
||||||
let ext_conn = wallet.get_current_ext_conn();
|
let ext_conn = wallet.get_current_ext_conn();
|
||||||
self.conn_modal_content = Some(WalletConnectionModal::new(ext_conn));
|
self.conn_selection_content = Some(WalletConnectionModal::new(ext_conn));
|
||||||
// Show modal.
|
// Show modal.
|
||||||
Modal::new(CONNECTION_SELECTION_MODAL)
|
Modal::new(CONNECTION_SELECTION_MODAL)
|
||||||
.position(ModalPosition::CenterTop)
|
.position(ModalPosition::CenterTop)
|
||||||
|
@ -525,11 +564,10 @@ impl WalletsContent {
|
||||||
.show();
|
.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Show [`Modal`] to open selected wallet.
|
/// Show [`Modal`] to select and open wallet.
|
||||||
fn show_open_wallet_modal(&mut self, cb: &dyn PlatformCallbacks) {
|
fn show_opening_modal(&mut self, id: i64, data: Option<String>, cb: &dyn PlatformCallbacks) {
|
||||||
// Reset modal values.
|
self.wallets.select(Some(id));
|
||||||
self.pass_edit = String::from("");
|
self.open_wallet_content = Some(OpenWalletModal::new(data));
|
||||||
self.wrong_pass = false;
|
|
||||||
// Show modal.
|
// Show modal.
|
||||||
Modal::new(OPEN_WALLET_MODAL)
|
Modal::new(OPEN_WALLET_MODAL)
|
||||||
.position(ModalPosition::CenterTop)
|
.position(ModalPosition::CenterTop)
|
||||||
|
@ -537,100 +575,6 @@ impl WalletsContent {
|
||||||
.show();
|
.show();
|
||||||
cb.show_keyboard();
|
cb.show_keyboard();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Draw wallet opening [`Modal`] content.
|
|
||||||
fn open_wallet_modal_ui(&mut self,
|
|
||||||
ui: &mut egui::Ui,
|
|
||||||
modal: &Modal,
|
|
||||||
cb: &dyn PlatformCallbacks) {
|
|
||||||
ui.add_space(6.0);
|
|
||||||
ui.vertical_centered(|ui| {
|
|
||||||
ui.label(RichText::new(t!("wallets.pass"))
|
|
||||||
.size(17.0)
|
|
||||||
.color(Colors::gray()));
|
|
||||||
ui.add_space(8.0);
|
|
||||||
|
|
||||||
// Show password input.
|
|
||||||
let mut pass_edit_opts = TextEditOptions::new(Id::from(modal.id)).password();
|
|
||||||
View::text_edit(ui, cb, &mut self.pass_edit, &mut pass_edit_opts);
|
|
||||||
|
|
||||||
// Show information when password is empty.
|
|
||||||
if self.pass_edit.is_empty() {
|
|
||||||
self.wrong_pass = false;
|
|
||||||
ui.add_space(10.0);
|
|
||||||
ui.label(RichText::new(t!("wallets.pass_empty"))
|
|
||||||
.size(17.0)
|
|
||||||
.color(Colors::inactive_text()));
|
|
||||||
} else if self.wrong_pass {
|
|
||||||
ui.add_space(10.0);
|
|
||||||
ui.label(RichText::new(t!("wallets.wrong_pass"))
|
|
||||||
.size(17.0)
|
|
||||||
.color(Colors::red()));
|
|
||||||
}
|
|
||||||
ui.add_space(12.0);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Show modal buttons.
|
|
||||||
ui.scope(|ui| {
|
|
||||||
// Setup spacing between buttons.
|
|
||||||
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
|
|
||||||
|
|
||||||
ui.columns(2, |columns| {
|
|
||||||
columns[0].vertical_centered_justified(|ui| {
|
|
||||||
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
|
|
||||||
// Close modal.
|
|
||||||
cb.hide_keyboard();
|
|
||||||
modal.close();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
columns[1].vertical_centered_justified(|ui| {
|
|
||||||
// Callback for button to continue.
|
|
||||||
let mut on_continue = || {
|
|
||||||
if self.pass_edit.is_empty() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
match self.wallets.open_selected(&self.pass_edit) {
|
|
||||||
Ok(_) => {
|
|
||||||
// Clear values.
|
|
||||||
self.pass_edit = "".to_string();
|
|
||||||
self.wrong_pass = false;
|
|
||||||
// Close modal.
|
|
||||||
cb.hide_keyboard();
|
|
||||||
modal.close();
|
|
||||||
// Reset wallet content.
|
|
||||||
self.wallet_content = WalletContent::default();
|
|
||||||
}
|
|
||||||
Err(_) => self.wrong_pass = true
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Continue on Enter key press.
|
|
||||||
View::on_enter_key(ui, || {
|
|
||||||
(on_continue)();
|
|
||||||
});
|
|
||||||
|
|
||||||
View::button(ui, t!("continue"), Colors::white_or_black(false), on_continue);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
ui.add_space(6.0);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Handle Back key event.
|
|
||||||
/// Return `false` when event was handled.
|
|
||||||
pub fn on_back(&mut self) -> bool {
|
|
||||||
let can_go_back = self.creation_content.can_go_back();
|
|
||||||
if can_go_back {
|
|
||||||
self.creation_content.back();
|
|
||||||
return false
|
|
||||||
} else {
|
|
||||||
if self.wallets.is_selected_open() {
|
|
||||||
self.wallets.select(None);
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if it's possible to show [`WalletsContent`] and [`WalletContent`] panels at same time.
|
/// Check if it's possible to show [`WalletsContent`] and [`WalletContent`] panels at same time.
|
||||||
|
|
|
@ -23,7 +23,7 @@ use crate::gui::views::{Modal, Content, View};
|
||||||
use crate::gui::views::types::{ModalPosition, TextEditOptions};
|
use crate::gui::views::types::{ModalPosition, TextEditOptions};
|
||||||
use crate::gui::views::wallets::creation::MnemonicSetup;
|
use crate::gui::views::wallets::creation::MnemonicSetup;
|
||||||
use crate::gui::views::wallets::creation::types::Step;
|
use crate::gui::views::wallets::creation::types::Step;
|
||||||
use crate::gui::views::wallets::settings::ConnectionSettings;
|
use crate::gui::views::wallets::ConnectionSettings;
|
||||||
use crate::node::Node;
|
use crate::node::Node;
|
||||||
use crate::wallet::{ExternalConnection, Wallet};
|
use crate::wallet::{ExternalConnection, Wallet};
|
||||||
use crate::wallet::types::PhraseMode;
|
use crate::wallet::types::PhraseMode;
|
||||||
|
|
|
@ -16,4 +16,7 @@ mod conn;
|
||||||
pub use conn::*;
|
pub use conn::*;
|
||||||
|
|
||||||
mod wallets;
|
mod wallets;
|
||||||
pub use wallets::*;
|
pub use wallets::*;
|
||||||
|
|
||||||
|
mod open;
|
||||||
|
pub use open::*;
|
121
src/gui/views/wallets/modals/open.rs
Normal file
121
src/gui/views/wallets/modals/open.rs
Normal file
|
@ -0,0 +1,121 @@
|
||||||
|
// Copyright 2024 The Grim Developers
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
use egui::{Id, RichText};
|
||||||
|
|
||||||
|
use crate::gui::Colors;
|
||||||
|
use crate::gui::platform::PlatformCallbacks;
|
||||||
|
use crate::gui::views::{Modal, View};
|
||||||
|
use crate::gui::views::types::TextEditOptions;
|
||||||
|
use crate::wallet::WalletList;
|
||||||
|
|
||||||
|
/// Wallet opening [`Modal`] content.
|
||||||
|
pub struct OpenWalletModal {
|
||||||
|
/// Password to open wallet.
|
||||||
|
pass_edit: String,
|
||||||
|
/// Flag to check if wrong password was entered.
|
||||||
|
wrong_pass: bool,
|
||||||
|
|
||||||
|
/// Optional data to pass after wallet opening.
|
||||||
|
data: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OpenWalletModal {
|
||||||
|
/// Create new content instance.
|
||||||
|
pub fn new(data: Option<String>) -> Self {
|
||||||
|
Self {
|
||||||
|
pass_edit: "".to_string(),
|
||||||
|
wrong_pass: false,
|
||||||
|
data,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// Draw [`Modal`] content.
|
||||||
|
pub fn ui(&mut self,
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
modal: &Modal,
|
||||||
|
wallets: &mut WalletList,
|
||||||
|
cb: &dyn PlatformCallbacks,
|
||||||
|
mut on_continue: impl FnMut(Option<String>)) {
|
||||||
|
ui.add_space(6.0);
|
||||||
|
ui.vertical_centered(|ui| {
|
||||||
|
ui.label(RichText::new(t!("wallets.pass"))
|
||||||
|
.size(17.0)
|
||||||
|
.color(Colors::gray()));
|
||||||
|
ui.add_space(8.0);
|
||||||
|
|
||||||
|
// Show password input.
|
||||||
|
let mut pass_edit_opts = TextEditOptions::new(Id::from(modal.id)).password();
|
||||||
|
View::text_edit(ui, cb, &mut self.pass_edit, &mut pass_edit_opts);
|
||||||
|
|
||||||
|
// Show information when password is empty.
|
||||||
|
if self.pass_edit.is_empty() {
|
||||||
|
self.wrong_pass = false;
|
||||||
|
ui.add_space(10.0);
|
||||||
|
ui.label(RichText::new(t!("wallets.pass_empty"))
|
||||||
|
.size(17.0)
|
||||||
|
.color(Colors::inactive_text()));
|
||||||
|
} else if self.wrong_pass {
|
||||||
|
ui.add_space(10.0);
|
||||||
|
ui.label(RichText::new(t!("wallets.wrong_pass"))
|
||||||
|
.size(17.0)
|
||||||
|
.color(Colors::red()));
|
||||||
|
}
|
||||||
|
ui.add_space(12.0);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Show modal buttons.
|
||||||
|
ui.scope(|ui| {
|
||||||
|
// Setup spacing between buttons.
|
||||||
|
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
|
||||||
|
|
||||||
|
ui.columns(2, |columns| {
|
||||||
|
columns[0].vertical_centered_justified(|ui| {
|
||||||
|
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
|
||||||
|
// Close modal.
|
||||||
|
cb.hide_keyboard();
|
||||||
|
modal.close();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
columns[1].vertical_centered_justified(|ui| {
|
||||||
|
// Callback for button to continue.
|
||||||
|
let mut on_continue = || {
|
||||||
|
if self.pass_edit.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
match wallets.open_selected(&self.pass_edit) {
|
||||||
|
Ok(_) => {
|
||||||
|
// Clear values.
|
||||||
|
self.pass_edit = "".to_string();
|
||||||
|
self.wrong_pass = false;
|
||||||
|
// Close modal.
|
||||||
|
cb.hide_keyboard();
|
||||||
|
modal.close();
|
||||||
|
on_continue(self.data.clone());
|
||||||
|
}
|
||||||
|
Err(_) => self.wrong_pass = true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Continue on Enter key press.
|
||||||
|
View::on_enter_key(ui, || {
|
||||||
|
(on_continue)();
|
||||||
|
});
|
||||||
|
|
||||||
|
View::button(ui, t!("continue"), Colors::white_or_black(false), on_continue);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
ui.add_space(6.0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -16,29 +16,53 @@ use egui::scroll_area::ScrollBarVisibility;
|
||||||
use egui::{Align, Layout, RichText, ScrollArea};
|
use egui::{Align, Layout, RichText, ScrollArea};
|
||||||
|
|
||||||
use crate::gui::Colors;
|
use crate::gui::Colors;
|
||||||
use crate::gui::icons::{CHECK, CHECK_FAT, COMPUTER_TOWER, GLOBE_SIMPLE, PLUGS_CONNECTED};
|
use crate::gui::icons::{CHECK, CHECK_FAT, COMPUTER_TOWER, FOLDER_OPEN, GLOBE_SIMPLE, PLUGS_CONNECTED};
|
||||||
|
use crate::gui::platform::PlatformCallbacks;
|
||||||
use crate::gui::views::{Modal, View};
|
use crate::gui::views::{Modal, View};
|
||||||
|
use crate::gui::views::types::ModalPosition;
|
||||||
|
use crate::gui::views::wallets::modals::OpenWalletModal;
|
||||||
|
use crate::gui::views::wallets::wallet::types::status_text;
|
||||||
use crate::wallet::{Wallet, WalletList};
|
use crate::wallet::{Wallet, WalletList};
|
||||||
|
|
||||||
/// Wallet list [`Modal`] content
|
/// Wallet list [`Modal`] content
|
||||||
pub struct WalletsModal {
|
pub struct WalletsModal {
|
||||||
/// Selected wallet id.
|
/// Selected wallet id.
|
||||||
selected: Option<i64>
|
selected: Option<i64>,
|
||||||
|
|
||||||
|
/// Optional data to pass after wallet selection.
|
||||||
|
data: Option<String>,
|
||||||
|
|
||||||
|
/// Flag to check if wallet can be opened from the list.
|
||||||
|
can_open: bool,
|
||||||
|
/// Wallet opening content.
|
||||||
|
open_wallet_content: Option<OpenWalletModal>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WalletsModal {
|
impl WalletsModal {
|
||||||
pub fn new(selected: Option<i64>) -> Self {
|
/// Create new content instance.
|
||||||
Self {
|
pub fn new(selected: Option<i64>, data: Option<String>, can_open: bool) -> Self {
|
||||||
selected,
|
Self { selected, data, can_open, open_wallet_content: None }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Draw [`Modal`] content.
|
/// Draw content.
|
||||||
pub fn ui(&mut self,
|
pub fn ui(&mut self,
|
||||||
ui: &mut egui::Ui,
|
ui: &mut egui::Ui,
|
||||||
modal: &Modal,
|
modal: &Modal,
|
||||||
wallets: &WalletList,
|
wallets: &mut WalletList,
|
||||||
mut on_select: impl FnMut(i64)) {
|
cb: &dyn PlatformCallbacks,
|
||||||
|
mut on_select: impl FnMut(i64, Option<String>)) {
|
||||||
|
// Draw wallet opening content if requested.
|
||||||
|
if let Some(open_content) = self.open_wallet_content.as_mut() {
|
||||||
|
open_content.ui(ui, modal, wallets, cb, |data| {
|
||||||
|
modal.close();
|
||||||
|
if let Some(id) = self.selected {
|
||||||
|
on_select(id, data);
|
||||||
|
}
|
||||||
|
self.data = None;
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
ui.add_space(4.0);
|
ui.add_space(4.0);
|
||||||
ScrollArea::vertical()
|
ScrollArea::vertical()
|
||||||
.max_height(373.0)
|
.max_height(373.0)
|
||||||
|
@ -48,10 +72,12 @@ impl WalletsModal {
|
||||||
.show(ui, |ui| {
|
.show(ui, |ui| {
|
||||||
ui.add_space(2.0);
|
ui.add_space(2.0);
|
||||||
ui.vertical_centered(|ui| {
|
ui.vertical_centered(|ui| {
|
||||||
for wallet in wallets.list() {
|
let data = self.data.clone();
|
||||||
|
for wallet in wallets.clone().list() {
|
||||||
// Draw wallet list item.
|
// Draw wallet list item.
|
||||||
self.wallet_item_ui(ui, wallet, modal, |id| {
|
self.wallet_item_ui(ui, wallet, wallets, |id| {
|
||||||
on_select(id);
|
modal.close();
|
||||||
|
on_select(id, data.clone());
|
||||||
});
|
});
|
||||||
ui.add_space(5.0);
|
ui.add_space(5.0);
|
||||||
}
|
}
|
||||||
|
@ -65,18 +91,19 @@ impl WalletsModal {
|
||||||
// Show button to close modal.
|
// Show button to close modal.
|
||||||
ui.vertical_centered_justified(|ui| {
|
ui.vertical_centered_justified(|ui| {
|
||||||
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
|
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
|
||||||
|
self.data = None;
|
||||||
modal.close();
|
modal.close();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
ui.add_space(6.0);
|
ui.add_space(6.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Draw wallet list item.
|
/// Draw wallet list item with provided callback on select.
|
||||||
fn wallet_item_ui(&mut self,
|
fn wallet_item_ui(&mut self,
|
||||||
ui: &mut egui::Ui,
|
ui: &mut egui::Ui,
|
||||||
wallet: &Wallet,
|
wallet: &Wallet,
|
||||||
modal: &Modal,
|
wallets: &mut WalletList,
|
||||||
mut on_select: impl FnMut(i64)) {
|
mut select: impl FnMut(i64)) {
|
||||||
let config = wallet.get_config();
|
let config = wallet.get_config();
|
||||||
let id = config.id;
|
let id = config.id;
|
||||||
|
|
||||||
|
@ -87,16 +114,34 @@ impl WalletsModal {
|
||||||
ui.painter().rect(rect, rounding, Colors::fill(), View::hover_stroke());
|
ui.painter().rect(rect, rounding, Colors::fill(), View::hover_stroke());
|
||||||
|
|
||||||
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
|
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
|
||||||
// Draw button to select wallet.
|
if self.can_open {
|
||||||
let current = self.selected.unwrap_or(0) == id;
|
// Show button to select or open closed wallet.
|
||||||
if current {
|
let icon = if wallet.is_open() {
|
||||||
ui.add_space(12.0);
|
CHECK
|
||||||
ui.label(RichText::new(CHECK_FAT).size(20.0).color(Colors::green()));
|
} else {
|
||||||
} else {
|
FOLDER_OPEN
|
||||||
View::item_button(ui, View::item_rounding(0, 1, true), CHECK, None, || {
|
};
|
||||||
on_select(id);
|
View::item_button(ui, View::item_rounding(0, 1, true), icon, None, || {
|
||||||
modal.close();
|
wallets.select(Some(id));
|
||||||
|
if wallet.is_open() {
|
||||||
|
select(id);
|
||||||
|
} else {
|
||||||
|
self.selected = wallets.selected_id;
|
||||||
|
Modal::change_position(ModalPosition::CenterTop);
|
||||||
|
self.open_wallet_content = Some(OpenWalletModal::new(self.data.clone()));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
// Draw button to select wallet.
|
||||||
|
let current = self.selected.unwrap_or(0) == id;
|
||||||
|
if current {
|
||||||
|
ui.add_space(12.0);
|
||||||
|
ui.label(RichText::new(CHECK_FAT).size(20.0).color(Colors::green()));
|
||||||
|
} else {
|
||||||
|
View::item_button(ui, View::item_rounding(0, 1, true), CHECK, None, || {
|
||||||
|
select(id);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let layout_size = ui.available_size();
|
let layout_size = ui.available_size();
|
||||||
|
@ -104,13 +149,13 @@ impl WalletsModal {
|
||||||
ui.add_space(6.0);
|
ui.add_space(6.0);
|
||||||
ui.vertical(|ui| {
|
ui.vertical(|ui| {
|
||||||
ui.add_space(3.0);
|
ui.add_space(3.0);
|
||||||
// Setup wallet name text.
|
// Show wallet name text.
|
||||||
ui.with_layout(Layout::left_to_right(Align::Min), |ui| {
|
ui.with_layout(Layout::left_to_right(Align::Min), |ui| {
|
||||||
ui.add_space(1.0);
|
ui.add_space(1.0);
|
||||||
View::ellipsize_text(ui, config.name, 18.0, Colors::title(false));
|
View::ellipsize_text(ui, config.name, 18.0, Colors::title(false));
|
||||||
});
|
});
|
||||||
|
|
||||||
// Setup wallet connection text.
|
// Show wallet connection text.
|
||||||
let conn = if let Some(conn) = wallet.get_current_ext_conn() {
|
let conn = if let Some(conn) = wallet.get_current_ext_conn() {
|
||||||
format!("{} {}", GLOBE_SIMPLE, conn.url)
|
format!("{} {}", GLOBE_SIMPLE, conn.url)
|
||||||
} else {
|
} else {
|
||||||
|
@ -119,14 +164,20 @@ impl WalletsModal {
|
||||||
View::ellipsize_text(ui, conn, 15.0, Colors::text(false));
|
View::ellipsize_text(ui, conn, 15.0, Colors::text(false));
|
||||||
ui.add_space(1.0);
|
ui.add_space(1.0);
|
||||||
|
|
||||||
// Setup wallet API text.
|
// Show wallet API text or open status.
|
||||||
let address = if let Some(port) = config.api_port {
|
if self.can_open {
|
||||||
format!("127.0.0.1:{}", port)
|
ui.label(RichText::new(status_text(wallet))
|
||||||
|
.size(15.0)
|
||||||
|
.color(Colors::gray()));
|
||||||
} else {
|
} else {
|
||||||
"-".to_string()
|
let address = if let Some(port) = config.api_port {
|
||||||
};
|
format!("127.0.0.1:{}", port)
|
||||||
let api_text = format!("{} {}", PLUGS_CONNECTED, address);
|
} else {
|
||||||
ui.label(RichText::new(api_text).size(15.0).color(Colors::gray()));
|
"-".to_string()
|
||||||
|
};
|
||||||
|
let api_text = format!("{} {}", PLUGS_CONNECTED, address);
|
||||||
|
ui.label(RichText::new(api_text).size(15.0).color(Colors::gray()));
|
||||||
|
}
|
||||||
ui.add_space(3.0);
|
ui.add_space(3.0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -13,57 +13,34 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use egui::{Align, Id, Layout, Margin, RichText, ScrollArea};
|
use egui::{Align, Id, Layout, Margin, RichText};
|
||||||
use egui::scroll_area::ScrollBarVisibility;
|
|
||||||
use grin_chain::SyncStatus;
|
use grin_chain::SyncStatus;
|
||||||
use grin_core::core::amount_to_hr_string;
|
use grin_core::core::amount_to_hr_string;
|
||||||
|
|
||||||
use crate::AppConfig;
|
use crate::AppConfig;
|
||||||
use crate::gui::Colors;
|
use crate::gui::Colors;
|
||||||
use crate::gui::icons::{ARROWS_CLOCKWISE, BRIDGE, CHAT_CIRCLE_TEXT, CHECK, CHECK_FAT, COPY, FOLDER_USER, GEAR_FINE, GRAPH, PACKAGE, PATH, POWER, SCAN, SPINNER, USERS_THREE};
|
use crate::gui::icons::{ARROWS_CLOCKWISE, BRIDGE, CHAT_CIRCLE_TEXT, FOLDER_USER, GEAR_FINE, GRAPH, PACKAGE, POWER, SCAN, SPINNER, USERS_THREE};
|
||||||
use crate::gui::platform::PlatformCallbacks;
|
use crate::gui::platform::PlatformCallbacks;
|
||||||
use crate::gui::views::{CameraContent, Modal, Content, View};
|
use crate::gui::views::{Modal, Content, View};
|
||||||
use crate::gui::views::types::{ModalPosition, QrScanResult, TextEditOptions};
|
use crate::gui::views::types::{ModalPosition, QrScanResult};
|
||||||
use crate::gui::views::wallets::{WalletTransactions, WalletMessages, WalletTransport};
|
use crate::gui::views::wallets::{WalletTransactions, WalletMessages, WalletTransport};
|
||||||
use crate::gui::views::wallets::types::{GRIN, WalletTab, WalletTabType};
|
use crate::gui::views::wallets::types::{GRIN, WalletTab, WalletTabType};
|
||||||
use crate::gui::views::wallets::settings::WalletSettings;
|
use crate::gui::views::wallets::wallet::modals::{WalletAccountsModal, WalletScanModal};
|
||||||
|
use crate::gui::views::wallets::wallet::WalletSettings;
|
||||||
use crate::node::Node;
|
use crate::node::Node;
|
||||||
use crate::wallet::{Wallet, WalletConfig};
|
use crate::wallet::{Wallet, WalletConfig};
|
||||||
use crate::wallet::types::{WalletAccount, WalletData};
|
use crate::wallet::types::WalletData;
|
||||||
|
|
||||||
/// Selected and opened wallet content.
|
/// Selected and opened wallet content.
|
||||||
pub struct WalletContent {
|
pub struct WalletContent {
|
||||||
/// List of wallet accounts for [`Modal`].
|
/// Wallet accounts [`Modal`] content.
|
||||||
accounts: Vec<WalletAccount>,
|
accounts_modal_content: Option<WalletAccountsModal>,
|
||||||
|
|
||||||
/// Flag to check if account is creating.
|
/// QR code scan [`Modal`] content.
|
||||||
account_creating: bool,
|
scan_modal_content: Option<WalletScanModal>,
|
||||||
/// Account label [`Modal`] value.
|
|
||||||
account_label_edit: String,
|
|
||||||
/// Flag to check if error occurred during account creation at [`Modal`].
|
|
||||||
account_creation_error: bool,
|
|
||||||
|
|
||||||
/// Camera content for QR scan [`Modal`].
|
|
||||||
camera_content: CameraContent,
|
|
||||||
/// QR code scan result
|
|
||||||
qr_scan_result: Option<QrScanResult>,
|
|
||||||
|
|
||||||
/// Current tab content to show.
|
/// Current tab content to show.
|
||||||
pub current_tab: Box<dyn WalletTab>
|
pub current_tab: Box<dyn WalletTab>,
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for WalletContent {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
accounts: vec![],
|
|
||||||
account_creating: false,
|
|
||||||
account_label_edit: "".to_string(),
|
|
||||||
account_creation_error: false,
|
|
||||||
camera_content: CameraContent::default(),
|
|
||||||
qr_scan_result: None,
|
|
||||||
current_tab: Box::new(WalletTransactions::default())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Identifier for account list [`Modal`].
|
/// Identifier for account list [`Modal`].
|
||||||
|
@ -73,6 +50,20 @@ const ACCOUNT_LIST_MODAL: &'static str = "account_list_modal";
|
||||||
const QR_CODE_SCAN_MODAL: &'static str = "qr_code_scan_modal";
|
const QR_CODE_SCAN_MODAL: &'static str = "qr_code_scan_modal";
|
||||||
|
|
||||||
impl WalletContent {
|
impl WalletContent {
|
||||||
|
/// Create new instance with optional data.
|
||||||
|
pub fn new(data: Option<String>) -> Self {
|
||||||
|
let mut content = Self {
|
||||||
|
accounts_modal_content: None,
|
||||||
|
scan_modal_content: None,
|
||||||
|
current_tab: Box::new(WalletTransactions::default()),
|
||||||
|
};
|
||||||
|
// Provide data to messages.
|
||||||
|
if data.is_some() {
|
||||||
|
content.current_tab = Box::new(WalletMessages::new(data));
|
||||||
|
}
|
||||||
|
content
|
||||||
|
}
|
||||||
|
|
||||||
/// Draw wallet content.
|
/// Draw wallet content.
|
||||||
pub fn ui(&mut self,
|
pub fn ui(&mut self,
|
||||||
ui: &mut egui::Ui,
|
ui: &mut egui::Ui,
|
||||||
|
@ -148,7 +139,7 @@ impl WalletContent {
|
||||||
ui.vertical_centered(|ui| {
|
ui.vertical_centered(|ui| {
|
||||||
// Draw wallet tabs.
|
// Draw wallet tabs.
|
||||||
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
|
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
|
||||||
self.tabs_ui(ui, wallet);
|
self.tabs_ui(ui);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -185,21 +176,50 @@ impl WalletContent {
|
||||||
/// Draw [`Modal`] content for this ui container.
|
/// Draw [`Modal`] content for this ui container.
|
||||||
fn modal_content_ui(&mut self,
|
fn modal_content_ui(&mut self,
|
||||||
ui: &mut egui::Ui,
|
ui: &mut egui::Ui,
|
||||||
wallet: &mut Wallet,
|
wallet: &Wallet,
|
||||||
cb: &dyn PlatformCallbacks) {
|
cb: &dyn PlatformCallbacks) {
|
||||||
match Modal::opened() {
|
match Modal::opened() {
|
||||||
None => {}
|
None => {}
|
||||||
Some(id) => {
|
Some(id) => {
|
||||||
match id {
|
match id {
|
||||||
ACCOUNT_LIST_MODAL => {
|
ACCOUNT_LIST_MODAL => {
|
||||||
Modal::ui(ui.ctx(), |ui, modal| {
|
if let Some(content) = self.accounts_modal_content.as_mut() {
|
||||||
self.account_list_modal_ui(ui, wallet, modal, cb);
|
Modal::ui(ui.ctx(), |ui, modal| {
|
||||||
});
|
content.ui(ui, wallet, modal, cb);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
QR_CODE_SCAN_MODAL => {
|
QR_CODE_SCAN_MODAL => {
|
||||||
Modal::ui(ui.ctx(), |ui, modal| {
|
if let Some(content) = self.scan_modal_content.as_mut() {
|
||||||
self.scan_qr_modal_ui(ui, wallet, modal, cb);
|
Modal::ui(ui.ctx(), |ui, modal| {
|
||||||
});
|
content.ui(ui, wallet, modal, cb, |result| {
|
||||||
|
match result {
|
||||||
|
QrScanResult::Slatepack(message) => {
|
||||||
|
modal.close();
|
||||||
|
let msg = Some(message.to_string());
|
||||||
|
let messages = WalletMessages::new(msg);
|
||||||
|
self.current_tab = Box::new(messages);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
QrScanResult::Address(receiver) => {
|
||||||
|
let balance = wallet.get_data()
|
||||||
|
.unwrap()
|
||||||
|
.info
|
||||||
|
.amount_currently_spendable;
|
||||||
|
if balance > 0 {
|
||||||
|
modal.close();
|
||||||
|
let mut transport = WalletTransport::default();
|
||||||
|
let rec = Some(receiver.to_string());
|
||||||
|
transport.show_send_tor_modal(cb, rec);
|
||||||
|
self.current_tab = Box::new(transport);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
@ -222,8 +242,7 @@ impl WalletContent {
|
||||||
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
|
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
|
||||||
// Draw button to scan QR code.
|
// Draw button to scan QR code.
|
||||||
View::item_button(ui, View::item_rounding(0, 2, true), SCAN, None, || {
|
View::item_button(ui, View::item_rounding(0, 2, true), SCAN, None, || {
|
||||||
self.qr_scan_result = None;
|
self.scan_modal_content = Some(WalletScanModal::default());
|
||||||
self.camera_content.clear_state();
|
|
||||||
// Show QR code scan modal.
|
// Show QR code scan modal.
|
||||||
Modal::new(QR_CODE_SCAN_MODAL)
|
Modal::new(QR_CODE_SCAN_MODAL)
|
||||||
.position(ModalPosition::CenterTop)
|
.position(ModalPosition::CenterTop)
|
||||||
|
@ -235,10 +254,7 @@ impl WalletContent {
|
||||||
|
|
||||||
// Draw button to show list of accounts.
|
// Draw button to show list of accounts.
|
||||||
View::item_button(ui, View::item_rounding(1, 3, true), USERS_THREE, None, || {
|
View::item_button(ui, View::item_rounding(1, 3, true), USERS_THREE, None, || {
|
||||||
// Load accounts.
|
self.accounts_modal_content = Some(WalletAccountsModal::new(wallet.accounts()));
|
||||||
self.account_label_edit = "".to_string();
|
|
||||||
self.accounts = wallet.accounts();
|
|
||||||
self.account_creating = false;
|
|
||||||
// Show account list modal.
|
// Show account list modal.
|
||||||
Modal::new(ACCOUNT_LIST_MODAL)
|
Modal::new(ACCOUNT_LIST_MODAL)
|
||||||
.position(ModalPosition::CenterTop)
|
.position(ModalPosition::CenterTop)
|
||||||
|
@ -305,230 +321,8 @@ impl WalletContent {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Draw account list [`Modal`] content.
|
|
||||||
fn account_list_modal_ui(&mut self,
|
|
||||||
ui: &mut egui::Ui,
|
|
||||||
wallet: &mut Wallet,
|
|
||||||
modal: &Modal,
|
|
||||||
cb: &dyn PlatformCallbacks) {
|
|
||||||
if self.account_creating {
|
|
||||||
ui.add_space(6.0);
|
|
||||||
ui.vertical_centered(|ui| {
|
|
||||||
ui.label(RichText::new(t!("wallets.new_account_desc"))
|
|
||||||
.size(17.0)
|
|
||||||
.color(Colors::gray()));
|
|
||||||
ui.add_space(8.0);
|
|
||||||
|
|
||||||
// Draw account name edit.
|
|
||||||
let text_edit_id = Id::from(modal.id).with(wallet.get_config().id);
|
|
||||||
let mut text_edit_opts = TextEditOptions::new(text_edit_id);
|
|
||||||
View::text_edit(ui, cb, &mut self.account_label_edit, &mut text_edit_opts);
|
|
||||||
|
|
||||||
// Show error occurred during account creation..
|
|
||||||
if self.account_creation_error {
|
|
||||||
ui.add_space(12.0);
|
|
||||||
ui.label(RichText::new(t!("error"))
|
|
||||||
.size(17.0)
|
|
||||||
.color(Colors::red()));
|
|
||||||
}
|
|
||||||
ui.add_space(12.0);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Setup spacing between buttons.
|
|
||||||
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
|
|
||||||
|
|
||||||
// Show modal buttons.
|
|
||||||
ui.columns(2, |columns| {
|
|
||||||
columns[0].vertical_centered_justified(|ui| {
|
|
||||||
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
|
|
||||||
// Close modal.
|
|
||||||
cb.hide_keyboard();
|
|
||||||
modal.close();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
columns[1].vertical_centered_justified(|ui| {
|
|
||||||
// Create button callback.
|
|
||||||
let mut on_create = || {
|
|
||||||
if !self.account_label_edit.is_empty() {
|
|
||||||
let label = &self.account_label_edit;
|
|
||||||
match wallet.create_account(label) {
|
|
||||||
Ok(_) => {
|
|
||||||
let _ = wallet.set_active_account(label);
|
|
||||||
cb.hide_keyboard();
|
|
||||||
modal.close();
|
|
||||||
},
|
|
||||||
Err(_) => self.account_creation_error = true
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
View::on_enter_key(ui, || {
|
|
||||||
(on_create)();
|
|
||||||
});
|
|
||||||
|
|
||||||
View::button(ui, t!("create"), Colors::white_or_black(false), on_create);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
ui.add_space(6.0);
|
|
||||||
} else {
|
|
||||||
ui.add_space(3.0);
|
|
||||||
|
|
||||||
// Show list of accounts.
|
|
||||||
let size = self.accounts.len();
|
|
||||||
ScrollArea::vertical()
|
|
||||||
.id_source("account_list_modal_scroll")
|
|
||||||
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
|
|
||||||
.max_height(266.0)
|
|
||||||
.auto_shrink([true; 2])
|
|
||||||
.show_rows(ui, ACCOUNT_ITEM_HEIGHT, size, |ui, row_range| {
|
|
||||||
for index in row_range {
|
|
||||||
// Add space before the first item.
|
|
||||||
if index == 0 {
|
|
||||||
ui.add_space(4.0);
|
|
||||||
}
|
|
||||||
let acc = self.accounts.get(index).unwrap();
|
|
||||||
account_item_ui(ui, modal, wallet, acc, index, size);
|
|
||||||
if index == size - 1 {
|
|
||||||
ui.add_space(4.0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
ui.add_space(2.0);
|
|
||||||
View::horizontal_line(ui, Colors::stroke());
|
|
||||||
ui.add_space(6.0);
|
|
||||||
|
|
||||||
// Setup spacing between buttons.
|
|
||||||
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
|
|
||||||
|
|
||||||
// Show modal buttons.
|
|
||||||
ui.columns(2, |columns| {
|
|
||||||
columns[0].vertical_centered_justified(|ui| {
|
|
||||||
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
|
|
||||||
modal.close();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
columns[1].vertical_centered_justified(|ui| {
|
|
||||||
View::button(ui, t!("create"), Colors::white_or_black(false), || {
|
|
||||||
self.account_creating = true;
|
|
||||||
cb.show_keyboard();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
ui.add_space(6.0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Draw QR code scan [`Modal`] content.
|
|
||||||
fn scan_qr_modal_ui(&mut self,
|
|
||||||
ui: &mut egui::Ui,
|
|
||||||
wallet: &mut Wallet,
|
|
||||||
modal: &Modal,
|
|
||||||
cb: &dyn PlatformCallbacks) {
|
|
||||||
// Show scan result if exists or show camera content while scanning.
|
|
||||||
if let Some(result) = &self.qr_scan_result {
|
|
||||||
let mut result_text = result.text();
|
|
||||||
View::horizontal_line(ui, Colors::item_stroke());
|
|
||||||
ui.add_space(3.0);
|
|
||||||
ScrollArea::vertical()
|
|
||||||
.id_source(Id::from("qr_scan_result_input").with(wallet.get_config().id))
|
|
||||||
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
|
|
||||||
.max_height(128.0)
|
|
||||||
.auto_shrink([false; 2])
|
|
||||||
.show(ui, |ui| {
|
|
||||||
ui.add_space(7.0);
|
|
||||||
egui::TextEdit::multiline(&mut result_text)
|
|
||||||
.font(egui::TextStyle::Small)
|
|
||||||
.desired_rows(5)
|
|
||||||
.interactive(false)
|
|
||||||
.desired_width(f32::INFINITY)
|
|
||||||
.show(ui);
|
|
||||||
ui.add_space(6.0);
|
|
||||||
});
|
|
||||||
ui.add_space(2.0);
|
|
||||||
View::horizontal_line(ui, Colors::item_stroke());
|
|
||||||
ui.add_space(10.0);
|
|
||||||
|
|
||||||
// Show copy button.
|
|
||||||
ui.vertical_centered(|ui| {
|
|
||||||
let copy_text = format!("{} {}", COPY, t!("copy"));
|
|
||||||
View::button(ui, copy_text, Colors::button(), || {
|
|
||||||
cb.copy_string_to_buffer(result_text.to_string());
|
|
||||||
self.qr_scan_result = None;
|
|
||||||
modal.close();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
ui.add_space(10.0);
|
|
||||||
View::horizontal_line(ui, Colors::item_stroke());
|
|
||||||
ui.add_space(6.0);
|
|
||||||
} else if let Some(result) = self.camera_content.qr_scan_result() {
|
|
||||||
cb.stop_camera();
|
|
||||||
self.camera_content.clear_state();
|
|
||||||
match &result {
|
|
||||||
QrScanResult::Slatepack(message) => {
|
|
||||||
// Redirect to messages to handle parsed message.
|
|
||||||
let mut messages =
|
|
||||||
WalletMessages::new(wallet.can_use_dandelion(), Some(message.to_string()));
|
|
||||||
messages.parse_message(wallet);
|
|
||||||
modal.close();
|
|
||||||
self.current_tab = Box::new(messages);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
QrScanResult::Address(receiver) => {
|
|
||||||
if wallet.get_data().unwrap().info.amount_currently_spendable > 0 {
|
|
||||||
// Redirect to send amount with Tor.
|
|
||||||
let addr = wallet.slatepack_address().unwrap();
|
|
||||||
let mut transport = WalletTransport::new(addr.clone());
|
|
||||||
modal.close();
|
|
||||||
transport.show_send_tor_modal(cb, Some(receiver.to_string()));
|
|
||||||
self.current_tab = Box::new(transport);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set result and rename modal title.
|
|
||||||
self.qr_scan_result = Some(result);
|
|
||||||
Modal::set_title(t!("scan_result"));
|
|
||||||
} else {
|
|
||||||
ui.add_space(6.0);
|
|
||||||
self.camera_content.ui(ui, cb);
|
|
||||||
ui.add_space(6.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.qr_scan_result.is_some() {
|
|
||||||
// Setup spacing between buttons.
|
|
||||||
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
|
|
||||||
|
|
||||||
ui.columns(2, |columns| {
|
|
||||||
columns[0].vertical_centered_justified(|ui| {
|
|
||||||
View::button(ui, t!("close"), Colors::white_or_black(false), || {
|
|
||||||
self.qr_scan_result = None;
|
|
||||||
modal.close();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
columns[1].vertical_centered_justified(|ui| {
|
|
||||||
View::button(ui, t!("repeat"), Colors::white_or_black(false), || {
|
|
||||||
Modal::set_title(t!("scan_qr"));
|
|
||||||
self.qr_scan_result = None;
|
|
||||||
cb.start_camera();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
ui.vertical_centered_justified(|ui| {
|
|
||||||
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
|
|
||||||
cb.stop_camera();
|
|
||||||
modal.close();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
ui.add_space(6.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Draw tab buttons in the bottom of the screen.
|
/// Draw tab buttons in the bottom of the screen.
|
||||||
fn tabs_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet) {
|
fn tabs_ui(&mut self, ui: &mut egui::Ui) {
|
||||||
ui.scope(|ui| {
|
ui.scope(|ui| {
|
||||||
// Setup spacing between tabs.
|
// Setup spacing between tabs.
|
||||||
ui.style_mut().spacing.item_spacing = egui::vec2(View::TAB_ITEMS_PADDING, 0.0);
|
ui.style_mut().spacing.item_spacing = egui::vec2(View::TAB_ITEMS_PADDING, 0.0);
|
||||||
|
@ -547,14 +341,13 @@ impl WalletContent {
|
||||||
let is_messages = current_type == WalletTabType::Messages;
|
let is_messages = current_type == WalletTabType::Messages;
|
||||||
View::tab_button(ui, CHAT_CIRCLE_TEXT, is_messages, || {
|
View::tab_button(ui, CHAT_CIRCLE_TEXT, is_messages, || {
|
||||||
self.current_tab = Box::new(
|
self.current_tab = Box::new(
|
||||||
WalletMessages::new(wallet.can_use_dandelion(), None)
|
WalletMessages::new(None)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
columns[2].vertical_centered_justified(|ui| {
|
columns[2].vertical_centered_justified(|ui| {
|
||||||
View::tab_button(ui, BRIDGE, current_type == WalletTabType::Transport, || {
|
View::tab_button(ui, BRIDGE, current_type == WalletTabType::Transport, || {
|
||||||
let addr = wallet.slatepack_address().unwrap();
|
self.current_tab = Box::new(WalletTransport::default());
|
||||||
self.current_tab = Box::new(WalletTransport::new(addr));
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
columns[3].vertical_centered_justified(|ui| {
|
columns[3].vertical_centered_justified(|ui| {
|
||||||
|
@ -675,68 +468,4 @@ impl WalletContent {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const ACCOUNT_ITEM_HEIGHT: f32 = 75.0;
|
|
||||||
|
|
||||||
/// Draw account item.
|
|
||||||
fn account_item_ui(ui: &mut egui::Ui,
|
|
||||||
modal: &Modal,
|
|
||||||
wallet: &mut Wallet,
|
|
||||||
acc: &WalletAccount,
|
|
||||||
index: usize,
|
|
||||||
size: usize) {
|
|
||||||
// Setup layout size.
|
|
||||||
let mut rect = ui.available_rect_before_wrap();
|
|
||||||
rect.set_height(ACCOUNT_ITEM_HEIGHT);
|
|
||||||
|
|
||||||
// Draw round background.
|
|
||||||
let bg_rect = rect.clone();
|
|
||||||
let item_rounding = View::item_rounding(index, size, false);
|
|
||||||
ui.painter().rect(bg_rect, item_rounding, Colors::fill(), View::item_stroke());
|
|
||||||
|
|
||||||
ui.vertical(|ui| {
|
|
||||||
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
|
|
||||||
// Draw button to select account.
|
|
||||||
let is_current_account = wallet.get_config().account == acc.label;
|
|
||||||
if !is_current_account {
|
|
||||||
let button_rounding = View::item_rounding(index, size, true);
|
|
||||||
View::item_button(ui, button_rounding, CHECK, None, || {
|
|
||||||
let _ = wallet.set_active_account(&acc.label);
|
|
||||||
modal.close();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
ui.add_space(12.0);
|
|
||||||
ui.label(RichText::new(CHECK_FAT).size(20.0).color(Colors::green()));
|
|
||||||
}
|
|
||||||
|
|
||||||
let layout_size = ui.available_size();
|
|
||||||
ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| {
|
|
||||||
ui.add_space(6.0);
|
|
||||||
ui.vertical(|ui| {
|
|
||||||
ui.add_space(4.0);
|
|
||||||
// Show spendable amount.
|
|
||||||
let amount = amount_to_hr_string(acc.spendable_amount, true);
|
|
||||||
let amount_text = format!("{} {}", amount, GRIN);
|
|
||||||
ui.label(RichText::new(amount_text).size(18.0).color(Colors::white_or_black(true)));
|
|
||||||
ui.add_space(-2.0);
|
|
||||||
|
|
||||||
// Show account name.
|
|
||||||
let default_acc_label = WalletConfig::DEFAULT_ACCOUNT_LABEL.to_string();
|
|
||||||
let acc_label = if acc.label == default_acc_label {
|
|
||||||
t!("wallets.default_account")
|
|
||||||
} else {
|
|
||||||
acc.label.to_owned()
|
|
||||||
};
|
|
||||||
let acc_name = format!("{} {}", FOLDER_USER, acc_label);
|
|
||||||
View::ellipsize_text(ui, acc_name, 15.0, Colors::text(false));
|
|
||||||
|
|
||||||
// Show account BIP32 derivation path.
|
|
||||||
let acc_path = format!("{} {}", PATH, acc.path);
|
|
||||||
ui.label(RichText::new(acc_path).size(15.0).color(Colors::gray()));
|
|
||||||
ui.add_space(3.0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
File diff suppressed because it is too large
Load diff
552
src/gui/views/wallets/wallet/messages/content.rs
Normal file
552
src/gui/views/wallets/wallet/messages/content.rs
Normal file
|
@ -0,0 +1,552 @@
|
||||||
|
// Copyright 2024 The Grim Developers
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::thread;
|
||||||
|
use egui::{Id, Margin, RichText, ScrollArea};
|
||||||
|
use egui::scroll_area::ScrollBarVisibility;
|
||||||
|
use grin_core::core::amount_to_hr_string;
|
||||||
|
use grin_wallet_libwallet::{Error, Slate, SlateState};
|
||||||
|
use parking_lot::RwLock;
|
||||||
|
|
||||||
|
use crate::gui::Colors;
|
||||||
|
use crate::gui::icons::{BROOM, CLIPBOARD_TEXT, DOWNLOAD_SIMPLE, SCAN, UPLOAD_SIMPLE};
|
||||||
|
use crate::gui::platform::PlatformCallbacks;
|
||||||
|
use crate::gui::views::{FilePickButton, Modal, Content, View, CameraContent};
|
||||||
|
use crate::gui::views::types::{ModalPosition, QrScanResult};
|
||||||
|
use crate::gui::views::wallets::wallet::messages::request::MessageRequestModal;
|
||||||
|
use crate::gui::views::wallets::wallet::types::{SLATEPACK_MESSAGE_HINT, WalletTab, WalletTabType};
|
||||||
|
use crate::gui::views::wallets::wallet::{WalletContent, WalletTransactionModal};
|
||||||
|
use crate::wallet::types::WalletTransaction;
|
||||||
|
use crate::wallet::Wallet;
|
||||||
|
|
||||||
|
/// Slatepack messages interaction tab content.
|
||||||
|
pub struct WalletMessages {
|
||||||
|
/// Flag to check if it's first content draw.
|
||||||
|
first_draw: bool,
|
||||||
|
|
||||||
|
/// Slatepacks message input text.
|
||||||
|
message_edit: String,
|
||||||
|
/// Flag to check if message request is loading.
|
||||||
|
message_loading: bool,
|
||||||
|
/// Error on finalization, parse or response creation.
|
||||||
|
message_error: String,
|
||||||
|
/// Parsed message result.
|
||||||
|
message_result: Arc<RwLock<Option<(Slate, Result<WalletTransaction, Error>)>>>,
|
||||||
|
|
||||||
|
/// Wallet transaction [`Modal`] content.
|
||||||
|
tx_info_content: Option<WalletTransactionModal>,
|
||||||
|
|
||||||
|
/// Invoice or sending request creation [`Modal`] content.
|
||||||
|
request_modal_content: Option<MessageRequestModal>,
|
||||||
|
|
||||||
|
/// Camera content for Slatepack message QR code scanning [`Modal`].
|
||||||
|
message_camera_content: CameraContent,
|
||||||
|
/// Flag to check if there is an error on scanning Slatepack message QR code at [`Modal`].
|
||||||
|
message_scan_error: bool,
|
||||||
|
|
||||||
|
/// Button to parse picked file content.
|
||||||
|
file_pick_button: FilePickButton,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Identifier for amount input [`Modal`] to create invoice or sending request.
|
||||||
|
const REQUEST_MODAL: &'static str = "messages_request";
|
||||||
|
|
||||||
|
/// Identifier for [`Modal`] modal to show transaction information.
|
||||||
|
const TX_INFO_MODAL: &'static str = "messages_tx_info";
|
||||||
|
|
||||||
|
/// Identifier for [`Modal`] to scan Slatepack message from QR code.
|
||||||
|
const SCAN_QR_MESSAGE_MODAL: &'static str = "qr_slatepack_message_scan_modal";
|
||||||
|
|
||||||
|
impl WalletTab for WalletMessages {
|
||||||
|
fn get_type(&self) -> WalletTabType {
|
||||||
|
WalletTabType::Messages
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ui(&mut self, ui: &mut egui::Ui, wallet: &mut Wallet, cb: &dyn PlatformCallbacks) {
|
||||||
|
if WalletContent::sync_ui(ui, wallet) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show modal content for this ui container.
|
||||||
|
self.modal_content_ui(ui, wallet, cb);
|
||||||
|
|
||||||
|
egui::CentralPanel::default()
|
||||||
|
.frame(egui::Frame {
|
||||||
|
stroke: View::item_stroke(),
|
||||||
|
fill: Colors::white_or_black(false),
|
||||||
|
inner_margin: Margin {
|
||||||
|
left: View::far_left_inset_margin(ui) + 4.0,
|
||||||
|
right: View::get_right_inset() + 4.0,
|
||||||
|
top: 3.0,
|
||||||
|
bottom: 4.0,
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.show_inside(ui, |ui| {
|
||||||
|
ScrollArea::vertical()
|
||||||
|
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
|
||||||
|
.id_source(Id::from("wallet_messages").with(wallet.get_config().id))
|
||||||
|
.auto_shrink([false; 2])
|
||||||
|
.show(ui, |ui| {
|
||||||
|
ui.vertical_centered(|ui| {
|
||||||
|
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
|
||||||
|
self.ui(ui, wallet, cb);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WalletMessages {
|
||||||
|
/// Create new content instance, put message into input if provided.
|
||||||
|
pub fn new(message: Option<String>) -> Self {
|
||||||
|
Self {
|
||||||
|
first_draw: true,
|
||||||
|
message_edit: message.unwrap_or("".to_string()),
|
||||||
|
message_loading: false,
|
||||||
|
message_error: "".to_string(),
|
||||||
|
message_result: Arc::new(Default::default()),
|
||||||
|
tx_info_content: None,
|
||||||
|
request_modal_content: None,
|
||||||
|
message_camera_content: Default::default(),
|
||||||
|
message_scan_error: false,
|
||||||
|
file_pick_button: FilePickButton::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw manual wallet transaction interaction content.
|
||||||
|
pub fn ui(&mut self,
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
wallet: &mut Wallet,
|
||||||
|
cb: &dyn PlatformCallbacks) {
|
||||||
|
if self.first_draw {
|
||||||
|
// Parse provided message on first draw.
|
||||||
|
if !self.message_edit.is_empty() {
|
||||||
|
self.parse_message(wallet);
|
||||||
|
}
|
||||||
|
self.first_draw = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.add_space(3.0);
|
||||||
|
|
||||||
|
// Show creation of request to send or receive funds.
|
||||||
|
self.request_ui(ui, wallet, cb);
|
||||||
|
|
||||||
|
ui.add_space(12.0);
|
||||||
|
View::horizontal_line(ui, Colors::item_stroke());
|
||||||
|
ui.add_space(6.0);
|
||||||
|
|
||||||
|
// Show Slatepack message input field.
|
||||||
|
self.input_slatepack_ui(ui, wallet, cb);
|
||||||
|
|
||||||
|
ui.add_space(6.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw [`Modal`] content for this ui container.
|
||||||
|
fn modal_content_ui(&mut self,
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
wallet: &mut Wallet,
|
||||||
|
cb: &dyn PlatformCallbacks) {
|
||||||
|
match Modal::opened() {
|
||||||
|
None => {}
|
||||||
|
Some(id) => {
|
||||||
|
match id {
|
||||||
|
REQUEST_MODAL => {
|
||||||
|
if let Some(content) = self.request_modal_content.as_mut() {
|
||||||
|
Modal::ui(ui.ctx(), |ui, modal| {
|
||||||
|
content.ui(ui, wallet, modal, cb);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TX_INFO_MODAL => {
|
||||||
|
if let Some(content) = self.tx_info_content.as_mut() {
|
||||||
|
Modal::ui(ui.ctx(), |ui, modal| {
|
||||||
|
content.ui(ui, wallet, modal, cb);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SCAN_QR_MESSAGE_MODAL => {
|
||||||
|
Modal::ui(ui.ctx(), |ui, modal| {
|
||||||
|
self.qr_message_scan_modal_ui(ui, modal, wallet, cb);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw creation of request to send or receive funds.
|
||||||
|
fn request_ui(&mut self,
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
wallet: &Wallet,
|
||||||
|
cb: &dyn PlatformCallbacks) {
|
||||||
|
ui.label(RichText::new(t!("wallets.create_request_desc"))
|
||||||
|
.size(16.0)
|
||||||
|
.color(Colors::inactive_text()));
|
||||||
|
ui.add_space(7.0);
|
||||||
|
|
||||||
|
// Show send button only if balance is not empty.
|
||||||
|
let data = wallet.get_data().unwrap();
|
||||||
|
if data.info.amount_currently_spendable > 0 {
|
||||||
|
// Setup spacing between buttons.
|
||||||
|
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
|
||||||
|
|
||||||
|
ui.columns(2, |columns| {
|
||||||
|
columns[0].vertical_centered_justified(|ui| {
|
||||||
|
let send_text = format!("{} {}", UPLOAD_SIMPLE, t!("wallets.send"));
|
||||||
|
View::colored_text_button(ui, send_text, Colors::red(), Colors::button(), || {
|
||||||
|
self.show_request_modal(false, cb);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
columns[1].vertical_centered_justified(|ui| {
|
||||||
|
self.receive_button_ui(ui, cb);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
self.receive_button_ui(ui, cb);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw invoice request creation button.
|
||||||
|
fn receive_button_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||||
|
let receive_text = format!("{} {}", DOWNLOAD_SIMPLE, t!("wallets.receive"));
|
||||||
|
View::colored_text_button(ui, receive_text, Colors::green(), Colors::button(), || {
|
||||||
|
self.show_request_modal(true, cb);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Show [`Modal`] to create invoice or sending request.
|
||||||
|
fn show_request_modal(&mut self, invoice: bool, cb: &dyn PlatformCallbacks) {
|
||||||
|
self.request_modal_content = Some(MessageRequestModal::new(invoice));
|
||||||
|
let title = if invoice {
|
||||||
|
t!("wallets.receive")
|
||||||
|
} else {
|
||||||
|
t!("wallets.send")
|
||||||
|
};
|
||||||
|
Modal::new(REQUEST_MODAL).position(ModalPosition::CenterTop).title(title).show();
|
||||||
|
cb.show_keyboard();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw Slatepack message input content.
|
||||||
|
fn input_slatepack_ui(&mut self,
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
wallet: &mut Wallet,
|
||||||
|
cb: &dyn PlatformCallbacks) {
|
||||||
|
// Setup description text.
|
||||||
|
if !self.message_error.is_empty() {
|
||||||
|
ui.label(RichText::new(&self.message_error).size(16.0).color(Colors::red()));
|
||||||
|
} else {
|
||||||
|
ui.label(RichText::new(t!("wallets.input_slatepack_desc"))
|
||||||
|
.size(16.0)
|
||||||
|
.color(Colors::inactive_text()));
|
||||||
|
}
|
||||||
|
ui.add_space(6.0);
|
||||||
|
|
||||||
|
View::horizontal_line(ui, Colors::item_stroke());
|
||||||
|
ui.add_space(3.0);
|
||||||
|
|
||||||
|
// Save message to check for changes.
|
||||||
|
let message_before = self.message_edit.clone();
|
||||||
|
|
||||||
|
let scroll_id = Id::from("message_input_scroll").with(wallet.get_config().id);
|
||||||
|
ScrollArea::vertical()
|
||||||
|
.id_source(scroll_id)
|
||||||
|
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
|
||||||
|
.max_height(128.0)
|
||||||
|
.auto_shrink([false; 2])
|
||||||
|
.show(ui, |ui| {
|
||||||
|
ui.add_space(7.0);
|
||||||
|
let input_id = scroll_id.with("_input");
|
||||||
|
let resp = egui::TextEdit::multiline(&mut self.message_edit)
|
||||||
|
.id(input_id)
|
||||||
|
.font(egui::TextStyle::Small)
|
||||||
|
.desired_rows(5)
|
||||||
|
.interactive(!self.message_loading)
|
||||||
|
.hint_text(SLATEPACK_MESSAGE_HINT)
|
||||||
|
.desired_width(f32::INFINITY)
|
||||||
|
.show(ui)
|
||||||
|
.response;
|
||||||
|
// Show soft keyboard on click.
|
||||||
|
if resp.clicked() {
|
||||||
|
resp.request_focus();
|
||||||
|
cb.show_keyboard();
|
||||||
|
}
|
||||||
|
if resp.has_focus() {
|
||||||
|
// Apply text from input on Android as temporary fix for egui.
|
||||||
|
View::on_soft_input(ui, input_id, &mut self.message_edit);
|
||||||
|
}
|
||||||
|
ui.add_space(6.0);
|
||||||
|
});
|
||||||
|
ui.add_space(2.0);
|
||||||
|
View::horizontal_line(ui, Colors::item_stroke());
|
||||||
|
ui.add_space(10.0);
|
||||||
|
|
||||||
|
// Parse message if input field was changed.
|
||||||
|
if message_before != self.message_edit {
|
||||||
|
self.parse_message(wallet);
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.message_loading {
|
||||||
|
View::small_loading_spinner(ui);
|
||||||
|
// Check loading result.
|
||||||
|
let has_tx = {
|
||||||
|
let r_res = self.message_result.read();
|
||||||
|
r_res.is_some()
|
||||||
|
};
|
||||||
|
if has_tx {
|
||||||
|
let mut w_res = self.message_result.write();
|
||||||
|
let tx_res = w_res.as_ref().unwrap();
|
||||||
|
let slate = &tx_res.0;
|
||||||
|
match &tx_res.1 {
|
||||||
|
Ok(tx) => {
|
||||||
|
self.message_edit.clear();
|
||||||
|
// Show transaction modal on success.
|
||||||
|
self.tx_info_content = Some(WalletTransactionModal::new(wallet, tx, false));
|
||||||
|
Modal::new(TX_INFO_MODAL)
|
||||||
|
.position(ModalPosition::CenterTop)
|
||||||
|
.title(t!("wallets.tx"))
|
||||||
|
.show();
|
||||||
|
*w_res = None;
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
match err {
|
||||||
|
// Set already canceled transaction error message.
|
||||||
|
Error::TransactionWasCancelled {..} => {
|
||||||
|
self.message_error = t!("wallets.resp_canceled_err");
|
||||||
|
}
|
||||||
|
// Set an error when there is not enough funds to pay.
|
||||||
|
Error::NotEnoughFunds {..} => {
|
||||||
|
let m = t!(
|
||||||
|
"wallets.pay_balance_error",
|
||||||
|
"amount" => amount_to_hr_string(slate.amount, true)
|
||||||
|
);
|
||||||
|
self.message_error = m;
|
||||||
|
}
|
||||||
|
// Set default error message.
|
||||||
|
_ => {
|
||||||
|
let finalize = slate.state == SlateState::Standard2 ||
|
||||||
|
slate.state == SlateState::Invoice2;
|
||||||
|
self.message_error = if finalize {
|
||||||
|
t!("wallets.finalize_slatepack_err")
|
||||||
|
} else {
|
||||||
|
t!("wallets.resp_slatepack_err")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.message_loading = false;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.scope(|ui| {
|
||||||
|
// Setup spacing between buttons.
|
||||||
|
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
|
||||||
|
|
||||||
|
ui.columns(2, |columns| {
|
||||||
|
columns[0].vertical_centered_justified(|ui| {
|
||||||
|
// Draw button to scan Slatepack message QR code.
|
||||||
|
let scan_text = format!("{} {}", SCAN, t!("scan"));
|
||||||
|
View::button(ui, scan_text, Colors::button(), || {
|
||||||
|
self.message_edit.clear();
|
||||||
|
self.message_error.clear();
|
||||||
|
self.show_qr_message_scan_modal(cb);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
columns[1].vertical_centered_justified(|ui| {
|
||||||
|
// Draw button to paste text from clipboard.
|
||||||
|
let paste = format!("{} {}", CLIPBOARD_TEXT, t!("paste"));
|
||||||
|
View::button(ui, paste, Colors::button(), || {
|
||||||
|
let buf = cb.get_string_from_buffer();
|
||||||
|
let previous = self.message_edit.clone();
|
||||||
|
self.message_edit = buf.clone().trim().to_string();
|
||||||
|
// Parse Slatepack message resetting message error.
|
||||||
|
if buf != previous {
|
||||||
|
self.parse_message(wallet);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
ui.add_space(10.0);
|
||||||
|
});
|
||||||
|
|
||||||
|
if self.message_edit.is_empty() {
|
||||||
|
// Draw button to choose file.
|
||||||
|
let mut parsed_text = "".to_string();
|
||||||
|
self.file_pick_button.ui(ui, cb, |text| {
|
||||||
|
parsed_text = text;
|
||||||
|
});
|
||||||
|
self.message_edit = parsed_text;
|
||||||
|
self.parse_message(wallet);
|
||||||
|
} else {
|
||||||
|
// Draw button to clear message input.
|
||||||
|
let clear_text = format!("{} {}", BROOM, t!("clear"));
|
||||||
|
View::button(ui, clear_text, Colors::button(), || {
|
||||||
|
self.message_edit.clear();
|
||||||
|
self.message_error.clear();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse message input making operation based on incoming status.
|
||||||
|
fn parse_message(&mut self, wallet: &Wallet) {
|
||||||
|
self.message_error.clear();
|
||||||
|
self.message_edit = self.message_edit.trim().to_string();
|
||||||
|
if self.message_edit.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if let Ok(mut slate) = wallet.parse_slatepack(&self.message_edit) {
|
||||||
|
// Try to setup empty amount from transaction by id.
|
||||||
|
if slate.amount == 0 {
|
||||||
|
let _ = wallet.get_data().unwrap().txs.as_ref().unwrap().iter().map(|tx| {
|
||||||
|
if tx.data.tx_slate_id == Some(slate.id) {
|
||||||
|
if slate.amount == 0 {
|
||||||
|
slate.amount = tx.amount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tx
|
||||||
|
}).collect::<Vec<&WalletTransaction>>();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if message with same id and state already exists to show tx modal.
|
||||||
|
let exists = wallet.read_slatepack(&slate).is_some();
|
||||||
|
if exists {
|
||||||
|
if let Some(tx) = wallet.tx_by_slate(&slate).as_ref() {
|
||||||
|
self.message_edit.clear();
|
||||||
|
self.tx_info_content = Some(WalletTransactionModal::new(wallet, tx, false));
|
||||||
|
Modal::new(TX_INFO_MODAL)
|
||||||
|
.position(ModalPosition::CenterTop)
|
||||||
|
.title(t!("wallets.tx"))
|
||||||
|
.show();
|
||||||
|
} else {
|
||||||
|
self.message_error = t!("wallets.parse_slatepack_err");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create response or finalize at separate thread.
|
||||||
|
let sl = slate.clone();
|
||||||
|
let message = self.message_edit.clone();
|
||||||
|
let message_result = self.message_result.clone();
|
||||||
|
let wallet = wallet.clone();
|
||||||
|
|
||||||
|
self.message_loading = true;
|
||||||
|
thread::spawn(move || {
|
||||||
|
let result = match slate.state {
|
||||||
|
SlateState::Standard1 | SlateState::Invoice1 => {
|
||||||
|
if sl.state != SlateState::Standard1 {
|
||||||
|
wallet.pay(&message)
|
||||||
|
} else {
|
||||||
|
wallet.receive(&message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SlateState::Standard2 | SlateState::Invoice2 => {
|
||||||
|
wallet.finalize(&message)
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
if let Some(tx) = wallet.tx_by_slate(&slate) {
|
||||||
|
Ok(tx)
|
||||||
|
} else {
|
||||||
|
Err(Error::GenericError(t!("wallets.parse_slatepack_err")))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let mut w_res = message_result.write();
|
||||||
|
*w_res = Some((slate, result));
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
self.message_error = t!("wallets.parse_slatepack_err");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Show QR code Slatepack message scanner [`Modal`].
|
||||||
|
pub fn show_qr_message_scan_modal(&mut self, cb: &dyn PlatformCallbacks) {
|
||||||
|
self.message_scan_error = false;
|
||||||
|
// Show QR code scan modal.
|
||||||
|
Modal::new(SCAN_QR_MESSAGE_MODAL)
|
||||||
|
.position(ModalPosition::CenterTop)
|
||||||
|
.title(t!("scan_qr"))
|
||||||
|
.closeable(false)
|
||||||
|
.show();
|
||||||
|
cb.start_camera();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw QR code scanner [`Modal`] content.
|
||||||
|
fn qr_message_scan_modal_ui(&mut self,
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
modal: &Modal,
|
||||||
|
wallet: &Wallet,
|
||||||
|
cb: &dyn PlatformCallbacks) {
|
||||||
|
if self.message_scan_error {
|
||||||
|
ui.add_space(6.0);
|
||||||
|
ui.vertical_centered(|ui| {
|
||||||
|
let err_text = format!("{}", t!("wallets.parse_slatepack_err")).replace(":", ".");
|
||||||
|
ui.label(RichText::new(err_text)
|
||||||
|
.size(17.0)
|
||||||
|
.color(Colors::red()));
|
||||||
|
});
|
||||||
|
ui.add_space(12.0);
|
||||||
|
|
||||||
|
// Setup spacing between buttons.
|
||||||
|
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
|
||||||
|
|
||||||
|
ui.columns(2, |columns| {
|
||||||
|
columns[0].vertical_centered_justified(|ui| {
|
||||||
|
View::button(ui, t!("close"), Colors::white_or_black(false), || {
|
||||||
|
self.message_scan_error = false;
|
||||||
|
modal.close();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
columns[1].vertical_centered_justified(|ui| {
|
||||||
|
View::button(ui, t!("repeat"), Colors::white_or_black(false), || {
|
||||||
|
Modal::set_title(t!("scan_qr"));
|
||||||
|
self.message_scan_error = false;
|
||||||
|
cb.start_camera();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
ui.add_space(6.0);
|
||||||
|
return;
|
||||||
|
} else if let Some(result) = self.message_camera_content.qr_scan_result() {
|
||||||
|
cb.stop_camera();
|
||||||
|
self.message_camera_content.clear_state();
|
||||||
|
match &result {
|
||||||
|
QrScanResult::Slatepack(text) => {
|
||||||
|
self.message_edit = text.to_string();
|
||||||
|
self.parse_message(wallet);
|
||||||
|
modal.close();
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
self.message_scan_error = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ui.add_space(6.0);
|
||||||
|
self.message_camera_content.ui(ui, cb);
|
||||||
|
ui.add_space(8.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.vertical_centered_justified(|ui| {
|
||||||
|
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
|
||||||
|
cb.stop_camera();
|
||||||
|
modal.close();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
ui.add_space(6.0);
|
||||||
|
}
|
||||||
|
}
|
18
src/gui/views/wallets/wallet/messages/mod.rs
Normal file
18
src/gui/views/wallets/wallet/messages/mod.rs
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
// Copyright 2024 The Grim Developers
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
mod content;
|
||||||
|
pub use content::*;
|
||||||
|
|
||||||
|
mod request;
|
260
src/gui/views/wallets/wallet/messages/request.rs
Normal file
260
src/gui/views/wallets/wallet/messages/request.rs
Normal file
|
@ -0,0 +1,260 @@
|
||||||
|
// Copyright 2024 The Grim Developers
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::thread;
|
||||||
|
use parking_lot::RwLock;
|
||||||
|
use egui::{Id, RichText};
|
||||||
|
use grin_core::core::{amount_from_hr_string, amount_to_hr_string};
|
||||||
|
use grin_wallet_libwallet::Error;
|
||||||
|
|
||||||
|
use crate::gui::Colors;
|
||||||
|
use crate::gui::platform::PlatformCallbacks;
|
||||||
|
use crate::gui::views::{Modal, View};
|
||||||
|
use crate::gui::views::types::TextEditOptions;
|
||||||
|
use crate::gui::views::wallets::wallet::WalletTransactionModal;
|
||||||
|
use crate::wallet::types::WalletTransaction;
|
||||||
|
use crate::wallet::Wallet;
|
||||||
|
|
||||||
|
/// Invoice or sending request creation [`Modal`] content.
|
||||||
|
pub struct MessageRequestModal {
|
||||||
|
/// Flag to check if invoice or sending request was opened.
|
||||||
|
invoice: bool,
|
||||||
|
|
||||||
|
/// Amount to send or receive.
|
||||||
|
amount_edit: String,
|
||||||
|
|
||||||
|
/// Flag to check if request is loading.
|
||||||
|
request_loading: bool,
|
||||||
|
/// Request result if there is no error.
|
||||||
|
request_result: Arc<RwLock<Option<Result<WalletTransaction, Error>>>>,
|
||||||
|
/// Flag to check if there is an error happened on request creation.
|
||||||
|
request_error: Option<String>,
|
||||||
|
|
||||||
|
/// Request result transaction content.
|
||||||
|
result_tx_content: Option<WalletTransactionModal>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MessageRequestModal {
|
||||||
|
/// Create new content instance.
|
||||||
|
pub fn new(invoice: bool) -> Self {
|
||||||
|
Self {
|
||||||
|
invoice,
|
||||||
|
amount_edit: "".to_string(),
|
||||||
|
request_loading: false,
|
||||||
|
request_result: Arc::new(RwLock::new(None)),
|
||||||
|
request_error: None,
|
||||||
|
result_tx_content: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw [`Modal`] content.
|
||||||
|
pub fn ui(&mut self,
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
wallet: &mut Wallet,
|
||||||
|
modal: &Modal,
|
||||||
|
cb: &dyn PlatformCallbacks) {
|
||||||
|
// Draw transaction information on request result.
|
||||||
|
if let Some(tx) = self.result_tx_content.as_mut() {
|
||||||
|
tx.ui(ui, wallet, modal, cb);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.add_space(6.0);
|
||||||
|
|
||||||
|
// Draw content on request loading.
|
||||||
|
if self.request_loading {
|
||||||
|
self.loading_request_ui(ui, wallet, modal);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw amount input content.
|
||||||
|
self.amount_input_ui(ui, wallet, modal, cb);
|
||||||
|
|
||||||
|
// Show request creation error.
|
||||||
|
if let Some(err) = &self.request_error {
|
||||||
|
ui.add_space(12.0);
|
||||||
|
ui.vertical_centered(|ui| {
|
||||||
|
ui.label(RichText::new(err)
|
||||||
|
.size(17.0)
|
||||||
|
.color(Colors::red()));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.add_space(12.0);
|
||||||
|
|
||||||
|
// Setup spacing between buttons.
|
||||||
|
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
|
||||||
|
|
||||||
|
ui.columns(2, |columns| {
|
||||||
|
columns[0].vertical_centered_justified(|ui| {
|
||||||
|
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
|
||||||
|
self.amount_edit = "".to_string();
|
||||||
|
self.request_error = None;
|
||||||
|
cb.hide_keyboard();
|
||||||
|
modal.close();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
columns[1].vertical_centered_justified(|ui| {
|
||||||
|
// Button to create Slatepack message request.
|
||||||
|
View::button(ui, t!("continue"), Colors::white_or_black(false), || {
|
||||||
|
if self.amount_edit.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if let Ok(a) = amount_from_hr_string(self.amount_edit.as_str()) {
|
||||||
|
cb.hide_keyboard();
|
||||||
|
modal.disable_closing();
|
||||||
|
// Setup data for request.
|
||||||
|
let wallet = wallet.clone();
|
||||||
|
let invoice = self.invoice.clone();
|
||||||
|
let result = self.request_result.clone();
|
||||||
|
// Send request at another thread.
|
||||||
|
self.request_loading = true;
|
||||||
|
thread::spawn(move || {
|
||||||
|
let res = if invoice {
|
||||||
|
wallet.issue_invoice(a)
|
||||||
|
} else {
|
||||||
|
wallet.send(a)
|
||||||
|
};
|
||||||
|
let mut w_result = result.write();
|
||||||
|
*w_result = Some(res);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
let err = if self.invoice {
|
||||||
|
t!("wallets.invoice_slatepack_err")
|
||||||
|
} else {
|
||||||
|
t!("wallets.send_slatepack_err")
|
||||||
|
};
|
||||||
|
self.request_error = Some(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
ui.add_space(6.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw amount input content.
|
||||||
|
fn amount_input_ui(&mut self,
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
wallet: &mut Wallet,
|
||||||
|
modal: &Modal,
|
||||||
|
cb: &dyn PlatformCallbacks) {
|
||||||
|
ui.vertical_centered(|ui| {
|
||||||
|
let enter_text = if self.invoice {
|
||||||
|
t!("wallets.enter_amount_receive")
|
||||||
|
} else {
|
||||||
|
let data = wallet.get_data().unwrap();
|
||||||
|
let amount = amount_to_hr_string(data.info.amount_currently_spendable, true);
|
||||||
|
t!("wallets.enter_amount_send","amount" => amount)
|
||||||
|
};
|
||||||
|
ui.label(RichText::new(enter_text)
|
||||||
|
.size(17.0)
|
||||||
|
.color(Colors::gray()));
|
||||||
|
});
|
||||||
|
ui.add_space(8.0);
|
||||||
|
|
||||||
|
// Draw request amount text input.
|
||||||
|
let amount_edit_id = Id::from(modal.id).with(wallet.get_config().id);
|
||||||
|
let mut amount_edit_opts = TextEditOptions::new(amount_edit_id).h_center();
|
||||||
|
let amount_edit_before = self.amount_edit.clone();
|
||||||
|
View::text_edit(ui, cb, &mut self.amount_edit, &mut amount_edit_opts);
|
||||||
|
|
||||||
|
// Check value if input was changed.
|
||||||
|
if amount_edit_before != self.amount_edit {
|
||||||
|
self.request_error = None;
|
||||||
|
if !self.amount_edit.is_empty() {
|
||||||
|
self.amount_edit = self.amount_edit.trim().replace(",", ".");
|
||||||
|
match amount_from_hr_string(self.amount_edit.as_str()) {
|
||||||
|
Ok(a) => {
|
||||||
|
if !self.amount_edit.contains(".") {
|
||||||
|
// To avoid input of several "0".
|
||||||
|
if a == 0 {
|
||||||
|
self.amount_edit = "0".to_string();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Check input after ".".
|
||||||
|
let parts = self.amount_edit
|
||||||
|
.split(".")
|
||||||
|
.collect::<Vec<&str>>();
|
||||||
|
if parts.len() == 2 && parts[1].len() > 9 {
|
||||||
|
self.amount_edit = amount_edit_before;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do not input amount more than balance in sending.
|
||||||
|
if !self.invoice {
|
||||||
|
let b = wallet.get_data().unwrap().info.amount_currently_spendable;
|
||||||
|
if b < a {
|
||||||
|
self.amount_edit = amount_edit_before;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
self.amount_edit = amount_edit_before;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw loading request content.
|
||||||
|
fn loading_request_ui(&mut self, ui: &mut egui::Ui, wallet: &mut Wallet, modal: &Modal) {
|
||||||
|
ui.add_space(34.0);
|
||||||
|
ui.vertical_centered(|ui| {
|
||||||
|
View::big_loading_spinner(ui);
|
||||||
|
});
|
||||||
|
ui.add_space(50.0);
|
||||||
|
|
||||||
|
// Check if there is request result error.
|
||||||
|
if self.request_error.is_some() {
|
||||||
|
modal.enable_closing();
|
||||||
|
self.request_loading = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update data on request result.
|
||||||
|
let r_request = self.request_result.read();
|
||||||
|
if r_request.is_some() {
|
||||||
|
modal.enable_closing();
|
||||||
|
let result = r_request.as_ref().unwrap();
|
||||||
|
match result {
|
||||||
|
Ok(tx) => {
|
||||||
|
self.result_tx_content = Some(WalletTransactionModal::new(wallet, tx, false));
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
match err {
|
||||||
|
Error::NotEnoughFunds { .. } => {
|
||||||
|
let m = t!(
|
||||||
|
"wallets.pay_balance_error",
|
||||||
|
"amount" => self.amount_edit
|
||||||
|
);
|
||||||
|
self.request_error = Some(m);
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
let m = if self.invoice {
|
||||||
|
t!("wallets.invoice_slatepack_err")
|
||||||
|
} else {
|
||||||
|
t!("wallets.send_slatepack_err")
|
||||||
|
};
|
||||||
|
self.request_error = Some(m);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.request_loading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,10 +13,12 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
pub mod types;
|
pub mod types;
|
||||||
pub mod settings;
|
|
||||||
|
mod settings;
|
||||||
|
pub use settings::*;
|
||||||
|
|
||||||
mod txs;
|
mod txs;
|
||||||
pub use txs::WalletTransactions;
|
pub use txs::*;
|
||||||
|
|
||||||
mod messages;
|
mod messages;
|
||||||
pub use messages::WalletMessages;
|
pub use messages::WalletMessages;
|
||||||
|
@ -25,4 +27,6 @@ mod transport;
|
||||||
pub use transport::WalletTransport;
|
pub use transport::WalletTransport;
|
||||||
|
|
||||||
mod content;
|
mod content;
|
||||||
pub use content::WalletContent;
|
pub use content::WalletContent;
|
||||||
|
|
||||||
|
mod modals;
|
240
src/gui/views/wallets/wallet/modals/accounts.rs
Normal file
240
src/gui/views/wallets/wallet/modals/accounts.rs
Normal file
|
@ -0,0 +1,240 @@
|
||||||
|
// Copyright 2024 The Grim Developers
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
use egui::{Align, Id, Layout, RichText, ScrollArea};
|
||||||
|
use egui::scroll_area::ScrollBarVisibility;
|
||||||
|
use grin_core::core::amount_to_hr_string;
|
||||||
|
|
||||||
|
use crate::gui::Colors;
|
||||||
|
use crate::gui::icons::{CHECK, CHECK_FAT, FOLDER_USER, PATH};
|
||||||
|
use crate::gui::platform::PlatformCallbacks;
|
||||||
|
use crate::gui::views::{Modal, View};
|
||||||
|
use crate::gui::views::types::TextEditOptions;
|
||||||
|
use crate::gui::views::wallets::wallet::types::GRIN;
|
||||||
|
use crate::wallet::types::WalletAccount;
|
||||||
|
use crate::wallet::{Wallet, WalletConfig};
|
||||||
|
|
||||||
|
/// Wallet accounts [`Modal`] content.
|
||||||
|
pub struct WalletAccountsModal {
|
||||||
|
/// List of wallet accounts.
|
||||||
|
accounts: Vec<WalletAccount>,
|
||||||
|
/// Flag to check if account is creating.
|
||||||
|
account_creating: bool,
|
||||||
|
/// Account label value.
|
||||||
|
account_label_edit: String,
|
||||||
|
/// Flag to check if error occurred during account creation.
|
||||||
|
account_creation_error: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for WalletAccountsModal {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
accounts: vec![],
|
||||||
|
account_creating: false,
|
||||||
|
account_label_edit: "".to_string(),
|
||||||
|
account_creation_error: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WalletAccountsModal {
|
||||||
|
/// Create new instance from wallet accounts.
|
||||||
|
pub fn new(accounts: Vec<WalletAccount>) -> Self {
|
||||||
|
Self {
|
||||||
|
accounts,
|
||||||
|
account_creating: false,
|
||||||
|
account_label_edit: "".to_string(),
|
||||||
|
account_creation_error: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw [`Modal`] content.
|
||||||
|
pub fn ui(&mut self,
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
wallet: &Wallet,
|
||||||
|
modal: &Modal,
|
||||||
|
cb: &dyn PlatformCallbacks) {
|
||||||
|
if self.account_creating {
|
||||||
|
ui.add_space(6.0);
|
||||||
|
ui.vertical_centered(|ui| {
|
||||||
|
ui.label(RichText::new(t!("wallets.new_account_desc"))
|
||||||
|
.size(17.0)
|
||||||
|
.color(Colors::gray()));
|
||||||
|
ui.add_space(8.0);
|
||||||
|
|
||||||
|
// Draw account name edit.
|
||||||
|
let text_edit_id = Id::from(modal.id).with(wallet.get_config().id);
|
||||||
|
let mut text_edit_opts = TextEditOptions::new(text_edit_id);
|
||||||
|
View::text_edit(ui, cb, &mut self.account_label_edit, &mut text_edit_opts);
|
||||||
|
|
||||||
|
// Show error occurred during account creation..
|
||||||
|
if self.account_creation_error {
|
||||||
|
ui.add_space(12.0);
|
||||||
|
ui.label(RichText::new(t!("error"))
|
||||||
|
.size(17.0)
|
||||||
|
.color(Colors::red()));
|
||||||
|
}
|
||||||
|
ui.add_space(12.0);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Setup spacing between buttons.
|
||||||
|
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
|
||||||
|
|
||||||
|
// Show modal buttons.
|
||||||
|
ui.columns(2, |columns| {
|
||||||
|
columns[0].vertical_centered_justified(|ui| {
|
||||||
|
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
|
||||||
|
// Close modal.
|
||||||
|
cb.hide_keyboard();
|
||||||
|
modal.close();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
columns[1].vertical_centered_justified(|ui| {
|
||||||
|
// Create button callback.
|
||||||
|
let mut on_create = || {
|
||||||
|
if !self.account_label_edit.is_empty() {
|
||||||
|
let label = &self.account_label_edit;
|
||||||
|
match wallet.create_account(label) {
|
||||||
|
Ok(_) => {
|
||||||
|
let _ = wallet.set_active_account(label);
|
||||||
|
cb.hide_keyboard();
|
||||||
|
modal.close();
|
||||||
|
},
|
||||||
|
Err(_) => self.account_creation_error = true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
View::on_enter_key(ui, || {
|
||||||
|
(on_create)();
|
||||||
|
});
|
||||||
|
|
||||||
|
View::button(ui, t!("create"), Colors::white_or_black(false), on_create);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
ui.add_space(6.0);
|
||||||
|
} else {
|
||||||
|
ui.add_space(3.0);
|
||||||
|
|
||||||
|
// Show list of accounts.
|
||||||
|
let size = self.accounts.len();
|
||||||
|
ScrollArea::vertical()
|
||||||
|
.id_source("account_list_modal_scroll")
|
||||||
|
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
|
||||||
|
.max_height(266.0)
|
||||||
|
.auto_shrink([true; 2])
|
||||||
|
.show_rows(ui, ACCOUNT_ITEM_HEIGHT, size, |ui, row_range| {
|
||||||
|
for index in row_range {
|
||||||
|
// Add space before the first item.
|
||||||
|
if index == 0 {
|
||||||
|
ui.add_space(4.0);
|
||||||
|
}
|
||||||
|
let acc = self.accounts.get(index).unwrap();
|
||||||
|
account_item_ui(ui, modal, wallet, acc, index, size);
|
||||||
|
if index == size - 1 {
|
||||||
|
ui.add_space(4.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ui.add_space(2.0);
|
||||||
|
View::horizontal_line(ui, Colors::stroke());
|
||||||
|
ui.add_space(6.0);
|
||||||
|
|
||||||
|
// Setup spacing between buttons.
|
||||||
|
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
|
||||||
|
|
||||||
|
// Show modal buttons.
|
||||||
|
ui.columns(2, |columns| {
|
||||||
|
columns[0].vertical_centered_justified(|ui| {
|
||||||
|
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
|
||||||
|
modal.close();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
columns[1].vertical_centered_justified(|ui| {
|
||||||
|
View::button(ui, t!("create"), Colors::white_or_black(false), || {
|
||||||
|
self.account_creating = true;
|
||||||
|
cb.show_keyboard();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
ui.add_space(6.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const ACCOUNT_ITEM_HEIGHT: f32 = 75.0;
|
||||||
|
|
||||||
|
/// Draw account item.
|
||||||
|
fn account_item_ui(ui: &mut egui::Ui,
|
||||||
|
modal: &Modal,
|
||||||
|
wallet: &Wallet,
|
||||||
|
acc: &WalletAccount,
|
||||||
|
index: usize,
|
||||||
|
size: usize) {
|
||||||
|
// Setup layout size.
|
||||||
|
let mut rect = ui.available_rect_before_wrap();
|
||||||
|
rect.set_height(ACCOUNT_ITEM_HEIGHT);
|
||||||
|
|
||||||
|
// Draw round background.
|
||||||
|
let bg_rect = rect.clone();
|
||||||
|
let item_rounding = View::item_rounding(index, size, false);
|
||||||
|
ui.painter().rect(bg_rect, item_rounding, Colors::fill(), View::item_stroke());
|
||||||
|
|
||||||
|
ui.vertical(|ui| {
|
||||||
|
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
|
||||||
|
// Draw button to select account.
|
||||||
|
let is_current_account = wallet.get_config().account == acc.label;
|
||||||
|
if !is_current_account {
|
||||||
|
let button_rounding = View::item_rounding(index, size, true);
|
||||||
|
View::item_button(ui, button_rounding, CHECK, None, || {
|
||||||
|
let _ = wallet.set_active_account(&acc.label);
|
||||||
|
modal.close();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
ui.add_space(12.0);
|
||||||
|
ui.label(RichText::new(CHECK_FAT).size(20.0).color(Colors::green()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let layout_size = ui.available_size();
|
||||||
|
ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| {
|
||||||
|
ui.add_space(6.0);
|
||||||
|
ui.vertical(|ui| {
|
||||||
|
ui.add_space(4.0);
|
||||||
|
// Show spendable amount.
|
||||||
|
let amount = amount_to_hr_string(acc.spendable_amount, true);
|
||||||
|
let amount_text = format!("{} {}", amount, GRIN);
|
||||||
|
ui.label(RichText::new(amount_text).size(18.0).color(Colors::white_or_black(true)));
|
||||||
|
ui.add_space(-2.0);
|
||||||
|
|
||||||
|
// Show account name.
|
||||||
|
let default_acc_label = WalletConfig::DEFAULT_ACCOUNT_LABEL.to_string();
|
||||||
|
let acc_label = if acc.label == default_acc_label {
|
||||||
|
t!("wallets.default_account")
|
||||||
|
} else {
|
||||||
|
acc.label.to_owned()
|
||||||
|
};
|
||||||
|
let acc_name = format!("{} {}", FOLDER_USER, acc_label);
|
||||||
|
View::ellipsize_text(ui, acc_name, 15.0, Colors::text(false));
|
||||||
|
|
||||||
|
// Show account BIP32 derivation path.
|
||||||
|
let acc_path = format!("{} {}", PATH, acc.path);
|
||||||
|
ui.label(RichText::new(acc_path).size(15.0).color(Colors::gray()));
|
||||||
|
ui.add_space(3.0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
19
src/gui/views/wallets/wallet/modals/mod.rs
Normal file
19
src/gui/views/wallets/wallet/modals/mod.rs
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
// Copyright 2024 The Grim Developers
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
mod accounts;
|
||||||
|
pub use accounts::*;
|
||||||
|
|
||||||
|
mod scan;
|
||||||
|
pub use scan::*;
|
133
src/gui/views/wallets/wallet/modals/scan.rs
Normal file
133
src/gui/views/wallets/wallet/modals/scan.rs
Normal file
|
@ -0,0 +1,133 @@
|
||||||
|
// Copyright 2024 The Grim Developers
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
use egui::scroll_area::ScrollBarVisibility;
|
||||||
|
use egui::{Id, ScrollArea};
|
||||||
|
|
||||||
|
use crate::gui::Colors;
|
||||||
|
use crate::gui::icons::COPY;
|
||||||
|
use crate::gui::platform::PlatformCallbacks;
|
||||||
|
use crate::gui::views::{CameraContent, Modal, View};
|
||||||
|
use crate::gui::views::types::QrScanResult;
|
||||||
|
use crate::wallet::Wallet;
|
||||||
|
|
||||||
|
/// QR code scan [`Modal`] content.
|
||||||
|
pub struct WalletScanModal {
|
||||||
|
/// Camera content for QR scan [`Modal`].
|
||||||
|
camera_content: Option<CameraContent>,
|
||||||
|
/// QR code scan result
|
||||||
|
qr_scan_result: Option<QrScanResult>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for WalletScanModal {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
camera_content: None,
|
||||||
|
qr_scan_result: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WalletScanModal {
|
||||||
|
/// Draw [`Modal`] content.
|
||||||
|
pub fn ui(&mut self,
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
wallet: &Wallet,
|
||||||
|
modal: &Modal,
|
||||||
|
cb: &dyn PlatformCallbacks,
|
||||||
|
mut on_result: impl FnMut(&QrScanResult)) {
|
||||||
|
// Show scan result if exists or show camera content while scanning.
|
||||||
|
if let Some(result) = &self.qr_scan_result {
|
||||||
|
let mut result_text = result.text();
|
||||||
|
View::horizontal_line(ui, Colors::item_stroke());
|
||||||
|
ui.add_space(3.0);
|
||||||
|
ScrollArea::vertical()
|
||||||
|
.id_source(Id::from("qr_scan_result_input").with(wallet.get_config().id))
|
||||||
|
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
|
||||||
|
.max_height(128.0)
|
||||||
|
.auto_shrink([false; 2])
|
||||||
|
.show(ui, |ui| {
|
||||||
|
ui.add_space(7.0);
|
||||||
|
egui::TextEdit::multiline(&mut result_text)
|
||||||
|
.font(egui::TextStyle::Small)
|
||||||
|
.desired_rows(5)
|
||||||
|
.interactive(false)
|
||||||
|
.desired_width(f32::INFINITY)
|
||||||
|
.show(ui);
|
||||||
|
ui.add_space(6.0);
|
||||||
|
});
|
||||||
|
ui.add_space(2.0);
|
||||||
|
View::horizontal_line(ui, Colors::item_stroke());
|
||||||
|
ui.add_space(10.0);
|
||||||
|
|
||||||
|
// Show copy button.
|
||||||
|
ui.vertical_centered(|ui| {
|
||||||
|
let copy_text = format!("{} {}", COPY, t!("copy"));
|
||||||
|
View::button(ui, copy_text, Colors::button(), || {
|
||||||
|
cb.copy_string_to_buffer(result_text.to_string());
|
||||||
|
self.qr_scan_result = None;
|
||||||
|
modal.close();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
ui.add_space(10.0);
|
||||||
|
View::horizontal_line(ui, Colors::item_stroke());
|
||||||
|
ui.add_space(6.0);
|
||||||
|
} else if let Some(result) = self.camera_content.get_or_insert(CameraContent::default())
|
||||||
|
.qr_scan_result() {
|
||||||
|
cb.stop_camera();
|
||||||
|
self.camera_content = None;
|
||||||
|
on_result(&result);
|
||||||
|
|
||||||
|
// Set result and rename modal title.
|
||||||
|
self.qr_scan_result = Some(result);
|
||||||
|
Modal::set_title(t!("scan_result"));
|
||||||
|
} else {
|
||||||
|
ui.add_space(6.0);
|
||||||
|
self.camera_content.as_mut().unwrap().ui(ui, cb);
|
||||||
|
ui.add_space(6.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.qr_scan_result.is_some() {
|
||||||
|
// Setup spacing between buttons.
|
||||||
|
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
|
||||||
|
|
||||||
|
ui.columns(2, |columns| {
|
||||||
|
columns[0].vertical_centered_justified(|ui| {
|
||||||
|
View::button(ui, t!("close"), Colors::white_or_black(false), || {
|
||||||
|
self.qr_scan_result = None;
|
||||||
|
self.camera_content = None;
|
||||||
|
modal.close();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
columns[1].vertical_centered_justified(|ui| {
|
||||||
|
View::button(ui, t!("repeat"), Colors::white_or_black(false), || {
|
||||||
|
Modal::set_title(t!("scan_qr"));
|
||||||
|
self.qr_scan_result = None;
|
||||||
|
self.camera_content = Some(CameraContent::default());
|
||||||
|
cb.start_camera();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
ui.vertical_centered_justified(|ui| {
|
||||||
|
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
|
||||||
|
cb.stop_camera();
|
||||||
|
self.camera_content = None;
|
||||||
|
modal.close();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
ui.add_space(6.0);
|
||||||
|
}
|
||||||
|
}
|
|
@ -36,7 +36,7 @@ pub struct CommonSettings {
|
||||||
new_pass_edit: String,
|
new_pass_edit: String,
|
||||||
|
|
||||||
/// Minimum confirmations number value.
|
/// Minimum confirmations number value.
|
||||||
min_confirmations_edit: String
|
min_confirmations_edit: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Identifier for wallet name [`Modal`].
|
/// Identifier for wallet name [`Modal`].
|
||||||
|
@ -54,25 +54,26 @@ impl Default for CommonSettings {
|
||||||
wrong_pass: false,
|
wrong_pass: false,
|
||||||
old_pass_edit: "".to_string(),
|
old_pass_edit: "".to_string(),
|
||||||
new_pass_edit: "".to_string(),
|
new_pass_edit: "".to_string(),
|
||||||
min_confirmations_edit: "".to_string()
|
min_confirmations_edit: "".to_string(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CommonSettings {
|
impl CommonSettings {
|
||||||
|
/// Draw common wallet settings content.
|
||||||
pub fn ui(&mut self, ui: &mut egui::Ui, wallet: &mut Wallet, cb: &dyn PlatformCallbacks) {
|
pub fn ui(&mut self, ui: &mut egui::Ui, wallet: &mut Wallet, cb: &dyn PlatformCallbacks) {
|
||||||
// Show modal content for this ui container.
|
// Show modal content for this ui container.
|
||||||
self.modal_content_ui(ui, wallet, cb);
|
self.modal_content_ui(ui, wallet, cb);
|
||||||
|
|
||||||
ui.vertical_centered(|ui| {
|
ui.vertical_centered(|ui| {
|
||||||
let wallet_name = wallet.get_config().name;
|
let config = wallet.get_config();
|
||||||
// Show wallet name.
|
// Show wallet name.
|
||||||
ui.add_space(2.0);
|
ui.add_space(2.0);
|
||||||
ui.label(RichText::new(t!("wallets.name"))
|
ui.label(RichText::new(t!("wallets.name"))
|
||||||
.size(16.0)
|
.size(16.0)
|
||||||
.color(Colors::gray()));
|
.color(Colors::gray()));
|
||||||
ui.add_space(2.0);
|
ui.add_space(2.0);
|
||||||
ui.label(RichText::new(wallet_name.clone())
|
ui.label(RichText::new(&config.name)
|
||||||
.size(16.0)
|
.size(16.0)
|
||||||
.color(Colors::white_or_black(true)));
|
.color(Colors::white_or_black(true)));
|
||||||
ui.add_space(8.0);
|
ui.add_space(8.0);
|
||||||
|
@ -80,7 +81,7 @@ impl CommonSettings {
|
||||||
// Show wallet name setup.
|
// Show wallet name setup.
|
||||||
let name_text = format!("{} {}", PENCIL, t!("change"));
|
let name_text = format!("{} {}", PENCIL, t!("change"));
|
||||||
View::button(ui, name_text, Colors::button(), || {
|
View::button(ui, name_text, Colors::button(), || {
|
||||||
self.name_edit = wallet_name;
|
self.name_edit = config.name;
|
||||||
// Show wallet name modal.
|
// Show wallet name modal.
|
||||||
Modal::new(NAME_EDIT_MODAL)
|
Modal::new(NAME_EDIT_MODAL)
|
||||||
.position(ModalPosition::CenterTop)
|
.position(ModalPosition::CenterTop)
|
||||||
|
@ -118,10 +119,9 @@ impl CommonSettings {
|
||||||
ui.add_space(6.0);
|
ui.add_space(6.0);
|
||||||
|
|
||||||
// Show minimum amount of confirmations value setup.
|
// Show minimum amount of confirmations value setup.
|
||||||
let min_confirmations = wallet.get_config().min_confirmations;
|
let min_conf_text = format!("{} {}", CLOCK_COUNTDOWN, config.min_confirmations);
|
||||||
let min_conf_text = format!("{} {}", CLOCK_COUNTDOWN, min_confirmations);
|
|
||||||
View::button(ui, min_conf_text, Colors::button(), || {
|
View::button(ui, min_conf_text, Colors::button(), || {
|
||||||
self.min_confirmations_edit = min_confirmations.to_string();
|
self.min_confirmations_edit = config.min_confirmations.to_string();
|
||||||
// Show minimum amount of confirmations value modal.
|
// Show minimum amount of confirmations value modal.
|
||||||
Modal::new(MIN_CONFIRMATIONS_EDIT_MODAL)
|
Modal::new(MIN_CONFIRMATIONS_EDIT_MODAL)
|
||||||
.position(ModalPosition::CenterTop)
|
.position(ModalPosition::CenterTop)
|
||||||
|
@ -131,8 +131,15 @@ impl CommonSettings {
|
||||||
});
|
});
|
||||||
|
|
||||||
ui.add_space(12.0);
|
ui.add_space(12.0);
|
||||||
|
|
||||||
|
// Setup ability to post wallet transactions with Dandelion.
|
||||||
|
View::checkbox(ui, wallet.can_use_dandelion(), t!("wallets.use_dandelion"), || {
|
||||||
|
wallet.update_use_dandelion(!wallet.can_use_dandelion());
|
||||||
|
});
|
||||||
|
|
||||||
|
ui.add_space(6.0);
|
||||||
View::horizontal_line(ui, Colors::stroke());
|
View::horizontal_line(ui, Colors::stroke());
|
||||||
ui.add_space(4.0);
|
ui.add_space(6.0);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,7 @@ use egui::scroll_area::ScrollBarVisibility;
|
||||||
use crate::gui::Colors;
|
use crate::gui::Colors;
|
||||||
use crate::gui::platform::PlatformCallbacks;
|
use crate::gui::platform::PlatformCallbacks;
|
||||||
use crate::gui::views::{Content, View};
|
use crate::gui::views::{Content, View};
|
||||||
use crate::gui::views::wallets::settings::{CommonSettings, ConnectionSettings, RecoverySettings};
|
use crate::gui::views::wallets::{CommonSettings, ConnectionSettings, RecoverySettings};
|
||||||
use crate::gui::views::wallets::types::{WalletTab, WalletTabType};
|
use crate::gui::views::wallets::types::{WalletTab, WalletTabType};
|
||||||
use crate::gui::views::wallets::WalletContent;
|
use crate::gui::views::wallets::WalletContent;
|
||||||
use crate::wallet::Wallet;
|
use crate::wallet::Wallet;
|
||||||
|
|
|
@ -232,7 +232,7 @@ impl RecoverySettings {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
columns[1].vertical_centered_justified(|ui| {
|
columns[1].vertical_centered_justified(|ui| {
|
||||||
View::button(ui, "OK".to_owned(), Colors::white_or_black(false), || {
|
let mut on_next = || {
|
||||||
match wallet.get_recovery(self.pass_edit.clone()) {
|
match wallet.get_recovery(self.pass_edit.clone()) {
|
||||||
Ok(phrase) => {
|
Ok(phrase) => {
|
||||||
self.wrong_pass = false;
|
self.wrong_pass = false;
|
||||||
|
@ -243,6 +243,12 @@ impl RecoverySettings {
|
||||||
self.wrong_pass = true;
|
self.wrong_pass = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
View::on_enter_key(ui, || {
|
||||||
|
(on_next)();
|
||||||
|
});
|
||||||
|
View::button(ui, "OK".to_owned(), Colors::white_or_black(false), || {
|
||||||
|
on_next();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,944 +0,0 @@
|
||||||
// Copyright 2023 The Grim Developers
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
use std::sync::Arc;
|
|
||||||
use std::thread;
|
|
||||||
use egui::{Align, Id, Layout, Margin, RichText, Rounding, ScrollArea};
|
|
||||||
use egui::os::OperatingSystem;
|
|
||||||
use egui::scroll_area::ScrollBarVisibility;
|
|
||||||
use parking_lot::RwLock;
|
|
||||||
use tor_rtcompat::BlockOn;
|
|
||||||
use tor_rtcompat::tokio::TokioNativeTlsRuntime;
|
|
||||||
use grin_core::core::{amount_from_hr_string, amount_to_hr_string};
|
|
||||||
use grin_wallet_libwallet::SlatepackAddress;
|
|
||||||
|
|
||||||
use crate::gui::Colors;
|
|
||||||
use crate::gui::icons::{CHECK_CIRCLE, COPY, DOTS_THREE_CIRCLE, EXPORT, GEAR_SIX, GLOBE_SIMPLE, POWER, QR_CODE, SHIELD_CHECKERED, SHIELD_SLASH, WARNING_CIRCLE, X_CIRCLE};
|
|
||||||
use crate::gui::platform::PlatformCallbacks;
|
|
||||||
use crate::gui::views::{CameraContent, Modal, QrCodeContent, Content, View};
|
|
||||||
use crate::gui::views::types::{ModalPosition, TextEditOptions};
|
|
||||||
use crate::gui::views::wallets::wallet::types::{WalletTab, WalletTabType};
|
|
||||||
use crate::gui::views::wallets::wallet::WalletContent;
|
|
||||||
use crate::tor::{Tor, TorBridge, TorConfig};
|
|
||||||
use crate::wallet::types::WalletData;
|
|
||||||
use crate::wallet::Wallet;
|
|
||||||
|
|
||||||
/// Wallet transport tab content.
|
|
||||||
pub struct WalletTransport {
|
|
||||||
/// Flag to check if transaction is sending over Tor to show progress at [`Modal`].
|
|
||||||
tor_sending: Arc<RwLock<bool>>,
|
|
||||||
/// Flag to check if error occurred during sending of transaction over Tor at [`Modal`].
|
|
||||||
tor_send_error: Arc<RwLock<bool>>,
|
|
||||||
/// Flag to check if transaction sent successfully over Tor [`Modal`].
|
|
||||||
tor_success: Arc<RwLock<bool>>,
|
|
||||||
/// Entered amount value for [`Modal`].
|
|
||||||
amount_edit: String,
|
|
||||||
/// Entered address value for [`Modal`].
|
|
||||||
address_edit: String,
|
|
||||||
/// Flag to check if entered address is incorrect at [`Modal`].
|
|
||||||
address_error: bool,
|
|
||||||
/// Flag to check if QR code scanner is opened at address [`Modal`].
|
|
||||||
show_address_scan: bool,
|
|
||||||
/// Address QR code scanner [`Modal`] content.
|
|
||||||
address_scan_content: CameraContent,
|
|
||||||
/// Flag to check if [`Modal`] was just opened to focus on first field.
|
|
||||||
modal_just_opened: bool,
|
|
||||||
|
|
||||||
/// QR code address image [`Modal`] content.
|
|
||||||
qr_address_content: QrCodeContent,
|
|
||||||
|
|
||||||
/// Flag to check if Tor settings were changed.
|
|
||||||
tor_settings_changed: bool,
|
|
||||||
/// Tor bridge binary path edit text.
|
|
||||||
bridge_bin_path_edit: String,
|
|
||||||
/// Tor bridge connection line edit text.
|
|
||||||
bridge_conn_line_edit: String,
|
|
||||||
/// Flag to check if QR code scanner is opened at bridge [`Modal`].
|
|
||||||
show_bridge_scan: bool,
|
|
||||||
/// Address QR code scanner [`Modal`] content.
|
|
||||||
bridge_qr_scan_content: CameraContent,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WalletTab for WalletTransport {
|
|
||||||
fn get_type(&self) -> WalletTabType {
|
|
||||||
WalletTabType::Transport
|
|
||||||
}
|
|
||||||
|
|
||||||
fn ui(&mut self,
|
|
||||||
ui: &mut egui::Ui,
|
|
||||||
wallet: &mut Wallet,
|
|
||||||
cb: &dyn PlatformCallbacks) {
|
|
||||||
if WalletContent::sync_ui(ui, wallet) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show modal content for this ui container.
|
|
||||||
self.modal_content_ui(ui, wallet, cb);
|
|
||||||
|
|
||||||
// Show transport content panel.
|
|
||||||
egui::CentralPanel::default()
|
|
||||||
.frame(egui::Frame {
|
|
||||||
stroke: View::item_stroke(),
|
|
||||||
fill: Colors::white_or_black(false),
|
|
||||||
inner_margin: Margin {
|
|
||||||
left: View::far_left_inset_margin(ui) + 4.0,
|
|
||||||
right: View::get_right_inset() + 4.0,
|
|
||||||
top: 3.0,
|
|
||||||
bottom: 4.0,
|
|
||||||
},
|
|
||||||
..Default::default()
|
|
||||||
})
|
|
||||||
.show_inside(ui, |ui| {
|
|
||||||
ScrollArea::vertical()
|
|
||||||
.id_source(Id::from("wallet_transport").with(wallet.get_config().id))
|
|
||||||
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
|
|
||||||
.auto_shrink([false; 2])
|
|
||||||
.show(ui, |ui| {
|
|
||||||
ui.vertical_centered(|ui| {
|
|
||||||
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
|
|
||||||
self.ui(ui, wallet, cb);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Identifier for [`Modal`] to send amount over Tor.
|
|
||||||
const SEND_TOR_MODAL: &'static str = "send_tor_modal";
|
|
||||||
|
|
||||||
/// Identifier for [`Modal`] to setup Tor service.
|
|
||||||
const TOR_SETTINGS_MODAL: &'static str = "tor_settings_modal";
|
|
||||||
|
|
||||||
/// Identifier for [`Modal`] to show QR code address image.
|
|
||||||
const QR_ADDRESS_MODAL: &'static str = "qr_address_modal";
|
|
||||||
|
|
||||||
impl WalletTransport {
|
|
||||||
/// Create new content instance from provided Slatepack address text.
|
|
||||||
pub fn new(addr: String) -> Self {
|
|
||||||
// Setup Tor bridge binary path edit text.
|
|
||||||
let bridge = TorConfig::get_bridge();
|
|
||||||
let (bin_path, conn_line) = if let Some(b) = bridge {
|
|
||||||
(b.binary_path(), b.connection_line())
|
|
||||||
} else {
|
|
||||||
("".to_string(), "".to_string())
|
|
||||||
};
|
|
||||||
Self {
|
|
||||||
tor_sending: Arc::new(RwLock::new(false)),
|
|
||||||
tor_send_error: Arc::new(RwLock::new(false)),
|
|
||||||
tor_success: Arc::new(RwLock::new(false)),
|
|
||||||
amount_edit: "".to_string(),
|
|
||||||
address_edit: "".to_string(),
|
|
||||||
address_error: false,
|
|
||||||
show_address_scan: false,
|
|
||||||
address_scan_content: CameraContent::default(),
|
|
||||||
modal_just_opened: false,
|
|
||||||
qr_address_content: QrCodeContent::new(addr, false),
|
|
||||||
tor_settings_changed: false,
|
|
||||||
bridge_bin_path_edit: bin_path,
|
|
||||||
bridge_conn_line_edit: conn_line,
|
|
||||||
show_bridge_scan: false,
|
|
||||||
bridge_qr_scan_content: CameraContent::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Draw wallet transport content.
|
|
||||||
pub fn ui(&mut self, ui: &mut egui::Ui, wallet: &mut Wallet, cb: &dyn PlatformCallbacks) {
|
|
||||||
ui.add_space(3.0);
|
|
||||||
ui.label(RichText::new(t!("transport.desc"))
|
|
||||||
.size(16.0)
|
|
||||||
.color(Colors::inactive_text()));
|
|
||||||
ui.add_space(7.0);
|
|
||||||
|
|
||||||
// Draw Tor content.
|
|
||||||
self.tor_ui(ui, wallet, cb);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Draw [`Modal`] content for this ui container.
|
|
||||||
fn modal_content_ui(&mut self,
|
|
||||||
ui: &mut egui::Ui,
|
|
||||||
wallet: &mut Wallet,
|
|
||||||
cb: &dyn PlatformCallbacks) {
|
|
||||||
match Modal::opened() {
|
|
||||||
None => {}
|
|
||||||
Some(id) => {
|
|
||||||
match id {
|
|
||||||
SEND_TOR_MODAL => {
|
|
||||||
Modal::ui(ui.ctx(), |ui, modal| {
|
|
||||||
self.send_tor_modal_ui(ui, wallet, modal, cb);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
TOR_SETTINGS_MODAL => {
|
|
||||||
Modal::ui(ui.ctx(), |ui, modal| {
|
|
||||||
self.tor_settings_modal_ui(ui, wallet, modal, cb);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
QR_ADDRESS_MODAL => {
|
|
||||||
Modal::ui(ui.ctx(), |ui, modal| {
|
|
||||||
self.qr_address_modal_ui(ui, modal, cb);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Draw Tor transport content.
|
|
||||||
fn tor_ui(&mut self, ui: &mut egui::Ui, wallet: &mut Wallet, cb: &dyn PlatformCallbacks) {
|
|
||||||
let data = wallet.get_data().unwrap();
|
|
||||||
|
|
||||||
// Draw header content.
|
|
||||||
self.tor_header_ui(ui, wallet);
|
|
||||||
|
|
||||||
// Draw receive info content.
|
|
||||||
if wallet.slatepack_address().is_some() {
|
|
||||||
self.tor_receive_ui(ui, wallet, &data, cb);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw send content.
|
|
||||||
if data.info.amount_currently_spendable > 0 && wallet.foreign_api_port().is_some() {
|
|
||||||
self.tor_send_ui(ui, cb);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Draw Tor transport header content.
|
|
||||||
fn tor_header_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet) {
|
|
||||||
// Setup layout size.
|
|
||||||
let mut rect = ui.available_rect_before_wrap();
|
|
||||||
rect.set_height(78.0);
|
|
||||||
|
|
||||||
// Draw round background.
|
|
||||||
let bg_rect = rect.clone();
|
|
||||||
let item_rounding = View::item_rounding(0, 2, false);
|
|
||||||
ui.painter().rect(bg_rect, item_rounding, Colors::button(), View::item_stroke());
|
|
||||||
|
|
||||||
ui.vertical(|ui| {
|
|
||||||
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
|
|
||||||
// Draw button to setup Tor transport.
|
|
||||||
let button_rounding = View::item_rounding(0, 2, true);
|
|
||||||
View::item_button(ui, button_rounding, GEAR_SIX, None, || {
|
|
||||||
self.show_tor_settings_modal();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Draw button to enable/disable Tor listener for current wallet.
|
|
||||||
let service_id = &wallet.identifier();
|
|
||||||
if !Tor::is_service_starting(service_id) && wallet.foreign_api_port().is_some() {
|
|
||||||
if !Tor::is_service_running(service_id) {
|
|
||||||
View::item_button(ui, Rounding::default(), POWER, Some(Colors::green()), || {
|
|
||||||
if let Ok(key) = wallet.secret_key() {
|
|
||||||
let api_port = wallet.foreign_api_port().unwrap();
|
|
||||||
Tor::start_service(api_port, key, service_id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
View::item_button(ui, Rounding::default(), POWER, Some(Colors::red()), || {
|
|
||||||
Tor::stop_service(service_id);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let layout_size = ui.available_size();
|
|
||||||
ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| {
|
|
||||||
ui.add_space(6.0);
|
|
||||||
ui.vertical(|ui| {
|
|
||||||
ui.add_space(3.0);
|
|
||||||
ui.with_layout(Layout::left_to_right(Align::Min), |ui| {
|
|
||||||
ui.add_space(1.0);
|
|
||||||
ui.label(RichText::new(t!("transport.tor_network"))
|
|
||||||
.size(18.0)
|
|
||||||
.color(Colors::title(false)));
|
|
||||||
});
|
|
||||||
|
|
||||||
// Setup Tor status text.
|
|
||||||
let is_running = Tor::is_service_running(service_id);
|
|
||||||
let is_starting = Tor::is_service_starting(service_id);
|
|
||||||
let has_error = Tor::is_service_failed(service_id);
|
|
||||||
let (icon, text) = if wallet.foreign_api_port().is_none() {
|
|
||||||
(DOTS_THREE_CIRCLE, t!("wallets.loading"))
|
|
||||||
} else if is_starting {
|
|
||||||
(DOTS_THREE_CIRCLE, t!("transport.connecting"))
|
|
||||||
} else if has_error {
|
|
||||||
(WARNING_CIRCLE, t!("transport.conn_error"))
|
|
||||||
} else if is_running {
|
|
||||||
(CHECK_CIRCLE, t!("transport.connected"))
|
|
||||||
} else {
|
|
||||||
(X_CIRCLE, t!("transport.disconnected"))
|
|
||||||
};
|
|
||||||
let status_text = format!("{} {}", icon, text);
|
|
||||||
ui.label(RichText::new(status_text).size(15.0).color(Colors::text(false)));
|
|
||||||
ui.add_space(1.0);
|
|
||||||
|
|
||||||
// Setup bridges status text.
|
|
||||||
let bridge = TorConfig::get_bridge();
|
|
||||||
let bridges_text = match &bridge {
|
|
||||||
None => {
|
|
||||||
format!("{} {}", SHIELD_SLASH, t!("transport.bridges_disabled"))
|
|
||||||
}
|
|
||||||
Some(b) => {
|
|
||||||
let name = b.protocol_name().to_uppercase();
|
|
||||||
format!("{} {}",
|
|
||||||
SHIELD_CHECKERED,
|
|
||||||
t!("transport.bridge_name", "b" = name))
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
ui.label(RichText::new(bridges_text).size(15.0).color(Colors::gray()));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Show Tor transport settings [`Modal`].
|
|
||||||
fn show_tor_settings_modal(&mut self) {
|
|
||||||
self.tor_settings_changed = false;
|
|
||||||
// Show Tor settings modal.
|
|
||||||
Modal::new(TOR_SETTINGS_MODAL)
|
|
||||||
.position(ModalPosition::CenterTop)
|
|
||||||
.title(t!("transport.tor_settings"))
|
|
||||||
.closeable(false)
|
|
||||||
.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Draw Tor transport settings [`Modal`] content.
|
|
||||||
fn tor_settings_modal_ui(&mut self,
|
|
||||||
ui: &mut egui::Ui,
|
|
||||||
wallet: &Wallet,
|
|
||||||
modal: &Modal,
|
|
||||||
cb: &dyn PlatformCallbacks) {
|
|
||||||
ui.add_space(6.0);
|
|
||||||
|
|
||||||
// Draw QR code scanner content if requested.
|
|
||||||
if self.show_bridge_scan {
|
|
||||||
let mut on_stop = |content: &mut CameraContent| {
|
|
||||||
cb.stop_camera();
|
|
||||||
content.clear_state();
|
|
||||||
modal.enable_closing();
|
|
||||||
self.show_bridge_scan = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(result) = self.bridge_qr_scan_content.qr_scan_result() {
|
|
||||||
self.bridge_conn_line_edit = result.text();
|
|
||||||
on_stop(&mut self.bridge_qr_scan_content);
|
|
||||||
cb.show_keyboard();
|
|
||||||
} else {
|
|
||||||
self.bridge_qr_scan_content.ui(ui, cb);
|
|
||||||
ui.add_space(12.0);
|
|
||||||
|
|
||||||
// Setup spacing between buttons.
|
|
||||||
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
|
|
||||||
|
|
||||||
// Show buttons to close modal or come back to sending input.
|
|
||||||
ui.columns(2, |cols| {
|
|
||||||
cols[0].vertical_centered_justified(|ui| {
|
|
||||||
View::button(ui, t!("close"), Colors::white_or_black(false), || {
|
|
||||||
on_stop(&mut self.bridge_qr_scan_content);
|
|
||||||
modal.close();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
cols[1].vertical_centered_justified(|ui| {
|
|
||||||
View::button(ui, t!("back"), Colors::white_or_black(false), || {
|
|
||||||
on_stop(&mut self.bridge_qr_scan_content);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
ui.add_space(6.0);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Do not show bridges setup on Android.
|
|
||||||
let os = OperatingSystem::from_target_os();
|
|
||||||
let show_bridges = os != OperatingSystem::Android;
|
|
||||||
if show_bridges {
|
|
||||||
let bridge = TorConfig::get_bridge();
|
|
||||||
ui.vertical_centered(|ui| {
|
|
||||||
ui.label(RichText::new(t!("transport.bridges_desc"))
|
|
||||||
.size(17.0)
|
|
||||||
.color(Colors::inactive_text()));
|
|
||||||
|
|
||||||
// Draw checkbox to enable/disable bridges.
|
|
||||||
View::checkbox(ui, bridge.is_some(), t!("transport.bridges"), || {
|
|
||||||
// Save value.
|
|
||||||
let value = if bridge.is_some() {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
let default_bridge = TorConfig::get_obfs4();
|
|
||||||
self.bridge_bin_path_edit = default_bridge.binary_path();
|
|
||||||
self.bridge_conn_line_edit = default_bridge.connection_line();
|
|
||||||
Some(default_bridge)
|
|
||||||
};
|
|
||||||
TorConfig::save_bridge(value);
|
|
||||||
self.tor_settings_changed = true;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Draw bridges selection and path.
|
|
||||||
if bridge.is_some() {
|
|
||||||
let current_bridge = bridge.unwrap();
|
|
||||||
let mut bridge = current_bridge.clone();
|
|
||||||
|
|
||||||
ui.add_space(6.0);
|
|
||||||
ui.columns(2, |columns| {
|
|
||||||
columns[0].vertical_centered(|ui| {
|
|
||||||
// Draw Obfs4 bridge selector.
|
|
||||||
let obfs4 = TorConfig::get_obfs4();
|
|
||||||
let name = obfs4.protocol_name().to_uppercase();
|
|
||||||
View::radio_value(ui, &mut bridge, obfs4, name);
|
|
||||||
});
|
|
||||||
columns[1].vertical_centered(|ui| {
|
|
||||||
// Draw Snowflake bridge selector.
|
|
||||||
let snowflake = TorConfig::get_snowflake();
|
|
||||||
let name = snowflake.protocol_name().to_uppercase();
|
|
||||||
View::radio_value(ui, &mut bridge, snowflake, name);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
ui.add_space(12.0);
|
|
||||||
|
|
||||||
// Check if bridge type was changed to save.
|
|
||||||
if current_bridge != bridge {
|
|
||||||
self.tor_settings_changed = true;
|
|
||||||
TorConfig::save_bridge(Some(bridge.clone()));
|
|
||||||
self.bridge_bin_path_edit = bridge.binary_path();
|
|
||||||
self.bridge_conn_line_edit = bridge.connection_line();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw binary path text edit.
|
|
||||||
let bin_edit_id = Id::from(modal.id)
|
|
||||||
.with(wallet.get_config().id)
|
|
||||||
.with("_bin_edit");
|
|
||||||
let mut bin_edit_opts = TextEditOptions::new(bin_edit_id)
|
|
||||||
.paste()
|
|
||||||
.no_focus();
|
|
||||||
let bin_edit_before = self.bridge_bin_path_edit.clone();
|
|
||||||
ui.vertical_centered(|ui| {
|
|
||||||
ui.label(RichText::new(t!("transport.bin_file"))
|
|
||||||
.size(17.0)
|
|
||||||
.color(Colors::inactive_text()));
|
|
||||||
ui.add_space(6.0);
|
|
||||||
View::text_edit(ui, cb, &mut self.bridge_bin_path_edit, &mut bin_edit_opts);
|
|
||||||
ui.add_space(6.0);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Draw connection line text edit.
|
|
||||||
let conn_edit_before = self.bridge_conn_line_edit.clone();
|
|
||||||
let conn_edit_id = Id::from(modal.id)
|
|
||||||
.with(wallet.get_config().id)
|
|
||||||
.with("_conn_edit");
|
|
||||||
let mut conn_edit_opts = TextEditOptions::new(conn_edit_id)
|
|
||||||
.paste()
|
|
||||||
.no_focus()
|
|
||||||
.scan_qr();
|
|
||||||
ui.vertical_centered(|ui| {
|
|
||||||
ui.label(RichText::new(t!("transport.conn_line"))
|
|
||||||
.size(17.0)
|
|
||||||
.color(Colors::inactive_text()));
|
|
||||||
ui.add_space(6.0);
|
|
||||||
View::text_edit(ui, cb, &mut self.bridge_conn_line_edit, &mut conn_edit_opts);
|
|
||||||
// Check if scan button was pressed.
|
|
||||||
if conn_edit_opts.scan_pressed {
|
|
||||||
cb.hide_keyboard();
|
|
||||||
modal.disable_closing();
|
|
||||||
conn_edit_opts.scan_pressed = false;
|
|
||||||
self.show_bridge_scan = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Check if bin path or connection line text was changed to save bridge.
|
|
||||||
if conn_edit_before != self.bridge_conn_line_edit ||
|
|
||||||
bin_edit_before != self.bridge_bin_path_edit {
|
|
||||||
let bin_path = self.bridge_bin_path_edit.trim().to_string();
|
|
||||||
let conn_line = self.bridge_conn_line_edit.trim().to_string();
|
|
||||||
let b = match bridge {
|
|
||||||
TorBridge::Snowflake(_, _) => {
|
|
||||||
TorBridge::Snowflake(bin_path, conn_line)
|
|
||||||
},
|
|
||||||
TorBridge::Obfs4(_, _) => {
|
|
||||||
TorBridge::Obfs4(bin_path, conn_line)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
TorConfig::save_bridge(Some(b));
|
|
||||||
self.tor_settings_changed = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
ui.add_space(2.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
ui.add_space(6.0);
|
|
||||||
View::horizontal_line(ui, Colors::item_stroke());
|
|
||||||
ui.add_space(6.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
ui.vertical_centered(|ui| {
|
|
||||||
ui.label(RichText::new(t!("transport.tor_autorun_desc"))
|
|
||||||
.size(17.0)
|
|
||||||
.color(Colors::inactive_text()));
|
|
||||||
|
|
||||||
// Show Tor service autorun checkbox.
|
|
||||||
let autorun = wallet.auto_start_tor_listener();
|
|
||||||
View::checkbox(ui, autorun, t!("network.autorun"), || {
|
|
||||||
wallet.update_auto_start_tor_listener(!autorun);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
ui.add_space(6.0);
|
|
||||||
ui.vertical_centered_justified(|ui| {
|
|
||||||
View::button(ui, t!("close"), Colors::white_or_black(false), || {
|
|
||||||
if self.tor_settings_changed {
|
|
||||||
self.tor_settings_changed = false;
|
|
||||||
// Restart running service or rebuild client.
|
|
||||||
let service_id = &wallet.identifier();
|
|
||||||
if Tor::is_service_running(service_id) {
|
|
||||||
if let Ok(key) = wallet.secret_key() {
|
|
||||||
let api_port = wallet.foreign_api_port().unwrap();
|
|
||||||
Tor::restart_service(api_port, key, service_id);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Tor::rebuild_client();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
modal.close();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
ui.add_space(6.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Draw Tor receive content.
|
|
||||||
fn tor_receive_ui(&mut self,
|
|
||||||
ui: &mut egui::Ui,
|
|
||||||
wallet: &Wallet,
|
|
||||||
data: &WalletData,
|
|
||||||
cb: &dyn PlatformCallbacks) {
|
|
||||||
let slatepack_addr = wallet.slatepack_address().unwrap();
|
|
||||||
let service_id = &wallet.identifier();
|
|
||||||
let can_send = data.info.amount_currently_spendable > 0;
|
|
||||||
|
|
||||||
// Setup layout size.
|
|
||||||
let mut rect = ui.available_rect_before_wrap();
|
|
||||||
rect.set_height(52.0);
|
|
||||||
|
|
||||||
// Draw round background.
|
|
||||||
let bg_rect = rect.clone();
|
|
||||||
let item_rounding = if can_send {
|
|
||||||
View::item_rounding(1, 3, false)
|
|
||||||
} else {
|
|
||||||
View::item_rounding(1, 2, false)
|
|
||||||
};
|
|
||||||
ui.painter().rect(bg_rect, item_rounding, Colors::button(), View::item_stroke());
|
|
||||||
|
|
||||||
ui.vertical(|ui| {
|
|
||||||
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
|
|
||||||
// Draw button to setup Tor transport.
|
|
||||||
let button_rounding = if can_send {
|
|
||||||
View::item_rounding(1, 3, true)
|
|
||||||
} else {
|
|
||||||
View::item_rounding(1, 2, true)
|
|
||||||
};
|
|
||||||
View::item_button(ui, button_rounding, QR_CODE, None, || {
|
|
||||||
// Show QR code image address modal.
|
|
||||||
self.qr_address_content.clear_state();
|
|
||||||
Modal::new(QR_ADDRESS_MODAL)
|
|
||||||
.position(ModalPosition::CenterTop)
|
|
||||||
.title(t!("network_mining.address"))
|
|
||||||
.show();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Show button to enable/disable Tor listener for current wallet.
|
|
||||||
View::item_button(ui, Rounding::default(), COPY, None, || {
|
|
||||||
cb.copy_string_to_buffer(slatepack_addr.clone());
|
|
||||||
});
|
|
||||||
|
|
||||||
let layout_size = ui.available_size();
|
|
||||||
ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| {
|
|
||||||
ui.add_space(6.0);
|
|
||||||
ui.vertical(|ui| {
|
|
||||||
ui.add_space(3.0);
|
|
||||||
|
|
||||||
// Show wallet Slatepack address.
|
|
||||||
let address_color = if Tor::is_service_starting(service_id) ||
|
|
||||||
wallet.foreign_api_port().is_none() {
|
|
||||||
Colors::inactive_text()
|
|
||||||
} else if Tor::is_service_running(service_id) {
|
|
||||||
Colors::green()
|
|
||||||
} else {
|
|
||||||
Colors::red()
|
|
||||||
};
|
|
||||||
View::ellipsize_text(ui, slatepack_addr, 15.0, address_color);
|
|
||||||
|
|
||||||
let address_label = format!("{} {}",
|
|
||||||
GLOBE_SIMPLE,
|
|
||||||
t!("network_mining.address"));
|
|
||||||
ui.label(RichText::new(address_label).size(15.0).color(Colors::gray()));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Draw QR code image address [`Modal`] content.
|
|
||||||
fn qr_address_modal_ui(&mut self, ui: &mut egui::Ui, m: &Modal, cb: &dyn PlatformCallbacks) {
|
|
||||||
ui.add_space(6.0);
|
|
||||||
|
|
||||||
// Draw QR code content.
|
|
||||||
let text = self.qr_address_content.text.clone();
|
|
||||||
self.qr_address_content.ui(ui, text.clone(), cb);
|
|
||||||
|
|
||||||
ui.vertical_centered_justified(|ui| {
|
|
||||||
View::button(ui, t!("close"), Colors::white_or_black(false), || {
|
|
||||||
self.qr_address_content.clear_state();
|
|
||||||
m.close();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
ui.add_space(6.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Draw Tor send content.
|
|
||||||
fn tor_send_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
|
||||||
// Setup layout size.
|
|
||||||
let mut rect = ui.available_rect_before_wrap();
|
|
||||||
rect.set_height(55.0);
|
|
||||||
|
|
||||||
// Draw round background.
|
|
||||||
let bg_rect = rect.clone();
|
|
||||||
let item_rounding = View::item_rounding(1, 2, false);
|
|
||||||
ui.painter().rect(bg_rect, item_rounding, Colors::fill(), View::item_stroke());
|
|
||||||
|
|
||||||
ui.vertical(|ui| {
|
|
||||||
ui.allocate_ui_with_layout(rect.size(), Layout::top_down(Align::Center), |ui| {
|
|
||||||
ui.add_space(7.0);
|
|
||||||
// Draw button to open sending modal.
|
|
||||||
let send_text = format!("{} {}", EXPORT, t!("wallets.send"));
|
|
||||||
View::button(ui, send_text, Colors::white_or_black(false), || {
|
|
||||||
self.show_send_tor_modal(cb, None);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Show [`Modal`] to send over Tor.
|
|
||||||
pub fn show_send_tor_modal(&mut self, cb: &dyn PlatformCallbacks, address: Option<String>) {
|
|
||||||
{
|
|
||||||
let mut w_send_err = self.tor_send_error.write();
|
|
||||||
*w_send_err = false;
|
|
||||||
let mut w_sending = self.tor_sending.write();
|
|
||||||
*w_sending = false;
|
|
||||||
let mut w_success = self.tor_success.write();
|
|
||||||
*w_success = false;
|
|
||||||
}
|
|
||||||
self.modal_just_opened = true;
|
|
||||||
self.amount_edit = "".to_string();
|
|
||||||
self.address_edit = address.unwrap_or("".to_string());
|
|
||||||
self.address_error = false;
|
|
||||||
// Show modal.
|
|
||||||
Modal::new(SEND_TOR_MODAL)
|
|
||||||
.position(ModalPosition::CenterTop)
|
|
||||||
.title(t!("wallets.send"))
|
|
||||||
.show();
|
|
||||||
cb.show_keyboard();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if error occurred during sending over Tor at [`Modal`].
|
|
||||||
fn has_tor_send_error(&self) -> bool {
|
|
||||||
let r_send_err = self.tor_send_error.read();
|
|
||||||
r_send_err.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if transaction is sending over Tor to show progress at [`Modal`].
|
|
||||||
fn tor_sending(&self) -> bool {
|
|
||||||
let r_sending = self.tor_sending.read();
|
|
||||||
r_sending.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if transaction sent over Tor with success at [`Modal`].
|
|
||||||
fn tor_success(&self) -> bool {
|
|
||||||
let r_success = self.tor_success.read();
|
|
||||||
r_success.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Draw amount input [`Modal`] content to send over Tor.
|
|
||||||
fn send_tor_modal_ui(&mut self,
|
|
||||||
ui: &mut egui::Ui,
|
|
||||||
wallet: &mut Wallet,
|
|
||||||
modal: &Modal,
|
|
||||||
cb: &dyn PlatformCallbacks) {
|
|
||||||
ui.add_space(6.0);
|
|
||||||
|
|
||||||
let has_send_err = self.has_tor_send_error();
|
|
||||||
let sending = self.tor_sending();
|
|
||||||
if !has_send_err && !sending {
|
|
||||||
// Draw QR code scanner content if requested.
|
|
||||||
if self.show_address_scan {
|
|
||||||
let mut on_stop = |content: &mut CameraContent| {
|
|
||||||
cb.stop_camera();
|
|
||||||
content.clear_state();
|
|
||||||
modal.enable_closing();
|
|
||||||
self.show_address_scan = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(result) = self.address_scan_content.qr_scan_result() {
|
|
||||||
self.address_edit = result.text();
|
|
||||||
self.modal_just_opened = true;
|
|
||||||
on_stop(&mut self.address_scan_content);
|
|
||||||
cb.show_keyboard();
|
|
||||||
} else {
|
|
||||||
self.address_scan_content.ui(ui, cb);
|
|
||||||
ui.add_space(6.0);
|
|
||||||
|
|
||||||
// Setup spacing between buttons.
|
|
||||||
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
|
|
||||||
|
|
||||||
// Show buttons to close modal or come back to sending input.
|
|
||||||
ui.columns(2, |cols| {
|
|
||||||
cols[0].vertical_centered_justified(|ui| {
|
|
||||||
View::button(ui, t!("close"), Colors::white_or_black(false), || {
|
|
||||||
on_stop(&mut self.address_scan_content);
|
|
||||||
modal.close();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
cols[1].vertical_centered_justified(|ui| {
|
|
||||||
View::button(ui, t!("back"), Colors::white_or_black(false), || {
|
|
||||||
self.modal_just_opened = true;
|
|
||||||
on_stop(&mut self.address_scan_content);
|
|
||||||
cb.show_keyboard();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
ui.add_space(6.0);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ui.vertical_centered(|ui| {
|
|
||||||
let data = wallet.get_data().unwrap();
|
|
||||||
let amount = amount_to_hr_string(data.info.amount_currently_spendable, true);
|
|
||||||
let enter_text = t!("wallets.enter_amount_send","amount" => amount);
|
|
||||||
ui.label(RichText::new(enter_text)
|
|
||||||
.size(17.0)
|
|
||||||
.color(Colors::gray()));
|
|
||||||
});
|
|
||||||
ui.add_space(8.0);
|
|
||||||
|
|
||||||
// Draw amount text edit.
|
|
||||||
let amount_edit_id = Id::from(modal.id).with("amount").with(wallet.get_config().id);
|
|
||||||
let mut amount_edit_opts = TextEditOptions::new(amount_edit_id).h_center().no_focus();
|
|
||||||
let amount_edit_before = self.amount_edit.clone();
|
|
||||||
if self.modal_just_opened {
|
|
||||||
self.modal_just_opened = false;
|
|
||||||
amount_edit_opts.focus = true;
|
|
||||||
}
|
|
||||||
View::text_edit(ui, cb, &mut self.amount_edit, &mut amount_edit_opts);
|
|
||||||
ui.add_space(8.0);
|
|
||||||
|
|
||||||
// Check value if input was changed.
|
|
||||||
if amount_edit_before != self.amount_edit {
|
|
||||||
if !self.amount_edit.is_empty() {
|
|
||||||
// Trim text, replace "," by "." and parse amount.
|
|
||||||
self.amount_edit = self.amount_edit.trim().replace(",", ".");
|
|
||||||
match amount_from_hr_string(self.amount_edit.as_str()) {
|
|
||||||
Ok(a) => {
|
|
||||||
if !self.amount_edit.contains(".") {
|
|
||||||
// To avoid input of several "0".
|
|
||||||
if a == 0 {
|
|
||||||
self.amount_edit = "0".to_string();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Check input after ".".
|
|
||||||
let parts = self.amount_edit.split(".").collect::<Vec<&str>>();
|
|
||||||
if parts.len() == 2 && parts[1].len() > 9 {
|
|
||||||
self.amount_edit = amount_edit_before;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Do not input amount more than balance in sending.
|
|
||||||
let b = wallet.get_data().unwrap().info.amount_currently_spendable;
|
|
||||||
if b < a {
|
|
||||||
self.amount_edit = amount_edit_before;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(_) => {
|
|
||||||
self.amount_edit = amount_edit_before;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show address error or input description.
|
|
||||||
ui.vertical_centered(|ui| {
|
|
||||||
if self.address_error {
|
|
||||||
ui.label(RichText::new(t!("transport.incorrect_addr_err"))
|
|
||||||
.size(17.0)
|
|
||||||
.color(Colors::red()));
|
|
||||||
} else {
|
|
||||||
ui.label(RichText::new(t!("transport.receiver_address"))
|
|
||||||
.size(17.0)
|
|
||||||
.color(Colors::gray()));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
ui.add_space(6.0);
|
|
||||||
|
|
||||||
// Draw address text edit.
|
|
||||||
let addr_edit_before = self.address_edit.clone();
|
|
||||||
let address_edit_id = Id::from(modal.id).with("address").with(wallet.get_config().id);
|
|
||||||
let mut address_edit_opts = TextEditOptions::new(address_edit_id)
|
|
||||||
.paste()
|
|
||||||
.no_focus()
|
|
||||||
.scan_qr();
|
|
||||||
View::text_edit(ui, cb, &mut self.address_edit, &mut address_edit_opts);
|
|
||||||
// Check if scan button was pressed.
|
|
||||||
if address_edit_opts.scan_pressed {
|
|
||||||
cb.hide_keyboard();
|
|
||||||
modal.disable_closing();
|
|
||||||
address_edit_opts.scan_pressed = false;
|
|
||||||
self.show_address_scan = true;
|
|
||||||
}
|
|
||||||
ui.add_space(12.0);
|
|
||||||
|
|
||||||
// Check value if input was changed.
|
|
||||||
if addr_edit_before != self.address_edit {
|
|
||||||
self.address_error = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Setup spacing between buttons.
|
|
||||||
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
|
|
||||||
|
|
||||||
ui.columns(2, |columns| {
|
|
||||||
columns[0].vertical_centered_justified(|ui| {
|
|
||||||
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
|
|
||||||
self.amount_edit = "".to_string();
|
|
||||||
self.address_edit = "".to_string();
|
|
||||||
cb.hide_keyboard();
|
|
||||||
modal.close();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
columns[1].vertical_centered_justified(|ui| {
|
|
||||||
View::button(ui, t!("continue"), Colors::white_or_black(false), || {
|
|
||||||
if self.amount_edit.is_empty() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check entered address.
|
|
||||||
let addr_str = self.address_edit.as_str();
|
|
||||||
if let Ok(addr) = SlatepackAddress::try_from(addr_str) {
|
|
||||||
// Parse amount and send over Tor.
|
|
||||||
if let Ok(a) = amount_from_hr_string(self.amount_edit.as_str()) {
|
|
||||||
cb.hide_keyboard();
|
|
||||||
modal.disable_closing();
|
|
||||||
let mut w_sending = self.tor_sending.write();
|
|
||||||
*w_sending = true;
|
|
||||||
{
|
|
||||||
let send_error = self.tor_send_error.clone();
|
|
||||||
let send_success = self.tor_success.clone();
|
|
||||||
let mut wallet = wallet.clone();
|
|
||||||
thread::spawn(move || {
|
|
||||||
let runtime = TokioNativeTlsRuntime::create().unwrap();
|
|
||||||
runtime
|
|
||||||
.block_on(async {
|
|
||||||
if wallet.send_tor(a, &addr)
|
|
||||||
.await
|
|
||||||
.is_some() {
|
|
||||||
let mut w_send_success = send_success.write();
|
|
||||||
*w_send_success = true;
|
|
||||||
} else {
|
|
||||||
let mut w_send_error = send_error.write();
|
|
||||||
*w_send_error = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
self.address_error = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
ui.add_space(6.0);
|
|
||||||
} else if has_send_err {
|
|
||||||
ui.add_space(6.0);
|
|
||||||
ui.vertical_centered(|ui| {
|
|
||||||
ui.label(RichText::new(t!("transport.tor_send_error"))
|
|
||||||
.size(17.0)
|
|
||||||
.color(Colors::red()));
|
|
||||||
});
|
|
||||||
ui.add_space(12.0);
|
|
||||||
|
|
||||||
// Setup spacing between buttons.
|
|
||||||
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
|
|
||||||
|
|
||||||
ui.columns(2, |columns| {
|
|
||||||
columns[0].vertical_centered_justified(|ui| {
|
|
||||||
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
|
|
||||||
self.amount_edit = "".to_string();
|
|
||||||
self.address_edit = "".to_string();
|
|
||||||
cb.hide_keyboard();
|
|
||||||
modal.close();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
columns[1].vertical_centered_justified(|ui| {
|
|
||||||
View::button(ui, t!("repeat"), Colors::white_or_black(false), || {
|
|
||||||
// Parse amount and send over Tor.
|
|
||||||
if let Ok(a) = amount_from_hr_string(self.amount_edit.as_str()) {
|
|
||||||
let mut w_send_error = self.tor_send_error.write();
|
|
||||||
*w_send_error = false;
|
|
||||||
let mut w_sending = self.tor_sending.write();
|
|
||||||
*w_sending = true;
|
|
||||||
{
|
|
||||||
let addr_text = self.address_edit.clone();
|
|
||||||
let send_error = self.tor_send_error.clone();
|
|
||||||
let send_success = self.tor_success.clone();
|
|
||||||
let mut wallet = wallet.clone();
|
|
||||||
thread::spawn(move || {
|
|
||||||
let runtime = TokioNativeTlsRuntime::create().unwrap();
|
|
||||||
runtime
|
|
||||||
.block_on(async {
|
|
||||||
let addr_str = addr_text.as_str();
|
|
||||||
let addr = &SlatepackAddress::try_from(addr_str)
|
|
||||||
.unwrap();
|
|
||||||
if wallet.send_tor(a, &addr)
|
|
||||||
.await
|
|
||||||
.is_some() {
|
|
||||||
let mut w_send_success = send_success.write();
|
|
||||||
*w_send_success = true;
|
|
||||||
} else {
|
|
||||||
let mut w_send_error = send_error.write();
|
|
||||||
*w_send_error = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
ui.add_space(6.0);
|
|
||||||
} else {
|
|
||||||
ui.add_space(16.0);
|
|
||||||
ui.vertical_centered(|ui| {
|
|
||||||
View::small_loading_spinner(ui);
|
|
||||||
ui.add_space(12.0);
|
|
||||||
ui.label(RichText::new(t!("transport.tor_sending", "amount" => self.amount_edit))
|
|
||||||
.size(17.0)
|
|
||||||
.color(Colors::gray()));
|
|
||||||
});
|
|
||||||
ui.add_space(10.0);
|
|
||||||
|
|
||||||
// Close modal on success sending.
|
|
||||||
if self.tor_success() {
|
|
||||||
modal.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
397
src/gui/views/wallets/wallet/transport/content.rs
Normal file
397
src/gui/views/wallets/wallet/transport/content.rs
Normal file
|
@ -0,0 +1,397 @@
|
||||||
|
// Copyright 2023 The Grim Developers
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
use egui::{Align, Id, Layout, Margin, RichText, Rounding, ScrollArea};
|
||||||
|
use egui::scroll_area::ScrollBarVisibility;
|
||||||
|
|
||||||
|
use crate::gui::Colors;
|
||||||
|
use crate::gui::icons::{CHECK_CIRCLE, COPY, DOTS_THREE_CIRCLE, EXPORT, GEAR_SIX, GLOBE_SIMPLE, POWER, QR_CODE, SHIELD_CHECKERED, SHIELD_SLASH, WARNING_CIRCLE, X_CIRCLE};
|
||||||
|
use crate::gui::platform::PlatformCallbacks;
|
||||||
|
use crate::gui::views::{Modal, QrCodeContent, Content, View};
|
||||||
|
use crate::gui::views::types::ModalPosition;
|
||||||
|
use crate::gui::views::wallets::wallet::transport::send::TransportSendModal;
|
||||||
|
use crate::gui::views::wallets::wallet::transport::settings::TransportSettingsModal;
|
||||||
|
use crate::gui::views::wallets::wallet::types::{WalletTab, WalletTabType};
|
||||||
|
use crate::gui::views::wallets::wallet::WalletContent;
|
||||||
|
use crate::tor::{Tor, TorConfig};
|
||||||
|
use crate::wallet::types::WalletData;
|
||||||
|
use crate::wallet::Wallet;
|
||||||
|
|
||||||
|
/// Wallet transport tab content.
|
||||||
|
pub struct WalletTransport {
|
||||||
|
/// Sending [`Modal`] content.
|
||||||
|
send_modal_content: Option<TransportSendModal>,
|
||||||
|
|
||||||
|
/// QR code address image [`Modal`] content.
|
||||||
|
qr_address_content: Option<QrCodeContent>,
|
||||||
|
|
||||||
|
/// Tor settings [`Modal`] content.
|
||||||
|
settings_modal_content: Option<TransportSettingsModal>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WalletTab for WalletTransport {
|
||||||
|
fn get_type(&self) -> WalletTabType {
|
||||||
|
WalletTabType::Transport
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ui(&mut self,
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
wallet: &mut Wallet,
|
||||||
|
cb: &dyn PlatformCallbacks) {
|
||||||
|
if WalletContent::sync_ui(ui, wallet) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show modal content for this ui container.
|
||||||
|
self.modal_content_ui(ui, wallet, cb);
|
||||||
|
|
||||||
|
// Show transport content panel.
|
||||||
|
egui::CentralPanel::default()
|
||||||
|
.frame(egui::Frame {
|
||||||
|
stroke: View::item_stroke(),
|
||||||
|
fill: Colors::white_or_black(false),
|
||||||
|
inner_margin: Margin {
|
||||||
|
left: View::far_left_inset_margin(ui) + 4.0,
|
||||||
|
right: View::get_right_inset() + 4.0,
|
||||||
|
top: 3.0,
|
||||||
|
bottom: 4.0,
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.show_inside(ui, |ui| {
|
||||||
|
ScrollArea::vertical()
|
||||||
|
.id_source(Id::from("wallet_transport").with(wallet.get_config().id))
|
||||||
|
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
|
||||||
|
.auto_shrink([false; 2])
|
||||||
|
.show(ui, |ui| {
|
||||||
|
ui.vertical_centered(|ui| {
|
||||||
|
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
|
||||||
|
self.ui(ui, wallet, cb);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Identifier for [`Modal`] to send amount over Tor.
|
||||||
|
const SEND_TOR_MODAL: &'static str = "send_tor_modal";
|
||||||
|
|
||||||
|
/// Identifier for [`Modal`] to setup Tor service.
|
||||||
|
const TOR_SETTINGS_MODAL: &'static str = "tor_settings_modal";
|
||||||
|
|
||||||
|
/// Identifier for [`Modal`] to show QR code address image.
|
||||||
|
const QR_ADDRESS_MODAL: &'static str = "qr_address_modal";
|
||||||
|
|
||||||
|
impl Default for WalletTransport {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
send_modal_content: None,
|
||||||
|
qr_address_content: None,
|
||||||
|
settings_modal_content: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WalletTransport {
|
||||||
|
/// Draw wallet transport content.
|
||||||
|
pub fn ui(&mut self, ui: &mut egui::Ui, wallet: &mut Wallet, cb: &dyn PlatformCallbacks) {
|
||||||
|
ui.add_space(3.0);
|
||||||
|
ui.label(RichText::new(t!("transport.desc"))
|
||||||
|
.size(16.0)
|
||||||
|
.color(Colors::inactive_text()));
|
||||||
|
ui.add_space(7.0);
|
||||||
|
|
||||||
|
// Draw Tor transport content.
|
||||||
|
self.tor_ui(ui, wallet, cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw [`Modal`] content for this ui container.
|
||||||
|
fn modal_content_ui(&mut self,
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
wallet: &mut Wallet,
|
||||||
|
cb: &dyn PlatformCallbacks) {
|
||||||
|
match Modal::opened() {
|
||||||
|
None => {}
|
||||||
|
Some(id) => {
|
||||||
|
match id {
|
||||||
|
SEND_TOR_MODAL => {
|
||||||
|
if let Some(content) = self.send_modal_content.as_mut() {
|
||||||
|
Modal::ui(ui.ctx(), |ui, modal| {
|
||||||
|
content.ui(ui, wallet, modal, cb);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TOR_SETTINGS_MODAL => {
|
||||||
|
if let Some(content) = self.settings_modal_content.as_mut() {
|
||||||
|
Modal::ui(ui.ctx(), |ui, modal| {
|
||||||
|
content.ui(ui, wallet, modal, cb);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
QR_ADDRESS_MODAL => {
|
||||||
|
Modal::ui(ui.ctx(), |ui, modal| {
|
||||||
|
self.qr_address_modal_ui(ui, modal, cb);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw Tor transport content.
|
||||||
|
fn tor_ui(&mut self, ui: &mut egui::Ui, wallet: &mut Wallet, cb: &dyn PlatformCallbacks) {
|
||||||
|
let data = wallet.get_data().unwrap();
|
||||||
|
|
||||||
|
// Draw header content.
|
||||||
|
self.tor_header_ui(ui, wallet);
|
||||||
|
|
||||||
|
// Draw receive info content.
|
||||||
|
if wallet.slatepack_address().is_some() {
|
||||||
|
self.tor_receive_ui(ui, wallet, &data, cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw send content.
|
||||||
|
let service_id = &wallet.identifier();
|
||||||
|
if data.info.amount_currently_spendable > 0 && wallet.foreign_api_port().is_some() &&
|
||||||
|
!Tor::is_service_starting(service_id) {
|
||||||
|
self.tor_send_ui(ui, cb);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw Tor transport header content.
|
||||||
|
fn tor_header_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet) {
|
||||||
|
// Setup layout size.
|
||||||
|
let mut rect = ui.available_rect_before_wrap();
|
||||||
|
rect.set_height(78.0);
|
||||||
|
|
||||||
|
// Draw round background.
|
||||||
|
let bg_rect = rect.clone();
|
||||||
|
let item_rounding = View::item_rounding(0, 2, false);
|
||||||
|
ui.painter().rect(bg_rect, item_rounding, Colors::button(), View::item_stroke());
|
||||||
|
|
||||||
|
ui.vertical(|ui| {
|
||||||
|
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
|
||||||
|
// Draw button to setup Tor transport.
|
||||||
|
let button_rounding = View::item_rounding(0, 2, true);
|
||||||
|
View::item_button(ui, button_rounding, GEAR_SIX, None, || {
|
||||||
|
self.settings_modal_content = Some(TransportSettingsModal::default());
|
||||||
|
// Show Tor settings modal.
|
||||||
|
Modal::new(TOR_SETTINGS_MODAL)
|
||||||
|
.position(ModalPosition::CenterTop)
|
||||||
|
.title(t!("transport.tor_settings"))
|
||||||
|
.closeable(false)
|
||||||
|
.show();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Draw button to enable/disable Tor listener for current wallet.
|
||||||
|
let service_id = &wallet.identifier();
|
||||||
|
if !Tor::is_service_starting(service_id) && wallet.foreign_api_port().is_some() {
|
||||||
|
if !Tor::is_service_running(service_id) {
|
||||||
|
View::item_button(ui, Rounding::default(), POWER, Some(Colors::green()), || {
|
||||||
|
if let Ok(key) = wallet.secret_key() {
|
||||||
|
let api_port = wallet.foreign_api_port().unwrap();
|
||||||
|
Tor::start_service(api_port, key, service_id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
View::item_button(ui, Rounding::default(), POWER, Some(Colors::red()), || {
|
||||||
|
Tor::stop_service(service_id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let layout_size = ui.available_size();
|
||||||
|
ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| {
|
||||||
|
ui.add_space(6.0);
|
||||||
|
ui.vertical(|ui| {
|
||||||
|
ui.add_space(3.0);
|
||||||
|
ui.with_layout(Layout::left_to_right(Align::Min), |ui| {
|
||||||
|
ui.add_space(1.0);
|
||||||
|
ui.label(RichText::new(t!("transport.tor_network"))
|
||||||
|
.size(18.0)
|
||||||
|
.color(Colors::title(false)));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Setup Tor status text.
|
||||||
|
let is_running = Tor::is_service_running(service_id);
|
||||||
|
let is_starting = Tor::is_service_starting(service_id);
|
||||||
|
let has_error = Tor::is_service_failed(service_id);
|
||||||
|
let (icon, text) = if wallet.foreign_api_port().is_none() {
|
||||||
|
(DOTS_THREE_CIRCLE, t!("wallets.loading"))
|
||||||
|
} else if is_starting {
|
||||||
|
(DOTS_THREE_CIRCLE, t!("transport.connecting"))
|
||||||
|
} else if has_error {
|
||||||
|
(WARNING_CIRCLE, t!("transport.conn_error"))
|
||||||
|
} else if is_running {
|
||||||
|
(CHECK_CIRCLE, t!("transport.connected"))
|
||||||
|
} else {
|
||||||
|
(X_CIRCLE, t!("transport.disconnected"))
|
||||||
|
};
|
||||||
|
let status_text = format!("{} {}", icon, text);
|
||||||
|
ui.label(RichText::new(status_text).size(15.0).color(Colors::text(false)));
|
||||||
|
ui.add_space(1.0);
|
||||||
|
|
||||||
|
// Setup bridges status text.
|
||||||
|
let bridge = TorConfig::get_bridge();
|
||||||
|
let bridges_text = match &bridge {
|
||||||
|
None => {
|
||||||
|
format!("{} {}", SHIELD_SLASH, t!("transport.bridges_disabled"))
|
||||||
|
}
|
||||||
|
Some(b) => {
|
||||||
|
let name = b.protocol_name().to_uppercase();
|
||||||
|
format!("{} {}",
|
||||||
|
SHIELD_CHECKERED,
|
||||||
|
t!("transport.bridge_name", "b" = name))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ui.label(RichText::new(bridges_text).size(15.0).color(Colors::gray()));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw Tor receive content.
|
||||||
|
fn tor_receive_ui(&mut self,
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
wallet: &Wallet,
|
||||||
|
data: &WalletData,
|
||||||
|
cb: &dyn PlatformCallbacks) {
|
||||||
|
let addr = wallet.slatepack_address().unwrap();
|
||||||
|
let service_id = &wallet.identifier();
|
||||||
|
let can_send = data.info.amount_currently_spendable > 0;
|
||||||
|
|
||||||
|
// Setup layout size.
|
||||||
|
let mut rect = ui.available_rect_before_wrap();
|
||||||
|
rect.set_height(52.0);
|
||||||
|
|
||||||
|
// Draw round background.
|
||||||
|
let bg_rect = rect.clone();
|
||||||
|
let item_rounding = if can_send {
|
||||||
|
View::item_rounding(1, 3, false)
|
||||||
|
} else {
|
||||||
|
View::item_rounding(1, 2, false)
|
||||||
|
};
|
||||||
|
ui.painter().rect(bg_rect, item_rounding, Colors::button(), View::item_stroke());
|
||||||
|
|
||||||
|
ui.vertical(|ui| {
|
||||||
|
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
|
||||||
|
// Draw button to setup Tor transport.
|
||||||
|
let button_rounding = if can_send {
|
||||||
|
View::item_rounding(1, 3, true)
|
||||||
|
} else {
|
||||||
|
View::item_rounding(1, 2, true)
|
||||||
|
};
|
||||||
|
View::item_button(ui, button_rounding, QR_CODE, None, || {
|
||||||
|
// Show QR code image address modal.
|
||||||
|
self.qr_address_content = Some(QrCodeContent::new(addr.clone(), false));
|
||||||
|
Modal::new(QR_ADDRESS_MODAL)
|
||||||
|
.position(ModalPosition::CenterTop)
|
||||||
|
.title(t!("network_mining.address"))
|
||||||
|
.show();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Show button to enable/disable Tor listener for current wallet.
|
||||||
|
View::item_button(ui, Rounding::default(), COPY, None, || {
|
||||||
|
cb.copy_string_to_buffer(addr.clone());
|
||||||
|
});
|
||||||
|
|
||||||
|
let layout_size = ui.available_size();
|
||||||
|
ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| {
|
||||||
|
ui.add_space(6.0);
|
||||||
|
ui.vertical(|ui| {
|
||||||
|
ui.add_space(3.0);
|
||||||
|
|
||||||
|
// Show wallet Slatepack address.
|
||||||
|
let address_color = if Tor::is_service_starting(service_id) ||
|
||||||
|
wallet.foreign_api_port().is_none() {
|
||||||
|
Colors::inactive_text()
|
||||||
|
} else if Tor::is_service_running(service_id) {
|
||||||
|
Colors::green()
|
||||||
|
} else {
|
||||||
|
Colors::red()
|
||||||
|
};
|
||||||
|
View::ellipsize_text(ui, addr, 15.0, address_color);
|
||||||
|
|
||||||
|
let address_label = format!("{} {}",
|
||||||
|
GLOBE_SIMPLE,
|
||||||
|
t!("network_mining.address"));
|
||||||
|
ui.label(RichText::new(address_label).size(15.0).color(Colors::gray()));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw QR code image address [`Modal`] content.
|
||||||
|
fn qr_address_modal_ui(&mut self,
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
modal: &Modal,
|
||||||
|
cb: &dyn PlatformCallbacks) {
|
||||||
|
ui.add_space(6.0);
|
||||||
|
|
||||||
|
// Draw QR code content.
|
||||||
|
if let Some(content) = self.qr_address_content.as_mut() {
|
||||||
|
content.ui(ui, cb);
|
||||||
|
} else {
|
||||||
|
modal.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.vertical_centered_justified(|ui| {
|
||||||
|
View::button(ui, t!("close"), Colors::white_or_black(false), || {
|
||||||
|
self.qr_address_content = None;
|
||||||
|
modal.close();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
ui.add_space(6.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw Tor send content.
|
||||||
|
fn tor_send_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||||
|
// Setup layout size.
|
||||||
|
let mut rect = ui.available_rect_before_wrap();
|
||||||
|
rect.set_height(55.0);
|
||||||
|
|
||||||
|
// Draw round background.
|
||||||
|
let bg_rect = rect.clone();
|
||||||
|
let item_rounding = View::item_rounding(1, 2, false);
|
||||||
|
ui.painter().rect(bg_rect, item_rounding, Colors::fill(), View::item_stroke());
|
||||||
|
|
||||||
|
ui.vertical(|ui| {
|
||||||
|
ui.allocate_ui_with_layout(rect.size(), Layout::top_down(Align::Center), |ui| {
|
||||||
|
ui.add_space(7.0);
|
||||||
|
// Draw button to open sending modal.
|
||||||
|
let send_text = format!("{} {}", EXPORT, t!("wallets.send"));
|
||||||
|
View::button(ui, send_text, Colors::white_or_black(false), || {
|
||||||
|
self.show_send_tor_modal(cb, None);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Show [`Modal`] to send over Tor.
|
||||||
|
pub fn show_send_tor_modal(&mut self, cb: &dyn PlatformCallbacks, address: Option<String>) {
|
||||||
|
self.send_modal_content = Some(TransportSendModal::new(address));
|
||||||
|
// Show modal.
|
||||||
|
Modal::new(SEND_TOR_MODAL)
|
||||||
|
.position(ModalPosition::CenterTop)
|
||||||
|
.title(t!("wallets.send"))
|
||||||
|
.show();
|
||||||
|
cb.show_keyboard();
|
||||||
|
}
|
||||||
|
}
|
19
src/gui/views/wallets/wallet/transport/mod.rs
Normal file
19
src/gui/views/wallets/wallet/transport/mod.rs
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
// Copyright 2024 The Grim Developers
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
mod content;
|
||||||
|
pub use content::*;
|
||||||
|
|
||||||
|
mod send;
|
||||||
|
mod settings;
|
357
src/gui/views/wallets/wallet/transport/send.rs
Normal file
357
src/gui/views/wallets/wallet/transport/send.rs
Normal file
|
@ -0,0 +1,357 @@
|
||||||
|
// Copyright 2024 The Grim Developers
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::thread;
|
||||||
|
use egui::{Id, RichText};
|
||||||
|
use grin_core::core::{amount_from_hr_string, amount_to_hr_string};
|
||||||
|
use grin_wallet_libwallet::{Error, SlatepackAddress};
|
||||||
|
use parking_lot::RwLock;
|
||||||
|
use tor_rtcompat::BlockOn;
|
||||||
|
use tor_rtcompat::tokio::TokioNativeTlsRuntime;
|
||||||
|
use crate::gui::Colors;
|
||||||
|
use crate::gui::platform::PlatformCallbacks;
|
||||||
|
|
||||||
|
use crate::gui::views::{CameraContent, Modal, View};
|
||||||
|
use crate::gui::views::types::TextEditOptions;
|
||||||
|
use crate::gui::views::wallets::wallet::WalletTransactionModal;
|
||||||
|
use crate::wallet::types::WalletTransaction;
|
||||||
|
use crate::wallet::Wallet;
|
||||||
|
|
||||||
|
/// Transport sending [`Modal`] content.
|
||||||
|
pub struct TransportSendModal {
|
||||||
|
/// Flag to focus on first input field after opening.
|
||||||
|
first_draw: bool,
|
||||||
|
|
||||||
|
/// Flag to check if transaction is sending to show progress.
|
||||||
|
sending: bool,
|
||||||
|
/// Flag to check if there is an error to repeat.
|
||||||
|
error: bool,
|
||||||
|
/// Transaction result.
|
||||||
|
send_result: Arc<RwLock<Option<Result<WalletTransaction, Error>>>>,
|
||||||
|
|
||||||
|
/// Entered amount value.
|
||||||
|
amount_edit: String,
|
||||||
|
/// Entered address value.
|
||||||
|
address_edit: String,
|
||||||
|
/// Flag to check if entered address is incorrect.
|
||||||
|
address_error: bool,
|
||||||
|
|
||||||
|
/// Address QR code scanner content.
|
||||||
|
address_scan_content: Option<CameraContent>,
|
||||||
|
|
||||||
|
/// Transaction information content.
|
||||||
|
tx_info_content: Option<WalletTransactionModal>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TransportSendModal {
|
||||||
|
/// Create new instance from provided address.
|
||||||
|
pub fn new(addr: Option<String>) -> Self {
|
||||||
|
Self {
|
||||||
|
first_draw: true,
|
||||||
|
sending: false,
|
||||||
|
error: false,
|
||||||
|
send_result: Arc::new(RwLock::new(None)),
|
||||||
|
amount_edit: "".to_string(),
|
||||||
|
address_edit: addr.unwrap_or("".to_string()),
|
||||||
|
address_error: false,
|
||||||
|
address_scan_content: None,
|
||||||
|
tx_info_content: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw [`Modal`] content.
|
||||||
|
pub fn ui(&mut self,
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
wallet: &mut Wallet,
|
||||||
|
modal: &Modal,
|
||||||
|
cb: &dyn PlatformCallbacks) {
|
||||||
|
// Draw transaction information on request result.
|
||||||
|
if let Some(tx) = self.tx_info_content.as_mut() {
|
||||||
|
tx.ui(ui, wallet, modal, cb);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw sending content, progress or an error.
|
||||||
|
if self.sending {
|
||||||
|
self.progress_ui(ui, wallet);
|
||||||
|
} else if self.error {
|
||||||
|
self.error_ui(ui, wallet, modal, cb);
|
||||||
|
} else {
|
||||||
|
self.content_ui(ui, wallet, modal, cb);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw content to send.
|
||||||
|
fn content_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, modal: &Modal,
|
||||||
|
cb: &dyn PlatformCallbacks) {
|
||||||
|
// Draw QR code scanner content if requested.
|
||||||
|
if let Some(scanner) = self.address_scan_content.as_mut() {
|
||||||
|
let mut on_stop = || {
|
||||||
|
self.first_draw = true;
|
||||||
|
cb.stop_camera();
|
||||||
|
modal.enable_closing();
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(result) = scanner.qr_scan_result() {
|
||||||
|
self.address_edit = result.text();
|
||||||
|
on_stop();
|
||||||
|
self.address_scan_content = None;
|
||||||
|
cb.show_keyboard();
|
||||||
|
} else {
|
||||||
|
scanner.ui(ui, cb);
|
||||||
|
ui.add_space(6.0);
|
||||||
|
|
||||||
|
// Setup spacing between buttons.
|
||||||
|
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
|
||||||
|
|
||||||
|
// Show buttons to close modal or come back to sending input.
|
||||||
|
ui.columns(2, |cols| {
|
||||||
|
cols[0].vertical_centered_justified(|ui| {
|
||||||
|
View::button(ui, t!("close"), Colors::white_or_black(false), || {
|
||||||
|
on_stop();
|
||||||
|
self.address_scan_content = None;
|
||||||
|
modal.close();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
cols[1].vertical_centered_justified(|ui| {
|
||||||
|
View::button(ui, t!("back"), Colors::white_or_black(false), || {
|
||||||
|
on_stop();
|
||||||
|
self.address_scan_content = None;
|
||||||
|
cb.show_keyboard();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
ui.add_space(6.0);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.vertical_centered(|ui| {
|
||||||
|
let data = wallet.get_data().unwrap();
|
||||||
|
let amount = amount_to_hr_string(data.info.amount_currently_spendable, true);
|
||||||
|
let enter_text = t!("wallets.enter_amount_send","amount" => amount);
|
||||||
|
ui.label(RichText::new(enter_text)
|
||||||
|
.size(17.0)
|
||||||
|
.color(Colors::gray()));
|
||||||
|
});
|
||||||
|
ui.add_space(8.0);
|
||||||
|
|
||||||
|
// Draw amount text edit.
|
||||||
|
let amount_edit_id = Id::from(modal.id).with("amount").with(wallet.get_config().id);
|
||||||
|
let mut amount_edit_opts = TextEditOptions::new(amount_edit_id).h_center().no_focus();
|
||||||
|
let amount_edit_before = self.amount_edit.clone();
|
||||||
|
if self.first_draw {
|
||||||
|
self.first_draw = false;
|
||||||
|
amount_edit_opts.focus = true;
|
||||||
|
}
|
||||||
|
View::text_edit(ui, cb, &mut self.amount_edit, &mut amount_edit_opts);
|
||||||
|
ui.add_space(8.0);
|
||||||
|
|
||||||
|
// Check value if input was changed.
|
||||||
|
if amount_edit_before != self.amount_edit {
|
||||||
|
if !self.amount_edit.is_empty() {
|
||||||
|
// Trim text, replace "," by "." and parse amount.
|
||||||
|
self.amount_edit = self.amount_edit.trim().replace(",", ".");
|
||||||
|
match amount_from_hr_string(self.amount_edit.as_str()) {
|
||||||
|
Ok(a) => {
|
||||||
|
if !self.amount_edit.contains(".") {
|
||||||
|
// To avoid input of several "0".
|
||||||
|
if a == 0 {
|
||||||
|
self.amount_edit = "0".to_string();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Check input after ".".
|
||||||
|
let parts = self.amount_edit.split(".").collect::<Vec<&str>>();
|
||||||
|
if parts.len() == 2 && parts[1].len() > 9 {
|
||||||
|
self.amount_edit = amount_edit_before;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do not input amount more than balance in sending.
|
||||||
|
let b = wallet.get_data().unwrap().info.amount_currently_spendable;
|
||||||
|
if b < a {
|
||||||
|
self.amount_edit = amount_edit_before;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
self.amount_edit = amount_edit_before;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show address error or input description.
|
||||||
|
ui.vertical_centered(|ui| {
|
||||||
|
if self.address_error {
|
||||||
|
ui.label(RichText::new(t!("transport.incorrect_addr_err"))
|
||||||
|
.size(17.0)
|
||||||
|
.color(Colors::red()));
|
||||||
|
} else {
|
||||||
|
ui.label(RichText::new(t!("transport.receiver_address"))
|
||||||
|
.size(17.0)
|
||||||
|
.color(Colors::gray()));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ui.add_space(6.0);
|
||||||
|
|
||||||
|
// Draw address text edit.
|
||||||
|
let addr_edit_before = self.address_edit.clone();
|
||||||
|
let address_edit_id = Id::from(modal.id).with("_address").with(wallet.get_config().id);
|
||||||
|
let mut address_edit_opts = TextEditOptions::new(address_edit_id)
|
||||||
|
.paste()
|
||||||
|
.no_focus()
|
||||||
|
.scan_qr();
|
||||||
|
View::text_edit(ui, cb, &mut self.address_edit, &mut address_edit_opts);
|
||||||
|
// Check if scan button was pressed.
|
||||||
|
if address_edit_opts.scan_pressed {
|
||||||
|
cb.hide_keyboard();
|
||||||
|
modal.disable_closing();
|
||||||
|
address_edit_opts.scan_pressed = false;
|
||||||
|
self.address_scan_content = Some(CameraContent::default());
|
||||||
|
}
|
||||||
|
ui.add_space(12.0);
|
||||||
|
|
||||||
|
// Check value if input was changed.
|
||||||
|
if addr_edit_before != self.address_edit {
|
||||||
|
self.address_error = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup spacing between buttons.
|
||||||
|
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
|
||||||
|
|
||||||
|
ui.columns(2, |columns| {
|
||||||
|
columns[0].vertical_centered_justified(|ui| {
|
||||||
|
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
|
||||||
|
self.close(modal, cb);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
columns[1].vertical_centered_justified(|ui| {
|
||||||
|
View::button(ui, t!("continue"), Colors::white_or_black(false), || {
|
||||||
|
self.send(wallet, modal, cb);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
ui.add_space(6.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw error content.
|
||||||
|
fn error_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, modal: &Modal, cb: &dyn PlatformCallbacks) {
|
||||||
|
ui.add_space(6.0);
|
||||||
|
ui.vertical_centered(|ui| {
|
||||||
|
ui.label(RichText::new(t!("transport.tor_send_error"))
|
||||||
|
.size(17.0)
|
||||||
|
.color(Colors::red()));
|
||||||
|
});
|
||||||
|
ui.add_space(12.0);
|
||||||
|
|
||||||
|
// Setup spacing between buttons.
|
||||||
|
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
|
||||||
|
|
||||||
|
ui.columns(2, |columns| {
|
||||||
|
columns[0].vertical_centered_justified(|ui| {
|
||||||
|
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
|
||||||
|
self.close(modal, cb);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
columns[1].vertical_centered_justified(|ui| {
|
||||||
|
View::button(ui, t!("repeat"), Colors::white_or_black(false), || {
|
||||||
|
self.send(wallet, modal, cb);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
ui.add_space(6.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Close modal and clear data.
|
||||||
|
fn close(&mut self, modal: &Modal, cb: &dyn PlatformCallbacks) {
|
||||||
|
self.amount_edit = "".to_string();
|
||||||
|
self.address_edit = "".to_string();
|
||||||
|
|
||||||
|
let mut w_res = self.send_result.write();
|
||||||
|
*w_res = None;
|
||||||
|
|
||||||
|
self.tx_info_content = None;
|
||||||
|
self.address_scan_content = None;
|
||||||
|
|
||||||
|
cb.hide_keyboard();
|
||||||
|
modal.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send entered amount to address.
|
||||||
|
fn send(&mut self, wallet: &Wallet, modal: &Modal, cb: &dyn PlatformCallbacks) {
|
||||||
|
if self.amount_edit.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let addr_str = self.address_edit.as_str();
|
||||||
|
if let Ok(addr) = SlatepackAddress::try_from(addr_str) {
|
||||||
|
if let Ok(a) = amount_from_hr_string(self.amount_edit.as_str()) {
|
||||||
|
cb.hide_keyboard();
|
||||||
|
modal.disable_closing();
|
||||||
|
// Send amount over Tor.
|
||||||
|
let mut wallet = wallet.clone();
|
||||||
|
let res = self.send_result.clone();
|
||||||
|
self.sending = true;
|
||||||
|
thread::spawn(move || {
|
||||||
|
let runtime = TokioNativeTlsRuntime::create().unwrap();
|
||||||
|
runtime
|
||||||
|
.block_on(async {
|
||||||
|
let result = wallet.send_tor(a, &addr).await;
|
||||||
|
let mut w_res = res.write();
|
||||||
|
*w_res = Some(result);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.address_error = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw sending progress content.
|
||||||
|
fn progress_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet) {
|
||||||
|
ui.add_space(16.0);
|
||||||
|
ui.vertical_centered(|ui| {
|
||||||
|
View::small_loading_spinner(ui);
|
||||||
|
ui.add_space(12.0);
|
||||||
|
ui.label(RichText::new(t!("transport.tor_sending", "amount" => self.amount_edit))
|
||||||
|
.size(17.0)
|
||||||
|
.color(Colors::gray()));
|
||||||
|
});
|
||||||
|
ui.add_space(10.0);
|
||||||
|
|
||||||
|
// Check sending result.
|
||||||
|
let has_result = {
|
||||||
|
let r_result = self.send_result.read();
|
||||||
|
r_result.is_some()
|
||||||
|
};
|
||||||
|
if has_result {
|
||||||
|
{
|
||||||
|
let res = self.send_result.read().clone().unwrap();
|
||||||
|
match res {
|
||||||
|
Ok(tx) => {
|
||||||
|
self.tx_info_content = Some(WalletTransactionModal::new(wallet, &tx, false));
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
self.error = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut w_res = self.send_result.write();
|
||||||
|
*w_res = None;
|
||||||
|
self.sending = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
258
src/gui/views/wallets/wallet/transport/settings.rs
Normal file
258
src/gui/views/wallets/wallet/transport/settings.rs
Normal file
|
@ -0,0 +1,258 @@
|
||||||
|
// Copyright 2024 The Grim Developers
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
use egui::os::OperatingSystem;
|
||||||
|
use egui::{Id, RichText};
|
||||||
|
|
||||||
|
use crate::gui::Colors;
|
||||||
|
use crate::gui::platform::PlatformCallbacks;
|
||||||
|
use crate::gui::views::{CameraContent, Modal, View};
|
||||||
|
use crate::gui::views::types::TextEditOptions;
|
||||||
|
use crate::tor::{Tor, TorBridge, TorConfig};
|
||||||
|
use crate::wallet::Wallet;
|
||||||
|
|
||||||
|
/// Transport settings [`Modal`] content.
|
||||||
|
pub struct TransportSettingsModal {
|
||||||
|
/// Flag to check if Tor settings were changed.
|
||||||
|
settings_changed: bool,
|
||||||
|
|
||||||
|
/// Tor bridge binary path edit text.
|
||||||
|
bridge_bin_path_edit: String,
|
||||||
|
/// Tor bridge connection line edit text.
|
||||||
|
bridge_conn_line_edit: String,
|
||||||
|
/// Address QR code scanner [`Modal`] content.
|
||||||
|
bridge_qr_scan_content: Option<CameraContent>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for TransportSettingsModal {
|
||||||
|
fn default() -> Self {
|
||||||
|
// Setup Tor bridge binary path edit text.
|
||||||
|
let bridge = TorConfig::get_bridge();
|
||||||
|
let (bin_path, conn_line) = if let Some(b) = bridge {
|
||||||
|
(b.binary_path(), b.connection_line())
|
||||||
|
} else {
|
||||||
|
("".to_string(), "".to_string())
|
||||||
|
};
|
||||||
|
Self {
|
||||||
|
settings_changed: false,
|
||||||
|
bridge_bin_path_edit: bin_path,
|
||||||
|
bridge_conn_line_edit: conn_line,
|
||||||
|
bridge_qr_scan_content: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TransportSettingsModal {
|
||||||
|
pub fn ui(&mut self,
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
wallet: &Wallet,
|
||||||
|
modal: &Modal,
|
||||||
|
cb: &dyn PlatformCallbacks) {
|
||||||
|
ui.add_space(6.0);
|
||||||
|
|
||||||
|
// Draw QR code scanner content if requested.
|
||||||
|
if let Some(scanner) = self.bridge_qr_scan_content.as_mut() {
|
||||||
|
let on_stop = || {
|
||||||
|
cb.stop_camera();
|
||||||
|
modal.enable_closing();
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(result) = scanner.qr_scan_result() {
|
||||||
|
self.bridge_conn_line_edit = result.text();
|
||||||
|
on_stop();
|
||||||
|
self.bridge_qr_scan_content = None;
|
||||||
|
cb.show_keyboard();
|
||||||
|
} else {
|
||||||
|
scanner.ui(ui, cb);
|
||||||
|
ui.add_space(12.0);
|
||||||
|
|
||||||
|
// Setup spacing between buttons.
|
||||||
|
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
|
||||||
|
|
||||||
|
// Show buttons to close modal or come back to sending input.
|
||||||
|
ui.columns(2, |cols| {
|
||||||
|
cols[0].vertical_centered_justified(|ui| {
|
||||||
|
View::button(ui, t!("close"), Colors::white_or_black(false), || {
|
||||||
|
on_stop();
|
||||||
|
self.bridge_qr_scan_content = None;
|
||||||
|
modal.close();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
cols[1].vertical_centered_justified(|ui| {
|
||||||
|
View::button(ui, t!("back"), Colors::white_or_black(false), || {
|
||||||
|
on_stop();
|
||||||
|
self.bridge_qr_scan_content = None;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
ui.add_space(6.0);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do not show bridges setup on Android.
|
||||||
|
let os = OperatingSystem::from_target_os();
|
||||||
|
let show_bridges = os != OperatingSystem::Android;
|
||||||
|
if show_bridges {
|
||||||
|
let bridge = TorConfig::get_bridge();
|
||||||
|
ui.vertical_centered(|ui| {
|
||||||
|
ui.label(RichText::new(t!("transport.bridges_desc"))
|
||||||
|
.size(17.0)
|
||||||
|
.color(Colors::inactive_text()));
|
||||||
|
|
||||||
|
// Draw checkbox to enable/disable bridges.
|
||||||
|
View::checkbox(ui, bridge.is_some(), t!("transport.bridges"), || {
|
||||||
|
// Save value.
|
||||||
|
let value = if bridge.is_some() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
let default_bridge = TorConfig::get_obfs4();
|
||||||
|
self.bridge_bin_path_edit = default_bridge.binary_path();
|
||||||
|
self.bridge_conn_line_edit = default_bridge.connection_line();
|
||||||
|
Some(default_bridge)
|
||||||
|
};
|
||||||
|
TorConfig::save_bridge(value);
|
||||||
|
self.settings_changed = true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Draw bridges selection and path.
|
||||||
|
if bridge.is_some() {
|
||||||
|
let current_bridge = bridge.unwrap();
|
||||||
|
let mut bridge = current_bridge.clone();
|
||||||
|
|
||||||
|
ui.add_space(6.0);
|
||||||
|
ui.columns(2, |columns| {
|
||||||
|
columns[0].vertical_centered(|ui| {
|
||||||
|
// Draw Obfs4 bridge selector.
|
||||||
|
let obfs4 = TorConfig::get_obfs4();
|
||||||
|
let name = obfs4.protocol_name().to_uppercase();
|
||||||
|
View::radio_value(ui, &mut bridge, obfs4, name);
|
||||||
|
});
|
||||||
|
columns[1].vertical_centered(|ui| {
|
||||||
|
// Draw Snowflake bridge selector.
|
||||||
|
let snowflake = TorConfig::get_snowflake();
|
||||||
|
let name = snowflake.protocol_name().to_uppercase();
|
||||||
|
View::radio_value(ui, &mut bridge, snowflake, name);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
ui.add_space(12.0);
|
||||||
|
|
||||||
|
// Check if bridge type was changed to save.
|
||||||
|
if current_bridge != bridge {
|
||||||
|
self.settings_changed = true;
|
||||||
|
TorConfig::save_bridge(Some(bridge.clone()));
|
||||||
|
self.bridge_bin_path_edit = bridge.binary_path();
|
||||||
|
self.bridge_conn_line_edit = bridge.connection_line();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw binary path text edit.
|
||||||
|
let bin_edit_id = Id::from(modal.id)
|
||||||
|
.with(wallet.get_config().id)
|
||||||
|
.with("_bin_edit");
|
||||||
|
let mut bin_edit_opts = TextEditOptions::new(bin_edit_id)
|
||||||
|
.paste()
|
||||||
|
.no_focus();
|
||||||
|
let bin_edit_before = self.bridge_bin_path_edit.clone();
|
||||||
|
ui.vertical_centered(|ui| {
|
||||||
|
ui.label(RichText::new(t!("transport.bin_file"))
|
||||||
|
.size(17.0)
|
||||||
|
.color(Colors::inactive_text()));
|
||||||
|
ui.add_space(6.0);
|
||||||
|
View::text_edit(ui, cb, &mut self.bridge_bin_path_edit, &mut bin_edit_opts);
|
||||||
|
ui.add_space(6.0);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Draw connection line text edit.
|
||||||
|
let conn_edit_before = self.bridge_conn_line_edit.clone();
|
||||||
|
let conn_edit_id = Id::from(modal.id)
|
||||||
|
.with(wallet.get_config().id)
|
||||||
|
.with("_conn_edit");
|
||||||
|
let mut conn_edit_opts = TextEditOptions::new(conn_edit_id)
|
||||||
|
.paste()
|
||||||
|
.no_focus()
|
||||||
|
.scan_qr();
|
||||||
|
ui.vertical_centered(|ui| {
|
||||||
|
ui.label(RichText::new(t!("transport.conn_line"))
|
||||||
|
.size(17.0)
|
||||||
|
.color(Colors::inactive_text()));
|
||||||
|
ui.add_space(6.0);
|
||||||
|
View::text_edit(ui, cb, &mut self.bridge_conn_line_edit, &mut conn_edit_opts);
|
||||||
|
// Check if scan button was pressed.
|
||||||
|
if conn_edit_opts.scan_pressed {
|
||||||
|
cb.hide_keyboard();
|
||||||
|
modal.disable_closing();
|
||||||
|
conn_edit_opts.scan_pressed = false;
|
||||||
|
self.bridge_qr_scan_content = Some(CameraContent::default());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check if bin path or connection line text was changed to save bridge.
|
||||||
|
if conn_edit_before != self.bridge_conn_line_edit ||
|
||||||
|
bin_edit_before != self.bridge_bin_path_edit {
|
||||||
|
let bin_path = self.bridge_bin_path_edit.trim().to_string();
|
||||||
|
let conn_line = self.bridge_conn_line_edit.trim().to_string();
|
||||||
|
let b = match bridge {
|
||||||
|
TorBridge::Snowflake(_, _) => {
|
||||||
|
TorBridge::Snowflake(bin_path, conn_line)
|
||||||
|
},
|
||||||
|
TorBridge::Obfs4(_, _) => {
|
||||||
|
TorBridge::Obfs4(bin_path, conn_line)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
TorConfig::save_bridge(Some(b));
|
||||||
|
self.settings_changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.add_space(2.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.add_space(6.0);
|
||||||
|
View::horizontal_line(ui, Colors::item_stroke());
|
||||||
|
ui.add_space(6.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.vertical_centered(|ui| {
|
||||||
|
ui.label(RichText::new(t!("transport.tor_autorun_desc"))
|
||||||
|
.size(17.0)
|
||||||
|
.color(Colors::inactive_text()));
|
||||||
|
|
||||||
|
// Show Tor service autorun checkbox.
|
||||||
|
let autorun = wallet.auto_start_tor_listener();
|
||||||
|
View::checkbox(ui, autorun, t!("network.autorun"), || {
|
||||||
|
wallet.update_auto_start_tor_listener(!autorun);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
ui.add_space(6.0);
|
||||||
|
ui.vertical_centered_justified(|ui| {
|
||||||
|
View::button(ui, t!("close"), Colors::white_or_black(false), || {
|
||||||
|
if self.settings_changed {
|
||||||
|
self.settings_changed = false;
|
||||||
|
// Restart running service or rebuild client.
|
||||||
|
let service_id = &wallet.identifier();
|
||||||
|
if Tor::is_service_running(service_id) {
|
||||||
|
if let Ok(key) = wallet.secret_key() {
|
||||||
|
let api_port = wallet.foreign_api_port().unwrap();
|
||||||
|
Tor::restart_service(api_port, key, service_id);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Tor::rebuild_client();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
modal.close();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
ui.add_space(6.0);
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because it is too large
Load diff
483
src/gui/views/wallets/wallet/txs/content.rs
Normal file
483
src/gui/views/wallets/wallet/txs/content.rs
Normal file
|
@ -0,0 +1,483 @@
|
||||||
|
// Copyright 2023 The Grim Developers
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
use egui::{Align, Id, Layout, Margin, Rect, RichText, Rounding, ScrollArea};
|
||||||
|
use egui::scroll_area::ScrollBarVisibility;
|
||||||
|
use grin_core::core::amount_to_hr_string;
|
||||||
|
use grin_wallet_libwallet::TxLogEntryType;
|
||||||
|
|
||||||
|
use crate::gui::Colors;
|
||||||
|
use crate::gui::icons::{ARROW_CIRCLE_DOWN, ARROW_CIRCLE_UP, BRIDGE, CALENDAR_CHECK, CHAT_CIRCLE_TEXT, CHECK, CHECK_CIRCLE, DOTS_THREE_CIRCLE, FILE_TEXT, GEAR_FINE, PROHIBIT, X_CIRCLE};
|
||||||
|
use crate::gui::platform::PlatformCallbacks;
|
||||||
|
use crate::gui::views::{Modal, PullToRefresh, Content, View};
|
||||||
|
use crate::gui::views::types::ModalPosition;
|
||||||
|
use crate::gui::views::wallets::types::WalletTab;
|
||||||
|
use crate::gui::views::wallets::wallet::types::{GRIN, WalletTabType};
|
||||||
|
use crate::gui::views::wallets::wallet::{WalletContent, WalletTransactionModal};
|
||||||
|
use crate::wallet::types::{WalletData, WalletTransaction};
|
||||||
|
use crate::wallet::Wallet;
|
||||||
|
|
||||||
|
/// Wallet transactions tab content.
|
||||||
|
pub struct WalletTransactions {
|
||||||
|
/// Transaction information [`Modal`] content.
|
||||||
|
tx_info_content: Option<WalletTransactionModal>,
|
||||||
|
|
||||||
|
/// Transaction identifier to use at confirmation [`Modal`].
|
||||||
|
confirm_cancel_tx_id: Option<u32>,
|
||||||
|
|
||||||
|
/// Flag to check if sync of wallet was initiated manually at time.
|
||||||
|
manual_sync: Option<u128>
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for WalletTransactions {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
tx_info_content: None,
|
||||||
|
confirm_cancel_tx_id: None,
|
||||||
|
manual_sync: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WalletTab for WalletTransactions {
|
||||||
|
fn get_type(&self) -> WalletTabType {
|
||||||
|
WalletTabType::Txs
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ui(&mut self, ui: &mut egui::Ui, wallet: &mut Wallet, cb: &dyn PlatformCallbacks) {
|
||||||
|
if WalletContent::sync_ui(ui, wallet) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show modal content for this ui container.
|
||||||
|
self.modal_content_ui(ui, wallet, cb);
|
||||||
|
|
||||||
|
// Show wallet transactions content.
|
||||||
|
egui::CentralPanel::default()
|
||||||
|
.frame(egui::Frame {
|
||||||
|
stroke: View::item_stroke(),
|
||||||
|
fill: Colors::button(),
|
||||||
|
inner_margin: Margin {
|
||||||
|
left: View::far_left_inset_margin(ui) + 4.0,
|
||||||
|
right: View::get_right_inset() + 4.0,
|
||||||
|
top: 0.0,
|
||||||
|
bottom: 4.0,
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.show_inside(ui, |ui| {
|
||||||
|
ui.vertical_centered(|ui| {
|
||||||
|
let data = wallet.get_data().unwrap();
|
||||||
|
self.txs_ui(ui, wallet, &data, cb);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Identifier for transaction information [`Modal`].
|
||||||
|
const TX_INFO_MODAL: &'static str = "tx_info_modal";
|
||||||
|
/// Identifier for transaction cancellation confirmation [`Modal`].
|
||||||
|
const CANCEL_TX_CONFIRMATION_MODAL: &'static str = "cancel_tx_conf_modal";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
impl WalletTransactions {
|
||||||
|
/// Height of transaction list item.
|
||||||
|
pub const TX_ITEM_HEIGHT: f32 = 76.0;
|
||||||
|
|
||||||
|
/// Draw transactions content.
|
||||||
|
fn txs_ui(&mut self,
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
wallet: &mut Wallet,
|
||||||
|
data: &WalletData,
|
||||||
|
cb: &dyn PlatformCallbacks) {
|
||||||
|
let amount_conf = data.info.amount_awaiting_confirmation;
|
||||||
|
let amount_fin = data.info.amount_awaiting_finalization;
|
||||||
|
let amount_locked = data.info.amount_locked;
|
||||||
|
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
|
||||||
|
// Show non-zero awaiting confirmation amount.
|
||||||
|
if amount_conf != 0 {
|
||||||
|
let awaiting_conf = amount_to_hr_string(amount_conf, true);
|
||||||
|
let rounding = if amount_fin != 0 || amount_locked != 0 {
|
||||||
|
[false, false, false, false]
|
||||||
|
} else {
|
||||||
|
[false, false, true, true]
|
||||||
|
};
|
||||||
|
View::rounded_box(ui,
|
||||||
|
format!("{} ツ", awaiting_conf),
|
||||||
|
t!("wallets.await_conf_amount"),
|
||||||
|
rounding);
|
||||||
|
}
|
||||||
|
// Show non-zero awaiting finalization amount.
|
||||||
|
if amount_fin != 0 {
|
||||||
|
let awaiting_conf = amount_to_hr_string(amount_fin, true);
|
||||||
|
let rounding = if amount_locked != 0 {
|
||||||
|
[false, false, false, false]
|
||||||
|
} else {
|
||||||
|
[false, false, true, true]
|
||||||
|
};
|
||||||
|
View::rounded_box(ui,
|
||||||
|
format!("{} ツ", awaiting_conf),
|
||||||
|
t!("wallets.await_fin_amount"),
|
||||||
|
rounding);
|
||||||
|
}
|
||||||
|
// Show non-zero locked amount.
|
||||||
|
if amount_locked != 0 {
|
||||||
|
let awaiting_conf = amount_to_hr_string(amount_locked, true);
|
||||||
|
View::rounded_box(ui,
|
||||||
|
format!("{} ツ", awaiting_conf),
|
||||||
|
t!("wallets.locked_amount"),
|
||||||
|
[false, false, true, true]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show message when txs are empty.
|
||||||
|
if let Some(txs) = data.txs.as_ref() {
|
||||||
|
if txs.is_empty() {
|
||||||
|
View::center_content(ui, 96.0, |ui| {
|
||||||
|
let empty_text = t!(
|
||||||
|
"wallets.txs_empty",
|
||||||
|
"message" => CHAT_CIRCLE_TEXT,
|
||||||
|
"transport" => BRIDGE,
|
||||||
|
"settings" => GEAR_FINE
|
||||||
|
);
|
||||||
|
ui.label(RichText::new(empty_text).size(16.0).color(Colors::inactive_text()));
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Show loader when txs are not loaded.
|
||||||
|
if data.txs.is_none() {
|
||||||
|
ui.centered_and_justified(|ui| {
|
||||||
|
View::big_loading_spinner(ui);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.add_space(4.0);
|
||||||
|
|
||||||
|
// Show list of transactions.
|
||||||
|
let txs = data.txs.as_ref().unwrap();
|
||||||
|
let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis();
|
||||||
|
let refresh = self.manual_sync.unwrap_or(0) + 1600 > now;
|
||||||
|
let refresh_resp = PullToRefresh::new(refresh)
|
||||||
|
.can_refresh(!refresh && !wallet.syncing())
|
||||||
|
.min_refresh_distance(70.0)
|
||||||
|
.scroll_area_ui(ui, |ui| {
|
||||||
|
ScrollArea::vertical()
|
||||||
|
.id_source(Id::from("txs_content").with(wallet.get_config().id))
|
||||||
|
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
|
||||||
|
.auto_shrink([false; 2])
|
||||||
|
.show_rows(ui, Self::TX_ITEM_HEIGHT, txs.len(), |ui, row_range| {
|
||||||
|
ui.add_space(1.0);
|
||||||
|
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
|
||||||
|
let padding = amount_conf != 0 || amount_fin != 0 || amount_locked != 0;
|
||||||
|
for index in row_range {
|
||||||
|
let tx = txs.get(index).unwrap();
|
||||||
|
let mut r = View::item_rounding(index, txs.len(), false);
|
||||||
|
let mut rect = ui.available_rect_before_wrap();
|
||||||
|
if padding {
|
||||||
|
rect.min += egui::emath::vec2(6.0, 0.0);
|
||||||
|
rect.max -= egui::emath::vec2(6.0, 0.0);
|
||||||
|
}
|
||||||
|
rect.set_height(Self::TX_ITEM_HEIGHT);
|
||||||
|
Self::tx_item_ui(ui, tx, rect, r, &data, |ui| {
|
||||||
|
// Draw button to show transaction info.
|
||||||
|
if tx.data.tx_slate_id.is_some() {
|
||||||
|
r.nw = 0.0;
|
||||||
|
r.sw = 0.0;
|
||||||
|
View::item_button(ui, r, FILE_TEXT, None, || {
|
||||||
|
self.show_tx_info_modal(wallet, tx, false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let wallet_loaded = wallet.foreign_api_port().is_some();
|
||||||
|
|
||||||
|
// Draw button to show transaction finalization.
|
||||||
|
if wallet_loaded && tx.can_finalize {
|
||||||
|
let (icon, color) = (CHECK, Some(Colors::green()));
|
||||||
|
View::item_button(ui, Rounding::default(), icon, color, || {
|
||||||
|
cb.hide_keyboard();
|
||||||
|
self.show_tx_info_modal(wallet, tx, true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw button to cancel transaction.
|
||||||
|
if wallet_loaded && tx.can_cancel() {
|
||||||
|
let (icon, color) = (PROHIBIT, Some(Colors::red()));
|
||||||
|
View::item_button(ui, Rounding::default(), icon, color, || {
|
||||||
|
self.confirm_cancel_tx_id = Some(tx.data.id);
|
||||||
|
// Show transaction cancellation confirmation modal.
|
||||||
|
Modal::new(CANCEL_TX_CONFIRMATION_MODAL)
|
||||||
|
.position(ModalPosition::Center)
|
||||||
|
.title(t!("modal.confirmation"))
|
||||||
|
.show();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sync wallet on refresh.
|
||||||
|
if refresh_resp.should_refresh() {
|
||||||
|
self.manual_sync = Some(now);
|
||||||
|
if !wallet.syncing() {
|
||||||
|
wallet.sync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw [`Modal`] content for this ui container.
|
||||||
|
fn modal_content_ui(&mut self,
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
wallet: &mut Wallet,
|
||||||
|
cb: &dyn PlatformCallbacks) {
|
||||||
|
match Modal::opened() {
|
||||||
|
None => {}
|
||||||
|
Some(id) => {
|
||||||
|
match id {
|
||||||
|
TX_INFO_MODAL => {
|
||||||
|
Modal::ui(ui.ctx(), |ui, modal| {
|
||||||
|
if let Some(content) = self.tx_info_content.as_mut() {
|
||||||
|
content.ui(ui, wallet, modal, cb);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
CANCEL_TX_CONFIRMATION_MODAL => {
|
||||||
|
Modal::ui(ui.ctx(), |ui, modal| {
|
||||||
|
self.cancel_confirmation_modal(ui, wallet, modal);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw transaction item.
|
||||||
|
pub fn tx_item_ui(ui: &mut egui::Ui,
|
||||||
|
tx: &WalletTransaction,
|
||||||
|
rect: Rect,
|
||||||
|
rounding: Rounding,
|
||||||
|
data: &WalletData,
|
||||||
|
buttons_ui: impl FnOnce(&mut egui::Ui)) {
|
||||||
|
// Draw round background.
|
||||||
|
let bg_rect = rect.clone();
|
||||||
|
ui.painter().rect(bg_rect, rounding, Colors::TRANSPARENT, View::item_stroke());
|
||||||
|
|
||||||
|
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Max), |ui| {
|
||||||
|
ui.horizontal_centered(|ui| {
|
||||||
|
// Draw buttons.
|
||||||
|
buttons_ui(ui);
|
||||||
|
});
|
||||||
|
|
||||||
|
ui.with_layout(Layout::left_to_right(Align::Min), |ui| {
|
||||||
|
ui.add_space(6.0);
|
||||||
|
ui.vertical(|ui| {
|
||||||
|
ui.add_space(3.0);
|
||||||
|
|
||||||
|
// Setup transaction amount.
|
||||||
|
let mut amount_text = if tx.data.tx_type == TxLogEntryType::TxSent ||
|
||||||
|
tx.data.tx_type == TxLogEntryType::TxSentCancelled {
|
||||||
|
"-"
|
||||||
|
} else if tx.data.tx_type == TxLogEntryType::TxReceived ||
|
||||||
|
tx.data.tx_type == TxLogEntryType::TxReceivedCancelled {
|
||||||
|
"+"
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
}.to_string();
|
||||||
|
amount_text = format!("{}{} {}",
|
||||||
|
amount_text,
|
||||||
|
amount_to_hr_string(tx.amount, true),
|
||||||
|
GRIN);
|
||||||
|
|
||||||
|
// Setup amount color.
|
||||||
|
let amount_color = match tx.data.tx_type {
|
||||||
|
TxLogEntryType::ConfirmedCoinbase => Colors::white_or_black(true),
|
||||||
|
TxLogEntryType::TxReceived => Colors::white_or_black(true),
|
||||||
|
TxLogEntryType::TxSent => Colors::white_or_black(true),
|
||||||
|
TxLogEntryType::TxReceivedCancelled => Colors::text(false),
|
||||||
|
TxLogEntryType::TxSentCancelled => Colors::text(false),
|
||||||
|
TxLogEntryType::TxReverted => Colors::text(false)
|
||||||
|
};
|
||||||
|
ui.with_layout(Layout::left_to_right(Align::Min), |ui| {
|
||||||
|
ui.add_space(1.0);
|
||||||
|
View::ellipsize_text(ui, amount_text, 18.0, amount_color);
|
||||||
|
});
|
||||||
|
ui.add_space(-2.0);
|
||||||
|
|
||||||
|
// Setup transaction status text.
|
||||||
|
let status_text = if !tx.data.confirmed {
|
||||||
|
let is_canceled = tx.data.tx_type == TxLogEntryType::TxSentCancelled
|
||||||
|
|| tx.data.tx_type == TxLogEntryType::TxReceivedCancelled;
|
||||||
|
if is_canceled {
|
||||||
|
format!("{} {}", X_CIRCLE, t!("wallets.tx_canceled"))
|
||||||
|
} else if tx.finalizing {
|
||||||
|
format!("{} {}", DOTS_THREE_CIRCLE, t!("wallets.tx_finalizing"))
|
||||||
|
} else {
|
||||||
|
if tx.cancelling {
|
||||||
|
format!("{} {}", DOTS_THREE_CIRCLE, t!("wallets.tx_cancelling"))
|
||||||
|
} else {
|
||||||
|
match tx.data.tx_type {
|
||||||
|
TxLogEntryType::TxReceived => {
|
||||||
|
format!("{} {}",
|
||||||
|
DOTS_THREE_CIRCLE,
|
||||||
|
t!("wallets.tx_receiving"))
|
||||||
|
},
|
||||||
|
TxLogEntryType::TxSent => {
|
||||||
|
format!("{} {}",
|
||||||
|
DOTS_THREE_CIRCLE,
|
||||||
|
t!("wallets.tx_sending"))
|
||||||
|
},
|
||||||
|
_ => {
|
||||||
|
format!("{} {}",
|
||||||
|
DOTS_THREE_CIRCLE,
|
||||||
|
t!("wallets.tx_confirmed"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
match tx.data.tx_type {
|
||||||
|
TxLogEntryType::ConfirmedCoinbase => {
|
||||||
|
format!("{} {}", CHECK_CIRCLE, t!("wallets.tx_confirmed"))
|
||||||
|
},
|
||||||
|
TxLogEntryType::TxSent | TxLogEntryType::TxReceived => {
|
||||||
|
let height = data.info.last_confirmed_height;
|
||||||
|
let min_conf = data.info.minimum_confirmations;
|
||||||
|
if tx.conf_height.is_none() || (tx.conf_height.unwrap() != 0 &&
|
||||||
|
height - tx.conf_height.unwrap() > min_conf - 1) {
|
||||||
|
let (i, t) = if tx.data.tx_type == TxLogEntryType::TxSent {
|
||||||
|
(ARROW_CIRCLE_UP, t!("wallets.tx_sent"))
|
||||||
|
} else {
|
||||||
|
(ARROW_CIRCLE_DOWN, t!("wallets.tx_received"))
|
||||||
|
};
|
||||||
|
format!("{} {}", i, t)
|
||||||
|
} else {
|
||||||
|
let tx_height = tx.conf_height.unwrap() - 1;
|
||||||
|
let left_conf = height - tx_height;
|
||||||
|
let conf_info = if tx_height != 0 && height >= tx_height &&
|
||||||
|
left_conf < min_conf {
|
||||||
|
format!("{}/{}", left_conf, min_conf)
|
||||||
|
} else {
|
||||||
|
"".to_string()
|
||||||
|
};
|
||||||
|
format!("{} {} {}",
|
||||||
|
DOTS_THREE_CIRCLE,
|
||||||
|
t!("wallets.tx_confirming"),
|
||||||
|
conf_info
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => format!("{} {}", X_CIRCLE, t!("wallets.canceled"))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Setup status text color.
|
||||||
|
let status_color = match tx.data.tx_type {
|
||||||
|
TxLogEntryType::ConfirmedCoinbase => Colors::text(false),
|
||||||
|
TxLogEntryType::TxReceived => if tx.data.confirmed {
|
||||||
|
Colors::green()
|
||||||
|
} else {
|
||||||
|
Colors::text(false)
|
||||||
|
},
|
||||||
|
TxLogEntryType::TxSent => if tx.data.confirmed {
|
||||||
|
Colors::red()
|
||||||
|
} else {
|
||||||
|
Colors::text(false)
|
||||||
|
},
|
||||||
|
TxLogEntryType::TxReceivedCancelled => Colors::inactive_text(),
|
||||||
|
TxLogEntryType::TxSentCancelled => Colors::inactive_text(),
|
||||||
|
TxLogEntryType::TxReverted => Colors::inactive_text(),
|
||||||
|
};
|
||||||
|
ui.label(RichText::new(status_text).size(15.0).color(status_color));
|
||||||
|
|
||||||
|
// Setup transaction time.
|
||||||
|
let tx_time = View::format_time(tx.data.creation_ts.timestamp());
|
||||||
|
let tx_time_text = format!("{} {}", CALENDAR_CHECK, tx_time);
|
||||||
|
ui.label(RichText::new(tx_time_text).size(15.0).color(Colors::gray()));
|
||||||
|
ui.add_space(3.0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Show transaction information [`Modal`].
|
||||||
|
fn show_tx_info_modal(&mut self, wallet: &Wallet, tx: &WalletTransaction, finalize: bool) {
|
||||||
|
let modal = WalletTransactionModal::new(wallet, tx, finalize);
|
||||||
|
self.tx_info_content = Some(modal);
|
||||||
|
Modal::new(TX_INFO_MODAL)
|
||||||
|
.position(ModalPosition::CenterTop)
|
||||||
|
.title(t!("wallets.tx"))
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Confirmation [`Modal`] to cancel transaction.
|
||||||
|
fn cancel_confirmation_modal(&mut self, ui: &mut egui::Ui, wallet: &mut Wallet, modal: &Modal) {
|
||||||
|
ui.add_space(6.0);
|
||||||
|
ui.vertical_centered(|ui| {
|
||||||
|
// Setup confirmation text.
|
||||||
|
let data = wallet.get_data().unwrap();
|
||||||
|
let data_txs = data.txs.unwrap();
|
||||||
|
let txs = data_txs.into_iter()
|
||||||
|
.filter(|tx| tx.data.id == self.confirm_cancel_tx_id.unwrap())
|
||||||
|
.collect::<Vec<WalletTransaction>>();
|
||||||
|
if txs.is_empty() {
|
||||||
|
modal.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let tx = txs.get(0).unwrap();
|
||||||
|
let amount = amount_to_hr_string(tx.amount, true);
|
||||||
|
let text = match tx.data.tx_type {
|
||||||
|
TxLogEntryType::TxReceived => {
|
||||||
|
t!("wallets.tx_receive_cancel_conf", "amount" => amount)
|
||||||
|
},
|
||||||
|
_ => {
|
||||||
|
t!("wallets.tx_send_cancel_conf", "amount" => amount)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
ui.label(RichText::new(text)
|
||||||
|
.size(17.0)
|
||||||
|
.color(Colors::text(false)));
|
||||||
|
ui.add_space(8.0);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Show modal buttons.
|
||||||
|
ui.scope(|ui| {
|
||||||
|
// Setup spacing between buttons.
|
||||||
|
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
|
||||||
|
|
||||||
|
ui.columns(2, |columns| {
|
||||||
|
columns[0].vertical_centered_justified(|ui| {
|
||||||
|
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
|
||||||
|
self.confirm_cancel_tx_id = None;
|
||||||
|
modal.close();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
columns[1].vertical_centered_justified(|ui| {
|
||||||
|
View::button(ui, "OK".to_string(), Colors::white_or_black(false), || {
|
||||||
|
wallet.cancel(self.confirm_cancel_tx_id.unwrap());
|
||||||
|
self.confirm_cancel_tx_id = None;
|
||||||
|
modal.close();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
ui.add_space(6.0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
19
src/gui/views/wallets/wallet/txs/mod.rs
Normal file
19
src/gui/views/wallets/wallet/txs/mod.rs
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
// Copyright 2024 The Grim Developers
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
mod content;
|
||||||
|
pub use content::*;
|
||||||
|
|
||||||
|
mod tx;
|
||||||
|
pub use tx::*;
|
578
src/gui/views/wallets/wallet/txs/tx.rs
Normal file
578
src/gui/views/wallets/wallet/txs/tx.rs
Normal file
|
@ -0,0 +1,578 @@
|
||||||
|
// Copyright 2024 The Grim Developers
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::thread;
|
||||||
|
use egui::{Align, Id, Layout, RichText, Rounding, ScrollArea};
|
||||||
|
use egui::scroll_area::ScrollBarVisibility;
|
||||||
|
use grin_core::core::amount_to_hr_string;
|
||||||
|
use grin_util::ToHex;
|
||||||
|
use grin_wallet_libwallet::{Error, Slate, SlateState, TxLogEntryType};
|
||||||
|
use parking_lot::RwLock;
|
||||||
|
use crate::gui::Colors;
|
||||||
|
use crate::gui::icons::{BROOM, CHECK, CLIPBOARD_TEXT, COPY, FILE_ARCHIVE, FILE_TEXT, HASH_STRAIGHT, PROHIBIT, QR_CODE, SCAN};
|
||||||
|
use crate::gui::platform::PlatformCallbacks;
|
||||||
|
|
||||||
|
use crate::gui::views::{CameraContent, FilePickButton, Modal, QrCodeContent, View};
|
||||||
|
use crate::gui::views::wallets::wallet::txs::WalletTransactions;
|
||||||
|
use crate::gui::views::wallets::wallet::types::SLATEPACK_MESSAGE_HINT;
|
||||||
|
use crate::wallet::types::WalletTransaction;
|
||||||
|
use crate::wallet::Wallet;
|
||||||
|
|
||||||
|
/// Transaction information [`Modal`] content.
|
||||||
|
pub struct WalletTransactionModal {
|
||||||
|
/// Transaction identifier.
|
||||||
|
tx_id: u32,
|
||||||
|
/// Identifier for [`Slate`].
|
||||||
|
slate_id: Option<String>,
|
||||||
|
|
||||||
|
/// Response Slatepack message input value.
|
||||||
|
response_edit: String,
|
||||||
|
|
||||||
|
/// Flag to show transaction finalization input.
|
||||||
|
show_finalization: bool,
|
||||||
|
/// Finalization Slatepack message input value.
|
||||||
|
finalize_edit: String,
|
||||||
|
/// Flag to check if error happened during transaction finalization.
|
||||||
|
finalize_error: bool,
|
||||||
|
/// Flag to check if transaction is finalizing.
|
||||||
|
finalizing: bool,
|
||||||
|
/// Transaction finalization result.
|
||||||
|
final_result: Arc<RwLock<Option<Result<WalletTransaction, Error>>>>,
|
||||||
|
|
||||||
|
/// QR code Slatepack message image content.
|
||||||
|
qr_code_content: Option<QrCodeContent>,
|
||||||
|
|
||||||
|
/// QR code scanner content.
|
||||||
|
qr_scan_content: Option<CameraContent>,
|
||||||
|
|
||||||
|
/// Button to parse picked file content.
|
||||||
|
file_pick_button: FilePickButton,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WalletTransactionModal {
|
||||||
|
/// Create new content instance with [`Wallet`] from provided [`WalletTransaction`].
|
||||||
|
pub fn new(wallet: &Wallet, tx: &WalletTransaction, show_finalization: bool) -> Self {
|
||||||
|
Self {
|
||||||
|
tx_id: tx.data.id,
|
||||||
|
slate_id: match tx.data.tx_slate_id {
|
||||||
|
None => None,
|
||||||
|
Some(id) => Some(id.to_string())
|
||||||
|
},
|
||||||
|
response_edit: if !tx.data.confirmed && tx.data.tx_slate_id.is_some() &&
|
||||||
|
(tx.data.tx_type == TxLogEntryType::TxSent ||
|
||||||
|
tx.data.tx_type == TxLogEntryType::TxReceived) {
|
||||||
|
let mut slate = Slate::blank(1, false);
|
||||||
|
slate.state = if tx.can_finalize {
|
||||||
|
if tx.data.tx_type == TxLogEntryType::TxSent {
|
||||||
|
SlateState::Standard1
|
||||||
|
} else {
|
||||||
|
SlateState::Invoice1
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if tx.data.tx_type == TxLogEntryType::TxReceived {
|
||||||
|
SlateState::Standard2
|
||||||
|
} else {
|
||||||
|
SlateState::Invoice2
|
||||||
|
}
|
||||||
|
};
|
||||||
|
slate.id = tx.data.tx_slate_id.unwrap();
|
||||||
|
wallet.read_slatepack(&slate).unwrap_or("".to_string())
|
||||||
|
} else {
|
||||||
|
"".to_string()
|
||||||
|
},
|
||||||
|
finalize_edit: "".to_string(),
|
||||||
|
finalize_error: false,
|
||||||
|
show_finalization,
|
||||||
|
finalizing: false,
|
||||||
|
final_result: Arc::new(RwLock::new(None)),
|
||||||
|
qr_code_content: None,
|
||||||
|
qr_scan_content: None,
|
||||||
|
file_pick_button: FilePickButton::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw [`Modal`] content.
|
||||||
|
pub fn ui(&mut self,
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
wallet: &mut Wallet,
|
||||||
|
modal: &Modal,
|
||||||
|
cb: &dyn PlatformCallbacks) {
|
||||||
|
// Check values and setup transaction data.
|
||||||
|
let wallet_data = wallet.get_data();
|
||||||
|
if wallet_data.is_none() {
|
||||||
|
modal.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let data = wallet_data.unwrap();
|
||||||
|
let data_txs = data.txs.clone().unwrap();
|
||||||
|
let txs = data_txs.into_iter()
|
||||||
|
.filter(|tx| tx.data.id == self.tx_id)
|
||||||
|
.collect::<Vec<WalletTransaction>>();
|
||||||
|
if txs.is_empty() {
|
||||||
|
cb.hide_keyboard();
|
||||||
|
modal.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let tx = txs.get(0).unwrap();
|
||||||
|
|
||||||
|
if self.qr_code_content.is_none() && self.qr_scan_content.is_none() {
|
||||||
|
ui.add_space(6.0);
|
||||||
|
|
||||||
|
// Show transaction amount status and time.
|
||||||
|
let r = View::item_rounding(0, 2, false);
|
||||||
|
let mut rect = ui.available_rect_before_wrap();
|
||||||
|
rect.set_height(WalletTransactions::TX_ITEM_HEIGHT);
|
||||||
|
WalletTransactions::tx_item_ui(ui, tx, rect, r, &data, |ui| {
|
||||||
|
// Do not show buttons on finalizing.
|
||||||
|
if self.finalizing {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let wallet_loaded = wallet.foreign_api_port().is_some();
|
||||||
|
|
||||||
|
// Draw button to show transaction finalization or transaction info.
|
||||||
|
if wallet_loaded && tx.can_finalize {
|
||||||
|
let (icon, color) = if self.show_finalization {
|
||||||
|
(FILE_TEXT, None)
|
||||||
|
} else {
|
||||||
|
(CHECK, Some(Colors::green()))
|
||||||
|
};
|
||||||
|
let mut r = r.clone();
|
||||||
|
r.nw = 0.0;
|
||||||
|
r.sw = 0.0;
|
||||||
|
View::item_button(ui, r, icon, color, || {
|
||||||
|
cb.hide_keyboard();
|
||||||
|
if self.show_finalization {
|
||||||
|
self.show_finalization = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.show_finalization = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw button to cancel transaction.
|
||||||
|
if wallet_loaded && tx.can_cancel() {
|
||||||
|
View::item_button(ui, Rounding::default(), PROHIBIT, Some(Colors::red()), || {
|
||||||
|
cb.hide_keyboard();
|
||||||
|
wallet.cancel(tx.data.id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Show transaction ID info.
|
||||||
|
if let Some(id) = tx.data.tx_slate_id {
|
||||||
|
let label = format!("{} {}", HASH_STRAIGHT, t!("id"));
|
||||||
|
Self::info_item_ui(ui, id.to_string(), label, true, cb);
|
||||||
|
}
|
||||||
|
// Show transaction kernel info.
|
||||||
|
if let Some(kernel) = tx.data.kernel_excess {
|
||||||
|
let label = format!("{} {}", FILE_ARCHIVE, t!("kernel"));
|
||||||
|
Self::info_item_ui(ui, kernel.0.to_hex(), label, true, cb);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show Slatepack message or reset QR code state if not available.
|
||||||
|
if !tx.finalizing && !tx.data.confirmed && !tx.cancelling &&
|
||||||
|
(tx.data.tx_type == TxLogEntryType::TxSent ||
|
||||||
|
tx.data.tx_type == TxLogEntryType::TxReceived) && !self.response_edit.is_empty() {
|
||||||
|
self.message_ui(ui, tx, wallet, modal, cb);
|
||||||
|
} else if let Some(qr_content) = self.qr_code_content.as_mut() {
|
||||||
|
qr_content.clear_state();
|
||||||
|
}
|
||||||
|
|
||||||
|
if !self.finalizing {
|
||||||
|
// Setup spacing between buttons.
|
||||||
|
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
|
||||||
|
|
||||||
|
if self.qr_code_content.is_some() {
|
||||||
|
// Show buttons to close modal or come back to text request content.
|
||||||
|
ui.columns(2, |cols| {
|
||||||
|
cols[0].vertical_centered_justified(|ui| {
|
||||||
|
View::button(ui, t!("close"), Colors::white_or_black(false), || {
|
||||||
|
self.qr_code_content = None;
|
||||||
|
modal.close();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
cols[1].vertical_centered_justified(|ui| {
|
||||||
|
View::button(ui, t!("back"), Colors::white_or_black(false), || {
|
||||||
|
self.qr_code_content = None;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else if self.qr_scan_content.is_some() {
|
||||||
|
ui.add_space(8.0);
|
||||||
|
// Show buttons to close modal or scanner.
|
||||||
|
ui.columns(2, |cols| {
|
||||||
|
cols[0].vertical_centered_justified(|ui| {
|
||||||
|
View::button(ui, t!("close"), Colors::white_or_black(false), || {
|
||||||
|
cb.stop_camera();
|
||||||
|
self.qr_scan_content = None;
|
||||||
|
modal.close();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
cols[1].vertical_centered_justified(|ui| {
|
||||||
|
View::button(ui, t!("back"), Colors::white_or_black(false), || {
|
||||||
|
cb.stop_camera();
|
||||||
|
self.qr_scan_content = None;
|
||||||
|
modal.enable_closing();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
ui.add_space(8.0);
|
||||||
|
View::horizontal_line(ui, Colors::item_stroke());
|
||||||
|
ui.add_space(8.0);
|
||||||
|
|
||||||
|
// Show button to close modal.
|
||||||
|
ui.vertical_centered_justified(|ui| {
|
||||||
|
View::button(ui, t!("close"), Colors::white_or_black(false), || {
|
||||||
|
cb.hide_keyboard();
|
||||||
|
modal.close();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
ui.add_space(6.0);
|
||||||
|
} else {
|
||||||
|
// Show loader on finalizing.
|
||||||
|
ui.vertical_centered(|ui| {
|
||||||
|
View::small_loading_spinner(ui);
|
||||||
|
ui.add_space(16.0);
|
||||||
|
});
|
||||||
|
// Check finalization result.
|
||||||
|
let has_res = {
|
||||||
|
let r_res = self.final_result.read();
|
||||||
|
r_res.is_some()
|
||||||
|
};
|
||||||
|
if has_res {
|
||||||
|
let res = {
|
||||||
|
let r_res = self.final_result.read();
|
||||||
|
r_res.as_ref().unwrap().clone()
|
||||||
|
};
|
||||||
|
if let Ok(_) = res {
|
||||||
|
self.show_finalization = false;
|
||||||
|
self.finalize_edit = "".to_string();
|
||||||
|
} else {
|
||||||
|
self.finalize_error = true;
|
||||||
|
}
|
||||||
|
// Clear status and result.
|
||||||
|
{
|
||||||
|
let mut w_res = self.final_result.write();
|
||||||
|
*w_res = None;
|
||||||
|
}
|
||||||
|
self.finalizing = false;
|
||||||
|
modal.enable_closing();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw transaction information item content.
|
||||||
|
fn info_item_ui(ui: &mut egui::Ui,
|
||||||
|
value: String,
|
||||||
|
label: String,
|
||||||
|
copy: bool,
|
||||||
|
cb: &dyn PlatformCallbacks) {
|
||||||
|
// Setup layout size.
|
||||||
|
let mut rect = ui.available_rect_before_wrap();
|
||||||
|
rect.set_height(50.0);
|
||||||
|
|
||||||
|
// Draw round background.
|
||||||
|
let bg_rect = rect.clone();
|
||||||
|
let mut rounding = View::item_rounding(1, 3, false);
|
||||||
|
|
||||||
|
ui.painter().rect(bg_rect, rounding, Colors::fill(), View::item_stroke());
|
||||||
|
|
||||||
|
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
|
||||||
|
// Draw button to copy transaction info value.
|
||||||
|
if copy {
|
||||||
|
rounding.nw = 0.0;
|
||||||
|
rounding.sw = 0.0;
|
||||||
|
View::item_button(ui, rounding, COPY, None, || {
|
||||||
|
cb.copy_string_to_buffer(value.clone());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw value information.
|
||||||
|
let layout_size = ui.available_size();
|
||||||
|
ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| {
|
||||||
|
ui.add_space(6.0);
|
||||||
|
ui.vertical(|ui| {
|
||||||
|
ui.add_space(3.0);
|
||||||
|
View::ellipsize_text(ui, value, 15.0, Colors::title(false));
|
||||||
|
ui.label(RichText::new(label).size(15.0).color(Colors::gray()));
|
||||||
|
ui.add_space(3.0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw Slatepack message content.
|
||||||
|
fn message_ui(&mut self,
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
tx: &WalletTransaction,
|
||||||
|
wallet: &Wallet,
|
||||||
|
modal: &Modal,
|
||||||
|
cb: &dyn PlatformCallbacks) {
|
||||||
|
if self.slate_id.is_none() {
|
||||||
|
cb.hide_keyboard();
|
||||||
|
modal.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ui.add_space(6.0);
|
||||||
|
|
||||||
|
// Draw QR code scanner content if requested.
|
||||||
|
if let Some(qr_scan_content) = self.qr_scan_content.as_mut() {
|
||||||
|
if let Some(result) = qr_scan_content.qr_scan_result() {
|
||||||
|
cb.stop_camera();
|
||||||
|
qr_scan_content.clear_state();
|
||||||
|
|
||||||
|
// Setup value to finalization input field.
|
||||||
|
self.finalize_edit = result.text();
|
||||||
|
self.on_finalization_input_change(tx, wallet, modal, cb);
|
||||||
|
|
||||||
|
modal.enable_closing();
|
||||||
|
self.qr_scan_content = None;
|
||||||
|
} else {
|
||||||
|
qr_scan_content.ui(ui, cb);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let amount = amount_to_hr_string(tx.amount, true);
|
||||||
|
|
||||||
|
// Draw Slatepack message description text.
|
||||||
|
ui.vertical_centered(|ui| {
|
||||||
|
if self.show_finalization {
|
||||||
|
let desc_text = if self.finalize_error {
|
||||||
|
t!("wallets.finalize_slatepack_err")
|
||||||
|
} else {
|
||||||
|
if tx.data.tx_type == TxLogEntryType::TxSent {
|
||||||
|
t!("wallets.parse_s2_slatepack_desc", "amount" => amount)
|
||||||
|
} else {
|
||||||
|
t!("wallets.parse_i2_slatepack_desc", "amount" => amount)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let desc_color = if self.finalize_error {
|
||||||
|
Colors::red()
|
||||||
|
} else {
|
||||||
|
Colors::gray()
|
||||||
|
};
|
||||||
|
ui.label(RichText::new(desc_text).size(16.0).color(desc_color));
|
||||||
|
} else {
|
||||||
|
let desc_text = if tx.can_finalize {
|
||||||
|
if tx.data.tx_type == TxLogEntryType::TxSent {
|
||||||
|
t!("wallets.send_request_desc", "amount" => amount)
|
||||||
|
} else {
|
||||||
|
t!("wallets.invoice_desc", "amount" => amount)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if tx.data.tx_type == TxLogEntryType::TxSent {
|
||||||
|
t!("wallets.parse_i1_slatepack_desc", "amount" => amount)
|
||||||
|
} else {
|
||||||
|
t!("wallets.parse_s1_slatepack_desc", "amount" => amount)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
ui.label(RichText::new(desc_text).size(16.0).color(Colors::gray()));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ui.add_space(6.0);
|
||||||
|
|
||||||
|
// Setup message input value.
|
||||||
|
let message_edit = if self.show_finalization {
|
||||||
|
&mut self.finalize_edit
|
||||||
|
} else {
|
||||||
|
&mut self.response_edit
|
||||||
|
};
|
||||||
|
let message_before = message_edit.clone();
|
||||||
|
|
||||||
|
// Draw QR code content if requested.
|
||||||
|
if let Some(qr_content) = self.qr_code_content.as_mut() {
|
||||||
|
qr_content.ui(ui, cb);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw Slatepack message finalization input or request text.
|
||||||
|
ui.vertical_centered(|ui| {
|
||||||
|
let scroll_id = if self.show_finalization {
|
||||||
|
Id::from("tx_info_message_finalize")
|
||||||
|
} else {
|
||||||
|
Id::from("tx_info_message_request")
|
||||||
|
}.with(self.slate_id.clone().unwrap()).with(tx.data.id);
|
||||||
|
View::horizontal_line(ui, Colors::item_stroke());
|
||||||
|
ui.add_space(3.0);
|
||||||
|
ScrollArea::vertical()
|
||||||
|
.id_source(scroll_id)
|
||||||
|
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
|
||||||
|
.max_height(128.0)
|
||||||
|
.auto_shrink([false; 2])
|
||||||
|
.show(ui, |ui| {
|
||||||
|
ui.add_space(7.0);
|
||||||
|
let input_id = scroll_id.with("_input");
|
||||||
|
let resp = egui::TextEdit::multiline(message_edit)
|
||||||
|
.id(input_id)
|
||||||
|
.font(egui::TextStyle::Small)
|
||||||
|
.desired_rows(5)
|
||||||
|
.interactive(self.show_finalization && !self.finalizing)
|
||||||
|
.hint_text(SLATEPACK_MESSAGE_HINT)
|
||||||
|
.desired_width(f32::INFINITY)
|
||||||
|
.show(ui).response;
|
||||||
|
// Show soft keyboard on click.
|
||||||
|
if self.show_finalization && resp.clicked() {
|
||||||
|
resp.request_focus();
|
||||||
|
cb.show_keyboard();
|
||||||
|
}
|
||||||
|
if self.show_finalization && resp.has_focus() {
|
||||||
|
// Apply text from input on Android as temporary fix for egui.
|
||||||
|
View::on_soft_input(ui, input_id, message_edit);
|
||||||
|
}
|
||||||
|
ui.add_space(6.0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
ui.add_space(2.0);
|
||||||
|
View::horizontal_line(ui, Colors::item_stroke());
|
||||||
|
ui.add_space(8.0);
|
||||||
|
|
||||||
|
// Do not show buttons on finalization.
|
||||||
|
if self.finalizing {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup spacing between buttons.
|
||||||
|
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
|
||||||
|
|
||||||
|
if self.show_finalization {
|
||||||
|
ui.columns(2, |columns| {
|
||||||
|
columns[0].vertical_centered_justified(|ui| {
|
||||||
|
// Draw button to scan Slatepack message QR code.
|
||||||
|
let qr_text = format!("{} {}", SCAN, t!("scan"));
|
||||||
|
View::button(ui, qr_text, Colors::button(), || {
|
||||||
|
cb.hide_keyboard();
|
||||||
|
modal.disable_closing();
|
||||||
|
cb.start_camera();
|
||||||
|
self.qr_scan_content = Some(CameraContent::default());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
columns[1].vertical_centered_justified(|ui| {
|
||||||
|
// Draw button to paste data from clipboard.
|
||||||
|
let paste_text = format!("{} {}", CLIPBOARD_TEXT, t!("paste"));
|
||||||
|
View::button(ui, paste_text, Colors::button(), || {
|
||||||
|
self.finalize_edit = cb.get_string_from_buffer();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
ui.add_space(8.0);
|
||||||
|
ui.vertical_centered(|ui| {
|
||||||
|
if self.finalize_error {
|
||||||
|
// Draw button to clear message input.
|
||||||
|
let clear_text = format!("{} {}", BROOM, t!("clear"));
|
||||||
|
View::button(ui, clear_text, Colors::button(), || {
|
||||||
|
self.finalize_edit.clear();
|
||||||
|
self.finalize_error = false;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Draw button to choose file.
|
||||||
|
self.file_pick_button.ui(ui, cb, |text| {
|
||||||
|
self.finalize_edit = text;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Callback on finalization message input change.
|
||||||
|
if message_before != self.finalize_edit {
|
||||||
|
self.on_finalization_input_change(tx, wallet, modal, cb);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ui.columns(2, |columns| {
|
||||||
|
columns[0].vertical_centered_justified(|ui| {
|
||||||
|
// Draw button to show Slatepack message as QR code.
|
||||||
|
let qr_text = format!("{} {}", QR_CODE, t!("qr_code"));
|
||||||
|
View::button(ui, qr_text.clone(), Colors::button(), || {
|
||||||
|
cb.hide_keyboard();
|
||||||
|
let text = self.response_edit.clone();
|
||||||
|
self.qr_code_content = Some(QrCodeContent::new(text, true));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
columns[1].vertical_centered_justified(|ui| {
|
||||||
|
// Draw copy button.
|
||||||
|
let copy_text = format!("{} {}", COPY, t!("copy"));
|
||||||
|
View::button(ui, copy_text, Colors::button(), || {
|
||||||
|
cb.copy_string_to_buffer(self.response_edit.clone());
|
||||||
|
self.finalize_edit = "".to_string();
|
||||||
|
if tx.can_finalize {
|
||||||
|
self.show_finalization = true;
|
||||||
|
} else {
|
||||||
|
cb.hide_keyboard();
|
||||||
|
modal.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Show button to share response as file.
|
||||||
|
ui.add_space(8.0);
|
||||||
|
ui.vertical_centered(|ui| {
|
||||||
|
let share_text = format!("{} {}", FILE_TEXT, t!("share"));
|
||||||
|
View::colored_text_button(ui,
|
||||||
|
share_text,
|
||||||
|
Colors::blue(),
|
||||||
|
Colors::white_or_black(false), || {
|
||||||
|
if let Some((s, _)) = wallet.read_slate_by_tx(tx) {
|
||||||
|
let name = format!("{}.{}.slatepack", s.id, s.state);
|
||||||
|
let data = self.response_edit.as_bytes().to_vec();
|
||||||
|
cb.share_data(name, data).unwrap_or_default();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse Slatepack message on transaction finalization input change.
|
||||||
|
fn on_finalization_input_change(&mut self,
|
||||||
|
tx: &WalletTransaction,
|
||||||
|
wallet: &Wallet,
|
||||||
|
modal: &Modal,
|
||||||
|
cb: &dyn PlatformCallbacks) {
|
||||||
|
let message = &self.finalize_edit;
|
||||||
|
if message.is_empty() {
|
||||||
|
self.finalize_error = false;
|
||||||
|
} else {
|
||||||
|
// Parse input message to finalize.
|
||||||
|
if let Ok(slate) = wallet.parse_slatepack(message) {
|
||||||
|
let send = slate.state == SlateState::Standard2 &&
|
||||||
|
tx.data.tx_type == TxLogEntryType::TxSent;
|
||||||
|
let receive = slate.state == SlateState::Invoice2 &&
|
||||||
|
tx.data.tx_type == TxLogEntryType::TxReceived;
|
||||||
|
if Some(slate.id) == tx.data.tx_slate_id && (send || receive) {
|
||||||
|
let message = message.clone();
|
||||||
|
let wallet = wallet.clone();
|
||||||
|
let final_res = self.final_result.clone();
|
||||||
|
// Finalize transaction at separate thread.
|
||||||
|
cb.hide_keyboard();
|
||||||
|
self.finalizing = true;
|
||||||
|
modal.disable_closing();
|
||||||
|
thread::spawn(move || {
|
||||||
|
let res = wallet.finalize(&message);
|
||||||
|
let mut w_res = final_res.write();
|
||||||
|
*w_res = Some(res);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
self.finalize_error = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.finalize_error = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,6 +12,7 @@
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
|
use crate::gui::icons::{FOLDER_LOCK, FOLDER_OPEN, SPINNER, WARNING_CIRCLE};
|
||||||
use crate::gui::platform::PlatformCallbacks;
|
use crate::gui::platform::PlatformCallbacks;
|
||||||
use crate::wallet::Wallet;
|
use crate::wallet::Wallet;
|
||||||
|
|
||||||
|
@ -48,4 +49,39 @@ impl WalletTabType {
|
||||||
WalletTabType::Settings => t!("wallets.settings")
|
WalletTabType::Settings => t!("wallets.settings")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get wallet status text.
|
||||||
|
pub fn status_text(wallet: &Wallet) -> String {
|
||||||
|
if wallet.is_open() {
|
||||||
|
if wallet.sync_error() {
|
||||||
|
format!("{} {}", WARNING_CIRCLE, t!("error"))
|
||||||
|
} else if wallet.is_closing() {
|
||||||
|
format!("{} {}", SPINNER, t!("wallets.closing"))
|
||||||
|
} else if wallet.is_repairing() {
|
||||||
|
let repair_progress = wallet.repairing_progress();
|
||||||
|
if repair_progress == 0 {
|
||||||
|
format!("{} {}", SPINNER, t!("wallets.checking"))
|
||||||
|
} else {
|
||||||
|
format!("{} {}: {}%",
|
||||||
|
SPINNER,
|
||||||
|
t!("wallets.checking"),
|
||||||
|
repair_progress)
|
||||||
|
}
|
||||||
|
} else if wallet.syncing() {
|
||||||
|
let info_progress = wallet.info_sync_progress();
|
||||||
|
if info_progress == 100 || info_progress == 0 {
|
||||||
|
format!("{} {}", SPINNER, t!("wallets.loading"))
|
||||||
|
} else {
|
||||||
|
format!("{} {}: {}%",
|
||||||
|
SPINNER,
|
||||||
|
t!("wallets.loading"),
|
||||||
|
info_progress)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
format!("{} {}", FOLDER_OPEN, t!("wallets.unlocked"))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
format!("{} {}", FOLDER_LOCK, t!("wallets.locked"))
|
||||||
|
}
|
||||||
}
|
}
|
54
src/lib.rs
54
src/lib.rs
|
@ -17,6 +17,9 @@ extern crate rust_i18n;
|
||||||
|
|
||||||
use eframe::NativeOptions;
|
use eframe::NativeOptions;
|
||||||
use egui::{Context, Stroke};
|
use egui::{Context, Stroke};
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use parking_lot::RwLock;
|
||||||
|
|
||||||
#[cfg(target_os = "android")]
|
#[cfg(target_os = "android")]
|
||||||
use winit::platform::android::activity::AndroidApp;
|
use winit::platform::android::activity::AndroidApp;
|
||||||
|
@ -255,4 +258,55 @@ fn setup_i18n() {
|
||||||
rust_i18n::set_locale(AppConfig::DEFAULT_LOCALE);
|
rust_i18n::set_locale(AppConfig::DEFAULT_LOCALE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get data from deeplink or opened file.
|
||||||
|
pub fn consume_incoming_data() -> Option<String> {
|
||||||
|
let has_data = {
|
||||||
|
let r_data = INCOMING_DATA.read();
|
||||||
|
r_data.is_some()
|
||||||
|
};
|
||||||
|
if has_data {
|
||||||
|
// Clear data.
|
||||||
|
let mut w_data = INCOMING_DATA.write();
|
||||||
|
let data = w_data.clone();
|
||||||
|
*w_data = None;
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Provide data from deeplink or opened file.
|
||||||
|
pub fn on_data(data: String) {
|
||||||
|
let mut w_data = INCOMING_DATA.write();
|
||||||
|
*w_data = Some(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
/// Data provided from deeplink or opened file.
|
||||||
|
pub static ref INCOMING_DATA: Arc<RwLock<Option<String>>> = Arc::new(RwLock::new(None));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Callback from Java code with with passed data.
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern "C" fn Java_mw_gri_android_MainActivity_onData(
|
||||||
|
_env: jni::JNIEnv,
|
||||||
|
_class: jni::objects::JObject,
|
||||||
|
char: jni::sys::jstring
|
||||||
|
) {
|
||||||
|
unsafe {
|
||||||
|
let j_obj = jni::objects::JString::from_raw(char);
|
||||||
|
if let Ok(j_str) = _env.get_string_unchecked(j_obj.as_ref()) {
|
||||||
|
match j_str.to_str() {
|
||||||
|
Ok(str) => {
|
||||||
|
let mut w_path = INCOMING_DATA.write();
|
||||||
|
*w_path = Some(str.to_string());
|
||||||
|
}
|
||||||
|
Err(_) => {}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
160
src/main.rs
160
src/main.rs
|
@ -29,6 +29,18 @@ fn real_main() {
|
||||||
.parse_default_env()
|
.parse_default_env()
|
||||||
.init();
|
.init();
|
||||||
|
|
||||||
|
// Handle file path argument passing.
|
||||||
|
let args: Vec<_> = std::env::args().collect();
|
||||||
|
let mut data = None;
|
||||||
|
if args.len() > 1 {
|
||||||
|
let path = std::path::PathBuf::from(&args[1]);
|
||||||
|
let content = match std::fs::read_to_string(path) {
|
||||||
|
Ok(s) => Some(s),
|
||||||
|
Err(_) => Some(args[1].clone())
|
||||||
|
};
|
||||||
|
data = content
|
||||||
|
}
|
||||||
|
|
||||||
// Setup callback on panic crash.
|
// Setup callback on panic crash.
|
||||||
std::panic::set_hook(Box::new(|info| {
|
std::panic::set_hook(Box::new(|info| {
|
||||||
let backtrace = backtrace::Backtrace::new();
|
let backtrace = backtrace::Backtrace::new();
|
||||||
|
@ -41,7 +53,7 @@ fn real_main() {
|
||||||
// Save backtrace to file.
|
// Save backtrace to file.
|
||||||
let log = grim::Settings::crash_report_path();
|
let log = grim::Settings::crash_report_path();
|
||||||
if log.exists() {
|
if log.exists() {
|
||||||
std::fs::remove_file(log.clone()).unwrap();
|
let _ = std::fs::remove_file(log.clone());
|
||||||
}
|
}
|
||||||
std::fs::write(log, err.as_bytes()).unwrap();
|
std::fs::write(log, err.as_bytes()).unwrap();
|
||||||
// Setup flag to show crash after app restart.
|
// Setup flag to show crash after app restart.
|
||||||
|
@ -49,20 +61,28 @@ fn real_main() {
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Start GUI.
|
// Start GUI.
|
||||||
let _ = std::panic::catch_unwind(|| {
|
match std::panic::catch_unwind(|| {
|
||||||
start_desktop_gui();
|
if is_app_running(&data) {
|
||||||
});
|
return;
|
||||||
|
} else if let Some(data) = data {
|
||||||
|
grim::on_data(data);
|
||||||
|
}
|
||||||
|
let platform = grim::gui::platform::Desktop::new();
|
||||||
|
start_app_socket(platform.clone());
|
||||||
|
start_desktop_gui(platform);
|
||||||
|
}) {
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(e) => println!("{:?}", e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Start GUI with Desktop related setup.
|
/// Start GUI with Desktop related setup passing data from opening.
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
#[cfg(not(target_os = "android"))]
|
#[cfg(not(target_os = "android"))]
|
||||||
fn start_desktop_gui() {
|
fn start_desktop_gui(platform: grim::gui::platform::Desktop) {
|
||||||
use grim::AppConfig;
|
use grim::AppConfig;
|
||||||
use dark_light::Mode;
|
use dark_light::Mode;
|
||||||
|
|
||||||
let platform = grim::gui::platform::Desktop::default();
|
|
||||||
|
|
||||||
// Setup system theme if not set.
|
// Setup system theme if not set.
|
||||||
if let None = AppConfig::dark_theme() {
|
if let None = AppConfig::dark_theme() {
|
||||||
let dark = match dark_light::detect() {
|
let dark = match dark_light::detect() {
|
||||||
|
@ -73,12 +93,11 @@ fn start_desktop_gui() {
|
||||||
AppConfig::set_dark_theme(dark);
|
AppConfig::set_dark_theme(dark);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setup window size.
|
|
||||||
let (width, height) = AppConfig::window_size();
|
let (width, height) = AppConfig::window_size();
|
||||||
|
|
||||||
let mut viewport = egui::ViewportBuilder::default()
|
let mut viewport = egui::ViewportBuilder::default()
|
||||||
.with_min_inner_size([AppConfig::MIN_WIDTH, AppConfig::MIN_HEIGHT])
|
.with_min_inner_size([AppConfig::MIN_WIDTH, AppConfig::MIN_HEIGHT])
|
||||||
.with_inner_size([width, height]);
|
.with_inner_size([width, height]);
|
||||||
|
|
||||||
// Setup an icon.
|
// Setup an icon.
|
||||||
if let Ok(icon) = eframe::icon_data::from_png_bytes(include_bytes!("../img/icon.png")) {
|
if let Ok(icon) = eframe::icon_data::from_png_bytes(include_bytes!("../img/icon.png")) {
|
||||||
viewport = viewport.with_icon(std::sync::Arc::new(icon));
|
viewport = viewport.with_icon(std::sync::Arc::new(icon));
|
||||||
|
@ -90,6 +109,7 @@ fn start_desktop_gui() {
|
||||||
// Setup window decorations.
|
// Setup window decorations.
|
||||||
let is_mac = egui::os::OperatingSystem::from_target_os() == egui::os::OperatingSystem::Mac;
|
let is_mac = egui::os::OperatingSystem::from_target_os() == egui::os::OperatingSystem::Mac;
|
||||||
viewport = viewport
|
viewport = viewport
|
||||||
|
.with_window_level(egui::WindowLevel::Normal)
|
||||||
.with_fullsize_content_view(true)
|
.with_fullsize_content_view(true)
|
||||||
.with_title_shown(false)
|
.with_title_shown(false)
|
||||||
.with_titlebar_buttons_shown(false)
|
.with_titlebar_buttons_shown(false)
|
||||||
|
@ -110,7 +130,8 @@ fn start_desktop_gui() {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Start GUI.
|
// Start GUI.
|
||||||
match grim::start(options.clone(), grim::app_creator(grim::gui::App::new(platform.clone()))) {
|
let app = grim::gui::App::new(platform.clone());
|
||||||
|
match grim::start(options.clone(), grim::app_creator(app)) {
|
||||||
Ok(_) => {}
|
Ok(_) => {}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
if win {
|
if win {
|
||||||
|
@ -118,7 +139,9 @@ fn start_desktop_gui() {
|
||||||
}
|
}
|
||||||
// Start with another renderer on error.
|
// Start with another renderer on error.
|
||||||
options.renderer = eframe::Renderer::Glow;
|
options.renderer = eframe::Renderer::Glow;
|
||||||
match grim::start(options, grim::app_creator(grim::gui::App::new(platform))) {
|
|
||||||
|
let app = grim::gui::App::new(platform);
|
||||||
|
match grim::start(options, grim::app_creator(app)) {
|
||||||
Ok(_) => {}
|
Ok(_) => {}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
panic!("{}", e);
|
panic!("{}", e);
|
||||||
|
@ -126,4 +149,117 @@ fn start_desktop_gui() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if application is already running to pass data.
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[cfg(not(target_os = "android"))]
|
||||||
|
fn is_app_running(data: &Option<String>) -> bool {
|
||||||
|
use tor_rtcompat::BlockOn;
|
||||||
|
let runtime = tor_rtcompat::tokio::TokioNativeTlsRuntime::create().unwrap();
|
||||||
|
let res: Result<(), Box<dyn std::error::Error>> = runtime
|
||||||
|
.block_on(async {
|
||||||
|
use interprocess::local_socket::{
|
||||||
|
tokio::{prelude::*, Stream},
|
||||||
|
GenericFilePath, GenericNamespaced
|
||||||
|
};
|
||||||
|
use tokio::{
|
||||||
|
io::AsyncWriteExt,
|
||||||
|
};
|
||||||
|
|
||||||
|
let socket_path = grim::Settings::socket_path();
|
||||||
|
let name = if GenericNamespaced::is_supported() {
|
||||||
|
grim::Settings::SOCKET_NAME.to_ns_name::<GenericNamespaced>()?
|
||||||
|
} else {
|
||||||
|
socket_path.clone().to_fs_name::<GenericFilePath>()?
|
||||||
|
};
|
||||||
|
// Connect to running application socket.
|
||||||
|
let conn = Stream::connect(name).await?;
|
||||||
|
let data = data.clone().unwrap_or("".to_string());
|
||||||
|
if data.is_empty() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let (rec, mut sen) = conn.split();
|
||||||
|
|
||||||
|
// Send data to socket.
|
||||||
|
let _ = sen.write_all(data.as_bytes()).await;
|
||||||
|
|
||||||
|
drop((rec, sen));
|
||||||
|
Ok(())
|
||||||
|
});
|
||||||
|
return match res {
|
||||||
|
Ok(_) => true,
|
||||||
|
Err(_) => false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Start desktop socket that handles data for single application instance.
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[cfg(not(target_os = "android"))]
|
||||||
|
fn start_app_socket(platform: grim::gui::platform::Desktop) {
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
use tor_rtcompat::BlockOn;
|
||||||
|
let runtime = tor_rtcompat::tokio::TokioNativeTlsRuntime::create().unwrap();
|
||||||
|
let _: Result<_, _> = runtime
|
||||||
|
.block_on(async {
|
||||||
|
use interprocess::local_socket::{
|
||||||
|
tokio::{prelude::*, Stream},
|
||||||
|
GenericFilePath, GenericNamespaced, Listener, ListenerOptions,
|
||||||
|
};
|
||||||
|
use std::io;
|
||||||
|
use tokio::{
|
||||||
|
io::{AsyncBufReadExt, BufReader},
|
||||||
|
};
|
||||||
|
use grim::gui::platform::PlatformCallbacks;
|
||||||
|
|
||||||
|
// Handle incoming connection.
|
||||||
|
async fn handle_conn(conn: Stream)
|
||||||
|
-> io::Result<String> {
|
||||||
|
let mut read = BufReader::new(&conn);
|
||||||
|
let mut buffer = String::new();
|
||||||
|
// Read data.
|
||||||
|
let _ = read.read_line(&mut buffer).await;
|
||||||
|
Ok(buffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
let socket_path = grim::Settings::socket_path();
|
||||||
|
let name = if GenericNamespaced::is_supported() {
|
||||||
|
grim::Settings::SOCKET_NAME.to_ns_name::<GenericNamespaced>()?
|
||||||
|
} else {
|
||||||
|
socket_path.clone().to_fs_name::<GenericFilePath>()?
|
||||||
|
};
|
||||||
|
if socket_path.exists() {
|
||||||
|
let _ = std::fs::remove_file(socket_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create listener.
|
||||||
|
let opts = ListenerOptions::new().name(name);
|
||||||
|
let listener = match opts.create_tokio() {
|
||||||
|
Err(e) if e.kind() == io::ErrorKind::AddrInUse => {
|
||||||
|
eprintln!("Socket file is occupied.");
|
||||||
|
return Err::<Listener, io::Error>(e);
|
||||||
|
}
|
||||||
|
x => x?,
|
||||||
|
};
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let conn = match listener.accept().await {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(e) => {
|
||||||
|
println!("{:?}", e);
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// Handle connection.
|
||||||
|
let res = handle_conn(conn).await;
|
||||||
|
match res {
|
||||||
|
Ok(data) => {
|
||||||
|
grim::on_data(data);
|
||||||
|
platform.request_user_attention();
|
||||||
|
},
|
||||||
|
Err(_) => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
|
@ -48,9 +48,10 @@ pub struct Settings {
|
||||||
impl Settings {
|
impl Settings {
|
||||||
/// Main application directory name.
|
/// Main application directory name.
|
||||||
pub const MAIN_DIR_NAME: &'static str = ".grim";
|
pub const MAIN_DIR_NAME: &'static str = ".grim";
|
||||||
|
|
||||||
/// Crash report file name.
|
/// Crash report file name.
|
||||||
pub const CRASH_REPORT_FILE_NAME: &'static str = "crash.log";
|
pub const CRASH_REPORT_FILE_NAME: &'static str = "crash.log";
|
||||||
|
/// Application socket name.
|
||||||
|
pub const SOCKET_NAME: &'static str = "grim.sock";
|
||||||
|
|
||||||
/// Initialize settings with app and node configs.
|
/// Initialize settings with app and node configs.
|
||||||
fn init() -> Self {
|
fn init() -> Self {
|
||||||
|
@ -141,6 +142,13 @@ impl Settings {
|
||||||
path
|
path
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get desktop application socket path.
|
||||||
|
pub fn socket_path() -> PathBuf {
|
||||||
|
let mut socket_path = Self::base_path(None);
|
||||||
|
socket_path.push(Self::SOCKET_NAME);
|
||||||
|
socket_path
|
||||||
|
}
|
||||||
|
|
||||||
/// Get configuration file path from provided name and sub-directory if needed.
|
/// Get configuration file path from provided name and sub-directory if needed.
|
||||||
pub fn config_path(config_name: &str, sub_dir: Option<String>) -> PathBuf {
|
pub fn config_path(config_name: &str, sub_dir: Option<String>) -> PathBuf {
|
||||||
let mut path = Self::base_path(sub_dir);
|
let mut path = Self::base_path(sub_dir);
|
||||||
|
|
|
@ -18,7 +18,8 @@ use grin_wallet_libwallet::Error;
|
||||||
use crate::AppConfig;
|
use crate::AppConfig;
|
||||||
use crate::wallet::{Wallet, WalletConfig};
|
use crate::wallet::{Wallet, WalletConfig};
|
||||||
|
|
||||||
/// Wrapper for [`Wallet`] list.
|
/// [`Wallet`] list container.
|
||||||
|
#[derive(Clone)]
|
||||||
pub struct WalletList {
|
pub struct WalletList {
|
||||||
/// List of wallets for [`ChainTypes::Mainnet`].
|
/// List of wallets for [`ChainTypes::Mainnet`].
|
||||||
pub main_list: Vec<Wallet>,
|
pub main_list: Vec<Wallet>,
|
||||||
|
|
|
@ -158,14 +158,12 @@ pub struct WalletTransaction {
|
||||||
pub amount: u64,
|
pub amount: u64,
|
||||||
/// Flag to check if transaction is cancelling.
|
/// Flag to check if transaction is cancelling.
|
||||||
pub cancelling: bool,
|
pub cancelling: bool,
|
||||||
/// Flag to check if transaction is posting after finalization.
|
|
||||||
pub posting: bool,
|
|
||||||
/// Flag to check if transaction can be finalized based on Slatepack message state.
|
/// Flag to check if transaction can be finalized based on Slatepack message state.
|
||||||
pub can_finalize: bool,
|
pub can_finalize: bool,
|
||||||
|
/// Flag to check if transaction is finalizing.
|
||||||
|
pub finalizing: bool,
|
||||||
/// Block height when tx was confirmed.
|
/// Block height when tx was confirmed.
|
||||||
pub conf_height: Option<u64>,
|
pub conf_height: Option<u64>,
|
||||||
/// Block height when tx was reposted.
|
|
||||||
pub repost_height: Option<u64>,
|
|
||||||
/// Flag to check if tx was received after sync from node.
|
/// Flag to check if tx was received after sync from node.
|
||||||
pub from_node: bool,
|
pub from_node: bool,
|
||||||
}
|
}
|
||||||
|
@ -173,16 +171,8 @@ pub struct WalletTransaction {
|
||||||
impl WalletTransaction {
|
impl WalletTransaction {
|
||||||
/// Check if transaction can be cancelled.
|
/// Check if transaction can be cancelled.
|
||||||
pub fn can_cancel(&self) -> bool {
|
pub fn can_cancel(&self) -> bool {
|
||||||
self.from_node && !self.cancelling && !self.posting && !self.data.confirmed &&
|
self.from_node && !self.cancelling && !self.data.confirmed &&
|
||||||
self.data.tx_type != TxLogEntryType::TxReceivedCancelled
|
self.data.tx_type != TxLogEntryType::TxReceivedCancelled
|
||||||
&& self.data.tx_type != TxLogEntryType::TxSentCancelled
|
&& self.data.tx_type != TxLogEntryType::TxSentCancelled
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if transaction can be reposted.
|
|
||||||
pub fn can_repost(&self, data: &WalletData) -> bool {
|
|
||||||
let last_height = data.info.last_confirmed_height;
|
|
||||||
let min_conf = data.info.minimum_confirmations;
|
|
||||||
self.from_node && self.posting && self.repost_height.is_some() &&
|
|
||||||
last_height - self.repost_height.unwrap() > min_conf
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -437,8 +437,8 @@ impl Wallet {
|
||||||
// Mark wallet as not opened.
|
// Mark wallet as not opened.
|
||||||
wallet_close.closing.store(false, Ordering::Relaxed);
|
wallet_close.closing.store(false, Ordering::Relaxed);
|
||||||
wallet_close.is_open.store(false, Ordering::Relaxed);
|
wallet_close.is_open.store(false, Ordering::Relaxed);
|
||||||
// Wake up thread to exit.
|
// Start sync to exit from thread.
|
||||||
wallet_close.sync(true);
|
wallet_close.sync();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -464,14 +464,14 @@ impl Wallet {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sync wallet data.
|
// Refresh wallet info.
|
||||||
self.sync(false);
|
sync_wallet_data(&self, false);
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set active account from provided label.
|
/// Set active account from provided label.
|
||||||
pub fn set_active_account(&mut self, label: &String) -> Result<(), Error> {
|
pub fn set_active_account(&self, label: &String) -> Result<(), Error> {
|
||||||
let mut api = Owner::new(self.instance.clone().unwrap(), None);
|
let mut api = Owner::new(self.instance.clone().unwrap(), None);
|
||||||
controller::owner_single_use(None, None, Some(&mut api), |api, m| {
|
controller::owner_single_use(None, None, Some(&mut api), |api, m| {
|
||||||
api.set_active_account(m, label)?;
|
api.set_active_account(m, label)?;
|
||||||
|
@ -498,7 +498,7 @@ impl Wallet {
|
||||||
self.info_sync_progress.store(0, Ordering::Relaxed);
|
self.info_sync_progress.store(0, Ordering::Relaxed);
|
||||||
|
|
||||||
// Sync wallet data.
|
// Sync wallet data.
|
||||||
self.sync(false);
|
self.sync();
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -555,18 +555,11 @@ impl Wallet {
|
||||||
r_data.clone()
|
r_data.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sync wallet data from node or locally.
|
/// Sync wallet data from node at sync thread or locally synchronously.
|
||||||
pub fn sync(&self, from_node: bool) {
|
pub fn sync(&self) {
|
||||||
if from_node {
|
let thread_r = self.sync_thread.read();
|
||||||
let thread_r = self.sync_thread.read();
|
if let Some(thread) = thread_r.as_ref() {
|
||||||
if let Some(thread) = thread_r.as_ref() {
|
thread.unpark();
|
||||||
thread.unpark();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let wallet = self.clone();
|
|
||||||
thread::spawn(move || {
|
|
||||||
sync_wallet_data(&wallet, false);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -625,13 +618,7 @@ impl Wallet {
|
||||||
let mut slate = None;
|
let mut slate = None;
|
||||||
if let Some(slate_id) = tx.data.tx_slate_id {
|
if let Some(slate_id) = tx.data.tx_slate_id {
|
||||||
// Get slate state based on tx state and status.
|
// Get slate state based on tx state and status.
|
||||||
let state = if tx.posting {
|
let state = if !tx.data.confirmed && (tx.data.tx_type == TxLogEntryType::TxSent ||
|
||||||
if tx.data.tx_type == TxLogEntryType::TxSent {
|
|
||||||
Some(SlateState::Standard3)
|
|
||||||
} else {
|
|
||||||
Some(SlateState::Invoice3)
|
|
||||||
}
|
|
||||||
} else if !tx.data.confirmed && (tx.data.tx_type == TxLogEntryType::TxSent ||
|
|
||||||
tx.data.tx_type == TxLogEntryType::TxReceived) {
|
tx.data.tx_type == TxLogEntryType::TxReceived) {
|
||||||
if tx.can_finalize {
|
if tx.can_finalize {
|
||||||
if tx.data.tx_type == TxLogEntryType::TxSent {
|
if tx.data.tx_type == TxLogEntryType::TxSent {
|
||||||
|
@ -681,7 +668,7 @@ impl Wallet {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Initialize a transaction to send amount, return request for funds receiver.
|
/// Initialize a transaction to send amount, return request for funds receiver.
|
||||||
pub fn send(&self, amount: u64) -> Result<(Slate, String), Error> {
|
pub fn send(&self, amount: u64) -> Result<WalletTransaction, Error> {
|
||||||
let config = self.get_config();
|
let config = self.get_config();
|
||||||
let args = InitTxArgs {
|
let args = InitTxArgs {
|
||||||
src_acct_name: Some(config.account),
|
src_acct_name: Some(config.account),
|
||||||
|
@ -698,51 +685,35 @@ impl Wallet {
|
||||||
api.tx_lock_outputs(None, &slate)?;
|
api.tx_lock_outputs(None, &slate)?;
|
||||||
|
|
||||||
// Create Slatepack message response.
|
// Create Slatepack message response.
|
||||||
let message_resp = self.create_slatepack_message(&slate)?;
|
let _ = self.create_slatepack_message(&slate)?;
|
||||||
|
|
||||||
// Sync wallet info.
|
// Refresh wallet info.
|
||||||
self.sync(false);
|
sync_wallet_data(&self, false);
|
||||||
|
|
||||||
Ok((slate, message_resp))
|
let tx = self.tx_by_slate(&slate).ok_or(Error::GenericError("No tx found".to_string()))?;
|
||||||
|
Ok(tx)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Send amount to provided address with Tor transport.
|
/// Send amount to provided address with Tor transport.
|
||||||
pub async fn send_tor(&mut self, amount: u64, addr: &SlatepackAddress) -> Option<Slate> {
|
pub async fn send_tor(&mut self,
|
||||||
|
amount: u64,
|
||||||
|
addr: &SlatepackAddress) -> Result<WalletTransaction, Error> {
|
||||||
// Initialize transaction.
|
// Initialize transaction.
|
||||||
let send_res = self.send(amount);
|
let tx = self.send(amount)?;
|
||||||
|
let slate_res = self.read_slate_by_tx(&tx);
|
||||||
if send_res.is_err() {
|
if slate_res.is_none() {
|
||||||
return None;
|
return Err(Error::GenericError("Slate not found".to_string()));
|
||||||
}
|
}
|
||||||
let slate = send_res.unwrap().0;
|
let (slate, _) = slate_res.unwrap();
|
||||||
|
|
||||||
// Function to cancel initialized tx in case of error.
|
// Function to cancel initialized tx in case of error.
|
||||||
let cancel_tx = || {
|
let cancel_tx = || {
|
||||||
let instance = self.instance.clone().unwrap();
|
let instance = self.instance.clone().unwrap();
|
||||||
let id = slate.clone().id;
|
let id = slate.clone().id;
|
||||||
cancel_tx(instance, None, &None, None, Some(id.clone())).unwrap();
|
cancel_tx(instance, None, &None, None, Some(id.clone())).unwrap();
|
||||||
// Setup posting flag, and ability to finalize.
|
|
||||||
{
|
|
||||||
let mut w_data = self.data.write();
|
|
||||||
let mut data = w_data.clone().unwrap();
|
|
||||||
let txs = data.txs.clone().unwrap().iter_mut().map(|tx| {
|
|
||||||
if tx.data.tx_slate_id == Some(id) {
|
|
||||||
tx.cancelling = false;
|
|
||||||
tx.posting = false;
|
|
||||||
tx.can_finalize = false;
|
|
||||||
tx.data.tx_type = if tx.data.tx_type == TxLogEntryType::TxReceived {
|
|
||||||
TxLogEntryType::TxReceivedCancelled
|
|
||||||
} else {
|
|
||||||
TxLogEntryType::TxSentCancelled
|
|
||||||
};
|
|
||||||
}
|
|
||||||
tx.clone()
|
|
||||||
}).collect::<Vec<WalletTransaction>>();
|
|
||||||
data.txs = Some(txs);
|
|
||||||
*w_data = Some(data);
|
|
||||||
}
|
|
||||||
// Refresh wallet info to update statuses.
|
// Refresh wallet info to update statuses.
|
||||||
self.sync(false);
|
sync_wallet_data(&self, false);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Initialize parameters.
|
// Initialize parameters.
|
||||||
|
@ -764,17 +735,15 @@ impl Wallet {
|
||||||
let req_res = Tor::post(body, url).await;
|
let req_res = Tor::post(body, url).await;
|
||||||
if req_res.is_none() {
|
if req_res.is_none() {
|
||||||
cancel_tx();
|
cancel_tx();
|
||||||
return None;
|
return Err(Error::GenericError("Tor post error".to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse response and finalize transaction.
|
// Parse response.
|
||||||
let res: Value = serde_json::from_str(&req_res.unwrap()).unwrap();
|
let res: Value = serde_json::from_str(&req_res.unwrap()).unwrap();
|
||||||
if res["error"] != json!(null) {
|
if res["error"] != json!(null) {
|
||||||
cancel_tx();
|
cancel_tx();
|
||||||
return None;
|
return Err(Error::GenericError("Tx error".to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Slatepack message json value.
|
|
||||||
let slate_value = res["result"]["Ok"].clone();
|
let slate_value = res["result"]["Ok"].clone();
|
||||||
|
|
||||||
let mut ret_slate = None;
|
let mut ret_slate = None;
|
||||||
|
@ -788,7 +757,7 @@ impl Wallet {
|
||||||
// Save Slatepack message to file.
|
// Save Slatepack message to file.
|
||||||
let _ = self.create_slatepack_message(&slate).unwrap_or("".to_string());
|
let _ = self.create_slatepack_message(&slate).unwrap_or("".to_string());
|
||||||
// Post transaction to blockchain.
|
// Post transaction to blockchain.
|
||||||
let result = self.post(&slate, self.can_use_dandelion());
|
let result = self.post(&slate);
|
||||||
match result {
|
match result {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -798,21 +767,25 @@ impl Wallet {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Err(Error::GenericError("TX finalization error".to_string()))
|
Err(Error::GenericError("Tx finalization error".to_string()))
|
||||||
};
|
};
|
||||||
}).unwrap();
|
})?;
|
||||||
}
|
}
|
||||||
Err(_) => {}
|
Err(_) => {}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Cancel transaction on error.
|
||||||
if ret_slate.is_none() {
|
if ret_slate.is_none() {
|
||||||
cancel_tx();
|
cancel_tx();
|
||||||
|
return Err(Error::GenericError("Tx error".to_string()));
|
||||||
}
|
}
|
||||||
ret_slate
|
let tx = self.tx_by_slate(ret_slate.as_ref().unwrap())
|
||||||
|
.ok_or(Error::GenericError("No tx found".to_string()))?;
|
||||||
|
Ok(tx)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Initialize an invoice transaction to receive amount, return request for funds sender.
|
/// Initialize an invoice transaction to receive amount, return request for funds sender.
|
||||||
pub fn issue_invoice(&self, amount: u64) -> Result<(Slate, String), Error> {
|
pub fn issue_invoice(&self, amount: u64) -> Result<WalletTransaction, Error> {
|
||||||
let args = IssueInvoiceTxArgs {
|
let args = IssueInvoiceTxArgs {
|
||||||
dest_acct_name: None,
|
dest_acct_name: None,
|
||||||
amount,
|
amount,
|
||||||
|
@ -822,16 +795,17 @@ impl Wallet {
|
||||||
let slate = api.issue_invoice_tx(None, args)?;
|
let slate = api.issue_invoice_tx(None, args)?;
|
||||||
|
|
||||||
// Create Slatepack message response.
|
// Create Slatepack message response.
|
||||||
let response = self.create_slatepack_message(&slate.clone())?;
|
let _ = self.create_slatepack_message(&slate)?;
|
||||||
|
|
||||||
// Sync wallet info.
|
// Refresh wallet info.
|
||||||
self.sync(false);
|
sync_wallet_data(&self, false);
|
||||||
|
|
||||||
Ok((slate, response))
|
let tx = self.tx_by_slate(&slate).ok_or(Error::GenericError("No tx found".to_string()))?;
|
||||||
|
Ok(tx)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handle message from the invoice issuer to send founds, return response for funds receiver.
|
/// Handle message from the invoice issuer to send founds, return response for funds receiver.
|
||||||
pub fn pay(&self, message: &String) -> Result<String, Error> {
|
pub fn pay(&self, message: &String) -> Result<WalletTransaction, Error> {
|
||||||
if let Ok(slate) = self.parse_slatepack(message) {
|
if let Ok(slate) = self.parse_slatepack(message) {
|
||||||
let config = self.get_config();
|
let config = self.get_config();
|
||||||
let args = InitTxArgs {
|
let args = InitTxArgs {
|
||||||
|
@ -846,19 +820,19 @@ impl Wallet {
|
||||||
api.tx_lock_outputs(None, &slate)?;
|
api.tx_lock_outputs(None, &slate)?;
|
||||||
|
|
||||||
// Create Slatepack message response.
|
// Create Slatepack message response.
|
||||||
let response = self.create_slatepack_message(&slate)?;
|
let _ = self.create_slatepack_message(&slate)?;
|
||||||
|
|
||||||
// Sync wallet info.
|
// Refresh wallet info.
|
||||||
self.sync(false);
|
sync_wallet_data(&self, false);
|
||||||
|
|
||||||
Ok(response)
|
Ok(self.tx_by_slate(&slate).ok_or(Error::GenericError("No tx found".to_string()))?)
|
||||||
} else {
|
} else {
|
||||||
Err(Error::SlatepackDeser("Slatepack parsing error".to_string()))
|
Err(Error::SlatepackDeser("Slatepack parsing error".to_string()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handle message to receive funds, return response to sender.
|
/// Handle message to receive funds, return response to sender.
|
||||||
pub fn receive(&self, message: &String) -> Result<String, Error> {
|
pub fn receive(&self, message: &String) -> Result<WalletTransaction, Error> {
|
||||||
if let Ok(mut slate) = self.parse_slatepack(message) {
|
if let Ok(mut slate) = self.parse_slatepack(message) {
|
||||||
let api = Owner::new(self.instance.clone().unwrap(), None);
|
let api = Owner::new(self.instance.clone().unwrap(), None);
|
||||||
controller::foreign_single_use(api.wallet_inst.clone(), None, |api| {
|
controller::foreign_single_use(api.wallet_inst.clone(), None, |api| {
|
||||||
|
@ -866,61 +840,47 @@ impl Wallet {
|
||||||
Ok(())
|
Ok(())
|
||||||
})?;
|
})?;
|
||||||
// Create Slatepack message response.
|
// Create Slatepack message response.
|
||||||
let response = self.create_slatepack_message(&slate)?;
|
let _ = self.create_slatepack_message(&slate)?;
|
||||||
|
|
||||||
// Sync wallet info.
|
// Refresh wallet info.
|
||||||
self.sync(false);
|
sync_wallet_data(&self, false);
|
||||||
|
|
||||||
Ok(response)
|
Ok(self.tx_by_slate(&slate).ok_or(Error::GenericError("No tx found".to_string()))?)
|
||||||
} else {
|
} else {
|
||||||
Err(Error::SlatepackDeser("Slatepack parsing error".to_string()))
|
Err(Error::SlatepackDeser("Slatepack parsing error".to_string()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Finalize transaction from provided message as sender or invoice issuer with Dandelion.
|
/// Finalize transaction from provided message as sender or invoice issuer with Dandelion.
|
||||||
pub fn finalize(&self, message: &String, dandelion: bool) -> Result<Slate, Error> {
|
pub fn finalize(&self, message: &String) -> Result<WalletTransaction, Error> {
|
||||||
if let Ok(mut slate) = self.parse_slatepack(message) {
|
if let Ok(mut slate) = self.parse_slatepack(message) {
|
||||||
let api = Owner::new(self.instance.clone().unwrap(), None);
|
let api = Owner::new(self.instance.clone().unwrap(), None);
|
||||||
slate = api.finalize_tx(None, &slate)?;
|
slate = api.finalize_tx(None, &slate)?;
|
||||||
// Save Slatepack message to file.
|
// Save Slatepack message to file.
|
||||||
let _ = self.create_slatepack_message(&slate)?;
|
let _ = self.create_slatepack_message(&slate)?;
|
||||||
|
|
||||||
// Post transaction to blockchain.
|
// Post transaction to blockchain.
|
||||||
let _ = self.post(&slate, dandelion);
|
let tx = self.post(&slate)?;
|
||||||
Ok(slate)
|
|
||||||
|
// Refresh wallet info.
|
||||||
|
sync_wallet_data(&self, false);
|
||||||
|
|
||||||
|
Ok(tx)
|
||||||
} else {
|
} else {
|
||||||
Err(Error::SlatepackDeser("Slatepack parsing error".to_string()))
|
Err(Error::SlatepackDeser("Slatepack parsing error".to_string()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Post transaction to blockchain.
|
/// Post transaction to blockchain.
|
||||||
pub fn post(&self, slate: &Slate, dandelion: bool) -> Result<(), Error> {
|
fn post(&self, slate: &Slate) -> Result<WalletTransaction, Error> {
|
||||||
// Post transaction to blockchain.
|
// Post transaction to blockchain.
|
||||||
let api = Owner::new(self.instance.clone().unwrap(), None);
|
let api = Owner::new(self.instance.clone().unwrap(), None);
|
||||||
api.post_tx(None, slate, dandelion)?;
|
api.post_tx(None, slate, self.can_use_dandelion())?;
|
||||||
// Setup transaction repost height, posting flag and ability to finalize.
|
|
||||||
let mut slate = slate.clone();
|
// Refresh wallet info.
|
||||||
if slate.state == SlateState::Invoice2 {
|
sync_wallet_data(&self, false);
|
||||||
slate.state = SlateState::Invoice3
|
|
||||||
} else if slate.state == SlateState::Standard2 {
|
Ok(self.tx_by_slate(&slate).ok_or(Error::GenericError("No tx found".to_string()))?)
|
||||||
slate.state = SlateState::Standard3
|
|
||||||
};
|
|
||||||
if let Some(tx) = self.tx_by_slate(&slate) {
|
|
||||||
let mut w_data = self.data.write();
|
|
||||||
let mut data = w_data.clone().unwrap();
|
|
||||||
let mut data_txs = data.txs.unwrap();
|
|
||||||
for t in &mut data_txs {
|
|
||||||
if t.data.id == tx.data.id {
|
|
||||||
t.repost_height = Some(data.info.last_confirmed_height);
|
|
||||||
t.posting = true;
|
|
||||||
t.can_finalize = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
data.txs = Some(data_txs);
|
|
||||||
*w_data = Some(data);
|
|
||||||
}
|
|
||||||
// Sync local wallet info.
|
|
||||||
self.sync(false);
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Cancel transaction.
|
/// Cancel transaction.
|
||||||
|
@ -948,27 +908,7 @@ impl Wallet {
|
||||||
}
|
}
|
||||||
let instance = wallet.instance.clone().unwrap();
|
let instance = wallet.instance.clone().unwrap();
|
||||||
let _ = cancel_tx(instance, None, &None, Some(id), None);
|
let _ = cancel_tx(instance, None, &None, Some(id), None);
|
||||||
// Setup tx status, cancelling, posting flag, and ability to finalize.
|
|
||||||
{
|
|
||||||
let mut w_data = wallet.data.write();
|
|
||||||
let mut data = w_data.clone().unwrap();
|
|
||||||
let mut data_txs = data.txs.unwrap();
|
|
||||||
let txs = data_txs.iter_mut().map(|tx| {
|
|
||||||
if tx.data.id == id {
|
|
||||||
tx.cancelling = false;
|
|
||||||
tx.posting = false;
|
|
||||||
tx.can_finalize = false;
|
|
||||||
tx.data.tx_type = if tx.data.tx_type == TxLogEntryType::TxReceived {
|
|
||||||
TxLogEntryType::TxReceivedCancelled
|
|
||||||
} else {
|
|
||||||
TxLogEntryType::TxSentCancelled
|
|
||||||
};
|
|
||||||
}
|
|
||||||
tx.clone()
|
|
||||||
}).collect::<Vec<WalletTransaction>>();
|
|
||||||
data.txs = Some(txs);
|
|
||||||
*w_data = Some(data);
|
|
||||||
}
|
|
||||||
// Refresh wallet info to update statuses.
|
// Refresh wallet info to update statuses.
|
||||||
sync_wallet_data(&wallet, false);
|
sync_wallet_data(&wallet, false);
|
||||||
});
|
});
|
||||||
|
@ -985,7 +925,7 @@ impl Wallet {
|
||||||
/// Initiate wallet repair by scanning its outputs.
|
/// Initiate wallet repair by scanning its outputs.
|
||||||
pub fn repair(&self) {
|
pub fn repair(&self) {
|
||||||
self.repair_needed.store(true, Ordering::Relaxed);
|
self.repair_needed.store(true, Ordering::Relaxed);
|
||||||
self.sync(true);
|
self.sync();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if wallet is repairing.
|
/// Check if wallet is repairing.
|
||||||
|
@ -1013,7 +953,7 @@ impl Wallet {
|
||||||
// Remove wallet db files.
|
// Remove wallet db files.
|
||||||
let _ = fs::remove_dir_all(wallet_delete.get_config().get_db_path());
|
let _ = fs::remove_dir_all(wallet_delete.get_config().get_db_path());
|
||||||
// Start sync to close thread.
|
// Start sync to close thread.
|
||||||
wallet_delete.sync(true);
|
wallet_delete.sync();
|
||||||
// Mark wallet to reopen.
|
// Mark wallet to reopen.
|
||||||
wallet_delete.set_reopen(reopen);
|
wallet_delete.set_reopen(reopen);
|
||||||
});
|
});
|
||||||
|
@ -1046,7 +986,7 @@ impl Wallet {
|
||||||
// Mark wallet as deleted.
|
// Mark wallet as deleted.
|
||||||
wallet_delete.deleted.store(true, Ordering::Relaxed);
|
wallet_delete.deleted.store(true, Ordering::Relaxed);
|
||||||
// Start sync to close thread.
|
// Start sync to close thread.
|
||||||
wallet_delete.sync(true);
|
wallet_delete.sync();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1268,7 +1208,7 @@ fn sync_wallet_data(wallet: &Wallet, from_node: bool) {
|
||||||
wallet.reset_sync_attempts();
|
wallet.reset_sync_attempts();
|
||||||
|
|
||||||
// Filter transactions for current account.
|
// Filter transactions for current account.
|
||||||
let filter_txs = txs.1.iter().map(|v| v.clone()).filter(|tx| {
|
let account_txs = txs.1.iter().map(|v| v.clone()).filter(|tx| {
|
||||||
match wallet.get_parent_key_id() {
|
match wallet.get_parent_key_id() {
|
||||||
Ok(key) => {
|
Ok(key) => {
|
||||||
tx.parent_key_id == key
|
tx.parent_key_id == key
|
||||||
|
@ -1286,7 +1226,7 @@ fn sync_wallet_data(wallet: &Wallet, from_node: bool) {
|
||||||
|
|
||||||
// Create wallet txs.
|
// Create wallet txs.
|
||||||
let mut new_txs: Vec<WalletTransaction> = vec![];
|
let mut new_txs: Vec<WalletTransaction> = vec![];
|
||||||
for tx in &filter_txs {
|
for tx in &account_txs {
|
||||||
// Setup transaction amount.
|
// Setup transaction amount.
|
||||||
let amount = if tx.amount_debited > tx.amount_credited {
|
let amount = if tx.amount_debited > tx.amount_credited {
|
||||||
tx.amount_debited - tx.amount_credited
|
tx.amount_debited - tx.amount_credited
|
||||||
|
@ -1294,54 +1234,36 @@ fn sync_wallet_data(wallet: &Wallet, from_node: bool) {
|
||||||
tx.amount_credited - tx.amount_debited
|
tx.amount_credited - tx.amount_debited
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Setup flag for ability to finalize transaction.
|
||||||
let unconfirmed_sent_or_received = tx.tx_slate_id.is_some() &&
|
let unconfirmed_sent_or_received = tx.tx_slate_id.is_some() &&
|
||||||
!tx.confirmed && (tx.tx_type == TxLogEntryType::TxSent ||
|
!tx.confirmed && (tx.tx_type == TxLogEntryType::TxSent ||
|
||||||
tx.tx_type == TxLogEntryType::TxReceived);
|
tx.tx_type == TxLogEntryType::TxReceived);
|
||||||
|
let mut finalizing = false;
|
||||||
// Setup transaction posting status based on slate state.
|
let can_finalize = if unconfirmed_sent_or_received {
|
||||||
let posting = if unconfirmed_sent_or_received {
|
let initial_state = {
|
||||||
// Create slate to check existing file.
|
let mut slate = Slate::blank(1, false);
|
||||||
let is_invoice = tx.tx_type == TxLogEntryType::TxReceived;
|
slate.id = tx.tx_slate_id.unwrap();
|
||||||
let mut slate = Slate::blank(0, is_invoice);
|
slate.state = match tx.tx_type {
|
||||||
slate.id = tx.tx_slate_id.unwrap();
|
TxLogEntryType::TxReceived => SlateState::Invoice1,
|
||||||
slate.state = match is_invoice {
|
_ => SlateState::Standard1
|
||||||
true => SlateState::Invoice3,
|
};
|
||||||
_ => SlateState::Standard3
|
wallet.read_slatepack(&slate).is_some()
|
||||||
};
|
};
|
||||||
|
finalizing = {
|
||||||
// Setup posting status if we have other tx with same slate id.
|
let mut slate = Slate::blank(1, false);
|
||||||
let mut same_tx_posting = false;
|
slate.id = tx.tx_slate_id.unwrap();
|
||||||
for t in &mut new_txs {
|
slate.state = match tx.tx_type {
|
||||||
if t.data.tx_slate_id == tx.tx_slate_id &&
|
TxLogEntryType::TxReceived => SlateState::Invoice3,
|
||||||
tx.tx_type != t.data.tx_type {
|
_ => SlateState::Standard3
|
||||||
same_tx_posting = t.posting ||
|
};
|
||||||
wallet.read_slatepack(&slate).is_some();
|
wallet.read_slatepack(&slate).is_some()
|
||||||
if same_tx_posting && !t.posting {
|
};
|
||||||
t.posting = true;
|
initial_state && !finalizing
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
same_tx_posting || wallet.read_slatepack(&slate).is_some()
|
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
};
|
};
|
||||||
|
|
||||||
// Setup flag for ability to finalize transaction.
|
// Setup confirmation and cancelling status.
|
||||||
let can_finalize = if !posting && unconfirmed_sent_or_received {
|
|
||||||
// Check existing file.
|
|
||||||
let mut slate = Slate::blank(1, false);
|
|
||||||
slate.id = tx.tx_slate_id.unwrap();
|
|
||||||
slate.state = match tx.tx_type {
|
|
||||||
TxLogEntryType::TxReceived => SlateState::Invoice1,
|
|
||||||
_ => SlateState::Standard1
|
|
||||||
};
|
|
||||||
wallet.read_slatepack(&slate).is_some()
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
};
|
|
||||||
|
|
||||||
// Setup confirmation, reposting height and cancelling status.
|
|
||||||
let mut conf_height = None;
|
let mut conf_height = None;
|
||||||
let mut setup_conf_height = |t: &TxLogEntry, current_empty: bool| -> bool {
|
let mut setup_conf_height = |t: &TxLogEntry, current_empty: bool| -> bool {
|
||||||
if current_empty && t.kernel_lookup_min_height.is_some() &&
|
if current_empty && t.kernel_lookup_min_height.is_some() &&
|
||||||
|
@ -1376,7 +1298,6 @@ fn sync_wallet_data(wallet: &Wallet, from_node: bool) {
|
||||||
false
|
false
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut repost_height = None;
|
|
||||||
let mut cancelling = false;
|
let mut cancelling = false;
|
||||||
if data_txs.is_empty() {
|
if data_txs.is_empty() {
|
||||||
setup_conf_height(tx, true);
|
setup_conf_height(tx, true);
|
||||||
|
@ -1387,7 +1308,6 @@ fn sync_wallet_data(wallet: &Wallet, from_node: bool) {
|
||||||
t.conf_height.unwrap() == 0) {
|
t.conf_height.unwrap() == 0) {
|
||||||
conf_height = t.conf_height;
|
conf_height = t.conf_height;
|
||||||
}
|
}
|
||||||
repost_height = t.repost_height;
|
|
||||||
if t.cancelling &&
|
if t.cancelling &&
|
||||||
tx.tx_type != TxLogEntryType::TxReceivedCancelled &&
|
tx.tx_type != TxLogEntryType::TxReceivedCancelled &&
|
||||||
tx.tx_type != TxLogEntryType::TxSentCancelled {
|
tx.tx_type != TxLogEntryType::TxSentCancelled {
|
||||||
|
@ -1403,10 +1323,9 @@ fn sync_wallet_data(wallet: &Wallet, from_node: bool) {
|
||||||
data: tx.clone(),
|
data: tx.clone(),
|
||||||
amount,
|
amount,
|
||||||
cancelling,
|
cancelling,
|
||||||
posting,
|
|
||||||
can_finalize,
|
can_finalize,
|
||||||
|
finalizing,
|
||||||
conf_height,
|
conf_height,
|
||||||
repost_height,
|
|
||||||
from_node: !fresh_sync || from_node
|
from_node: !fresh_sync || from_node
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
14
wix/main.wxs
14
wix/main.wxs
|
@ -16,8 +16,8 @@
|
||||||
AllowSameVersionUpgrades = "yes"
|
AllowSameVersionUpgrades = "yes"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Icon Id='ProductICO' SourceFile='wix\Product.ico'/>
|
<Icon Id='Product.ico' SourceFile='wix\Product.ico'/>
|
||||||
<Property Id='ARPPRODUCTICON' Value='ProductICO' />
|
<Property Id='ARPPRODUCTICON' Value='Product.ico' />
|
||||||
|
|
||||||
<Directory Id="TARGETDIR" Name="SourceDir">
|
<Directory Id="TARGETDIR" Name="SourceDir">
|
||||||
<Directory Id='$(var.PlatformProgramFilesFolder)'>
|
<Directory Id='$(var.PlatformProgramFilesFolder)'>
|
||||||
|
@ -28,7 +28,7 @@
|
||||||
<Component Id="ApplicationShortcutDesktop" Guid="14efa019-7ed7-4765-8263-fa5460f92495">
|
<Component Id="ApplicationShortcutDesktop" Guid="14efa019-7ed7-4765-8263-fa5460f92495">
|
||||||
<Shortcut Id="ApplicationDesktopShortcut"
|
<Shortcut Id="ApplicationDesktopShortcut"
|
||||||
Name="Grim"
|
Name="Grim"
|
||||||
Icon="ProductICO"
|
Icon="Product.ico"
|
||||||
Description="GUI for Grin"
|
Description="GUI for Grin"
|
||||||
Target="[APPLICATIONROOTDIRECTORY]grim.exe"
|
Target="[APPLICATIONROOTDIRECTORY]grim.exe"
|
||||||
WorkingDirectory="APPLICATIONROOTDIRECTORY"/>
|
WorkingDirectory="APPLICATIONROOTDIRECTORY"/>
|
||||||
|
@ -55,6 +55,12 @@
|
||||||
</Component>
|
</Component>
|
||||||
<Component Id="grim.exe" Guid="95444223-45BF-427A-85CA-61B035044305">
|
<Component Id="grim.exe" Guid="95444223-45BF-427A-85CA-61B035044305">
|
||||||
<File Id="grim.exe" Source="$(var.CargoTargetBinDir)\grim.exe" KeyPath="yes" Checksum="yes"/>
|
<File Id="grim.exe" Source="$(var.CargoTargetBinDir)\grim.exe" KeyPath="yes" Checksum="yes"/>
|
||||||
|
<File Id="slatepack.ico" Source="wix\Product.ico" />
|
||||||
|
<ProgId Id='grim.slatepack' Description='Grin Slatepack message' Icon='slatepack.ico'>
|
||||||
|
<Extension Id='slatepack' ContentType='text/plain'>
|
||||||
|
<Verb Id='open' Command='Open' Target='[APPLICATIONROOTDIRECTORY]grim.exe' Argument='%1' />
|
||||||
|
</Extension>
|
||||||
|
</ProgId>
|
||||||
</Component>
|
</Component>
|
||||||
</DirectoryRef>
|
</DirectoryRef>
|
||||||
|
|
||||||
|
@ -64,7 +70,7 @@
|
||||||
<Shortcut Id="ApplicationStartMenuShortcut"
|
<Shortcut Id="ApplicationStartMenuShortcut"
|
||||||
Name="Grim"
|
Name="Grim"
|
||||||
Description="Cross-platform GUI for Grin with focus on usability and availability to be used by anyone, anywhere."
|
Description="Cross-platform GUI for Grin with focus on usability and availability to be used by anyone, anywhere."
|
||||||
Icon="ProductICO"
|
Icon="Product.ico"
|
||||||
Target="[#grim.exe]"
|
Target="[#grim.exe]"
|
||||||
WorkingDirectory="APPLICATIONROOTDIRECTORY"/>
|
WorkingDirectory="APPLICATIONROOTDIRECTORY"/>
|
||||||
<RemoveFolder Id="ApplicationProgramsFolder" On="uninstall"/>
|
<RemoveFolder Id="ApplicationProgramsFolder" On="uninstall"/>
|
||||||
|
|
Loading…
Reference in a new issue