54 Commits

Author SHA1 Message Date
ardocrat 04bf5a5349 github: coreutils for macos 2024-09-20 21:46:17 +03:00
ardocrat 9cce52a7d9 github: fix sha256sum 2024-09-20 20:38:05 +03:00
ardocrat 51e0d87d27 github: fix release 2024-09-20 15:17:41 +03:00
ardocrat d6f7e2e976 github: release sha256sum 2024-09-20 15:15:18 +03:00
ardocrat 0bbf395a62 build: android warning fix 2024-09-20 15:03:56 +03:00
ardocrat 609d7ceb7a build: remove panic message dependency 2024-09-20 14:45:40 +03:00
ardocrat b91605864d github: fix macos release 2024-09-20 14:42:37 +03:00
ardocrat 7857b708c9 release: v0.2.0 2024-09-20 14:17:03 +03:00
ardocrat a0f85538e9 ui: tx modal height 2024-09-20 14:09:53 +03:00
ardocrat c52da4f479 wallet: accounts balance calculating optimization, payment proof support on send, selection_strategy_is_use_all 2024-09-20 13:56:25 +03:00
ardocrat af597df7b1 i18n: move confirmation word 2024-09-20 13:49:31 +03:00
ardocrat 2adb29f4ee ui: external connection check and ui repaint fix, tab button callback argument 2024-09-20 13:42:45 +03:00
ardocrat 2b83944f34 ui: show node error status on connection item 2024-09-20 11:10:05 +03:00
ardocrat 71e80f6df7 ui: reset node config from ui on error 2024-09-20 10:58:52 +03:00
ardocrat 0ead11ec6c tx: receiver address 2024-09-20 02:39:06 +03:00
ardocrat 3e249c5314 android: share file type 2024-09-20 00:16:12 +03:00
ardocrat bacc87945c messages: qr scan modal 2024-09-20 00:09:08 +03:00
ardocrat 2cfd428c4c ui: do not clear qr state 2024-09-19 21:39:59 +03:00
ardocrat c155deedb5 wallet: qr scan modal, connections content and default list, wallet creation and list refactoring, tx height 2024-09-19 15:56:53 +03:00
Ardocrat 3bc8c407b4 Merge pull request #13 from ardocrat/slatepack_ext_file
Open .slatepack file with the app
2024-09-16 16:08:27 +00:00
ardocrat c3fae38d5c desktop: open camera check 2024-09-15 15:54:07 +03:00
ardocrat d6ec4213ab ui: ability to finalize tx only when wallet is loaded 2024-09-14 21:21:03 +03:00
ardocrat 150a0de1c4 android: always build with release-apk profile 2024-09-14 21:17:43 +03:00
ardocrat 7cedebc70e ui: qr scan and accounts modals module, parsing messages fix 2024-09-14 21:11:52 +03:00
ardocrat fe5aca6f0e build: remove debug from release profile 2024-09-14 16:08:40 +03:00
ardocrat 5d83710fed ui: dark colors fix 2024-09-14 16:02:20 +03:00
ardocrat 1431e307ee ui: separate wallet accounts modal 2024-09-14 15:21:08 +03:00
ardocrat 1934dc3377 desktop: args text 2024-09-14 15:04:11 +03:00
ardocrat 8af06d8860 build: android fix 2024-09-14 13:07:48 +03:00
ardocrat 9ea0da95b7 build: release sha256sum 2024-09-14 12:12:50 +03:00
ardocrat d39e2ec21e build: android signed release 2024-09-14 02:06:35 +03:00
ardocrat 68c9c9df04 build: local android release 2024-09-14 01:47:06 +03:00
ardocrat 6f7156ef17 github: android secrets 2024-09-13 22:31:28 +03:00
ardocrat 50638ff54e github: android keystore 2024-09-13 22:00:59 +03:00
ardocrat 8594279b98 android: java call result fixes 2024-09-13 21:08:14 +03:00
ardocrat 0205e01b3c build: macos fix 2024-09-13 19:51:33 +03:00
ardocrat 17545c1b7c macos: platform build 2024-09-13 18:57:09 +03:00
ardocrat bcf821c06a macos: initial file type association 2024-09-13 15:21:43 +03:00
ardocrat 34376d3490 build: fix macos 2024-09-13 14:56:04 +03:00
ardocrat 8ed2308340 macos: build, warn fix 2024-09-13 14:53:22 +03:00
ardocrat c73cd58eed platform: android file opening, better exit 2024-09-13 14:22:15 +03:00
ardocrat d78ec570b0 platform: passed data at lib, desktop user attention, check existing file on share at android 2024-09-12 21:27:37 +03:00
ardocrat dd45f7ce38 desktop: platform socket fix, file extension association for windows 2024-09-12 18:02:02 +03:00
ardocrat fb7312cb80 desktop: request window focus on data 2024-09-11 21:13:52 +03:00
ardocrat dbc28205e8 desktop: parse file content from argument on launch, single app instance, wallets selection and opening modals refactoring 2024-09-11 17:01:05 +03:00
ardocrat a3ed3bd234 build: linux release 2024-09-07 12:45:05 +03:00
ardocrat 21ecf200b8 wallet + ui: optimize sync after tx actions, remove tx repost, share message as file from tx modal, show tx info after tor sending and message creation or finalization, messages and transport modules refactoring, qr code text optimization, wallet dandelion setting, recovery phrase modal next step on enter 2024-09-07 00:11:17 +03:00
ardocrat c8bca08bdc txs: share message as file from modal, module refactoring 2024-08-15 23:09:42 +03:00
ardocrat 68bd2b81ec peers: fix config edit and load, default mainnet dnsseed 2024-08-13 02:31:38 +03:00
ardocrat 09cfb84b94 fix: ellipsized sync status text at connections 2024-08-12 18:30:10 +03:00
ardocrat 5c1ffb5636 build: push version 2024-08-10 12:15:40 +03:00
ardocrat 7f79cc0708 release: v0.1.3 2024-08-10 12:08:20 +03:00
ardocrat b0b4f9068a build: version release 2024-08-10 11:59:12 +03:00
ardocrat cb9e86750c mnemonic: words import and errors check refactoring 2024-08-10 02:35:42 +03:00
86 changed files with 6323 additions and 5628 deletions
-37
View File
@@ -2,43 +2,6 @@ name: Build
on: [push, pull_request]
jobs:
android:
name: Android Build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
cache: gradle
- name: Setup build
run: |
cargo install cargo-ndk
rustup target add aarch64-linux-android
rustup target add armv7-linux-androideabi
rustup target add x86_64-linux-android
- name: Setup Java build
run: |
chmod +x android/gradlew
echo "${{ secrets.ANDROID_RELEASE_KEYSTORE }}" > release.keystore.asc
gpg -d --passphrase "${{ secrets.ANDROID_RELEASE_SECRET }}" --batch release.keystore.asc > android/keystore
echo -e "storePassword=${{ secrets.ANDROID_PASS }}\nkeyPassword=${{ secrets.ANDROID_PASS }}\nkeyAlias=grim\nstoreFile=../keystore" > android/keystore.properties
- name: Build lib 1/2
continue-on-error: true
run: |
sed -i -e 's/"rlib"/"cdylib","rlib"/g' Cargo.toml
export CPPFLAGS="-DMDB_USE_ROBUST=0" && export CFLAGS="-DMDB_USE_ROBUST=0" && cargo ndk -t arm64-v8a build --profile release-apk
- name: Build lib 2/2
run: |
unset CPPFLAGS && unset CFLAGS && cargo ndk -t arm64-v8a -o android/app/src/main/jniLibs build --profile release-apk
sed -i -e 's/"cdylib","rlib"/"rlib"/g' Cargo.toml
- name: Build APK
working-directory: android
run: |
./gradlew assembleRelease
linux:
name: Linux Build
runs-on: ubuntu-latest
+16 -98
View File
@@ -6,89 +6,6 @@ on:
- "v*.*.*"
jobs:
android_release:
name: Android Release
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
cache: gradle
- name: Setup Rust build
run: |
cargo install cargo-ndk
rustup target add aarch64-linux-android
rustup target add armv7-linux-androideabi
rustup target add x86_64-linux-android
- name: Setup Java build
run: |
chmod +x android/gradlew
echo "${{ secrets.ANDROID_RELEASE_KEYSTORE }}" > release.keystore.asc
gpg -d --passphrase "${{ secrets.ANDROID_RELEASE_SECRET }}" --batch release.keystore.asc > android/keystore
echo -e "storePassword=${{ secrets.ANDROID_PASS }}\nkeyPassword=${{ secrets.ANDROID_PASS }}\nkeyAlias=grim\nstoreFile=../keystore" > android/keystore.properties
- name: Build lib ARMv8 1/2
continue-on-error: true
run: |
sed -i -e 's/"rlib"/"cdylib","rlib"/g' Cargo.toml
export CPPFLAGS="-DMDB_USE_ROBUST=0" && export CFLAGS="-DMDB_USE_ROBUST=0" && cargo ndk -t arm64-v8a build --profile release-apk
- name: Build lib ARMv8 2/2
run: |
unset CPPFLAGS && unset CFLAGS && cargo ndk -t arm64-v8a -o android/app/src/main/jniLibs build --profile release-apk
sed -i -e 's/"cdylib","rlib"/"rlib"/g' Cargo.toml
- name: Build 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:
name: Linux Release
runs-on: ubuntu-latest
@@ -120,16 +37,16 @@ jobs:
./appimagetool-x86_64.AppImage linux/Grim.AppDir target/x86_64-unknown-linux-gnu/release/grim-${{ github.ref_name }}-linux-x86_64.AppImage
- name: Checksum AppImage x86
working-directory: target/x86_64-unknown-linux-gnu/release
shell: pwsh
run: get-filehash -algorithm sha256 grim-${{ github.ref_name }}-linux-x86_64.AppImage | Format-List | Out-String | ForEach-Object { $_.Trim() } > grim-${{ github.ref_name }}-linux-x86_64-appimage-sha256sum.txt
shell: bash
run: sha256sum grim-${{ github.ref_name }}-linux-x86_64.AppImage > grim-${{ github.ref_name }}-linux-x86_64-appimage-sha256sum.txt
- name: AppImage ARM
run: |
cp target/aarch64-unknown-linux-gnu/release/grim linux/Grim.AppDir/AppRun
./appimagetool-x86_64.AppImage linux/Grim.AppDir target/aarch64-unknown-linux-gnu/release/grim-${{ github.ref_name }}-linux-arm.AppImage
- name: Checksum AppImage ARM
working-directory: target/aarch64-unknown-linux-gnu/release
shell: 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
shell: bash
run: sha256sum grim-${{ github.ref_name }}-linux-arm.AppImage > grim-${{ github.ref_name }}-linux-arm-appimage-sha256sum.txt
- name: Release
uses: softprops/action-gh-release@v1
with:
@@ -156,16 +73,16 @@ jobs:
dest: target/release/grim-${{ github.ref_name }}-win-x86_64.zip
- name: Checksum release
working-directory: target/release
shell: pwsh
run: get-filehash -algorithm sha256 grim-${{ github.ref_name }}-win-x86_64.zip | Format-List | Out-String | ForEach-Object { $_.Trim() } > grim-${{ github.ref_name }}-win-x86_64-sha256sum.txt
shell: bash
run: sha256sum grim-${{ github.ref_name }}-win-x86_64.zip > grim-${{ github.ref_name }}-win-x86_64-sha256sum.txt
- name: Install cargo-wix
run: cargo install cargo-wix
- name: Run cargo-wix
run: cargo wix -p grim -o ./target/wix/grim-${{ github.ref_name }}-win-x86_64.msi --nocapture
- name: Checksum msi
working-directory: target/wix
shell: 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
shell: bash
run: sha256sum grim-${{ github.ref_name }}-win-x86_64.msi > grim-${{ github.ref_name }}-win-x86_64-msi-sha256sum.txt
- name: Release
uses: softprops/action-gh-release@v1
with:
@@ -187,13 +104,14 @@ jobs:
uses: goto-bus-stop/setup-zig@v2
with:
version: 0.12.1
- name: Install coreutils
run: brew install coreutils
- name: Install cargo-zigbuild
run: cargo install cargo-zigbuild
- name: Release x86
run: |
rustup target add x86_64-apple-darwin
cargo zigbuild --release --target x86_64-apple-darwin
mkdir macos/Grim.app/Contents/MacOS
yes | cp -rf target/x86_64-apple-darwin/release/grim macos/Grim.app/Contents/MacOS
- name: Archive x86
run: |
@@ -203,8 +121,8 @@ jobs:
cd ..
- name: Checksum Release x86
working-directory: target/x86_64-apple-darwin/release
shell: pwsh
run: get-filehash -algorithm sha256 grim-${{ github.ref_name }}-macos-x86_64.zip | Format-List | Out-String | ForEach-Object { $_.Trim() } > grim-${{ github.ref_name }}-macos-x86_64-sha256sum.txt
shell: bash
run: sha256sum grim-${{ github.ref_name }}-macos-x86_64.zip > grim-${{ github.ref_name }}-macos-x86_64-sha256sum.txt
- name: Release ARM
run: |
rustup target add aarch64-apple-darwin
@@ -218,8 +136,8 @@ jobs:
cd ..
- name: Checksum Release ARM
working-directory: target/aarch64-apple-darwin/release
shell: pwsh
run: get-filehash -algorithm sha256 grim-${{ github.ref_name }}-macos-arm.zip | Format-List | Out-String | ForEach-Object { $_.Trim() } > grim-${{ github.ref_name }}-macos-arm-sha256sum.txt
shell: bash
run: sha256sum grim-${{ github.ref_name }}-macos-arm.zip > grim-${{ github.ref_name }}-macos-arm-sha256sum.txt
- name: Release Universal
run: |
rustup target add aarch64-apple-darwin
@@ -234,8 +152,8 @@ jobs:
cd ..
- name: Checksum Release Universal
working-directory: target/universal2-apple-darwin/release
shell: pwsh
run: get-filehash -algorithm sha256 grim-${{ github.ref_name }}-macos-universal.zip | Format-List | Out-String | ForEach-Object { $_.Trim() } > grim-${{ github.ref_name }}-macos-universal-sha256sum.txt
shell: bash
run: sha256sum grim-${{ github.ref_name }}-macos-universal.zip > grim-${{ github.ref_name }}-macos-universal-sha256sum.txt
- name: Release
uses: softprops/action-gh-release@v1
with:
-1
View File
@@ -13,7 +13,6 @@ android/keystore.properties
target
.cargo/
app/src/main/jniLibs
macos/Grim.app/Contents/MacOS/grim
macos/cert.pem
linux/Grim.AppDir/AppRun
.intentionally-empty-file.o
Generated
+29 -8
View File
@@ -2483,6 +2483,12 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0688c2a7f92e427f44895cd63841bff7b29f8d7a1648b9e7e07a4a365b2e1257"
[[package]]
name = "doctest-file"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aac81fa3e28d21450aa4d2ac065992ba96a1d7303efbce51a95f4fd175b67562"
[[package]]
name = "document-features"
version = "0.2.8"
@@ -3796,7 +3802,7 @@ dependencies = [
[[package]]
name = "grim"
version = "0.1.2"
version = "0.2.0"
dependencies = [
"android-activity 0.6.0",
"android_logger",
@@ -3833,6 +3839,7 @@ dependencies = [
"hyper 0.14.29",
"hyper-tls 0.5.0",
"image 0.25.1",
"interprocess",
"jni",
"lazy_static",
"local-ip-address",
@@ -3840,7 +3847,6 @@ dependencies = [
"nokhwa",
"openpnp_capture_sys",
"openssl-sys",
"panic-message",
"parking_lot 0.12.3",
"qrcode",
"qrcodegen",
@@ -4976,6 +4982,21 @@ dependencies = [
"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]]
name = "intl-memoizer"
version = "0.5.2"
@@ -6565,12 +6586,6 @@ dependencies = [
"sha2 0.10.8",
]
[[package]]
name = "panic-message"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384e52fd8fbd4cbe3c317e8216260c21a0f9134de108cea8a4dd4e7e152c472d"
[[package]]
name = "parking"
version = "2.2.0"
@@ -7432,6 +7447,12 @@ dependencies = [
"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]]
name = "redox_syscall"
version = "0.1.57"
+2 -5
View File
@@ -1,6 +1,6 @@
[package]
name = "grim"
version = "0.1.2"
version = "0.2.0"
authors = ["Ardocrat <ardocrat@proton.me>"]
description = "Cross-platform GUI for Grin with focus on usability and availability to be used by anyone, anywhere."
license = "Apache-2.0"
@@ -16,9 +16,6 @@ path = "src/main.rs"
name="grim"
crate-type = ["rlib"]
[profile.release]
debug = 1
[profile.release-apk]
inherits = "release"
strip = true
@@ -55,7 +52,6 @@ rust-i18n = "2.3.1"
## other
backtrace = "0.3"
panic-message = "0.3.0"
thiserror = "1.0.58"
futures = "0.3"
dirs = "5.0.1"
@@ -119,6 +115,7 @@ eframe = { version = "0.28.1", features = ["wgpu", "glow"] }
arboard = "3.2.0"
rfd = "0.14.1"
dark-light = "1.1.1"
interprocess = { version = "2.2.1", features = ["tokio"] }
[target.'cfg(target_os = "android")'.dependencies]
android_logger = "0.13.1"
+20 -12
View File
@@ -2,10 +2,6 @@ plugins {
id 'com.android.application'
}
def keystorePropertiesFile = rootProject.file("keystore.properties")
def keystoreProperties = new Properties()
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
android {
compileSdk 33
ndkVersion '26.0.10792818'
@@ -15,23 +11,35 @@ android {
minSdk 24
targetSdk 33
versionCode 3
versionName "0.1.2"
versionName "0.2.0"
}
signingConfigs {
release {
keyAlias keystoreProperties['keyAlias']
keyPassword keystoreProperties['keyPassword']
storeFile file(keystoreProperties['storeFile'])
storePassword keystoreProperties['storePassword']
def keystorePropertiesFile = rootProject.file("keystore.properties")
def keystoreProperties = new Properties()
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
signingConfigs {
release {
keyAlias keystoreProperties['keyAlias']
keyPassword keystoreProperties['keyPassword']
storeFile file(keystoreProperties['storeFile'])
storePassword keystoreProperties['storePassword']
}
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.release
}
if (keystorePropertiesFile.exists()) {
signedRelease {
initWith release
signingConfig signingConfigs.release
}
}
debug {
minifyEnabled false
+21 -4
View File
@@ -1,15 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
<manifest xmlns:tools="http://schemas.android.com/tools" xmlns:android="http://schemas.android.com/apk/res/android"
>
<uses-feature android:name="android.hardware.camera" android:required="false"/>
<uses-permission android:name="android.permission.EXPAND_STATUS_BAR" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" tools:ignore="ScopedStorage"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" tools:ignore="ScopedStorage"/>
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" tools:ignore="ScopedStorage"/>
<application
android:hardwareAccelerated="true"
@@ -18,7 +20,6 @@
android:icon="@mipmap/ic_launcher"
android:label="Grim"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Main">
<receiver android:name=".NotificationActionsReceiver"/>
@@ -44,6 +45,22 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter android:scheme="http" tools:ignore="AppLinkUrlError">
<action android:name="android.intent.action.SEND" />
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/*" />
</intent-filter>
<intent-filter android:scheme="http" tools:ignore="AppLinkUrlError">
<action android:name="android.intent.action.SEND" />
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="application/*" />
</intent-filter>
<meta-data android:name="android.app.lib_name" android:value="grim" />
</activity>
<service android:name=".BackgroundService" android:stopWithTask="true" />
@@ -152,13 +152,17 @@ public class BackgroundService extends Service {
// Show notification with sync status.
Intent i = getPackageManager().getLaunchIntentForPackage(this.getPackageName());
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, i, PendingIntent.FLAG_IMMUTABLE);
mNotificationBuilder = new NotificationCompat.Builder(this, TAG)
.setContentTitle(this.getSyncTitle())
.setContentText(this.getSyncStatusText())
.setStyle(new NotificationCompat.BigTextStyle().bigText(this.getSyncStatusText()))
.setSmallIcon(R.drawable.ic_stat_name)
.setPriority(NotificationCompat.PRIORITY_MAX)
.setContentIntent(pendingIntent);
try {
mNotificationBuilder = new NotificationCompat.Builder(this, TAG)
.setContentTitle(this.getSyncTitle())
.setContentText(this.getSyncStatusText())
.setStyle(new NotificationCompat.BigTextStyle().bigText(this.getSyncStatusText()))
.setSmallIcon(R.drawable.ic_stat_name)
.setPriority(NotificationCompat.PRIORITY_MAX)
.setContentIntent(pendingIntent);
} catch (UnsatisfiedLinkError e) {
return;
}
Notification notification = mNotificationBuilder.build();
// Start service at foreground state to prevent killing by system.
@@ -7,9 +7,9 @@ import android.content.*;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.*;
import android.os.Process;
import android.provider.Settings;
import android.system.ErrnoException;
import android.system.Os;
import android.util.Size;
@@ -51,8 +51,7 @@ public class MainActivity extends GameActivity {
@Override
public void onReceive(Context ctx, Intent i) {
if (i.getAction().equals(STOP_APP_ACTION)) {
onExit();
Process.killProcess(Process.myPid());
exit();
}
}
};
@@ -67,11 +66,19 @@ public class MainActivity extends GameActivity {
private ExecutorService mCameraExecutor = null;
private boolean mUseBackCamera = true;
private ActivityResultLauncher<Intent> mFilePickResultLauncher = null;
private ActivityResultLauncher<Intent> mFilePickResult = null;
private ActivityResultLauncher<Intent> mOpenFilePermissionsResult = null;
@SuppressLint("UnspecifiedRegisterReceiverFlag")
@Override
protected void onCreate(Bundle savedInstanceState) {
// Check if activity was launched to exclude from recent apps on exit.
if ((getIntent().getFlags() & Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS) != 0) {
super.onCreate(null);
finish();
return;
}
// Clear cache on start.
if (savedInstanceState == null) {
Utils.deleteDirectoryContent(new File(getExternalCacheDir().getPath()), false);
@@ -91,8 +98,21 @@ public class MainActivity extends GameActivity {
// Register receiver to finish activity from the BackgroundService.
registerReceiver(mBroadcastReceiver, new IntentFilter(STOP_APP_ACTION));
// Register file pick result launcher.
mFilePickResultLauncher = registerForActivityResult(
// Register associated file opening result.
mOpenFilePermissionsResult = registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(),
result -> {
if (Build.VERSION.SDK_INT >= 30) {
if (Environment.isExternalStorageManager()) {
onFile();
}
} else if (result.getResultCode() == RESULT_OK) {
onFile();
}
}
);
// Register file pick result.
mFilePickResult = registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(),
result -> {
int resultCode = result.getResultCode();
@@ -105,11 +125,11 @@ public class MainActivity extends GameActivity {
File file = new File(getExternalCacheDir(), name);
try (InputStream is = getContentResolver().openInputStream(uri);
OutputStream os = new FileOutputStream(file)) {
byte[] buffer = new byte[1024];
int length;
while ((length = is.read(buffer)) > 0) {
os.write(buffer, 0, length);
}
byte[] buffer = new byte[1024];
int length;
while ((length = is.read(buffer)) > 0) {
os.write(buffer, 0, length);
}
} catch (Exception e) {
e.printStackTrace();
}
@@ -124,7 +144,7 @@ public class MainActivity extends GameActivity {
// Listener for display insets (cutouts) to pass values into native code.
View content = getWindow().getDecorView().findViewById(android.R.id.content);
ViewCompat.setOnApplyWindowInsetsListener(content, (v, insets) -> {
// Setup cutouts values.
// Get display cutouts.
DisplayCutoutCompat dc = insets.getDisplayCutout();
int cutoutTop = 0;
int cutoutRight = 0;
@@ -140,7 +160,7 @@ public class MainActivity extends GameActivity {
// Get display insets.
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
// Setup values to pass into native code.
// Pass values into native code.
int[] values = new int[]{0, 0, 0, 0};
values[0] = Utils.pxToDp(Integer.max(cutoutTop, systemBars.top), this);
values[1] = Utils.pxToDp(Integer.max(cutoutRight, systemBars.right), this);
@@ -166,8 +186,61 @@ public class MainActivity extends GameActivity {
BackgroundService.start(this);
}
});
// Check if intent has data on launch.
if (savedInstanceState == null) {
onNewIntent(getIntent());
}
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
String action = intent.getAction();
// Check if file was open with the application.
if (action != null && action.equals(Intent.ACTION_VIEW)) {
Intent i = getIntent();
i.setData(intent.getData());
setIntent(i);
onFile();
}
}
// Callback when associated file was open.
private void onFile() {
Uri data = getIntent().getData();
if (data == null) {
return;
}
if (Build.VERSION.SDK_INT >= 30) {
if (!Environment.isExternalStorageManager()) {
Intent i = new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION);
mOpenFilePermissionsResult.launch(i);
return;
}
}
try {
ParcelFileDescriptor parcelFile = getContentResolver().openFileDescriptor(data, "r");
FileReader fileReader = new FileReader(parcelFile.getFileDescriptor());
BufferedReader reader = new BufferedReader(fileReader);
String line;
StringBuilder buff = new StringBuilder();
while ((line = reader.readLine()) != null) {
buff.append(line);
}
reader.close();
fileReader.close();
// Provide file content into native code.
onData(buff.toString());
} catch (Exception e) {
e.printStackTrace();
}
}
// Pass data into native code.
public native void onData(String data);
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
@@ -232,17 +305,17 @@ public class MainActivity extends GameActivity {
// Implemented into native code to handle key code BACK event.
public native void onBack();
// Actions on app exit.
private void onExit() {
unregisterReceiver(mBroadcastReceiver);
BackgroundService.stop(this);
// Called from native code to exit app.
public void exit() {
finishAndRemoveTask();
}
@Override
protected void onDestroy() {
onExit();
unregisterReceiver(mBroadcastReceiver);
BackgroundService.stop(this);
// Kill process after 3 seconds if app was terminated from recent apps to prevent app hanging.
// Kill process after 3 secs if app was terminated from recent apps to prevent app hang.
new Thread(() -> {
try {
onTermination();
@@ -253,9 +326,7 @@ public class MainActivity extends GameActivity {
}
}).start();
// Destroy an app and kill process.
super.onDestroy();
Process.killProcess(Process.myPid());
}
// Notify native code to stop activity (e.g. node) if app was terminated from recent apps.
@@ -298,18 +369,16 @@ public class MainActivity extends GameActivity {
// Called from native code to start camera.
public void startCamera() {
// Check permissions.
String notificationsPermission = Manifest.permission.CAMERA;
if (checkSelfPermission(notificationsPermission) != PackageManager.PERMISSION_GRANTED) {
requestPermissions(new String[] { notificationsPermission }, CAMERA_PERMISSION_CODE);
} else {
// Start .
if (mCameraProviderFuture == null) {
mCameraProviderFuture = ProcessCameraProvider.getInstance(this);
mCameraProviderFuture.addListener(() -> {
try {
mCameraProvider = mCameraProviderFuture.get();
// Launch camera.
// Start camera.
openCamera();
} catch (Exception e) {
View content = findViewById(android.R.id.content);
@@ -381,14 +450,14 @@ public class MainActivity extends GameActivity {
// Pass image from camera into native code.
public native void onCameraImage(byte[] buff, int rotation);
// Called from native code to share image from provided path.
public void shareImage(String path) {
// Called from native code to share data from provided path.
public void shareData(String path) {
File file = new File(path);
Uri uri = FileProvider.getUriForFile(this, "mw.gri.android.fileprovider", file);
Intent intent = new Intent(Intent.ACTION_SEND);
intent.putExtra(Intent.EXTRA_STREAM, uri);
intent.setType("image/*");
startActivity(Intent.createChooser(intent, "Share image"));
intent.setType("*/*");
startActivity(Intent.createChooser(intent, "Share data"));
}
// Called from native code to check if device is using dark theme.
@@ -402,7 +471,7 @@ public class MainActivity extends GameActivity {
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.setType("*/*");
try {
mFilePickResultLauncher.launch(Intent.createChooser(intent, "Pick file"));
mFilePickResult.launch(Intent.createChooser(intent, "Pick file"));
} catch (android.content.ActivityNotFoundException ex) {
onFilePick("");
}
+1 -1
View File
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<paths>
<external-cache-path name="images" path="images/" />
<external-cache-path name="share" path="share/" />
</paths>
+2 -1
View File
@@ -3,4 +3,5 @@ Name=Grim
Exec=grim
Icon=grim
Type=Application
Categories=Finance
Categories=Finance
MimeType=application/x-slatepack;text/plain;
+1 -3
View File
@@ -17,9 +17,7 @@ cd ..
[[ $2 == "x86_64" ]] && arch+=(x86_64-unknown-linux-gnu)
[[ $2 == "arm" ]] && arch+=(aarch64-unknown-linux-gnu)
# Start release build with zig linker for cross-compilation
cargo install cargo-zigbuild
cargo zigbuild --release --target ${arch}
cargo build --release --target ${arch}
# Create AppImage with https://github.com/AppImage/appimagetool
cp target/${arch}/release/grim linux/Grim.AppDir/AppRun
+1 -1
View File
@@ -28,6 +28,7 @@ light: Hell
choose_file: Datei auswählen
crash_report: Absturzbericht
crash_report_warning: Anwendung wurde beim letzten Mal unerwartet geschlossen, Sie können den Absturzbericht mit Entwicklern teilen.
confirmation: Bestätigung
wallets:
await_conf_amount: Erwarte Bestätigung
await_fin_amount: Warten auf die Fertigstellung
@@ -287,7 +288,6 @@ network_settings:
modal:
cancel: Abbrechen
save: Speichern
confirmation: Bestätigung
add: Hinzufügen
modal_exit:
description: Sind Sie sicher, dass Sie die Anwendung beenden wollen?
+1 -1
View File
@@ -28,6 +28,7 @@ light: Light
choose_file: Choose file
crash_report: Crash report
crash_report_warning: Application closed unexpectedly last time, you can share crash report with developers.
confirmation: Confirmation
wallets:
await_conf_amount: Awaiting confirmation
await_fin_amount: Awaiting finalization
@@ -287,7 +288,6 @@ network_settings:
modal:
cancel: Cancel
save: Save
confirmation: Confirmation
add: Add
modal_exit:
description: Are you sure you want to quit the application?
+1 -1
View File
@@ -28,6 +28,7 @@ light: Clair
choose_file: Choisir un fichier
crash_report: Rapport d'échec
crash_report_warning: L'application s'est fermée de manière inattendue la dernière fois, vous pouvez partager un rapport d'incident avec les développeurs.
confirmation: Confirmation
wallets:
await_conf_amount: En attente de confirmation
await_fin_amount: En attente de finalisation
@@ -287,7 +288,6 @@ network_settings:
modal:
cancel: Annuler
save: Sauvegarder
confirmation: Confirmation
add: Ajouter
modal_exit:
description: "Êtes-vous sûr de vouloir quitter l'application ?"
+1 -1
View File
@@ -28,6 +28,7 @@ light: Светлая
choose_file: Выбрать файл
crash_report: Отчёт о сбое
crash_report_warning: В прошлый раз приложение неожиданно закрылось, вы можете поделиться отчетом о сбое с разработчиками.
confirmation: Подтверждение
wallets:
await_conf_amount: Ожидает подтверждения
await_fin_amount: Ожидает завершения
@@ -287,7 +288,6 @@ network_settings:
modal:
cancel: Отмена
save: Сохранить
confirmation: Подтверждение
add: Добавить
modal_exit:
description: Вы уверены, что хотите выйти из приложения?
+1 -1
View File
@@ -28,6 +28,7 @@ light: Isik
choose_file: Dosya seçin
crash_report: Ariza Raporu
crash_report_warning: Uygulama beklenmedik bir sekilde kapandi son kez, kilitlenme raporunu gelistiricilerle paylasabilirsiniz.
confirmation: Onay
wallets:
await_conf_amount: Onay bekleniyor
await_fin_amount: Tamamlanma bekleniyor
@@ -287,7 +288,6 @@ network_settings:
modal:
cancel: Iptal
save: Kaydet
confirmation: Onay
add: Ekle
modal_exit:
description: Uygulamadan cikmak için exit, emin misiniz?
+28
View File
@@ -40,6 +40,34 @@
<key>CFBundleVersion</key>
<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>
<string>public.app-category.finance</string>
+1
View File
@@ -0,0 +1 @@
!.gitignore
+8 -13
View File
@@ -9,14 +9,13 @@ case $2 in
exit 1
esac
if [[ ! -v SDKROOT ]]; then
if [[ "$OSTYPE" != "darwin"* ]]; then
if [ -z ${SDKROOT+x} ]; then
echo "MacOS SDKROOT is not set"
exit 1
elif [[ -z "SDKROOT" ]]; then
echo "MacOS SDKROOT is set to the empty string"
exit 1
else
else
echo "Use MacOS SDK: ${SDKROOT}"
fi
fi
# Setup build directory
@@ -28,22 +27,18 @@ cd ..
rustup target add x86_64-apple-darwin
rustup target add aarch64-apple-darwin
rm -rf target/x86_64-apple-darwin
rm -rf target/aarch64-apple-darwin
[[ $2 == "x86_64" ]] && arch+=(x86_64-apple-darwin)
[[ $2 == "arm" ]] && arch+=(aarch64-apple-darwin)
[[ $2 == "universal" ]] && arch+=(universal2-apple-darwin)
[[ $2 == "universal" ]]; arch+=(universal2-apple-darwin)
# Start release build with zig linker for cross-compilation
# zig 0.12+ required
# Start release build with zig linker, requires zig 0.12.1
cargo install cargo-zigbuild
cargo zigbuild --release --target ${arch}
rm -rf .intentionally-empty-file.o
mkdir macos/Grim.app/Contents/MacOS
yes | cp -rf target/${arch}/release/grim macos/Grim.app/Contents/MacOS
### Sign .app resources on change:
# Sign .app resources on change:
#rcodesign generate-self-signed-certificate
#rcodesign sign --pem-file cert.pem macos/Grim.app
+88 -56
View File
@@ -1,81 +1,113 @@
#!/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
debug|release)
build|release)
;;
*)
printf "$usage"
exit 1
esac
case $2 in
v7|v8)
;;
*)
printf "$usage"
exit 1
esac
if [[ $1 == "build" ]]; then
case $2 in
v7|v8|x86)
;;
*)
printf "$usage"
exit 1
esac
fi
# Setup build directory
BASEDIR=$(cd $(dirname $0) && pwd)
cd ${BASEDIR}
cd ..
# Setup release argument
type=$1
[[ ${type} == "release" ]] && release_param="--profile release-apk"
# Setup platform argument
[[ $2 == "v7" ]] && arch+=(armeabi-v7a)
[[ $2 == "v8" ]] && arch+=(arm64-v8a)
# Setup platform path
[[ $2 == "v7" ]] && platform+=(armv7-linux-androideabi)
[[ $2 == "v8" ]] && platform+=(aarch64-linux-android)
# Install platform
[[ $2 == "v7" ]] && rustup target install armv7-linux-androideabi
[[ $2 == "v8" ]] && rustup target install aarch64-linux-android
# Build native code
# Install platforms and tools
rustup target add armv7-linux-androideabi
rustup target add aarch64-linux-android
rustup target add x86_64-linux-android
cargo install cargo-ndk
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
success=0
export CPPFLAGS="-DMDB_USE_ROBUST=0" && export CFLAGS="-DMDB_USE_ROBUST=0"
cargo ndk -t ${arch} build ${release_param}
unset CPPFLAGS && unset CFLAGS
cargo ndk -t ${arch} -o android/app/src/main/jniLibs build ${release_param}
if [ $? -eq 0 ]
then
success=1
fi
### Build native code
function build_lib() {
[[ $1 == "v7" ]] && arch=(armeabi-v7a)
[[ $1 == "v8" ]] && arch=(arm64-v8a)
[[ $1 == "x86" ]] && arch=(x86_64)
sed -i -e 's/"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
# Setup gradle argument
[[ $1 == "release" ]] && gradle_param+=(assembleRelease)
[[ $1 == "debug" ]] && gradle_param+=(build)
./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
[[ $1 == "release" ]] && apk_path+=(app/build/outputs/apk/release/app-release.apk)
[[ $1 == "debug" ]] && apk_path+=(app/build/outputs/apk/debug/app-debug.apk)
if [[ $1 == "" ]]; then
# Launch application at all connected devices.
for SERIAL in $(adb devices | grep -v List | cut -f 1);
do
adb -s $SERIAL install ${apk_path}
sleep 1s
adb -s $SERIAL shell am start -n mw.gri.android/.MainActivity;
done
else
# 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);
do
adb -s $SERIAL install ${apk_path}
sleep 1s
adb -s $SERIAL shell am start -n mw.gri.android/.MainActivity;
done
# Calculate checksum
checksum=grim-${version}-android-$1-sha256sum.txt
[[ $1 == "arm" ]] && checksum=grim-${version}-android-sha256sum.txt
rm -rf ${checksum}
sha256sum ${name} > ${checksum}
fi
cd ..
}
rm -rf android/app/src/main/jniLibs/*
if [[ $1 == "build" ]]; then
build_lib $2
[ $success -eq 1 ] && build_apk
else
rm -rf target/release-apk
rm -rf target/aarch64-linux-android
rm -rf target/x86_64-linux-android
rm -rf target/armv7-linux-androideabi
build_lib "v7"
[ $success -eq 1 ] && build_lib "v8"
[ $success -eq 1 ] && build_apk "arm"
rm -rf android/app/src/main/jniLibs/*
[ $success -eq 1 ] && build_lib "x86"
[ $success -eq 1 ] && build_apk "x86_64"
fi
+99
View File
@@ -0,0 +1,99 @@
#!/bin/bash
# Usage to bump version
# ./version.sh patch
# ./version.sh minor
# ./version.sh major
# Setup base directory
BASEDIR=$(cd $(dirname $0) && pwd)
cd ${BASEDIR}
cd ..
# Exit script if command fails or uninitialized variables used
set -euo pipefail
# ==================================
# Verify repo is clean
# ==================================
# List uncommitted changes and
# check if the output is not empty
if [ -n "$(git status --porcelain)" ]; then
# Print error message
printf "\nError: repo has uncommitted changes\n\n"
# Exit with error code
exit 1
fi
# ==================================
# Get latest version from git tags
# ==================================
# List git tags sorted lexicographically
# so version numbers sorted correctly
GIT_TAGS=$(git tag --sort=version:refname)
# Get last line of output which returns the
# last tag (most recent version)
GIT_TAG_LATEST=$(echo "$GIT_TAGS" | tail -n 1)
# If no tag found, default to v0.1.0
if [ -z "$GIT_TAG_LATEST" ]; then
GIT_TAG_LATEST="v0.1.0"
fi
# Strip prefix 'v' from the tag to easily increment
GIT_TAG_LATEST=$(echo "$GIT_TAG_LATEST" | sed 's/^v//')
# ==================================
# Increment version number
# ==================================
# Get version type from first argument passed to script
VERSION_TYPE="${1-}"
VERSION_NEXT=""
if [ "$VERSION_TYPE" = "patch" ]; then
# Increment patch version
VERSION_NEXT="$(echo "$GIT_TAG_LATEST" | awk -F. '{$NF++; print $1"."$2"."$NF}')"
elif [ "$VERSION_TYPE" = "minor" ]; then
# Increment minor version
VERSION_NEXT="$(echo "$GIT_TAG_LATEST" | awk -F. '{$2++; $3=0; print $1"."$2"."$3}')"
elif [ "$VERSION_TYPE" = "major" ]; then
# Increment major version
VERSION_NEXT="$(echo "$GIT_TAG_LATEST" | awk -F. '{$1++; $2=0; $3=0; print $1"."$2"."$3}')"
else
# Print error for unknown versioning type
printf "\nError: invalid VERSION_TYPE arg passed, must be 'patch', 'minor' or 'major'\n\n"
# Exit with error code
exit 1
fi
# ==================================
# Update Android build.gradle file
# and package version at Cargo.toml
# ==================================
# Update version in build.gradle
sed -i 's/versionName [0-9a-zA-Z -_]*/versionName "'"$VERSION_NEXT"'"/' android/app/build.gradle
# Update version in Cargo.toml
sed -i "s/^version = .*/version = \"$VERSION_NEXT\"/" Cargo.toml
# Update Cargo.lock as this changes when
# updating the version in your manifest
cargo update -p grim
# Commit the changes
git add .
git commit -m "release: v$VERSION_NEXT"
# ==================================
# Create git tag for new version
# ==================================
# Create a tag and push to master branch
git tag "v$VERSION_NEXT" master
git push origin master --follow-tags
+52 -17
View File
@@ -23,6 +23,7 @@ use crate::gui::Colors;
use crate::gui::icons::{ARROWS_IN, ARROWS_OUT, CARET_DOWN, MOON, SUN, X};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Content, TitlePanel, View};
use crate::wallet::ExternalConnection;
lazy_static! {
/// State to check if platform Back button was pressed.
@@ -32,25 +33,45 @@ lazy_static! {
/// Implements ui entry point and contains platform-specific callbacks.
pub struct App<Platform> {
/// Platform specific callbacks handler.
pub(crate) platform: Platform,
pub platform: Platform,
/// Main ui content.
/// Main content.
content: Content,
/// Last window resize direction.
resize_direction: Option<ResizeDirection>
resize_direction: Option<ResizeDirection>,
/// Flag to check if it's first draw.
first_draw: bool,
}
impl<Platform: PlatformCallbacks> App<Platform> {
pub fn new(platform: Platform) -> Self {
Self { platform, content: Content::default(), resize_direction: None }
Self {
platform,
content: Content::default(),
resize_direction: None,
first_draw: true
}
}
/// Draw application content.
pub fn ui(&mut self, ctx: &Context) {
if self.first_draw {
// Set platform context.
if View::is_desktop() {
self.platform.set_context(ctx);
}
// Check external connections availability.
ExternalConnection::check(None, ctx);
self.first_draw = false;
}
// Handle Esc keyboard key event and platform Back button key event.
let back_pressed = BACK_BUTTON_PRESSED.load(Ordering::Relaxed);
if ctx.input_mut(|i| i.consume_key(Modifiers::NONE, egui::Key::Escape)) || back_pressed {
if back_pressed || ctx.input_mut(|i| i.consume_key(Modifiers::NONE, egui::Key::Escape)) {
self.content.on_back();
if back_pressed {
BACK_BUTTON_PRESSED.store(false, Ordering::Relaxed);
@@ -59,8 +80,8 @@ impl<Platform: PlatformCallbacks> App<Platform> {
ctx.request_repaint();
}
// Handle Close event (on desktop).
if ctx.input(|i| i.viewport().close_requested()) {
// Handle Close event on desktop.
if View::is_desktop() && ctx.input(|i| i.viewport().close_requested()) {
if !self.content.exit_allowed {
ctx.send_viewport_cmd(ViewportCommand::CancelClose);
Content::show_exit_modal();
@@ -92,7 +113,20 @@ impl<Platform: PlatformCallbacks> App<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.
@@ -244,20 +278,21 @@ impl<Platform: PlatformCallbacks> App<Platform> {
// Paint the title.
let dual_wallets_panel =
ui.available_width() >= (Content::SIDE_PANEL_WIDTH * 3.0) + View::get_right_inset();
let wallet_panel_opened = self.content.wallets.wallet_panel_opened();
let hide_app_name = if dual_wallets_panel {
!wallet_panel_opened || (AppConfig::show_wallets_at_dual_panel() &&
self.content.wallets.showing_wallet() && !self.content.wallets.creating_wallet())
ui.available_width() >= (Content::SIDE_PANEL_WIDTH * 3.0)
+ View::get_right_inset() + View::get_left_inset();
let wallet_panel_opened = self.content.wallets.showing_wallet();
let show_app_name = if dual_wallets_panel {
wallet_panel_opened && !AppConfig::show_wallets_at_dual_panel()
} else if Content::is_dual_panel_mode(ui) {
!wallet_panel_opened
wallet_panel_opened
} else {
!Content::is_network_panel_open() && !wallet_panel_opened
Content::is_network_panel_open() || wallet_panel_opened
};
let title_text = if hide_app_name {
"".to_string()
} else {
let creating_wallet = self.content.wallets.creating_wallet();
let title_text = if creating_wallet || show_app_name {
format!("Grim {}", crate::VERSION)
} else {
"".to_string()
};
painter.text(
title_rect.center(),
+7 -3
View File
@@ -31,10 +31,14 @@ const YELLOW: Color32 = Color32::from_rgb(254, 241, 2);
const YELLOW_DARK: Color32 = Color32::from_rgb(239, 229, 3);
const GREEN: Color32 = Color32::from_rgb(0, 0x64, 0);
const GREEN_DARK: Color32 = Color32::from_rgb(0, (0x64 as f32 * 1.3 + 0.5) as u8, 0);
const RED: Color32 = Color32::from_rgb(0x8B, 0, 0);
const RED_DARK: Color32 = Color32::from_rgb((0x8B as f32 * 1.3 + 0.5) as u8, 0, 0);
const BLUE: Color32 = Color32::from_rgb(0, 0x66, 0xE4);
const BLUE_DARK: Color32 =
Color32::from_rgb(0, (0x66 as f32 * 1.3 + 0.5) as u8, (0xE4 as f32 * 1.3 + 0.5) as u8);
const FILL: Color32 = Color32::from_gray(244);
const FILL_DARK: Color32 = Color32::from_gray(24);
@@ -125,7 +129,7 @@ impl Colors {
pub fn green() -> Color32 {
if use_dark() {
GREEN.gamma_multiply(1.3)
GREEN_DARK
} else {
GREEN
}
@@ -133,7 +137,7 @@ impl Colors {
pub fn red() -> Color32 {
if use_dark() {
RED.gamma_multiply(1.3)
RED_DARK
} else {
RED
}
@@ -141,7 +145,7 @@ impl Colors {
pub fn blue() -> Color32 {
if use_dark() {
BLUE.gamma_multiply(1.3)
BLUE_DARK
} else {
BLUE
}
+53 -24
View File
@@ -30,7 +30,11 @@ use crate::gui::platform::PlatformCallbacks;
/// Android platform implementation.
#[derive(Clone)]
pub struct Android {
/// Android related state.
android_app: AndroidApp,
/// Context to repaint content and handle viewport commands.
ctx: Arc<RwLock<Option<egui::Context>>>,
}
impl Android {
@@ -38,6 +42,7 @@ impl Android {
pub fn new(app: AndroidApp) -> Self {
Self {
android_app: app,
ctx: Arc::new(RwLock::new(None)),
}
}
@@ -56,27 +61,36 @@ impl Android {
}
impl PlatformCallbacks for Android {
fn set_context(&mut self, ctx: &egui::Context) {
let mut w_ctx = self.ctx.write();
*w_ctx = Some(ctx.clone());
}
fn exit(&self) {
let _ = self.call_java_method("exit", "()V", &[]);
}
fn show_keyboard(&self) {
// Disable NDK soft input show call before fix for egui.
// self.android_app.show_soft_input(false);
self.call_java_method("showKeyboard", "()V", &[]).unwrap();
let _ = self.call_java_method("showKeyboard", "()V", &[]);
}
fn hide_keyboard(&self) {
// Disable NDK soft input hide call before fix for egui.
// self.android_app.hide_soft_input(false);
self.call_java_method("hideKeyboard", "()V", &[]).unwrap();
let _ = self.call_java_method("hideKeyboard", "()V", &[]);
}
fn copy_string_to_buffer(&self, data: String) {
let vm = unsafe { jni::JavaVM::from_raw(self.android_app.vm_as_ptr() as _) }.unwrap();
let env = vm.attach_current_thread().unwrap();
let arg_value = env.new_string(data).unwrap();
self.call_java_method("copyText",
"(Ljava/lang/String;)V",
&[JValue::Object(&JObject::from(arg_value))]).unwrap();
let _ = self.call_java_method("copyText",
"(Ljava/lang/String;)V",
&[JValue::Object(&JObject::from(arg_value))]);
}
fn get_string_from_buffer(&self) -> String {
@@ -95,12 +109,12 @@ impl PlatformCallbacks for Android {
let mut w_image = LAST_CAMERA_IMAGE.write();
*w_image = None;
// Start camera.
self.call_java_method("startCamera", "()V", &[]).unwrap();
let _ = self.call_java_method("startCamera", "()V", &[]);
}
fn stop_camera(&self) {
// Stop camera.
self.call_java_method("stopCamera", "()V", &[]).unwrap();
let _ = self.call_java_method("stopCamera", "()V", &[]);
// Clear image.
let mut w_image = LAST_CAMERA_IMAGE.write();
*w_image = None;
@@ -115,32 +129,39 @@ impl PlatformCallbacks for Android {
}
fn can_switch_camera(&self) -> bool {
let result = self.call_java_method("camerasAmount", "()I", &[]).unwrap();
let amount = unsafe { result.i };
amount > 1
if let Some(res) = self.call_java_method("camerasAmount", "()I", &[]) {
let amount = unsafe { res.i };
return amount > 1;
}
false
}
fn switch_camera(&self) {
self.call_java_method("switchCamera", "()V", &[]).unwrap();
let _ = self.call_java_method("switchCamera", "()V", &[]);
}
fn share_data(&self, name: String, data: Vec<u8>) -> Result<(), std::io::Error> {
// Create file at cache dir.
let default_cache = OsString::from(dirs::cache_dir().unwrap());
let mut cache = PathBuf::from(env::var_os("XDG_CACHE_HOME").unwrap_or(default_cache));
cache.push("images");
std::fs::create_dir_all(cache.to_str().unwrap())?;
cache.push(name);
let mut image = File::create_new(cache.clone()).unwrap();
image.write_all(data.as_slice()).unwrap();
image.sync_all().unwrap();
let mut file = PathBuf::from(env::var_os("XDG_CACHE_HOME").unwrap_or(default_cache));
// File path for Android provider.
file.push("share");
if !file.exists() {
std::fs::create_dir(file.clone())?;
}
file.push(name);
if file.exists() {
std::fs::remove_file(file.clone())?;
}
let mut image = File::create_new(file.clone())?;
image.write_all(data.as_slice())?;
image.sync_all()?;
// Call share modal at system.
let vm = unsafe { jni::JavaVM::from_raw(self.android_app.vm_as_ptr() as _) }.unwrap();
let env = vm.attach_current_thread().unwrap();
let arg_value = env.new_string(cache.to_str().unwrap()).unwrap();
self.call_java_method("shareImage",
"(Ljava/lang/String;)V",
&[JValue::Object(&JObject::from(arg_value))]).unwrap();
let arg_value = env.new_string(file.to_str().unwrap()).unwrap();
let _ = self.call_java_method("shareData",
"(Ljava/lang/String;)V",
&[JValue::Object(&JObject::from(arg_value))]);
Ok(())
}
@@ -149,7 +170,7 @@ impl PlatformCallbacks for Android {
let mut w_path = PICKED_FILE_PATH.write();
*w_path = None;
// Launch file picker.
let _ = self.call_java_method("pickFile", "()V", &[]).unwrap();
let _ = self.call_java_method("pickFile", "()V", &[]);
// Return empty string to identify async pick.
Some("".to_string())
}
@@ -167,6 +188,14 @@ impl PlatformCallbacks for Android {
}
None
}
fn request_user_attention(&self) {}
fn user_attention_required(&self) -> bool {
false
}
fn clear_user_attention(&self) {}
}
lazy_static! {
+94 -37
View File
@@ -13,12 +13,13 @@
// limitations under the License.
use std::fs::File;
use std::io:: Write;
use lazy_static::lazy_static;
use parking_lot::RwLock;
use std::io::Write;
use std::thread;
use std::sync::atomic::{AtomicBool, Ordering};
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 crate::gui::platform::PlatformCallbacks;
@@ -26,19 +27,30 @@ use crate::gui::platform::PlatformCallbacks;
/// Desktop platform related actions.
#[derive(Clone)]
pub struct Desktop {
/// Context to repaint content and handle viewport commands.
ctx: Arc<RwLock<Option<egui::Context>>>,
/// Flag to check if camera stop is needed.
stop_camera: Arc<AtomicBool>,
}
impl Default for Desktop {
fn default() -> Self {
Self {
stop_camera: Arc::new(AtomicBool::new(false)),
}
}
/// Flag to check if attention required after window focusing.
attention_required: Arc<AtomicBool>,
}
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 hide_keyboard(&self) {}
@@ -119,9 +131,55 @@ impl PlatformCallbacks for Desktop {
fn picked_file(&self) -> Option<String> {
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 {
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)]
#[cfg(target_os = "windows")]
fn start_camera_capture(stop_camera: Arc<AtomicBool>) {
@@ -168,36 +226,35 @@ impl Desktop {
let ctx = PlatformContext::default();
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 stream_desc = streams[0].clone();
let w = stream_desc.width;
let h = stream_desc.height;
let mut stream = dev.start_stream(&stream_desc).unwrap();
let mut stream = dev.start_stream(&stream_desc).unwrap();
loop {
// Stop if camera was stopped.
if stop_camera.load(Ordering::Relaxed) {
stop_camera.store(false, Ordering::Relaxed);
// Clear image.
loop {
// Stop if camera was stopped.
if stop_camera.load(Ordering::Relaxed) {
stop_camera.store(false, Ordering::Relaxed);
let mut w_image = LAST_CAMERA_IMAGE.write();
*w_image = None;
break;
}
// Get a frame.
let frame = stream.next().expect("Stream is dead").expect("Failed to capture a frame");
let mut out = vec![];
if let Some(buf) = 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 = None;
break;
*w_image = Some((out, 0));
}
// 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));
}
}
}
+5
View File
@@ -22,6 +22,8 @@ pub mod platform;
pub mod platform;
pub trait PlatformCallbacks {
fn set_context(&mut self, ctx: &egui::Context);
fn exit(&self);
fn show_keyboard(&self);
fn hide_keyboard(&self);
fn copy_string_to_buffer(&self, data: String);
@@ -34,4 +36,7 @@ pub trait PlatformCallbacks {
fn share_data(&self, name: String, data: Vec<u8>) -> Result<(), std::io::Error>;
fn pick_file(&self) -> Option<String>;
fn picked_file(&self) -> Option<String>;
fn request_user_attention(&self);
fn user_attention_required(&self) -> bool;
fn clear_user_attention(&self);
}
-10
View File
@@ -430,14 +430,4 @@ impl CameraContent {
}
None
}
/// Reset camera content state to default.
pub fn clear_state(&mut self) {
// Clear QR code scanning state.
let mut w_scan = self.qr_scan_state.write();
*w_scan = QrScanState::default();
// Clear UR data.
let mut w_data = self.ur_data.write();
*w_data = None;
}
}
+10 -17
View File
@@ -25,7 +25,7 @@ use crate::gui::views::types::{ModalContainer, ModalPosition};
use crate::node::Node;
use crate::{AppConfig, Settings};
use crate::gui::icons::{CHECK, CHECK_FAT, FILE_X};
use crate::gui::views::network::{NetworkContent, NodeSetup};
use crate::gui::views::network::NetworkContent;
use crate::gui::views::wallets::WalletsContent;
lazy_static! {
@@ -40,8 +40,8 @@ pub struct Content {
/// Central panel [`WalletsContent`] content.
pub wallets: WalletsContent,
/// Check if app exit is allowed on close event of [`eframe::App`] implementation.
pub(crate) exit_allowed: bool,
/// Check if app exit is allowed on Desktop close event.
pub exit_allowed: bool,
/// Flag to show exit progress at [`Modal`].
show_exit_progress: bool,
@@ -83,7 +83,7 @@ impl ModalContainer for Content {
modal: &Modal,
cb: &dyn PlatformCallbacks) {
match modal.id {
Self::EXIT_CONFIRMATION_MODAL => self.exit_modal_content(ui, modal),
Self::EXIT_CONFIRMATION_MODAL => self.exit_modal_content(ui, modal, cb),
Self::SETTINGS_MODAL => self.settings_modal_ui(ui, modal),
Self::ANDROID_INTEGRATED_NODE_WARNING_MODAL => self.android_warning_modal_ui(ui, modal),
Self::CRASH_REPORT_MODAL => self.crash_report_modal_ui(ui, modal, cb),
@@ -201,16 +201,16 @@ impl Content {
/// Show exit confirmation [`Modal`].
pub fn show_exit_modal() {
Modal::new(Self::EXIT_CONFIRMATION_MODAL)
.title(t!("modal.confirmation"))
.title(t!("confirmation"))
.show();
}
/// Draw exit confirmation modal content.
fn exit_modal_content(&mut self, ui: &mut egui::Ui, modal: &Modal) {
fn exit_modal_content(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
if self.show_exit_progress {
if !Node::is_running() {
self.exit_allowed = true;
ui.ctx().send_viewport_cmd(egui::ViewportCommand::Close);
cb.exit();
modal.close();
}
ui.add_space(16.0);
@@ -241,10 +241,10 @@ impl Content {
});
});
columns[1].vertical_centered_justified(|ui| {
View::button_ui(ui, t!("modal_exit.exit"), Colors::white_or_black(false), |ui| {
View::button_ui(ui, t!("modal_exit.exit"), Colors::white_or_black(false), |_| {
if !Node::is_running() {
self.exit_allowed = true;
ui.ctx().send_viewport_cmd(egui::ViewportCommand::Close);
cb.exit();
modal.close();
} else {
Node::stop(true);
@@ -272,14 +272,7 @@ impl Content {
pub fn settings_modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal) {
ui.add_space(6.0);
// Draw chain type selection.
NodeSetup::chain_type_ui(ui);
ui.add_space(8.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(8.0);
// Draw theme selection.
// Show theme selection.
Self::theme_selection_ui(ui);
ui.add_space(8.0);
+6 -3
View File
@@ -36,8 +36,11 @@ pub use camera::*;
mod qr;
pub use qr::*;
mod file;
pub use file::*;
mod file_pick;
pub use file_pick::*;
mod pull_to_refresh;
pub use pull_to_refresh::*;
pub use pull_to_refresh::*;
mod scan;
pub use scan::*;
+7 -1
View File
@@ -35,7 +35,7 @@ pub struct Modal {
/// Identifier for modal.
pub(crate) id: &'static str,
/// Position on the screen.
position: ModalPosition,
pub position: ModalPosition,
/// To check if it can be closed.
closeable: Arc<AtomicBool>,
/// Title text
@@ -64,6 +64,12 @@ impl Modal {
self
}
/// Change [`Modal`] position on the screen.
pub fn change_position(position: ModalPosition) {
let mut w_state = MODAL_STATE.write();
w_state.modal.as_mut().unwrap().position = position;
}
/// Mark [`Modal`] closed.
pub fn close(&self) {
let mut w_nav = MODAL_STATE.write();
+34 -30
View File
@@ -16,7 +16,7 @@ use egui::{Align, Layout, RichText, Rounding};
use crate::AppConfig;
use crate::gui::Colors;
use crate::gui::icons::{CARET_RIGHT, CHECK_CIRCLE, COMPUTER_TOWER, DOTS_THREE_CIRCLE, GLOBE_SIMPLE, PENCIL, PLUS_CIRCLE, POWER, TRASH, X_CIRCLE};
use crate::gui::icons::{CARET_RIGHT, CHECK_CIRCLE, COMPUTER_TOWER, DOTS_THREE_CIRCLE, GLOBE_SIMPLE, PENCIL, PLUS_CIRCLE, POWER, TRASH, WARNING_CIRCLE, X_CIRCLE};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Modal, View};
use crate::gui::views::network::modals::ExternalConnectionModal;
@@ -36,7 +36,6 @@ pub struct ConnectionsContent {
impl Default for ConnectionsContent {
fn default() -> Self {
ExternalConnection::check_ext_conn_availability(None);
Self {
ext_conn_modal: ExternalConnectionModal::new(None),
modal_ids: vec![
@@ -78,7 +77,7 @@ impl ConnectionsContent {
// Check connections availability.
if saved_chain_type != AppConfig::chain_type() {
ExternalConnection::check_ext_conn_availability(None);
ExternalConnection::check(None, ui.ctx());
}
// Show integrated node info content.
@@ -103,23 +102,20 @@ impl ConnectionsContent {
ui.add_space(4.0);
let ext_conn_list = ConnectionsConfig::ext_conn_list();
if !ext_conn_list.is_empty() {
let ext_conn_size = ext_conn_list.len();
if ext_conn_size != 0 {
ui.add_space(8.0);
for (index, conn) in ext_conn_list.iter().enumerate() {
for (index, conn) in ext_conn_list.iter().filter(|c| !c.deleted).enumerate() {
ui.horizontal_wrapped(|ui| {
// Draw connection list item.
let len = ext_conn_list.len();
Self::ext_conn_item_ui(ui, conn, index, len, |ui| {
// Draw buttons for non-default connections.
if conn.url != ExternalConnection::DEFAULT_MAIN_URL {
let button_rounding = View::item_rounding(index, len, true);
View::item_button(ui, button_rounding, TRASH, None, || {
ConnectionsConfig::remove_ext_conn(conn.id);
});
View::item_button(ui, Rounding::default(), PENCIL, None, || {
self.show_add_ext_conn_modal(Some(conn.clone()), cb);
});
}
Self::ext_conn_item_ui(ui, conn, index, ext_conn_size, |ui| {
let button_rounding = View::item_rounding(index, ext_conn_size, true);
View::item_button(ui, button_rounding, TRASH, None, || {
ConnectionsConfig::remove_ext_conn(conn.id);
});
View::item_button(ui, Rounding::default(), PENCIL, None, || {
self.show_add_ext_conn_modal(Some(conn.clone()), cb);
});
});
});
}
@@ -138,16 +134,17 @@ impl ConnectionsContent {
// Draw custom button.
custom_button(ui);
if !Node::is_running() {
// Draw button to start integrated node.
View::item_button(ui, Rounding::default(), POWER, Some(Colors::green()), || {
Node::start();
});
} else if !Node::is_starting() && !Node::is_stopping() && !Node::is_restarting() {
// Draw button to stop integrated node.
View::item_button(ui, Rounding::default(), POWER, Some(Colors::red()), || {
Node::stop(false);
});
// Draw buttons to start/stop node.
if Node::get_error().is_none() {
if !Node::is_running() {
View::item_button(ui, Rounding::default(), POWER, Some(Colors::green()), || {
Node::start();
});
} else if !Node::is_starting() && !Node::is_stopping() && !Node::is_restarting() {
View::item_button(ui, Rounding::default(), POWER, Some(Colors::red()), || {
Node::stop(false);
});
}
}
let layout_size = ui.available_size();
@@ -163,15 +160,22 @@ impl ConnectionsContent {
});
// Setup node status text.
let status_icon = if !Node::is_running() {
let has_error = Node::get_error().is_some();
let status_icon = if has_error {
WARNING_CIRCLE
} else if !Node::is_running() {
X_CIRCLE
} else if Node::not_syncing() {
CHECK_CIRCLE
} else {
DOTS_THREE_CIRCLE
};
let status_text = format!("{} {}", status_icon, Node::get_sync_status_text());
ui.label(RichText::new(status_text).size(15.0).color(Colors::text(false)));
let status_text = format!("{} {}", status_icon, if has_error {
t!("error")
} else {
Node::get_sync_status_text()
});
View::ellipsize_text(ui, status_text, 15.0, Colors::text(false));
ui.add_space(1.0);
// Setup node API address text.
+15 -10
View File
@@ -12,7 +12,6 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use std::time::Duration;
use egui::{Id, Margin, RichText, ScrollArea};
use egui::scroll_area::ScrollBarVisibility;
@@ -24,7 +23,7 @@ use crate::gui::views::{Content, TitlePanel, View};
use crate::gui::views::network::{ConnectionsContent, NetworkMetrics, NetworkMining, NetworkNode, NetworkSettings};
use crate::gui::views::network::types::{NetworkTab, NetworkTabType};
use crate::gui::views::types::{TitleContentType, TitleType};
use crate::node::{Node, NodeError};
use crate::node::{Node, NodeConfig, NodeError};
use crate::wallet::ExternalConnection;
/// Network content.
@@ -149,8 +148,6 @@ impl NetworkContent {
// Redraw after delay.
if Node::is_running() {
ui.ctx().request_repaint_after(Node::STATS_UPDATE_DELAY);
} else if show_connections {
ui.ctx().request_repaint_after(Duration::from_millis(1000));
}
}
@@ -166,22 +163,22 @@ impl NetworkContent {
let current_type = self.node_tab_content.get_type();
ui.columns(4, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::tab_button(ui, DATABASE, current_type == NetworkTabType::Node, || {
View::tab_button(ui, DATABASE, current_type == NetworkTabType::Node, |_| {
self.node_tab_content = Box::new(NetworkNode::default());
});
});
columns[1].vertical_centered_justified(|ui| {
View::tab_button(ui, GAUGE, current_type == NetworkTabType::Metrics, || {
View::tab_button(ui, GAUGE, current_type == NetworkTabType::Metrics, |_| {
self.node_tab_content = Box::new(NetworkMetrics::default());
});
});
columns[2].vertical_centered_justified(|ui| {
View::tab_button(ui, FACTORY, current_type == NetworkTabType::Mining, || {
View::tab_button(ui, FACTORY, current_type == NetworkTabType::Mining, |_| {
self.node_tab_content = Box::new(NetworkMining::default());
});
});
columns[3].vertical_centered_justified(|ui| {
View::tab_button(ui, FADERS, current_type == NetworkTabType::Settings, || {
View::tab_button(ui, FADERS, current_type == NetworkTabType::Settings, |_| {
self.node_tab_content = Box::new(NetworkSettings::default());
});
});
@@ -204,10 +201,10 @@ impl NetworkContent {
// Draw title panel.
TitlePanel::new(Id::from("network_title_panel")).ui(TitleType::Single(title_content), |ui| {
if !show_connections {
View::title_button_big(ui, DOTS_THREE_OUTLINE_VERTICAL, |_| {
View::title_button_big(ui, DOTS_THREE_OUTLINE_VERTICAL, |ui| {
AppConfig::toggle_show_connections_network_panel();
if AppConfig::show_connections_network_panel() {
ExternalConnection::check_ext_conn_availability(None);
ExternalConnection::check(None, ui.ctx());
}
});
}
@@ -312,6 +309,14 @@ impl NetworkContent {
.size(16.0)
.color(Colors::red())
);
ui.add_space(8.0);
let btn_txt = format!("{} {}",
ARROWS_COUNTER_CLOCKWISE,
t!("network_settings.reset"));
View::action_button(ui, btn_txt, || {
NodeConfig::reset_to_default();
Node::start();
});
ui.add_space(2.0);
});
}
+11 -4
View File
@@ -119,7 +119,7 @@ impl ExternalConnectionModal {
});
columns[1].vertical_centered_justified(|ui| {
// Add connection button callback.
let mut on_add = || {
let mut on_add = |ui: &mut egui::Ui| {
if !self.ext_node_url_edit.starts_with("http") {
self.ext_node_url_edit = format!("http://{}", self.ext_node_url_edit)
}
@@ -139,7 +139,7 @@ impl ExternalConnectionModal {
ext_conn.id = id;
}
ConnectionsConfig::add_ext_conn(ext_conn.clone());
ExternalConnection::check_ext_conn_availability(Some(ext_conn.id));
ExternalConnection::check(Some(ext_conn.id), ui.ctx());
on_save(ext_conn);
// Close modal.
@@ -150,10 +150,17 @@ impl ExternalConnectionModal {
modal.close();
}
};
// Handle Enter key press.
let mut enter = false;
View::on_enter_key(ui, || {
(on_add)();
enter = true;
});
View::button(ui, if self.ext_conn_id.is_some() {
if enter {
(on_add)(ui);
}
View::button_ui(ui, if self.ext_conn_id.is_some() {
t!("modal.save")
} else {
t!("modal.add")
+1 -1
View File
@@ -212,7 +212,7 @@ fn reset_settings_ui(ui: &mut egui::Ui) {
// Show modal to confirm settings reset.
Modal::new(RESET_SETTINGS_MODAL)
.position(ModalPosition::Center)
.title(t!("modal.confirmation"))
.title(t!("confirmation"))
.show();
});
+3 -1
View File
@@ -91,7 +91,7 @@ impl Default for P2PSetup {
fn default() -> Self {
let port = NodeConfig::get_p2p_port();
let is_port_available = NodeConfig::is_p2p_port_available(&port);
let default_main_seeds = grin_servers::MAINNET_DNS_SEEDS
let default_main_seeds = Node::MAINNET_DNS_SEEDS
.iter()
.map(|s| s.to_string())
.collect();
@@ -370,6 +370,8 @@ impl P2PSetup {
ui.label(RichText::new(desc)
.size(16.0)
.color(Colors::inactive_text()));
}
if !peers.is_empty() {
ui.add_space(12.0);
}
+9 -6
View File
@@ -83,7 +83,7 @@ impl Default for StratumSetup {
Self {
wallets: WalletList::default(),
wallets_modal: WalletsModal::new(wallet_id),
wallets_modal: WalletsModal::new(wallet_id, None, false),
available_ips: NodeConfig::get_ip_addrs(),
stratum_port_edit: port,
stratum_port_available_edit: is_port_available,
@@ -111,10 +111,13 @@ impl ModalContainer for StratumSetup {
modal: &Modal,
cb: &dyn PlatformCallbacks) {
match modal.id {
WALLET_SELECTION_MODAL => self.wallets_modal.ui(ui, modal, &self.wallets, |id| {
NodeConfig::save_stratum_wallet_id(id);
self.wallet_name = WalletConfig::name_by_id(id);
}),
WALLET_SELECTION_MODAL => {
self.wallets_modal.ui(ui, modal, &mut self.wallets, cb, |wallet, _| {
let id = wallet.get_config().id;
NodeConfig::save_stratum_wallet_id(id);
self.wallet_name = WalletConfig::name_by_id(id);
})
},
STRATUM_PORT_MODAL => self.port_modal(ui, modal, cb),
ATTEMPT_TIME_MODAL => self.attempt_modal(ui, modal, cb),
MIN_SHARE_DIFF_MODAL => self.min_diff_modal(ui, modal, cb),
@@ -240,7 +243,7 @@ impl StratumSetup {
/// Show wallet selection [`Modal`].
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.
Modal::new(WALLET_SELECTION_MODAL)
.position(ModalPosition::Center)
+33 -35
View File
@@ -30,8 +30,8 @@ use crate::gui::views::View;
/// QR code image from text.
pub struct QrCodeContent {
/// Text to create QR code.
pub(crate) text: String,
/// QR code text.
text: String,
/// Flag to draw animated QR with Uniform Resources
/// https://github.com/BlockchainCommons/Research/blob/master/papers/bcr-2020-005-ur.md
@@ -62,18 +62,18 @@ impl QrCodeContent {
}
/// 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 {
// Show animated QR code.
self.animated_ui(ui, text, cb);
self.animated_ui(ui, cb);
} else {
// Show static QR code.
self.static_ui(ui, text, cb);
self.static_ui(ui, cb);
}
}
/// 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() {
let space = (ui.available_width() - View::BIG_SPINNER_SIZE) / 2.0;
ui.vertical_centered(|ui| {
@@ -84,7 +84,7 @@ impl QrCodeContent {
// Create multiple vector images from text if not creating.
if !self.loading() {
self.create_svg_list(text);
self.create_svg_list();
}
} else {
let svg_list = {
@@ -111,7 +111,7 @@ impl QrCodeContent {
// Show QR code text.
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.vertical_centered(|ui| {
@@ -131,7 +131,7 @@ impl QrCodeContent {
w_state.exporting = true;
}
// Create GIF to export.
self.create_qr_gif(text, DEFAULT_QR_SIZE as usize);
self.create_qr_gif();
});
} else {
ui.vertical_centered(|ui| {
@@ -171,7 +171,7 @@ impl QrCodeContent {
}
/// 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() {
let space = (ui.available_width() - View::BIG_SPINNER_SIZE) / 2.0;
ui.vertical_centered(|ui| {
@@ -182,7 +182,7 @@ impl QrCodeContent {
// Create vector image from text if not creating.
if !self.loading() {
self.create_svg(text);
self.create_svg();
}
} else {
// Create image from SVG data.
@@ -194,7 +194,7 @@ impl QrCodeContent {
// Show QR code text.
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);
// Show button to share QR.
@@ -204,21 +204,22 @@ impl QrCodeContent {
share_text,
Colors::blue(),
Colors::white_or_black(false), || {
if let Ok(qr) = QrCode::encode_text(text.as_str(), qrcodegen::QrCodeEcc::Low) {
if let Some(data) = Self::qr_to_image_data(qr, DEFAULT_QR_SIZE as usize) {
let mut png = vec![];
let png_enc = PngEncoder::new_with_quality(&mut png,
CompressionType::Best,
FilterType::NoFilter);
if let Ok(()) = png_enc.write_image(data.as_slice(),
DEFAULT_QR_SIZE,
DEFAULT_QR_SIZE,
ExtendedColorType::L8) {
let name = format!("{}.png", chrono::Utc::now().timestamp());
cb.share_data(name, png).unwrap_or_default();
let text = self.text.as_str();
if let Ok(qr) = QrCode::encode_text(text, qrcodegen::QrCodeEcc::Low) {
if let Some(data) = Self::qr_to_image_data(qr, DEFAULT_QR_SIZE as usize) {
let mut png = vec![];
let png_enc = PngEncoder::new_with_quality(&mut png,
CompressionType::Best,
FilterType::NoFilter);
if let Ok(()) = png_enc.write_image(data.as_slice(),
DEFAULT_QR_SIZE,
DEFAULT_QR_SIZE,
ExtendedColorType::L8) {
let name = format!("{}.png", chrono::Utc::now().timestamp());
cb.share_data(name, png).unwrap_or_default();
}
}
}
}
});
});
ui.add_space(8.0);
@@ -267,8 +268,9 @@ impl QrCodeContent {
}
/// 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 text = self.text.clone();
thread::spawn(move || {
let mut encoder = ur::Encoder::bytes(text.as_bytes(), 100).unwrap();
let mut data = Vec::with_capacity(encoder.fragment_count());
@@ -294,8 +296,9 @@ impl QrCodeContent {
}
/// 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 text = self.text.clone();
thread::spawn(move || {
if let Ok(qr) = QrCode::encode_text(text.as_str(), qrcodegen::QrCodeEcc::Low) {
let svg = Self::qr_to_svg(qr, 0);
@@ -332,13 +335,14 @@ impl QrCodeContent {
}
/// 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();
w_state.gif_creating = true;
}
let qr_state = self.qr_image_state.clone();
let text = self.text.clone();
thread::spawn(move || {
// Setup GIF image encoder.
let mut gif = vec![];
@@ -354,7 +358,7 @@ impl QrCodeContent {
) {
// Create an image from QR data.
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]))
.light_color(image::Rgb([255, 255, 255]))
.build();
@@ -428,10 +432,4 @@ impl QrCodeContent {
}
Some(img_raw)
}
/// Reset QR code image content state to default.
pub fn clear_state(&mut self) {
let mut w_create = self.qr_image_state.write();
*w_create = QrImageState::default();
}
}
+131
View File
@@ -0,0 +1,131 @@
// Copyright 2024 The Grim Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use egui::scroll_area::ScrollBarVisibility;
use egui::{Id, ScrollArea};
use crate::gui::Colors;
use crate::gui::icons::COPY;
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{CameraContent, Modal, View};
use crate::gui::views::types::QrScanResult;
/// QR code scan [`Modal`] content.
pub struct CameraScanModal {
/// Camera content for QR scan [`Modal`].
camera_content: Option<CameraContent>,
/// QR code scan result
qr_scan_result: Option<QrScanResult>,
}
impl Default for CameraScanModal {
fn default() -> Self {
Self {
camera_content: None,
qr_scan_result: None,
}
}
}
impl CameraScanModal {
/// Draw [`Modal`] content.
pub fn ui(&mut self,
ui: &mut egui::Ui,
modal: &Modal,
cb: &dyn PlatformCallbacks,
mut on_result: impl FnMut(&QrScanResult)) {
// Show scan result if exists or show camera content while scanning.
if let Some(result) = &self.qr_scan_result {
let mut result_text = result.text();
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(3.0);
ScrollArea::vertical()
.id_source(Id::from("qr_scan_result_input"))
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.max_height(128.0)
.auto_shrink([false; 2])
.show(ui, |ui| {
ui.add_space(7.0);
egui::TextEdit::multiline(&mut result_text)
.font(egui::TextStyle::Small)
.desired_rows(5)
.interactive(false)
.desired_width(f32::INFINITY)
.show(ui);
ui.add_space(6.0);
});
ui.add_space(2.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(10.0);
// Show copy button.
ui.vertical_centered(|ui| {
let copy_text = format!("{} {}", COPY, t!("copy"));
View::button(ui, copy_text, Colors::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);
}
}
+17 -2
View File
@@ -217,7 +217,10 @@ impl View {
pub const TAB_ITEMS_PADDING: f32 = 5.0;
/// Tab button with white background fill color, contains only icon.
pub fn tab_button(ui: &mut egui::Ui, icon: &str, active: bool, action: impl FnOnce()) {
pub fn tab_button(ui: &mut egui::Ui,
icon: &str,
active: bool,
action: impl FnOnce(&mut egui::Ui)) {
ui.scope(|ui| {
let text_color = match active {
true => Colors::title(false),
@@ -245,7 +248,7 @@ impl View {
let br = button.ui(ui).on_hover_cursor(CursorIcon::PointingHand);
br.surrender_focus();
if Self::touched(ui, br) {
(action)();
(action)(ui);
}
});
}
@@ -280,6 +283,18 @@ impl View {
}
}
/// Draw [`Button`] with specified background fill color and text color.
pub fn colored_text_button_ui(ui: &mut egui::Ui,
text: String,
text_color: Color32,
fill: Color32,
action: impl FnOnce(&mut egui::Ui)) {
let br = Self::button_resp(ui, text, text_color, fill);
if Self::touched(ui, br) {
(action)(ui);
}
}
/// Draw gold action [`Button`].
pub fn action_button(ui: &mut egui::Ui,
text: String, action: impl FnOnce()) {
+310 -337
View File
@@ -18,33 +18,36 @@ use egui::scroll_area::ScrollBarVisibility;
use crate::AppConfig;
use crate::gui::Colors;
use crate::gui::icons::{ARROW_LEFT, CARET_RIGHT, COMPUTER_TOWER, FOLDER_LOCK, FOLDER_OPEN, GEAR, GLOBE, GLOBE_SIMPLE, LOCK_KEY, PLUS, SIDEBAR_SIMPLE, SPINNER, SUITCASE, WARNING_CIRCLE};
use crate::gui::icons::{ARROW_LEFT, CARET_RIGHT, COMPUTER_TOWER, FOLDER_OPEN, FOLDER_PLUS, GEAR, GLOBE, GLOBE_SIMPLE, LOCK_KEY, PLUS, SIDEBAR_SIMPLE, SUITCASE};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::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::modals::WalletConnectionModal;
use crate::gui::views::wallets::modals::{AddWalletModal, OpenWalletModal, WalletConnectionModal, WalletsModal};
use crate::gui::views::wallets::types::WalletTabType;
use crate::gui::views::wallets::wallet::types::wallet_status_text;
use crate::gui::views::wallets::WalletContent;
use crate::wallet::{Wallet, WalletList};
use crate::wallet::{ExternalConnection, Wallet, WalletList};
use crate::wallet::types::ConnectionMethod;
/// Wallets content.
pub struct WalletsContent {
/// List of wallets.
wallets: WalletList,
/// Password to open wallet for [`Modal`].
pass_edit: String,
/// Flag to check if wrong password was entered at [`Modal`].
wrong_pass: bool,
/// Initial wallet creation [`Modal`] content.
add_wallet_modal_content: Option<AddWalletModal>,
/// Wallet opening [`Modal`] content.
open_wallet_content: Option<OpenWalletModal>,
/// Wallet connection selection content.
conn_modal_content: Option<WalletConnectionModal>,
conn_selection_content: Option<WalletConnectionModal>,
/// Wallet selection [`Modal`] content.
wallet_selection_content: Option<WalletsModal>,
/// Selected [`Wallet`] content.
wallet_content: WalletContent,
wallet_content: Option<WalletContent>,
/// Wallet creation content.
creation_content: WalletCreation,
creation_content: Option<WalletCreation>,
/// Flag to show [`Wallet`] list at dual panel mode.
show_wallets_at_dual_panel: bool,
@@ -53,26 +56,28 @@ pub struct WalletsContent {
modal_ids: Vec<&'static str>
}
/// Identifier for connection selection [`Modal`].
const CONNECTION_SELECTION_MODAL: &'static str = "wallets_connection_selection_modal";
/// Identifier for wallet opening [`Modal`].
const OPEN_WALLET_MODAL: &'static str = "open_wallet_modal";
const ADD_WALLET_MODAL: &'static str = "wallets_add_modal";
const OPEN_WALLET_MODAL: &'static str = "wallets_open_wallet";
const SELECT_CONNECTION_MODAL: &'static str = "wallets_select_conn_modal";
const SELECT_WALLET_MODAL: &'static str = "wallets_select_modal";
impl Default for WalletsContent {
fn default() -> Self {
Self {
wallets: WalletList::default(),
pass_edit: "".to_string(),
wrong_pass: false,
conn_modal_content: None,
wallet_content: WalletContent::default(),
creation_content: WalletCreation::default(),
wallet_selection_content: None,
open_wallet_content: None,
conn_selection_content: None,
wallet_content: None,
creation_content: None,
show_wallets_at_dual_panel: AppConfig::show_wallets_at_dual_panel(),
modal_ids: vec![
ADD_WALLET_MODAL,
OPEN_WALLET_MODAL,
WalletCreation::NAME_PASS_MODAL,
CONNECTION_SELECTION_MODAL,
]
SELECT_CONNECTION_MODAL,
SELECT_WALLET_MODAL,
],
add_wallet_modal_content: None,
}
}
}
@@ -87,117 +92,105 @@ impl ModalContainer for WalletsContent {
modal: &Modal,
cb: &dyn PlatformCallbacks) {
match modal.id {
OPEN_WALLET_MODAL => self.open_wallet_modal_ui(ui, modal, cb),
WalletCreation::NAME_PASS_MODAL => {
self.creation_content.name_pass_modal_ui(ui, modal, cb)
ADD_WALLET_MODAL => {
if let Some(content) = self.add_wallet_modal_content.as_mut() {
content.ui(ui, modal, cb, |name, pass| {
self.creation_content = Some(
WalletCreation::new(name.clone(), pass.clone())
);
});
}
if self.creation_content.is_some() {
self.add_wallet_modal_content = None;
}
},
CONNECTION_SELECTION_MODAL => {
if let Some(content) = self.conn_modal_content.as_mut() {
content.ui(ui, modal, cb, |id| {
let list = self.wallets.list();
for w in list {
if self.wallets.selected_id == Some(w.get_config().id) {
w.update_ext_conn_id(id);
}
OPEN_WALLET_MODAL => {
let mut open = false;
if let Some(open_content) = self.open_wallet_content.as_mut() {
open_content.ui(ui, modal, cb, |wallet, data| {
self.wallet_content = Some(WalletContent::new(wallet, data));
open = true;
});
}
if open {
self.open_wallet_content = None;
}
},
SELECT_CONNECTION_MODAL => {
if let Some(content) = self.conn_selection_content.as_mut() {
content.ui(ui, modal, cb, |conn| {
if let Some(wallet_content) = &self.wallet_content {
wallet_content.wallet.update_connection(&conn);
}
});
}
}
SELECT_WALLET_MODAL => {
let mut select = false;
if let Some(content) = self.wallet_selection_content.as_mut() {
content.ui(ui, modal, &mut self.wallets, cb, |wallet, data| {
self.wallet_content = Some(WalletContent::new(wallet, data));
select = true;
});
}
if select {
self.wallet_selection_content = None;
}
}
_ => {}
}
}
}
impl WalletsContent {
/// Draw wallets content.
pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
// Draw modal content for current ui container.
self.current_modal_ui(ui, cb);
// Setup wallet content flags.
let empty_list = self.wallets.is_current_list_empty();
let create_wallet = self.creation_content.can_go_back();
let show_wallet = self.wallets.is_selected_open();
// Setup panels parameters.
let creating_wallet = self.creating_wallet();
let showing_wallet = self.showing_wallet() && !creating_wallet;
let dual_panel = is_dual_panel_mode(ui);
let wallet_panel_width = self.wallet_panel_width(ui, empty_list, dual_panel, show_wallet);
let content_width = ui.available_width();
let root_dual_panel = Content::is_dual_panel_mode(ui);
// Flag to check if wallet list is hidden on the screen.
let list_hidden = content_width == 0.0 || empty_list || create_wallet
|| (dual_panel && show_wallet && !self.show_wallets_at_dual_panel)
|| (!dual_panel && show_wallet) ||
(!root_dual_panel && Content::is_network_panel_open());
let list_hidden = creating_wallet || self.wallets.list().is_empty()
|| (dual_panel && showing_wallet && !self.show_wallets_at_dual_panel)
|| (!dual_panel && showing_wallet);
// Show title panel.
self.title_ui(ui, dual_panel, create_wallet, show_wallet);
self.title_ui(ui, dual_panel, showing_wallet);
// Show wallet panel content.
let wallet_panel_opened = self.wallet_panel_opened();
egui::SidePanel::right("wallet_panel")
.resizable(false)
.exact_width(wallet_panel_width)
.frame(egui::Frame {
fill: if empty_list && !create_wallet
|| (dual_panel && show_wallet && !self.show_wallets_at_dual_panel) {
Colors::fill_deep()
if showing_wallet {
egui::SidePanel::right("wallet_panel")
.resizable(false)
.exact_width(if list_hidden {
content_width
} else {
if create_wallet {
Colors::white_or_black(false)
} else {
Colors::button()
content_width - Content::SIDE_PANEL_WIDTH
})
.frame(egui::Frame {
fill: Colors::fill_deep(),
..Default::default()
})
.show_inside(ui, |ui| {
// Show opened wallet content.
if let Some(content) = self.wallet_content.as_mut() {
content.ui(ui, cb);
}
},
..Default::default()
})
.show_animated_inside(ui, wallet_panel_opened, |ui| {
if create_wallet || !show_wallet {
// Show wallet creation content.
self.creation_content.ui(ui, cb, |wallet| {
// Add created wallet to list.
self.wallets.add(wallet);
// Reset wallet content.
self.wallet_content = WalletContent::default();
});
} else {
let selected_id = self.wallets.selected_id.clone();
let list = self.wallets.mut_list();
for wallet in list {
// Show content for selected wallet.
if selected_id == Some(wallet.get_config().id) {
// Setup wallet content width.
let mut rect = ui.available_rect_before_wrap();
let mut width = ui.available_width();
if dual_panel && self.show_wallets_at_dual_panel {
width = content_width - Content::SIDE_PANEL_WIDTH;
}
rect.set_width(width);
// Show wallet content.
ui.allocate_ui_at_rect(rect, |ui| {
self.wallet_content.ui(ui, wallet, cb);
});
break;
}
}
}
});
});
}
// Show wallets bottom panel.
let show_bottom_panel = !list_hidden || dual_panel;
if show_bottom_panel {
if !list_hidden {
egui::TopBottomPanel::bottom("wallets_bottom_panel")
.frame(egui::Frame {
fill: Colors::fill(),
inner_margin: Margin {
left: View::get_left_inset() + View::TAB_ITEMS_PADDING,
left: View::far_left_inset_margin(ui) + View::TAB_ITEMS_PADDING,
right: View::far_right_inset_margin(ui) + View::TAB_ITEMS_PADDING,
top: View::TAB_ITEMS_PADDING,
bottom: View::get_bottom_inset() + View::TAB_ITEMS_PADDING,
},
..Default::default()
})
.resizable(false)
.show_inside(ui, |ui| {
// Setup spacing between tabs.
ui.style_mut().spacing.item_spacing = egui::vec2(View::TAB_ITEMS_PADDING, 0.0);
@@ -205,20 +198,21 @@ impl WalletsContent {
ui.style_mut().spacing.button_padding = egui::vec2(10.0, 4.0);
ui.vertical_centered(|ui| {
let pressed = Modal::opened() == Some(WalletCreation::NAME_PASS_MODAL);
View::tab_button(ui, PLUS, pressed, || {
self.creation_content.show_name_pass_modal(cb);
let pressed = Modal::opened() == Some(ADD_WALLET_MODAL);
View::tab_button(ui, PLUS, pressed, |_| {
self.show_add_wallet_modal(cb);
});
});
});
}
// Show wallet list.
egui::CentralPanel::default()
.frame(if list_hidden {
egui::Frame::default()
} else {
egui::Frame {
egui::SidePanel::left("wallet_list_panel")
.exact_width(if dual_panel && showing_wallet {
Content::SIDE_PANEL_WIDTH
} else {
content_width
})
.resizable(false)
.frame(egui::Frame {
stroke: View::item_stroke(),
fill: Colors::fill_deep(),
inner_margin: Margin {
@@ -228,67 +222,183 @@ impl WalletsContent {
bottom: 4.0,
},
..Default::default()
}
})
.show_inside(ui, |ui| {
if !list_hidden && !dual_panel && !showing_wallet && !creating_wallet {
ui.ctx().request_repaint_after(Duration::from_millis(1000));
}
// Show wallet list.
self.wallet_list_ui(ui, cb);
});
}
// Show central panel with wallet creation.
egui::CentralPanel::default()
.frame(egui::Frame {
stroke: View::item_stroke(),
fill: if self.creation_content.is_some() {
Colors::white_or_black(false)
} else {
Colors::fill_deep()
},
..Default::default()
})
.show_inside(ui, |ui| {
if !list_hidden && !dual_panel {
ui.ctx().request_repaint_after(Duration::from_millis(1000));
}
self.wallet_list_ui(ui, cb);
});
}
if self.creation_content.is_some() {
let creation = self.creation_content.as_mut().unwrap();
let pass = creation.pass.clone();
let mut created = false;
// Show wallet creation content.
creation.ui(ui, cb, |wallet| {
self.wallets.add(wallet.clone());
if let Ok(_) = wallet.open(pass.clone()) {
self.wallet_content = Some(WalletContent::new(wallet, None));
}
created = true;
});
if created {
self.creation_content = None;
}
} else if self.wallets.list().is_empty() {
View::center_content(ui, 350.0 + View::get_bottom_inset(), |ui| {
View::app_logo_name_version(ui);
ui.add_space(4.0);
/// Check if wallet panel is showing.
pub fn wallet_panel_opened(&self) -> bool {
let empty_list = self.wallets.is_current_list_empty();
empty_list || self.creating_wallet() || self.showing_wallet()
let text = t!("wallets.create_desc");
ui.label(RichText::new(text)
.size(16.0)
.color(Colors::gray())
);
ui.add_space(8.0);
// Show wallet creation button.
let add_text = format!("{} {}", FOLDER_PLUS, t!("wallets.add"));
View::button(ui, add_text, Colors::white_or_black(false), || {
self.show_add_wallet_modal(cb);
});
});
}
});
}
/// Check if opened wallet is showing.
pub fn showing_wallet(&self) -> bool {
self.wallets.is_selected_open()
if let Some(wallet_content) = &self.wallet_content {
let w = &wallet_content.wallet;
return w.is_open() && w.get_config().chain_type == AppConfig::chain_type();
}
false
}
/// Check if wallet is creating.
pub fn creating_wallet(&self) -> bool {
self.creation_content.can_go_back()
self.creation_content.is_some()
}
/// Handle data from deeplink or opened file.
pub fn on_data(&mut self, ui: &mut egui::Ui, data: Option<String>, cb: &dyn PlatformCallbacks) {
let wallets_size = self.wallets.list().len();
if wallets_size == 0 {
return;
}
// Close network panel on single panel mode.
if !Content::is_dual_panel_mode(ui) && Content::is_network_panel_open() {
Content::toggle_network_panel();
}
// Pass data to opened selected wallet or show wallets selection.
if self.wallet_content.is_some() {
if self.showing_wallet() {
if wallets_size == 1 {
let wallet_content = self.wallet_content.as_mut().unwrap();
wallet_content.on_data(data);
} else {
self.show_wallet_selection_modal(data);
}
} else {
if wallets_size == 1 {
let wallet_content = self.wallet_content.as_ref().unwrap();
self.show_opening_modal(wallet_content.wallet.clone(), data, cb);
} else {
self.show_wallet_selection_modal(data);
}
}
}
}
/// Show initial wallet creation [`Modal`].
pub fn show_add_wallet_modal(&mut self, cb: &dyn PlatformCallbacks) {
self.add_wallet_modal_content = Some(AddWalletModal::default());
Modal::new(ADD_WALLET_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("wallets.add"))
.show();
cb.show_keyboard();
}
/// Show wallet selection with provided optional data.
fn show_wallet_selection_modal(&mut self, data: Option<String>) {
self.wallet_selection_content = Some(WalletsModal::new(None, data, true));
Modal::new(SELECT_WALLET_MODAL)
.position(ModalPosition::Center)
.title(t!("network_settings.choose_wallet"))
.show();
}
/// Handle Back key event returning `false` when event was handled.
pub fn on_back(&mut self) -> bool {
if self.creation_content.is_some() {
// Close wallet creation.
let creation = self.creation_content.as_mut().unwrap();
if creation.on_back() {
self.creation_content = None;
}
return false
} else {
// Close opened wallet.
if self.showing_wallet() {
self.wallet_content = None;
return false
}
}
true
}
/// Draw [`TitlePanel`] content.
fn title_ui(&mut self,
ui: &mut egui::Ui,
dual_panel: bool,
create_wallet: bool,
show_wallet: bool) {
let show_list = self.show_wallets_at_dual_panel;
let creating_wallet = self.creating_wallet();
// Setup title.
let title_content = if self.wallets.is_selected_open() && (!dual_panel
|| (dual_panel && !show_list)) && !create_wallet {
let title_text = self.wallet_content.current_tab.get_type().name().to_uppercase();
if self.wallet_content.current_tab.get_type() == WalletTabType::Settings {
let title_content = if show_wallet && (!dual_panel
|| (dual_panel && !show_list)) && !creating_wallet {
let wallet_content = self.wallet_content.as_ref().unwrap();
let wallet_tab_type = wallet_content.current_tab.get_type();
let title_text = wallet_tab_type.name().to_uppercase();
if wallet_tab_type == WalletTabType::Settings {
TitleType::Single(TitleContentType::Title(title_text))
} else {
let subtitle_text = self.wallets.selected_name();
let subtitle_text = wallet_content.wallet.get_config().name;
TitleType::Single(TitleContentType::WithSubTitle(title_text, subtitle_text, false))
}
} else {
let title_text = if create_wallet {
let title_text = if creating_wallet {
t!("wallets.add")
} else {
t!("wallets.title")
}.to_uppercase();
let dual_title = !create_wallet && show_wallet && dual_panel;
let dual_title = !creating_wallet && show_wallet && dual_panel;
if dual_title {
let wallet_tab_type = self.wallet_content.current_tab.get_type();
let wallet_tab_name = wallet_tab_type.name().to_uppercase();
let title_content = if wallet_tab_type == WalletTabType::Settings {
TitleContentType::Title(wallet_tab_name)
let wallet_content = self.wallet_content.as_ref().unwrap();
let wallet_tab_type = wallet_content.current_tab.get_type();
let wallet_title_text = wallet_tab_type.name().to_uppercase();
let wallet_title_content = if wallet_tab_type == WalletTabType::Settings {
TitleContentType::Title(wallet_title_text)
} else {
let subtitle_text = self.wallets.selected_name();
TitleContentType::WithSubTitle(wallet_tab_name, subtitle_text, false)
let subtitle_text = wallet_content.wallet.get_config().name;
TitleContentType::WithSubTitle(wallet_title_text, subtitle_text, false)
};
TitleType::Dual(TitleContentType::Title(title_text), title_content)
TitleType::Dual(TitleContentType::Title(title_text), wallet_title_content)
} else {
TitleType::Single(TitleContentType::Title(title_text))
}
@@ -298,12 +408,20 @@ impl WalletsContent {
TitlePanel::new(Id::new("wallets_title_panel")).ui(title_content, |ui| {
if show_wallet && !dual_panel {
View::title_button_big(ui, ARROW_LEFT, |_| {
self.wallets.select(None);
});
} else if create_wallet {
View::title_button_big(ui, ARROW_LEFT, |_| {
self.creation_content.back();
self.wallet_content = None;
});
} else if self.creation_content.is_some() {
let mut close = false;
if let Some(creation) = self.creation_content.as_mut() {
View::title_button_big(ui, ARROW_LEFT, |_| {
if creation.on_back() {
close = true;
}
});
}
if close {
self.creation_content = None;
}
} else if show_wallet && dual_panel {
let list_icon = if show_list {
SIDEBAR_SIMPLE
@@ -330,29 +448,6 @@ impl WalletsContent {
}, ui);
}
/// Calculate [`WalletContent`] panel width.
fn wallet_panel_width(
&self,
ui:&mut egui::Ui,
list_empty: bool,
dual_panel: bool,
show_wallet: bool
) -> f32 {
let create_wallet = self.creation_content.can_go_back();
let available_width = if list_empty || create_wallet || (show_wallet && !dual_panel)
|| (show_wallet && !self.show_wallets_at_dual_panel) {
ui.available_width()
} else {
ui.available_width() - Content::SIDE_PANEL_WIDTH
};
if dual_panel && show_wallet && self.show_wallets_at_dual_panel {
let min_width = Content::SIDE_PANEL_WIDTH + View::get_right_inset();
f32::max(min_width, available_width)
} else {
available_width
}
}
/// Draw list of wallets.
fn wallet_list_ui(&mut self,
ui: &mut egui::Ui,
@@ -373,7 +468,7 @@ impl WalletsContent {
list.retain(|w| {
let deleted = w.is_deleted();
if deleted {
self.wallets.select(None);
self.wallet_content = None;
self.wallets.remove(w.get_config().id);
ui.ctx().request_repaint();
}
@@ -381,10 +476,9 @@ impl WalletsContent {
});
for wallet in &list {
// Check if wallet reopen is needed.
if !wallet.is_open() && wallet.reopen_needed() {
if wallet.reopen_needed() && !wallet.is_open() {
wallet.set_reopen(false);
self.wallets.select(Some(wallet.get_config().id));
self.show_open_wallet_modal(cb);
self.show_opening_modal(wallet.clone(), None, cb);
}
// Draw wallet list item.
self.wallet_item_ui(ui, wallet, cb);
@@ -401,9 +495,11 @@ impl WalletsContent {
wallet: &Wallet,
cb: &dyn PlatformCallbacks) {
let config = wallet.get_config();
let id = config.id;
let is_selected = self.wallets.selected_id == Some(id);
let current = is_selected && wallet.is_open();
let current = if let Some(content) = &self.wallet_content {
content.wallet.get_config().id == config.id && wallet.is_open()
} else {
false
};
// Draw round background.
let mut rect = ui.available_rect_before_wrap();
@@ -420,27 +516,37 @@ impl WalletsContent {
if !wallet.is_open() {
// Show button to open closed wallet.
View::item_button(ui, View::item_rounding(0, 1, true), FOLDER_OPEN, None, || {
self.wallets.select(Some(id));
self.show_open_wallet_modal(cb);
self.show_opening_modal(wallet.clone(), None, cb);
});
// Show button to select connection if not syncing.
if !wallet.syncing() {
let mut show_selection = false;
View::item_button(ui, Rounding::default(), GLOBE, None, || {
self.wallets.select(Some(id));
self.show_connection_selector_modal(wallet);
self.wallet_content = Some(WalletContent::new(wallet.clone(), None));
self.conn_selection_content = Some(
WalletConnectionModal::new(wallet.get_current_connection())
);
// Show connection selection modal.
Modal::new(SELECT_CONNECTION_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("wallets.conn_method"))
.show();
show_selection = true;
});
if show_selection {
self.conn_selection_content = None;
ExternalConnection::check(None, ui.ctx());
}
}
} else {
if !is_selected {
if !current {
// Show button to select opened wallet.
View::item_button(ui, View::item_rounding(0, 1, true), CARET_RIGHT, None, || {
self.wallets.select(Some(id));
self.wallet_content = WalletContent::default();
self.wallet_content = Some(WalletContent::new(wallet.clone(), None));
});
}
// Show button to close opened wallet.
if !wallet.is_closing() {
View::item_button(ui, if !is_selected {
View::item_button(ui, if !current {
Rounding::default()
} else {
View::item_rounding(0, 1, true)
@@ -455,8 +561,8 @@ impl WalletsContent {
ui.add_space(6.0);
ui.vertical(|ui| {
ui.add_space(3.0);
// Setup wallet name text.
let name_color = if is_selected {
// Show wallet name text.
let name_color = if current {
Colors::white_or_black(true)
} else {
Colors::title(false)
@@ -466,46 +572,17 @@ impl WalletsContent {
View::ellipsize_text(ui, config.name, 18.0, name_color);
});
// Setup wallet status text.
let status_text = if wallet.is_open() {
if wallet.sync_error() {
format!("{} {}", WARNING_CIRCLE, t!("error"))
} else if wallet.is_closing() {
format!("{} {}", SPINNER, t!("wallets.closing"))
} else if wallet.is_repairing() {
let repair_progress = wallet.repairing_progress();
if repair_progress == 0 {
format!("{} {}", SPINNER, t!("wallets.checking"))
} else {
format!("{} {}: {}%",
SPINNER,
t!("wallets.checking"),
repair_progress)
}
} else if wallet.syncing() {
let info_progress = wallet.info_sync_progress();
if info_progress == 100 || info_progress == 0 {
format!("{} {}", SPINNER, t!("wallets.loading"))
} else {
format!("{} {}: {}%",
SPINNER,
t!("wallets.loading"),
info_progress)
}
} else {
format!("{} {}", FOLDER_OPEN, t!("wallets.unlocked"))
}
} else {
format!("{} {}", FOLDER_LOCK, t!("wallets.locked"))
};
View::ellipsize_text(ui, status_text, 15.0, Colors::text(false));
// Show wallet status text.
View::ellipsize_text(ui, wallet_status_text(wallet), 15.0, Colors::text(false));
ui.add_space(1.0);
// Setup wallet connection text.
let conn_text = if let Some(conn) = wallet.get_current_ext_conn() {
format!("{} {}", GLOBE_SIMPLE, conn.url)
} else {
format!("{} {}", COMPUTER_TOWER, t!("network.node"))
// Show wallet connection text.
let connection = wallet.get_current_connection();
let conn_text = match connection {
ConnectionMethod::Integrated => {
format!("{} {}", COMPUTER_TOWER, t!("network.node"))
}
ConnectionMethod::External(_, url) => format!("{} {}", GLOBE_SIMPLE, url)
};
ui.label(RichText::new(conn_text).size(15.0).color(Colors::gray()));
ui.add_space(3.0);
@@ -514,123 +591,19 @@ impl WalletsContent {
});
}
/// Show [`Modal`] to select connection for the wallet.
fn show_connection_selector_modal(&mut self, wallet: &Wallet) {
let ext_conn = wallet.get_current_ext_conn();
self.conn_modal_content = Some(WalletConnectionModal::new(ext_conn));
// Show modal.
Modal::new(CONNECTION_SELECTION_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("wallets.conn_method"))
.show();
}
/// Show [`Modal`] to open selected wallet.
fn show_open_wallet_modal(&mut self, cb: &dyn PlatformCallbacks) {
// Reset modal values.
self.pass_edit = String::from("");
self.wrong_pass = false;
// Show modal.
/// Show [`Modal`] to select and open wallet.
fn show_opening_modal(&mut self,
wallet: Wallet,
data: Option<String>,
cb: &dyn PlatformCallbacks) {
self.wallet_content = Some(WalletContent::new(wallet.clone(), None));
self.open_wallet_content = Some(OpenWalletModal::new(wallet, data));
Modal::new(OPEN_WALLET_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("wallets.open"))
.show();
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.
+236 -330
View File
@@ -17,90 +17,128 @@ use egui::scroll_area::ScrollBarVisibility;
use grin_util::ZeroingString;
use crate::gui::Colors;
use crate::gui::icons::{CHECK, CLIPBOARD_TEXT, COPY, FOLDER_PLUS, SCAN, SHARE_FAT};
use crate::gui::icons::{CHECK, CLIPBOARD_TEXT, COPY, SCAN};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Modal, Content, View};
use crate::gui::views::types::{ModalPosition, TextEditOptions};
use crate::gui::views::{Modal, Content, View, CameraScanModal};
use crate::gui::views::types::{ModalContainer, ModalPosition, QrScanResult};
use crate::gui::views::wallets::creation::MnemonicSetup;
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::wallet::{ExternalConnection, Wallet};
use crate::wallet::types::PhraseMode;
/// Wallet creation content.
pub struct WalletCreation {
/// Wallet creation step.
step: Option<Step>,
/// Wallet name.
pub name: String,
/// Wallet password.
pub pass: ZeroingString,
/// Flag to check if wallet creation [`Modal`] was just opened to focus on first field.
modal_just_opened: bool,
/// Wallet name value.
name_edit: String,
/// Password to encrypt created wallet.
pass_edit: String,
/// Wallet creation step.
step: Step,
/// QR code scanning [`Modal`] content.
scan_modal_content: Option<CameraScanModal>,
/// Mnemonic phrase setup content.
pub(crate) mnemonic_setup: MnemonicSetup,
mnemonic_setup: MnemonicSetup,
/// Network setup content.
pub(crate) network_setup: ConnectionSettings,
network_setup: ConnectionSettings,
/// Flag to check if an error occurred during wallet creation.
creation_error: Option<String>,
/// [`Modal`] identifiers allowed at this ui container.
modal_ids: Vec<&'static str>
}
impl Default for WalletCreation {
fn default() -> Self {
Self {
step: None,
modal_just_opened: true,
name_edit: String::from(""),
pass_edit: String::from(""),
mnemonic_setup: MnemonicSetup::default(),
network_setup: ConnectionSettings::default(),
creation_error: None,
const QR_CODE_PHRASE_SCAN_MODAL: &'static str = "qr_code_rec_phrase_scan_modal";
impl ModalContainer for WalletCreation {
fn modal_ids(&self) -> &Vec<&'static str> {
&self.modal_ids
}
fn modal_ui(&mut self,
ui: &mut egui::Ui,
modal: &Modal,
cb: &dyn PlatformCallbacks) {
match modal.id {
QR_CODE_PHRASE_SCAN_MODAL => {
if let Some(content) = self.scan_modal_content.as_mut() {
content.ui(ui, modal, cb, |result| {
match result {
QrScanResult::Text(text) => {
self.mnemonic_setup.mnemonic.import(&text);
modal.close();
}
QrScanResult::SeedQR(text) => {
self.mnemonic_setup.mnemonic.import(&text);
modal.close();
}
_ => {}
}
});
}
},
_ => {}
}
}
}
impl WalletCreation {
/// Wallet name/password input modal identifier.
pub const NAME_PASS_MODAL: &'static str = "name_pass_modal";
/// Create new wallet creation instance from name and password.
pub fn new(name: String, pass: ZeroingString) -> Self {
Self {
name,
pass,
step: Step::EnterMnemonic,
scan_modal_content: None,
mnemonic_setup: MnemonicSetup::default(),
network_setup: ConnectionSettings::default(),
creation_error: None,
modal_ids: vec![
QR_CODE_PHRASE_SCAN_MODAL
],
}
}
/// Draw wallet creation content.
pub fn ui(&mut self,
ui: &mut egui::Ui,
cb: &dyn PlatformCallbacks,
on_create: impl FnOnce(Wallet)) {
// Show wallet creation step description and confirmation panel.
if self.step.is_some() {
egui::TopBottomPanel::bottom("wallet_creation_step_panel")
.frame(egui::Frame {
fill: Colors::fill(),
inner_margin: Margin {
left: View::far_left_inset_margin(ui) + 8.0,
right: View::get_right_inset() + 8.0,
top: 4.0,
bottom: View::get_bottom_inset(),
},
..Default::default()
})
.show_inside(ui, |ui| {
ui.vertical_centered(|ui| {
ui.vertical_centered(|ui| {
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 2.0, |ui| {
self.step_control_ui(ui, on_create, cb);
});
});
on_create: impl FnMut(Wallet)) {
self.current_modal_ui(ui, cb);
// Show wallet creation step description and confirmation panel.
egui::TopBottomPanel::bottom("wallet_creation_step_panel")
.frame(egui::Frame {
stroke: View::item_stroke(),
fill: Colors::fill_deep(),
inner_margin: Margin {
left: View::far_left_inset_margin(ui) + 8.0,
right: View::get_right_inset() + 8.0,
top: 4.0,
bottom: View::get_bottom_inset(),
},
..Default::default()
})
.show_inside(ui, |ui| {
ui.vertical_centered(|ui| {
ui.vertical_centered(|ui| {
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 2.0, |ui| {
self.step_control_ui(ui, on_create, cb);
});
});
});
}
});
// Show wallet creation step content panel.
egui::CentralPanel::default()
.frame(egui::Frame {
stroke: View::item_stroke(),
inner_margin: Margin {
left: View::far_left_inset_margin(ui) + 4.0,
right: View::get_right_inset() + 4.0,
@@ -110,18 +148,13 @@ impl WalletCreation {
..Default::default()
})
.show_inside(ui, |ui| {
let id = if let Some(step) = &self.step {
format!("creation_step_scroll_{}", step.name())
} else {
"creation_step_scroll".to_owned()
};
ScrollArea::vertical()
.id_source(id)
.id_source(Id::from(format!("creation_step_scroll_{}", self.step.name())))
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.auto_shrink([false; 2])
.show(ui, |ui| {
ui.vertical_centered(|ui| {
let max_width = if self.step == Some(Step::SetupConnection) {
let max_width = if self.step == Step::SetupConnection {
Content::SIDE_PANEL_WIDTH * 1.3
} else {
Content::SIDE_PANEL_WIDTH * 2.0
@@ -139,59 +172,59 @@ impl WalletCreation {
ui: &mut egui::Ui,
on_create: impl FnOnce(Wallet),
cb: &dyn PlatformCallbacks) {
if let Some(step) = self.step.clone() {
// Setup step description text and availability.
let (step_text, mut step_available) = match step {
Step::EnterMnemonic => {
let mode = &self.mnemonic_setup.mnemonic.mode;
let text = if mode == &PhraseMode::Generate {
t!("wallets.create_phrase_desc")
} else {
t!("wallets.restore_phrase_desc")
};
let available = !self
.mnemonic_setup
.mnemonic
.words
.contains(&String::from(""));
(text, available)
}
Step::ConfirmMnemonic => {
let text = t!("wallets.restore_phrase_desc");
let available = !self
.mnemonic_setup
.mnemonic
.confirm_words
.contains(&String::from(""));
(text, available)
}
Step::SetupConnection => {
(t!("wallets.setup_conn_desc"), self.creation_error.is_none())
}
};
// Show step description or error if entered phrase is not valid.
if self.mnemonic_setup.valid_phrase && self.creation_error.is_none() {
ui.add_space(2.0);
ui.label(RichText::new(step_text).size(16.0).color(Colors::gray()));
ui.add_space(2.0);
} else {
step_available = false;
// Show error text.
if let Some(err) = &self.creation_error {
ui.add_space(10.0);
ui.label(RichText::new(err)
.size(16.0)
.color(Colors::red()));
ui.add_space(10.0);
} else {
ui.label(RichText::new(&t!("wallets.not_valid_phrase"))
.size(16.0)
.color(Colors::red()));
ui.add_space(2.0);
let step = &self.step;
// Setup description and next step availability.
let (step_text, mut next) = match step {
Step::EnterMnemonic => {
let mode = &self.mnemonic_setup.mnemonic.mode();
let (text, available) = match mode {
PhraseMode::Generate => (t!("wallets.create_phrase_desc"), true),
PhraseMode::Import => {
let available = !self.mnemonic_setup.mnemonic.has_empty_or_invalid();
(t!("wallets.restore_phrase_desc"), available)
}
};
(text, available)
}
if step == Step::EnterMnemonic {
Step::ConfirmMnemonic => {
let text = t!("wallets.restore_phrase_desc");
let available = !self.mnemonic_setup.mnemonic.has_empty_or_invalid();
(text, available)
}
Step::SetupConnection => {
(t!("wallets.setup_conn_desc"), self.creation_error.is_none())
}
};
// Show step description or error.
let generate_step = step == &Step::EnterMnemonic &&
self.mnemonic_setup.mnemonic.mode() == PhraseMode::Generate;
if (self.mnemonic_setup.mnemonic.valid() && self.creation_error.is_none()) ||
generate_step {
ui.add_space(2.0);
ui.label(RichText::new(step_text).size(16.0).color(Colors::gray()));
ui.add_space(2.0);
} else {
next = false;
// Show error text.
if let Some(err) = &self.creation_error {
ui.add_space(10.0);
ui.label(RichText::new(err)
.size(16.0)
.color(Colors::red()));
ui.add_space(10.0);
} else {
ui.add_space(2.0);
ui.label(RichText::new(&t!("wallets.not_valid_phrase"))
.size(16.0)
.color(Colors::red()));
ui.add_space(2.0);
};
}
// Setup buttons.
match step {
Step::EnterMnemonic => {
ui.add_space(4.0);
// Setup spacing between buttons.
@@ -200,132 +233,123 @@ impl WalletCreation {
ui.columns(2, |columns| {
// Show copy or paste button for mnemonic phrase step.
columns[0].vertical_centered_justified(|ui| {
self.copy_or_paste_button_ui(ui, cb);
match self.mnemonic_setup.mnemonic.mode() {
PhraseMode::Generate => {
// Show copy button.
let c_t = format!("{} {}", COPY, t!("copy").to_uppercase());
View::button(ui,
c_t.to_uppercase(),
Colors::white_or_black(false), || {
cb.copy_string_to_buffer(self.mnemonic_setup
.mnemonic
.get_phrase());
});
}
PhraseMode::Import => {
// Show paste button.
let p_t = format!("{} {}",
CLIPBOARD_TEXT,
t!("paste").to_uppercase());
View::button(ui, p_t, Colors::white_or_black(false), || {
let data = ZeroingString::from(cb.get_string_from_buffer());
self.mnemonic_setup.mnemonic.import(&data);
});
}
}
});
// Show next step or QR code scan button.
columns[1].vertical_centered_justified(|ui| {
if step_available {
// Show next step button if there are no empty words.
self.next_step_button_ui(ui, step, on_create);
if next {
self.next_step_button_ui(ui, on_create);
} else {
// Show QR code scan button.
let scan_text = format!("{} {}", SCAN, t!("scan").to_uppercase());
View::button(ui, scan_text, Colors::white_or_black(false), || {
self.mnemonic_setup.show_qr_scan_modal(cb);
self.scan_modal_content = Some(CameraScanModal::default());
// Show QR code scan modal.
Modal::new(QR_CODE_PHRASE_SCAN_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("scan_qr"))
.closeable(false)
.show();
cb.start_camera();
});
}
});
});
ui.add_space(4.0);
} else if step == Step::ConfirmMnemonic {
}
Step::ConfirmMnemonic => {
ui.add_space(4.0);
// Show next step or paste button.
if step_available {
self.next_step_button_ui(ui, step, on_create);
if next {
self.next_step_button_ui(ui, on_create);
} else {
let paste_text = format!("{} {}", CLIPBOARD_TEXT, t!("paste").to_uppercase());
View::button(ui, paste_text, Colors::white_or_black(false), || {
let data = ZeroingString::from(cb.get_string_from_buffer().trim());
self.mnemonic_setup.mnemonic.import_text(&data, true);
let data = ZeroingString::from(cb.get_string_from_buffer());
self.mnemonic_setup.mnemonic.import(&data);
});
}
ui.add_space(4.0);
} else if step_available {
ui.add_space(4.0);
self.next_step_button_ui(ui, step, on_create);
ui.add_space(4.0);
}
ui.add_space(4.0);
}
}
/// Draw copy or paste button at [`Step::EnterMnemonic`].
fn copy_or_paste_button_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
match self.mnemonic_setup.mnemonic.mode {
PhraseMode::Generate => {
// Show copy button.
let c_t = format!("{} {}", COPY, t!("copy").to_uppercase());
View::button(ui, c_t.to_uppercase(), Colors::white_or_black(false), || {
cb.copy_string_to_buffer(self.mnemonic_setup.mnemonic.get_phrase());
});
}
PhraseMode::Import => {
// Show paste button.
let p_t = format!("{} {}", CLIPBOARD_TEXT, t!("paste").to_uppercase());
View::button(ui, p_t, Colors::white_or_black(false), || {
let data = ZeroingString::from(cb.get_string_from_buffer().trim());
self.mnemonic_setup.mnemonic.import_text(&data, false);
});
Step::SetupConnection => {
if next {
ui.add_space(4.0);
self.next_step_button_ui(ui, on_create);
ui.add_space(4.0);
}
}
}
ui.add_space(3.0);
}
/// Draw button to go to next [`Step`].
fn next_step_button_ui(&mut self,
ui: &mut egui::Ui,
step: Step,
on_create: impl FnOnce(Wallet)) {
// Setup button text.
let (next_text, text_color, bg_color) = if step == Step::SetupConnection {
let (next_text, text_color, bg_color) = if self.step == Step::SetupConnection {
(format!("{} {}", CHECK, t!("complete")), Colors::title(true), Colors::gold())
} else {
let text = format!("{} {}", SHARE_FAT, t!("continue"));
(text, Colors::text_button(), Colors::white_or_black(false))
(t!("continue"), Colors::text_button(), Colors::white_or_black(false))
};
// Show next step button.
View::colored_text_button(ui, next_text.to_uppercase(), text_color, bg_color, || {
self.step = if let Some(step) = &self.step {
match step {
Step::EnterMnemonic => {
if self.mnemonic_setup.mnemonic.mode == PhraseMode::Generate {
Some(Step::ConfirmMnemonic)
} else {
// Check if entered phrase was valid.
if self.mnemonic_setup.valid_phrase {
Some(Step::SetupConnection)
} else {
Some(Step::EnterMnemonic)
}
}
View::colored_text_button_ui(ui, next_text.to_uppercase(), text_color, bg_color, |ui| {
self.step = match self.step {
Step::EnterMnemonic => {
if self.mnemonic_setup.mnemonic.mode() == PhraseMode::Generate {
Step::ConfirmMnemonic
} else {
Step::SetupConnection
}
Step::ConfirmMnemonic => {
Some(Step::SetupConnection)
},
Step::SetupConnection => {
// Create wallet at last step.
let conn_method = &self.network_setup.method;
match Wallet::create(&self.name_edit,
&self.pass_edit,
&self.mnemonic_setup.mnemonic,
conn_method) {
Ok(mut w) => {
// Open created wallet.
w.open(&self.pass_edit).unwrap();
// Pass created wallet to callback.
(on_create)(w);
// Reset input data.
self.step = None;
self.name_edit = String::from("");
self.pass_edit = String::from("");
self.mnemonic_setup.reset();
None
}
Err(e) => {
self.creation_error = Some(format!("{:?}", e));
Some(Step::SetupConnection)
}
}
Step::ConfirmMnemonic => {
Step::SetupConnection
},
Step::SetupConnection => {
// Create wallet at last step.
match Wallet::create(&self.name,
&self.pass,
&self.mnemonic_setup.mnemonic,
&self.network_setup.method) {
Ok(w) => {
self.mnemonic_setup.reset();
// Pass created wallet to callback.
(on_create)(w);
Step::EnterMnemonic
}
Err(e) => {
self.creation_error = Some(format!("{:?}", e));
Step::SetupConnection
}
}
}
} else {
Some(Step::EnterMnemonic)
};
// Check external connections availability on connection setup.
if self.step == Some(Step::SetupConnection) {
ExternalConnection::check_ext_conn_availability(None);
if self.step == Step::SetupConnection {
ExternalConnection::check(None, ui.ctx());
}
});
}
@@ -333,149 +357,31 @@ impl WalletCreation {
/// Draw wallet creation [`Step`] content.
fn step_content_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
match &self.step {
None => {
// Show wallet creation message if step is empty.
View::center_content(ui, 350.0 + View::get_bottom_inset(), |ui| {
// Show app logo.
View::app_logo_name_version(ui);
ui.add_space(4.0);
let text = t!("wallets.create_desc");
ui.label(RichText::new(text)
.size(16.0)
.color(Colors::gray())
);
ui.add_space(8.0);
// Show wallet creation button.
let add_text = format!("{} {}", FOLDER_PLUS, t!("wallets.add"));
View::button(ui, add_text, Colors::white_or_black(false), || {
self.show_name_pass_modal(cb);
});
});
}
Some(step) => {
match step {
Step::EnterMnemonic => self.mnemonic_setup.ui(ui, cb),
Step::ConfirmMnemonic => self.mnemonic_setup.confirm_ui(ui, cb),
Step::SetupConnection => {
// Redraw if node is running.
if Node::is_running() {
ui.ctx().request_repaint_after(Node::STATS_UPDATE_DELAY);
}
self.network_setup.create_ui(ui, cb)
}
Step::EnterMnemonic => self.mnemonic_setup.ui(ui, cb),
Step::ConfirmMnemonic => self.mnemonic_setup.confirm_ui(ui, cb),
Step::SetupConnection => {
// Redraw if node is running.
if Node::is_running() && !Content::is_dual_panel_mode(ui) {
ui.ctx().request_repaint_after(Node::STATS_UPDATE_DELAY);
}
self.network_setup.create_ui(ui, cb)
}
}
}
/// Check if it's possible to go back for current step.
pub fn can_go_back(&self) -> bool {
self.step.is_some()
}
/// Back to previous wallet creation [`Step`].
pub fn back(&mut self) {
/// Back to previous wallet creation [`Step`], return `true` to close creation.
pub fn on_back(&mut self) -> bool {
match &self.step {
None => {}
Some(step) => {
match step {
Step::EnterMnemonic => {
self.step = None;
self.name_edit = String::from("");
self.pass_edit = String::from("");
self.mnemonic_setup.reset();
self.creation_error = None;
},
Step::ConfirmMnemonic => self.step = Some(Step::EnterMnemonic),
Step::SetupConnection => self.step = Some(Step::EnterMnemonic)
}
Step::ConfirmMnemonic => {
self.step = Step::EnterMnemonic;
false
},
Step::SetupConnection => {
self.creation_error = None;
self.step = Step::EnterMnemonic;
false
}
_ => true
}
}
/// Start wallet creation from showing [`Modal`] to enter name and password.
pub fn show_name_pass_modal(&mut self, cb: &dyn PlatformCallbacks) {
// Reset modal values.
self.modal_just_opened = true;
self.name_edit = t!("wallets.default_wallet");
self.pass_edit = String::from("");
// Show modal.
Modal::new(Self::NAME_PASS_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("wallets.add"))
.show();
cb.show_keyboard();
}
/// Draw creating wallet name/password input [`Modal`] content.
pub fn name_pass_modal_ui(&mut self,
ui: &mut egui::Ui,
modal: &Modal,
cb: &dyn PlatformCallbacks) {
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("wallets.name"))
.size(17.0)
.color(Colors::gray()));
ui.add_space(8.0);
// Show wallet name text edit.
let mut name_edit_opts = TextEditOptions::new(Id::from(modal.id).with("name"))
.no_focus();
if self.modal_just_opened {
self.modal_just_opened = false;
name_edit_opts.focus = true;
}
View::text_edit(ui, cb, &mut self.name_edit, &mut name_edit_opts);
ui.add_space(8.0);
ui.label(RichText::new(t!("wallets.pass"))
.size(17.0)
.color(Colors::gray()));
ui.add_space(8.0);
// Draw wallet password text edit.
let mut pass_text_edit_opts = TextEditOptions::new(Id::from(modal.id).with("pass"))
.password()
.no_focus();
View::text_edit(ui, cb, &mut self.pass_edit, &mut pass_text_edit_opts);
ui.add_space(12.0);
});
// Show modal buttons.
ui.scope(|ui| {
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
// Close modal.
cb.hide_keyboard();
modal.close();
});
});
columns[1].vertical_centered_justified(|ui| {
let mut on_next = || {
// Check if input values are not empty.
if self.name_edit.is_empty() || self.pass_edit.is_empty() {
return;
}
self.step = Some(Step::EnterMnemonic);
cb.hide_keyboard();
modal.close();
};
// Go to next creation step on Enter button press.
View::on_enter_key(ui, || {
(on_next)();
});
View::button(ui, t!("continue"), Colors::white_or_black(false), on_next);
});
});
ui.add_space(6.0);
});
}
}
}
+48 -165
View File
@@ -17,31 +17,23 @@ use egui::{Id, RichText};
use crate::gui::Colors;
use crate::gui::icons::PENCIL;
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{CameraContent, Modal, Content, View};
use crate::gui::views::types::{ModalContainer, ModalPosition, QrScanResult, TextEditOptions};
use crate::gui::views::{Modal, Content, View};
use crate::gui::views::types::{ModalContainer, ModalPosition, TextEditOptions};
use crate::wallet::Mnemonic;
use crate::wallet::types::{PhraseMode, PhraseSize};
use crate::wallet::types::{PhraseMode, PhraseSize, PhraseWord};
/// Mnemonic phrase setup content.
pub struct MnemonicSetup {
/// Current mnemonic phrase.
pub(crate) mnemonic: Mnemonic,
/// Flag to check if entered phrase was valid.
pub(crate) valid_phrase: bool,
pub mnemonic: Mnemonic,
/// Current word number to edit at [`Modal`].
word_num_edit: usize,
word_index_edit: usize,
/// Entered word value for [`Modal`].
word_edit: String,
/// Flag to check if entered word is valid.
/// Flag to check if entered word is valid at [`Modal`].
valid_word_edit: bool,
/// Camera content for QR scan [`Modal`].
camera_content: CameraContent,
/// Flag to check if recovery phrase was found at QR code scanning [`Modal`].
scan_phrase_not_found: Option<bool>,
/// [`Modal`] identifiers allowed at this ui container.
modal_ids: Vec<&'static str>
}
@@ -49,22 +41,15 @@ pub struct MnemonicSetup {
/// Identifier for word input [`Modal`].
pub const WORD_INPUT_MODAL: &'static str = "word_input_modal";
/// Identifier for QR code recovery phrase scan [`Modal`].
const QR_CODE_PHRASE_SCAN_MODAL: &'static str = "qr_code_rec_phrase_scan_modal";
impl Default for MnemonicSetup {
fn default() -> Self {
Self {
mnemonic: Mnemonic::default(),
valid_phrase: true,
word_num_edit: 0,
word_index_edit: 0,
word_edit: String::from(""),
valid_word_edit: true,
camera_content: CameraContent::default(),
scan_phrase_not_found: None,
modal_ids: vec![
WORD_INPUT_MODAL,
QR_CODE_PHRASE_SCAN_MODAL
WORD_INPUT_MODAL
]
}
}
@@ -81,7 +66,6 @@ impl ModalContainer for MnemonicSetup {
cb: &dyn PlatformCallbacks) {
match modal.id {
WORD_INPUT_MODAL => self.word_modal_ui(ui, modal, cb),
QR_CODE_PHRASE_SCAN_MODAL => self.scan_qr_modal_ui(ui, modal, cb),
_ => {}
}
}
@@ -103,7 +87,7 @@ impl MnemonicSetup {
ui.add_space(6.0);
// Show words setup.
self.word_list_ui(ui, self.mnemonic.mode == PhraseMode::Import, cb);
self.word_list_ui(ui, self.mnemonic.mode() == PhraseMode::Import, cb);
}
/// Draw content for phrase confirmation step.
@@ -123,7 +107,7 @@ impl MnemonicSetup {
/// Draw mode and size setup.
fn mode_type_ui(&mut self, ui: &mut egui::Ui) {
// Show mode setup.
let mut mode = self.mnemonic.mode.clone();
let mut mode = self.mnemonic.mode();
ui.columns(2, |columns| {
columns[0].vertical_centered(|ui| {
let create_mode = PhraseMode::Generate;
@@ -136,8 +120,8 @@ impl MnemonicSetup {
View::radio_value(ui, &mut mode, import_mode, import_text);
});
});
if mode != self.mnemonic.mode {
self.mnemonic.set_mode(mode)
if mode != self.mnemonic.mode() {
self.mnemonic.set_mode(mode);
}
ui.add_space(10.0);
@@ -150,7 +134,7 @@ impl MnemonicSetup {
ui.add_space(6.0);
// Show mnemonic phrase size setup.
let mut size = self.mnemonic.size.clone();
let mut size = self.mnemonic.size();
ui.columns(5, |columns| {
for (index, word) in PhraseSize::VALUES.iter().enumerate() {
columns[index].vertical_centered(|ui| {
@@ -159,29 +143,20 @@ impl MnemonicSetup {
});
}
});
if size != self.mnemonic.size {
if size != self.mnemonic.size() {
self.mnemonic.set_size(size);
}
}
/// Draw list of words for mnemonic phrase.
fn word_list_ui(&mut self, ui: &mut egui::Ui, edit_words: bool, cb: &dyn PlatformCallbacks) {
/// Draw grid of words for mnemonic phrase.
fn word_list_ui(&mut self, ui: &mut egui::Ui, edit: bool, cb: &dyn PlatformCallbacks) {
ui.add_space(6.0);
ui.scope(|ui| {
// Setup spacing between columns.
ui.spacing_mut().item_spacing = egui::Vec2::new(6.0, 6.0);
// Select list of words based on current mode and edit flag.
let words = match self.mnemonic.mode {
PhraseMode::Generate => {
if edit_words {
&self.mnemonic.confirm_words
} else {
&self.mnemonic.words
}
}
PhraseMode::Import => &self.mnemonic.words
}.clone();
let words = self.mnemonic.words(edit);
let mut word_number = 0;
let cols = list_columns_count(ui);
@@ -192,25 +167,25 @@ impl MnemonicSetup {
ui.columns(cols, |columns| {
columns[0].horizontal(|ui| {
let word = chunk.get(0).unwrap();
self.word_item_ui(ui, word_number, word, edit_words, cb);
self.word_item_ui(ui, word_number, word, edit, cb);
});
columns[1].horizontal(|ui| {
word_number += 1;
let word = chunk.get(1).unwrap();
self.word_item_ui(ui, word_number, word, edit_words, cb);
self.word_item_ui(ui, word_number, word, edit, cb);
});
if size > 2 {
columns[2].horizontal(|ui| {
word_number += 1;
let word = chunk.get(2).unwrap();
self.word_item_ui(ui, word_number, word, edit_words, cb);
self.word_item_ui(ui, word_number, word, edit, cb);
});
}
if size > 3 {
columns[3].horizontal(|ui| {
word_number += 1;
let word = chunk.get(3).unwrap();
self.word_item_ui(ui, word_number, word, edit_words, cb);
self.word_item_ui(ui, word_number, word, edit, cb);
});
}
});
@@ -218,7 +193,7 @@ impl MnemonicSetup {
ui.columns(cols, |columns| {
columns[0].horizontal(|ui| {
let word = chunk.get(0).unwrap();
self.word_item_ui(ui, word_number, word, edit_words, cb);
self.word_item_ui(ui, word_number, word, edit, cb);
});
});
}
@@ -227,20 +202,24 @@ impl MnemonicSetup {
ui.add_space(6.0);
}
/// Draw word list item for current mode.
/// Draw word grid item.
fn word_item_ui(&mut self,
ui: &mut egui::Ui,
num: usize,
word: &String,
word: &PhraseWord,
edit: bool,
cb: &dyn PlatformCallbacks) {
let color = if !word.valid || (word.text.is_empty() && !self.mnemonic.valid()) {
Colors::red()
} else {
Colors::white_or_black(true)
};
if edit {
ui.add_space(6.0);
View::button(ui, PENCIL.to_string(), Colors::button(), || {
// Setup modal values.
self.word_num_edit = num;
self.word_edit = word.clone();
self.valid_word_edit = true;
self.word_index_edit = num - 1;
self.word_edit = word.text.clone();
self.valid_word_edit = word.valid;
// Show word edit modal.
Modal::new(WORD_INPUT_MODAL)
.position(ModalPosition::CenterTop)
@@ -248,34 +227,33 @@ impl MnemonicSetup {
.show();
cb.show_keyboard();
});
ui.label(RichText::new(format!("#{} {}", num, word))
ui.label(RichText::new(format!("#{} {}", num, word.text))
.size(17.0)
.color(Colors::white_or_black(true)));
.color(color));
} else {
ui.add_space(12.0);
let text = format!("#{} {}", num, word);
ui.label(RichText::new(text).size(17.0).color(Colors::white_or_black(true)));
let text = format!("#{} {}", num, word.text);
ui.label(RichText::new(text).size(17.0).color(color));
}
}
/// Reset mnemonic phrase to default values.
/// Reset mnemonic phrase state to default values.
pub fn reset(&mut self) {
self.mnemonic = Mnemonic::default();
self.valid_phrase = true;
}
/// Draw word input [`Modal`] content.
fn word_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.enter_word", "number" => self.word_num_edit))
ui.label(RichText::new(t!("wallets.enter_word", "number" => self.word_index_edit + 1))
.size(17.0)
.color(Colors::gray()));
ui.add_space(8.0);
// Draw word value text edit.
let mut text_edit_opts = TextEditOptions::new(
Id::from(modal.id).with(self.word_num_edit)
Id::from(modal.id).with(self.word_index_edit)
);
View::text_edit(ui, cb, &mut self.word_edit, &mut text_edit_opts);
@@ -305,38 +283,22 @@ impl MnemonicSetup {
columns[1].vertical_centered_justified(|ui| {
// Callback to save the word.
let mut save = || {
self.word_edit = self.word_edit.trim().to_string();
// Check if word is valid.
if !self.mnemonic.is_valid_word(&self.word_edit) {
self.valid_word_edit = false;
// Insert word checking validity.
let word = &self.word_edit.trim().to_string();
self.valid_word_edit = self.mnemonic.insert(self.word_index_edit, word);
if !self.valid_word_edit {
return;
}
self.valid_word_edit = true;
// Select list where to save word.
let words = match self.mnemonic.mode {
PhraseMode::Generate => &mut self.mnemonic.confirm_words,
PhraseMode::Import => &mut self.mnemonic.words
};
// Save word at list.
let word_index = self.word_num_edit - 1;
words.remove(word_index);
words.insert(word_index, self.word_edit.clone());
// Close modal or go to next word to edit.
let close_modal = words.len() == self.word_num_edit
|| !words.get(self.word_num_edit).unwrap().is_empty();
let next_word = self.mnemonic.get(self.word_index_edit + 1);
let close_modal = next_word.is_none() ||
(!next_word.as_ref().unwrap().text.is_empty() &&
next_word.unwrap().valid);
if close_modal {
// Check if entered phrase was valid when all words were entered.
if !self.mnemonic.words.contains(&String::from("")) {
self.valid_phrase = self.mnemonic.is_valid_phrase();
}
cb.hide_keyboard();
modal.close();
} else {
self.word_num_edit += 1;
self.word_index_edit += 1;
self.word_edit = String::from("");
}
};
@@ -351,85 +313,6 @@ impl MnemonicSetup {
ui.add_space(6.0);
});
}
/// Show QR code recovery phrase scanner [`Modal`].
pub fn show_qr_scan_modal(&mut self, cb: &dyn PlatformCallbacks) {
self.scan_phrase_not_found = None;
// Show QR code scan modal.
Modal::new(QR_CODE_PHRASE_SCAN_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("scan_qr"))
.closeable(false)
.show();
cb.start_camera();
}
/// Draw QR code scan [`Modal`] content.
fn scan_qr_modal_ui(&mut self,
ui: &mut egui::Ui,
modal: &Modal,
cb: &dyn PlatformCallbacks) {
// Show scan result if exists or show camera content while scanning.
if let Some(_) = &self.scan_phrase_not_found {
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("wallets.rec_phrase_not_found"))
.size(17.0)
.color(Colors::red()));
});
ui.add_space(6.0);
} else if let Some(result) = self.camera_content.qr_scan_result() {
cb.stop_camera();
self.camera_content.clear_state();
match &result {
QrScanResult::Text(text) => {
self.mnemonic.import_text(text, false);
if self.mnemonic.is_valid_phrase() {
modal.close();
return;
}
}
_ => {}
}
// Set an error when found phrase was not valid.
self.scan_phrase_not_found = Some(true);
Modal::set_title(t!("scan_result"));
} else {
ui.add_space(6.0);
self.camera_content.ui(ui, cb);
ui.add_space(6.0);
}
if self.scan_phrase_not_found.is_some() {
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(ui, t!("close"), Colors::white_or_black(false), || {
self.scan_phrase_not_found = None;
modal.close();
});
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("repeat"), Colors::white_or_black(false), || {
Modal::set_title(t!("scan_qr"));
self.scan_phrase_not_found = None;
cb.start_camera();
});
});
});
} else {
ui.vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
cb.stop_camera();
modal.close();
});
});
}
ui.add_space(6.0);
}
}
/// Calculate word list columns count based on available ui width.
+116
View File
@@ -0,0 +1,116 @@
// Copyright 2024 The Grim Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use egui::{Id, RichText};
use grin_util::ZeroingString;
use crate::gui::Colors;
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Modal, View};
use crate::gui::views::types::TextEditOptions;
/// Initial wallet creation [`Modal`] content.
pub struct AddWalletModal {
/// Flag to check if it's first draw to focus on first field.
first_draw: bool,
/// Wallet name.
pub name_edit: String,
/// Password to encrypt created wallet.
pub pass_edit: String,
}
impl Default for AddWalletModal {
fn default() -> Self {
Self {
first_draw: true,
name_edit: t!("wallets.default_wallet"),
pass_edit: "".to_string(),
}
}
}
impl AddWalletModal {
/// Draw creating wallet name/password input [`Modal`] content.
pub fn ui(&mut self,
ui: &mut egui::Ui,
modal: &Modal,
cb: &dyn PlatformCallbacks,
mut on_input: impl FnMut(String, ZeroingString)) {
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("wallets.name"))
.size(17.0)
.color(Colors::gray()));
ui.add_space(8.0);
// Show wallet name text edit.
let mut name_edit_opts = TextEditOptions::new(Id::from(modal.id).with("name"))
.no_focus();
if self.first_draw {
self.first_draw = false;
name_edit_opts.focus = true;
}
View::text_edit(ui, cb, &mut self.name_edit, &mut name_edit_opts);
ui.add_space(8.0);
ui.label(RichText::new(t!("wallets.pass"))
.size(17.0)
.color(Colors::gray()));
ui.add_space(8.0);
// Draw wallet password text edit.
let mut pass_text_edit_opts = TextEditOptions::new(Id::from(modal.id).with("pass"))
.password()
.no_focus();
View::text_edit(ui, cb, &mut self.pass_edit, &mut pass_text_edit_opts);
ui.add_space(12.0);
});
// Show modal buttons.
ui.scope(|ui| {
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
// Close modal.
cb.hide_keyboard();
modal.close();
});
});
columns[1].vertical_centered_justified(|ui| {
let mut on_next = || {
let name = self.name_edit.clone();
let pass = self.pass_edit.clone();
if name.is_empty() || pass.is_empty() {
return;
}
cb.hide_keyboard();
modal.close();
on_input(name, ZeroingString::from(pass));
};
// Go to next creation step on Enter button press.
View::on_enter_key(ui, || {
(on_next)();
});
View::button(ui, t!("continue"), Colors::white_or_black(false), on_next);
});
});
ui.add_space(6.0);
});
}
}
+42 -44
View File
@@ -21,28 +21,24 @@ use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Modal, View};
use crate::gui::views::network::ConnectionsContent;
use crate::gui::views::network::modals::ExternalConnectionModal;
use crate::wallet::{ConnectionsConfig, ExternalConnection};
use crate::wallet::ConnectionsConfig;
use crate::wallet::types::ConnectionMethod;
/// Wallet connection [`Modal`] content.
/// Wallet connection selection [`Modal`] content.
pub struct WalletConnectionModal {
/// Current external connection.
pub ext_conn: Option<ExternalConnection>,
/// Current connection method.
pub conn: ConnectionMethod,
/// Flag to show connection creation.
show_conn_creation: bool,
/// External connection creation content.
add_ext_conn_content: ExternalConnectionModal
/// External connection content.
ext_conn_content: Option<ExternalConnectionModal>
}
impl WalletConnectionModal {
/// Create from provided wallet connection.
pub fn new(ext_conn: Option<ExternalConnection>) -> Self {
ExternalConnection::check_ext_conn_availability(None);
pub fn new(conn: ConnectionMethod) -> Self {
Self {
ext_conn,
show_conn_creation: false,
add_ext_conn_content: ExternalConnectionModal::new(None),
conn,
ext_conn_content: None,
}
}
@@ -51,17 +47,17 @@ impl WalletConnectionModal {
ui: &mut egui::Ui,
modal: &Modal,
cb: &dyn PlatformCallbacks,
on_select: impl Fn(Option<i64>)) {
ui.add_space(4.0);
// Draw external connection creation content.
if self.show_conn_creation {
self.add_ext_conn_content.ui(ui, cb, modal, |conn| {
on_select(Some(conn.id));
on_select: impl Fn(ConnectionMethod)) {
// Draw external connection content.
if let Some(ext_content) = self.ext_conn_content.as_mut() {
ext_content.ui(ui, cb, modal, |conn| {
on_select(ConnectionMethod::External(conn.id, conn.url));
});
return;
}
ui.add_space(4.0);
let ext_conn_list = ConnectionsConfig::ext_conn_list();
ScrollArea::vertical()
.max_height(if ext_conn_list.len() < 4 {
@@ -77,52 +73,54 @@ impl WalletConnectionModal {
// Show integrated node selection.
ConnectionsContent::integrated_node_item_ui(ui, |ui| {
let is_current_method = self.ext_conn.is_none();
if !is_current_method {
View::item_button(ui, View::item_rounding(0, 1, true), CHECK, None, || {
self.ext_conn = None;
on_select(None);
modal.close();
});
} else {
ui.add_space(14.0);
ui.label(RichText::new(CHECK_FAT).size(20.0).color(Colors::green()));
ui.add_space(14.0);
match self.conn {
ConnectionMethod::Integrated => {
ui.add_space(14.0);
ui.label(RichText::new(CHECK_FAT).size(20.0).color(Colors::green()));
ui.add_space(14.0);
}
_ => {
View::item_button(ui, View::item_rounding(0, 1, true), CHECK, None, || {
on_select(ConnectionMethod::Integrated);
modal.close();
});
}
}
});
// Show button to add new external node connection.
ui.add_space(8.0);
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("wallets.ext_conn"))
.size(16.0)
.color(Colors::gray()));
ui.add_space(6.0);
// Show button to add new external node connection.
let add_node_text = format!("{} {}", PLUS_CIRCLE, t!("wallets.add_node"));
View::button(ui, add_node_text, Colors::button(), || {
self.show_conn_creation = true;
self.ext_conn_content = Some(ExternalConnectionModal::new(None));
});
});
ui.add_space(4.0);
if !ext_conn_list.is_empty() {
ui.add_space(8.0);
for (index, conn) in ext_conn_list.iter().enumerate() {
for (index, conn) in ext_conn_list.iter().filter(|c| !c.deleted).enumerate() {
if conn.deleted {
continue;
}
ui.horizontal_wrapped(|ui| {
// Draw external connection item.
let len = ext_conn_list.len();
ConnectionsContent::ext_conn_item_ui(ui, conn, index, len, |ui| {
// Draw button to select connection.
let is_current_method = if let Some(c) = self.ext_conn.as_ref() {
c.id == conn.id
} else {
false
let current_ext_conn = match self.conn {
ConnectionMethod::Integrated => false,
ConnectionMethod::External(id, _) => id == conn.id
};
if !is_current_method {
if !current_ext_conn {
let button_rounding = View::item_rounding(index, len, true);
View::item_button(ui, button_rounding, CHECK, None, || {
self.ext_conn = Some(conn.clone());
on_select(Some(conn.id));
on_select(
ConnectionMethod::External(conn.id, conn.url.clone())
);
modal.close();
});
} else {
+7 -1
View File
@@ -16,4 +16,10 @@ mod conn;
pub use conn::*;
mod wallets;
pub use wallets::*;
pub use wallets::*;
mod open;
pub use open::*;
mod add;
pub use add::*;
+123
View File
@@ -0,0 +1,123 @@
// Copyright 2024 The Grim Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use egui::{Id, RichText};
use grin_util::ZeroingString;
use crate::gui::Colors;
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Modal, View};
use crate::gui::views::types::TextEditOptions;
use crate::wallet::Wallet;
/// Wallet opening [`Modal`] content.
pub struct OpenWalletModal {
/// Wallet to open.
wallet: Wallet,
/// Password to open wallet.
pass_edit: String,
/// Flag to check if wrong password was entered.
wrong_pass: bool,
/// Optional data to pass after wallet opening.
data: Option<String>,
}
impl OpenWalletModal {
/// Create new content instance.
pub fn new(wallet: Wallet, data: Option<String>) -> Self {
Self {
wallet,
pass_edit: "".to_string(),
wrong_pass: false,
data,
}
}
/// Draw [`Modal`] content.
pub fn ui(&mut self,
ui: &mut egui::Ui,
modal: &Modal,
cb: &dyn PlatformCallbacks,
mut on_continue: impl FnMut(Wallet, Option<String>)) {
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("wallets.pass"))
.size(17.0)
.color(Colors::gray()));
ui.add_space(8.0);
// Show password input.
let mut pass_edit_opts = TextEditOptions::new(Id::from(modal.id)).password();
View::text_edit(ui, cb, &mut self.pass_edit, &mut pass_edit_opts);
// Show information when password is empty.
if self.pass_edit.is_empty() {
self.wrong_pass = false;
ui.add_space(10.0);
ui.label(RichText::new(t!("wallets.pass_empty"))
.size(17.0)
.color(Colors::inactive_text()));
} else if self.wrong_pass {
ui.add_space(10.0);
ui.label(RichText::new(t!("wallets.wrong_pass"))
.size(17.0)
.color(Colors::red()));
}
ui.add_space(12.0);
});
// Show modal buttons.
ui.scope(|ui| {
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
// Close modal.
cb.hide_keyboard();
modal.close();
});
});
columns[1].vertical_centered_justified(|ui| {
// Callback for button to continue.
let mut on_continue = || {
let pass = self.pass_edit.clone();
if pass.is_empty() {
return;
}
match self.wallet.open(ZeroingString::from(pass)) {
Ok(_) => {
self.pass_edit = "".to_string();
cb.hide_keyboard();
modal.close();
on_continue(self.wallet.clone(), self.data.clone());
}
Err(_) => self.wrong_pass = true
}
};
// Continue on Enter key press.
View::on_enter_key(ui, || {
(on_continue)();
});
View::button(ui, t!("continue"), Colors::white_or_black(false), on_continue);
});
});
ui.add_space(6.0);
});
}
}
+86 -36
View File
@@ -16,29 +16,51 @@ use egui::scroll_area::ScrollBarVisibility;
use egui::{Align, Layout, RichText, ScrollArea};
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::types::ModalPosition;
use crate::gui::views::wallets::modals::OpenWalletModal;
use crate::gui::views::wallets::wallet::types::wallet_status_text;
use crate::wallet::{Wallet, WalletList};
use crate::wallet::types::ConnectionMethod;
/// Wallet list [`Modal`] content
pub struct WalletsModal {
/// Selected wallet id.
selected: Option<i64>
selected_id: Option<i64>,
/// Optional data to pass after wallet selection.
data: Option<String>,
/// Flag to check if wallet can be opened from the list.
can_open: bool,
/// Wallet opening content.
open_wallet_content: Option<OpenWalletModal>,
}
impl WalletsModal {
pub fn new(selected: Option<i64>) -> Self {
Self {
selected,
}
/// Create new content instance.
pub fn new(selected_id: Option<i64>, data: Option<String>, can_open: bool) -> Self {
Self { selected_id, data, can_open, open_wallet_content: None }
}
/// Draw [`Modal`] content.
/// Draw content.
pub fn ui(&mut self,
ui: &mut egui::Ui,
modal: &Modal,
wallets: &WalletList,
mut on_select: impl FnMut(i64)) {
cb: &dyn PlatformCallbacks,
mut on_select: impl FnMut(Wallet, Option<String>)) {
// Draw wallet opening modal content.
if let Some(open_content) = self.open_wallet_content.as_mut() {
open_content.ui(ui, modal, cb, |wallet, data| {
on_select(wallet, data);
self.data = None;
});
return;
}
ui.add_space(4.0);
ScrollArea::vertical()
.max_height(373.0)
@@ -48,10 +70,12 @@ impl WalletsModal {
.show(ui, |ui| {
ui.add_space(2.0);
ui.vertical_centered(|ui| {
let data = self.data.clone();
for wallet in wallets.list() {
// Draw wallet list item.
self.wallet_item_ui(ui, wallet, modal, |id| {
on_select(id);
self.wallet_item_ui(ui, wallet, || {
modal.close();
on_select(wallet.clone(), data.clone());
});
ui.add_space(5.0);
}
@@ -65,18 +89,18 @@ impl WalletsModal {
// Show button to close modal.
ui.vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
self.data = None;
modal.close();
});
});
ui.add_space(6.0);
}
/// Draw wallet list item.
/// Draw wallet list item with provided callback on select.
fn wallet_item_ui(&mut self,
ui: &mut egui::Ui,
wallet: &Wallet,
modal: &Modal,
mut on_select: impl FnMut(i64)) {
on_select: impl FnOnce()) {
let config = wallet.get_config();
let id = config.id;
@@ -87,16 +111,34 @@ impl WalletsModal {
ui.painter().rect(rect, rounding, Colors::fill(), View::hover_stroke());
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
// 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, || {
on_select(id);
modal.close();
if self.can_open {
// Show button to select or open closed wallet.
let icon = if wallet.is_open() {
CHECK
} else {
FOLDER_OPEN
};
View::item_button(ui, View::item_rounding(0, 1, true), icon, None, || {
if wallet.is_open() {
on_select();
} else {
Modal::change_position(ModalPosition::CenterTop);
self.open_wallet_content = Some(
OpenWalletModal::new(wallet.clone(), self.data.clone())
);
}
});
} else {
// Draw button to select wallet.
let current = self.selected_id.unwrap_or(0) == id;
if current {
ui.add_space(12.0);
ui.label(RichText::new(CHECK_FAT).size(20.0).color(Colors::green()));
} else {
View::item_button(ui, View::item_rounding(0, 1, true), CHECK, None, || {
on_select();
});
}
}
let layout_size = ui.available_size();
@@ -104,29 +146,37 @@ impl WalletsModal {
ui.add_space(6.0);
ui.vertical(|ui| {
ui.add_space(3.0);
// Setup wallet name text.
// Show wallet name text.
ui.with_layout(Layout::left_to_right(Align::Min), |ui| {
ui.add_space(1.0);
View::ellipsize_text(ui, config.name, 18.0, Colors::title(false));
});
// Setup wallet connection text.
let conn = if let Some(conn) = wallet.get_current_ext_conn() {
format!("{} {}", GLOBE_SIMPLE, conn.url)
} else {
format!("{} {}", COMPUTER_TOWER, t!("network.node"))
// Show wallet connection text.
let connection = wallet.get_current_connection();
let conn_text = match connection {
ConnectionMethod::Integrated => {
format!("{} {}", COMPUTER_TOWER, t!("network.node"))
}
ConnectionMethod::External(_, url) => format!("{} {}", GLOBE_SIMPLE, url)
};
View::ellipsize_text(ui, conn, 15.0, Colors::text(false));
ui.label(RichText::new(conn_text).size(15.0).color(Colors::text(false)));
ui.add_space(1.0);
// Setup wallet API text.
let address = if let Some(port) = config.api_port {
format!("127.0.0.1:{}", port)
// Show wallet API text or open status.
if self.can_open {
ui.label(RichText::new(wallet_status_text(wallet))
.size(15.0)
.color(Colors::gray()));
} else {
"-".to_string()
};
let api_text = format!("{} {}", PLUGS_CONNECTED, address);
ui.label(RichText::new(api_text).size(15.0).color(Colors::gray()));
let address = if let Some(port) = config.api_port {
format!("127.0.0.1:{}", port)
} else {
"-".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);
});
});
+128 -386
View File
@@ -13,57 +13,36 @@
// limitations under the License.
use std::time::Duration;
use egui::{Align, Id, Layout, Margin, RichText, ScrollArea};
use egui::scroll_area::ScrollBarVisibility;
use egui::{Align, Id, Layout, Margin, RichText};
use grin_chain::SyncStatus;
use grin_core::core::amount_to_hr_string;
use crate::AppConfig;
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::views::{CameraContent, Modal, Content, View};
use crate::gui::views::types::{ModalPosition, QrScanResult, TextEditOptions};
use crate::gui::views::{Modal, Content, View, CameraScanModal};
use crate::gui::views::types::{ModalPosition, QrScanResult};
use crate::gui::views::wallets::{WalletTransactions, WalletMessages, WalletTransport};
use crate::gui::views::wallets::types::{GRIN, WalletTab, WalletTabType};
use crate::gui::views::wallets::settings::WalletSettings;
use crate::gui::views::wallets::wallet::modals::WalletAccountsModal;
use crate::gui::views::wallets::wallet::WalletSettings;
use crate::node::Node;
use crate::wallet::{Wallet, WalletConfig};
use crate::wallet::types::{WalletAccount, WalletData};
use crate::wallet::{ExternalConnection, Wallet, WalletConfig};
use crate::wallet::types::{ConnectionMethod, WalletData};
/// Selected and opened wallet content.
/// Wallet content.
pub struct WalletContent {
/// List of wallet accounts for [`Modal`].
accounts: Vec<WalletAccount>,
/// Selected and opened wallet.
pub wallet: Wallet,
/// Flag to check if account is creating.
account_creating: bool,
/// Account label [`Modal`] value.
account_label_edit: String,
/// Flag to check if error occurred during account creation at [`Modal`].
account_creation_error: bool,
/// Camera content for QR scan [`Modal`].
camera_content: CameraContent,
/// QR code scan result
qr_scan_result: Option<QrScanResult>,
/// Wallet accounts [`Modal`] content.
accounts_modal_content: Option<WalletAccountsModal>,
/// QR code scan [`Modal`] content.
scan_modal_content: Option<CameraScanModal>,
/// Current tab content to show.
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())
}
}
pub current_tab: Box<dyn WalletTab>,
}
/// Identifier for account list [`Modal`].
@@ -73,24 +52,42 @@ const ACCOUNT_LIST_MODAL: &'static str = "account_list_modal";
const QR_CODE_SCAN_MODAL: &'static str = "qr_code_scan_modal";
impl WalletContent {
/// Create new instance with optional data.
pub fn new(wallet: Wallet, data: Option<String>) -> Self {
let mut content = Self {
wallet,
accounts_modal_content: None,
scan_modal_content: None,
current_tab: Box::new(WalletTransactions::default()),
};
if data.is_some() {
content.on_data(data);
}
content
}
/// Handle data from deeplink or opened file.
pub fn on_data(&mut self, data: Option<String>) {
// Provide data to messages.
self.current_tab = Box::new(WalletMessages::new(data));
}
/// Draw wallet content.
pub fn ui(&mut self,
ui: &mut egui::Ui,
wallet: &mut Wallet,
cb: &dyn PlatformCallbacks) {
// Show modal content for this ui container.
self.modal_content_ui(ui, wallet, cb);
pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
self.modal_content_ui(ui, cb);
let dual_panel = Content::is_dual_panel_mode(ui);
let wallet = &self.wallet;
let data = wallet.get_data();
let data_empty = data.is_none();
let hide_tabs = Self::block_navigation_on_sync(wallet);
// Show wallet balance panel not on Settings tab with selected non-repairing
// wallet, when there is no error and data is not empty.
let mut show_balance = self.current_tab.get_type() != WalletTabType::Settings && !data_empty
&& !wallet.sync_error() && !wallet.is_repairing() && !wallet.is_closing();
if wallet.get_current_ext_conn().is_none() && !Node::is_running() {
if wallet.get_current_connection() == ConnectionMethod::Integrated && !Node::is_running() {
show_balance = false;
}
egui::TopBottomPanel::top(Id::from("wallet_balance").with(wallet.identifier()))
@@ -126,13 +123,12 @@ impl WalletContent {
}
// Draw account info.
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
self.account_ui(ui, wallet, data.unwrap(), cb);
self.account_ui(ui, data.unwrap(), cb);
});
});
});
// Show wallet tabs panel.
let show_tabs = !Self::block_navigation_on_sync(wallet);
egui::TopBottomPanel::bottom("wallet_tabs_content")
.frame(egui::Frame {
fill: Colors::fill(),
@@ -144,11 +140,11 @@ impl WalletContent {
},
..Default::default()
})
.show_animated_inside(ui, show_tabs, |ui| {
.show_animated_inside(ui, !hide_tabs, |ui| {
ui.vertical_centered(|ui| {
// Draw wallet tabs.
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
self.tabs_ui(ui, wallet);
self.tabs_ui(ui);
});
});
});
@@ -171,7 +167,7 @@ impl WalletContent {
..Default::default()
})
.show_inside(ui, |ui| {
self.current_tab.ui(ui, wallet, cb);
self.current_tab.ui(ui, &self.wallet, cb);
});
// Refresh content after 1 second for synced wallet.
@@ -182,24 +178,70 @@ impl WalletContent {
}
}
/// Check when to block tabs navigation on sync progress.
pub fn block_navigation_on_sync(wallet: &Wallet) -> bool {
let sync_error = wallet.sync_error();
let integrated_node = wallet.get_current_connection() == ConnectionMethod::Integrated;
let integrated_node_ready = Node::get_sync_status() == Some(SyncStatus::NoSync);
let sync_after_opening = wallet.get_data().is_none() && !wallet.sync_error();
// Block navigation if wallet is repairing and integrated node is not launching
// and if wallet is closing or syncing after opening when there is no data to show.
(wallet.is_repairing() && (integrated_node_ready || !integrated_node) && !sync_error)
|| wallet.is_closing() || (sync_after_opening &&
(!integrated_node || integrated_node_ready))
}
/// Draw [`Modal`] content for this ui container.
fn modal_content_ui(&mut self,
ui: &mut egui::Ui,
wallet: &mut Wallet,
cb: &dyn PlatformCallbacks) {
fn modal_content_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
match Modal::opened() {
None => {}
Some(id) => {
match id {
ACCOUNT_LIST_MODAL => {
Modal::ui(ui.ctx(), |ui, modal| {
self.account_list_modal_ui(ui, wallet, modal, cb);
});
if let Some(content) = self.accounts_modal_content.as_mut() {
Modal::ui(ui.ctx(), |ui, modal| {
content.ui(ui, &self.wallet, modal, cb);
});
}
}
QR_CODE_SCAN_MODAL => {
Modal::ui(ui.ctx(), |ui, modal| {
self.scan_qr_modal_ui(ui, wallet, modal, cb);
});
let mut success = false;
if let Some(content) = self.scan_modal_content.as_mut() {
Modal::ui(ui.ctx(), |ui, modal| {
content.ui(ui, modal, cb, |result| {
match result {
QrScanResult::Slatepack(message) => {
success = true;
let msg = Some(message.to_string());
let messages = WalletMessages::new(msg);
self.current_tab = Box::new(messages);
return;
}
QrScanResult::Address(receiver) => {
success = true;
let balance = self.wallet.get_data()
.unwrap()
.info
.amount_currently_spendable;
if balance > 0 {
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;
}
}
_ => {}
}
if success {
modal.close();
}
});
});
}
if success {
self.scan_modal_content = None;
}
}
_ => {}
}
@@ -210,7 +252,6 @@ impl WalletContent {
/// Draw wallet account content.
fn account_ui(&mut self,
ui: &mut egui::Ui,
wallet: &Wallet,
data: WalletData,
cb: &dyn PlatformCallbacks) {
let mut rect = ui.available_rect_before_wrap();
@@ -220,11 +261,9 @@ impl WalletContent {
ui.painter().rect(rect, rounding, Colors::button(), View::hover_stroke());
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
// Draw button to scan QR code.
// Draw button to show QR code scanner.
View::item_button(ui, View::item_rounding(0, 2, true), SCAN, None, || {
self.qr_scan_result = None;
self.camera_content.clear_state();
// Show QR code scan modal.
self.scan_modal_content = Some(CameraScanModal::default());
Modal::new(QR_CODE_SCAN_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("scan_qr"))
@@ -235,11 +274,9 @@ impl WalletContent {
// Draw button to show list of accounts.
View::item_button(ui, View::item_rounding(1, 3, true), USERS_THREE, None, || {
// Load accounts.
self.account_label_edit = "".to_string();
self.accounts = wallet.accounts();
self.account_creating = false;
// Show account list modal.
self.accounts_modal_content = Some(
WalletAccountsModal::new(self.wallet.accounts())
);
Modal::new(ACCOUNT_LIST_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("wallets.accounts"))
@@ -263,7 +300,7 @@ impl WalletContent {
ui.add_space(-2.0);
// Show account label.
let account = wallet.get_config().account;
let account = self.wallet.get_config().account;
let default_acc_label = WalletConfig::DEFAULT_ACCOUNT_LABEL.to_string();
let acc_label = if account == default_acc_label {
t!("wallets.default_account")
@@ -274,15 +311,15 @@ impl WalletContent {
View::ellipsize_text(ui, acc_text, 15.0, Colors::text(false));
// Show confirmed height or sync progress.
let status_text = if !wallet.syncing() {
let status_text = if !self.wallet.syncing() {
format!("{} {}", PACKAGE, data.info.last_confirmed_height)
} else {
let info_progress = wallet.info_sync_progress();
let info_progress = self.wallet.info_sync_progress();
if info_progress == 100 || info_progress == 0 {
format!("{} {}", SPINNER, t!("wallets.wallet_loading"))
} else {
if wallet.is_repairing() {
let rep_progress = wallet.repairing_progress();
if self.wallet.is_repairing() {
let rep_progress = self.wallet.repairing_progress();
if rep_progress == 0 {
format!("{} {}", SPINNER, t!("wallets.wallet_checking"))
} else {
@@ -299,236 +336,18 @@ impl WalletContent {
}
}
};
View::animate_text(ui, status_text, 15.0, Colors::gray(), wallet.syncing());
View::animate_text(ui,
status_text,
15.0,
Colors::gray(),
self.wallet.syncing());
})
});
});
}
/// Draw account list [`Modal`] content.
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.
fn tabs_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet) {
fn tabs_ui(&mut self, ui: &mut egui::Ui) {
ui.scope(|ui| {
// Setup spacing between tabs.
ui.style_mut().spacing.item_spacing = egui::vec2(View::TAB_ITEMS_PADDING, 0.0);
@@ -539,26 +358,26 @@ impl WalletContent {
let current_type = self.current_tab.get_type();
ui.columns(4, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::tab_button(ui, GRAPH, current_type == WalletTabType::Txs, || {
View::tab_button(ui, GRAPH, current_type == WalletTabType::Txs, |_| {
self.current_tab = Box::new(WalletTransactions::default());
});
});
columns[1].vertical_centered_justified(|ui| {
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(
WalletMessages::new(wallet.can_use_dandelion(), None)
WalletMessages::new(None)
);
});
});
columns[2].vertical_centered_justified(|ui| {
View::tab_button(ui, BRIDGE, current_type == WalletTabType::Transport, || {
let addr = wallet.slatepack_address().unwrap();
self.current_tab = Box::new(WalletTransport::new(addr));
View::tab_button(ui, BRIDGE, current_type == WalletTabType::Transport, |_| {
self.current_tab = Box::new(WalletTransport::default());
});
});
columns[3].vertical_centered_justified(|ui| {
View::tab_button(ui, GEAR_FINE, current_type == WalletTabType::Settings, || {
View::tab_button(ui, GEAR_FINE, current_type == WalletTabType::Settings, |ui| {
ExternalConnection::check(None, ui.ctx());
self.current_tab = Box::new(WalletSettings::default());
});
});
@@ -574,7 +393,7 @@ impl WalletContent {
} else if wallet.is_closing() {
Self::sync_progress_ui(ui, wallet);
return true;
} else if wallet.get_current_ext_conn().is_none() {
} else if wallet.get_current_connection() == ConnectionMethod::Integrated {
if !Node::is_running() || Node::is_stopping() {
View::center_content(ui, 108.0, |ui| {
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.5, |ui| {
@@ -625,19 +444,6 @@ impl WalletContent {
});
}
/// Check when to block tabs navigation on sync progress.
pub fn block_navigation_on_sync(wallet: &Wallet) -> bool {
let sync_error = wallet.sync_error();
let integrated_node = wallet.get_current_ext_conn().is_none();
let integrated_node_ready = Node::get_sync_status() == Some(SyncStatus::NoSync);
let sync_after_opening = wallet.get_data().is_none() && !wallet.sync_error();
// Block navigation if wallet is repairing and integrated node is not launching
// and if wallet is closing or syncing after opening when there is no data to show.
(wallet.is_repairing() && (integrated_node_ready || !integrated_node) && !sync_error)
|| wallet.is_closing() || (sync_after_opening &&
(!integrated_node || integrated_node_ready))
}
/// Draw wallet sync progress content.
pub fn sync_progress_ui(ui: &mut egui::Ui, wallet: &Wallet) {
View::center_content(ui, 162.0, |ui| {
@@ -646,13 +452,13 @@ impl WalletContent {
ui.add_space(18.0);
// Setup sync progress text.
let text = {
let integrated_node = wallet.get_current_ext_conn().is_none();
let integrated_node_ready = Node::get_sync_status() == Some(SyncStatus::NoSync);
let int_node = wallet.get_current_connection() == ConnectionMethod::Integrated;
let int_ready = Node::get_sync_status() == Some(SyncStatus::NoSync);
let info_progress = wallet.info_sync_progress();
if wallet.is_closing() {
t!("wallets.wallet_closing")
} else if integrated_node && !integrated_node_ready {
} else if int_node && !int_ready {
t!("wallets.node_loading", "settings" => GEAR_FINE)
} else if wallet.is_repairing() {
let repair_progress = wallet.repairing_progress();
@@ -675,68 +481,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
@@ -0,0 +1,498 @@
// 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, CameraScanModal};
use crate::gui::views::types::{ModalPosition, QrScanResult};
use crate::gui::views::wallets::wallet::messages::request::MessageRequestModal;
use crate::gui::views::wallets::wallet::types::{SLATEPACK_MESSAGE_HINT, WalletTab, WalletTabType};
use crate::gui::views::wallets::wallet::{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,
/// Invoice or sending request creation [`Modal`] content.
request_modal_content: Option<MessageRequestModal>,
/// Wallet transaction [`Modal`] content.
tx_info_content: Option<WalletTransactionModal>,
/// Slatepacks message input text.
message_edit: String,
/// Flag to check if message request is loading.
message_loading: bool,
/// Error on finalization, parse or response creation.
message_error: String,
/// Parsed message result.
message_result: Arc<RwLock<Option<(Slate, Result<WalletTransaction, Error>)>>>,
/// QR code scanner [`Modal`] content.
scan_modal_content: Option<CameraScanModal>,
/// Button to parse picked file content.
file_pick_button: FilePickButton,
}
/// Identifier for amount input [`Modal`] to create invoice or sending request.
const REQUEST_MODAL: &'static str = "messages_request_modal";
/// Identifier for [`Modal`] modal to show transaction information.
const TX_INFO_MODAL: &'static str = "messages_tx_info_modal";
/// Identifier for [`Modal`] to scan Slatepack message from QR code.
const SCAN_QR_MODAL: &'static str = "messages_scan_qr_modal";
impl WalletTab for WalletMessages {
fn get_type(&self) -> WalletTabType {
WalletTabType::Messages
}
fn ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) {
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,
file_pick_button: FilePickButton::default(),
scan_modal_content: None,
}
}
/// Draw manual wallet transaction interaction content.
pub fn ui(&mut self,
ui: &mut egui::Ui,
wallet: &Wallet,
cb: &dyn PlatformCallbacks) {
if self.first_draw {
// Parse provided message on first draw.
if !self.message_edit.is_empty() {
self.parse_message(wallet);
}
self.first_draw = false;
}
ui.add_space(3.0);
// Show creation of request to send or receive funds.
self.request_ui(ui, wallet, cb);
ui.add_space(12.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(6.0);
// Show Slatepack message input field.
self.input_slatepack_ui(ui, wallet, cb);
ui.add_space(6.0);
}
/// Draw [`Modal`] content for this ui container.
fn modal_content_ui(&mut self,
ui: &mut egui::Ui,
wallet: &Wallet,
cb: &dyn PlatformCallbacks) {
match Modal::opened() {
None => {}
Some(id) => {
match id {
REQUEST_MODAL => {
if let Some(content) = self.request_modal_content.as_mut() {
Modal::ui(ui.ctx(), |ui, modal| {
content.ui(ui, wallet, modal, cb);
});
}
}
TX_INFO_MODAL => {
if let Some(content) = self.tx_info_content.as_mut() {
Modal::ui(ui.ctx(), |ui, modal| {
content.ui(ui, wallet, modal, cb);
});
}
}
SCAN_QR_MODAL => {
let mut result = None;
if let Some(content) = self.scan_modal_content.as_mut() {
Modal::ui(ui.ctx(), |ui, modal| {
content.ui(ui, modal, cb, |res| {
result = Some(res.clone());
modal.close();
});
});
}
if let Some(res) = result {
self.scan_modal_content = None;
match &res {
QrScanResult::Slatepack(text) => {
self.message_edit = text.to_string();
self.parse_message(wallet);
}
_ => {
self.message_edit = res.text();
self.message_error = t!("wallets.parse_slatepack_err");
}
}
}
}
_ => {}
}
}
}
}
/// Draw creation of request to send or receive funds.
fn request_ui(&mut self,
ui: &mut egui::Ui,
wallet: &Wallet,
cb: &dyn PlatformCallbacks) {
ui.label(RichText::new(t!("wallets.create_request_desc"))
.size(16.0)
.color(Colors::inactive_text()));
ui.add_space(7.0);
// Show send button only if balance is not empty.
let data = wallet.get_data().unwrap();
if data.info.amount_currently_spendable > 0 {
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
let send_text = format!("{} {}", UPLOAD_SIMPLE, t!("wallets.send"));
View::colored_text_button(ui, send_text, Colors::red(), Colors::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: &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| {
let scan_text = format!("{} {}", SCAN, t!("scan"));
View::button(ui, scan_text, Colors::button(), || {
self.message_edit.clear();
self.message_error.clear();
self.scan_modal_content = Some(CameraScanModal::default());
// Show QR code scan modal.
Modal::new(SCAN_QR_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("scan_qr"))
.closeable(false)
.show();
cb.start_camera();
});
});
columns[1].vertical_centered_justified(|ui| {
// Draw button to paste text from clipboard.
let paste = format!("{} {}", CLIPBOARD_TEXT, t!("paste"));
View::button(ui, paste, Colors::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");
}
}
}
@@ -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;
@@ -0,0 +1,260 @@
// Copyright 2024 The Grim Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::sync::Arc;
use std::thread;
use parking_lot::RwLock;
use egui::{Id, RichText};
use grin_core::core::{amount_from_hr_string, amount_to_hr_string};
use grin_wallet_libwallet::Error;
use crate::gui::Colors;
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Modal, View};
use crate::gui::views::types::TextEditOptions;
use crate::gui::views::wallets::wallet::WalletTransactionModal;
use crate::wallet::types::WalletTransaction;
use crate::wallet::Wallet;
/// Invoice or sending request creation [`Modal`] content.
pub struct MessageRequestModal {
/// Flag to check if invoice or sending request was opened.
invoice: bool,
/// Amount to send or receive.
amount_edit: String,
/// Flag to check if request is loading.
request_loading: bool,
/// Request result if there is no error.
request_result: Arc<RwLock<Option<Result<WalletTransaction, Error>>>>,
/// Flag to check if there is an error happened on request creation.
request_error: Option<String>,
/// Request result transaction content.
result_tx_content: Option<WalletTransactionModal>,
}
impl MessageRequestModal {
/// Create new content instance.
pub fn new(invoice: bool) -> Self {
Self {
invoice,
amount_edit: "".to_string(),
request_loading: false,
request_result: Arc::new(RwLock::new(None)),
request_error: None,
result_tx_content: None,
}
}
/// Draw [`Modal`] content.
pub fn ui(&mut self,
ui: &mut egui::Ui,
wallet: &Wallet,
modal: &Modal,
cb: &dyn PlatformCallbacks) {
// Draw transaction information on request result.
if let Some(tx) = self.result_tx_content.as_mut() {
tx.ui(ui, wallet, modal, cb);
return;
}
ui.add_space(6.0);
// Draw content on request loading.
if self.request_loading {
self.loading_request_ui(ui, wallet, modal);
return;
}
// Draw amount input content.
self.amount_input_ui(ui, wallet, modal, cb);
// Show request creation error.
if let Some(err) = &self.request_error {
ui.add_space(12.0);
ui.vertical_centered(|ui| {
ui.label(RichText::new(err)
.size(17.0)
.color(Colors::red()));
});
}
ui.add_space(12.0);
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
self.amount_edit = "".to_string();
self.request_error = None;
cb.hide_keyboard();
modal.close();
});
});
columns[1].vertical_centered_justified(|ui| {
// Button to create Slatepack message request.
View::button(ui, t!("continue"), Colors::white_or_black(false), || {
if self.amount_edit.is_empty() {
return;
}
if let Ok(a) = amount_from_hr_string(self.amount_edit.as_str()) {
cb.hide_keyboard();
modal.disable_closing();
// Setup data for request.
let wallet = wallet.clone();
let invoice = self.invoice.clone();
let result = self.request_result.clone();
// Send request at another thread.
self.request_loading = true;
thread::spawn(move || {
let res = if invoice {
wallet.issue_invoice(a)
} else {
wallet.send(a, None)
};
let mut w_result = result.write();
*w_result = Some(res);
});
} else {
let err = if self.invoice {
t!("wallets.invoice_slatepack_err")
} else {
t!("wallets.send_slatepack_err")
};
self.request_error = Some(err);
}
});
});
});
ui.add_space(6.0);
}
/// Draw amount input content.
fn amount_input_ui(&mut self,
ui: &mut egui::Ui,
wallet: &Wallet,
modal: &Modal,
cb: &dyn PlatformCallbacks) {
ui.vertical_centered(|ui| {
let enter_text = if self.invoice {
t!("wallets.enter_amount_receive")
} else {
let data = wallet.get_data().unwrap();
let amount = amount_to_hr_string(data.info.amount_currently_spendable, true);
t!("wallets.enter_amount_send","amount" => amount)
};
ui.label(RichText::new(enter_text)
.size(17.0)
.color(Colors::gray()));
});
ui.add_space(8.0);
// Draw request amount text input.
let amount_edit_id = Id::from(modal.id).with(wallet.get_config().id);
let mut amount_edit_opts = TextEditOptions::new(amount_edit_id).h_center();
let amount_edit_before = self.amount_edit.clone();
View::text_edit(ui, cb, &mut self.amount_edit, &mut amount_edit_opts);
// Check value if input was changed.
if amount_edit_before != self.amount_edit {
self.request_error = None;
if !self.amount_edit.is_empty() {
self.amount_edit = self.amount_edit.trim().replace(",", ".");
match amount_from_hr_string(self.amount_edit.as_str()) {
Ok(a) => {
if !self.amount_edit.contains(".") {
// To avoid input of several "0".
if a == 0 {
self.amount_edit = "0".to_string();
return;
}
} else {
// Check input after ".".
let parts = self.amount_edit
.split(".")
.collect::<Vec<&str>>();
if parts.len() == 2 && parts[1].len() > 9 {
self.amount_edit = amount_edit_before;
return;
}
}
// Do not input amount more than balance in sending.
if !self.invoice {
let b = wallet.get_data().unwrap().info.amount_currently_spendable;
if b < a {
self.amount_edit = amount_edit_before;
}
}
}
Err(_) => {
self.amount_edit = amount_edit_before;
}
}
}
}
}
/// Draw loading request content.
fn loading_request_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, modal: &Modal) {
ui.add_space(34.0);
ui.vertical_centered(|ui| {
View::big_loading_spinner(ui);
});
ui.add_space(50.0);
// Check if there is request result error.
if self.request_error.is_some() {
modal.enable_closing();
self.request_loading = false;
return;
}
// Update data on request result.
let r_request = self.request_result.read();
if r_request.is_some() {
modal.enable_closing();
let result = r_request.as_ref().unwrap();
match result {
Ok(tx) => {
self.result_tx_content = Some(WalletTransactionModal::new(wallet, tx, false));
}
Err(err) => {
match err {
Error::NotEnoughFunds { .. } => {
let m = t!(
"wallets.pay_balance_error",
"amount" => self.amount_edit
);
self.request_error = Some(m);
}
_ => {
let m = if self.invoice {
t!("wallets.invoice_slatepack_err")
} else {
t!("wallets.send_slatepack_err")
};
self.request_error = Some(m);
}
}
self.request_loading = false;
}
}
}
}
}
+7 -3
View File
@@ -13,10 +13,12 @@
// limitations under the License.
pub mod types;
pub mod settings;
mod settings;
pub use settings::*;
mod txs;
pub use txs::WalletTransactions;
pub use txs::*;
mod messages;
pub use messages::WalletMessages;
@@ -25,4 +27,6 @@ mod transport;
pub use transport::WalletTransport;
mod content;
pub use content::WalletContent;
pub use content::WalletContent;
mod modals;
@@ -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);
});
});
});
});
}
@@ -0,0 +1,16 @@
// Copyright 2024 The Grim Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
mod accounts;
pub use accounts::*;
+133
View 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);
}
}
+21 -14
View File
@@ -36,7 +36,7 @@ pub struct CommonSettings {
new_pass_edit: String,
/// Minimum confirmations number value.
min_confirmations_edit: String
min_confirmations_edit: String,
}
/// Identifier for wallet name [`Modal`].
@@ -54,25 +54,26 @@ impl Default for CommonSettings {
wrong_pass: false,
old_pass_edit: "".to_string(),
new_pass_edit: "".to_string(),
min_confirmations_edit: "".to_string()
min_confirmations_edit: "".to_string(),
}
}
}
impl CommonSettings {
pub fn ui(&mut self, ui: &mut egui::Ui, wallet: &mut Wallet, cb: &dyn PlatformCallbacks) {
/// Draw common wallet settings content.
pub fn ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) {
// Show modal content for this ui container.
self.modal_content_ui(ui, wallet, cb);
ui.vertical_centered(|ui| {
let wallet_name = wallet.get_config().name;
let config = wallet.get_config();
// Show wallet name.
ui.add_space(2.0);
ui.label(RichText::new(t!("wallets.name"))
.size(16.0)
.color(Colors::gray()));
ui.add_space(2.0);
ui.label(RichText::new(wallet_name.clone())
ui.label(RichText::new(&config.name)
.size(16.0)
.color(Colors::white_or_black(true)));
ui.add_space(8.0);
@@ -80,7 +81,7 @@ impl CommonSettings {
// Show wallet name setup.
let name_text = format!("{} {}", PENCIL, t!("change"));
View::button(ui, name_text, Colors::button(), || {
self.name_edit = wallet_name;
self.name_edit = config.name;
// Show wallet name modal.
Modal::new(NAME_EDIT_MODAL)
.position(ModalPosition::CenterTop)
@@ -118,10 +119,9 @@ impl CommonSettings {
ui.add_space(6.0);
// Show minimum amount of confirmations value setup.
let min_confirmations = wallet.get_config().min_confirmations;
let min_conf_text = format!("{} {}", CLOCK_COUNTDOWN, min_confirmations);
let min_conf_text = format!("{} {}", CLOCK_COUNTDOWN, config.min_confirmations);
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.
Modal::new(MIN_CONFIRMATIONS_EDIT_MODAL)
.position(ModalPosition::CenterTop)
@@ -131,15 +131,22 @@ impl CommonSettings {
});
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());
ui.add_space(4.0);
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,
wallet: &Wallet,
cb: &dyn PlatformCallbacks) {
match Modal::opened() {
None => {}
@@ -169,7 +176,7 @@ impl CommonSettings {
/// Draw wallet name [`Modal`] content.
fn name_modal_ui(&mut self,
ui: &mut egui::Ui,
wallet: &mut Wallet,
wallet: &Wallet,
modal: &Modal,
cb: &dyn PlatformCallbacks) {
ui.add_space(6.0);
@@ -223,7 +230,7 @@ impl CommonSettings {
/// Draw wallet pass [`Modal`] content.
fn pass_modal_ui(&mut self,
ui: &mut egui::Ui,
wallet: &mut Wallet,
wallet: &Wallet,
modal: &Modal,
cb: &dyn PlatformCallbacks) {
let wallet_id = wallet.get_config().id;
@@ -321,7 +328,7 @@ impl CommonSettings {
/// Draw wallet name [`Modal`] content.
fn min_conf_modal_ui(&mut self,
ui: &mut egui::Ui,
wallet: &mut Wallet,
wallet: &Wallet,
modal: &Modal,
cb: &dyn PlatformCallbacks) {
ui.add_space(6.0);
@@ -29,9 +29,6 @@ pub struct ConnectionSettings {
/// Selected connection method.
pub method: ConnectionMethod,
/// Current wallet external connection.
curr_ext_conn: Option<ExternalConnection>,
/// External connection [`Modal`] content.
ext_conn_modal: ExternalConnectionModal,
@@ -41,10 +38,8 @@ pub struct ConnectionSettings {
impl Default for ConnectionSettings {
fn default() -> Self {
ExternalConnection::check_ext_conn_availability(None);
Self {
method: ConnectionMethod::Integrated,
curr_ext_conn: None,
ext_conn_modal: ExternalConnectionModal::new(None),
modal_ids: vec![
ExternalConnectionModal::WALLET_ID
@@ -74,52 +69,29 @@ impl ModalContainer for ConnectionSettings {
impl ConnectionSettings {
/// Draw wallet creation setup content.
pub fn create_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
self.ui(ui, None, cb);
self.ui(ui, cb);
}
/// Draw existing wallet connection setup content.
pub fn wallet_ui(&mut self, ui: &mut egui::Ui, w: &mut Wallet, cb: &dyn PlatformCallbacks) {
// Setup connection value from provided wallet.
match w.get_config().ext_conn_id {
None => self.method = ConnectionMethod::Integrated,
Some(id) => self.method = ConnectionMethod::External(id)
}
pub fn wallet_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) {
self.method = wallet.get_current_connection();
// Draw setup content.
self.ui(ui, Some(w), cb);
let changed = self.ui(ui, cb);
// Setup wallet connection value after change.
let changed = match self.method {
ConnectionMethod::Integrated => {
let changed = w.get_current_ext_conn().is_some();
if changed {
w.update_ext_conn_id(None);
}
changed
}
ConnectionMethod::External(id) => {
let changed = w.get_config().ext_conn_id != Some(id);
if changed {
w.update_ext_conn_id(Some(id));
}
changed
}
};
// Reopen wallet if connection changed.
if changed {
if !w.reopen_needed() {
w.set_reopen(true);
w.close();
wallet.update_connection(&self.method);
// Reopen wallet if connection changed.
if !wallet.reopen_needed() {
wallet.set_reopen(true);
wallet.close();
}
}
}
/// Draw connection setup content.
fn ui(&mut self,
ui: &mut egui::Ui,
wallet: Option<&Wallet>,
cb: &dyn PlatformCallbacks) {
/// Draw connection setup content, returning `true` if connection was changed.
fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) -> bool {
let mut changed = false;
// Draw modal content for current ui container.
self.current_modal_ui(ui, cb);
@@ -129,14 +101,14 @@ impl ConnectionSettings {
ui.add_space(6.0);
ui.vertical_centered(|ui| {
// Show integrated node selection.
ui.add_space(6.0);
// Show integrated node selection.
ConnectionsContent::integrated_node_item_ui(ui, |ui| {
// Draw button to select integrated node if it was not selected.
let is_current_method = self.method == ConnectionMethod::Integrated;
if !is_current_method {
View::item_button(ui, View::item_rounding(0, 1, true), CHECK, None, || {
self.method = ConnectionMethod::Integrated;
changed = true;
});
} else {
ui.add_space(14.0);
@@ -145,7 +117,6 @@ impl ConnectionSettings {
}
});
// Show external connections.
ui.add_space(8.0);
ui.label(RichText::new(t!("wallets.ext_conn")).size(16.0).color(Colors::gray()));
ui.add_space(6.0);
@@ -153,45 +124,58 @@ impl ConnectionSettings {
// Show button to add new external node connection.
let add_node_text = format!("{} {}", PLUS_CIRCLE, t!("wallets.add_node"));
View::button(ui, add_node_text, Colors::button(), || {
self.show_add_ext_conn_modal(cb);
self.ext_conn_modal = ExternalConnectionModal::new(None);
Modal::new(ExternalConnectionModal::WALLET_ID)
.position(ModalPosition::CenterTop)
.title(t!("wallets.add_node"))
.show();
cb.show_keyboard();
});
ui.add_space(4.0);
let mut ext_conn_list = ConnectionsConfig::ext_conn_list();
// Check if current external connection was deleted to show at 1st place.
if let Some(wallet) = wallet {
if let Some(conn) = wallet.get_current_ext_conn() {
if ext_conn_list.iter()
.filter(|c| c.id == conn.id)
.collect::<Vec<&ExternalConnection>>().is_empty() {
if self.curr_ext_conn.is_none() {
self.curr_ext_conn = Some(conn);
}
ext_conn_list.insert(0, self.curr_ext_conn.as_ref().unwrap().clone());
}
// Check if it's current method.
let is_current = |m: &ConnectionMethod, c: &ExternalConnection| -> Option<bool> {
match m {
ConnectionMethod::External(id, _) => if c.deleted && *id == c.id {
None
} else {
Some(*id == c.id)
},
_ => Some(false)
}
}
};
if !ext_conn_list.is_empty() {
let method = &self.method.clone();
let ext_conn_list = ConnectionsConfig::ext_conn_list();
let ext_list = ext_conn_list.iter().filter(|c| {
!c.deleted || is_current(method, c).unwrap_or(true)
}).collect::<Vec<&ExternalConnection>>();
let ext_size = ext_list.len();
if ext_size != 0 {
ui.add_space(8.0);
for (index, conn) in ext_conn_list.iter().enumerate() {
for (i, c) in ext_list.iter().enumerate() {
ui.horizontal_wrapped(|ui| {
// Draw external connection item.
self.ext_conn_item_ui(ui, wallet, conn, index, ext_conn_list.len());
let is_current = is_current(method, c);
Self::ext_conn_item_ui(ui, c, is_current, i, ext_size, || {
self.method = ConnectionMethod::External(c.id, c.url.clone());
changed = true;
});
});
}
}
});
changed
}
/// Draw external connection item content.
fn ext_conn_item_ui(&mut self,
ui: &mut egui::Ui,
wallet: Option<&Wallet>,
fn ext_conn_item_ui(ui: &mut egui::Ui,
conn: &ExternalConnection,
is_current: Option<bool>,
index: usize,
len: usize) {
len: usize,
mut on_select: impl FnMut()) {
// Setup layout size.
let mut rect = ui.available_rect_before_wrap();
rect.set_height(52.0);
@@ -203,24 +187,15 @@ impl ConnectionSettings {
ui.vertical(|ui| {
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
// Draw button to select connection.
let is_current_method = if let Some(wallet) = wallet {
if let Some(cur) = wallet.get_config().ext_conn_id {
cur == conn.id
} else {
false
}
} else {
self.method == ConnectionMethod::External(conn.id)
};
if !is_current_method {
let button_rounding = View::item_rounding(index, len, true);
View::item_button(ui, button_rounding, CHECK, None, || {
self.method = ConnectionMethod::External(conn.id);
});
} else {
if is_current.unwrap_or(true) {
ui.add_space(12.0);
ui.label(RichText::new(CHECK_FAT).size(20.0).color(Colors::green()));
} else {
// Draw button to select connection.
let button_rounding = View::item_rounding(index, len, true);
View::item_button(ui, button_rounding, CHECK, None, || {
on_select();
});
}
let layout_size = ui.available_size();
@@ -235,7 +210,11 @@ impl ConnectionSettings {
// Setup connection status text.
let status_text = if let Some(available) = conn.available {
if available {
format!("{} {}", CHECK_CIRCLE, t!("network.available"))
format!("{} {}", CHECK_CIRCLE, if is_current.is_none() {
t!("transport.connected")
} else {
t!("network.available")
})
} else {
format!("{} {}", X_CIRCLE, t!("network.not_available"))
}
@@ -249,15 +228,4 @@ impl ConnectionSettings {
});
});
}
/// Show external connection adding [`Modal`].
fn show_add_ext_conn_modal(&mut self, cb: &dyn PlatformCallbacks) {
self.ext_conn_modal = ExternalConnectionModal::new(None);
// Show modal.
Modal::new(ExternalConnectionModal::WALLET_ID)
.position(ModalPosition::CenterTop)
.title(t!("wallets.add_node"))
.show();
cb.show_keyboard();
}
}
@@ -18,7 +18,7 @@ use egui::scroll_area::ScrollBarVisibility;
use crate::gui::Colors;
use crate::gui::platform::PlatformCallbacks;
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::WalletContent;
use crate::wallet::Wallet;
@@ -50,7 +50,7 @@ impl WalletTab for WalletSettings {
fn ui(&mut self,
ui: &mut egui::Ui,
wallet: &mut Wallet,
wallet: &Wallet,
cb: &dyn PlatformCallbacks) {
// Show loading progress if navigation is blocked.
if WalletContent::block_navigation_on_sync(wallet) {
@@ -22,6 +22,7 @@ use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Modal, View};
use crate::gui::views::types::{ModalPosition, TextEditOptions};
use crate::node::Node;
use crate::wallet::types::ConnectionMethod;
use crate::wallet::Wallet;
/// Wallet recovery settings content.
@@ -51,7 +52,7 @@ impl Default for RecoverySettings {
}
impl RecoverySettings {
pub fn ui(&mut self, ui: &mut egui::Ui, wallet: &mut Wallet, cb: &dyn PlatformCallbacks) {
pub fn ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) {
// Show modal content for this ui container.
self.modal_content_ui(ui, wallet, cb);
@@ -63,7 +64,7 @@ impl RecoverySettings {
ui.add_space(4.0);
ui.vertical_centered(|ui| {
let integrated_node = wallet.get_current_ext_conn().is_none();
let integrated_node = wallet.get_current_connection() == ConnectionMethod::Integrated;
let integrated_node_ready = Node::get_sync_status() == Some(SyncStatus::NoSync);
if wallet.sync_error() || (integrated_node && !integrated_node_ready) {
ui.add_space(2.0);
@@ -126,7 +127,7 @@ impl RecoverySettings {
View::colored_text_button(ui, delete_text, Colors::red(), Colors::button(), || {
Modal::new(DELETE_CONFIRMATION_MODAL)
.position(ModalPosition::Center)
.title(t!("modal.confirmation"))
.title(t!("confirmation"))
.show();
});
ui.add_space(8.0);
@@ -136,7 +137,7 @@ impl RecoverySettings {
/// Draw [`Modal`] content for this ui container.
fn modal_content_ui(&mut self,
ui: &mut egui::Ui,
wallet: &mut Wallet,
wallet: &Wallet,
cb: &dyn PlatformCallbacks) {
match Modal::opened() {
None => {}
@@ -175,7 +176,7 @@ impl RecoverySettings {
/// Draw recovery phrase [`Modal`] content.
fn recovery_phrase_modal_ui(&mut self,
ui: &mut egui::Ui,
wallet: &mut Wallet,
wallet: &Wallet,
modal: &Modal,
cb: &dyn PlatformCallbacks) {
ui.add_space(6.0);
@@ -232,7 +233,7 @@ impl RecoverySettings {
});
});
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()) {
Ok(phrase) => {
self.wrong_pass = false;
@@ -243,6 +244,12 @@ impl RecoverySettings {
self.wrong_pass = true;
}
}
};
View::on_enter_key(ui, || {
(on_next)();
});
View::button(ui, "OK".to_owned(), Colors::white_or_black(false), || {
on_next();
});
});
});
@@ -254,7 +261,7 @@ impl RecoverySettings {
/// Draw wallet deletion [`Modal`] content.
fn deletion_modal_ui(&mut self,
ui: &mut egui::Ui,
wallet: &mut Wallet,
wallet: &Wallet,
modal: &Modal) {
ui.add_space(8.0);
ui.vertical_centered(|ui| {
-944
View File
@@ -1,944 +0,0 @@
// Copyright 2023 The Grim Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::sync::Arc;
use std::thread;
use egui::{Align, Id, Layout, Margin, RichText, Rounding, ScrollArea};
use egui::os::OperatingSystem;
use egui::scroll_area::ScrollBarVisibility;
use parking_lot::RwLock;
use tor_rtcompat::BlockOn;
use tor_rtcompat::tokio::TokioNativeTlsRuntime;
use grin_core::core::{amount_from_hr_string, amount_to_hr_string};
use grin_wallet_libwallet::SlatepackAddress;
use crate::gui::Colors;
use crate::gui::icons::{CHECK_CIRCLE, COPY, DOTS_THREE_CIRCLE, EXPORT, GEAR_SIX, GLOBE_SIMPLE, POWER, QR_CODE, SHIELD_CHECKERED, SHIELD_SLASH, WARNING_CIRCLE, X_CIRCLE};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{CameraContent, Modal, QrCodeContent, Content, View};
use crate::gui::views::types::{ModalPosition, TextEditOptions};
use crate::gui::views::wallets::wallet::types::{WalletTab, WalletTabType};
use crate::gui::views::wallets::wallet::WalletContent;
use crate::tor::{Tor, TorBridge, TorConfig};
use crate::wallet::types::WalletData;
use crate::wallet::Wallet;
/// Wallet transport tab content.
pub struct WalletTransport {
/// Flag to check if transaction is sending over Tor to show progress at [`Modal`].
tor_sending: Arc<RwLock<bool>>,
/// Flag to check if error occurred during sending of transaction over Tor at [`Modal`].
tor_send_error: Arc<RwLock<bool>>,
/// Flag to check if transaction sent successfully over Tor [`Modal`].
tor_success: Arc<RwLock<bool>>,
/// Entered amount value for [`Modal`].
amount_edit: String,
/// Entered address value for [`Modal`].
address_edit: String,
/// Flag to check if entered address is incorrect at [`Modal`].
address_error: bool,
/// Flag to check if QR code scanner is opened at address [`Modal`].
show_address_scan: bool,
/// Address QR code scanner [`Modal`] content.
address_scan_content: CameraContent,
/// Flag to check if [`Modal`] was just opened to focus on first field.
modal_just_opened: bool,
/// QR code address image [`Modal`] content.
qr_address_content: QrCodeContent,
/// Flag to check if Tor settings were changed.
tor_settings_changed: bool,
/// Tor bridge binary path edit text.
bridge_bin_path_edit: String,
/// Tor bridge connection line edit text.
bridge_conn_line_edit: String,
/// Flag to check if QR code scanner is opened at bridge [`Modal`].
show_bridge_scan: bool,
/// Address QR code scanner [`Modal`] content.
bridge_qr_scan_content: CameraContent,
}
impl WalletTab for WalletTransport {
fn get_type(&self) -> WalletTabType {
WalletTabType::Transport
}
fn ui(&mut self,
ui: &mut egui::Ui,
wallet: &mut Wallet,
cb: &dyn PlatformCallbacks) {
if WalletContent::sync_ui(ui, wallet) {
return;
}
// Show modal content for this ui container.
self.modal_content_ui(ui, wallet, cb);
// Show transport content panel.
egui::CentralPanel::default()
.frame(egui::Frame {
stroke: View::item_stroke(),
fill: Colors::white_or_black(false),
inner_margin: Margin {
left: View::far_left_inset_margin(ui) + 4.0,
right: View::get_right_inset() + 4.0,
top: 3.0,
bottom: 4.0,
},
..Default::default()
})
.show_inside(ui, |ui| {
ScrollArea::vertical()
.id_source(Id::from("wallet_transport").with(wallet.get_config().id))
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.auto_shrink([false; 2])
.show(ui, |ui| {
ui.vertical_centered(|ui| {
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
self.ui(ui, wallet, cb);
});
});
});
});
}
}
/// Identifier for [`Modal`] to send amount over Tor.
const SEND_TOR_MODAL: &'static str = "send_tor_modal";
/// Identifier for [`Modal`] to setup Tor service.
const TOR_SETTINGS_MODAL: &'static str = "tor_settings_modal";
/// Identifier for [`Modal`] to show QR code address image.
const QR_ADDRESS_MODAL: &'static str = "qr_address_modal";
impl WalletTransport {
/// Create new content instance from provided Slatepack address text.
pub fn new(addr: String) -> Self {
// Setup Tor bridge binary path edit text.
let bridge = TorConfig::get_bridge();
let (bin_path, conn_line) = if let Some(b) = bridge {
(b.binary_path(), b.connection_line())
} else {
("".to_string(), "".to_string())
};
Self {
tor_sending: Arc::new(RwLock::new(false)),
tor_send_error: Arc::new(RwLock::new(false)),
tor_success: Arc::new(RwLock::new(false)),
amount_edit: "".to_string(),
address_edit: "".to_string(),
address_error: false,
show_address_scan: false,
address_scan_content: CameraContent::default(),
modal_just_opened: false,
qr_address_content: QrCodeContent::new(addr, false),
tor_settings_changed: false,
bridge_bin_path_edit: bin_path,
bridge_conn_line_edit: conn_line,
show_bridge_scan: false,
bridge_qr_scan_content: CameraContent::default(),
}
}
/// Draw wallet transport content.
pub fn ui(&mut self, ui: &mut egui::Ui, wallet: &mut Wallet, cb: &dyn PlatformCallbacks) {
ui.add_space(3.0);
ui.label(RichText::new(t!("transport.desc"))
.size(16.0)
.color(Colors::inactive_text()));
ui.add_space(7.0);
// Draw Tor content.
self.tor_ui(ui, wallet, cb);
}
/// Draw [`Modal`] content for this ui container.
fn modal_content_ui(&mut self,
ui: &mut egui::Ui,
wallet: &mut Wallet,
cb: &dyn PlatformCallbacks) {
match Modal::opened() {
None => {}
Some(id) => {
match id {
SEND_TOR_MODAL => {
Modal::ui(ui.ctx(), |ui, modal| {
self.send_tor_modal_ui(ui, wallet, modal, cb);
});
}
TOR_SETTINGS_MODAL => {
Modal::ui(ui.ctx(), |ui, modal| {
self.tor_settings_modal_ui(ui, wallet, modal, cb);
});
}
QR_ADDRESS_MODAL => {
Modal::ui(ui.ctx(), |ui, modal| {
self.qr_address_modal_ui(ui, modal, cb);
});
}
_ => {}
}
}
}
}
/// Draw Tor transport content.
fn tor_ui(&mut self, ui: &mut egui::Ui, wallet: &mut Wallet, cb: &dyn PlatformCallbacks) {
let data = wallet.get_data().unwrap();
// Draw header content.
self.tor_header_ui(ui, wallet);
// Draw receive info content.
if wallet.slatepack_address().is_some() {
self.tor_receive_ui(ui, wallet, &data, cb);
}
// Draw send content.
if data.info.amount_currently_spendable > 0 && wallet.foreign_api_port().is_some() {
self.tor_send_ui(ui, cb);
}
}
/// Draw Tor transport header content.
fn tor_header_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet) {
// Setup layout size.
let mut rect = ui.available_rect_before_wrap();
rect.set_height(78.0);
// Draw round background.
let bg_rect = rect.clone();
let item_rounding = View::item_rounding(0, 2, false);
ui.painter().rect(bg_rect, item_rounding, Colors::button(), View::item_stroke());
ui.vertical(|ui| {
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
// Draw button to setup Tor transport.
let button_rounding = View::item_rounding(0, 2, true);
View::item_button(ui, button_rounding, GEAR_SIX, None, || {
self.show_tor_settings_modal();
});
// Draw button to enable/disable Tor listener for current wallet.
let service_id = &wallet.identifier();
if !Tor::is_service_starting(service_id) && wallet.foreign_api_port().is_some() {
if !Tor::is_service_running(service_id) {
View::item_button(ui, Rounding::default(), POWER, Some(Colors::green()), || {
if let Ok(key) = wallet.secret_key() {
let api_port = wallet.foreign_api_port().unwrap();
Tor::start_service(api_port, key, service_id);
}
});
} else {
View::item_button(ui, Rounding::default(), POWER, Some(Colors::red()), || {
Tor::stop_service(service_id);
});
}
}
let layout_size = ui.available_size();
ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| {
ui.add_space(6.0);
ui.vertical(|ui| {
ui.add_space(3.0);
ui.with_layout(Layout::left_to_right(Align::Min), |ui| {
ui.add_space(1.0);
ui.label(RichText::new(t!("transport.tor_network"))
.size(18.0)
.color(Colors::title(false)));
});
// Setup Tor status text.
let is_running = Tor::is_service_running(service_id);
let is_starting = Tor::is_service_starting(service_id);
let has_error = Tor::is_service_failed(service_id);
let (icon, text) = if wallet.foreign_api_port().is_none() {
(DOTS_THREE_CIRCLE, t!("wallets.loading"))
} else if is_starting {
(DOTS_THREE_CIRCLE, t!("transport.connecting"))
} else if has_error {
(WARNING_CIRCLE, t!("transport.conn_error"))
} else if is_running {
(CHECK_CIRCLE, t!("transport.connected"))
} else {
(X_CIRCLE, t!("transport.disconnected"))
};
let status_text = format!("{} {}", icon, text);
ui.label(RichText::new(status_text).size(15.0).color(Colors::text(false)));
ui.add_space(1.0);
// Setup bridges status text.
let bridge = TorConfig::get_bridge();
let bridges_text = match &bridge {
None => {
format!("{} {}", SHIELD_SLASH, t!("transport.bridges_disabled"))
}
Some(b) => {
let name = b.protocol_name().to_uppercase();
format!("{} {}",
SHIELD_CHECKERED,
t!("transport.bridge_name", "b" = name))
}
};
ui.label(RichText::new(bridges_text).size(15.0).color(Colors::gray()));
});
});
});
});
}
/// Show Tor transport settings [`Modal`].
fn show_tor_settings_modal(&mut self) {
self.tor_settings_changed = false;
// Show Tor settings modal.
Modal::new(TOR_SETTINGS_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("transport.tor_settings"))
.closeable(false)
.show();
}
/// Draw Tor transport settings [`Modal`] content.
fn tor_settings_modal_ui(&mut self,
ui: &mut egui::Ui,
wallet: &Wallet,
modal: &Modal,
cb: &dyn PlatformCallbacks) {
ui.add_space(6.0);
// Draw QR code scanner content if requested.
if self.show_bridge_scan {
let mut on_stop = |content: &mut CameraContent| {
cb.stop_camera();
content.clear_state();
modal.enable_closing();
self.show_bridge_scan = false;
};
if let Some(result) = self.bridge_qr_scan_content.qr_scan_result() {
self.bridge_conn_line_edit = result.text();
on_stop(&mut self.bridge_qr_scan_content);
cb.show_keyboard();
} else {
self.bridge_qr_scan_content.ui(ui, cb);
ui.add_space(12.0);
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
// Show buttons to close modal or come back to sending input.
ui.columns(2, |cols| {
cols[0].vertical_centered_justified(|ui| {
View::button(ui, t!("close"), Colors::white_or_black(false), || {
on_stop(&mut self.bridge_qr_scan_content);
modal.close();
});
});
cols[1].vertical_centered_justified(|ui| {
View::button(ui, t!("back"), Colors::white_or_black(false), || {
on_stop(&mut self.bridge_qr_scan_content);
});
});
});
ui.add_space(6.0);
}
return;
}
// Do not show bridges setup on Android.
let os = OperatingSystem::from_target_os();
let show_bridges = os != OperatingSystem::Android;
if show_bridges {
let bridge = TorConfig::get_bridge();
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("transport.bridges_desc"))
.size(17.0)
.color(Colors::inactive_text()));
// Draw checkbox to enable/disable bridges.
View::checkbox(ui, bridge.is_some(), t!("transport.bridges"), || {
// Save value.
let value = if bridge.is_some() {
None
} else {
let default_bridge = TorConfig::get_obfs4();
self.bridge_bin_path_edit = default_bridge.binary_path();
self.bridge_conn_line_edit = default_bridge.connection_line();
Some(default_bridge)
};
TorConfig::save_bridge(value);
self.tor_settings_changed = true;
});
});
// Draw bridges selection and path.
if bridge.is_some() {
let current_bridge = bridge.unwrap();
let mut bridge = current_bridge.clone();
ui.add_space(6.0);
ui.columns(2, |columns| {
columns[0].vertical_centered(|ui| {
// Draw Obfs4 bridge selector.
let obfs4 = TorConfig::get_obfs4();
let name = obfs4.protocol_name().to_uppercase();
View::radio_value(ui, &mut bridge, obfs4, name);
});
columns[1].vertical_centered(|ui| {
// Draw Snowflake bridge selector.
let snowflake = TorConfig::get_snowflake();
let name = snowflake.protocol_name().to_uppercase();
View::radio_value(ui, &mut bridge, snowflake, name);
});
});
ui.add_space(12.0);
// Check if bridge type was changed to save.
if current_bridge != bridge {
self.tor_settings_changed = true;
TorConfig::save_bridge(Some(bridge.clone()));
self.bridge_bin_path_edit = bridge.binary_path();
self.bridge_conn_line_edit = bridge.connection_line();
}
// Draw binary path text edit.
let bin_edit_id = Id::from(modal.id)
.with(wallet.get_config().id)
.with("_bin_edit");
let mut bin_edit_opts = TextEditOptions::new(bin_edit_id)
.paste()
.no_focus();
let bin_edit_before = self.bridge_bin_path_edit.clone();
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("transport.bin_file"))
.size(17.0)
.color(Colors::inactive_text()));
ui.add_space(6.0);
View::text_edit(ui, cb, &mut self.bridge_bin_path_edit, &mut bin_edit_opts);
ui.add_space(6.0);
});
// Draw connection line text edit.
let conn_edit_before = self.bridge_conn_line_edit.clone();
let conn_edit_id = Id::from(modal.id)
.with(wallet.get_config().id)
.with("_conn_edit");
let mut conn_edit_opts = TextEditOptions::new(conn_edit_id)
.paste()
.no_focus()
.scan_qr();
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("transport.conn_line"))
.size(17.0)
.color(Colors::inactive_text()));
ui.add_space(6.0);
View::text_edit(ui, cb, &mut self.bridge_conn_line_edit, &mut conn_edit_opts);
// Check if scan button was pressed.
if conn_edit_opts.scan_pressed {
cb.hide_keyboard();
modal.disable_closing();
conn_edit_opts.scan_pressed = false;
self.show_bridge_scan = true;
}
});
// Check if bin path or connection line text was changed to save bridge.
if conn_edit_before != self.bridge_conn_line_edit ||
bin_edit_before != self.bridge_bin_path_edit {
let bin_path = self.bridge_bin_path_edit.trim().to_string();
let conn_line = self.bridge_conn_line_edit.trim().to_string();
let b = match bridge {
TorBridge::Snowflake(_, _) => {
TorBridge::Snowflake(bin_path, conn_line)
},
TorBridge::Obfs4(_, _) => {
TorBridge::Obfs4(bin_path, conn_line)
}
};
TorConfig::save_bridge(Some(b));
self.tor_settings_changed = true;
}
ui.add_space(2.0);
}
ui.add_space(6.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(6.0);
}
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("transport.tor_autorun_desc"))
.size(17.0)
.color(Colors::inactive_text()));
// Show Tor service autorun checkbox.
let autorun = wallet.auto_start_tor_listener();
View::checkbox(ui, autorun, t!("network.autorun"), || {
wallet.update_auto_start_tor_listener(!autorun);
});
});
ui.add_space(6.0);
ui.vertical_centered_justified(|ui| {
View::button(ui, t!("close"), Colors::white_or_black(false), || {
if self.tor_settings_changed {
self.tor_settings_changed = false;
// Restart running service or rebuild client.
let service_id = &wallet.identifier();
if Tor::is_service_running(service_id) {
if let Ok(key) = wallet.secret_key() {
let api_port = wallet.foreign_api_port().unwrap();
Tor::restart_service(api_port, key, service_id);
}
} else {
Tor::rebuild_client();
}
}
modal.close();
});
});
ui.add_space(6.0);
}
/// Draw Tor receive content.
fn tor_receive_ui(&mut self,
ui: &mut egui::Ui,
wallet: &Wallet,
data: &WalletData,
cb: &dyn PlatformCallbacks) {
let slatepack_addr = wallet.slatepack_address().unwrap();
let service_id = &wallet.identifier();
let can_send = data.info.amount_currently_spendable > 0;
// Setup layout size.
let mut rect = ui.available_rect_before_wrap();
rect.set_height(52.0);
// Draw round background.
let bg_rect = rect.clone();
let item_rounding = if can_send {
View::item_rounding(1, 3, false)
} else {
View::item_rounding(1, 2, false)
};
ui.painter().rect(bg_rect, item_rounding, Colors::button(), View::item_stroke());
ui.vertical(|ui| {
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
// Draw button to setup Tor transport.
let button_rounding = if can_send {
View::item_rounding(1, 3, true)
} else {
View::item_rounding(1, 2, true)
};
View::item_button(ui, button_rounding, QR_CODE, None, || {
// Show QR code image address modal.
self.qr_address_content.clear_state();
Modal::new(QR_ADDRESS_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("network_mining.address"))
.show();
});
// Show button to enable/disable Tor listener for current wallet.
View::item_button(ui, Rounding::default(), COPY, None, || {
cb.copy_string_to_buffer(slatepack_addr.clone());
});
let layout_size = ui.available_size();
ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| {
ui.add_space(6.0);
ui.vertical(|ui| {
ui.add_space(3.0);
// Show wallet Slatepack address.
let address_color = if Tor::is_service_starting(service_id) ||
wallet.foreign_api_port().is_none() {
Colors::inactive_text()
} else if Tor::is_service_running(service_id) {
Colors::green()
} else {
Colors::red()
};
View::ellipsize_text(ui, slatepack_addr, 15.0, address_color);
let address_label = format!("{} {}",
GLOBE_SIMPLE,
t!("network_mining.address"));
ui.label(RichText::new(address_label).size(15.0).color(Colors::gray()));
});
});
});
});
}
/// Draw QR code image address [`Modal`] content.
fn qr_address_modal_ui(&mut self, ui: &mut egui::Ui, m: &Modal, cb: &dyn PlatformCallbacks) {
ui.add_space(6.0);
// Draw QR code content.
let text = self.qr_address_content.text.clone();
self.qr_address_content.ui(ui, text.clone(), cb);
ui.vertical_centered_justified(|ui| {
View::button(ui, t!("close"), Colors::white_or_black(false), || {
self.qr_address_content.clear_state();
m.close();
});
});
ui.add_space(6.0);
}
/// Draw Tor send content.
fn tor_send_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
// Setup layout size.
let mut rect = ui.available_rect_before_wrap();
rect.set_height(55.0);
// Draw round background.
let bg_rect = rect.clone();
let item_rounding = View::item_rounding(1, 2, false);
ui.painter().rect(bg_rect, item_rounding, Colors::fill(), View::item_stroke());
ui.vertical(|ui| {
ui.allocate_ui_with_layout(rect.size(), Layout::top_down(Align::Center), |ui| {
ui.add_space(7.0);
// Draw button to open sending modal.
let send_text = format!("{} {}", EXPORT, t!("wallets.send"));
View::button(ui, send_text, Colors::white_or_black(false), || {
self.show_send_tor_modal(cb, None);
});
});
});
}
/// Show [`Modal`] to send over Tor.
pub fn show_send_tor_modal(&mut self, cb: &dyn PlatformCallbacks, address: Option<String>) {
{
let mut w_send_err = self.tor_send_error.write();
*w_send_err = false;
let mut w_sending = self.tor_sending.write();
*w_sending = false;
let mut w_success = self.tor_success.write();
*w_success = false;
}
self.modal_just_opened = true;
self.amount_edit = "".to_string();
self.address_edit = address.unwrap_or("".to_string());
self.address_error = false;
// Show modal.
Modal::new(SEND_TOR_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("wallets.send"))
.show();
cb.show_keyboard();
}
/// Check if error occurred during sending over Tor at [`Modal`].
fn has_tor_send_error(&self) -> bool {
let r_send_err = self.tor_send_error.read();
r_send_err.clone()
}
/// Check if transaction is sending over Tor to show progress at [`Modal`].
fn tor_sending(&self) -> bool {
let r_sending = self.tor_sending.read();
r_sending.clone()
}
/// Check if transaction sent over Tor with success at [`Modal`].
fn tor_success(&self) -> bool {
let r_success = self.tor_success.read();
r_success.clone()
}
/// Draw amount input [`Modal`] content to send over Tor.
fn send_tor_modal_ui(&mut self,
ui: &mut egui::Ui,
wallet: &mut Wallet,
modal: &Modal,
cb: &dyn PlatformCallbacks) {
ui.add_space(6.0);
let has_send_err = self.has_tor_send_error();
let sending = self.tor_sending();
if !has_send_err && !sending {
// Draw QR code scanner content if requested.
if self.show_address_scan {
let mut on_stop = |content: &mut CameraContent| {
cb.stop_camera();
content.clear_state();
modal.enable_closing();
self.show_address_scan = false;
};
if let Some(result) = self.address_scan_content.qr_scan_result() {
self.address_edit = result.text();
self.modal_just_opened = true;
on_stop(&mut self.address_scan_content);
cb.show_keyboard();
} else {
self.address_scan_content.ui(ui, cb);
ui.add_space(6.0);
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
// Show buttons to close modal or come back to sending input.
ui.columns(2, |cols| {
cols[0].vertical_centered_justified(|ui| {
View::button(ui, t!("close"), Colors::white_or_black(false), || {
on_stop(&mut self.address_scan_content);
modal.close();
});
});
cols[1].vertical_centered_justified(|ui| {
View::button(ui, t!("back"), Colors::white_or_black(false), || {
self.modal_just_opened = true;
on_stop(&mut self.address_scan_content);
cb.show_keyboard();
});
});
});
ui.add_space(6.0);
}
return;
}
ui.vertical_centered(|ui| {
let data = wallet.get_data().unwrap();
let amount = amount_to_hr_string(data.info.amount_currently_spendable, true);
let enter_text = t!("wallets.enter_amount_send","amount" => amount);
ui.label(RichText::new(enter_text)
.size(17.0)
.color(Colors::gray()));
});
ui.add_space(8.0);
// Draw amount text edit.
let amount_edit_id = Id::from(modal.id).with("amount").with(wallet.get_config().id);
let mut amount_edit_opts = TextEditOptions::new(amount_edit_id).h_center().no_focus();
let amount_edit_before = self.amount_edit.clone();
if self.modal_just_opened {
self.modal_just_opened = false;
amount_edit_opts.focus = true;
}
View::text_edit(ui, cb, &mut self.amount_edit, &mut amount_edit_opts);
ui.add_space(8.0);
// Check value if input was changed.
if amount_edit_before != self.amount_edit {
if !self.amount_edit.is_empty() {
// Trim text, replace "," by "." and parse amount.
self.amount_edit = self.amount_edit.trim().replace(",", ".");
match amount_from_hr_string(self.amount_edit.as_str()) {
Ok(a) => {
if !self.amount_edit.contains(".") {
// To avoid input of several "0".
if a == 0 {
self.amount_edit = "0".to_string();
return;
}
} else {
// Check input after ".".
let parts = self.amount_edit.split(".").collect::<Vec<&str>>();
if parts.len() == 2 && parts[1].len() > 9 {
self.amount_edit = amount_edit_before;
return;
}
}
// Do not input amount more than balance in sending.
let b = wallet.get_data().unwrap().info.amount_currently_spendable;
if b < a {
self.amount_edit = amount_edit_before;
}
}
Err(_) => {
self.amount_edit = amount_edit_before;
}
}
}
}
// Show address error or input description.
ui.vertical_centered(|ui| {
if self.address_error {
ui.label(RichText::new(t!("transport.incorrect_addr_err"))
.size(17.0)
.color(Colors::red()));
} else {
ui.label(RichText::new(t!("transport.receiver_address"))
.size(17.0)
.color(Colors::gray()));
}
});
ui.add_space(6.0);
// Draw address text edit.
let addr_edit_before = self.address_edit.clone();
let address_edit_id = Id::from(modal.id).with("address").with(wallet.get_config().id);
let mut address_edit_opts = TextEditOptions::new(address_edit_id)
.paste()
.no_focus()
.scan_qr();
View::text_edit(ui, cb, &mut self.address_edit, &mut address_edit_opts);
// Check if scan button was pressed.
if address_edit_opts.scan_pressed {
cb.hide_keyboard();
modal.disable_closing();
address_edit_opts.scan_pressed = false;
self.show_address_scan = true;
}
ui.add_space(12.0);
// Check value if input was changed.
if addr_edit_before != self.address_edit {
self.address_error = false;
}
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
self.amount_edit = "".to_string();
self.address_edit = "".to_string();
cb.hide_keyboard();
modal.close();
});
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("continue"), Colors::white_or_black(false), || {
if self.amount_edit.is_empty() {
return;
}
// Check entered address.
let addr_str = self.address_edit.as_str();
if let Ok(addr) = SlatepackAddress::try_from(addr_str) {
// Parse amount and send over Tor.
if let Ok(a) = amount_from_hr_string(self.amount_edit.as_str()) {
cb.hide_keyboard();
modal.disable_closing();
let mut w_sending = self.tor_sending.write();
*w_sending = true;
{
let send_error = self.tor_send_error.clone();
let send_success = self.tor_success.clone();
let mut wallet = wallet.clone();
thread::spawn(move || {
let runtime = TokioNativeTlsRuntime::create().unwrap();
runtime
.block_on(async {
if wallet.send_tor(a, &addr)
.await
.is_some() {
let mut w_send_success = send_success.write();
*w_send_success = true;
} else {
let mut w_send_error = send_error.write();
*w_send_error = true;
}
});
});
}
}
} else {
self.address_error = true;
}
});
});
});
ui.add_space(6.0);
} else if has_send_err {
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("transport.tor_send_error"))
.size(17.0)
.color(Colors::red()));
});
ui.add_space(12.0);
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
self.amount_edit = "".to_string();
self.address_edit = "".to_string();
cb.hide_keyboard();
modal.close();
});
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("repeat"), Colors::white_or_black(false), || {
// Parse amount and send over Tor.
if let Ok(a) = amount_from_hr_string(self.amount_edit.as_str()) {
let mut w_send_error = self.tor_send_error.write();
*w_send_error = false;
let mut w_sending = self.tor_sending.write();
*w_sending = true;
{
let addr_text = self.address_edit.clone();
let send_error = self.tor_send_error.clone();
let send_success = self.tor_success.clone();
let mut wallet = wallet.clone();
thread::spawn(move || {
let runtime = TokioNativeTlsRuntime::create().unwrap();
runtime
.block_on(async {
let addr_str = addr_text.as_str();
let addr = &SlatepackAddress::try_from(addr_str)
.unwrap();
if wallet.send_tor(a, &addr)
.await
.is_some() {
let mut w_send_success = send_success.write();
*w_send_success = true;
} else {
let mut w_send_error = send_error.write();
*w_send_error = true;
}
});
});
}
}
});
});
});
ui.add_space(6.0);
} else {
ui.add_space(16.0);
ui.vertical_centered(|ui| {
View::small_loading_spinner(ui);
ui.add_space(12.0);
ui.label(RichText::new(t!("transport.tor_sending", "amount" => self.amount_edit))
.size(17.0)
.color(Colors::gray()));
});
ui.add_space(10.0);
// Close modal on success sending.
if self.tor_success() {
modal.close();
}
}
}
}
@@ -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: &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: &Wallet, cb: &dyn PlatformCallbacks) {
ui.add_space(3.0);
ui.label(RichText::new(t!("transport.desc"))
.size(16.0)
.color(Colors::inactive_text()));
ui.add_space(7.0);
// Draw Tor transport content.
self.tor_ui(ui, wallet, cb);
}
/// Draw [`Modal`] content for this ui container.
fn modal_content_ui(&mut self,
ui: &mut egui::Ui,
wallet: &Wallet,
cb: &dyn PlatformCallbacks) {
match Modal::opened() {
None => {}
Some(id) => {
match id {
SEND_TOR_MODAL => {
if let Some(content) = self.send_modal_content.as_mut() {
Modal::ui(ui.ctx(), |ui, modal| {
content.ui(ui, wallet, modal, cb);
});
}
}
TOR_SETTINGS_MODAL => {
if let Some(content) = self.settings_modal_content.as_mut() {
Modal::ui(ui.ctx(), |ui, modal| {
content.ui(ui, wallet, modal, cb);
});
}
}
QR_ADDRESS_MODAL => {
Modal::ui(ui.ctx(), |ui, modal| {
self.qr_address_modal_ui(ui, modal, cb);
});
}
_ => {}
}
}
}
}
/// Draw Tor transport content.
fn tor_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) {
let data = wallet.get_data().unwrap();
// Draw header content.
self.tor_header_ui(ui, wallet);
// Draw receive info content.
if wallet.slatepack_address().is_some() {
self.tor_receive_ui(ui, wallet, &data, cb);
}
// Draw send content.
let service_id = &wallet.identifier();
if data.info.amount_currently_spendable > 0 && wallet.foreign_api_port().is_some() &&
!Tor::is_service_starting(service_id) {
self.tor_send_ui(ui, cb);
}
}
/// Draw Tor transport header content.
fn tor_header_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet) {
// Setup layout size.
let mut rect = ui.available_rect_before_wrap();
rect.set_height(78.0);
// Draw round background.
let bg_rect = rect.clone();
let item_rounding = View::item_rounding(0, 2, false);
ui.painter().rect(bg_rect, item_rounding, Colors::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();
}
}
@@ -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;
@@ -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: &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;
}
}
}
@@ -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
View 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: &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: &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!("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: &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.height.is_none() || (tx.height.unwrap() != 0 &&
height - tx.height.unwrap() > min_conf - 1) {
let (i, t) = if tx.data.tx_type == TxLogEntryType::TxSent {
(ARROW_CIRCLE_UP, t!("wallets.tx_sent"))
} else {
(ARROW_CIRCLE_DOWN, t!("wallets.tx_received"))
};
format!("{} {}", i, t)
} else {
let tx_height = tx.height.unwrap() - 1;
let left_conf = height - tx_height;
let conf_info = if tx_height != 0 && height >= tx_height &&
left_conf < min_conf {
format!("{}/{}", left_conf, min_conf)
} else {
"".to_string()
};
format!("{} {} {}",
DOTS_THREE_CIRCLE,
t!("wallets.tx_confirming"),
conf_info
)
}
},
_ => format!("{} {}", X_CIRCLE, t!("wallets.canceled"))
}
};
// Setup status text color.
let status_color = match tx.data.tx_type {
TxLogEntryType::ConfirmedCoinbase => Colors::text(false),
TxLogEntryType::TxReceived => if tx.data.confirmed {
Colors::green()
} else {
Colors::text(false)
},
TxLogEntryType::TxSent => if tx.data.confirmed {
Colors::red()
} else {
Colors::text(false)
},
TxLogEntryType::TxReceivedCancelled => Colors::inactive_text(),
TxLogEntryType::TxSentCancelled => Colors::inactive_text(),
TxLogEntryType::TxReverted => Colors::inactive_text(),
};
ui.label(RichText::new(status_text).size(15.0).color(status_color));
// Setup transaction time.
let tx_time = View::format_time(tx.data.creation_ts.timestamp());
let tx_time_text = format!("{} {}", CALENDAR_CHECK, tx_time);
ui.label(RichText::new(tx_time_text).size(15.0).color(Colors::gray()));
ui.add_space(3.0);
});
});
});
}
/// Show transaction information [`Modal`].
fn show_tx_info_modal(&mut self, wallet: &Wallet, tx: &WalletTransaction, finalize: bool) {
let modal = WalletTransactionModal::new(wallet, tx, finalize);
self.tx_info_content = Some(modal);
Modal::new(TX_INFO_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("wallets.tx"))
.show();
}
/// Confirmation [`Modal`] to cancel transaction.
fn cancel_confirmation_modal(&mut self, ui: &mut egui::Ui, wallet: &Wallet, modal: &Modal) {
ui.add_space(6.0);
ui.vertical_centered(|ui| {
// Setup confirmation text.
let data = wallet.get_data().unwrap();
let data_txs = data.txs.unwrap();
let txs = data_txs.into_iter()
.filter(|tx| tx.data.id == self.confirm_cancel_tx_id.unwrap())
.collect::<Vec<WalletTransaction>>();
if txs.is_empty() {
modal.close();
return;
}
let tx = txs.get(0).unwrap();
let amount = amount_to_hr_string(tx.amount, true);
let text = match tx.data.tx_type {
TxLogEntryType::TxReceived => {
t!("wallets.tx_receive_cancel_conf", "amount" => amount)
},
_ => {
t!("wallets.tx_send_cancel_conf", "amount" => amount)
}
};
ui.label(RichText::new(text)
.size(17.0)
.color(Colors::text(false)));
ui.add_space(8.0);
});
// Show modal buttons.
ui.scope(|ui| {
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
self.confirm_cancel_tx_id = None;
modal.close();
});
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, "OK".to_string(), Colors::white_or_black(false), || {
wallet.cancel(self.confirm_cancel_tx_id.unwrap());
self.confirm_cancel_tx_id = None;
modal.close();
});
});
});
ui.add_space(6.0);
});
}
}
+19
View File
@@ -0,0 +1,19 @@
// Copyright 2024 The Grim Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
mod content;
pub use content::*;
mod tx;
pub use tx::*;
+581
View File
@@ -0,0 +1,581 @@
// Copyright 2024 The Grim Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::thread;
use std::sync::Arc;
use parking_lot::RwLock;
use egui::scroll_area::ScrollBarVisibility;
use egui::{Align, Id, Layout, RichText, Rounding, ScrollArea};
use grin_util::ToHex;
use grin_core::core::amount_to_hr_string;
use grin_wallet_libwallet::{Error, Slate, SlateState, TxLogEntryType};
use crate::gui::Colors;
use crate::gui::icons::{BROOM, CHECK, CLIPBOARD_TEXT, COPY, CUBE, FILE_ARCHIVE, FILE_TEXT, HASH_STRAIGHT, PROHIBIT, QR_CODE, SCAN};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{CameraContent, FilePickButton, Modal, QrCodeContent, View};
use crate::gui::views::wallets::wallet::txs::WalletTransactions;
use crate::gui::views::wallets::wallet::types::SLATEPACK_MESSAGE_HINT;
use crate::wallet::types::WalletTransaction;
use crate::wallet::Wallet;
/// Transaction information [`Modal`] content.
pub struct WalletTransactionModal {
/// Transaction identifier.
tx_id: u32,
/// Response Slatepack message input value.
response_edit: String,
/// Flag to show transaction finalization input.
show_finalization: bool,
/// Finalization Slatepack message input value.
finalize_edit: String,
/// Flag to check if error happened during transaction finalization.
finalize_error: bool,
/// Flag to check if transaction is finalizing.
finalizing: bool,
/// Transaction finalization result.
final_result: Arc<RwLock<Option<Result<WalletTransaction, Error>>>>,
/// QR code Slatepack message image content.
qr_code_content: Option<QrCodeContent>,
/// QR code scanner content.
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,
response_edit: if !tx.cancelling && !tx.finalizing && !tx.data.confirmed &&
tx.data.tx_slate_id.is_some() &&
(tx.data.tx_type == TxLogEntryType::TxSent ||
tx.data.tx_type == TxLogEntryType::TxReceived) {
let mut slate = Slate::blank(1, false);
slate.state = if tx.can_finalize {
if tx.data.tx_type == TxLogEntryType::TxSent {
SlateState::Standard1
} else {
SlateState::Invoice1
}
} else {
if tx.data.tx_type == TxLogEntryType::TxReceived {
SlateState::Standard2
} else {
SlateState::Invoice2
}
};
slate.id = tx.data.tx_slate_id.unwrap();
wallet.read_slatepack(&slate).unwrap_or("".to_string())
} else {
"".to_string()
},
finalize_edit: "".to_string(),
finalize_error: false,
show_finalization,
finalizing: false,
final_result: Arc::new(RwLock::new(None)),
qr_code_content: None,
qr_scan_content: None,
file_pick_button: FilePickButton::default(),
}
}
/// Draw [`Modal`] content.
pub fn ui(&mut self,
ui: &mut egui::Ui,
wallet: &Wallet,
modal: &Modal,
cb: &dyn PlatformCallbacks) {
// Check values and setup transaction data.
let wallet_data = wallet.get_data();
if wallet_data.is_none() {
modal.close();
return;
}
let data = wallet_data.unwrap();
let data_txs = data.txs.clone().unwrap();
let txs = data_txs.into_iter()
.filter(|tx| tx.data.id == self.tx_id)
.collect::<Vec<WalletTransaction>>();
if txs.is_empty() {
cb.hide_keyboard();
modal.close();
return;
}
let tx = txs.get(0).unwrap();
if self.qr_code_content.is_none() && self.qr_scan_content.is_none() {
ui.add_space(6.0);
let r = View::item_rounding(0, 2, false);
let mut rect = ui.available_rect_before_wrap();
rect.set_height(WalletTransactions::TX_ITEM_HEIGHT);
// Show transaction amount status and time.
WalletTransactions::tx_item_ui(ui, tx, rect, r, &data, |ui| {
if self.finalizing {
return;
}
// Show block height or buttons.
if let Some(h) = tx.height {
if h != 0 {
ui.add_space(6.0);
let height = format!("{} {}", CUBE, h.to_string());
ui.with_layout(Layout::bottom_up(Align::Max), |ui| {
ui.add_space(3.0);
ui.label(RichText::new(height)
.size(15.0)
.color(Colors::text(false)));
});
}
return;
}
let wallet_loaded = wallet.foreign_api_port().is_some();
// Draw button to show transaction finalization or 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 identifier.
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 kernel.
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 receiver address.
if let Some(rec) = tx.receiver() {
let label = format!("{} {}", CUBE, t!("network_mining.address"));
Self::info_item_ui(ui, rec.to_string(), label, true, cb);
}
}
// Show Slatepack message interaction.
if !self.response_edit.is_empty() {
self.message_ui(ui, tx, wallet, modal, cb);
}
if !self.finalizing {
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
if self.qr_code_content.is_some() {
// Show buttons to close modal or come back to text request content.
ui.columns(2, |cols| {
cols[0].vertical_centered_justified(|ui| {
View::button(ui, t!("close"), Colors::white_or_black(false), || {
self.qr_code_content = None;
modal.close();
});
});
cols[1].vertical_centered_justified(|ui| {
View::button(ui, t!("back"), Colors::white_or_black(false), || {
self.qr_code_content = None;
});
});
});
} else if self.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) {
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();
// 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(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;
}
}
}
}
+37 -1
View File
@@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use crate::gui::icons::{FOLDER_LOCK, FOLDER_OPEN, SPINNER, WARNING_CIRCLE};
use crate::gui::platform::PlatformCallbacks;
use crate::wallet::Wallet;
@@ -25,7 +26,7 @@ pub trait WalletTab {
fn get_type(&self) -> WalletTabType;
fn ui(&mut self,
ui: &mut egui::Ui,
wallet: &mut Wallet,
wallet: &Wallet,
cb: &dyn PlatformCallbacks);
}
@@ -48,4 +49,39 @@ impl WalletTabType {
WalletTabType::Settings => t!("wallets.settings")
}
}
}
/// Get wallet status text.
pub fn wallet_status_text(wallet: &Wallet) -> String {
if wallet.is_open() {
if wallet.sync_error() {
format!("{} {}", WARNING_CIRCLE, t!("error"))
} else if wallet.is_closing() {
format!("{} {}", SPINNER, t!("wallets.closing"))
} else if wallet.is_repairing() {
let repair_progress = wallet.repairing_progress();
if repair_progress == 0 {
format!("{} {}", SPINNER, t!("wallets.checking"))
} else {
format!("{} {}: {}%",
SPINNER,
t!("wallets.checking"),
repair_progress)
}
} else if wallet.syncing() {
let info_progress = wallet.info_sync_progress();
if info_progress == 100 || info_progress == 0 {
format!("{} {}", SPINNER, t!("wallets.loading"))
} else {
format!("{} {}: {}%",
SPINNER,
t!("wallets.loading"),
info_progress)
}
} else {
format!("{} {}", FOLDER_OPEN, t!("wallets.unlocked"))
}
} else {
format!("{} {}", FOLDER_LOCK, t!("wallets.locked"))
}
}
+54
View File
@@ -17,6 +17,9 @@ extern crate rust_i18n;
use eframe::NativeOptions;
use egui::{Context, Stroke};
use lazy_static::lazy_static;
use std::sync::Arc;
use parking_lot::RwLock;
#[cfg(target_os = "android")]
use winit::platform::android::activity::AndroidApp;
@@ -255,4 +258,55 @@ fn setup_i18n() {
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(_) => {}
}
};
}
}
+165 -13
View File
@@ -29,6 +29,18 @@ fn real_main() {
.parse_default_env()
.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.
std::panic::set_hook(Box::new(|info| {
let backtrace = backtrace::Backtrace::new();
@@ -36,12 +48,12 @@ fn real_main() {
let time = grim::gui::views::View::format_time(chrono::Utc::now().timestamp());
let target = egui::os::OperatingSystem::from_target_os();
let ver = grim::VERSION;
let msg = panic_message::panic_info_message(info);
let msg = panic_info_message(info);
let err = format!("{} - {:?} - v{}\n\n{}\n\n{:?}", time, target, ver, msg, backtrace);
// Save backtrace to file.
let log = grim::Settings::crash_report_path();
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();
// Setup flag to show crash after app restart.
@@ -49,20 +61,44 @@ fn real_main() {
}));
// Start GUI.
let _ = std::panic::catch_unwind(|| {
start_desktop_gui();
});
match std::panic::catch_unwind(|| {
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.
/// Get panic message from crash payload.
#[allow(dead_code)]
#[cfg(not(target_os = "android"))]
fn start_desktop_gui() {
fn panic_info_message<'pi>(panic_info: &'pi std::panic::PanicInfo<'_>) -> &'pi str {
let payload = panic_info.payload();
// taken from: https://github.com/rust-lang/rust/blob/4b9f4b221b92193c7e95b1beb502c6eb32c3b613/library/std/src/panicking.rs#L194-L200
match payload.downcast_ref::<&'static str>() {
Some(msg) => *msg,
None => match payload.downcast_ref::<String>() {
Some(msg) => msg.as_str(),
// Copy what rustc does in the default panic handler
None => "Box<dyn Any>",
},
}
}
/// Start GUI with Desktop related setup passing data from opening.
#[allow(dead_code)]
#[cfg(not(target_os = "android"))]
fn start_desktop_gui(platform: grim::gui::platform::Desktop) {
use grim::AppConfig;
use dark_light::Mode;
let platform = grim::gui::platform::Desktop::default();
// Setup system theme if not set.
if let None = AppConfig::dark_theme() {
let dark = match dark_light::detect() {
@@ -73,12 +109,11 @@ fn start_desktop_gui() {
AppConfig::set_dark_theme(dark);
}
// Setup window size.
let (width, height) = AppConfig::window_size();
let mut viewport = egui::ViewportBuilder::default()
.with_min_inner_size([AppConfig::MIN_WIDTH, AppConfig::MIN_HEIGHT])
.with_inner_size([width, height]);
// Setup an icon.
if let Ok(icon) = eframe::icon_data::from_png_bytes(include_bytes!("../img/icon.png")) {
viewport = viewport.with_icon(std::sync::Arc::new(icon));
@@ -90,6 +125,7 @@ fn start_desktop_gui() {
// Setup window decorations.
let is_mac = egui::os::OperatingSystem::from_target_os() == egui::os::OperatingSystem::Mac;
viewport = viewport
.with_window_level(egui::WindowLevel::Normal)
.with_fullsize_content_view(true)
.with_title_shown(false)
.with_titlebar_buttons_shown(false)
@@ -110,7 +146,8 @@ fn start_desktop_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(_) => {}
Err(e) => {
if win {
@@ -118,7 +155,9 @@ fn start_desktop_gui() {
}
// Start with another renderer on error.
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(_) => {}
Err(e) => {
panic!("{}", e);
@@ -126,4 +165,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(_) => {}
}
}
});
});
}
+4 -7
View File
@@ -49,8 +49,6 @@ impl PeersConfig {
let chain_type = AppConfig::chain_type();
let config_path = Settings::config_path(Self::FILE_NAME, Some(chain_type.shortname()));
Settings::write_to_file(self, config_path);
// Load changes to node server config.
Self::load_to_server_config();
}
/// Convert string to [`PeerAddr`] if address is in correct format (`host:port`) and available.
@@ -71,7 +69,7 @@ impl PeersConfig {
}
/// Load saved peers to node server [`ConfigMembers`] config.
pub(crate) fn load_to_server_config() {
pub fn load_to_server_config() {
let mut w_config = Settings::node_config_to_update();
// Load seeds.
for seed in w_config.peers.seeds.clone() {
@@ -683,10 +681,9 @@ impl NodeConfig {
/// Toggle seeding type to use default or custom seed list.
pub fn toggle_seeding_type() {
let seeding_type = if Self::is_default_seeding_type() {
Seeding::List
} else {
Seeding::DNSSeed
let seeding_type = match Self::is_default_seeding_type() {
true => Seeding::List,
false => Seeding::DNSSeed
};
let mut w_config = Settings::node_config_to_update();
w_config.node.server.p2p_config.seeding_type = seeding_type;
+33 -2
View File
@@ -24,10 +24,12 @@ use futures::channel::oneshot;
use grin_chain::SyncStatus;
use grin_core::global;
use grin_core::global::ChainTypes;
use grin_p2p::msg::PeerAddrs;
use grin_p2p::Seeding;
use grin_servers::{Server, ServerStats, StratumServerConfig, StratumStats};
use grin_servers::common::types::Error;
use crate::node::{NodeConfig, NodeError};
use crate::node::{NodeConfig, NodeError, PeersConfig};
use crate::node::stratum::{StratumStopState, StratumServer};
lazy_static! {
@@ -83,6 +85,16 @@ impl Node {
/// Delay for thread to update the stats.
pub const STATS_UPDATE_DELAY: Duration = Duration::from_millis(1000);
/// Default Mainnet DNS Seeds
pub const MAINNET_DNS_SEEDS: &'static[&'static str] = &[
"mainnet.seed.grin.lesceller.com",
"grinseed.revcore.net",
"mainnet-seed.grinnode.live",
"mainnet.grin.punksec.de",
"grinnode.30-r.com",
"grincoin.org"
];
/// Stop the [`Server`] and setup exit flag after if needed.
pub fn stop(exit_after_stop: bool) {
NODE_STATE.stop_needed.store(true, Ordering::Relaxed);
@@ -516,10 +528,29 @@ impl Node {
/// Start the node [`Server`].
fn start_node_server() -> Result<Server, Error> {
// Get saved server config.
// Setup server config.
PeersConfig::load_to_server_config();
let config = NodeConfig::node_server_config();
let mut server_config = config.server.clone();
// Setup Mainnet DNSSeed
if server_config.chain_type == ChainTypes::Mainnet && NodeConfig::is_default_seeding_type() {
server_config.p2p_config.seeding_type = Seeding::List;
server_config.p2p_config.seeds = Some(PeerAddrs::default());
for seed in Node::MAINNET_DNS_SEEDS {
let addr = format!("{}:3414", seed);
if let Some(p) = PeersConfig::peer_to_addr(addr) {
let mut seeds = server_config
.p2p_config
.seeds
.clone()
.unwrap_or(PeerAddrs::default());
seeds.peers.insert(seeds.peers.len(), p);
server_config.p2p_config.seeds = Some(seeds);
}
}
}
// Fix to avoid too many opened files.
server_config.p2p_config.peer_min_preferred_outbound_count =
server_config.p2p_config.peer_max_outbound_count;
+1 -5
View File
@@ -16,7 +16,7 @@ use grin_core::global::ChainTypes;
use serde_derive::{Deserialize, Serialize};
use crate::gui::views::Content;
use crate::node::{NodeConfig, PeersConfig};
use crate::node::NodeConfig;
use crate::Settings;
use crate::wallet::ConnectionsConfig;
@@ -113,10 +113,6 @@ impl AppConfig {
w_node_config.node = node_config.node;
w_node_config.peers = node_config.peers;
}
// Load saved peers to node config.
{
PeersConfig::load_to_server_config();
}
// Load connections configuration
{
let mut w_conn_config = Settings::conn_config_to_update();
+9 -1
View File
@@ -48,9 +48,10 @@ pub struct Settings {
impl Settings {
/// Main application directory name.
pub const MAIN_DIR_NAME: &'static str = ".grim";
/// Crash report file name.
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.
fn init() -> Self {
@@ -141,6 +142,13 @@ impl Settings {
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.
pub fn config_path(config_name: &str, sub_dir: Option<String>) -> PathBuf {
let mut path = Self::base_path(sub_dir);
+14 -1
View File
@@ -22,6 +22,7 @@ use rand::Rng;
use serde_derive::{Deserialize, Serialize};
use crate::{AppConfig, Settings};
use crate::wallet::ConnectionsConfig;
use crate::wallet::types::ConnectionMethod;
/// Wallet configuration.
@@ -75,7 +76,7 @@ impl WalletConfig {
name,
ext_conn_id: match conn_method {
ConnectionMethod::Integrated => None,
ConnectionMethod::External(id) => Some(*id)
ConnectionMethod::External(id, _) => Some(*id)
},
min_confirmations: MIN_CONFIRMATIONS_DEFAULT,
use_dandelion: Some(true),
@@ -116,6 +117,18 @@ impl WalletConfig {
None
}
/// Get wallet connection method.
pub fn connection(&self) -> ConnectionMethod {
if let Some(ext_conn_id) = self.ext_conn_id {
if let Some(conn) = ConnectionsConfig::ext_conn(ext_conn_id) {
if !conn.deleted {
return ConnectionMethod::External(conn.id, conn.url);
}
}
}
ConnectionMethod::Integrated
}
/// Save wallet config.
pub fn save(&self) {
let config_path = Self::get_config_file_path(self.chain_type, self.id);
+27 -34
View File
@@ -38,13 +38,7 @@ impl ConnectionsConfig {
if !path.exists() || parsed.is_err() {
let default_config = ConnectionsConfig {
chain_type: *chain_type,
external: if chain_type == &ChainTypes::Mainnet {
vec![
ExternalConnection::default_main()
]
} else {
vec![]
},
external: ExternalConnection::default(chain_type),
};
Settings::write_to_file(&default_config, path);
default_config
@@ -53,11 +47,17 @@ impl ConnectionsConfig {
}
}
/// Save connections configuration to the file.
pub fn save(&self) {
let chain_type = AppConfig::chain_type();
let sub_dir = Some(chain_type.shortname());
Settings::write_to_file(self, Settings::config_path(Self::FILE_NAME, sub_dir));
/// Save connections configuration.
pub fn save(&mut self) {
// Check deleted external connections.
let mut config = self.clone();
config.external = config.external.iter()
.map(|c| c.clone())
.filter(|c| !c.deleted)
.collect::<Vec<ExternalConnection>>();
let sub_dir = Some(AppConfig::chain_type().shortname());
Settings::write_to_file(&config, Settings::config_path(Self::FILE_NAME, sub_dir));
}
/// Get [`ExternalConnection`] list.
@@ -68,29 +68,19 @@ impl ConnectionsConfig {
/// Save [`ExternalConnection`] in configuration.
pub fn add_ext_conn(conn: ExternalConnection) {
// Do not update default connection.
if conn.url == ExternalConnection::DEFAULT_MAIN_URL {
return;
}
let mut w_config = Settings::conn_config_to_update();
let mut exists = false;
for c in w_config.external.iter_mut() {
// Update connection if config exists.
if c.id == conn.id {
c.url = conn.url.clone();
c.secret = conn.secret.clone();
exists = true;
break;
}
}
// Create new connection if URL not exists.
if !exists {
if let Some(pos) = w_config.external.iter().position(|c| {
c.id == conn.id
}) {
w_config.external.remove(pos);
w_config.external.insert(pos, conn);
} else {
w_config.external.push(conn);
}
w_config.save();
}
/// Get [`ExternalConnection`] by provided identifier.
/// Get external node connection with provided identifier.
pub fn ext_conn(id: i64) -> Option<ExternalConnection> {
let r_config = Settings::conn_config_to_read();
for c in &r_config.external {
@@ -113,13 +103,16 @@ impl ConnectionsConfig {
}
}
/// Remove external node connection by provided identifier.
/// Remove [`ExternalConnection`] with provided identifier.
pub fn remove_ext_conn(id: i64) {
let mut w_config = Settings::conn_config_to_update();
let index = w_config.external.iter().position(|c| c.id == id);
if let Some(i) = index {
w_config.external.remove(i);
w_config.save();
if let Some(pos) = w_config.external.iter().position(|c| {
c.id == id
}) {
if let Some(conn) = w_config.external.get_mut(pos) {
conn.deleted = true;
w_config.save();
}
}
}
}
+93 -65
View File
@@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use grin_core::global::ChainTypes;
use grin_util::to_base64;
use serde_derive::{Deserialize, Serialize};
@@ -28,89 +29,116 @@ pub struct ExternalConnection {
pub secret: Option<String>,
/// Flag to check if server is available.
#[serde(skip_serializing)]
pub available: Option<bool>
#[serde(skip_serializing, skip_deserializing)]
pub available: Option<bool>,
/// Flag to check if connection was deleted.
#[serde(skip_serializing, skip_deserializing)]
pub deleted: bool
}
impl ExternalConnection {
/// Default external node URL for main network.
pub const DEFAULT_MAIN_URL: &'static str = "https://grinnode.live:3413";
/// Default external node URL for main network.
const DEFAULT_MAIN_URLS: [&'static str; 2] = [
"https://grincoin.org",
"https://grinnode.live:3413"
];
/// Create default external connection.
pub fn default_main() -> Self {
Self { id: 1, url: Self::DEFAULT_MAIN_URL.to_string(), secret: None, available: None }
/// Default external node URL for main network.
const DEFAULT_TEST_URLS: [&'static str; 1] = [
"https://testnet.grincoin.org"
];
impl ExternalConnection {
/// Get default connections for provided chain type.
pub fn default(chain_type: &ChainTypes) -> Vec<ExternalConnection> {
let urls = match chain_type {
ChainTypes::Mainnet => DEFAULT_MAIN_URLS.to_vec(),
_ => DEFAULT_TEST_URLS.to_vec()
};
urls.iter().enumerate().map(|(index, url)| {
ExternalConnection {
id: index as i64,
url: url.to_string(),
secret: None,
available: None,
deleted: false,
}
}).collect::<Vec<ExternalConnection>>()
}
/// Create new external connection.
pub fn new(url: String, secret: Option<String>) -> Self {
let id = chrono::Utc::now().timestamp();
Self { id, url, secret, available: None }
}
/// Check connection availability.
fn check_conn_availability(&self) {
let conn = self.clone();
ConnectionsConfig::update_ext_conn_status(conn.id, None);
std::thread::spawn(move || {
tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.unwrap()
.block_on(async {
let url = url::Url::parse(conn.url.as_str()).unwrap();
if let Ok(_) = url.socket_addrs(|| None) {
let addr = format!("{}v2/foreign", url.to_string());
// Setup http client.
let client = hyper::Client::builder()
.build::<_, hyper::Body>(hyper_tls::HttpsConnector::new());
let mut req_setup = hyper::Request::builder()
.method(hyper::Method::POST)
.uri(addr.clone());
// Setup secret key auth.
if let Some(key) = conn.secret {
let basic_auth = format!(
"Basic {}",
to_base64(&format!("grin:{}", key))
);
req_setup = req_setup
.header(hyper::header::AUTHORIZATION, basic_auth.clone());
}
let req = req_setup.body(hyper::Body::from(
r#"{"id":1,"jsonrpc":"2.0","method":"get_version","params":{} }"#)
).unwrap();
// Send request.
match client.request(req).await {
Ok(res) => {
let status = res.status().as_u16();
// Available on 200 HTTP status code.
if status == 200 {
ConnectionsConfig::update_ext_conn_status(conn.id, Some(true));
} else {
ConnectionsConfig::update_ext_conn_status(conn.id, Some(false));
}
}
Err(_) => {
ConnectionsConfig::update_ext_conn_status(conn.id, Some(false));
}
}
} else {
ConnectionsConfig::update_ext_conn_status(conn.id, Some(false));
}
});
});
Self {
id,
url,
secret,
available: None,
deleted: false
}
}
/// Check external connections availability.
pub fn check_ext_conn_availability(id: Option<i64>) {
pub fn check(id: Option<i64>, ui_ctx: &egui::Context) {
let conn_list = ConnectionsConfig::ext_conn_list();
for conn in conn_list {
if let Some(id) = id {
if id == conn.id {
conn.check_conn_availability();
check_ext_conn(&conn, ui_ctx);
}
} else {
conn.check_conn_availability();
check_ext_conn(&conn, ui_ctx);
}
}
}
}
/// Check connection availability.
fn check_ext_conn(conn: &ExternalConnection, ui_ctx: &egui::Context) {
let conn = conn.clone();
let ui_ctx = ui_ctx.clone();
ConnectionsConfig::update_ext_conn_status(conn.id, None);
std::thread::spawn(move || {
tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.unwrap()
.block_on(async {
let url = url::Url::parse(conn.url.as_str()).unwrap();
if let Ok(_) = url.socket_addrs(|| None) {
let addr = format!("{}v2/foreign", url.to_string());
// Setup http client.
let client = hyper::Client::builder()
.build::<_, hyper::Body>(hyper_tls::HttpsConnector::new());
let mut req_setup = hyper::Request::builder()
.method(hyper::Method::POST)
.uri(addr.clone());
// Setup secret key auth.
if let Some(key) = conn.secret {
let basic_auth = format!(
"Basic {}",
to_base64(&format!("grin:{}", key))
);
req_setup = req_setup
.header(hyper::header::AUTHORIZATION, basic_auth.clone());
}
let req = req_setup.body(hyper::Body::from(
r#"{"id":1,"jsonrpc":"2.0","method":"get_version","params":{} }"#)
).unwrap();
// Send request.
match client.request(req).await {
Ok(res) => {
let status = res.status().as_u16();
// Available on 200 HTTP status code.
ConnectionsConfig::update_ext_conn_status(conn.id, Some(status == 200));
}
Err(_) => ConnectionsConfig::update_ext_conn_status(conn.id, Some(false))
}
} else {
ConnectionsConfig::update_ext_conn_status(conn.id, Some(false));
}
// Repaint ui on change.
ui_ctx.request_repaint();
});
});
}
+3 -49
View File
@@ -13,25 +13,23 @@
// limitations under the License.
use grin_core::global::ChainTypes;
use grin_wallet_libwallet::Error;
use crate::AppConfig;
use crate::wallet::{Wallet, WalletConfig};
/// Wrapper for [`Wallet`] list.
/// [`Wallet`] list container.
#[derive(Clone)]
pub struct WalletList {
/// List of wallets for [`ChainTypes::Mainnet`].
pub main_list: Vec<Wallet>,
/// List of wallets for [`ChainTypes::Testnet`].
pub test_list: Vec<Wallet>,
/// Selected [`Wallet`] identifier.
pub selected_id: Option<i64>,
}
impl Default for WalletList {
fn default() -> Self {
let (main_list, test_list) = Self::init();
Self { main_list, test_list, selected_id: None }
Self { main_list, test_list }
}
}
@@ -84,7 +82,6 @@ impl WalletList {
/// Add created [`Wallet`] to the list.
pub fn add(&mut self, wallet: Wallet) {
self.selected_id = Some(wallet.get_config().id);
let list = self.mut_list();
list.insert(0, wallet);
}
@@ -99,47 +96,4 @@ impl WalletList {
}
}
}
/// Select [`Wallet`] with provided identifier.
pub fn select(&mut self, id: Option<i64>) {
self.selected_id = id;
}
/// Get selected [`Wallet`] name.
pub fn selected_name(&self) -> String {
for w in self.list() {
let config = w.get_config();
if Some(config.id) == self.selected_id {
return config.name.clone()
}
}
t!("wallets.unlocked")
}
/// Check if selected [`Wallet`] is open.
pub fn is_selected_open(&self) -> bool {
for w in self.list() {
let config = w.get_config();
if Some(config.id) == self.selected_id {
return w.is_open()
}
}
false
}
/// Check if current list is empty.
pub fn is_current_list_empty(&self) -> bool {
self.list().is_empty()
}
/// Open selected [`Wallet`].
pub fn open_selected(&mut self, password: &String) -> Result<(), Error> {
let selected_id = self.selected_id.clone();
for w in self.mut_list() {
if Some(w.get_config().id) == selected_id {
return w.open(password);
}
}
Err(Error::GenericError("Wallet is not selected".to_string()))
}
}
+147 -37
View File
@@ -16,18 +16,20 @@ use grin_keychain::mnemonic::{from_entropy, search, to_entropy};
use grin_util::ZeroingString;
use rand::{Rng, thread_rng};
use crate::wallet::types::{PhraseMode, PhraseSize};
use crate::wallet::types::{PhraseMode, PhraseSize, PhraseWord};
/// Mnemonic phrase container.
pub struct Mnemonic {
/// Phrase setup mode.
pub(crate) mode: PhraseMode,
mode: PhraseMode,
/// Size of phrase based on words count.
pub(crate) size: PhraseSize,
size: PhraseSize,
/// Generated words.
pub(crate) words: Vec<String>,
words: Vec<PhraseWord>,
/// Words to confirm the phrase.
pub(crate) confirm_words: Vec<String>
confirmation: Vec<PhraseWord>,
/// Flag to check if entered phrase if valid.
valid: bool,
}
impl Default for Mnemonic {
@@ -35,43 +37,67 @@ impl Default for Mnemonic {
let size = PhraseSize::Words24;
let mode = PhraseMode::Generate;
let words = Self::generate_words(&mode, &size);
let confirm_words = Self::empty_words(&size);
Self { mode, size, words, confirm_words }
let confirmation = Self::empty_words(&size);
Self { mode, size, words, confirmation, valid: true }
}
}
impl Mnemonic {
/// Change mnemonic phrase setup [`PhraseMode`].
/// Generate words based on provided [`PhraseMode`].
pub fn set_mode(&mut self, mode: PhraseMode) {
self.mode = mode;
self.words = Self::generate_words(&self.mode, &self.size);
self.confirm_words = Self::empty_words(&self.size);
self.confirmation = Self::empty_words(&self.size);
self.valid = true;
}
/// Change mnemonic phrase words [`PhraseSize`].
/// Get current phrase mode.
pub fn mode(&self) -> PhraseMode {
self.mode.clone()
}
/// Generate words based on provided [`PhraseSize`].
pub fn set_size(&mut self, size: PhraseSize) {
self.size = size;
self.words = Self::generate_words(&self.mode, &self.size);
self.confirm_words = Self::empty_words(&self.size);
self.confirmation = Self::empty_words(&self.size);
self.valid = true;
}
/// Check if provided word is in BIP39 format.
pub fn is_valid_word(&self, word: &String) -> bool {
search(word).is_ok()
/// Get current phrase size.
pub fn size(&self) -> PhraseSize {
self.size.clone()
}
/// Get words based on current [`PhraseMode`].
pub fn words(&self, edit: bool) -> Vec<PhraseWord> {
match self.mode {
PhraseMode::Generate => {
if edit {
&self.confirmation
} else {
&self.words
}
}
PhraseMode::Import => &self.words
}.clone()
}
/// Check if current phrase is valid.
pub fn is_valid_phrase(&self) -> bool {
to_entropy(self.get_phrase().as_str()).is_ok()
pub fn valid(&self) -> bool {
self.valid
}
/// Get phrase from words.
pub fn get_phrase(&self) -> String {
self.words.iter().map(|x| x.to_string() + " ").collect::<String>()
self.words.iter()
.enumerate()
.map(|(i, x)| if i == 0 { "" } else { " " }.to_owned() + &x.text)
.collect::<String>()
}
/// Generate list of words based on provided [`PhraseMode`] and [`PhraseSize`].
fn generate_words(mode: &PhraseMode, size: &PhraseSize) -> Vec<String> {
/// Generate [`PhraseWord`] list based on provided [`PhraseMode`] and [`PhraseSize`].
fn generate_words(mode: &PhraseMode, size: &PhraseSize) -> Vec<PhraseWord> {
match mode {
PhraseMode::Generate => {
let mut rng = thread_rng();
@@ -81,8 +107,14 @@ impl Mnemonic {
}
from_entropy(&entropy).unwrap()
.split(" ")
.map(|s| String::from(s))
.collect::<Vec<String>>()
.map(|s| {
let text = s.to_string();
PhraseWord {
text,
valid: true,
}
})
.collect::<Vec<PhraseWord>>()
},
PhraseMode::Import => {
Self::empty_words(size)
@@ -91,39 +123,117 @@ impl Mnemonic {
}
/// Generate empty list of words based on provided [`PhraseSize`].
fn empty_words(size: &PhraseSize) -> Vec<String> {
fn empty_words(size: &PhraseSize) -> Vec<PhraseWord> {
let mut words = Vec::with_capacity(size.value());
for _ in 0..size.value() {
words.push(String::from(""))
words.push(PhraseWord {
text: "".to_string(),
valid: true,
});
}
words
}
/// Set words from provided text if possible.
pub fn import_text(&mut self, text: &ZeroingString, confirmation: bool) {
/// Insert word into provided index and return validation result.
pub fn insert(&mut self, index: usize, word: &String) -> bool {
// Check if word is valid.
let found = search(word).is_ok();
if !found {
return false;
}
let is_confirmation = self.mode == PhraseMode::Generate;
if is_confirmation {
let w = self.words.get(index).unwrap();
if word != &w.text {
return false;
}
}
// Save valid word at list.
let words = if is_confirmation {
&mut self.confirmation
} else {
&mut self.words
};
words.remove(index);
words.insert(index, PhraseWord { text: word.to_owned(), valid: true });
// Validate phrase when all words are entered.
let mut has_empty = false;
let _: Vec<_> = words.iter().map(|w| {
if w.text.is_empty() {
has_empty = true;
}
}).collect();
if !has_empty {
self.valid = to_entropy(self.get_phrase().as_str()).is_ok();
}
true
}
/// Get word from provided index.
pub fn get(&self, index: usize) -> Option<PhraseWord> {
let words = match self.mode {
PhraseMode::Generate => &self.confirmation,
PhraseMode::Import => &self.words
};
let word = words.get(index);
if let Some(w) = word {
return Some(PhraseWord {
text: w.text.clone(),
valid: w.valid
});
}
None
}
/// Setup phrase from provided text if possible.
pub fn import(&mut self, text: &ZeroingString) {
let words_split = text.trim().split(" ");
let count = words_split.clone().count();
if let Some(size) = PhraseSize::type_for_value(count) {
if !confirmation {
// Setup phrase size.
let confirm = self.mode == PhraseMode::Generate;
if !confirm {
self.size = size;
} else if self.size != size {
return;
}
// Setup word list.
let mut words = vec![];
words_split.for_each(|word| {
if confirmation && !self.is_valid_word(&word.to_string()) {
words = vec![];
return;
}
words.push(word.to_string())
words_split.for_each(|w| {
let mut text = w.to_string();
text.retain(|c| c.is_alphabetic());
let valid = search(&text).is_ok();
words.push(PhraseWord { text, valid });
});
if confirmation {
if !words.is_empty() {
self.confirm_words = words;
let mut has_invalid = false;
for (i, w) in words.iter().enumerate() {
if !self.insert(i, &w.text) {
has_invalid = true;
}
} else {
self.words = words;
}
self.valid = !has_invalid;
}
}
/// Check if phrase has invalid or empty words.
pub fn has_empty_or_invalid(&self) -> bool {
let words = match self.mode {
PhraseMode::Generate => &self.confirmation,
PhraseMode::Import => &self.words
};
let mut has_empty = false;
let mut has_invalid = false;
let _: Vec<_> = words.iter().map(|w| {
if w.text.is_empty() {
has_empty = true;
}
if !w.valid {
has_invalid = true;
}
}).collect();
has_empty || has_invalid
}
}
+29 -17
View File
@@ -17,7 +17,18 @@ use std::sync::Arc;
use grin_keychain::ExtKeychain;
use grin_util::Mutex;
use grin_wallet_impls::{DefaultLCProvider, HTTPNodeClient};
use grin_wallet_libwallet::{TxLogEntry, TxLogEntryType, WalletInfo, WalletInst};
use grin_wallet_libwallet::{SlatepackAddress, TxLogEntry, TxLogEntryType, WalletInfo, WalletInst};
use grin_wallet_util::OnionV3Address;
use serde_derive::{Deserialize, Serialize};
/// Mnemonic phrase word.
#[derive(Clone)]
pub struct PhraseWord {
/// Word text.
pub text: String,
/// Flag to check if word is valid.
pub valid: bool,
}
/// Mnemonic phrase setup mode.
#[derive(PartialEq, Clone)]
@@ -98,12 +109,12 @@ impl PhraseSize {
}
/// Wallet connection method.
#[derive(PartialEq)]
#[derive(Serialize, Deserialize, Clone, PartialEq)]
pub enum ConnectionMethod {
/// Integrated node.
Integrated,
/// External node, contains connection identifier.
External(i64)
/// External node, contains connection identifier and URL.
External(i64, String)
}
/// Wallet instance type.
@@ -149,14 +160,12 @@ pub struct WalletTransaction {
pub amount: u64,
/// Flag to check if transaction is cancelling.
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.
pub can_finalize: bool,
/// Block height when tx was confirmed.
pub conf_height: Option<u64>,
/// Block height when tx was reposted.
pub repost_height: Option<u64>,
/// Flag to check if transaction is finalizing.
pub finalizing: bool,
/// Block height where tx was included.
pub height: Option<u64>,
/// Flag to check if tx was received after sync from node.
pub from_node: bool,
}
@@ -164,16 +173,19 @@ pub struct WalletTransaction {
impl WalletTransaction {
/// Check if transaction can be cancelled.
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::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
/// Get receiver address if payment proof was created.
pub fn receiver(&self) -> Option<SlatepackAddress> {
if let Some(proof) = &self.data.payment_proof {
let onion_addr = OnionV3Address::from_bytes(proof.receiver_address.to_bytes());
if let Ok(addr) = SlatepackAddress::try_from(onion_addr) {
return Some(addr);
}
}
None
}
}
+298 -341
View File
File diff suppressed because it is too large Load Diff
+10 -4
View File
@@ -16,8 +16,8 @@
AllowSameVersionUpgrades = "yes"
/>
<Icon Id='ProductICO' SourceFile='wix\Product.ico'/>
<Property Id='ARPPRODUCTICON' Value='ProductICO' />
<Icon Id='Product.ico' SourceFile='wix\Product.ico'/>
<Property Id='ARPPRODUCTICON' Value='Product.ico' />
<Directory Id="TARGETDIR" Name="SourceDir">
<Directory Id='$(var.PlatformProgramFilesFolder)'>
@@ -28,7 +28,7 @@
<Component Id="ApplicationShortcutDesktop" Guid="14efa019-7ed7-4765-8263-fa5460f92495">
<Shortcut Id="ApplicationDesktopShortcut"
Name="Grim"
Icon="ProductICO"
Icon="Product.ico"
Description="GUI for Grin"
Target="[APPLICATIONROOTDIRECTORY]grim.exe"
WorkingDirectory="APPLICATIONROOTDIRECTORY"/>
@@ -55,6 +55,12 @@
</Component>
<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="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>
</DirectoryRef>
@@ -64,7 +70,7 @@
<Shortcut Id="ApplicationStartMenuShortcut"
Name="Grim"
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]"
WorkingDirectory="APPLICATIONROOTDIRECTORY"/>
<RemoveFolder Id="ApplicationProgramsFolder" On="uninstall"/>