Merge pull request #13 from ardocrat/slatepack_ext_file

Open .slatepack file with the app
This commit is contained in:
Ardocrat 2024-09-16 16:08:27 +00:00 committed by GitHub
commit 3bc8c407b4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
60 changed files with 4667 additions and 4276 deletions

View file

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

View file

@ -6,89 +6,6 @@ on:
- "v*.*.*" - "v*.*.*"
jobs: jobs:
android_release:
name: Android Release
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
cache: gradle
- name: Setup Rust build
run: |
cargo install cargo-ndk
rustup target add aarch64-linux-android
rustup target add armv7-linux-androideabi
rustup target add x86_64-linux-android
- name: Setup Java build
run: |
chmod +x android/gradlew
echo "${{ secrets.ANDROID_RELEASE_KEYSTORE }}" > release.keystore.asc
gpg -d --passphrase "${{ secrets.ANDROID_RELEASE_SECRET }}" --batch release.keystore.asc > android/keystore
echo -e "storePassword=${{ secrets.ANDROID_PASS }}\nkeyPassword=${{ secrets.ANDROID_PASS }}\nkeyAlias=grim\nstoreFile=../keystore" > android/keystore.properties
- name: Build lib ARMv8 1/2
continue-on-error: true
run: |
sed -i -e 's/"rlib"/"cdylib","rlib"/g' Cargo.toml
export CPPFLAGS="-DMDB_USE_ROBUST=0" && export CFLAGS="-DMDB_USE_ROBUST=0" && cargo ndk -t arm64-v8a build --profile release-apk
- name: Build lib ARMv8 2/2
run: |
unset CPPFLAGS && unset CFLAGS && cargo ndk -t arm64-v8a -o android/app/src/main/jniLibs build --profile release-apk
sed -i -e 's/"cdylib","rlib"/"rlib"/g' Cargo.toml
- name: Build lib ARMv7 1/2
continue-on-error: true
run: |
sed -i -e 's/"rlib"/"cdylib","rlib"/g' Cargo.toml
export CPPFLAGS="-DMDB_USE_ROBUST=0" && export CFLAGS="-DMDB_USE_ROBUST=0" && cargo ndk -t armeabi-v7a build --profile release-apk
- name: Build lib ARMv7 2/2
run: |
unset CPPFLAGS && unset CFLAGS && cargo ndk -t armeabi-v7a -o android/app/src/main/jniLibs build --profile release-apk
sed -i -e 's/"cdylib","rlib"/"rlib"/g' Cargo.toml
- name: Build APK ARM
working-directory: android
run: |
rm -rf app/build
./gradlew assembleRelease
mv app/build/outputs/apk/release/app-release.apk grim-${{ github.ref_name }}-android.apk
rm -rf app/src/main/jniLibs/*
- name: Checksum APK ARM
working-directory: android
shell: pwsh
run: get-filehash -algorithm sha256 grim-${{ github.ref_name }}-android.apk | Format-List | Out-String | ForEach-Object { $_.Trim() } > grim-${{ github.ref_name }}-android-sha256sum.txt
- name: Build lib x86 1/2
continue-on-error: true
run: |
sed -i -e 's/"rlib"/"cdylib","rlib"/g' Cargo.toml
export CPPFLAGS="-DMDB_USE_ROBUST=0" && export CFLAGS="-DMDB_USE_ROBUST=0" && cargo ndk -t x86_64 build --profile release-apk
- name: Build lib x86 2/2
run: |
unset CPPFLAGS && unset CFLAGS && cargo ndk -t x86_64 -o android/app/src/main/jniLibs build --profile release-apk
sed -i -e 's/"cdylib","rlib"/"rlib"/g' Cargo.toml
- name: Build APK x86
working-directory: android
run: |
rm -rf app/build
./gradlew assembleRelease
mv app/build/outputs/apk/release/app-release.apk grim-${{ github.ref_name }}-android-x86_64.apk
- name: Checksum APK x86
working-directory: android
shell: pwsh
run: get-filehash -algorithm sha256 grim-${{ github.ref_name }}-android-x86_64.apk | Format-List | Out-String | ForEach-Object { $_.Trim() } > grim-${{ github.ref_name }}-android-x86_64-sha256sum.txt
- name: Release
uses: softprops/action-gh-release@v1
with:
files: |
android/grim-${{ github.ref_name }}-android.apk
android/grim-${{ github.ref_name }}-android-sha256sum.txt
android/grim-${{ github.ref_name }}-android-x86_64.apk
android/grim-${{ github.ref_name }}-android-x86_64-sha256sum.txt
linux_release: linux_release:
name: Linux Release name: Linux Release
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -121,7 +38,7 @@ jobs:
- name: Checksum AppImage x86 - name: Checksum AppImage x86
working-directory: target/x86_64-unknown-linux-gnu/release working-directory: target/x86_64-unknown-linux-gnu/release
shell: pwsh shell: pwsh
run: get-filehash -algorithm sha256 grim-${{ github.ref_name }}-linux-x86_64.AppImage | Format-List | Out-String | ForEach-Object { $_.Trim() } > grim-${{ github.ref_name }}-linux-x86_64-appimage-sha256sum.txt run: sha256sum grim-${{ github.ref_name }}-linux-x86_64.AppImage > grim-${{ github.ref_name }}-linux-x86_64-appimage-sha256sum.txt
- name: AppImage ARM - name: AppImage ARM
run: | run: |
cp target/aarch64-unknown-linux-gnu/release/grim linux/Grim.AppDir/AppRun cp target/aarch64-unknown-linux-gnu/release/grim linux/Grim.AppDir/AppRun
@ -129,7 +46,7 @@ jobs:
- name: Checksum AppImage ARM - name: Checksum AppImage ARM
working-directory: target/aarch64-unknown-linux-gnu/release working-directory: target/aarch64-unknown-linux-gnu/release
shell: pwsh shell: pwsh
run: get-filehash -algorithm sha256 grim-${{ github.ref_name }}-linux-arm.AppImage | Format-List | Out-String | ForEach-Object { $_.Trim() } > grim-${{ github.ref_name }}-linux-arm-appimage-sha256sum.txt run: sha256sum grim-${{ github.ref_name }}-linux-arm.AppImage > grim-${{ github.ref_name }}-linux-arm-appimage-sha256sum.txt
- name: Release - name: Release
uses: softprops/action-gh-release@v1 uses: softprops/action-gh-release@v1
with: with:
@ -157,7 +74,7 @@ jobs:
- name: Checksum release - name: Checksum release
working-directory: target/release working-directory: target/release
shell: pwsh shell: pwsh
run: get-filehash -algorithm sha256 grim-${{ github.ref_name }}-win-x86_64.zip | Format-List | Out-String | ForEach-Object { $_.Trim() } > grim-${{ github.ref_name }}-win-x86_64-sha256sum.txt run: sha256sum grim-${{ github.ref_name }}-win-x86_64.zip > grim-${{ github.ref_name }}-win-x86_64-sha256sum.txt
- name: Install cargo-wix - name: Install cargo-wix
run: cargo install cargo-wix run: cargo install cargo-wix
- name: Run cargo-wix - name: Run cargo-wix
@ -165,7 +82,7 @@ jobs:
- name: Checksum msi - name: Checksum msi
working-directory: target/wix working-directory: target/wix
shell: pwsh shell: pwsh
run: get-filehash -algorithm sha256 grim-${{ github.ref_name }}-win-x86_64.msi | Format-List | Out-String | ForEach-Object { $_.Trim() } > grim-${{ github.ref_name }}-win-x86_64-msi-sha256sum.txt run: sha256sum grim-${{ github.ref_name }}-win-x86_64.msi > grim-${{ github.ref_name }}-win-x86_64-msi-sha256sum.txt
- name: Release - name: Release
uses: softprops/action-gh-release@v1 uses: softprops/action-gh-release@v1
with: with:
@ -204,7 +121,7 @@ jobs:
- name: Checksum Release x86 - name: Checksum Release x86
working-directory: target/x86_64-apple-darwin/release working-directory: target/x86_64-apple-darwin/release
shell: pwsh shell: pwsh
run: get-filehash -algorithm sha256 grim-${{ github.ref_name }}-macos-x86_64.zip | Format-List | Out-String | ForEach-Object { $_.Trim() } > grim-${{ github.ref_name }}-macos-x86_64-sha256sum.txt run: sha256sum grim-${{ github.ref_name }}-macos-x86_64.zip > grim-${{ github.ref_name }}-macos-x86_64-sha256sum.txt
- name: Release ARM - name: Release ARM
run: | run: |
rustup target add aarch64-apple-darwin rustup target add aarch64-apple-darwin
@ -219,7 +136,7 @@ jobs:
- name: Checksum Release ARM - name: Checksum Release ARM
working-directory: target/aarch64-apple-darwin/release working-directory: target/aarch64-apple-darwin/release
shell: pwsh shell: pwsh
run: get-filehash -algorithm sha256 grim-${{ github.ref_name }}-macos-arm.zip | Format-List | Out-String | ForEach-Object { $_.Trim() } > grim-${{ github.ref_name }}-macos-arm-sha256sum.txt run: sha256sum grim-${{ github.ref_name }}-macos-arm.zip > grim-${{ github.ref_name }}-macos-arm-sha256sum.txt
- name: Release Universal - name: Release Universal
run: | run: |
rustup target add aarch64-apple-darwin rustup target add aarch64-apple-darwin
@ -235,7 +152,7 @@ jobs:
- name: Checksum Release Universal - name: Checksum Release Universal
working-directory: target/universal2-apple-darwin/release working-directory: target/universal2-apple-darwin/release
shell: pwsh shell: pwsh
run: get-filehash -algorithm sha256 grim-${{ github.ref_name }}-macos-universal.zip | Format-List | Out-String | ForEach-Object { $_.Trim() } > grim-${{ github.ref_name }}-macos-universal-sha256sum.txt run: sha256sum grim-${{ github.ref_name }}-macos-universal.zip > grim-${{ github.ref_name }}-macos-universal-sha256sum.txt
- name: Release - name: Release
uses: softprops/action-gh-release@v1 uses: softprops/action-gh-release@v1
with: with:

1
.gitignore vendored
View file

@ -13,7 +13,6 @@ android/keystore.properties
target target
.cargo/ .cargo/
app/src/main/jniLibs app/src/main/jniLibs
macos/Grim.app/Contents/MacOS/grim
macos/cert.pem macos/cert.pem
linux/Grim.AppDir/AppRun linux/Grim.AppDir/AppRun
.intentionally-empty-file.o .intentionally-empty-file.o

28
Cargo.lock generated
View file

@ -2483,6 +2483,12 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0688c2a7f92e427f44895cd63841bff7b29f8d7a1648b9e7e07a4a365b2e1257" checksum = "0688c2a7f92e427f44895cd63841bff7b29f8d7a1648b9e7e07a4a365b2e1257"
[[package]]
name = "doctest-file"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aac81fa3e28d21450aa4d2ac065992ba96a1d7303efbce51a95f4fd175b67562"
[[package]] [[package]]
name = "document-features" name = "document-features"
version = "0.2.8" version = "0.2.8"
@ -3833,6 +3839,7 @@ dependencies = [
"hyper 0.14.29", "hyper 0.14.29",
"hyper-tls 0.5.0", "hyper-tls 0.5.0",
"image 0.25.1", "image 0.25.1",
"interprocess",
"jni", "jni",
"lazy_static", "lazy_static",
"local-ip-address", "local-ip-address",
@ -4976,6 +4983,21 @@ dependencies = [
"syn 2.0.66", "syn 2.0.66",
] ]
[[package]]
name = "interprocess"
version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2f4e4a06d42fab3e85ab1b419ad32b09eab58b901d40c57935ff92db3287a13"
dependencies = [
"doctest-file",
"futures-core",
"libc",
"recvmsg",
"tokio 1.38.0",
"widestring",
"windows-sys 0.52.0",
]
[[package]] [[package]]
name = "intl-memoizer" name = "intl-memoizer"
version = "0.5.2" version = "0.5.2"
@ -7432,6 +7454,12 @@ dependencies = [
"rand_core 0.3.1", "rand_core 0.3.1",
] ]
[[package]]
name = "recvmsg"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3edd4d5d42c92f0a659926464d4cce56b562761267ecf0f469d85b7de384175"
[[package]] [[package]]
name = "redox_syscall" name = "redox_syscall"
version = "0.1.57" version = "0.1.57"

View file

@ -16,9 +16,6 @@ path = "src/main.rs"
name="grim" name="grim"
crate-type = ["rlib"] crate-type = ["rlib"]
[profile.release]
debug = 1
[profile.release-apk] [profile.release-apk]
inherits = "release" inherits = "release"
strip = true strip = true
@ -119,6 +116,7 @@ eframe = { version = "0.28.1", features = ["wgpu", "glow"] }
arboard = "3.2.0" arboard = "3.2.0"
rfd = "0.14.1" rfd = "0.14.1"
dark-light = "1.1.1" dark-light = "1.1.1"
interprocess = { version = "2.2.1", features = ["tokio"] }
[target.'cfg(target_os = "android")'.dependencies] [target.'cfg(target_os = "android")'.dependencies]
android_logger = "0.13.1" android_logger = "0.13.1"

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -40,6 +40,34 @@
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>1</string> <string>1</string>
<key>CFBundleDocumentTypes</key>
<array>
<dict>
<key>CFBundleTypeName</key>
<string>Apple SimpleText document</string>
<key>CFBundleTypeRole</key>
<string>Viewer</string>
<key>LSItemContentTypes</key>
<array>
<string>com.apple.traditional-mac-plain-text</string>
</array>
<key>NSDocumentClass</key>
<string>Document</string>
</dict>
<dict>
<key>CFBundleTypeName</key>
<string>Unknown document</string>
<key>CFBundleTypeRole</key>
<string>Viewer</string>
<key>LSItemContentTypes</key>
<array>
<string>public.data</string>
</array>
<key>NSDocumentClass</key>
<string>Document</string>
</dict>
</array>
<key>LSApplicationCategoryType</key> <key>LSApplicationCategoryType</key>
<string>public.app-category.finance</string> <string>public.app-category.finance</string>

View file

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

View file

@ -9,15 +9,14 @@ case $2 in
exit 1 exit 1
esac esac
if [[ ! -v SDKROOT ]]; then if [[ "$OSTYPE" != "darwin"* ]]; then
if [ -z ${SDKROOT+x} ]; then
echo "MacOS SDKROOT is not set" echo "MacOS SDKROOT is not set"
exit 1 exit 1
elif [[ -z "SDKROOT" ]]; then
echo "MacOS SDKROOT is set to the empty string"
exit 1
else else
echo "Use MacOS SDK: ${SDKROOT}" echo "Use MacOS SDK: ${SDKROOT}"
fi fi
fi
# Setup build directory # Setup build directory
BASEDIR=$(cd $(dirname $0) && pwd) BASEDIR=$(cd $(dirname $0) && pwd)
@ -28,22 +27,18 @@ cd ..
rustup target add x86_64-apple-darwin rustup target add x86_64-apple-darwin
rustup target add aarch64-apple-darwin rustup target add aarch64-apple-darwin
rm -rf target/x86_64-apple-darwin
rm -rf target/aarch64-apple-darwin
[[ $2 == "x86_64" ]] && arch+=(x86_64-apple-darwin) [[ $2 == "x86_64" ]] && arch+=(x86_64-apple-darwin)
[[ $2 == "arm" ]] && arch+=(aarch64-apple-darwin) [[ $2 == "arm" ]] && arch+=(aarch64-apple-darwin)
[[ $2 == "universal" ]] && arch+=(universal2-apple-darwin) [[ $2 == "universal" ]]; arch+=(universal2-apple-darwin)
# Start release build with zig linker for cross-compilation # Start release build with zig linker, requires zig 0.12.1
# zig 0.12+ required
cargo install cargo-zigbuild cargo install cargo-zigbuild
cargo zigbuild --release --target ${arch} cargo zigbuild --release --target ${arch}
rm -rf .intentionally-empty-file.o rm -rf .intentionally-empty-file.o
mkdir macos/Grim.app/Contents/MacOS
yes | cp -rf target/${arch}/release/grim macos/Grim.app/Contents/MacOS yes | cp -rf target/${arch}/release/grim macos/Grim.app/Contents/MacOS
### Sign .app resources on change: # Sign .app resources on change:
#rcodesign generate-self-signed-certificate #rcodesign generate-self-signed-certificate
#rcodesign sign --pem-file cert.pem macos/Grim.app #rcodesign sign --pem-file cert.pem macos/Grim.app

View file

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

View file

@ -32,25 +32,41 @@ lazy_static! {
/// Implements ui entry point and contains platform-specific callbacks. /// Implements ui entry point and contains platform-specific callbacks.
pub struct App<Platform> { pub struct App<Platform> {
/// Platform specific callbacks handler. /// Platform specific callbacks handler.
pub(crate) platform: Platform, pub platform: Platform,
/// Main ui content. /// Main content.
content: Content, content: Content,
/// Last window resize direction. /// Last window resize direction.
resize_direction: Option<ResizeDirection> resize_direction: Option<ResizeDirection>,
/// Flag to check if it's first draw.
first_draw: bool,
} }
impl<Platform: PlatformCallbacks> App<Platform> { impl<Platform: PlatformCallbacks> App<Platform> {
pub fn new(platform: Platform) -> Self { pub fn new(platform: Platform) -> Self {
Self { platform, content: Content::default(), resize_direction: None } Self {
platform,
content: Content::default(),
resize_direction: None,
first_draw: true
}
} }
/// Draw application content. /// Draw application content.
pub fn ui(&mut self, ctx: &Context) { pub fn ui(&mut self, ctx: &Context) {
// Set platform context on first draw.
if self.first_draw {
if View::is_desktop() {
self.platform.set_context(ctx);
}
self.first_draw = false;
}
// Handle Esc keyboard key event and platform Back button key event. // Handle Esc keyboard key event and platform Back button key event.
let back_pressed = BACK_BUTTON_PRESSED.load(Ordering::Relaxed); let back_pressed = BACK_BUTTON_PRESSED.load(Ordering::Relaxed);
if ctx.input_mut(|i| i.consume_key(Modifiers::NONE, egui::Key::Escape)) || back_pressed { if back_pressed || ctx.input_mut(|i| i.consume_key(Modifiers::NONE, egui::Key::Escape)) {
self.content.on_back(); self.content.on_back();
if back_pressed { if back_pressed {
BACK_BUTTON_PRESSED.store(false, Ordering::Relaxed); BACK_BUTTON_PRESSED.store(false, Ordering::Relaxed);
@ -59,8 +75,8 @@ impl<Platform: PlatformCallbacks> App<Platform> {
ctx.request_repaint(); ctx.request_repaint();
} }
// Handle Close event (on desktop). // Handle Close event on desktop.
if ctx.input(|i| i.viewport().close_requested()) { if View::is_desktop() && ctx.input(|i| i.viewport().close_requested()) {
if !self.content.exit_allowed { if !self.content.exit_allowed {
ctx.send_viewport_cmd(ViewportCommand::CancelClose); ctx.send_viewport_cmd(ViewportCommand::CancelClose);
Content::show_exit_modal(); Content::show_exit_modal();
@ -92,7 +108,20 @@ impl<Platform: PlatformCallbacks> App<Platform> {
} }
self.content.ui(ui, &self.platform); self.content.ui(ui, &self.platform);
} }
// Provide incoming data to wallets.
if let Some(data) = crate::consume_incoming_data() {
if !data.is_empty() {
self.content.wallets.on_data(ui, Some(data), &self.platform);
}
}
}); });
// Check if desktop window was focused after requested attention.
if self.platform.user_attention_required() &&
ctx.input(|i| i.viewport().focused.unwrap_or(true)) {
self.platform.clear_user_attention();
}
} }
/// Draw custom resizeable window content. /// Draw custom resizeable window content.

View file

@ -31,10 +31,14 @@ const YELLOW: Color32 = Color32::from_rgb(254, 241, 2);
const YELLOW_DARK: Color32 = Color32::from_rgb(239, 229, 3); const YELLOW_DARK: Color32 = Color32::from_rgb(239, 229, 3);
const GREEN: Color32 = Color32::from_rgb(0, 0x64, 0); const GREEN: Color32 = Color32::from_rgb(0, 0x64, 0);
const GREEN_DARK: Color32 = Color32::from_rgb(0, (0x64 as f32 * 1.3 + 0.5) as u8, 0);
const RED: Color32 = Color32::from_rgb(0x8B, 0, 0); const RED: Color32 = Color32::from_rgb(0x8B, 0, 0);
const RED_DARK: Color32 = Color32::from_rgb((0x8B as f32 * 1.3 + 0.5) as u8, 0, 0);
const BLUE: Color32 = Color32::from_rgb(0, 0x66, 0xE4); const BLUE: Color32 = Color32::from_rgb(0, 0x66, 0xE4);
const BLUE_DARK: Color32 =
Color32::from_rgb(0, (0x66 as f32 * 1.3 + 0.5) as u8, (0xE4 as f32 * 1.3 + 0.5) as u8);
const FILL: Color32 = Color32::from_gray(244); const FILL: Color32 = Color32::from_gray(244);
const FILL_DARK: Color32 = Color32::from_gray(24); const FILL_DARK: Color32 = Color32::from_gray(24);
@ -125,7 +129,7 @@ impl Colors {
pub fn green() -> Color32 { pub fn green() -> Color32 {
if use_dark() { if use_dark() {
GREEN.gamma_multiply(1.3) GREEN_DARK
} else { } else {
GREEN GREEN
} }
@ -133,7 +137,7 @@ impl Colors {
pub fn red() -> Color32 { pub fn red() -> Color32 {
if use_dark() { if use_dark() {
RED.gamma_multiply(1.3) RED_DARK
} else { } else {
RED RED
} }
@ -141,7 +145,7 @@ impl Colors {
pub fn blue() -> Color32 { pub fn blue() -> Color32 {
if use_dark() { if use_dark() {
BLUE.gamma_multiply(1.3) BLUE_DARK
} else { } else {
BLUE BLUE
} }

View file

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

View file

@ -14,11 +14,12 @@
use std::fs::File; use std::fs::File;
use std::io::Write; use std::io::Write;
use lazy_static::lazy_static; use std::thread;
use parking_lot::RwLock;
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc; use std::sync::Arc;
use std::thread; use parking_lot::RwLock;
use lazy_static::lazy_static;
use egui::{UserAttentionType, ViewportCommand, WindowLevel};
use rfd::FileDialog; use rfd::FileDialog;
use crate::gui::platform::PlatformCallbacks; use crate::gui::platform::PlatformCallbacks;
@ -26,19 +27,30 @@ use crate::gui::platform::PlatformCallbacks;
/// Desktop platform related actions. /// Desktop platform related actions.
#[derive(Clone)] #[derive(Clone)]
pub struct Desktop { pub struct Desktop {
/// Context to repaint content and handle viewport commands.
ctx: Arc<RwLock<Option<egui::Context>>>,
/// Flag to check if camera stop is needed. /// Flag to check if camera stop is needed.
stop_camera: Arc<AtomicBool>, stop_camera: Arc<AtomicBool>,
}
impl Default for Desktop { /// Flag to check if attention required after window focusing.
fn default() -> Self { attention_required: Arc<AtomicBool>,
Self {
stop_camera: Arc::new(AtomicBool::new(false)),
}
}
} }
impl PlatformCallbacks for Desktop { impl PlatformCallbacks for Desktop {
fn set_context(&mut self, ctx: &egui::Context) {
let mut w_ctx = self.ctx.write();
*w_ctx = Some(ctx.clone());
}
fn exit(&self) {
let r_ctx = self.ctx.read();
if r_ctx.is_some() {
let ctx = r_ctx.as_ref().unwrap();
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
}
}
fn show_keyboard(&self) {} fn show_keyboard(&self) {}
fn hide_keyboard(&self) {} fn hide_keyboard(&self) {}
@ -119,9 +131,55 @@ impl PlatformCallbacks for Desktop {
fn picked_file(&self) -> Option<String> { fn picked_file(&self) -> Option<String> {
None None
} }
fn request_user_attention(&self) {
let r_ctx = self.ctx.read();
if r_ctx.is_some() {
let ctx = r_ctx.as_ref().unwrap();
// Request attention on taskbar.
ctx.send_viewport_cmd(
ViewportCommand::RequestUserAttention(UserAttentionType::Informational)
);
// Un-minimize window.
if ctx.input(|i| i.viewport().minimized.unwrap_or(false)) {
ctx.send_viewport_cmd(ViewportCommand::Minimized(false));
}
// Focus to window.
if !ctx.input(|i| i.viewport().focused.unwrap_or(false)) {
ctx.send_viewport_cmd(ViewportCommand::WindowLevel(WindowLevel::AlwaysOnTop));
ctx.send_viewport_cmd(ViewportCommand::Focus);
}
ctx.request_repaint();
}
self.attention_required.store(true, Ordering::Relaxed);
}
fn user_attention_required(&self) -> bool {
self.attention_required.load(Ordering::Relaxed)
}
fn clear_user_attention(&self) {
let r_ctx = self.ctx.read();
if r_ctx.is_some() {
let ctx = r_ctx.as_ref().unwrap();
ctx.send_viewport_cmd(
ViewportCommand::RequestUserAttention(UserAttentionType::Reset)
);
ctx.send_viewport_cmd(ViewportCommand::WindowLevel(WindowLevel::Normal));
}
self.attention_required.store(false, Ordering::Relaxed);
}
} }
impl Desktop { impl Desktop {
pub fn new() -> Self {
Self {
stop_camera: Arc::new(AtomicBool::new(false)),
ctx: Arc::new(RwLock::new(None)),
attention_required: Arc::new(AtomicBool::new(false)),
}
}
#[allow(dead_code)] #[allow(dead_code)]
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
fn start_camera_capture(stop_camera: Arc<AtomicBool>) { fn start_camera_capture(stop_camera: Arc<AtomicBool>) {
@ -168,8 +226,7 @@ impl Desktop {
let ctx = PlatformContext::default(); let ctx = PlatformContext::default();
let devices = ctx.devices().unwrap(); let devices = ctx.devices().unwrap();
let dev = ctx.open_device(&devices[0].uri).unwrap(); if let Ok(dev) = ctx.open_device(&devices[0].uri) {
let streams = dev.streams().unwrap(); let streams = dev.streams().unwrap();
let stream_desc = streams[0].clone(); let stream_desc = streams[0].clone();
let w = stream_desc.width; let w = stream_desc.width;
@ -181,7 +238,6 @@ impl Desktop {
// Stop if camera was stopped. // Stop if camera was stopped.
if stop_camera.load(Ordering::Relaxed) { if stop_camera.load(Ordering::Relaxed) {
stop_camera.store(false, Ordering::Relaxed); stop_camera.store(false, Ordering::Relaxed);
// Clear image.
let mut w_image = LAST_CAMERA_IMAGE.write(); let mut w_image = LAST_CAMERA_IMAGE.write();
*w_image = None; *w_image = None;
break; break;
@ -201,6 +257,7 @@ impl Desktop {
} }
} }
} }
}
lazy_static! { lazy_static! {
/// Last captured image from started camera. /// Last captured image from started camera.

View file

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

View file

@ -40,8 +40,8 @@ pub struct Content {
/// Central panel [`WalletsContent`] content. /// Central panel [`WalletsContent`] content.
pub wallets: WalletsContent, pub wallets: WalletsContent,
/// Check if app exit is allowed on close event of [`eframe::App`] implementation. /// Check if app exit is allowed on Desktop close event.
pub(crate) exit_allowed: bool, pub exit_allowed: bool,
/// Flag to show exit progress at [`Modal`]. /// Flag to show exit progress at [`Modal`].
show_exit_progress: bool, show_exit_progress: bool,
@ -83,7 +83,7 @@ impl ModalContainer for Content {
modal: &Modal, modal: &Modal,
cb: &dyn PlatformCallbacks) { cb: &dyn PlatformCallbacks) {
match modal.id { match modal.id {
Self::EXIT_CONFIRMATION_MODAL => self.exit_modal_content(ui, modal), Self::EXIT_CONFIRMATION_MODAL => self.exit_modal_content(ui, modal, cb),
Self::SETTINGS_MODAL => self.settings_modal_ui(ui, modal), Self::SETTINGS_MODAL => self.settings_modal_ui(ui, modal),
Self::ANDROID_INTEGRATED_NODE_WARNING_MODAL => self.android_warning_modal_ui(ui, modal), Self::ANDROID_INTEGRATED_NODE_WARNING_MODAL => self.android_warning_modal_ui(ui, modal),
Self::CRASH_REPORT_MODAL => self.crash_report_modal_ui(ui, modal, cb), Self::CRASH_REPORT_MODAL => self.crash_report_modal_ui(ui, modal, cb),
@ -206,11 +206,11 @@ impl Content {
} }
/// Draw exit confirmation modal content. /// Draw exit confirmation modal content.
fn exit_modal_content(&mut self, ui: &mut egui::Ui, modal: &Modal) { fn exit_modal_content(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
if self.show_exit_progress { if self.show_exit_progress {
if !Node::is_running() { if !Node::is_running() {
self.exit_allowed = true; self.exit_allowed = true;
ui.ctx().send_viewport_cmd(egui::ViewportCommand::Close); cb.exit();
modal.close(); modal.close();
} }
ui.add_space(16.0); ui.add_space(16.0);
@ -241,10 +241,10 @@ impl Content {
}); });
}); });
columns[1].vertical_centered_justified(|ui| { columns[1].vertical_centered_justified(|ui| {
View::button_ui(ui, t!("modal_exit.exit"), Colors::white_or_black(false), |ui| { View::button_ui(ui, t!("modal_exit.exit"), Colors::white_or_black(false), |_| {
if !Node::is_running() { if !Node::is_running() {
self.exit_allowed = true; self.exit_allowed = true;
ui.ctx().send_viewport_cmd(egui::ViewportCommand::Close); cb.exit();
modal.close(); modal.close();
} else { } else {
Node::stop(true); Node::stop(true);

View file

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

View file

@ -35,7 +35,7 @@ pub struct Modal {
/// Identifier for modal. /// Identifier for modal.
pub(crate) id: &'static str, pub(crate) id: &'static str,
/// Position on the screen. /// Position on the screen.
position: ModalPosition, pub position: ModalPosition,
/// To check if it can be closed. /// To check if it can be closed.
closeable: Arc<AtomicBool>, closeable: Arc<AtomicBool>,
/// Title text /// Title text
@ -64,6 +64,12 @@ impl Modal {
self self
} }
/// Change [`Modal`] position on the screen.
pub fn change_position(position: ModalPosition) {
let mut w_state = MODAL_STATE.write();
w_state.modal.as_mut().unwrap().position = position;
}
/// Mark [`Modal`] closed. /// Mark [`Modal`] closed.
pub fn close(&self) { pub fn close(&self) {
let mut w_nav = MODAL_STATE.write(); let mut w_nav = MODAL_STATE.write();

View file

@ -83,7 +83,7 @@ impl Default for StratumSetup {
Self { Self {
wallets: WalletList::default(), wallets: WalletList::default(),
wallets_modal: WalletsModal::new(wallet_id), wallets_modal: WalletsModal::new(wallet_id, None, false),
available_ips: NodeConfig::get_ip_addrs(), available_ips: NodeConfig::get_ip_addrs(),
stratum_port_edit: port, stratum_port_edit: port,
stratum_port_available_edit: is_port_available, stratum_port_available_edit: is_port_available,
@ -111,10 +111,12 @@ impl ModalContainer for StratumSetup {
modal: &Modal, modal: &Modal,
cb: &dyn PlatformCallbacks) { cb: &dyn PlatformCallbacks) {
match modal.id { match modal.id {
WALLET_SELECTION_MODAL => self.wallets_modal.ui(ui, modal, &self.wallets, |id| { WALLET_SELECTION_MODAL => {
self.wallets_modal.ui(ui, modal, &mut self.wallets, cb, |id, _| {
NodeConfig::save_stratum_wallet_id(id); NodeConfig::save_stratum_wallet_id(id);
self.wallet_name = WalletConfig::name_by_id(id); self.wallet_name = WalletConfig::name_by_id(id);
}), })
},
STRATUM_PORT_MODAL => self.port_modal(ui, modal, cb), STRATUM_PORT_MODAL => self.port_modal(ui, modal, cb),
ATTEMPT_TIME_MODAL => self.attempt_modal(ui, modal, cb), ATTEMPT_TIME_MODAL => self.attempt_modal(ui, modal, cb),
MIN_SHARE_DIFF_MODAL => self.min_diff_modal(ui, modal, cb), MIN_SHARE_DIFF_MODAL => self.min_diff_modal(ui, modal, cb),
@ -240,7 +242,7 @@ impl StratumSetup {
/// Show wallet selection [`Modal`]. /// Show wallet selection [`Modal`].
fn show_wallets_modal(&mut self) { fn show_wallets_modal(&mut self) {
self.wallets_modal = WalletsModal::new(NodeConfig::get_stratum_wallet_id()); self.wallets_modal = WalletsModal::new(NodeConfig::get_stratum_wallet_id(), None, false);
// Show modal. // Show modal.
Modal::new(WALLET_SELECTION_MODAL) Modal::new(WALLET_SELECTION_MODAL)
.position(ModalPosition::Center) .position(ModalPosition::Center)

View file

@ -30,8 +30,8 @@ use crate::gui::views::View;
/// QR code image from text. /// QR code image from text.
pub struct QrCodeContent { pub struct QrCodeContent {
/// Text to create QR code. /// QR code text.
pub(crate) text: String, text: String,
/// Flag to draw animated QR with Uniform Resources /// Flag to draw animated QR with Uniform Resources
/// https://github.com/BlockchainCommons/Research/blob/master/papers/bcr-2020-005-ur.md /// https://github.com/BlockchainCommons/Research/blob/master/papers/bcr-2020-005-ur.md
@ -62,18 +62,18 @@ impl QrCodeContent {
} }
/// Draw QR code. /// Draw QR code.
pub fn ui(&mut self, ui: &mut egui::Ui, text: String, cb: &dyn PlatformCallbacks) { pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
if self.animated { if self.animated {
// Show animated QR code. // Show animated QR code.
self.animated_ui(ui, text, cb); self.animated_ui(ui, cb);
} else { } else {
// Show static QR code. // Show static QR code.
self.static_ui(ui, text, cb); self.static_ui(ui, cb);
} }
} }
/// Draw animated QR code content. /// Draw animated QR code content.
fn animated_ui(&mut self, ui: &mut egui::Ui, text: String, cb: &dyn PlatformCallbacks) { fn animated_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
if !self.has_image() { if !self.has_image() {
let space = (ui.available_width() - View::BIG_SPINNER_SIZE) / 2.0; let space = (ui.available_width() - View::BIG_SPINNER_SIZE) / 2.0;
ui.vertical_centered(|ui| { ui.vertical_centered(|ui| {
@ -84,7 +84,7 @@ impl QrCodeContent {
// Create multiple vector images from text if not creating. // Create multiple vector images from text if not creating.
if !self.loading() { if !self.loading() {
self.create_svg_list(text); self.create_svg_list();
} }
} else { } else {
let svg_list = { let svg_list = {
@ -111,7 +111,7 @@ impl QrCodeContent {
// Show QR code text. // Show QR code text.
ui.add_space(6.0); ui.add_space(6.0);
View::ellipsize_text(ui, text.clone(), 16.0, Colors::inactive_text()); View::ellipsize_text(ui, self.text.clone(), 16.0, Colors::inactive_text());
ui.add_space(6.0); ui.add_space(6.0);
ui.vertical_centered(|ui| { ui.vertical_centered(|ui| {
@ -131,7 +131,7 @@ impl QrCodeContent {
w_state.exporting = true; w_state.exporting = true;
} }
// Create GIF to export. // Create GIF to export.
self.create_qr_gif(text, DEFAULT_QR_SIZE as usize); self.create_qr_gif();
}); });
} else { } else {
ui.vertical_centered(|ui| { ui.vertical_centered(|ui| {
@ -171,7 +171,7 @@ impl QrCodeContent {
} }
/// Draw static QR code content. /// Draw static QR code content.
fn static_ui(&mut self, ui: &mut egui::Ui, text: String, cb: &dyn PlatformCallbacks) { fn static_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
if !self.has_image() { if !self.has_image() {
let space = (ui.available_width() - View::BIG_SPINNER_SIZE) / 2.0; let space = (ui.available_width() - View::BIG_SPINNER_SIZE) / 2.0;
ui.vertical_centered(|ui| { ui.vertical_centered(|ui| {
@ -182,7 +182,7 @@ impl QrCodeContent {
// Create vector image from text if not creating. // Create vector image from text if not creating.
if !self.loading() { if !self.loading() {
self.create_svg(text); self.create_svg();
} }
} else { } else {
// Create image from SVG data. // Create image from SVG data.
@ -194,7 +194,7 @@ impl QrCodeContent {
// Show QR code text. // Show QR code text.
ui.add_space(6.0); ui.add_space(6.0);
View::ellipsize_text(ui, text.clone(), 16.0, Colors::inactive_text()); View::ellipsize_text(ui, self.text.clone(), 16.0, Colors::inactive_text());
ui.add_space(6.0); ui.add_space(6.0);
// Show button to share QR. // Show button to share QR.
@ -204,7 +204,8 @@ impl QrCodeContent {
share_text, share_text,
Colors::blue(), Colors::blue(),
Colors::white_or_black(false), || { Colors::white_or_black(false), || {
if let Ok(qr) = QrCode::encode_text(text.as_str(), qrcodegen::QrCodeEcc::Low) { let text = self.text.as_str();
if let Ok(qr) = QrCode::encode_text(text, qrcodegen::QrCodeEcc::Low) {
if let Some(data) = Self::qr_to_image_data(qr, DEFAULT_QR_SIZE as usize) { if let Some(data) = Self::qr_to_image_data(qr, DEFAULT_QR_SIZE as usize) {
let mut png = vec![]; let mut png = vec![];
let png_enc = PngEncoder::new_with_quality(&mut png, let png_enc = PngEncoder::new_with_quality(&mut png,
@ -267,8 +268,9 @@ impl QrCodeContent {
} }
/// Create multiple vector QR code images at separate thread. /// Create multiple vector QR code images at separate thread.
fn create_svg_list(&self, text: String) { fn create_svg_list(&self) {
let qr_state = self.qr_image_state.clone(); let qr_state = self.qr_image_state.clone();
let text = self.text.clone();
thread::spawn(move || { thread::spawn(move || {
let mut encoder = ur::Encoder::bytes(text.as_bytes(), 100).unwrap(); let mut encoder = ur::Encoder::bytes(text.as_bytes(), 100).unwrap();
let mut data = Vec::with_capacity(encoder.fragment_count()); let mut data = Vec::with_capacity(encoder.fragment_count());
@ -294,8 +296,9 @@ impl QrCodeContent {
} }
/// Create vector QR code image at separate thread. /// Create vector QR code image at separate thread.
fn create_svg(&self, text: String) { fn create_svg(&self) {
let qr_state = self.qr_image_state.clone(); let qr_state = self.qr_image_state.clone();
let text = self.text.clone();
thread::spawn(move || { thread::spawn(move || {
if let Ok(qr) = QrCode::encode_text(text.as_str(), qrcodegen::QrCodeEcc::Low) { if let Ok(qr) = QrCode::encode_text(text.as_str(), qrcodegen::QrCodeEcc::Low) {
let svg = Self::qr_to_svg(qr, 0); let svg = Self::qr_to_svg(qr, 0);
@ -332,13 +335,14 @@ impl QrCodeContent {
} }
/// Create GIF image at separate thread. /// Create GIF image at separate thread.
fn create_qr_gif(&self, text: String, size: usize) { fn create_qr_gif(&self) {
{ {
let mut w_state = self.qr_image_state.write(); let mut w_state = self.qr_image_state.write();
w_state.gif_creating = true; w_state.gif_creating = true;
} }
let qr_state = self.qr_image_state.clone(); let qr_state = self.qr_image_state.clone();
let text = self.text.clone();
thread::spawn(move || { thread::spawn(move || {
// Setup GIF image encoder. // Setup GIF image encoder.
let mut gif = vec![]; let mut gif = vec![];
@ -354,7 +358,7 @@ impl QrCodeContent {
) { ) {
// Create an image from QR data. // Create an image from QR data.
let image = qr.render() let image = qr.render()
.max_dimensions(size as u32, size as u32) .max_dimensions(DEFAULT_QR_SIZE, DEFAULT_QR_SIZE)
.dark_color(image::Rgb([0, 0, 0])) .dark_color(image::Rgb([0, 0, 0]))
.light_color(image::Rgb([255, 255, 255])) .light_color(image::Rgb([255, 255, 255]))
.build(); .build();

View file

@ -18,13 +18,14 @@ use egui::scroll_area::ScrollBarVisibility;
use crate::AppConfig; use crate::AppConfig;
use crate::gui::Colors; use crate::gui::Colors;
use crate::gui::icons::{ARROW_LEFT, CARET_RIGHT, COMPUTER_TOWER, FOLDER_LOCK, FOLDER_OPEN, GEAR, GLOBE, GLOBE_SIMPLE, LOCK_KEY, PLUS, SIDEBAR_SIMPLE, SPINNER, SUITCASE, WARNING_CIRCLE}; use crate::gui::icons::{ARROW_LEFT, CARET_RIGHT, COMPUTER_TOWER, FOLDER_OPEN, GEAR, GLOBE, GLOBE_SIMPLE, LOCK_KEY, PLUS, SIDEBAR_SIMPLE, SUITCASE};
use crate::gui::platform::PlatformCallbacks; use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Modal, Content, TitlePanel, View}; use crate::gui::views::{Modal, Content, TitlePanel, View};
use crate::gui::views::types::{ModalContainer, ModalPosition, TextEditOptions, TitleContentType, TitleType}; use crate::gui::views::types::{ModalContainer, ModalPosition, TitleContentType, TitleType};
use crate::gui::views::wallets::creation::WalletCreation; use crate::gui::views::wallets::creation::WalletCreation;
use crate::gui::views::wallets::modals::WalletConnectionModal; use crate::gui::views::wallets::modals::{OpenWalletModal, WalletConnectionModal, WalletsModal};
use crate::gui::views::wallets::types::WalletTabType; use crate::gui::views::wallets::types::WalletTabType;
use crate::gui::views::wallets::wallet::types::status_text;
use crate::gui::views::wallets::WalletContent; use crate::gui::views::wallets::WalletContent;
use crate::wallet::{Wallet, WalletList}; use crate::wallet::{Wallet, WalletList};
@ -33,13 +34,12 @@ pub struct WalletsContent {
/// List of wallets. /// List of wallets.
wallets: WalletList, wallets: WalletList,
/// Password to open wallet for [`Modal`]. /// Wallet selection [`Modal`] content.
pass_edit: String, wallet_selection_content: Option<WalletsModal>,
/// Flag to check if wrong password was entered at [`Modal`]. /// Wallet opening [`Modal`] content.
wrong_pass: bool, open_wallet_content: Option<OpenWalletModal>,
/// Wallet connection selection content. /// Wallet connection selection content.
conn_modal_content: Option<WalletConnectionModal>, conn_selection_content: Option<WalletConnectionModal>,
/// Selected [`Wallet`] content. /// Selected [`Wallet`] content.
wallet_content: WalletContent, wallet_content: WalletContent,
@ -54,24 +54,29 @@ pub struct WalletsContent {
} }
/// Identifier for connection selection [`Modal`]. /// Identifier for connection selection [`Modal`].
const CONNECTION_SELECTION_MODAL: &'static str = "wallets_connection_selection_modal"; const CONNECTION_SELECTION_MODAL: &'static str = "wallets_connection_selection";
/// Identifier for wallet opening [`Modal`]. /// Identifier for wallet opening [`Modal`].
const OPEN_WALLET_MODAL: &'static str = "open_wallet_modal"; const OPEN_WALLET_MODAL: &'static str = "wallets_open_wallet";
/// Identifier for wallet opening [`Modal`].
const SELECT_WALLET_MODAL: &'static str = "wallets_select_wallet";
impl Default for WalletsContent { impl Default for WalletsContent {
fn default() -> Self { fn default() -> Self {
Self { Self {
wallets: WalletList::default(), wallets: WalletList::default(),
pass_edit: "".to_string(), wallet_selection_content: None,
wrong_pass: false, open_wallet_content: None,
conn_modal_content: None, conn_selection_content: None,
wallet_content: WalletContent::default(), wallet_content: WalletContent::new(None),
creation_content: WalletCreation::default(), creation_content: WalletCreation::default(),
show_wallets_at_dual_panel: AppConfig::show_wallets_at_dual_panel(), show_wallets_at_dual_panel: AppConfig::show_wallets_at_dual_panel(),
modal_ids: vec![ modal_ids: vec![
OPEN_WALLET_MODAL, OPEN_WALLET_MODAL,
WalletCreation::NAME_PASS_MODAL, WalletCreation::NAME_PASS_MODAL,
CONNECTION_SELECTION_MODAL, CONNECTION_SELECTION_MODAL,
SELECT_WALLET_MODAL
] ]
} }
} }
@ -87,13 +92,21 @@ impl ModalContainer for WalletsContent {
modal: &Modal, modal: &Modal,
cb: &dyn PlatformCallbacks) { cb: &dyn PlatformCallbacks) {
match modal.id { match modal.id {
OPEN_WALLET_MODAL => self.open_wallet_modal_ui(ui, modal, cb), OPEN_WALLET_MODAL => {
if let Some(content) = self.open_wallet_content.as_mut() {
content.ui(ui, modal, &mut self.wallets, cb, |data| {
// Setup wallet content.
self.wallet_content = WalletContent::new(data);
});
}
},
WalletCreation::NAME_PASS_MODAL => { WalletCreation::NAME_PASS_MODAL => {
self.creation_content.name_pass_modal_ui(ui, modal, cb) self.creation_content.name_pass_modal_ui(ui, modal, cb)
}, },
CONNECTION_SELECTION_MODAL => { CONNECTION_SELECTION_MODAL => {
if let Some(content) = self.conn_modal_content.as_mut() { if let Some(content) = self.conn_selection_content.as_mut() {
content.ui(ui, modal, cb, |id| { content.ui(ui, modal, cb, |id| {
// Update wallet connection on select.
let list = self.wallets.list(); let list = self.wallets.list();
for w in list { for w in list {
if self.wallets.selected_id == Some(w.get_config().id) { if self.wallets.selected_id == Some(w.get_config().id) {
@ -103,12 +116,20 @@ impl ModalContainer for WalletsContent {
}); });
} }
} }
SELECT_WALLET_MODAL => {
if let Some(content) = self.wallet_selection_content.as_mut() {
content.ui(ui, modal, &mut self.wallets, cb, |_, data| {
self.wallet_content = WalletContent::new(data);
});
}
}
_ => {} _ => {}
} }
} }
} }
impl WalletsContent { impl WalletsContent {
/// Draw wallets content.
pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
// Draw modal content for current ui container. // Draw modal content for current ui container.
self.current_modal_ui(ui, cb); self.current_modal_ui(ui, cb);
@ -159,7 +180,7 @@ impl WalletsContent {
// Add created wallet to list. // Add created wallet to list.
self.wallets.add(wallet); self.wallets.add(wallet);
// Reset wallet content. // Reset wallet content.
self.wallet_content = WalletContent::default(); self.wallet_content = WalletContent::new(None);
}); });
} else { } else {
let selected_id = self.wallets.selected_id.clone(); let selected_id = self.wallets.selected_id.clone();
@ -254,6 +275,57 @@ impl WalletsContent {
self.creation_content.can_go_back() self.creation_content.can_go_back()
} }
/// Handle data from deeplink or opened file.
pub fn on_data(&mut self, ui: &mut egui::Ui, data: Option<String>, cb: &dyn PlatformCallbacks) {
let wallets_size = self.wallets.list().len();
if wallets_size == 0 {
return;
}
// Close network panel on single panel mode.
if !Content::is_dual_panel_mode(ui) && Content::is_network_panel_open() {
Content::toggle_network_panel();
}
// Pass data to opened selected wallet or show wallets selection.
if self.wallets.is_selected_open() {
if wallets_size == 1 {
self.wallet_content = WalletContent::new(data);
} else {
self.show_wallet_selection_modal(data);
}
} else {
if wallets_size == 1 {
self.show_opening_modal(self.wallets.list()[0].get_config().id, data, cb);
} else {
self.show_wallet_selection_modal(data);
}
}
}
/// Show wallet selection with provided optional data.
fn show_wallet_selection_modal(&mut self, data: Option<String>) {
self.wallet_selection_content = Some(WalletsModal::new(None, data, true));
// Show wallet selection modal.
Modal::new(SELECT_WALLET_MODAL)
.position(ModalPosition::Center)
.title(t!("network_settings.choose_wallet"))
.show();
}
/// Handle Back key event returning `false` when event was handled.
pub fn on_back(&mut self) -> bool {
let can_go_back = self.creation_content.can_go_back();
if can_go_back {
self.creation_content.back();
return false
} else {
if self.wallets.is_selected_open() {
self.wallets.select(None);
return false
}
}
true
}
/// Draw [`TitlePanel`] content. /// Draw [`TitlePanel`] content.
fn title_ui(&mut self, fn title_ui(&mut self,
ui: &mut egui::Ui, ui: &mut egui::Ui,
@ -383,8 +455,7 @@ impl WalletsContent {
// Check if wallet reopen is needed. // Check if wallet reopen is needed.
if !wallet.is_open() && wallet.reopen_needed() { if !wallet.is_open() && wallet.reopen_needed() {
wallet.set_reopen(false); wallet.set_reopen(false);
self.wallets.select(Some(wallet.get_config().id)); self.show_opening_modal(wallet.get_config().id, None, cb);
self.show_open_wallet_modal(cb);
} }
// Draw wallet list item. // Draw wallet list item.
self.wallet_item_ui(ui, wallet, cb); self.wallet_item_ui(ui, wallet, cb);
@ -420,8 +491,7 @@ impl WalletsContent {
if !wallet.is_open() { if !wallet.is_open() {
// Show button to open closed wallet. // Show button to open closed wallet.
View::item_button(ui, View::item_rounding(0, 1, true), FOLDER_OPEN, None, || { View::item_button(ui, View::item_rounding(0, 1, true), FOLDER_OPEN, None, || {
self.wallets.select(Some(id)); self.show_opening_modal(id, None, cb);
self.show_open_wallet_modal(cb);
}); });
// Show button to select connection if not syncing. // Show button to select connection if not syncing.
if !wallet.syncing() { if !wallet.syncing() {
@ -435,7 +505,7 @@ impl WalletsContent {
// Show button to select opened wallet. // Show button to select opened wallet.
View::item_button(ui, View::item_rounding(0, 1, true), CARET_RIGHT, None, || { View::item_button(ui, View::item_rounding(0, 1, true), CARET_RIGHT, None, || {
self.wallets.select(Some(id)); self.wallets.select(Some(id));
self.wallet_content = WalletContent::default(); self.wallet_content = WalletContent::new(None);
}); });
} }
// Show button to close opened wallet. // Show button to close opened wallet.
@ -455,7 +525,7 @@ impl WalletsContent {
ui.add_space(6.0); ui.add_space(6.0);
ui.vertical(|ui| { ui.vertical(|ui| {
ui.add_space(3.0); ui.add_space(3.0);
// Setup wallet name text. // Show wallet name text.
let name_color = if is_selected { let name_color = if is_selected {
Colors::white_or_black(true) Colors::white_or_black(true)
} else { } else {
@ -466,42 +536,11 @@ impl WalletsContent {
View::ellipsize_text(ui, config.name, 18.0, name_color); View::ellipsize_text(ui, config.name, 18.0, name_color);
}); });
// Setup wallet status text. // Show wallet status text.
let status_text = if wallet.is_open() { View::ellipsize_text(ui, status_text(wallet), 15.0, Colors::text(false));
if wallet.sync_error() {
format!("{} {}", WARNING_CIRCLE, t!("error"))
} else if wallet.is_closing() {
format!("{} {}", SPINNER, t!("wallets.closing"))
} else if wallet.is_repairing() {
let repair_progress = wallet.repairing_progress();
if repair_progress == 0 {
format!("{} {}", SPINNER, t!("wallets.checking"))
} else {
format!("{} {}: {}%",
SPINNER,
t!("wallets.checking"),
repair_progress)
}
} else if wallet.syncing() {
let info_progress = wallet.info_sync_progress();
if info_progress == 100 || info_progress == 0 {
format!("{} {}", SPINNER, t!("wallets.loading"))
} else {
format!("{} {}: {}%",
SPINNER,
t!("wallets.loading"),
info_progress)
}
} else {
format!("{} {}", FOLDER_OPEN, t!("wallets.unlocked"))
}
} else {
format!("{} {}", FOLDER_LOCK, t!("wallets.locked"))
};
View::ellipsize_text(ui, status_text, 15.0, Colors::text(false));
ui.add_space(1.0); ui.add_space(1.0);
// Setup wallet connection text. // Show wallet connection text.
let conn_text = if let Some(conn) = wallet.get_current_ext_conn() { let conn_text = if let Some(conn) = wallet.get_current_ext_conn() {
format!("{} {}", GLOBE_SIMPLE, conn.url) format!("{} {}", GLOBE_SIMPLE, conn.url)
} else { } else {
@ -517,7 +556,7 @@ impl WalletsContent {
/// Show [`Modal`] to select connection for the wallet. /// Show [`Modal`] to select connection for the wallet.
fn show_connection_selector_modal(&mut self, wallet: &Wallet) { fn show_connection_selector_modal(&mut self, wallet: &Wallet) {
let ext_conn = wallet.get_current_ext_conn(); let ext_conn = wallet.get_current_ext_conn();
self.conn_modal_content = Some(WalletConnectionModal::new(ext_conn)); self.conn_selection_content = Some(WalletConnectionModal::new(ext_conn));
// Show modal. // Show modal.
Modal::new(CONNECTION_SELECTION_MODAL) Modal::new(CONNECTION_SELECTION_MODAL)
.position(ModalPosition::CenterTop) .position(ModalPosition::CenterTop)
@ -525,11 +564,10 @@ impl WalletsContent {
.show(); .show();
} }
/// Show [`Modal`] to open selected wallet. /// Show [`Modal`] to select and open wallet.
fn show_open_wallet_modal(&mut self, cb: &dyn PlatformCallbacks) { fn show_opening_modal(&mut self, id: i64, data: Option<String>, cb: &dyn PlatformCallbacks) {
// Reset modal values. self.wallets.select(Some(id));
self.pass_edit = String::from(""); self.open_wallet_content = Some(OpenWalletModal::new(data));
self.wrong_pass = false;
// Show modal. // Show modal.
Modal::new(OPEN_WALLET_MODAL) Modal::new(OPEN_WALLET_MODAL)
.position(ModalPosition::CenterTop) .position(ModalPosition::CenterTop)
@ -537,100 +575,6 @@ impl WalletsContent {
.show(); .show();
cb.show_keyboard(); cb.show_keyboard();
} }
/// Draw wallet opening [`Modal`] content.
fn open_wallet_modal_ui(&mut self,
ui: &mut egui::Ui,
modal: &Modal,
cb: &dyn PlatformCallbacks) {
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("wallets.pass"))
.size(17.0)
.color(Colors::gray()));
ui.add_space(8.0);
// Show password input.
let mut pass_edit_opts = TextEditOptions::new(Id::from(modal.id)).password();
View::text_edit(ui, cb, &mut self.pass_edit, &mut pass_edit_opts);
// Show information when password is empty.
if self.pass_edit.is_empty() {
self.wrong_pass = false;
ui.add_space(10.0);
ui.label(RichText::new(t!("wallets.pass_empty"))
.size(17.0)
.color(Colors::inactive_text()));
} else if self.wrong_pass {
ui.add_space(10.0);
ui.label(RichText::new(t!("wallets.wrong_pass"))
.size(17.0)
.color(Colors::red()));
}
ui.add_space(12.0);
});
// Show modal buttons.
ui.scope(|ui| {
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
// Close modal.
cb.hide_keyboard();
modal.close();
});
});
columns[1].vertical_centered_justified(|ui| {
// Callback for button to continue.
let mut on_continue = || {
if self.pass_edit.is_empty() {
return;
}
match self.wallets.open_selected(&self.pass_edit) {
Ok(_) => {
// Clear values.
self.pass_edit = "".to_string();
self.wrong_pass = false;
// Close modal.
cb.hide_keyboard();
modal.close();
// Reset wallet content.
self.wallet_content = WalletContent::default();
}
Err(_) => self.wrong_pass = true
}
};
// Continue on Enter key press.
View::on_enter_key(ui, || {
(on_continue)();
});
View::button(ui, t!("continue"), Colors::white_or_black(false), on_continue);
});
});
ui.add_space(6.0);
});
}
/// Handle Back key event.
/// Return `false` when event was handled.
pub fn on_back(&mut self) -> bool {
let can_go_back = self.creation_content.can_go_back();
if can_go_back {
self.creation_content.back();
return false
} else {
if self.wallets.is_selected_open() {
self.wallets.select(None);
return false
}
}
true
}
} }
/// Check if it's possible to show [`WalletsContent`] and [`WalletContent`] panels at same time. /// Check if it's possible to show [`WalletsContent`] and [`WalletContent`] panels at same time.

View file

@ -23,7 +23,7 @@ use crate::gui::views::{Modal, Content, View};
use crate::gui::views::types::{ModalPosition, TextEditOptions}; use crate::gui::views::types::{ModalPosition, TextEditOptions};
use crate::gui::views::wallets::creation::MnemonicSetup; use crate::gui::views::wallets::creation::MnemonicSetup;
use crate::gui::views::wallets::creation::types::Step; use crate::gui::views::wallets::creation::types::Step;
use crate::gui::views::wallets::settings::ConnectionSettings; use crate::gui::views::wallets::ConnectionSettings;
use crate::node::Node; use crate::node::Node;
use crate::wallet::{ExternalConnection, Wallet}; use crate::wallet::{ExternalConnection, Wallet};
use crate::wallet::types::PhraseMode; use crate::wallet::types::PhraseMode;

View file

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

View file

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

View file

@ -16,29 +16,53 @@ use egui::scroll_area::ScrollBarVisibility;
use egui::{Align, Layout, RichText, ScrollArea}; use egui::{Align, Layout, RichText, ScrollArea};
use crate::gui::Colors; use crate::gui::Colors;
use crate::gui::icons::{CHECK, CHECK_FAT, COMPUTER_TOWER, GLOBE_SIMPLE, PLUGS_CONNECTED}; use crate::gui::icons::{CHECK, CHECK_FAT, COMPUTER_TOWER, FOLDER_OPEN, GLOBE_SIMPLE, PLUGS_CONNECTED};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Modal, View}; use crate::gui::views::{Modal, View};
use crate::gui::views::types::ModalPosition;
use crate::gui::views::wallets::modals::OpenWalletModal;
use crate::gui::views::wallets::wallet::types::status_text;
use crate::wallet::{Wallet, WalletList}; use crate::wallet::{Wallet, WalletList};
/// Wallet list [`Modal`] content /// Wallet list [`Modal`] content
pub struct WalletsModal { pub struct WalletsModal {
/// Selected wallet id. /// Selected wallet id.
selected: Option<i64> selected: Option<i64>,
/// Optional data to pass after wallet selection.
data: Option<String>,
/// Flag to check if wallet can be opened from the list.
can_open: bool,
/// Wallet opening content.
open_wallet_content: Option<OpenWalletModal>,
} }
impl WalletsModal { impl WalletsModal {
pub fn new(selected: Option<i64>) -> Self { /// Create new content instance.
Self { pub fn new(selected: Option<i64>, data: Option<String>, can_open: bool) -> Self {
selected, Self { selected, data, can_open, open_wallet_content: None }
}
} }
/// Draw [`Modal`] content. /// Draw content.
pub fn ui(&mut self, pub fn ui(&mut self,
ui: &mut egui::Ui, ui: &mut egui::Ui,
modal: &Modal, modal: &Modal,
wallets: &WalletList, wallets: &mut WalletList,
mut on_select: impl FnMut(i64)) { cb: &dyn PlatformCallbacks,
mut on_select: impl FnMut(i64, Option<String>)) {
// Draw wallet opening content if requested.
if let Some(open_content) = self.open_wallet_content.as_mut() {
open_content.ui(ui, modal, wallets, cb, |data| {
modal.close();
if let Some(id) = self.selected {
on_select(id, data);
}
self.data = None;
});
return;
}
ui.add_space(4.0); ui.add_space(4.0);
ScrollArea::vertical() ScrollArea::vertical()
.max_height(373.0) .max_height(373.0)
@ -48,10 +72,12 @@ impl WalletsModal {
.show(ui, |ui| { .show(ui, |ui| {
ui.add_space(2.0); ui.add_space(2.0);
ui.vertical_centered(|ui| { ui.vertical_centered(|ui| {
for wallet in wallets.list() { let data = self.data.clone();
for wallet in wallets.clone().list() {
// Draw wallet list item. // Draw wallet list item.
self.wallet_item_ui(ui, wallet, modal, |id| { self.wallet_item_ui(ui, wallet, wallets, |id| {
on_select(id); modal.close();
on_select(id, data.clone());
}); });
ui.add_space(5.0); ui.add_space(5.0);
} }
@ -65,18 +91,19 @@ impl WalletsModal {
// Show button to close modal. // Show button to close modal.
ui.vertical_centered_justified(|ui| { ui.vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || { View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
self.data = None;
modal.close(); modal.close();
}); });
}); });
ui.add_space(6.0); ui.add_space(6.0);
} }
/// Draw wallet list item. /// Draw wallet list item with provided callback on select.
fn wallet_item_ui(&mut self, fn wallet_item_ui(&mut self,
ui: &mut egui::Ui, ui: &mut egui::Ui,
wallet: &Wallet, wallet: &Wallet,
modal: &Modal, wallets: &mut WalletList,
mut on_select: impl FnMut(i64)) { mut select: impl FnMut(i64)) {
let config = wallet.get_config(); let config = wallet.get_config();
let id = config.id; let id = config.id;
@ -87,6 +114,24 @@ impl WalletsModal {
ui.painter().rect(rect, rounding, Colors::fill(), View::hover_stroke()); ui.painter().rect(rect, rounding, Colors::fill(), View::hover_stroke());
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| { ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
if self.can_open {
// Show button to select or open closed wallet.
let icon = if wallet.is_open() {
CHECK
} else {
FOLDER_OPEN
};
View::item_button(ui, View::item_rounding(0, 1, true), icon, None, || {
wallets.select(Some(id));
if wallet.is_open() {
select(id);
} else {
self.selected = wallets.selected_id;
Modal::change_position(ModalPosition::CenterTop);
self.open_wallet_content = Some(OpenWalletModal::new(self.data.clone()));
}
});
} else {
// Draw button to select wallet. // Draw button to select wallet.
let current = self.selected.unwrap_or(0) == id; let current = self.selected.unwrap_or(0) == id;
if current { if current {
@ -94,23 +139,23 @@ impl WalletsModal {
ui.label(RichText::new(CHECK_FAT).size(20.0).color(Colors::green())); ui.label(RichText::new(CHECK_FAT).size(20.0).color(Colors::green()));
} else { } else {
View::item_button(ui, View::item_rounding(0, 1, true), CHECK, None, || { View::item_button(ui, View::item_rounding(0, 1, true), CHECK, None, || {
on_select(id); select(id);
modal.close();
}); });
} }
}
let layout_size = ui.available_size(); let layout_size = ui.available_size();
ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| { ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| {
ui.add_space(6.0); ui.add_space(6.0);
ui.vertical(|ui| { ui.vertical(|ui| {
ui.add_space(3.0); ui.add_space(3.0);
// Setup wallet name text. // Show wallet name text.
ui.with_layout(Layout::left_to_right(Align::Min), |ui| { ui.with_layout(Layout::left_to_right(Align::Min), |ui| {
ui.add_space(1.0); ui.add_space(1.0);
View::ellipsize_text(ui, config.name, 18.0, Colors::title(false)); View::ellipsize_text(ui, config.name, 18.0, Colors::title(false));
}); });
// Setup wallet connection text. // Show wallet connection text.
let conn = if let Some(conn) = wallet.get_current_ext_conn() { let conn = if let Some(conn) = wallet.get_current_ext_conn() {
format!("{} {}", GLOBE_SIMPLE, conn.url) format!("{} {}", GLOBE_SIMPLE, conn.url)
} else { } else {
@ -119,7 +164,12 @@ impl WalletsModal {
View::ellipsize_text(ui, conn, 15.0, Colors::text(false)); View::ellipsize_text(ui, conn, 15.0, Colors::text(false));
ui.add_space(1.0); ui.add_space(1.0);
// Setup wallet API text. // Show wallet API text or open status.
if self.can_open {
ui.label(RichText::new(status_text(wallet))
.size(15.0)
.color(Colors::gray()));
} else {
let address = if let Some(port) = config.api_port { let address = if let Some(port) = config.api_port {
format!("127.0.0.1:{}", port) format!("127.0.0.1:{}", port)
} else { } else {
@ -127,6 +177,7 @@ impl WalletsModal {
}; };
let api_text = format!("{} {}", PLUGS_CONNECTED, address); let api_text = format!("{} {}", PLUGS_CONNECTED, address);
ui.label(RichText::new(api_text).size(15.0).color(Colors::gray())); ui.label(RichText::new(api_text).size(15.0).color(Colors::gray()));
}
ui.add_space(3.0); ui.add_space(3.0);
}); });
}); });

View file

@ -13,57 +13,34 @@
// limitations under the License. // limitations under the License.
use std::time::Duration; use std::time::Duration;
use egui::{Align, Id, Layout, Margin, RichText, ScrollArea}; use egui::{Align, Id, Layout, Margin, RichText};
use egui::scroll_area::ScrollBarVisibility;
use grin_chain::SyncStatus; use grin_chain::SyncStatus;
use grin_core::core::amount_to_hr_string; use grin_core::core::amount_to_hr_string;
use crate::AppConfig; use crate::AppConfig;
use crate::gui::Colors; use crate::gui::Colors;
use crate::gui::icons::{ARROWS_CLOCKWISE, BRIDGE, CHAT_CIRCLE_TEXT, CHECK, CHECK_FAT, COPY, FOLDER_USER, GEAR_FINE, GRAPH, PACKAGE, PATH, POWER, SCAN, SPINNER, USERS_THREE}; use crate::gui::icons::{ARROWS_CLOCKWISE, BRIDGE, CHAT_CIRCLE_TEXT, FOLDER_USER, GEAR_FINE, GRAPH, PACKAGE, POWER, SCAN, SPINNER, USERS_THREE};
use crate::gui::platform::PlatformCallbacks; use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{CameraContent, Modal, Content, View}; use crate::gui::views::{Modal, Content, View};
use crate::gui::views::types::{ModalPosition, QrScanResult, TextEditOptions}; use crate::gui::views::types::{ModalPosition, QrScanResult};
use crate::gui::views::wallets::{WalletTransactions, WalletMessages, WalletTransport}; use crate::gui::views::wallets::{WalletTransactions, WalletMessages, WalletTransport};
use crate::gui::views::wallets::types::{GRIN, WalletTab, WalletTabType}; use crate::gui::views::wallets::types::{GRIN, WalletTab, WalletTabType};
use crate::gui::views::wallets::settings::WalletSettings; use crate::gui::views::wallets::wallet::modals::{WalletAccountsModal, WalletScanModal};
use crate::gui::views::wallets::wallet::WalletSettings;
use crate::node::Node; use crate::node::Node;
use crate::wallet::{Wallet, WalletConfig}; use crate::wallet::{Wallet, WalletConfig};
use crate::wallet::types::{WalletAccount, WalletData}; use crate::wallet::types::WalletData;
/// Selected and opened wallet content. /// Selected and opened wallet content.
pub struct WalletContent { pub struct WalletContent {
/// List of wallet accounts for [`Modal`]. /// Wallet accounts [`Modal`] content.
accounts: Vec<WalletAccount>, accounts_modal_content: Option<WalletAccountsModal>,
/// Flag to check if account is creating. /// QR code scan [`Modal`] content.
account_creating: bool, scan_modal_content: Option<WalletScanModal>,
/// Account label [`Modal`] value.
account_label_edit: String,
/// Flag to check if error occurred during account creation at [`Modal`].
account_creation_error: bool,
/// Camera content for QR scan [`Modal`].
camera_content: CameraContent,
/// QR code scan result
qr_scan_result: Option<QrScanResult>,
/// Current tab content to show. /// Current tab content to show.
pub current_tab: Box<dyn WalletTab> pub current_tab: Box<dyn WalletTab>,
}
impl Default for WalletContent {
fn default() -> Self {
Self {
accounts: vec![],
account_creating: false,
account_label_edit: "".to_string(),
account_creation_error: false,
camera_content: CameraContent::default(),
qr_scan_result: None,
current_tab: Box::new(WalletTransactions::default())
}
}
} }
/// Identifier for account list [`Modal`]. /// Identifier for account list [`Modal`].
@ -73,6 +50,20 @@ const ACCOUNT_LIST_MODAL: &'static str = "account_list_modal";
const QR_CODE_SCAN_MODAL: &'static str = "qr_code_scan_modal"; const QR_CODE_SCAN_MODAL: &'static str = "qr_code_scan_modal";
impl WalletContent { impl WalletContent {
/// Create new instance with optional data.
pub fn new(data: Option<String>) -> Self {
let mut content = Self {
accounts_modal_content: None,
scan_modal_content: None,
current_tab: Box::new(WalletTransactions::default()),
};
// Provide data to messages.
if data.is_some() {
content.current_tab = Box::new(WalletMessages::new(data));
}
content
}
/// Draw wallet content. /// Draw wallet content.
pub fn ui(&mut self, pub fn ui(&mut self,
ui: &mut egui::Ui, ui: &mut egui::Ui,
@ -148,7 +139,7 @@ impl WalletContent {
ui.vertical_centered(|ui| { ui.vertical_centered(|ui| {
// Draw wallet tabs. // Draw wallet tabs.
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| { View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
self.tabs_ui(ui, wallet); self.tabs_ui(ui);
}); });
}); });
}); });
@ -185,21 +176,50 @@ impl WalletContent {
/// Draw [`Modal`] content for this ui container. /// Draw [`Modal`] content for this ui container.
fn modal_content_ui(&mut self, fn modal_content_ui(&mut self,
ui: &mut egui::Ui, ui: &mut egui::Ui,
wallet: &mut Wallet, wallet: &Wallet,
cb: &dyn PlatformCallbacks) { cb: &dyn PlatformCallbacks) {
match Modal::opened() { match Modal::opened() {
None => {} None => {}
Some(id) => { Some(id) => {
match id { match id {
ACCOUNT_LIST_MODAL => { ACCOUNT_LIST_MODAL => {
if let Some(content) = self.accounts_modal_content.as_mut() {
Modal::ui(ui.ctx(), |ui, modal| { Modal::ui(ui.ctx(), |ui, modal| {
self.account_list_modal_ui(ui, wallet, modal, cb); content.ui(ui, wallet, modal, cb);
}); });
} }
}
QR_CODE_SCAN_MODAL => { QR_CODE_SCAN_MODAL => {
if let Some(content) = self.scan_modal_content.as_mut() {
Modal::ui(ui.ctx(), |ui, modal| { Modal::ui(ui.ctx(), |ui, modal| {
self.scan_qr_modal_ui(ui, wallet, modal, cb); content.ui(ui, wallet, modal, cb, |result| {
match result {
QrScanResult::Slatepack(message) => {
modal.close();
let msg = Some(message.to_string());
let messages = WalletMessages::new(msg);
self.current_tab = Box::new(messages);
return;
}
QrScanResult::Address(receiver) => {
let balance = wallet.get_data()
.unwrap()
.info
.amount_currently_spendable;
if balance > 0 {
modal.close();
let mut transport = WalletTransport::default();
let rec = Some(receiver.to_string());
transport.show_send_tor_modal(cb, rec);
self.current_tab = Box::new(transport);
return;
}
}
_ => {}
}
}); });
});
}
} }
_ => {} _ => {}
} }
@ -222,8 +242,7 @@ impl WalletContent {
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| { ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
// Draw button to scan QR code. // Draw button to scan QR code.
View::item_button(ui, View::item_rounding(0, 2, true), SCAN, None, || { View::item_button(ui, View::item_rounding(0, 2, true), SCAN, None, || {
self.qr_scan_result = None; self.scan_modal_content = Some(WalletScanModal::default());
self.camera_content.clear_state();
// Show QR code scan modal. // Show QR code scan modal.
Modal::new(QR_CODE_SCAN_MODAL) Modal::new(QR_CODE_SCAN_MODAL)
.position(ModalPosition::CenterTop) .position(ModalPosition::CenterTop)
@ -235,10 +254,7 @@ impl WalletContent {
// Draw button to show list of accounts. // Draw button to show list of accounts.
View::item_button(ui, View::item_rounding(1, 3, true), USERS_THREE, None, || { View::item_button(ui, View::item_rounding(1, 3, true), USERS_THREE, None, || {
// Load accounts. self.accounts_modal_content = Some(WalletAccountsModal::new(wallet.accounts()));
self.account_label_edit = "".to_string();
self.accounts = wallet.accounts();
self.account_creating = false;
// Show account list modal. // Show account list modal.
Modal::new(ACCOUNT_LIST_MODAL) Modal::new(ACCOUNT_LIST_MODAL)
.position(ModalPosition::CenterTop) .position(ModalPosition::CenterTop)
@ -305,230 +321,8 @@ impl WalletContent {
}); });
} }
/// Draw account list [`Modal`] content.
fn account_list_modal_ui(&mut self,
ui: &mut egui::Ui,
wallet: &mut Wallet,
modal: &Modal,
cb: &dyn PlatformCallbacks) {
if self.account_creating {
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("wallets.new_account_desc"))
.size(17.0)
.color(Colors::gray()));
ui.add_space(8.0);
// Draw account name edit.
let text_edit_id = Id::from(modal.id).with(wallet.get_config().id);
let mut text_edit_opts = TextEditOptions::new(text_edit_id);
View::text_edit(ui, cb, &mut self.account_label_edit, &mut text_edit_opts);
// Show error occurred during account creation..
if self.account_creation_error {
ui.add_space(12.0);
ui.label(RichText::new(t!("error"))
.size(17.0)
.color(Colors::red()));
}
ui.add_space(12.0);
});
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
// Show modal buttons.
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
// Close modal.
cb.hide_keyboard();
modal.close();
});
});
columns[1].vertical_centered_justified(|ui| {
// Create button callback.
let mut on_create = || {
if !self.account_label_edit.is_empty() {
let label = &self.account_label_edit;
match wallet.create_account(label) {
Ok(_) => {
let _ = wallet.set_active_account(label);
cb.hide_keyboard();
modal.close();
},
Err(_) => self.account_creation_error = true
};
}
};
View::on_enter_key(ui, || {
(on_create)();
});
View::button(ui, t!("create"), Colors::white_or_black(false), on_create);
});
});
ui.add_space(6.0);
} else {
ui.add_space(3.0);
// Show list of accounts.
let size = self.accounts.len();
ScrollArea::vertical()
.id_source("account_list_modal_scroll")
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.max_height(266.0)
.auto_shrink([true; 2])
.show_rows(ui, ACCOUNT_ITEM_HEIGHT, size, |ui, row_range| {
for index in row_range {
// Add space before the first item.
if index == 0 {
ui.add_space(4.0);
}
let acc = self.accounts.get(index).unwrap();
account_item_ui(ui, modal, wallet, acc, index, size);
if index == size - 1 {
ui.add_space(4.0);
}
}
});
ui.add_space(2.0);
View::horizontal_line(ui, Colors::stroke());
ui.add_space(6.0);
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
// Show modal buttons.
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
modal.close();
});
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("create"), Colors::white_or_black(false), || {
self.account_creating = true;
cb.show_keyboard();
});
});
});
ui.add_space(6.0);
}
}
/// Draw QR code scan [`Modal`] content.
fn scan_qr_modal_ui(&mut self,
ui: &mut egui::Ui,
wallet: &mut Wallet,
modal: &Modal,
cb: &dyn PlatformCallbacks) {
// Show scan result if exists or show camera content while scanning.
if let Some(result) = &self.qr_scan_result {
let mut result_text = result.text();
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(3.0);
ScrollArea::vertical()
.id_source(Id::from("qr_scan_result_input").with(wallet.get_config().id))
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.max_height(128.0)
.auto_shrink([false; 2])
.show(ui, |ui| {
ui.add_space(7.0);
egui::TextEdit::multiline(&mut result_text)
.font(egui::TextStyle::Small)
.desired_rows(5)
.interactive(false)
.desired_width(f32::INFINITY)
.show(ui);
ui.add_space(6.0);
});
ui.add_space(2.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(10.0);
// Show copy button.
ui.vertical_centered(|ui| {
let copy_text = format!("{} {}", COPY, t!("copy"));
View::button(ui, copy_text, Colors::button(), || {
cb.copy_string_to_buffer(result_text.to_string());
self.qr_scan_result = None;
modal.close();
});
});
ui.add_space(10.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(6.0);
} else if let Some(result) = self.camera_content.qr_scan_result() {
cb.stop_camera();
self.camera_content.clear_state();
match &result {
QrScanResult::Slatepack(message) => {
// Redirect to messages to handle parsed message.
let mut messages =
WalletMessages::new(wallet.can_use_dandelion(), Some(message.to_string()));
messages.parse_message(wallet);
modal.close();
self.current_tab = Box::new(messages);
return;
}
QrScanResult::Address(receiver) => {
if wallet.get_data().unwrap().info.amount_currently_spendable > 0 {
// Redirect to send amount with Tor.
let addr = wallet.slatepack_address().unwrap();
let mut transport = WalletTransport::new(addr.clone());
modal.close();
transport.show_send_tor_modal(cb, Some(receiver.to_string()));
self.current_tab = Box::new(transport);
return;
}
}
_ => {}
}
// Set result and rename modal title.
self.qr_scan_result = Some(result);
Modal::set_title(t!("scan_result"));
} else {
ui.add_space(6.0);
self.camera_content.ui(ui, cb);
ui.add_space(6.0);
}
if self.qr_scan_result.is_some() {
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(ui, t!("close"), Colors::white_or_black(false), || {
self.qr_scan_result = None;
modal.close();
});
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("repeat"), Colors::white_or_black(false), || {
Modal::set_title(t!("scan_qr"));
self.qr_scan_result = None;
cb.start_camera();
});
});
});
} else {
ui.vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
cb.stop_camera();
modal.close();
});
});
}
ui.add_space(6.0);
}
/// Draw tab buttons in the bottom of the screen. /// Draw tab buttons in the bottom of the screen.
fn tabs_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet) { fn tabs_ui(&mut self, ui: &mut egui::Ui) {
ui.scope(|ui| { ui.scope(|ui| {
// Setup spacing between tabs. // Setup spacing between tabs.
ui.style_mut().spacing.item_spacing = egui::vec2(View::TAB_ITEMS_PADDING, 0.0); ui.style_mut().spacing.item_spacing = egui::vec2(View::TAB_ITEMS_PADDING, 0.0);
@ -547,14 +341,13 @@ impl WalletContent {
let is_messages = current_type == WalletTabType::Messages; let is_messages = current_type == WalletTabType::Messages;
View::tab_button(ui, CHAT_CIRCLE_TEXT, is_messages, || { View::tab_button(ui, CHAT_CIRCLE_TEXT, is_messages, || {
self.current_tab = Box::new( self.current_tab = Box::new(
WalletMessages::new(wallet.can_use_dandelion(), None) WalletMessages::new(None)
); );
}); });
}); });
columns[2].vertical_centered_justified(|ui| { columns[2].vertical_centered_justified(|ui| {
View::tab_button(ui, BRIDGE, current_type == WalletTabType::Transport, || { View::tab_button(ui, BRIDGE, current_type == WalletTabType::Transport, || {
let addr = wallet.slatepack_address().unwrap(); self.current_tab = Box::new(WalletTransport::default());
self.current_tab = Box::new(WalletTransport::new(addr));
}); });
}); });
columns[3].vertical_centered_justified(|ui| { columns[3].vertical_centered_justified(|ui| {
@ -676,67 +469,3 @@ 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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,240 @@
// Copyright 2024 The Grim Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use egui::{Align, Id, Layout, RichText, ScrollArea};
use egui::scroll_area::ScrollBarVisibility;
use grin_core::core::amount_to_hr_string;
use crate::gui::Colors;
use crate::gui::icons::{CHECK, CHECK_FAT, FOLDER_USER, PATH};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Modal, View};
use crate::gui::views::types::TextEditOptions;
use crate::gui::views::wallets::wallet::types::GRIN;
use crate::wallet::types::WalletAccount;
use crate::wallet::{Wallet, WalletConfig};
/// Wallet accounts [`Modal`] content.
pub struct WalletAccountsModal {
/// List of wallet accounts.
accounts: Vec<WalletAccount>,
/// Flag to check if account is creating.
account_creating: bool,
/// Account label value.
account_label_edit: String,
/// Flag to check if error occurred during account creation.
account_creation_error: bool,
}
impl Default for WalletAccountsModal {
fn default() -> Self {
Self {
accounts: vec![],
account_creating: false,
account_label_edit: "".to_string(),
account_creation_error: false,
}
}
}
impl WalletAccountsModal {
/// Create new instance from wallet accounts.
pub fn new(accounts: Vec<WalletAccount>) -> Self {
Self {
accounts,
account_creating: false,
account_label_edit: "".to_string(),
account_creation_error: false,
}
}
/// Draw [`Modal`] content.
pub fn ui(&mut self,
ui: &mut egui::Ui,
wallet: &Wallet,
modal: &Modal,
cb: &dyn PlatformCallbacks) {
if self.account_creating {
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("wallets.new_account_desc"))
.size(17.0)
.color(Colors::gray()));
ui.add_space(8.0);
// Draw account name edit.
let text_edit_id = Id::from(modal.id).with(wallet.get_config().id);
let mut text_edit_opts = TextEditOptions::new(text_edit_id);
View::text_edit(ui, cb, &mut self.account_label_edit, &mut text_edit_opts);
// Show error occurred during account creation..
if self.account_creation_error {
ui.add_space(12.0);
ui.label(RichText::new(t!("error"))
.size(17.0)
.color(Colors::red()));
}
ui.add_space(12.0);
});
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
// Show modal buttons.
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
// Close modal.
cb.hide_keyboard();
modal.close();
});
});
columns[1].vertical_centered_justified(|ui| {
// Create button callback.
let mut on_create = || {
if !self.account_label_edit.is_empty() {
let label = &self.account_label_edit;
match wallet.create_account(label) {
Ok(_) => {
let _ = wallet.set_active_account(label);
cb.hide_keyboard();
modal.close();
},
Err(_) => self.account_creation_error = true
};
}
};
View::on_enter_key(ui, || {
(on_create)();
});
View::button(ui, t!("create"), Colors::white_or_black(false), on_create);
});
});
ui.add_space(6.0);
} else {
ui.add_space(3.0);
// Show list of accounts.
let size = self.accounts.len();
ScrollArea::vertical()
.id_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);
});
});
});
});
}

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 accounts;
pub use accounts::*;
mod scan;
pub use scan::*;

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);
}
}

View file

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

View file

@ -18,7 +18,7 @@ use egui::scroll_area::ScrollBarVisibility;
use crate::gui::Colors; use crate::gui::Colors;
use crate::gui::platform::PlatformCallbacks; use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Content, View}; use crate::gui::views::{Content, View};
use crate::gui::views::wallets::settings::{CommonSettings, ConnectionSettings, RecoverySettings}; use crate::gui::views::wallets::{CommonSettings, ConnectionSettings, RecoverySettings};
use crate::gui::views::wallets::types::{WalletTab, WalletTabType}; use crate::gui::views::wallets::types::{WalletTab, WalletTabType};
use crate::gui::views::wallets::WalletContent; use crate::gui::views::wallets::WalletContent;
use crate::wallet::Wallet; use crate::wallet::Wallet;

View file

@ -232,7 +232,7 @@ impl RecoverySettings {
}); });
}); });
columns[1].vertical_centered_justified(|ui| { columns[1].vertical_centered_justified(|ui| {
View::button(ui, "OK".to_owned(), Colors::white_or_black(false), || { let mut on_next = || {
match wallet.get_recovery(self.pass_edit.clone()) { match wallet.get_recovery(self.pass_edit.clone()) {
Ok(phrase) => { Ok(phrase) => {
self.wrong_pass = false; self.wrong_pass = false;
@ -243,6 +243,12 @@ impl RecoverySettings {
self.wrong_pass = true; self.wrong_pass = true;
} }
} }
};
View::on_enter_key(ui, || {
(on_next)();
});
View::button(ui, "OK".to_owned(), Colors::white_or_black(false), || {
on_next();
}); });
}); });
}); });

View file

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

View file

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

View file

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

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

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

View file

@ -17,6 +17,9 @@ extern crate rust_i18n;
use eframe::NativeOptions; use eframe::NativeOptions;
use egui::{Context, Stroke}; use egui::{Context, Stroke};
use lazy_static::lazy_static;
use std::sync::Arc;
use parking_lot::RwLock;
#[cfg(target_os = "android")] #[cfg(target_os = "android")]
use winit::platform::android::activity::AndroidApp; use winit::platform::android::activity::AndroidApp;
@ -256,3 +259,54 @@ fn setup_i18n() {
} }
} }
} }
/// Get data from deeplink or opened file.
pub fn consume_incoming_data() -> Option<String> {
let has_data = {
let r_data = INCOMING_DATA.read();
r_data.is_some()
};
if has_data {
// Clear data.
let mut w_data = INCOMING_DATA.write();
let data = w_data.clone();
*w_data = None;
return data;
}
None
}
/// Provide data from deeplink or opened file.
pub fn on_data(data: String) {
let mut w_data = INCOMING_DATA.write();
*w_data = Some(data);
}
lazy_static! {
/// Data provided from deeplink or opened file.
pub static ref INCOMING_DATA: Arc<RwLock<Option<String>>> = Arc::new(RwLock::new(None));
}
/// Callback from Java code with with passed data.
#[allow(dead_code)]
#[allow(non_snake_case)]
#[cfg(target_os = "android")]
#[no_mangle]
pub extern "C" fn Java_mw_gri_android_MainActivity_onData(
_env: jni::JNIEnv,
_class: jni::objects::JObject,
char: jni::sys::jstring
) {
unsafe {
let j_obj = jni::objects::JString::from_raw(char);
if let Ok(j_str) = _env.get_string_unchecked(j_obj.as_ref()) {
match j_str.to_str() {
Ok(str) => {
let mut w_path = INCOMING_DATA.write();
*w_path = Some(str.to_string());
}
Err(_) => {}
}
};
}
}

View file

@ -29,6 +29,18 @@ fn real_main() {
.parse_default_env() .parse_default_env()
.init(); .init();
// Handle file path argument passing.
let args: Vec<_> = std::env::args().collect();
let mut data = None;
if args.len() > 1 {
let path = std::path::PathBuf::from(&args[1]);
let content = match std::fs::read_to_string(path) {
Ok(s) => Some(s),
Err(_) => Some(args[1].clone())
};
data = content
}
// Setup callback on panic crash. // Setup callback on panic crash.
std::panic::set_hook(Box::new(|info| { std::panic::set_hook(Box::new(|info| {
let backtrace = backtrace::Backtrace::new(); let backtrace = backtrace::Backtrace::new();
@ -41,7 +53,7 @@ fn real_main() {
// Save backtrace to file. // Save backtrace to file.
let log = grim::Settings::crash_report_path(); let log = grim::Settings::crash_report_path();
if log.exists() { if log.exists() {
std::fs::remove_file(log.clone()).unwrap(); let _ = std::fs::remove_file(log.clone());
} }
std::fs::write(log, err.as_bytes()).unwrap(); std::fs::write(log, err.as_bytes()).unwrap();
// Setup flag to show crash after app restart. // Setup flag to show crash after app restart.
@ -49,20 +61,28 @@ fn real_main() {
})); }));
// Start GUI. // Start GUI.
let _ = std::panic::catch_unwind(|| { match std::panic::catch_unwind(|| {
start_desktop_gui(); if is_app_running(&data) {
}); return;
} else if let Some(data) = data {
grim::on_data(data);
}
let platform = grim::gui::platform::Desktop::new();
start_app_socket(platform.clone());
start_desktop_gui(platform);
}) {
Ok(_) => {}
Err(e) => println!("{:?}", e)
}
} }
/// Start GUI with Desktop related setup. /// Start GUI with Desktop related setup passing data from opening.
#[allow(dead_code)] #[allow(dead_code)]
#[cfg(not(target_os = "android"))] #[cfg(not(target_os = "android"))]
fn start_desktop_gui() { fn start_desktop_gui(platform: grim::gui::platform::Desktop) {
use grim::AppConfig; use grim::AppConfig;
use dark_light::Mode; use dark_light::Mode;
let platform = grim::gui::platform::Desktop::default();
// Setup system theme if not set. // Setup system theme if not set.
if let None = AppConfig::dark_theme() { if let None = AppConfig::dark_theme() {
let dark = match dark_light::detect() { let dark = match dark_light::detect() {
@ -73,12 +93,11 @@ fn start_desktop_gui() {
AppConfig::set_dark_theme(dark); AppConfig::set_dark_theme(dark);
} }
// Setup window size.
let (width, height) = AppConfig::window_size(); let (width, height) = AppConfig::window_size();
let mut viewport = egui::ViewportBuilder::default() let mut viewport = egui::ViewportBuilder::default()
.with_min_inner_size([AppConfig::MIN_WIDTH, AppConfig::MIN_HEIGHT]) .with_min_inner_size([AppConfig::MIN_WIDTH, AppConfig::MIN_HEIGHT])
.with_inner_size([width, height]); .with_inner_size([width, height]);
// Setup an icon. // Setup an icon.
if let Ok(icon) = eframe::icon_data::from_png_bytes(include_bytes!("../img/icon.png")) { if let Ok(icon) = eframe::icon_data::from_png_bytes(include_bytes!("../img/icon.png")) {
viewport = viewport.with_icon(std::sync::Arc::new(icon)); viewport = viewport.with_icon(std::sync::Arc::new(icon));
@ -90,6 +109,7 @@ fn start_desktop_gui() {
// Setup window decorations. // Setup window decorations.
let is_mac = egui::os::OperatingSystem::from_target_os() == egui::os::OperatingSystem::Mac; let is_mac = egui::os::OperatingSystem::from_target_os() == egui::os::OperatingSystem::Mac;
viewport = viewport viewport = viewport
.with_window_level(egui::WindowLevel::Normal)
.with_fullsize_content_view(true) .with_fullsize_content_view(true)
.with_title_shown(false) .with_title_shown(false)
.with_titlebar_buttons_shown(false) .with_titlebar_buttons_shown(false)
@ -110,7 +130,8 @@ fn start_desktop_gui() {
}; };
// Start GUI. // Start GUI.
match grim::start(options.clone(), grim::app_creator(grim::gui::App::new(platform.clone()))) { let app = grim::gui::App::new(platform.clone());
match grim::start(options.clone(), grim::app_creator(app)) {
Ok(_) => {} Ok(_) => {}
Err(e) => { Err(e) => {
if win { if win {
@ -118,7 +139,9 @@ fn start_desktop_gui() {
} }
// Start with another renderer on error. // Start with another renderer on error.
options.renderer = eframe::Renderer::Glow; options.renderer = eframe::Renderer::Glow;
match grim::start(options, grim::app_creator(grim::gui::App::new(platform))) {
let app = grim::gui::App::new(platform);
match grim::start(options, grim::app_creator(app)) {
Ok(_) => {} Ok(_) => {}
Err(e) => { Err(e) => {
panic!("{}", e); panic!("{}", e);
@ -127,3 +150,116 @@ 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(_) => {}
}
}
});
});
}

View file

@ -48,9 +48,10 @@ pub struct Settings {
impl Settings { impl Settings {
/// Main application directory name. /// Main application directory name.
pub const MAIN_DIR_NAME: &'static str = ".grim"; pub const MAIN_DIR_NAME: &'static str = ".grim";
/// Crash report file name. /// Crash report file name.
pub const CRASH_REPORT_FILE_NAME: &'static str = "crash.log"; pub const CRASH_REPORT_FILE_NAME: &'static str = "crash.log";
/// Application socket name.
pub const SOCKET_NAME: &'static str = "grim.sock";
/// Initialize settings with app and node configs. /// Initialize settings with app and node configs.
fn init() -> Self { fn init() -> Self {
@ -141,6 +142,13 @@ impl Settings {
path path
} }
/// Get desktop application socket path.
pub fn socket_path() -> PathBuf {
let mut socket_path = Self::base_path(None);
socket_path.push(Self::SOCKET_NAME);
socket_path
}
/// Get configuration file path from provided name and sub-directory if needed. /// Get configuration file path from provided name and sub-directory if needed.
pub fn config_path(config_name: &str, sub_dir: Option<String>) -> PathBuf { pub fn config_path(config_name: &str, sub_dir: Option<String>) -> PathBuf {
let mut path = Self::base_path(sub_dir); let mut path = Self::base_path(sub_dir);

View file

@ -18,7 +18,8 @@ use grin_wallet_libwallet::Error;
use crate::AppConfig; use crate::AppConfig;
use crate::wallet::{Wallet, WalletConfig}; use crate::wallet::{Wallet, WalletConfig};
/// Wrapper for [`Wallet`] list. /// [`Wallet`] list container.
#[derive(Clone)]
pub struct WalletList { pub struct WalletList {
/// List of wallets for [`ChainTypes::Mainnet`]. /// List of wallets for [`ChainTypes::Mainnet`].
pub main_list: Vec<Wallet>, pub main_list: Vec<Wallet>,

View file

@ -158,14 +158,12 @@ pub struct WalletTransaction {
pub amount: u64, pub amount: u64,
/// Flag to check if transaction is cancelling. /// Flag to check if transaction is cancelling.
pub cancelling: bool, pub cancelling: bool,
/// Flag to check if transaction is posting after finalization.
pub posting: bool,
/// Flag to check if transaction can be finalized based on Slatepack message state. /// Flag to check if transaction can be finalized based on Slatepack message state.
pub can_finalize: bool, pub can_finalize: bool,
/// Flag to check if transaction is finalizing.
pub finalizing: bool,
/// Block height when tx was confirmed. /// Block height when tx was confirmed.
pub conf_height: Option<u64>, pub conf_height: Option<u64>,
/// Block height when tx was reposted.
pub repost_height: Option<u64>,
/// Flag to check if tx was received after sync from node. /// Flag to check if tx was received after sync from node.
pub from_node: bool, pub from_node: bool,
} }
@ -173,16 +171,8 @@ pub struct WalletTransaction {
impl WalletTransaction { impl WalletTransaction {
/// Check if transaction can be cancelled. /// Check if transaction can be cancelled.
pub fn can_cancel(&self) -> bool { pub fn can_cancel(&self) -> bool {
self.from_node && !self.cancelling && !self.posting && !self.data.confirmed && self.from_node && !self.cancelling && !self.data.confirmed &&
self.data.tx_type != TxLogEntryType::TxReceivedCancelled self.data.tx_type != TxLogEntryType::TxReceivedCancelled
&& self.data.tx_type != TxLogEntryType::TxSentCancelled && self.data.tx_type != TxLogEntryType::TxSentCancelled
} }
/// Check if transaction can be reposted.
pub fn can_repost(&self, data: &WalletData) -> bool {
let last_height = data.info.last_confirmed_height;
let min_conf = data.info.minimum_confirmations;
self.from_node && self.posting && self.repost_height.is_some() &&
last_height - self.repost_height.unwrap() > min_conf
}
} }

View file

@ -437,8 +437,8 @@ impl Wallet {
// Mark wallet as not opened. // Mark wallet as not opened.
wallet_close.closing.store(false, Ordering::Relaxed); wallet_close.closing.store(false, Ordering::Relaxed);
wallet_close.is_open.store(false, Ordering::Relaxed); wallet_close.is_open.store(false, Ordering::Relaxed);
// Wake up thread to exit. // Start sync to exit from thread.
wallet_close.sync(true); wallet_close.sync();
}); });
} }
@ -464,14 +464,14 @@ impl Wallet {
}); });
} }
// Sync wallet data. // Refresh wallet info.
self.sync(false); sync_wallet_data(&self, false);
Ok(()) Ok(())
}) })
} }
/// Set active account from provided label. /// Set active account from provided label.
pub fn set_active_account(&mut self, label: &String) -> Result<(), Error> { pub fn set_active_account(&self, label: &String) -> Result<(), Error> {
let mut api = Owner::new(self.instance.clone().unwrap(), None); let mut api = Owner::new(self.instance.clone().unwrap(), None);
controller::owner_single_use(None, None, Some(&mut api), |api, m| { controller::owner_single_use(None, None, Some(&mut api), |api, m| {
api.set_active_account(m, label)?; api.set_active_account(m, label)?;
@ -498,7 +498,7 @@ impl Wallet {
self.info_sync_progress.store(0, Ordering::Relaxed); self.info_sync_progress.store(0, Ordering::Relaxed);
// Sync wallet data. // Sync wallet data.
self.sync(false); self.sync();
Ok(()) Ok(())
} }
@ -555,19 +555,12 @@ impl Wallet {
r_data.clone() r_data.clone()
} }
/// Sync wallet data from node or locally. /// Sync wallet data from node at sync thread or locally synchronously.
pub fn sync(&self, from_node: bool) { pub fn sync(&self) {
if from_node {
let thread_r = self.sync_thread.read(); let thread_r = self.sync_thread.read();
if let Some(thread) = thread_r.as_ref() { if let Some(thread) = thread_r.as_ref() {
thread.unpark(); thread.unpark();
} }
} else {
let wallet = self.clone();
thread::spawn(move || {
sync_wallet_data(&wallet, false);
});
}
} }
/// Check if wallet is syncing. /// Check if wallet is syncing.
@ -625,13 +618,7 @@ impl Wallet {
let mut slate = None; let mut slate = None;
if let Some(slate_id) = tx.data.tx_slate_id { if let Some(slate_id) = tx.data.tx_slate_id {
// Get slate state based on tx state and status. // Get slate state based on tx state and status.
let state = if tx.posting { let state = if !tx.data.confirmed && (tx.data.tx_type == TxLogEntryType::TxSent ||
if tx.data.tx_type == TxLogEntryType::TxSent {
Some(SlateState::Standard3)
} else {
Some(SlateState::Invoice3)
}
} else if !tx.data.confirmed && (tx.data.tx_type == TxLogEntryType::TxSent ||
tx.data.tx_type == TxLogEntryType::TxReceived) { tx.data.tx_type == TxLogEntryType::TxReceived) {
if tx.can_finalize { if tx.can_finalize {
if tx.data.tx_type == TxLogEntryType::TxSent { if tx.data.tx_type == TxLogEntryType::TxSent {
@ -681,7 +668,7 @@ impl Wallet {
} }
/// Initialize a transaction to send amount, return request for funds receiver. /// Initialize a transaction to send amount, return request for funds receiver.
pub fn send(&self, amount: u64) -> Result<(Slate, String), Error> { pub fn send(&self, amount: u64) -> Result<WalletTransaction, Error> {
let config = self.get_config(); let config = self.get_config();
let args = InitTxArgs { let args = InitTxArgs {
src_acct_name: Some(config.account), src_acct_name: Some(config.account),
@ -698,51 +685,35 @@ impl Wallet {
api.tx_lock_outputs(None, &slate)?; api.tx_lock_outputs(None, &slate)?;
// Create Slatepack message response. // Create Slatepack message response.
let message_resp = self.create_slatepack_message(&slate)?; let _ = self.create_slatepack_message(&slate)?;
// Sync wallet info. // Refresh wallet info.
self.sync(false); sync_wallet_data(&self, false);
Ok((slate, message_resp)) let tx = self.tx_by_slate(&slate).ok_or(Error::GenericError("No tx found".to_string()))?;
Ok(tx)
} }
/// Send amount to provided address with Tor transport. /// Send amount to provided address with Tor transport.
pub async fn send_tor(&mut self, amount: u64, addr: &SlatepackAddress) -> Option<Slate> { pub async fn send_tor(&mut self,
amount: u64,
addr: &SlatepackAddress) -> Result<WalletTransaction, Error> {
// Initialize transaction. // Initialize transaction.
let send_res = self.send(amount); let tx = self.send(amount)?;
let slate_res = self.read_slate_by_tx(&tx);
if send_res.is_err() { if slate_res.is_none() {
return None; return Err(Error::GenericError("Slate not found".to_string()));
} }
let slate = send_res.unwrap().0; let (slate, _) = slate_res.unwrap();
// Function to cancel initialized tx in case of error. // Function to cancel initialized tx in case of error.
let cancel_tx = || { let cancel_tx = || {
let instance = self.instance.clone().unwrap(); let instance = self.instance.clone().unwrap();
let id = slate.clone().id; let id = slate.clone().id;
cancel_tx(instance, None, &None, None, Some(id.clone())).unwrap(); cancel_tx(instance, None, &None, None, Some(id.clone())).unwrap();
// Setup posting flag, and ability to finalize.
{
let mut w_data = self.data.write();
let mut data = w_data.clone().unwrap();
let txs = data.txs.clone().unwrap().iter_mut().map(|tx| {
if tx.data.tx_slate_id == Some(id) {
tx.cancelling = false;
tx.posting = false;
tx.can_finalize = false;
tx.data.tx_type = if tx.data.tx_type == TxLogEntryType::TxReceived {
TxLogEntryType::TxReceivedCancelled
} else {
TxLogEntryType::TxSentCancelled
};
}
tx.clone()
}).collect::<Vec<WalletTransaction>>();
data.txs = Some(txs);
*w_data = Some(data);
}
// Refresh wallet info to update statuses. // Refresh wallet info to update statuses.
self.sync(false); sync_wallet_data(&self, false);
}; };
// Initialize parameters. // Initialize parameters.
@ -764,17 +735,15 @@ impl Wallet {
let req_res = Tor::post(body, url).await; let req_res = Tor::post(body, url).await;
if req_res.is_none() { if req_res.is_none() {
cancel_tx(); cancel_tx();
return None; return Err(Error::GenericError("Tor post error".to_string()));
} }
// Parse response and finalize transaction. // Parse response.
let res: Value = serde_json::from_str(&req_res.unwrap()).unwrap(); let res: Value = serde_json::from_str(&req_res.unwrap()).unwrap();
if res["error"] != json!(null) { if res["error"] != json!(null) {
cancel_tx(); cancel_tx();
return None; return Err(Error::GenericError("Tx error".to_string()));
} }
// Slatepack message json value.
let slate_value = res["result"]["Ok"].clone(); let slate_value = res["result"]["Ok"].clone();
let mut ret_slate = None; let mut ret_slate = None;
@ -788,7 +757,7 @@ impl Wallet {
// Save Slatepack message to file. // Save Slatepack message to file.
let _ = self.create_slatepack_message(&slate).unwrap_or("".to_string()); let _ = self.create_slatepack_message(&slate).unwrap_or("".to_string());
// Post transaction to blockchain. // Post transaction to blockchain.
let result = self.post(&slate, self.can_use_dandelion()); let result = self.post(&slate);
match result { match result {
Ok(_) => { Ok(_) => {
Ok(()) Ok(())
@ -798,21 +767,25 @@ impl Wallet {
} }
} }
} else { } else {
Err(Error::GenericError("TX finalization error".to_string())) Err(Error::GenericError("Tx finalization error".to_string()))
}; };
}).unwrap(); })?;
} }
Err(_) => {} Err(_) => {}
}; };
// Cancel transaction on error.
if ret_slate.is_none() { if ret_slate.is_none() {
cancel_tx(); cancel_tx();
return Err(Error::GenericError("Tx error".to_string()));
} }
ret_slate let tx = self.tx_by_slate(ret_slate.as_ref().unwrap())
.ok_or(Error::GenericError("No tx found".to_string()))?;
Ok(tx)
} }
/// Initialize an invoice transaction to receive amount, return request for funds sender. /// Initialize an invoice transaction to receive amount, return request for funds sender.
pub fn issue_invoice(&self, amount: u64) -> Result<(Slate, String), Error> { pub fn issue_invoice(&self, amount: u64) -> Result<WalletTransaction, Error> {
let args = IssueInvoiceTxArgs { let args = IssueInvoiceTxArgs {
dest_acct_name: None, dest_acct_name: None,
amount, amount,
@ -822,16 +795,17 @@ impl Wallet {
let slate = api.issue_invoice_tx(None, args)?; let slate = api.issue_invoice_tx(None, args)?;
// Create Slatepack message response. // Create Slatepack message response.
let response = self.create_slatepack_message(&slate.clone())?; let _ = self.create_slatepack_message(&slate)?;
// Sync wallet info. // Refresh wallet info.
self.sync(false); sync_wallet_data(&self, false);
Ok((slate, response)) let tx = self.tx_by_slate(&slate).ok_or(Error::GenericError("No tx found".to_string()))?;
Ok(tx)
} }
/// Handle message from the invoice issuer to send founds, return response for funds receiver. /// Handle message from the invoice issuer to send founds, return response for funds receiver.
pub fn pay(&self, message: &String) -> Result<String, Error> { pub fn pay(&self, message: &String) -> Result<WalletTransaction, Error> {
if let Ok(slate) = self.parse_slatepack(message) { if let Ok(slate) = self.parse_slatepack(message) {
let config = self.get_config(); let config = self.get_config();
let args = InitTxArgs { let args = InitTxArgs {
@ -846,19 +820,19 @@ impl Wallet {
api.tx_lock_outputs(None, &slate)?; api.tx_lock_outputs(None, &slate)?;
// Create Slatepack message response. // Create Slatepack message response.
let response = self.create_slatepack_message(&slate)?; let _ = self.create_slatepack_message(&slate)?;
// Sync wallet info. // Refresh wallet info.
self.sync(false); sync_wallet_data(&self, false);
Ok(response) Ok(self.tx_by_slate(&slate).ok_or(Error::GenericError("No tx found".to_string()))?)
} else { } else {
Err(Error::SlatepackDeser("Slatepack parsing error".to_string())) Err(Error::SlatepackDeser("Slatepack parsing error".to_string()))
} }
} }
/// Handle message to receive funds, return response to sender. /// Handle message to receive funds, return response to sender.
pub fn receive(&self, message: &String) -> Result<String, Error> { pub fn receive(&self, message: &String) -> Result<WalletTransaction, Error> {
if let Ok(mut slate) = self.parse_slatepack(message) { if let Ok(mut slate) = self.parse_slatepack(message) {
let api = Owner::new(self.instance.clone().unwrap(), None); let api = Owner::new(self.instance.clone().unwrap(), None);
controller::foreign_single_use(api.wallet_inst.clone(), None, |api| { controller::foreign_single_use(api.wallet_inst.clone(), None, |api| {
@ -866,61 +840,47 @@ impl Wallet {
Ok(()) Ok(())
})?; })?;
// Create Slatepack message response. // Create Slatepack message response.
let response = self.create_slatepack_message(&slate)?; let _ = self.create_slatepack_message(&slate)?;
// Sync wallet info. // Refresh wallet info.
self.sync(false); sync_wallet_data(&self, false);
Ok(response) Ok(self.tx_by_slate(&slate).ok_or(Error::GenericError("No tx found".to_string()))?)
} else { } else {
Err(Error::SlatepackDeser("Slatepack parsing error".to_string())) Err(Error::SlatepackDeser("Slatepack parsing error".to_string()))
} }
} }
/// Finalize transaction from provided message as sender or invoice issuer with Dandelion. /// Finalize transaction from provided message as sender or invoice issuer with Dandelion.
pub fn finalize(&self, message: &String, dandelion: bool) -> Result<Slate, Error> { pub fn finalize(&self, message: &String) -> Result<WalletTransaction, Error> {
if let Ok(mut slate) = self.parse_slatepack(message) { if let Ok(mut slate) = self.parse_slatepack(message) {
let api = Owner::new(self.instance.clone().unwrap(), None); let api = Owner::new(self.instance.clone().unwrap(), None);
slate = api.finalize_tx(None, &slate)?; slate = api.finalize_tx(None, &slate)?;
// Save Slatepack message to file. // Save Slatepack message to file.
let _ = self.create_slatepack_message(&slate)?; let _ = self.create_slatepack_message(&slate)?;
// Post transaction to blockchain. // Post transaction to blockchain.
let _ = self.post(&slate, dandelion); let tx = self.post(&slate)?;
Ok(slate)
// Refresh wallet info.
sync_wallet_data(&self, false);
Ok(tx)
} else { } else {
Err(Error::SlatepackDeser("Slatepack parsing error".to_string())) Err(Error::SlatepackDeser("Slatepack parsing error".to_string()))
} }
} }
/// Post transaction to blockchain. /// Post transaction to blockchain.
pub fn post(&self, slate: &Slate, dandelion: bool) -> Result<(), Error> { fn post(&self, slate: &Slate) -> Result<WalletTransaction, Error> {
// Post transaction to blockchain. // Post transaction to blockchain.
let api = Owner::new(self.instance.clone().unwrap(), None); let api = Owner::new(self.instance.clone().unwrap(), None);
api.post_tx(None, slate, dandelion)?; api.post_tx(None, slate, self.can_use_dandelion())?;
// Setup transaction repost height, posting flag and ability to finalize.
let mut slate = slate.clone(); // Refresh wallet info.
if slate.state == SlateState::Invoice2 { sync_wallet_data(&self, false);
slate.state = SlateState::Invoice3
} else if slate.state == SlateState::Standard2 { Ok(self.tx_by_slate(&slate).ok_or(Error::GenericError("No tx found".to_string()))?)
slate.state = SlateState::Standard3
};
if let Some(tx) = self.tx_by_slate(&slate) {
let mut w_data = self.data.write();
let mut data = w_data.clone().unwrap();
let mut data_txs = data.txs.unwrap();
for t in &mut data_txs {
if t.data.id == tx.data.id {
t.repost_height = Some(data.info.last_confirmed_height);
t.posting = true;
t.can_finalize = false;
}
}
data.txs = Some(data_txs);
*w_data = Some(data);
}
// Sync local wallet info.
self.sync(false);
Ok(())
} }
/// Cancel transaction. /// Cancel transaction.
@ -948,27 +908,7 @@ impl Wallet {
} }
let instance = wallet.instance.clone().unwrap(); let instance = wallet.instance.clone().unwrap();
let _ = cancel_tx(instance, None, &None, Some(id), None); let _ = cancel_tx(instance, None, &None, Some(id), None);
// Setup tx status, cancelling, posting flag, and ability to finalize.
{
let mut w_data = wallet.data.write();
let mut data = w_data.clone().unwrap();
let mut data_txs = data.txs.unwrap();
let txs = data_txs.iter_mut().map(|tx| {
if tx.data.id == id {
tx.cancelling = false;
tx.posting = false;
tx.can_finalize = false;
tx.data.tx_type = if tx.data.tx_type == TxLogEntryType::TxReceived {
TxLogEntryType::TxReceivedCancelled
} else {
TxLogEntryType::TxSentCancelled
};
}
tx.clone()
}).collect::<Vec<WalletTransaction>>();
data.txs = Some(txs);
*w_data = Some(data);
}
// Refresh wallet info to update statuses. // Refresh wallet info to update statuses.
sync_wallet_data(&wallet, false); sync_wallet_data(&wallet, false);
}); });
@ -985,7 +925,7 @@ impl Wallet {
/// Initiate wallet repair by scanning its outputs. /// Initiate wallet repair by scanning its outputs.
pub fn repair(&self) { pub fn repair(&self) {
self.repair_needed.store(true, Ordering::Relaxed); self.repair_needed.store(true, Ordering::Relaxed);
self.sync(true); self.sync();
} }
/// Check if wallet is repairing. /// Check if wallet is repairing.
@ -1013,7 +953,7 @@ impl Wallet {
// Remove wallet db files. // Remove wallet db files.
let _ = fs::remove_dir_all(wallet_delete.get_config().get_db_path()); let _ = fs::remove_dir_all(wallet_delete.get_config().get_db_path());
// Start sync to close thread. // Start sync to close thread.
wallet_delete.sync(true); wallet_delete.sync();
// Mark wallet to reopen. // Mark wallet to reopen.
wallet_delete.set_reopen(reopen); wallet_delete.set_reopen(reopen);
}); });
@ -1046,7 +986,7 @@ impl Wallet {
// Mark wallet as deleted. // Mark wallet as deleted.
wallet_delete.deleted.store(true, Ordering::Relaxed); wallet_delete.deleted.store(true, Ordering::Relaxed);
// Start sync to close thread. // Start sync to close thread.
wallet_delete.sync(true); wallet_delete.sync();
}); });
} }
@ -1268,7 +1208,7 @@ fn sync_wallet_data(wallet: &Wallet, from_node: bool) {
wallet.reset_sync_attempts(); wallet.reset_sync_attempts();
// Filter transactions for current account. // Filter transactions for current account.
let filter_txs = txs.1.iter().map(|v| v.clone()).filter(|tx| { let account_txs = txs.1.iter().map(|v| v.clone()).filter(|tx| {
match wallet.get_parent_key_id() { match wallet.get_parent_key_id() {
Ok(key) => { Ok(key) => {
tx.parent_key_id == key tx.parent_key_id == key
@ -1286,7 +1226,7 @@ fn sync_wallet_data(wallet: &Wallet, from_node: bool) {
// Create wallet txs. // Create wallet txs.
let mut new_txs: Vec<WalletTransaction> = vec![]; let mut new_txs: Vec<WalletTransaction> = vec![];
for tx in &filter_txs { for tx in &account_txs {
// Setup transaction amount. // Setup transaction amount.
let amount = if tx.amount_debited > tx.amount_credited { let amount = if tx.amount_debited > tx.amount_credited {
tx.amount_debited - tx.amount_credited tx.amount_debited - tx.amount_credited
@ -1294,42 +1234,13 @@ fn sync_wallet_data(wallet: &Wallet, from_node: bool) {
tx.amount_credited - tx.amount_debited tx.amount_credited - tx.amount_debited
}; };
// Setup flag for ability to finalize transaction.
let unconfirmed_sent_or_received = tx.tx_slate_id.is_some() && let unconfirmed_sent_or_received = tx.tx_slate_id.is_some() &&
!tx.confirmed && (tx.tx_type == TxLogEntryType::TxSent || !tx.confirmed && (tx.tx_type == TxLogEntryType::TxSent ||
tx.tx_type == TxLogEntryType::TxReceived); tx.tx_type == TxLogEntryType::TxReceived);
let mut finalizing = false;
// Setup transaction posting status based on slate state. let can_finalize = if unconfirmed_sent_or_received {
let posting = if unconfirmed_sent_or_received { let initial_state = {
// Create slate to check existing file.
let is_invoice = tx.tx_type == TxLogEntryType::TxReceived;
let mut slate = Slate::blank(0, is_invoice);
slate.id = tx.tx_slate_id.unwrap();
slate.state = match is_invoice {
true => SlateState::Invoice3,
_ => SlateState::Standard3
};
// Setup posting status if we have other tx with same slate id.
let mut same_tx_posting = false;
for t in &mut new_txs {
if t.data.tx_slate_id == tx.tx_slate_id &&
tx.tx_type != t.data.tx_type {
same_tx_posting = t.posting ||
wallet.read_slatepack(&slate).is_some();
if same_tx_posting && !t.posting {
t.posting = true;
}
break;
}
}
same_tx_posting || wallet.read_slatepack(&slate).is_some()
} else {
false
};
// Setup flag for ability to finalize transaction.
let can_finalize = if !posting && unconfirmed_sent_or_received {
// Check existing file.
let mut slate = Slate::blank(1, false); let mut slate = Slate::blank(1, false);
slate.id = tx.tx_slate_id.unwrap(); slate.id = tx.tx_slate_id.unwrap();
slate.state = match tx.tx_type { slate.state = match tx.tx_type {
@ -1337,11 +1248,22 @@ fn sync_wallet_data(wallet: &Wallet, from_node: bool) {
_ => SlateState::Standard1 _ => SlateState::Standard1
}; };
wallet.read_slatepack(&slate).is_some() wallet.read_slatepack(&slate).is_some()
};
finalizing = {
let mut slate = Slate::blank(1, false);
slate.id = tx.tx_slate_id.unwrap();
slate.state = match tx.tx_type {
TxLogEntryType::TxReceived => SlateState::Invoice3,
_ => SlateState::Standard3
};
wallet.read_slatepack(&slate).is_some()
};
initial_state && !finalizing
} else { } else {
false false
}; };
// Setup confirmation, reposting height and cancelling status. // Setup confirmation and cancelling status.
let mut conf_height = None; let mut conf_height = None;
let mut setup_conf_height = |t: &TxLogEntry, current_empty: bool| -> bool { let mut setup_conf_height = |t: &TxLogEntry, current_empty: bool| -> bool {
if current_empty && t.kernel_lookup_min_height.is_some() && if current_empty && t.kernel_lookup_min_height.is_some() &&
@ -1376,7 +1298,6 @@ fn sync_wallet_data(wallet: &Wallet, from_node: bool) {
false false
}; };
let mut repost_height = None;
let mut cancelling = false; let mut cancelling = false;
if data_txs.is_empty() { if data_txs.is_empty() {
setup_conf_height(tx, true); setup_conf_height(tx, true);
@ -1387,7 +1308,6 @@ fn sync_wallet_data(wallet: &Wallet, from_node: bool) {
t.conf_height.unwrap() == 0) { t.conf_height.unwrap() == 0) {
conf_height = t.conf_height; conf_height = t.conf_height;
} }
repost_height = t.repost_height;
if t.cancelling && if t.cancelling &&
tx.tx_type != TxLogEntryType::TxReceivedCancelled && tx.tx_type != TxLogEntryType::TxReceivedCancelled &&
tx.tx_type != TxLogEntryType::TxSentCancelled { tx.tx_type != TxLogEntryType::TxSentCancelled {
@ -1403,10 +1323,9 @@ fn sync_wallet_data(wallet: &Wallet, from_node: bool) {
data: tx.clone(), data: tx.clone(),
amount, amount,
cancelling, cancelling,
posting,
can_finalize, can_finalize,
finalizing,
conf_height, conf_height,
repost_height,
from_node: !fresh_sync || from_node from_node: !fresh_sync || from_node
}); });
} }

View file

@ -16,8 +16,8 @@
AllowSameVersionUpgrades = "yes" AllowSameVersionUpgrades = "yes"
/> />
<Icon Id='ProductICO' SourceFile='wix\Product.ico'/> <Icon Id='Product.ico' SourceFile='wix\Product.ico'/>
<Property Id='ARPPRODUCTICON' Value='ProductICO' /> <Property Id='ARPPRODUCTICON' Value='Product.ico' />
<Directory Id="TARGETDIR" Name="SourceDir"> <Directory Id="TARGETDIR" Name="SourceDir">
<Directory Id='$(var.PlatformProgramFilesFolder)'> <Directory Id='$(var.PlatformProgramFilesFolder)'>
@ -28,7 +28,7 @@
<Component Id="ApplicationShortcutDesktop" Guid="14efa019-7ed7-4765-8263-fa5460f92495"> <Component Id="ApplicationShortcutDesktop" Guid="14efa019-7ed7-4765-8263-fa5460f92495">
<Shortcut Id="ApplicationDesktopShortcut" <Shortcut Id="ApplicationDesktopShortcut"
Name="Grim" Name="Grim"
Icon="ProductICO" Icon="Product.ico"
Description="GUI for Grin" Description="GUI for Grin"
Target="[APPLICATIONROOTDIRECTORY]grim.exe" Target="[APPLICATIONROOTDIRECTORY]grim.exe"
WorkingDirectory="APPLICATIONROOTDIRECTORY"/> WorkingDirectory="APPLICATIONROOTDIRECTORY"/>
@ -55,6 +55,12 @@
</Component> </Component>
<Component Id="grim.exe" Guid="95444223-45BF-427A-85CA-61B035044305"> <Component Id="grim.exe" Guid="95444223-45BF-427A-85CA-61B035044305">
<File Id="grim.exe" Source="$(var.CargoTargetBinDir)\grim.exe" KeyPath="yes" Checksum="yes"/> <File Id="grim.exe" Source="$(var.CargoTargetBinDir)\grim.exe" KeyPath="yes" Checksum="yes"/>
<File Id="slatepack.ico" Source="wix\Product.ico" />
<ProgId Id='grim.slatepack' Description='Grin Slatepack message' Icon='slatepack.ico'>
<Extension Id='slatepack' ContentType='text/plain'>
<Verb Id='open' Command='Open' Target='[APPLICATIONROOTDIRECTORY]grim.exe' Argument='%1' />
</Extension>
</ProgId>
</Component> </Component>
</DirectoryRef> </DirectoryRef>
@ -64,7 +70,7 @@
<Shortcut Id="ApplicationStartMenuShortcut" <Shortcut Id="ApplicationStartMenuShortcut"
Name="Grim" Name="Grim"
Description="Cross-platform GUI for Grin with focus on usability and availability to be used by anyone, anywhere." Description="Cross-platform GUI for Grin with focus on usability and availability to be used by anyone, anywhere."
Icon="ProductICO" Icon="Product.ico"
Target="[#grim.exe]" Target="[#grim.exe]"
WorkingDirectory="APPLICATIONROOTDIRECTORY"/> WorkingDirectory="APPLICATIONROOTDIRECTORY"/>
<RemoveFolder Id="ApplicationProgramsFolder" On="uninstall"/> <RemoveFolder Id="ApplicationProgramsFolder" On="uninstall"/>