diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f199f0b..c47b10f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,43 +2,6 @@ name: Build on: [push, pull_request] jobs: - android: - name: Android Build - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Setup JDK 17 - uses: actions/setup-java@v3 - with: - java-version: '17' - distribution: 'temurin' - cache: gradle - - name: Setup build - run: | - cargo install cargo-ndk - rustup target add aarch64-linux-android - rustup target add armv7-linux-androideabi - rustup target add x86_64-linux-android - - name: Setup Java build - run: | - chmod +x android/gradlew - echo "${{ secrets.ANDROID_RELEASE_KEYSTORE }}" > release.keystore.asc - gpg -d --passphrase "${{ secrets.ANDROID_RELEASE_SECRET }}" --batch release.keystore.asc > android/keystore - echo -e "storePassword=${{ secrets.ANDROID_PASS }}\nkeyPassword=${{ secrets.ANDROID_PASS }}\nkeyAlias=grim\nstoreFile=../keystore" > android/keystore.properties - - name: Build lib 1/2 - continue-on-error: true - run: | - sed -i -e 's/"rlib"/"cdylib","rlib"/g' Cargo.toml - export CPPFLAGS="-DMDB_USE_ROBUST=0" && export CFLAGS="-DMDB_USE_ROBUST=0" && cargo ndk -t arm64-v8a build --profile release-apk - - name: Build lib 2/2 - run: | - unset CPPFLAGS && unset CFLAGS && cargo ndk -t arm64-v8a -o android/app/src/main/jniLibs build --profile release-apk - sed -i -e 's/"cdylib","rlib"/"rlib"/g' Cargo.toml - - name: Build APK - working-directory: android - run: | - ./gradlew assembleRelease - linux: name: Linux Build runs-on: ubuntu-latest diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 55daabd..cb40fb2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -6,89 +6,6 @@ on: - "v*.*.*" jobs: - android_release: - name: Android Release - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - name: Setup JDK 17 - uses: actions/setup-java@v3 - with: - java-version: '17' - distribution: 'temurin' - cache: gradle - - name: Setup Rust build - run: | - cargo install cargo-ndk - rustup target add aarch64-linux-android - rustup target add armv7-linux-androideabi - rustup target add x86_64-linux-android - - name: Setup Java build - run: | - chmod +x android/gradlew - echo "${{ secrets.ANDROID_RELEASE_KEYSTORE }}" > release.keystore.asc - gpg -d --passphrase "${{ secrets.ANDROID_RELEASE_SECRET }}" --batch release.keystore.asc > android/keystore - echo -e "storePassword=${{ secrets.ANDROID_PASS }}\nkeyPassword=${{ secrets.ANDROID_PASS }}\nkeyAlias=grim\nstoreFile=../keystore" > android/keystore.properties - - name: Build lib ARMv8 1/2 - continue-on-error: true - run: | - sed -i -e 's/"rlib"/"cdylib","rlib"/g' Cargo.toml - export CPPFLAGS="-DMDB_USE_ROBUST=0" && export CFLAGS="-DMDB_USE_ROBUST=0" && cargo ndk -t arm64-v8a build --profile release-apk - - name: Build lib ARMv8 2/2 - run: | - unset CPPFLAGS && unset CFLAGS && cargo ndk -t arm64-v8a -o android/app/src/main/jniLibs build --profile release-apk - sed -i -e 's/"cdylib","rlib"/"rlib"/g' Cargo.toml - - name: Build lib ARMv7 1/2 - continue-on-error: true - run: | - sed -i -e 's/"rlib"/"cdylib","rlib"/g' Cargo.toml - export CPPFLAGS="-DMDB_USE_ROBUST=0" && export CFLAGS="-DMDB_USE_ROBUST=0" && cargo ndk -t armeabi-v7a build --profile release-apk - - name: Build lib ARMv7 2/2 - run: | - unset CPPFLAGS && unset CFLAGS && cargo ndk -t armeabi-v7a -o android/app/src/main/jniLibs build --profile release-apk - sed -i -e 's/"cdylib","rlib"/"rlib"/g' Cargo.toml - - name: Build APK ARM - working-directory: android - run: | - rm -rf app/build - ./gradlew assembleRelease - mv app/build/outputs/apk/release/app-release.apk grim-${{ github.ref_name }}-android.apk - rm -rf app/src/main/jniLibs/* - - name: Checksum APK ARM - working-directory: android - shell: pwsh - run: get-filehash -algorithm sha256 grim-${{ github.ref_name }}-android.apk | Format-List | Out-String | ForEach-Object { $_.Trim() } > grim-${{ github.ref_name }}-android-sha256sum.txt - - name: Build lib x86 1/2 - continue-on-error: true - run: | - sed -i -e 's/"rlib"/"cdylib","rlib"/g' Cargo.toml - export CPPFLAGS="-DMDB_USE_ROBUST=0" && export CFLAGS="-DMDB_USE_ROBUST=0" && cargo ndk -t x86_64 build --profile release-apk - - name: Build lib x86 2/2 - run: | - unset CPPFLAGS && unset CFLAGS && cargo ndk -t x86_64 -o android/app/src/main/jniLibs build --profile release-apk - sed -i -e 's/"cdylib","rlib"/"rlib"/g' Cargo.toml - - name: Build APK x86 - working-directory: android - run: | - rm -rf app/build - ./gradlew assembleRelease - mv app/build/outputs/apk/release/app-release.apk grim-${{ github.ref_name }}-android-x86_64.apk - - name: Checksum APK x86 - working-directory: android - shell: pwsh - run: get-filehash -algorithm sha256 grim-${{ github.ref_name }}-android-x86_64.apk | Format-List | Out-String | ForEach-Object { $_.Trim() } > grim-${{ github.ref_name }}-android-x86_64-sha256sum.txt - - name: Release - uses: softprops/action-gh-release@v1 - with: - files: | - android/grim-${{ github.ref_name }}-android.apk - android/grim-${{ github.ref_name }}-android-sha256sum.txt - android/grim-${{ github.ref_name }}-android-x86_64.apk - android/grim-${{ github.ref_name }}-android-x86_64-sha256sum.txt - linux_release: name: Linux Release runs-on: ubuntu-latest @@ -121,7 +38,7 @@ jobs: - name: Checksum AppImage x86 working-directory: target/x86_64-unknown-linux-gnu/release shell: pwsh - run: get-filehash -algorithm sha256 grim-${{ github.ref_name }}-linux-x86_64.AppImage | Format-List | Out-String | ForEach-Object { $_.Trim() } > grim-${{ github.ref_name }}-linux-x86_64-appimage-sha256sum.txt + run: sha256sum grim-${{ github.ref_name }}-linux-x86_64.AppImage > grim-${{ github.ref_name }}-linux-x86_64-appimage-sha256sum.txt - name: AppImage ARM run: | cp target/aarch64-unknown-linux-gnu/release/grim linux/Grim.AppDir/AppRun @@ -129,7 +46,7 @@ jobs: - name: Checksum AppImage ARM working-directory: target/aarch64-unknown-linux-gnu/release shell: pwsh - run: get-filehash -algorithm sha256 grim-${{ github.ref_name }}-linux-arm.AppImage | Format-List | Out-String | ForEach-Object { $_.Trim() } > grim-${{ github.ref_name }}-linux-arm-appimage-sha256sum.txt + run: sha256sum grim-${{ github.ref_name }}-linux-arm.AppImage > grim-${{ github.ref_name }}-linux-arm-appimage-sha256sum.txt - name: Release uses: softprops/action-gh-release@v1 with: @@ -157,7 +74,7 @@ jobs: - name: Checksum release working-directory: target/release shell: pwsh - run: get-filehash -algorithm sha256 grim-${{ github.ref_name }}-win-x86_64.zip | Format-List | Out-String | ForEach-Object { $_.Trim() } > grim-${{ github.ref_name }}-win-x86_64-sha256sum.txt + run: sha256sum grim-${{ github.ref_name }}-win-x86_64.zip > grim-${{ github.ref_name }}-win-x86_64-sha256sum.txt - name: Install cargo-wix run: cargo install cargo-wix - name: Run cargo-wix @@ -165,7 +82,7 @@ jobs: - name: Checksum msi working-directory: target/wix shell: pwsh - run: get-filehash -algorithm sha256 grim-${{ github.ref_name }}-win-x86_64.msi | Format-List | Out-String | ForEach-Object { $_.Trim() } > grim-${{ github.ref_name }}-win-x86_64-msi-sha256sum.txt + run: sha256sum grim-${{ github.ref_name }}-win-x86_64.msi > grim-${{ github.ref_name }}-win-x86_64-msi-sha256sum.txt - name: Release uses: softprops/action-gh-release@v1 with: @@ -204,7 +121,7 @@ jobs: - name: Checksum Release x86 working-directory: target/x86_64-apple-darwin/release shell: pwsh - run: get-filehash -algorithm sha256 grim-${{ github.ref_name }}-macos-x86_64.zip | Format-List | Out-String | ForEach-Object { $_.Trim() } > grim-${{ github.ref_name }}-macos-x86_64-sha256sum.txt + run: sha256sum grim-${{ github.ref_name }}-macos-x86_64.zip > grim-${{ github.ref_name }}-macos-x86_64-sha256sum.txt - name: Release ARM run: | rustup target add aarch64-apple-darwin @@ -219,7 +136,7 @@ jobs: - name: Checksum Release ARM working-directory: target/aarch64-apple-darwin/release shell: pwsh - run: get-filehash -algorithm sha256 grim-${{ github.ref_name }}-macos-arm.zip | Format-List | Out-String | ForEach-Object { $_.Trim() } > grim-${{ github.ref_name }}-macos-arm-sha256sum.txt + run: sha256sum grim-${{ github.ref_name }}-macos-arm.zip > grim-${{ github.ref_name }}-macos-arm-sha256sum.txt - name: Release Universal run: | rustup target add aarch64-apple-darwin @@ -235,7 +152,7 @@ jobs: - name: Checksum Release Universal working-directory: target/universal2-apple-darwin/release shell: pwsh - run: get-filehash -algorithm sha256 grim-${{ github.ref_name }}-macos-universal.zip | Format-List | Out-String | ForEach-Object { $_.Trim() } > grim-${{ github.ref_name }}-macos-universal-sha256sum.txt + run: sha256sum grim-${{ github.ref_name }}-macos-universal.zip > grim-${{ github.ref_name }}-macos-universal-sha256sum.txt - name: Release uses: softprops/action-gh-release@v1 with: diff --git a/.gitignore b/.gitignore index af26ef6..ea10035 100644 --- a/.gitignore +++ b/.gitignore @@ -13,7 +13,6 @@ android/keystore.properties target .cargo/ app/src/main/jniLibs -macos/Grim.app/Contents/MacOS/grim macos/cert.pem linux/Grim.AppDir/AppRun .intentionally-empty-file.o \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 0134201..7e53eb3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2483,6 +2483,12 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0688c2a7f92e427f44895cd63841bff7b29f8d7a1648b9e7e07a4a365b2e1257" +[[package]] +name = "doctest-file" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac81fa3e28d21450aa4d2ac065992ba96a1d7303efbce51a95f4fd175b67562" + [[package]] name = "document-features" version = "0.2.8" @@ -3833,6 +3839,7 @@ dependencies = [ "hyper 0.14.29", "hyper-tls 0.5.0", "image 0.25.1", + "interprocess", "jni", "lazy_static", "local-ip-address", @@ -4976,6 +4983,21 @@ dependencies = [ "syn 2.0.66", ] +[[package]] +name = "interprocess" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f4e4a06d42fab3e85ab1b419ad32b09eab58b901d40c57935ff92db3287a13" +dependencies = [ + "doctest-file", + "futures-core", + "libc", + "recvmsg", + "tokio 1.38.0", + "widestring", + "windows-sys 0.52.0", +] + [[package]] name = "intl-memoizer" version = "0.5.2" @@ -7432,6 +7454,12 @@ dependencies = [ "rand_core 0.3.1", ] +[[package]] +name = "recvmsg" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3edd4d5d42c92f0a659926464d4cce56b562761267ecf0f469d85b7de384175" + [[package]] name = "redox_syscall" version = "0.1.57" diff --git a/Cargo.toml b/Cargo.toml index e2ff09d..87fe238 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,9 +16,6 @@ path = "src/main.rs" name="grim" crate-type = ["rlib"] -[profile.release] -debug = 1 - [profile.release-apk] inherits = "release" strip = true @@ -119,6 +116,7 @@ eframe = { version = "0.28.1", features = ["wgpu", "glow"] } arboard = "3.2.0" rfd = "0.14.1" dark-light = "1.1.1" +interprocess = { version = "2.2.1", features = ["tokio"] } [target.'cfg(target_os = "android")'.dependencies] android_logger = "0.13.1" diff --git a/android/app/build.gradle b/android/app/build.gradle index 98a54b8..71d2129 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -2,10 +2,6 @@ plugins { id 'com.android.application' } -def keystorePropertiesFile = rootProject.file("keystore.properties") -def keystoreProperties = new Properties() -keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) - android { compileSdk 33 ndkVersion '26.0.10792818' @@ -18,20 +14,32 @@ android { versionName "0.1.3" } - signingConfigs { - release { - keyAlias keystoreProperties['keyAlias'] - keyPassword keystoreProperties['keyPassword'] - storeFile file(keystoreProperties['storeFile']) - storePassword keystoreProperties['storePassword'] + def keystorePropertiesFile = rootProject.file("keystore.properties") + def keystoreProperties = new Properties() + if (keystorePropertiesFile.exists()) { + keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) + + signingConfigs { + release { + keyAlias keystoreProperties['keyAlias'] + keyPassword keystoreProperties['keyPassword'] + storeFile file(keystoreProperties['storeFile']) + storePassword keystoreProperties['storePassword'] + } } + } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' - signingConfig signingConfigs.release + } + if (keystorePropertiesFile.exists()) { + signedRelease { + initWith release + signingConfig signingConfigs.release + } } debug { minifyEnabled false diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 04e6d7b..39aadbd 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,15 +1,17 @@ - - + - + + + @@ -44,6 +45,22 @@ + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/java/mw/gri/android/BackgroundService.java b/android/app/src/main/java/mw/gri/android/BackgroundService.java index f34b700..bb668a5 100644 --- a/android/app/src/main/java/mw/gri/android/BackgroundService.java +++ b/android/app/src/main/java/mw/gri/android/BackgroundService.java @@ -152,13 +152,17 @@ public class BackgroundService extends Service { // Show notification with sync status. Intent i = getPackageManager().getLaunchIntentForPackage(this.getPackageName()); PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, i, PendingIntent.FLAG_IMMUTABLE); - mNotificationBuilder = new NotificationCompat.Builder(this, TAG) - .setContentTitle(this.getSyncTitle()) - .setContentText(this.getSyncStatusText()) - .setStyle(new NotificationCompat.BigTextStyle().bigText(this.getSyncStatusText())) - .setSmallIcon(R.drawable.ic_stat_name) - .setPriority(NotificationCompat.PRIORITY_MAX) - .setContentIntent(pendingIntent); + try { + mNotificationBuilder = new NotificationCompat.Builder(this, TAG) + .setContentTitle(this.getSyncTitle()) + .setContentText(this.getSyncStatusText()) + .setStyle(new NotificationCompat.BigTextStyle().bigText(this.getSyncStatusText())) + .setSmallIcon(R.drawable.ic_stat_name) + .setPriority(NotificationCompat.PRIORITY_MAX) + .setContentIntent(pendingIntent); + } catch (UnsatisfiedLinkError e) { + return; + } Notification notification = mNotificationBuilder.build(); // Start service at foreground state to prevent killing by system. diff --git a/android/app/src/main/java/mw/gri/android/MainActivity.java b/android/app/src/main/java/mw/gri/android/MainActivity.java index 409d23c..a589934 100644 --- a/android/app/src/main/java/mw/gri/android/MainActivity.java +++ b/android/app/src/main/java/mw/gri/android/MainActivity.java @@ -7,9 +7,9 @@ import android.content.*; import android.content.pm.PackageManager; import android.content.res.Configuration; import android.net.Uri; -import android.os.Build; -import android.os.Bundle; +import android.os.*; import android.os.Process; +import android.provider.Settings; import android.system.ErrnoException; import android.system.Os; import android.util.Size; @@ -51,8 +51,7 @@ public class MainActivity extends GameActivity { @Override public void onReceive(Context ctx, Intent i) { if (i.getAction().equals(STOP_APP_ACTION)) { - onExit(); - Process.killProcess(Process.myPid()); + exit(); } } }; @@ -67,11 +66,19 @@ public class MainActivity extends GameActivity { private ExecutorService mCameraExecutor = null; private boolean mUseBackCamera = true; - private ActivityResultLauncher mFilePickResultLauncher = null; + private ActivityResultLauncher mFilePickResult = null; + private ActivityResultLauncher mOpenFilePermissionsResult = null; @SuppressLint("UnspecifiedRegisterReceiverFlag") @Override protected void onCreate(Bundle savedInstanceState) { + // Check if activity was launched to exclude from recent apps on exit. + if ((getIntent().getFlags() & Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS) != 0) { + super.onCreate(null); + finish(); + return; + } + // Clear cache on start. if (savedInstanceState == null) { Utils.deleteDirectoryContent(new File(getExternalCacheDir().getPath()), false); @@ -91,8 +98,21 @@ public class MainActivity extends GameActivity { // Register receiver to finish activity from the BackgroundService. registerReceiver(mBroadcastReceiver, new IntentFilter(STOP_APP_ACTION)); - // Register file pick result launcher. - mFilePickResultLauncher = registerForActivityResult( + // Register associated file opening result. + mOpenFilePermissionsResult = registerForActivityResult( + new ActivityResultContracts.StartActivityForResult(), + result -> { + if (Build.VERSION.SDK_INT >= 30) { + if (Environment.isExternalStorageManager()) { + onFile(); + } + } else if (result.getResultCode() == RESULT_OK) { + onFile(); + } + } + ); + // Register file pick result. + mFilePickResult = registerForActivityResult( new ActivityResultContracts.StartActivityForResult(), result -> { int resultCode = result.getResultCode(); @@ -105,11 +125,11 @@ public class MainActivity extends GameActivity { File file = new File(getExternalCacheDir(), name); try (InputStream is = getContentResolver().openInputStream(uri); OutputStream os = new FileOutputStream(file)) { - byte[] buffer = new byte[1024]; - int length; - while ((length = is.read(buffer)) > 0) { - os.write(buffer, 0, length); - } + byte[] buffer = new byte[1024]; + int length; + while ((length = is.read(buffer)) > 0) { + os.write(buffer, 0, length); + } } catch (Exception e) { e.printStackTrace(); } @@ -124,7 +144,7 @@ public class MainActivity extends GameActivity { // Listener for display insets (cutouts) to pass values into native code. View content = getWindow().getDecorView().findViewById(android.R.id.content); ViewCompat.setOnApplyWindowInsetsListener(content, (v, insets) -> { - // Setup cutouts values. + // Get display cutouts. DisplayCutoutCompat dc = insets.getDisplayCutout(); int cutoutTop = 0; int cutoutRight = 0; @@ -140,7 +160,7 @@ public class MainActivity extends GameActivity { // Get display insets. Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()); - // Setup values to pass into native code. + // Pass values into native code. int[] values = new int[]{0, 0, 0, 0}; values[0] = Utils.pxToDp(Integer.max(cutoutTop, systemBars.top), this); values[1] = Utils.pxToDp(Integer.max(cutoutRight, systemBars.right), this); @@ -166,8 +186,61 @@ public class MainActivity extends GameActivity { BackgroundService.start(this); } }); + + // Check if intent has data on launch. + if (savedInstanceState == null) { + onNewIntent(getIntent()); + } } + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + String action = intent.getAction(); + // Check if file was open with the application. + if (action != null && action.equals(Intent.ACTION_VIEW)) { + Intent i = getIntent(); + i.setData(intent.getData()); + setIntent(i); + onFile(); + } + } + + // Callback when associated file was open. + private void onFile() { + Uri data = getIntent().getData(); + if (data == null) { + return; + } + if (Build.VERSION.SDK_INT >= 30) { + if (!Environment.isExternalStorageManager()) { + Intent i = new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION); + mOpenFilePermissionsResult.launch(i); + return; + } + } + try { + ParcelFileDescriptor parcelFile = getContentResolver().openFileDescriptor(data, "r"); + FileReader fileReader = new FileReader(parcelFile.getFileDescriptor()); + BufferedReader reader = new BufferedReader(fileReader); + String line; + StringBuilder buff = new StringBuilder(); + while ((line = reader.readLine()) != null) { + buff.append(line); + } + reader.close(); + fileReader.close(); + + // Provide file content into native code. + onData(buff.toString()); + } catch (Exception e) { + e.printStackTrace(); + } + } + + // Pass data into native code. + public native void onData(String data); + @Override public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); @@ -232,17 +305,17 @@ public class MainActivity extends GameActivity { // Implemented into native code to handle key code BACK event. public native void onBack(); - // Actions on app exit. - private void onExit() { - unregisterReceiver(mBroadcastReceiver); - BackgroundService.stop(this); + // Called from native code to exit app. + public void exit() { + finishAndRemoveTask(); } @Override protected void onDestroy() { - onExit(); + unregisterReceiver(mBroadcastReceiver); + BackgroundService.stop(this); - // Kill process after 3 seconds if app was terminated from recent apps to prevent app hanging. + // Kill process after 3 secs if app was terminated from recent apps to prevent app hang. new Thread(() -> { try { onTermination(); @@ -253,9 +326,7 @@ public class MainActivity extends GameActivity { } }).start(); - // Destroy an app and kill process. super.onDestroy(); - Process.killProcess(Process.myPid()); } // Notify native code to stop activity (e.g. node) if app was terminated from recent apps. @@ -298,18 +369,16 @@ public class MainActivity extends GameActivity { // Called from native code to start camera. public void startCamera() { - // Check permissions. String notificationsPermission = Manifest.permission.CAMERA; if (checkSelfPermission(notificationsPermission) != PackageManager.PERMISSION_GRANTED) { requestPermissions(new String[] { notificationsPermission }, CAMERA_PERMISSION_CODE); } else { - // Start . if (mCameraProviderFuture == null) { mCameraProviderFuture = ProcessCameraProvider.getInstance(this); mCameraProviderFuture.addListener(() -> { try { mCameraProvider = mCameraProviderFuture.get(); - // Launch camera. + // Start camera. openCamera(); } catch (Exception e) { View content = findViewById(android.R.id.content); @@ -402,7 +471,7 @@ public class MainActivity extends GameActivity { Intent intent = new Intent(Intent.ACTION_GET_CONTENT); intent.setType("*/*"); try { - mFilePickResultLauncher.launch(Intent.createChooser(intent, "Pick file")); + mFilePickResult.launch(Intent.createChooser(intent, "Pick file")); } catch (android.content.ActivityNotFoundException ex) { onFilePick(""); } diff --git a/linux/Grim.AppDir/grim.desktop b/linux/Grim.AppDir/grim.desktop index 907cf68..781f500 100644 --- a/linux/Grim.AppDir/grim.desktop +++ b/linux/Grim.AppDir/grim.desktop @@ -3,4 +3,5 @@ Name=Grim Exec=grim Icon=grim Type=Application -Categories=Finance \ No newline at end of file +Categories=Finance +MimeType=application/x-slatepack;text/plain; \ No newline at end of file diff --git a/linux/build_release.sh b/linux/build_release.sh index b669567..f1042b4 100755 --- a/linux/build_release.sh +++ b/linux/build_release.sh @@ -17,9 +17,7 @@ cd .. [[ $2 == "x86_64" ]] && arch+=(x86_64-unknown-linux-gnu) [[ $2 == "arm" ]] && arch+=(aarch64-unknown-linux-gnu) -# Start release build with zig linker for cross-compilation -cargo install cargo-zigbuild -cargo zigbuild --release --target ${arch} +cargo build --release --target ${arch} # Create AppImage with https://github.com/AppImage/appimagetool cp target/${arch}/release/grim linux/Grim.AppDir/AppRun diff --git a/macos/Grim.app/Contents/Info.plist b/macos/Grim.app/Contents/Info.plist index c56394a..669d7aa 100644 --- a/macos/Grim.app/Contents/Info.plist +++ b/macos/Grim.app/Contents/Info.plist @@ -40,6 +40,34 @@ CFBundleVersion 1 + CFBundleDocumentTypes + + + CFBundleTypeName + Apple SimpleText document + CFBundleTypeRole + Viewer + LSItemContentTypes + + com.apple.traditional-mac-plain-text + + NSDocumentClass + Document + + + CFBundleTypeName + Unknown document + CFBundleTypeRole + Viewer + LSItemContentTypes + + public.data + + NSDocumentClass + Document + + + LSApplicationCategoryType public.app-category.finance diff --git a/macos/Grim.app/Contents/MacOS/.gitignore b/macos/Grim.app/Contents/MacOS/.gitignore new file mode 100644 index 0000000..b722e9e --- /dev/null +++ b/macos/Grim.app/Contents/MacOS/.gitignore @@ -0,0 +1 @@ +!.gitignore \ No newline at end of file diff --git a/macos/build_release.sh b/macos/build_release.sh index f533246..5f50d35 100755 --- a/macos/build_release.sh +++ b/macos/build_release.sh @@ -9,14 +9,13 @@ case $2 in exit 1 esac -if [[ ! -v SDKROOT ]]; then +if [[ "$OSTYPE" != "darwin"* ]]; then + if [ -z ${SDKROOT+x} ]; then echo "MacOS SDKROOT is not set" exit 1 -elif [[ -z "SDKROOT" ]]; then - echo "MacOS SDKROOT is set to the empty string" - exit 1 -else + else echo "Use MacOS SDK: ${SDKROOT}" + fi fi # Setup build directory @@ -28,22 +27,18 @@ cd .. rustup target add x86_64-apple-darwin rustup target add aarch64-apple-darwin -rm -rf target/x86_64-apple-darwin -rm -rf target/aarch64-apple-darwin - [[ $2 == "x86_64" ]] && arch+=(x86_64-apple-darwin) [[ $2 == "arm" ]] && arch+=(aarch64-apple-darwin) -[[ $2 == "universal" ]] && arch+=(universal2-apple-darwin) +[[ $2 == "universal" ]]; arch+=(universal2-apple-darwin) -# Start release build with zig linker for cross-compilation -# zig 0.12+ required +# Start release build with zig linker, requires zig 0.12.1 cargo install cargo-zigbuild cargo zigbuild --release --target ${arch} rm -rf .intentionally-empty-file.o -mkdir macos/Grim.app/Contents/MacOS + yes | cp -rf target/${arch}/release/grim macos/Grim.app/Contents/MacOS -### Sign .app resources on change: +# Sign .app resources on change: #rcodesign generate-self-signed-certificate #rcodesign sign --pem-file cert.pem macos/Grim.app diff --git a/scripts/android.sh b/scripts/android.sh index 6366791..4446447 100755 --- a/scripts/android.sh +++ b/scripts/android.sh @@ -1,81 +1,113 @@ #!/bin/bash -usage="Usage: build_run_android.sh [type] [platform]\n - type: 'debug', 'release'\n - platform: 'v7', 'v8'" +usage="Usage: android.sh [type] [platform]\n - type: 'build', 'release', ''\n - platform, for build type: 'v7', 'v8', 'x86'" case $1 in - debug|release) + build|release) ;; *) printf "$usage" exit 1 esac -case $2 in - v7|v8) - ;; - *) - printf "$usage" - exit 1 -esac +if [[ $1 == "build" ]]; then + case $2 in + v7|v8|x86) + ;; + *) + printf "$usage" + exit 1 + esac +fi # Setup build directory BASEDIR=$(cd $(dirname $0) && pwd) cd ${BASEDIR} cd .. -# Setup release argument -type=$1 -[[ ${type} == "release" ]] && release_param="--profile release-apk" - -# Setup platform argument -[[ $2 == "v7" ]] && arch+=(armeabi-v7a) -[[ $2 == "v8" ]] && arch+=(arm64-v8a) - -# Setup platform path -[[ $2 == "v7" ]] && platform+=(armv7-linux-androideabi) -[[ $2 == "v8" ]] && platform+=(aarch64-linux-android) - -# Install platform -[[ $2 == "v7" ]] && rustup target install armv7-linux-androideabi -[[ $2 == "v8" ]] && rustup target install aarch64-linux-android - -# Build native code +# Install platforms and tools +rustup target add armv7-linux-androideabi +rustup target add aarch64-linux-android +rustup target add x86_64-linux-android cargo install cargo-ndk -sed -i -e 's/"rlib"/"cdylib","rlib"/g' Cargo.toml +success=1 -# temp fix for https://stackoverflow.com/questions/57193895/error-use-of-undeclared-identifier-pthread-mutex-robust-cargo-build-liblmdb-s -success=0 -export CPPFLAGS="-DMDB_USE_ROBUST=0" && export CFLAGS="-DMDB_USE_ROBUST=0" -cargo ndk -t ${arch} build ${release_param} -unset CPPFLAGS && unset CFLAGS -cargo ndk -t ${arch} -o android/app/src/main/jniLibs build ${release_param} -if [ $? -eq 0 ] -then - success=1 -fi +### Build native code +function build_lib() { + [[ $1 == "v7" ]] && arch=(armeabi-v7a) + [[ $1 == "v8" ]] && arch=(arm64-v8a) + [[ $1 == "x86" ]] && arch=(x86_64) -sed -i -e 's/"cdylib","rlib"/"rlib"/g' Cargo.toml + sed -i -e 's/"rlib"/"cdylib","rlib"/g' Cargo.toml + + # Fix for https://stackoverflow.com/questions/57193895/error-use-of-undeclared-identifier-pthread-mutex-robust-cargo-build-liblmdb-s + export CPPFLAGS="-DMDB_USE_ROBUST=0" && export CFLAGS="-DMDB_USE_ROBUST=0" + cargo ndk -t ${arch} build --profile release-apk + unset CPPFLAGS && unset CFLAGS + cargo ndk -t ${arch} -o android/app/src/main/jniLibs build --profile release-apk + if [ $? -eq 0 ] + then + success=1 + fi + + sed -i -e 's/"cdylib","rlib"/"rlib"/g' Cargo.toml +} + +### Build application +function build_apk() { + version=$(grep -m 1 -Po 'version = "\K[^"]*' Cargo.toml) -# Build Android application and launch at all connected devices -if [ $success -eq 1 ] -then cd android - - # Setup gradle argument - [[ $1 == "release" ]] && gradle_param+=(assembleRelease) - [[ $1 == "debug" ]] && gradle_param+=(build) - ./gradlew clean - ./gradlew ${gradle_param} + # Build signed apk if keystore exists + if [ ! -f keystore.properties ]; then + ./gradlew assembleRelease + apk_path=app/build/outputs/apk/release/app-release.apk + else + ./gradlew assembleSignedRelease + apk_path=app/build/outputs/apk/signedRelease/app-signedRelease.apk + fi - # Setup apk path - [[ $1 == "release" ]] && apk_path+=(app/build/outputs/apk/release/app-release.apk) - [[ $1 == "debug" ]] && apk_path+=(app/build/outputs/apk/debug/app-debug.apk) + if [[ $1 == "" ]]; then + # Launch application at all connected devices. + for SERIAL in $(adb devices | grep -v List | cut -f 1); + do + adb -s $SERIAL install ${apk_path} + sleep 1s + adb -s $SERIAL shell am start -n mw.gri.android/.MainActivity; + done + else + # Setup release file name + name=grim-${version}-android-$1.apk + [[ $1 == "arm" ]] && name=grim-${version}-android.apk + rm -rf ${name} + mv ${apk_path} ${name} - for SERIAL in $(adb devices | grep -v List | cut -f 1); - do - adb -s $SERIAL install ${apk_path} - sleep 1s - adb -s $SERIAL shell am start -n mw.gri.android/.MainActivity; - done + # Calculate checksum + checksum=grim-${version}-android-$1-sha256sum.txt + [[ $1 == "arm" ]] && checksum=grim-${version}-android-sha256sum.txt + rm -rf ${checksum} + sha256sum ${name} > ${checksum} + fi + + cd .. +} + +rm -rf android/app/src/main/jniLibs/* + +if [[ $1 == "build" ]]; then + build_lib $2 + [ $success -eq 1 ] && build_apk +else + rm -rf target/release-apk + rm -rf target/aarch64-linux-android + rm -rf target/x86_64-linux-android + rm -rf target/armv7-linux-androideabi + + build_lib "v7" + [ $success -eq 1 ] && build_lib "v8" + [ $success -eq 1 ] && build_apk "arm" + rm -rf android/app/src/main/jniLibs/* + [ $success -eq 1 ] && build_lib "x86" + [ $success -eq 1 ] && build_apk "x86_64" fi \ No newline at end of file diff --git a/src/gui/app.rs b/src/gui/app.rs index 17fe021..f95176a 100644 --- a/src/gui/app.rs +++ b/src/gui/app.rs @@ -32,25 +32,41 @@ lazy_static! { /// Implements ui entry point and contains platform-specific callbacks. pub struct App { /// Platform specific callbacks handler. - pub(crate) platform: Platform, + pub platform: Platform, - /// Main ui content. + /// Main content. content: Content, /// Last window resize direction. - resize_direction: Option + resize_direction: Option, + + /// Flag to check if it's first draw. + first_draw: bool, } impl App { pub fn new(platform: Platform) -> Self { - Self { platform, content: Content::default(), resize_direction: None } + Self { + platform, + content: Content::default(), + resize_direction: None, + first_draw: true + } } /// Draw application content. pub fn ui(&mut self, ctx: &Context) { + // 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. let back_pressed = BACK_BUTTON_PRESSED.load(Ordering::Relaxed); - if ctx.input_mut(|i| i.consume_key(Modifiers::NONE, egui::Key::Escape)) || back_pressed { + if back_pressed || ctx.input_mut(|i| i.consume_key(Modifiers::NONE, egui::Key::Escape)) { self.content.on_back(); if back_pressed { BACK_BUTTON_PRESSED.store(false, Ordering::Relaxed); @@ -59,8 +75,8 @@ impl App { ctx.request_repaint(); } - // Handle Close event (on desktop). - if ctx.input(|i| i.viewport().close_requested()) { + // Handle Close event on desktop. + if View::is_desktop() && ctx.input(|i| i.viewport().close_requested()) { if !self.content.exit_allowed { ctx.send_viewport_cmd(ViewportCommand::CancelClose); Content::show_exit_modal(); @@ -92,7 +108,20 @@ impl App { } 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. diff --git a/src/gui/colors.rs b/src/gui/colors.rs index 109725e..0799e02 100644 --- a/src/gui/colors.rs +++ b/src/gui/colors.rs @@ -31,10 +31,14 @@ const YELLOW: Color32 = Color32::from_rgb(254, 241, 2); const YELLOW_DARK: Color32 = Color32::from_rgb(239, 229, 3); const GREEN: Color32 = Color32::from_rgb(0, 0x64, 0); +const GREEN_DARK: Color32 = Color32::from_rgb(0, (0x64 as f32 * 1.3 + 0.5) as u8, 0); const RED: Color32 = Color32::from_rgb(0x8B, 0, 0); +const RED_DARK: Color32 = Color32::from_rgb((0x8B as f32 * 1.3 + 0.5) as u8, 0, 0); const BLUE: Color32 = Color32::from_rgb(0, 0x66, 0xE4); +const BLUE_DARK: Color32 = + Color32::from_rgb(0, (0x66 as f32 * 1.3 + 0.5) as u8, (0xE4 as f32 * 1.3 + 0.5) as u8); const FILL: Color32 = Color32::from_gray(244); const FILL_DARK: Color32 = Color32::from_gray(24); @@ -125,7 +129,7 @@ impl Colors { pub fn green() -> Color32 { if use_dark() { - GREEN.gamma_multiply(1.3) + GREEN_DARK } else { GREEN } @@ -133,7 +137,7 @@ impl Colors { pub fn red() -> Color32 { if use_dark() { - RED.gamma_multiply(1.3) + RED_DARK } else { RED } @@ -141,7 +145,7 @@ impl Colors { pub fn blue() -> Color32 { if use_dark() { - BLUE.gamma_multiply(1.3) + BLUE_DARK } else { BLUE } diff --git a/src/gui/platform/android/mod.rs b/src/gui/platform/android/mod.rs index bb077f7..01565b9 100644 --- a/src/gui/platform/android/mod.rs +++ b/src/gui/platform/android/mod.rs @@ -30,7 +30,11 @@ use crate::gui::platform::PlatformCallbacks; /// Android platform implementation. #[derive(Clone)] pub struct Android { + /// Android related state. android_app: AndroidApp, + + /// Context to repaint content and handle viewport commands. + ctx: Arc>>, } impl Android { @@ -38,6 +42,7 @@ impl Android { pub fn new(app: AndroidApp) -> Self { Self { android_app: app, + ctx: Arc::new(RwLock::new(None)), } } @@ -56,27 +61,36 @@ impl Android { } impl PlatformCallbacks for Android { + fn set_context(&mut self, ctx: &egui::Context) { + let mut w_ctx = self.ctx.write(); + *w_ctx = Some(ctx.clone()); + } + + fn exit(&self) { + let _ = self.call_java_method("exit", "()V", &[]); + } + fn show_keyboard(&self) { // Disable NDK soft input show call before fix for egui. // self.android_app.show_soft_input(false); - self.call_java_method("showKeyboard", "()V", &[]).unwrap(); + let _ = self.call_java_method("showKeyboard", "()V", &[]); } fn hide_keyboard(&self) { // Disable NDK soft input hide call before fix for egui. // self.android_app.hide_soft_input(false); - self.call_java_method("hideKeyboard", "()V", &[]).unwrap(); + let _ = self.call_java_method("hideKeyboard", "()V", &[]); } fn copy_string_to_buffer(&self, data: String) { let vm = unsafe { jni::JavaVM::from_raw(self.android_app.vm_as_ptr() as _) }.unwrap(); let env = vm.attach_current_thread().unwrap(); let arg_value = env.new_string(data).unwrap(); - self.call_java_method("copyText", - "(Ljava/lang/String;)V", - &[JValue::Object(&JObject::from(arg_value))]).unwrap(); + let _ = self.call_java_method("copyText", + "(Ljava/lang/String;)V", + &[JValue::Object(&JObject::from(arg_value))]); } fn get_string_from_buffer(&self) -> String { @@ -95,12 +109,12 @@ impl PlatformCallbacks for Android { let mut w_image = LAST_CAMERA_IMAGE.write(); *w_image = None; // Start camera. - self.call_java_method("startCamera", "()V", &[]).unwrap(); + let _ = self.call_java_method("startCamera", "()V", &[]); } fn stop_camera(&self) { // Stop camera. - self.call_java_method("stopCamera", "()V", &[]).unwrap(); + let _ = self.call_java_method("stopCamera", "()V", &[]); // Clear image. let mut w_image = LAST_CAMERA_IMAGE.write(); *w_image = None; @@ -115,32 +129,39 @@ impl PlatformCallbacks for Android { } fn can_switch_camera(&self) -> bool { - let result = self.call_java_method("camerasAmount", "()I", &[]).unwrap(); - let amount = unsafe { result.i }; - amount > 1 + if let Some(res) = self.call_java_method("camerasAmount", "()I", &[]) { + let amount = unsafe { res.i }; + return amount > 1; + } + false } fn switch_camera(&self) { - self.call_java_method("switchCamera", "()V", &[]).unwrap(); + let _ = self.call_java_method("switchCamera", "()V", &[]); } fn share_data(&self, name: String, data: Vec) -> Result<(), std::io::Error> { - // Create file at cache dir. let default_cache = OsString::from(dirs::cache_dir().unwrap()); - let mut cache = PathBuf::from(env::var_os("XDG_CACHE_HOME").unwrap_or(default_cache)); - cache.push("images"); - std::fs::create_dir_all(cache.to_str().unwrap())?; - cache.push(name); - let mut image = File::create_new(cache.clone()).unwrap(); - image.write_all(data.as_slice()).unwrap(); - image.sync_all().unwrap(); + let mut file = PathBuf::from(env::var_os("XDG_CACHE_HOME").unwrap_or(default_cache)); + // File path for Android provider. + file.push("images"); + if !file.exists() { + std::fs::create_dir(file.clone())?; + } + file.push(name); + if file.exists() { + std::fs::remove_file(file.clone())?; + } + let mut image = File::create_new(file.clone())?; + image.write_all(data.as_slice())?; + image.sync_all()?; // Call share modal at system. let vm = unsafe { jni::JavaVM::from_raw(self.android_app.vm_as_ptr() as _) }.unwrap(); let env = vm.attach_current_thread().unwrap(); - let arg_value = env.new_string(cache.to_str().unwrap()).unwrap(); - self.call_java_method("shareImage", - "(Ljava/lang/String;)V", - &[JValue::Object(&JObject::from(arg_value))]).unwrap(); + let arg_value = env.new_string(file.to_str().unwrap()).unwrap(); + let _ = self.call_java_method("shareImage", + "(Ljava/lang/String;)V", + &[JValue::Object(&JObject::from(arg_value))]); Ok(()) } @@ -149,7 +170,7 @@ impl PlatformCallbacks for Android { let mut w_path = PICKED_FILE_PATH.write(); *w_path = None; // Launch file picker. - let _ = self.call_java_method("pickFile", "()V", &[]).unwrap(); + let _ = self.call_java_method("pickFile", "()V", &[]); // Return empty string to identify async pick. Some("".to_string()) } @@ -167,6 +188,14 @@ impl PlatformCallbacks for Android { } None } + + fn request_user_attention(&self) {} + + fn user_attention_required(&self) -> bool { + false + } + + fn clear_user_attention(&self) {} } lazy_static! { diff --git a/src/gui/platform/desktop/mod.rs b/src/gui/platform/desktop/mod.rs index 5318bf4..5c02cd2 100644 --- a/src/gui/platform/desktop/mod.rs +++ b/src/gui/platform/desktop/mod.rs @@ -13,12 +13,13 @@ // limitations under the License. use std::fs::File; -use std::io:: Write; -use lazy_static::lazy_static; -use parking_lot::RwLock; +use std::io::Write; +use std::thread; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; -use std::thread; +use parking_lot::RwLock; +use lazy_static::lazy_static; +use egui::{UserAttentionType, ViewportCommand, WindowLevel}; use rfd::FileDialog; use crate::gui::platform::PlatformCallbacks; @@ -26,19 +27,30 @@ use crate::gui::platform::PlatformCallbacks; /// Desktop platform related actions. #[derive(Clone)] pub struct Desktop { + /// Context to repaint content and handle viewport commands. + ctx: Arc>>, + /// Flag to check if camera stop is needed. stop_camera: Arc, -} -impl Default for Desktop { - fn default() -> Self { - Self { - stop_camera: Arc::new(AtomicBool::new(false)), - } - } + /// Flag to check if attention required after window focusing. + attention_required: Arc, } impl PlatformCallbacks for Desktop { + fn set_context(&mut self, ctx: &egui::Context) { + let mut w_ctx = self.ctx.write(); + *w_ctx = Some(ctx.clone()); + } + + fn exit(&self) { + let r_ctx = self.ctx.read(); + if r_ctx.is_some() { + let ctx = r_ctx.as_ref().unwrap(); + ctx.send_viewport_cmd(egui::ViewportCommand::Close); + } + } + fn show_keyboard(&self) {} fn hide_keyboard(&self) {} @@ -119,9 +131,55 @@ impl PlatformCallbacks for Desktop { fn picked_file(&self) -> Option { None } + + fn request_user_attention(&self) { + let r_ctx = self.ctx.read(); + if r_ctx.is_some() { + let ctx = r_ctx.as_ref().unwrap(); + // Request attention on taskbar. + ctx.send_viewport_cmd( + ViewportCommand::RequestUserAttention(UserAttentionType::Informational) + ); + // Un-minimize window. + if ctx.input(|i| i.viewport().minimized.unwrap_or(false)) { + ctx.send_viewport_cmd(ViewportCommand::Minimized(false)); + } + // Focus to window. + if !ctx.input(|i| i.viewport().focused.unwrap_or(false)) { + ctx.send_viewport_cmd(ViewportCommand::WindowLevel(WindowLevel::AlwaysOnTop)); + ctx.send_viewport_cmd(ViewportCommand::Focus); + } + ctx.request_repaint(); + } + self.attention_required.store(true, Ordering::Relaxed); + } + + fn user_attention_required(&self) -> bool { + self.attention_required.load(Ordering::Relaxed) + } + + fn clear_user_attention(&self) { + let r_ctx = self.ctx.read(); + if r_ctx.is_some() { + let ctx = r_ctx.as_ref().unwrap(); + ctx.send_viewport_cmd( + ViewportCommand::RequestUserAttention(UserAttentionType::Reset) + ); + ctx.send_viewport_cmd(ViewportCommand::WindowLevel(WindowLevel::Normal)); + } + self.attention_required.store(false, Ordering::Relaxed); + } } impl Desktop { + pub fn new() -> Self { + Self { + stop_camera: Arc::new(AtomicBool::new(false)), + ctx: Arc::new(RwLock::new(None)), + attention_required: Arc::new(AtomicBool::new(false)), + } + } + #[allow(dead_code)] #[cfg(target_os = "windows")] fn start_camera_capture(stop_camera: Arc) { @@ -168,36 +226,35 @@ impl Desktop { let ctx = PlatformContext::default(); let devices = ctx.devices().unwrap(); - let dev = ctx.open_device(&devices[0].uri).unwrap(); + if let Ok(dev) = ctx.open_device(&devices[0].uri) { + let streams = dev.streams().unwrap(); + let stream_desc = streams[0].clone(); + let w = stream_desc.width; + let h = stream_desc.height; - let streams = dev.streams().unwrap(); - let stream_desc = streams[0].clone(); - let w = stream_desc.width; - let h = stream_desc.height; + let mut stream = dev.start_stream(&stream_desc).unwrap(); - let mut stream = dev.start_stream(&stream_desc).unwrap(); - - loop { - // Stop if camera was stopped. - if stop_camera.load(Ordering::Relaxed) { - stop_camera.store(false, Ordering::Relaxed); - // Clear image. + loop { + // Stop if camera was stopped. + if stop_camera.load(Ordering::Relaxed) { + stop_camera.store(false, Ordering::Relaxed); + let mut w_image = LAST_CAMERA_IMAGE.write(); + *w_image = None; + break; + } + // Get a frame. + let frame = stream.next().expect("Stream is dead").expect("Failed to capture a frame"); + let mut out = vec![]; + if let Some(buf) = image::ImageBuffer::, &[u8]>::from_raw(w, h, &frame) { + image::codecs::jpeg::JpegEncoder::new(&mut out) + .write_image(buf.as_raw(), w, h, image::ExtendedColorType::Rgb8).unwrap(); + } else { + out = frame.to_vec(); + } + // Save image. let mut w_image = LAST_CAMERA_IMAGE.write(); - *w_image = None; - break; + *w_image = Some((out, 0)); } - // Get a frame. - let frame = stream.next().expect("Stream is dead").expect("Failed to capture a frame"); - let mut out = vec![]; - if let Some(buf) = image::ImageBuffer::, &[u8]>::from_raw(w, h, &frame) { - image::codecs::jpeg::JpegEncoder::new(&mut out) - .write_image(buf.as_raw(), w, h, image::ExtendedColorType::Rgb8).unwrap(); - } else { - out = frame.to_vec(); - } - // Save image. - let mut w_image = LAST_CAMERA_IMAGE.write(); - *w_image = Some((out, 0)); } } } diff --git a/src/gui/platform/mod.rs b/src/gui/platform/mod.rs index e5bc3be..cd4ed15 100644 --- a/src/gui/platform/mod.rs +++ b/src/gui/platform/mod.rs @@ -22,6 +22,8 @@ pub mod platform; pub mod platform; pub trait PlatformCallbacks { + fn set_context(&mut self, ctx: &egui::Context); + fn exit(&self); fn show_keyboard(&self); fn hide_keyboard(&self); fn copy_string_to_buffer(&self, data: String); @@ -34,4 +36,7 @@ pub trait PlatformCallbacks { fn share_data(&self, name: String, data: Vec) -> Result<(), std::io::Error>; fn pick_file(&self) -> Option; fn picked_file(&self) -> Option; + fn request_user_attention(&self); + fn user_attention_required(&self) -> bool; + fn clear_user_attention(&self); } \ No newline at end of file diff --git a/src/gui/views/content.rs b/src/gui/views/content.rs index 8780813..2590a42 100644 --- a/src/gui/views/content.rs +++ b/src/gui/views/content.rs @@ -40,8 +40,8 @@ pub struct Content { /// Central panel [`WalletsContent`] content. pub wallets: WalletsContent, - /// Check if app exit is allowed on close event of [`eframe::App`] implementation. - pub(crate) exit_allowed: bool, + /// Check if app exit is allowed on Desktop close event. + pub exit_allowed: bool, /// Flag to show exit progress at [`Modal`]. show_exit_progress: bool, @@ -83,7 +83,7 @@ impl ModalContainer for Content { modal: &Modal, cb: &dyn PlatformCallbacks) { match modal.id { - Self::EXIT_CONFIRMATION_MODAL => self.exit_modal_content(ui, modal), + Self::EXIT_CONFIRMATION_MODAL => self.exit_modal_content(ui, modal, cb), Self::SETTINGS_MODAL => self.settings_modal_ui(ui, modal), Self::ANDROID_INTEGRATED_NODE_WARNING_MODAL => self.android_warning_modal_ui(ui, modal), Self::CRASH_REPORT_MODAL => self.crash_report_modal_ui(ui, modal, cb), @@ -206,11 +206,11 @@ impl 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 !Node::is_running() { self.exit_allowed = true; - ui.ctx().send_viewport_cmd(egui::ViewportCommand::Close); + cb.exit(); modal.close(); } ui.add_space(16.0); @@ -241,10 +241,10 @@ impl Content { }); }); columns[1].vertical_centered_justified(|ui| { - View::button_ui(ui, t!("modal_exit.exit"), Colors::white_or_black(false), |ui| { + View::button_ui(ui, t!("modal_exit.exit"), Colors::white_or_black(false), |_| { if !Node::is_running() { self.exit_allowed = true; - ui.ctx().send_viewport_cmd(egui::ViewportCommand::Close); + cb.exit(); modal.close(); } else { Node::stop(true); diff --git a/src/gui/views/file.rs b/src/gui/views/file_pick.rs similarity index 100% rename from src/gui/views/file.rs rename to src/gui/views/file_pick.rs diff --git a/src/gui/views/mod.rs b/src/gui/views/mod.rs index 03a2fbd..3be7242 100644 --- a/src/gui/views/mod.rs +++ b/src/gui/views/mod.rs @@ -36,8 +36,8 @@ pub use camera::*; mod qr; pub use qr::*; -mod file; -pub use file::*; +mod file_pick; +pub use file_pick::*; mod pull_to_refresh; pub use pull_to_refresh::*; \ No newline at end of file diff --git a/src/gui/views/modal.rs b/src/gui/views/modal.rs index 6cff50e..7b3359a 100644 --- a/src/gui/views/modal.rs +++ b/src/gui/views/modal.rs @@ -35,7 +35,7 @@ pub struct Modal { /// Identifier for modal. pub(crate) id: &'static str, /// Position on the screen. - position: ModalPosition, + pub position: ModalPosition, /// To check if it can be closed. closeable: Arc, /// Title text @@ -64,6 +64,12 @@ impl Modal { self } + /// Change [`Modal`] position on the screen. + pub fn change_position(position: ModalPosition) { + let mut w_state = MODAL_STATE.write(); + w_state.modal.as_mut().unwrap().position = position; + } + /// Mark [`Modal`] closed. pub fn close(&self) { let mut w_nav = MODAL_STATE.write(); diff --git a/src/gui/views/network/setup/stratum.rs b/src/gui/views/network/setup/stratum.rs index 046c412..3d85aca 100644 --- a/src/gui/views/network/setup/stratum.rs +++ b/src/gui/views/network/setup/stratum.rs @@ -83,7 +83,7 @@ impl Default for StratumSetup { Self { wallets: WalletList::default(), - wallets_modal: WalletsModal::new(wallet_id), + wallets_modal: WalletsModal::new(wallet_id, None, false), available_ips: NodeConfig::get_ip_addrs(), stratum_port_edit: port, stratum_port_available_edit: is_port_available, @@ -111,10 +111,12 @@ impl ModalContainer for StratumSetup { modal: &Modal, cb: &dyn PlatformCallbacks) { match modal.id { - WALLET_SELECTION_MODAL => self.wallets_modal.ui(ui, modal, &self.wallets, |id| { - NodeConfig::save_stratum_wallet_id(id); - self.wallet_name = WalletConfig::name_by_id(id); - }), + WALLET_SELECTION_MODAL => { + self.wallets_modal.ui(ui, modal, &mut self.wallets, cb, |id, _| { + NodeConfig::save_stratum_wallet_id(id); + self.wallet_name = WalletConfig::name_by_id(id); + }) + }, STRATUM_PORT_MODAL => self.port_modal(ui, modal, cb), ATTEMPT_TIME_MODAL => self.attempt_modal(ui, modal, cb), MIN_SHARE_DIFF_MODAL => self.min_diff_modal(ui, modal, cb), @@ -240,7 +242,7 @@ impl StratumSetup { /// Show wallet selection [`Modal`]. fn show_wallets_modal(&mut self) { - self.wallets_modal = WalletsModal::new(NodeConfig::get_stratum_wallet_id()); + self.wallets_modal = WalletsModal::new(NodeConfig::get_stratum_wallet_id(), None, false); // Show modal. Modal::new(WALLET_SELECTION_MODAL) .position(ModalPosition::Center) diff --git a/src/gui/views/qr.rs b/src/gui/views/qr.rs index e0690ff..86599cf 100644 --- a/src/gui/views/qr.rs +++ b/src/gui/views/qr.rs @@ -30,8 +30,8 @@ use crate::gui::views::View; /// QR code image from text. pub struct QrCodeContent { - /// Text to create QR code. - pub(crate) text: String, + /// QR code text. + text: String, /// Flag to draw animated QR with Uniform Resources /// https://github.com/BlockchainCommons/Research/blob/master/papers/bcr-2020-005-ur.md @@ -62,18 +62,18 @@ impl QrCodeContent { } /// Draw QR code. - pub fn ui(&mut self, ui: &mut egui::Ui, text: String, cb: &dyn PlatformCallbacks) { + pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { if self.animated { // Show animated QR code. - self.animated_ui(ui, text, cb); + self.animated_ui(ui, cb); } else { // Show static QR code. - self.static_ui(ui, text, cb); + self.static_ui(ui, cb); } } /// Draw animated QR code content. - fn animated_ui(&mut self, ui: &mut egui::Ui, text: String, cb: &dyn PlatformCallbacks) { + fn animated_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { if !self.has_image() { let space = (ui.available_width() - View::BIG_SPINNER_SIZE) / 2.0; ui.vertical_centered(|ui| { @@ -84,7 +84,7 @@ impl QrCodeContent { // Create multiple vector images from text if not creating. if !self.loading() { - self.create_svg_list(text); + self.create_svg_list(); } } else { let svg_list = { @@ -111,7 +111,7 @@ impl QrCodeContent { // Show QR code text. ui.add_space(6.0); - View::ellipsize_text(ui, text.clone(), 16.0, Colors::inactive_text()); + View::ellipsize_text(ui, self.text.clone(), 16.0, Colors::inactive_text()); ui.add_space(6.0); ui.vertical_centered(|ui| { @@ -131,7 +131,7 @@ impl QrCodeContent { w_state.exporting = true; } // Create GIF to export. - self.create_qr_gif(text, DEFAULT_QR_SIZE as usize); + self.create_qr_gif(); }); } else { ui.vertical_centered(|ui| { @@ -171,7 +171,7 @@ impl QrCodeContent { } /// Draw static QR code content. - fn static_ui(&mut self, ui: &mut egui::Ui, text: String, cb: &dyn PlatformCallbacks) { + fn static_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { if !self.has_image() { let space = (ui.available_width() - View::BIG_SPINNER_SIZE) / 2.0; ui.vertical_centered(|ui| { @@ -182,7 +182,7 @@ impl QrCodeContent { // Create vector image from text if not creating. if !self.loading() { - self.create_svg(text); + self.create_svg(); } } else { // Create image from SVG data. @@ -194,7 +194,7 @@ impl QrCodeContent { // Show QR code text. ui.add_space(6.0); - View::ellipsize_text(ui, text.clone(), 16.0, Colors::inactive_text()); + View::ellipsize_text(ui, self.text.clone(), 16.0, Colors::inactive_text()); ui.add_space(6.0); // Show button to share QR. @@ -204,21 +204,22 @@ impl QrCodeContent { share_text, Colors::blue(), Colors::white_or_black(false), || { - if let Ok(qr) = QrCode::encode_text(text.as_str(), qrcodegen::QrCodeEcc::Low) { - if let Some(data) = Self::qr_to_image_data(qr, DEFAULT_QR_SIZE as usize) { - let mut png = vec![]; - let png_enc = PngEncoder::new_with_quality(&mut png, - CompressionType::Best, - FilterType::NoFilter); - if let Ok(()) = png_enc.write_image(data.as_slice(), - DEFAULT_QR_SIZE, - DEFAULT_QR_SIZE, - ExtendedColorType::L8) { - let name = format!("{}.png", chrono::Utc::now().timestamp()); - cb.share_data(name, png).unwrap_or_default(); + let text = self.text.as_str(); + if let Ok(qr) = QrCode::encode_text(text, qrcodegen::QrCodeEcc::Low) { + if let Some(data) = Self::qr_to_image_data(qr, DEFAULT_QR_SIZE as usize) { + let mut png = vec![]; + let png_enc = PngEncoder::new_with_quality(&mut png, + CompressionType::Best, + FilterType::NoFilter); + if let Ok(()) = png_enc.write_image(data.as_slice(), + DEFAULT_QR_SIZE, + DEFAULT_QR_SIZE, + ExtendedColorType::L8) { + let name = format!("{}.png", chrono::Utc::now().timestamp()); + cb.share_data(name, png).unwrap_or_default(); + } } } - } }); }); ui.add_space(8.0); @@ -267,8 +268,9 @@ impl QrCodeContent { } /// Create multiple vector QR code images at separate thread. - fn create_svg_list(&self, text: String) { + fn create_svg_list(&self) { let qr_state = self.qr_image_state.clone(); + let text = self.text.clone(); thread::spawn(move || { let mut encoder = ur::Encoder::bytes(text.as_bytes(), 100).unwrap(); let mut data = Vec::with_capacity(encoder.fragment_count()); @@ -294,8 +296,9 @@ impl QrCodeContent { } /// Create vector QR code image at separate thread. - fn create_svg(&self, text: String) { + fn create_svg(&self) { let qr_state = self.qr_image_state.clone(); + let text = self.text.clone(); thread::spawn(move || { if let Ok(qr) = QrCode::encode_text(text.as_str(), qrcodegen::QrCodeEcc::Low) { let svg = Self::qr_to_svg(qr, 0); @@ -332,13 +335,14 @@ impl QrCodeContent { } /// Create GIF image at separate thread. - fn create_qr_gif(&self, text: String, size: usize) { + fn create_qr_gif(&self) { { let mut w_state = self.qr_image_state.write(); w_state.gif_creating = true; } let qr_state = self.qr_image_state.clone(); + let text = self.text.clone(); thread::spawn(move || { // Setup GIF image encoder. let mut gif = vec![]; @@ -354,7 +358,7 @@ impl QrCodeContent { ) { // Create an image from QR data. let image = qr.render() - .max_dimensions(size as u32, size as u32) + .max_dimensions(DEFAULT_QR_SIZE, DEFAULT_QR_SIZE) .dark_color(image::Rgb([0, 0, 0])) .light_color(image::Rgb([255, 255, 255])) .build(); diff --git a/src/gui/views/wallets/content.rs b/src/gui/views/wallets/content.rs index 61b34c0..df5495b 100644 --- a/src/gui/views/wallets/content.rs +++ b/src/gui/views/wallets/content.rs @@ -18,13 +18,14 @@ use egui::scroll_area::ScrollBarVisibility; use crate::AppConfig; use crate::gui::Colors; -use crate::gui::icons::{ARROW_LEFT, CARET_RIGHT, COMPUTER_TOWER, FOLDER_LOCK, FOLDER_OPEN, GEAR, GLOBE, GLOBE_SIMPLE, LOCK_KEY, PLUS, SIDEBAR_SIMPLE, SPINNER, SUITCASE, WARNING_CIRCLE}; +use crate::gui::icons::{ARROW_LEFT, CARET_RIGHT, COMPUTER_TOWER, FOLDER_OPEN, GEAR, GLOBE, GLOBE_SIMPLE, LOCK_KEY, PLUS, SIDEBAR_SIMPLE, SUITCASE}; use crate::gui::platform::PlatformCallbacks; use crate::gui::views::{Modal, Content, TitlePanel, View}; -use crate::gui::views::types::{ModalContainer, ModalPosition, TextEditOptions, TitleContentType, TitleType}; +use crate::gui::views::types::{ModalContainer, ModalPosition, TitleContentType, TitleType}; use crate::gui::views::wallets::creation::WalletCreation; -use crate::gui::views::wallets::modals::WalletConnectionModal; +use crate::gui::views::wallets::modals::{OpenWalletModal, WalletConnectionModal, WalletsModal}; use crate::gui::views::wallets::types::WalletTabType; +use crate::gui::views::wallets::wallet::types::status_text; use crate::gui::views::wallets::WalletContent; use crate::wallet::{Wallet, WalletList}; @@ -33,13 +34,12 @@ pub struct WalletsContent { /// List of wallets. wallets: WalletList, - /// Password to open wallet for [`Modal`]. - pass_edit: String, - /// Flag to check if wrong password was entered at [`Modal`]. - wrong_pass: bool, - + /// Wallet selection [`Modal`] content. + wallet_selection_content: Option, + /// Wallet opening [`Modal`] content. + open_wallet_content: Option, /// Wallet connection selection content. - conn_modal_content: Option, + conn_selection_content: Option, /// Selected [`Wallet`] content. wallet_content: WalletContent, @@ -54,24 +54,29 @@ pub struct WalletsContent { } /// 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`]. -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 { fn default() -> Self { Self { wallets: WalletList::default(), - pass_edit: "".to_string(), - wrong_pass: false, - conn_modal_content: None, - wallet_content: WalletContent::default(), + wallet_selection_content: None, + open_wallet_content: None, + conn_selection_content: None, + wallet_content: WalletContent::new(None), creation_content: WalletCreation::default(), show_wallets_at_dual_panel: AppConfig::show_wallets_at_dual_panel(), modal_ids: vec![ OPEN_WALLET_MODAL, WalletCreation::NAME_PASS_MODAL, CONNECTION_SELECTION_MODAL, + SELECT_WALLET_MODAL ] } } @@ -87,13 +92,21 @@ impl ModalContainer for WalletsContent { modal: &Modal, cb: &dyn PlatformCallbacks) { 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 => { self.creation_content.name_pass_modal_ui(ui, modal, cb) }, 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| { + // Update wallet connection on select. let list = self.wallets.list(); for w in list { 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 { + /// Draw wallets content. pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { // Draw modal content for current ui container. self.current_modal_ui(ui, cb); @@ -159,7 +180,7 @@ impl WalletsContent { // Add created wallet to list. self.wallets.add(wallet); // Reset wallet content. - self.wallet_content = WalletContent::default(); + self.wallet_content = WalletContent::new(None); }); } else { let selected_id = self.wallets.selected_id.clone(); @@ -254,6 +275,57 @@ impl WalletsContent { 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, 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) { + 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. fn title_ui(&mut self, ui: &mut egui::Ui, @@ -383,8 +455,7 @@ impl WalletsContent { // Check if wallet reopen is needed. if !wallet.is_open() && wallet.reopen_needed() { wallet.set_reopen(false); - self.wallets.select(Some(wallet.get_config().id)); - self.show_open_wallet_modal(cb); + self.show_opening_modal(wallet.get_config().id, None, cb); } // Draw wallet list item. self.wallet_item_ui(ui, wallet, cb); @@ -420,8 +491,7 @@ impl WalletsContent { if !wallet.is_open() { // Show button to open closed wallet. View::item_button(ui, View::item_rounding(0, 1, true), FOLDER_OPEN, None, || { - self.wallets.select(Some(id)); - self.show_open_wallet_modal(cb); + self.show_opening_modal(id, None, cb); }); // Show button to select connection if not syncing. if !wallet.syncing() { @@ -435,7 +505,7 @@ impl WalletsContent { // Show button to select opened wallet. View::item_button(ui, View::item_rounding(0, 1, true), CARET_RIGHT, None, || { self.wallets.select(Some(id)); - self.wallet_content = WalletContent::default(); + self.wallet_content = WalletContent::new(None); }); } // Show button to close opened wallet. @@ -455,7 +525,7 @@ impl WalletsContent { ui.add_space(6.0); ui.vertical(|ui| { ui.add_space(3.0); - // Setup wallet name text. + // Show wallet name text. let name_color = if is_selected { Colors::white_or_black(true) } else { @@ -466,42 +536,11 @@ impl WalletsContent { View::ellipsize_text(ui, config.name, 18.0, name_color); }); - // Setup wallet status text. - let status_text = if wallet.is_open() { - if wallet.sync_error() { - format!("{} {}", WARNING_CIRCLE, t!("error")) - } else if wallet.is_closing() { - format!("{} {}", SPINNER, t!("wallets.closing")) - } else if wallet.is_repairing() { - let repair_progress = wallet.repairing_progress(); - if repair_progress == 0 { - format!("{} {}", SPINNER, t!("wallets.checking")) - } else { - format!("{} {}: {}%", - SPINNER, - t!("wallets.checking"), - repair_progress) - } - } else if wallet.syncing() { - let info_progress = wallet.info_sync_progress(); - if info_progress == 100 || info_progress == 0 { - format!("{} {}", SPINNER, t!("wallets.loading")) - } else { - format!("{} {}: {}%", - SPINNER, - t!("wallets.loading"), - info_progress) - } - } else { - format!("{} {}", FOLDER_OPEN, t!("wallets.unlocked")) - } - } else { - format!("{} {}", FOLDER_LOCK, t!("wallets.locked")) - }; - View::ellipsize_text(ui, status_text, 15.0, Colors::text(false)); + // Show wallet status text. + View::ellipsize_text(ui, status_text(wallet), 15.0, Colors::text(false)); 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() { format!("{} {}", GLOBE_SIMPLE, conn.url) } else { @@ -517,7 +556,7 @@ impl WalletsContent { /// Show [`Modal`] to select connection for the wallet. fn show_connection_selector_modal(&mut self, wallet: &Wallet) { let ext_conn = wallet.get_current_ext_conn(); - self.conn_modal_content = Some(WalletConnectionModal::new(ext_conn)); + self.conn_selection_content = Some(WalletConnectionModal::new(ext_conn)); // Show modal. Modal::new(CONNECTION_SELECTION_MODAL) .position(ModalPosition::CenterTop) @@ -525,11 +564,10 @@ impl WalletsContent { .show(); } - /// Show [`Modal`] to open selected wallet. - fn show_open_wallet_modal(&mut self, cb: &dyn PlatformCallbacks) { - // Reset modal values. - self.pass_edit = String::from(""); - self.wrong_pass = false; + /// Show [`Modal`] to select and open wallet. + fn show_opening_modal(&mut self, id: i64, data: Option, cb: &dyn PlatformCallbacks) { + self.wallets.select(Some(id)); + self.open_wallet_content = Some(OpenWalletModal::new(data)); // Show modal. Modal::new(OPEN_WALLET_MODAL) .position(ModalPosition::CenterTop) @@ -537,100 +575,6 @@ impl WalletsContent { .show(); cb.show_keyboard(); } - - /// Draw wallet opening [`Modal`] content. - fn open_wallet_modal_ui(&mut self, - ui: &mut egui::Ui, - modal: &Modal, - cb: &dyn PlatformCallbacks) { - ui.add_space(6.0); - ui.vertical_centered(|ui| { - ui.label(RichText::new(t!("wallets.pass")) - .size(17.0) - .color(Colors::gray())); - ui.add_space(8.0); - - // Show password input. - let mut pass_edit_opts = TextEditOptions::new(Id::from(modal.id)).password(); - View::text_edit(ui, cb, &mut self.pass_edit, &mut pass_edit_opts); - - // Show information when password is empty. - if self.pass_edit.is_empty() { - self.wrong_pass = false; - ui.add_space(10.0); - ui.label(RichText::new(t!("wallets.pass_empty")) - .size(17.0) - .color(Colors::inactive_text())); - } else if self.wrong_pass { - ui.add_space(10.0); - ui.label(RichText::new(t!("wallets.wrong_pass")) - .size(17.0) - .color(Colors::red())); - } - ui.add_space(12.0); - }); - - // Show modal buttons. - ui.scope(|ui| { - // Setup spacing between buttons. - ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); - - ui.columns(2, |columns| { - columns[0].vertical_centered_justified(|ui| { - View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || { - // Close modal. - cb.hide_keyboard(); - modal.close(); - }); - }); - columns[1].vertical_centered_justified(|ui| { - // Callback for button to continue. - let mut on_continue = || { - if self.pass_edit.is_empty() { - return; - } - match self.wallets.open_selected(&self.pass_edit) { - Ok(_) => { - // Clear values. - self.pass_edit = "".to_string(); - self.wrong_pass = false; - // Close modal. - cb.hide_keyboard(); - modal.close(); - // Reset wallet content. - self.wallet_content = WalletContent::default(); - } - Err(_) => self.wrong_pass = true - } - }; - - // Continue on Enter key press. - View::on_enter_key(ui, || { - (on_continue)(); - }); - - View::button(ui, t!("continue"), Colors::white_or_black(false), on_continue); - }); - }); - ui.add_space(6.0); - }); - } - - /// Handle Back key event. - /// Return `false` when event was handled. - pub fn on_back(&mut self) -> bool { - let can_go_back = self.creation_content.can_go_back(); - if can_go_back { - self.creation_content.back(); - return false - } else { - if self.wallets.is_selected_open() { - self.wallets.select(None); - return false - } - } - true - } } /// Check if it's possible to show [`WalletsContent`] and [`WalletContent`] panels at same time. diff --git a/src/gui/views/wallets/creation/creation.rs b/src/gui/views/wallets/creation/creation.rs index d8b3cc8..eaf3621 100644 --- a/src/gui/views/wallets/creation/creation.rs +++ b/src/gui/views/wallets/creation/creation.rs @@ -23,7 +23,7 @@ use crate::gui::views::{Modal, Content, View}; use crate::gui::views::types::{ModalPosition, TextEditOptions}; use crate::gui::views::wallets::creation::MnemonicSetup; use crate::gui::views::wallets::creation::types::Step; -use crate::gui::views::wallets::settings::ConnectionSettings; +use crate::gui::views::wallets::ConnectionSettings; use crate::node::Node; use crate::wallet::{ExternalConnection, Wallet}; use crate::wallet::types::PhraseMode; diff --git a/src/gui/views/wallets/modals/mod.rs b/src/gui/views/wallets/modals/mod.rs index c4f566c..bdb7bb6 100644 --- a/src/gui/views/wallets/modals/mod.rs +++ b/src/gui/views/wallets/modals/mod.rs @@ -16,4 +16,7 @@ mod conn; pub use conn::*; mod wallets; -pub use wallets::*; \ No newline at end of file +pub use wallets::*; + +mod open; +pub use open::*; \ No newline at end of file diff --git a/src/gui/views/wallets/modals/open.rs b/src/gui/views/wallets/modals/open.rs new file mode 100644 index 0000000..c585d99 --- /dev/null +++ b/src/gui/views/wallets/modals/open.rs @@ -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, +} + +impl OpenWalletModal { + /// Create new content instance. + pub fn new(data: Option) -> 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)) { + 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); + }); + } +} \ No newline at end of file diff --git a/src/gui/views/wallets/modals/wallets.rs b/src/gui/views/wallets/modals/wallets.rs index b2bb8d5..b8b846b 100644 --- a/src/gui/views/wallets/modals/wallets.rs +++ b/src/gui/views/wallets/modals/wallets.rs @@ -16,29 +16,53 @@ use egui::scroll_area::ScrollBarVisibility; use egui::{Align, Layout, RichText, ScrollArea}; use crate::gui::Colors; -use crate::gui::icons::{CHECK, CHECK_FAT, COMPUTER_TOWER, GLOBE_SIMPLE, PLUGS_CONNECTED}; +use crate::gui::icons::{CHECK, CHECK_FAT, COMPUTER_TOWER, FOLDER_OPEN, GLOBE_SIMPLE, PLUGS_CONNECTED}; +use crate::gui::platform::PlatformCallbacks; use crate::gui::views::{Modal, View}; +use crate::gui::views::types::ModalPosition; +use crate::gui::views::wallets::modals::OpenWalletModal; +use crate::gui::views::wallets::wallet::types::status_text; use crate::wallet::{Wallet, WalletList}; /// Wallet list [`Modal`] content pub struct WalletsModal { /// Selected wallet id. - selected: Option + selected: Option, + + /// Optional data to pass after wallet selection. + data: Option, + + /// Flag to check if wallet can be opened from the list. + can_open: bool, + /// Wallet opening content. + open_wallet_content: Option, } impl WalletsModal { - pub fn new(selected: Option) -> Self { - Self { - selected, - } + /// Create new content instance. + pub fn new(selected: Option, data: Option, can_open: bool) -> Self { + Self { selected, data, can_open, open_wallet_content: None } } - /// Draw [`Modal`] content. + /// Draw content. pub fn ui(&mut self, ui: &mut egui::Ui, modal: &Modal, - wallets: &WalletList, - mut on_select: impl FnMut(i64)) { + wallets: &mut WalletList, + cb: &dyn PlatformCallbacks, + mut on_select: impl FnMut(i64, Option)) { + // 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); ScrollArea::vertical() .max_height(373.0) @@ -48,10 +72,12 @@ impl WalletsModal { .show(ui, |ui| { ui.add_space(2.0); ui.vertical_centered(|ui| { - for wallet in wallets.list() { + let data = self.data.clone(); + for wallet in wallets.clone().list() { // Draw wallet list item. - self.wallet_item_ui(ui, wallet, modal, |id| { - on_select(id); + self.wallet_item_ui(ui, wallet, wallets, |id| { + modal.close(); + on_select(id, data.clone()); }); ui.add_space(5.0); } @@ -65,18 +91,19 @@ impl WalletsModal { // Show button to close modal. ui.vertical_centered_justified(|ui| { View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || { + self.data = None; modal.close(); }); }); ui.add_space(6.0); } - /// Draw wallet list item. + /// Draw wallet list item with provided callback on select. fn wallet_item_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, - modal: &Modal, - mut on_select: impl FnMut(i64)) { + wallets: &mut WalletList, + mut select: impl FnMut(i64)) { let config = wallet.get_config(); let id = config.id; @@ -87,16 +114,34 @@ impl WalletsModal { ui.painter().rect(rect, rounding, Colors::fill(), View::hover_stroke()); ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| { - // Draw button to select wallet. - let current = self.selected.unwrap_or(0) == id; - if current { - ui.add_space(12.0); - ui.label(RichText::new(CHECK_FAT).size(20.0).color(Colors::green())); - } else { - View::item_button(ui, View::item_rounding(0, 1, true), CHECK, None, || { - on_select(id); - modal.close(); + if self.can_open { + // Show button to select or open closed wallet. + let icon = if wallet.is_open() { + CHECK + } else { + FOLDER_OPEN + }; + View::item_button(ui, View::item_rounding(0, 1, true), icon, None, || { + wallets.select(Some(id)); + if wallet.is_open() { + select(id); + } else { + self.selected = wallets.selected_id; + Modal::change_position(ModalPosition::CenterTop); + self.open_wallet_content = Some(OpenWalletModal::new(self.data.clone())); + } }); + } else { + // Draw button to select wallet. + let current = self.selected.unwrap_or(0) == id; + if current { + ui.add_space(12.0); + ui.label(RichText::new(CHECK_FAT).size(20.0).color(Colors::green())); + } else { + View::item_button(ui, View::item_rounding(0, 1, true), CHECK, None, || { + select(id); + }); + } } let layout_size = ui.available_size(); @@ -104,13 +149,13 @@ impl WalletsModal { ui.add_space(6.0); ui.vertical(|ui| { ui.add_space(3.0); - // Setup wallet name text. + // Show wallet name text. ui.with_layout(Layout::left_to_right(Align::Min), |ui| { ui.add_space(1.0); View::ellipsize_text(ui, config.name, 18.0, Colors::title(false)); }); - // Setup wallet connection text. + // Show wallet connection text. let conn = if let Some(conn) = wallet.get_current_ext_conn() { format!("{} {}", GLOBE_SIMPLE, conn.url) } else { @@ -119,14 +164,20 @@ impl WalletsModal { View::ellipsize_text(ui, conn, 15.0, Colors::text(false)); ui.add_space(1.0); - // Setup wallet API text. - let address = if let Some(port) = config.api_port { - format!("127.0.0.1:{}", port) + // Show wallet API text or open status. + if self.can_open { + ui.label(RichText::new(status_text(wallet)) + .size(15.0) + .color(Colors::gray())); } else { - "-".to_string() - }; - let api_text = format!("{} {}", PLUGS_CONNECTED, address); - ui.label(RichText::new(api_text).size(15.0).color(Colors::gray())); + let address = if let Some(port) = config.api_port { + format!("127.0.0.1:{}", port) + } else { + "-".to_string() + }; + let api_text = format!("{} {}", PLUGS_CONNECTED, address); + ui.label(RichText::new(api_text).size(15.0).color(Colors::gray())); + } ui.add_space(3.0); }); }); diff --git a/src/gui/views/wallets/wallet/content.rs b/src/gui/views/wallets/wallet/content.rs index 1de3632..f235666 100644 --- a/src/gui/views/wallets/wallet/content.rs +++ b/src/gui/views/wallets/wallet/content.rs @@ -13,57 +13,34 @@ // limitations under the License. use std::time::Duration; -use egui::{Align, Id, Layout, Margin, RichText, ScrollArea}; -use egui::scroll_area::ScrollBarVisibility; +use egui::{Align, Id, Layout, Margin, RichText}; use grin_chain::SyncStatus; use grin_core::core::amount_to_hr_string; use crate::AppConfig; use crate::gui::Colors; -use crate::gui::icons::{ARROWS_CLOCKWISE, BRIDGE, CHAT_CIRCLE_TEXT, CHECK, CHECK_FAT, COPY, FOLDER_USER, GEAR_FINE, GRAPH, PACKAGE, PATH, POWER, SCAN, SPINNER, USERS_THREE}; +use crate::gui::icons::{ARROWS_CLOCKWISE, BRIDGE, CHAT_CIRCLE_TEXT, FOLDER_USER, GEAR_FINE, GRAPH, PACKAGE, POWER, SCAN, SPINNER, USERS_THREE}; use crate::gui::platform::PlatformCallbacks; -use crate::gui::views::{CameraContent, Modal, Content, View}; -use crate::gui::views::types::{ModalPosition, QrScanResult, TextEditOptions}; +use crate::gui::views::{Modal, Content, View}; +use crate::gui::views::types::{ModalPosition, QrScanResult}; use crate::gui::views::wallets::{WalletTransactions, WalletMessages, WalletTransport}; use crate::gui::views::wallets::types::{GRIN, WalletTab, WalletTabType}; -use crate::gui::views::wallets::settings::WalletSettings; +use crate::gui::views::wallets::wallet::modals::{WalletAccountsModal, WalletScanModal}; +use crate::gui::views::wallets::wallet::WalletSettings; use crate::node::Node; use crate::wallet::{Wallet, WalletConfig}; -use crate::wallet::types::{WalletAccount, WalletData}; +use crate::wallet::types::WalletData; /// Selected and opened wallet content. pub struct WalletContent { - /// List of wallet accounts for [`Modal`]. - accounts: Vec, + /// Wallet accounts [`Modal`] content. + accounts_modal_content: Option, - /// Flag to check if account is creating. - account_creating: bool, - /// Account label [`Modal`] value. - account_label_edit: String, - /// Flag to check if error occurred during account creation at [`Modal`]. - account_creation_error: bool, - - /// Camera content for QR scan [`Modal`]. - camera_content: CameraContent, - /// QR code scan result - qr_scan_result: Option, + /// QR code scan [`Modal`] content. + scan_modal_content: Option, /// Current tab content to show. - pub current_tab: Box -} - -impl Default for WalletContent { - fn default() -> Self { - Self { - accounts: vec![], - account_creating: false, - account_label_edit: "".to_string(), - account_creation_error: false, - camera_content: CameraContent::default(), - qr_scan_result: None, - current_tab: Box::new(WalletTransactions::default()) - } - } + pub current_tab: Box, } /// 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"; impl WalletContent { + /// Create new instance with optional data. + pub fn new(data: Option) -> 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. pub fn ui(&mut self, ui: &mut egui::Ui, @@ -148,7 +139,7 @@ impl WalletContent { ui.vertical_centered(|ui| { // Draw wallet tabs. View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| { - self.tabs_ui(ui, wallet); + self.tabs_ui(ui); }); }); }); @@ -185,21 +176,50 @@ impl WalletContent { /// Draw [`Modal`] content for this ui container. fn modal_content_ui(&mut self, ui: &mut egui::Ui, - wallet: &mut Wallet, + wallet: &Wallet, cb: &dyn PlatformCallbacks) { match Modal::opened() { None => {} Some(id) => { match id { ACCOUNT_LIST_MODAL => { - Modal::ui(ui.ctx(), |ui, modal| { - self.account_list_modal_ui(ui, wallet, modal, cb); - }); + if let Some(content) = self.accounts_modal_content.as_mut() { + Modal::ui(ui.ctx(), |ui, modal| { + content.ui(ui, wallet, modal, cb); + }); + } } QR_CODE_SCAN_MODAL => { - Modal::ui(ui.ctx(), |ui, modal| { - self.scan_qr_modal_ui(ui, wallet, modal, cb); - }); + if let Some(content) = self.scan_modal_content.as_mut() { + Modal::ui(ui.ctx(), |ui, modal| { + content.ui(ui, wallet, modal, cb, |result| { + match result { + QrScanResult::Slatepack(message) => { + modal.close(); + let msg = Some(message.to_string()); + let messages = WalletMessages::new(msg); + self.current_tab = Box::new(messages); + return; + } + QrScanResult::Address(receiver) => { + let balance = wallet.get_data() + .unwrap() + .info + .amount_currently_spendable; + if balance > 0 { + modal.close(); + let mut transport = WalletTransport::default(); + let rec = Some(receiver.to_string()); + transport.show_send_tor_modal(cb, rec); + self.current_tab = Box::new(transport); + return; + } + } + _ => {} + } + }); + }); + } } _ => {} } @@ -222,8 +242,7 @@ impl WalletContent { ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| { // Draw button to scan QR code. View::item_button(ui, View::item_rounding(0, 2, true), SCAN, None, || { - self.qr_scan_result = None; - self.camera_content.clear_state(); + self.scan_modal_content = Some(WalletScanModal::default()); // Show QR code scan modal. Modal::new(QR_CODE_SCAN_MODAL) .position(ModalPosition::CenterTop) @@ -235,10 +254,7 @@ impl WalletContent { // Draw button to show list of accounts. View::item_button(ui, View::item_rounding(1, 3, true), USERS_THREE, None, || { - // Load accounts. - self.account_label_edit = "".to_string(); - self.accounts = wallet.accounts(); - self.account_creating = false; + self.accounts_modal_content = Some(WalletAccountsModal::new(wallet.accounts())); // Show account list modal. Modal::new(ACCOUNT_LIST_MODAL) .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. - fn tabs_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet) { + fn tabs_ui(&mut self, ui: &mut egui::Ui) { ui.scope(|ui| { // Setup spacing between tabs. ui.style_mut().spacing.item_spacing = egui::vec2(View::TAB_ITEMS_PADDING, 0.0); @@ -547,14 +341,13 @@ impl WalletContent { let is_messages = current_type == WalletTabType::Messages; View::tab_button(ui, CHAT_CIRCLE_TEXT, is_messages, || { self.current_tab = Box::new( - WalletMessages::new(wallet.can_use_dandelion(), None) + WalletMessages::new(None) ); }); }); columns[2].vertical_centered_justified(|ui| { View::tab_button(ui, BRIDGE, current_type == WalletTabType::Transport, || { - let addr = wallet.slatepack_address().unwrap(); - self.current_tab = Box::new(WalletTransport::new(addr)); + self.current_tab = Box::new(WalletTransport::default()); }); }); columns[3].vertical_centered_justified(|ui| { @@ -675,68 +468,4 @@ impl WalletContent { }); }); } -} - -const ACCOUNT_ITEM_HEIGHT: f32 = 75.0; - -/// Draw account item. -fn account_item_ui(ui: &mut egui::Ui, - modal: &Modal, - wallet: &mut Wallet, - acc: &WalletAccount, - index: usize, - size: usize) { - // Setup layout size. - let mut rect = ui.available_rect_before_wrap(); - rect.set_height(ACCOUNT_ITEM_HEIGHT); - - // Draw round background. - let bg_rect = rect.clone(); - let item_rounding = View::item_rounding(index, size, false); - ui.painter().rect(bg_rect, item_rounding, Colors::fill(), View::item_stroke()); - - ui.vertical(|ui| { - ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| { - // Draw button to select account. - let is_current_account = wallet.get_config().account == acc.label; - if !is_current_account { - let button_rounding = View::item_rounding(index, size, true); - View::item_button(ui, button_rounding, CHECK, None, || { - let _ = wallet.set_active_account(&acc.label); - modal.close(); - }); - } else { - ui.add_space(12.0); - ui.label(RichText::new(CHECK_FAT).size(20.0).color(Colors::green())); - } - - let layout_size = ui.available_size(); - ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| { - ui.add_space(6.0); - ui.vertical(|ui| { - ui.add_space(4.0); - // Show spendable amount. - let amount = amount_to_hr_string(acc.spendable_amount, true); - let amount_text = format!("{} {}", amount, GRIN); - ui.label(RichText::new(amount_text).size(18.0).color(Colors::white_or_black(true))); - ui.add_space(-2.0); - - // Show account name. - let default_acc_label = WalletConfig::DEFAULT_ACCOUNT_LABEL.to_string(); - let acc_label = if acc.label == default_acc_label { - t!("wallets.default_account") - } else { - acc.label.to_owned() - }; - let acc_name = format!("{} {}", FOLDER_USER, acc_label); - View::ellipsize_text(ui, acc_name, 15.0, Colors::text(false)); - - // Show account BIP32 derivation path. - let acc_path = format!("{} {}", PATH, acc.path); - ui.label(RichText::new(acc_path).size(15.0).color(Colors::gray())); - ui.add_space(3.0); - }); - }); - }); - }); } \ No newline at end of file diff --git a/src/gui/views/wallets/wallet/messages.rs b/src/gui/views/wallets/wallet/messages.rs deleted file mode 100644 index 1306b1e..0000000 --- a/src/gui/views/wallets/wallet/messages.rs +++ /dev/null @@ -1,1166 +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::{Id, Margin, RichText, ScrollArea}; -use egui::scroll_area::ScrollBarVisibility; -use grin_core::core::{amount_from_hr_string, amount_to_hr_string}; -use grin_wallet_libwallet::{Error, Slate, SlateState}; -use log::error; -use parking_lot::RwLock; - -use crate::gui::Colors; -use crate::gui::icons::{BROOM, CLIPBOARD_TEXT, COPY, DOWNLOAD_SIMPLE, PROHIBIT, QR_CODE, SCAN, UPLOAD_SIMPLE}; -use crate::gui::platform::PlatformCallbacks; -use crate::gui::views::{CameraContent, FilePickButton, Modal, QrCodeContent, Content, View}; -use crate::gui::views::types::{ModalPosition, QrScanResult, TextEditOptions}; -use crate::gui::views::wallets::wallet::types::{SLATEPACK_MESSAGE_HINT, WalletTab, WalletTabType}; -use crate::gui::views::wallets::wallet::WalletContent; -use crate::wallet::types::WalletTransaction; -use crate::wallet::Wallet; - -#[derive(Clone, Eq, PartialEq, Debug, thiserror::Error)] -enum MessageError { - #[error("{0}")] - Response(String), - #[error("{0}")] - Parse(String), - #[error("{0}")] - Finalize(String), - #[error("{0}")] - Other(String), -} - -impl MessageError { - pub fn text(&self) -> &String { - match self { - MessageError::Response(text) => text, - MessageError::Parse(text) => text, - MessageError::Finalize(text) => text, - MessageError::Other(text) => text - } - } -} - -/// Slatepacks messages interaction tab content. -pub struct WalletMessages { - /// Slatepack message to create response message. - message_edit: String, - /// Parsed Slatepack message. - message_slate: Option, - /// Flag to check if message request is loading. - message_loading: bool, - /// Message request result. - receive_pay_result: Arc)>>>, - /// Message finalize or post result. - final_post_result: Arc>>>, - /// Slatepack error on finalization, parse and response creation. - message_error: Option, - /// Generated Slatepack response message. - response_edit: String, - /// Flag to check if Dandelion is needed to finalize transaction. - dandelion: bool, - /// Button to parse picked file content. - file_pick_button: FilePickButton, - - /// Flag to check if invoice or sending request was opened for [`Modal`]. - request_invoice: bool, - /// Amount to send or receive at [`Modal`]. - request_amount_edit: String, - /// Generated Slatepack message as request to send or receive funds at [`Modal`]. - request_edit: String, - /// Flag to check if there is an error happened on request creation at [`Modal`]. - request_error: Option, - /// Flag to check if response Slatepack message is showing as QR code image at [`Modal`]. - request_qr: bool, - /// Request Slatepack message QR code image [`Modal`] content. - request_qr_content: QrCodeContent, - /// Flag to check if request is loading at [`Modal`]. - request_loading: bool, - /// Request result if there is no error at [`Modal`]. - request_result: Arc>>>, - - /// 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, - - /// QR code Slatepacks message text to show at [`Modal`]. - qr_message_text: Option, - /// QR code Slatepack message image [`Modal`] content. - qr_message_content: QrCodeContent, -} - -/// Identifier for amount input [`Modal`] to create invoice or sending request. -const REQUEST_MODAL: &'static str = "messages_request_modal"; - -/// Identifier for QR code Slatepack message scan [`Modal`]. -const QR_SLATEPACK_MESSAGE_SCAN_MODAL: &'static str = "qr_slatepack_message_scan_modal"; - -/// Identifier for [`Modal`] to show QR code Slatepack message image. -const QR_SLATEPACK_MESSAGE_MODAL: &'static str = "qr_slatepack_message_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(dandelion: bool, message: Option) -> Self { - Self { - request_invoice: false, - message_edit: message.unwrap_or("".to_string()), - message_slate: None, - message_loading: false, - receive_pay_result: Arc::new(RwLock::new(None)), - final_post_result: Arc::new(RwLock::new(None)), - message_error: None, - response_edit: "".to_string(), - dandelion, - file_pick_button: FilePickButton::default(), - request_amount_edit: "".to_string(), - request_edit: "".to_string(), - request_error: None, - request_qr: false, - request_qr_content: QrCodeContent::new("".to_string(), true), - request_loading: false, - request_result: Arc::new(RwLock::new(None)), - message_camera_content: CameraContent::default(), - message_scan_error: false, - qr_message_text: None, - qr_message_content: QrCodeContent::new("".to_string(), true), - } - } - - /// Draw manual wallet transaction interaction content. - pub fn ui(&mut self, - ui: &mut egui::Ui, - wallet: &mut Wallet, - cb: &dyn PlatformCallbacks) { - 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 => { - Modal::ui(ui.ctx(), |ui, modal| { - self.request_modal_ui(ui, wallet, modal, cb); - }); - } - QR_SLATEPACK_MESSAGE_SCAN_MODAL => { - Modal::ui(ui.ctx(), |ui, modal| { - self.qr_message_scan_modal_ui(ui, modal, wallet, cb); - }); - } - QR_SLATEPACK_MESSAGE_MODAL => { - Modal::ui(ui.ctx(), |ui, modal| { - self.qr_message_modal_ui(ui, modal, 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| { - // Draw sending request creation button. - 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| { - // Draw invoice request creation button. - self.receive_button_ui(ui, cb); - }); - }); - } else { - // Draw invoice creation button. - 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) { - // Setup modal values. - self.request_invoice = invoice; - self.request_qr = false; - self.request_edit = "".to_string(); - self.request_amount_edit = "".to_string(); - self.request_error = None; - { - let mut w_result = self.request_result.write(); - *w_result = None; - } - // Show receive amount modal. - let title = if self.request_invoice { - t!("wallets.receive") - } else { - t!("wallets.send") - }; - Modal::new(REQUEST_MODAL).position(ModalPosition::CenterTop).title(title).show(); - cb.show_keyboard(); - } - - /// Draw invoice or sending request creation [`Modal`] content. - fn request_modal_ui(&mut self, - ui: &mut egui::Ui, - wallet: &mut Wallet, - modal: &Modal, - cb: &dyn PlatformCallbacks) { - ui.add_space(6.0); - - if self.request_loading { - 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() { - let message = r_request.as_ref().unwrap(); - match message { - Ok((_, message)) => { - self.request_edit = message.clone(); - } - Err(err) => { - match err { - Error::NotEnoughFunds { .. } => { - let m = t!( - "wallets.pay_balance_error", - "amount" => self.request_amount_edit - ); - self.request_error = Some(MessageError::Other(m)); - } - _ => { - let m = t!("wallets.invoice_slatepack_err"); - self.request_error = Some(MessageError::Other(m)); - } - } - } - } - modal.enable_closing(); - self.request_loading = false; - } - } else if self.request_edit.is_empty() { - ui.vertical_centered(|ui| { - let enter_text = if self.request_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.request_amount_edit.clone(); - View::text_edit(ui, cb, &mut self.request_amount_edit, &mut amount_edit_opts); - - // Check value if input was changed. - if amount_edit_before != self.request_amount_edit { - self.request_error = None; - if !self.request_amount_edit.is_empty() { - self.request_amount_edit = self.request_amount_edit.trim().replace(",", "."); - match amount_from_hr_string(self.request_amount_edit.as_str()) { - Ok(a) => { - if !self.request_amount_edit.contains(".") { - // To avoid input of several "0". - if a == 0 { - self.request_amount_edit = "0".to_string(); - return; - } - } else { - // Check input after ".". - let parts = self.request_amount_edit - .split(".") - .collect::>(); - if parts.len() == 2 && parts[1].len() > 9 { - self.request_amount_edit = amount_edit_before; - return; - } - } - - // Do not input amount more than balance in sending. - if !self.request_invoice { - let b = wallet.get_data().unwrap().info.amount_currently_spendable; - if b < a { - self.request_amount_edit = amount_edit_before; - } - } - } - Err(_) => { - self.request_amount_edit = amount_edit_before; - } - } - } - } - - // Show request creation error. - if self.request_error.is_some() { - ui.add_space(12.0); - ui.vertical_centered(|ui| { - ui.label(RichText::new(self.request_error.clone().unwrap().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!("modal.cancel"), Colors::white_or_black(false), || { - self.request_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.request_amount_edit.is_empty() { - return; - } - if let Ok(a) = amount_from_hr_string(self.request_amount_edit.as_str()) { - cb.hide_keyboard(); - // Setup data for request. - let wallet = wallet.clone(); - let invoice = self.request_invoice.clone(); - let result = self.request_result.clone(); - // Send request at another thread. - self.request_loading = true; - modal.disable_closing(); - thread::spawn(move || { - let message = if invoice { - wallet.issue_invoice(a) - } else { - wallet.send(a) - }; - let mut w_result = result.write(); - *w_result = Some(message); - }); - } else { - self.request_error = Some( - MessageError::Other(t!("wallets.invoice_slatepack_err")) - ); - } - }); - }); - }); - ui.add_space(6.0); - } else { - ui.vertical_centered(|ui| { - let amount = amount_from_hr_string(self.request_amount_edit.as_str()).unwrap(); - let amount_format = amount_to_hr_string(amount, true); - let desc_text = if self.request_invoice { - t!("wallets.invoice_desc","amount" => amount_format) - } else { - t!("wallets.send_request_desc","amount" => amount_format) - }; - ui.label(RichText::new(desc_text).size(16.0).color(Colors::gray())); - }); - ui.add_space(6.0); - - // Draw QR code content if requested. - if self.request_qr { - // Draw QR code content. - let text = self.request_edit.clone(); - if text.is_empty() { - self.request_qr = false; - } - self.request_qr_content.ui(ui, text.clone(), cb); - - // Show button to close modal. - ui.vertical_centered_justified(|ui| { - View::button(ui, t!("close"), Colors::white_or_black(false), || { - self.request_qr_content.clear_state(); - self.request_qr = false; - modal.close(); - }); - }); - ui.add_space(6.0); - return; - } - - View::horizontal_line(ui, Colors::item_stroke()); - ui.add_space(3.0); - - // Draw request Slatepack message text. - let scroll_id = if self.request_invoice { - Id::from("receive_request").with(wallet.get_config().id) - } else { - Id::from("send_request").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 = Id::from(scroll_id).with("_input"); - egui::TextEdit::multiline(&mut self.request_edit) - .id(input_id) - .font(egui::TextStyle::Small) - .desired_rows(5) - .interactive(false) - .hint_text(SLATEPACK_MESSAGE_HINT) - .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); - - 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 show request as QR code. - let qr_text = format!("{} {}", QR_CODE, t!("qr_code")); - View::button(ui, qr_text, Colors::button(), || { - self.request_qr = true; - }); - }); - columns[1].vertical_centered_justified(|ui| { - // Draw button to copy request to clipboard. - let copy_text = format!("{} {}", COPY, t!("copy")); - View::button(ui, copy_text, Colors::button(), || { - cb.copy_string_to_buffer(self.request_edit.clone()); - self.request_amount_edit = "".to_string(); - self.request_edit = "".to_string(); - modal.close(); - }); - }); - }); - - ui.add_space(10.0); - - ui.columns(2, |columns| { - columns[0].vertical_centered_justified(|ui| { - // Draw button to cancel transaction. - let cancel = t!("modal.cancel"); - View::colored_text_button(ui, cancel, Colors::red(), Colors::button(), || { - if let Ok(slate) = wallet.parse_slatepack(&self.request_edit) { - if let Some(tx) = wallet.tx_by_slate(&slate) { - wallet.cancel(tx.data.id); - } - } - self.request_amount_edit = "".to_string(); - self.request_edit = "".to_string(); - cb.hide_keyboard(); - modal.close(); - }); - }); - columns[1].vertical_centered_justified(|ui| { - // Draw button to close modal. - View::button(ui, t!("close"), Colors::white_or_black(false), || { - self.request_amount_edit = "".to_string(); - self.request_edit = "".to_string(); - modal.close(); - }); - }); - }); - }); - ui.add_space(6.0); - } - } - - /// Draw Slatepack message input content. - fn input_slatepack_ui(&mut self, - ui: &mut egui::Ui, - wallet: &mut Wallet, - cb: &dyn PlatformCallbacks) { - // Setup description text. - let empty_fields = self.message_edit.is_empty() && self.request_edit.is_empty(); - let response_empty = self.response_edit.is_empty(); - if let Some(err) = &self.message_error { - ui.label(RichText::new(err.text()).size(16.0).color(Colors::red())); - } else { - let desc_text = if self.message_slate.is_none() || empty_fields { - t!("wallets.input_slatepack_desc") - } else { - let slate = self.message_slate.clone().unwrap(); - let amount = amount_to_hr_string(slate.amount, true); - match slate.state { - SlateState::Standard1 => { - t!("wallets.parse_s1_slatepack_desc","amount" => amount) - } - SlateState::Standard2 => { - t!("wallets.parse_s2_slatepack_desc","amount" => amount) - } - SlateState::Standard3 => { - t!("wallets.parse_s3_slatepack_desc","amount" => amount) - } - SlateState::Invoice1 => { - t!("wallets.parse_i1_slatepack_desc","amount" => amount) - } - SlateState::Invoice2 => { - t!("wallets.parse_i2_slatepack_desc","amount" => amount) - } - SlateState::Invoice3 => { - t!("wallets.parse_i3_slatepack_desc","amount" => amount) - } - _ => { - t!("wallets.input_slatepack_desc") - } - } - }; - ui.label(RichText::new(desc_text).size(16.0).color(Colors::inactive_text())); - } - ui.add_space(6.0); - - // Setup Slatepack message text input. - let message = if response_empty { - &mut self.message_edit - } else { - &mut self.response_edit - }; - - // Save message to check for changes. - let message_before = message.clone(); - - View::horizontal_line(ui, Colors::item_stroke()); - ui.add_space(3.0); - let scroll_id = Id::from( - if response_empty { - "message_input" - } else { - "response_input" - }).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(message) - .id(input_id) - .font(egui::TextStyle::Small) - .desired_rows(5) - .interactive(response_empty && !self.message_loading) - .hint_text(SLATEPACK_MESSAGE_HINT) - .desired_width(f32::INFINITY) - .show(ui) - .response; - // Show soft keyboard on click. - if response_empty && resp.clicked() { - resp.request_focus(); - cb.show_keyboard(); - } - if response_empty && resp.has_focus() { - // Apply text from input on Android as temporary fix for egui. - View::on_soft_input(ui, input_id, message); - } - ui.add_space(6.0); - }); - ui.add_space(2.0); - View::horizontal_line(ui, Colors::item_stroke()); - ui.add_space(10.0); - - // Parse Slatepack message if input field was changed, resetting message error. - if &message_before != message { - self.parse_message(wallet); - } - - // Draw buttons to clear/copy/paste. - let columns_num = if self.message_loading { 1 } else { 2 }; - let mut show_dandelion = false; - ui.scope(|ui| { - // Setup spacing between buttons. - ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); - - ui.columns(columns_num, |columns| { - let first_column_content = |ui: &mut egui::Ui| { - if self.message_slate.is_some() && !empty_fields { - if self.response_edit.is_empty() { - // 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.response_edit.clear(); - self.message_error = None; - self.message_slate = None; - }); - } else { - // Draw button to show Slatepack message as QR code. - let qr_text = format!("{} {}", QR_CODE, t!("qr_code")); - View::button(ui, qr_text, Colors::button(), || { - let text = self.response_edit.clone(); - self.message_edit.clear(); - self.response_edit.clear(); - self.show_qr_message_modal(text); - }); - } - } else { - if self.message_loading { - View::small_loading_spinner(ui); - // Check loading result. - self.check_message_loading_result(wallet); - } else { - // 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 = None; - self.show_qr_message_scan_modal(cb); - }); - } - } - }; - if columns_num == 1 { - columns[0].vertical_centered(first_column_content); - } else { - columns[0].vertical_centered_justified(first_column_content); - columns[1].vertical_centered_justified(|ui| { - if self.message_slate.is_some() && !empty_fields { - if !self.response_edit.is_empty() { - // Draw button to copy response to clipboard. - let copy_text = format!("{} {}", COPY, t!("copy")); - View::button(ui, copy_text, Colors::button(), || { - cb.copy_string_to_buffer(self.response_edit.clone()); - self.message_edit.clear(); - self.response_edit.clear(); - self.message_slate = None; - }); - } else { - show_dandelion = true; - // Draw button to finalize or repost transaction. - View::action_button(ui, t!("wallets.finalize"), || { - let slate = self.message_slate.clone().unwrap(); - self.message_slate = None; - let dandelion = self.dandelion; - let message_edit = self.message_edit.clone(); - let wallet = wallet.clone(); - let result = self.final_post_result.clone(); - - // Finalize or post transaction at separate thread. - self.message_loading = true; - thread::spawn(move || { - let res = if slate.state == SlateState::Invoice3 || - slate.state == SlateState::Standard3 { - wallet.post(&slate, dandelion) - } else { - match wallet.finalize(&message_edit, dandelion) { - Ok(_) => { - Ok(()) - } - Err(e) => { - Err(e) - } - } - }; - let mut w_res = result.write(); - *w_res = Some(res); - }); - }); - } - } else { - // 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); - - // Draw clear button on message input, - // cancel and clear buttons on response - // or button to choose text or image file. - if !self.message_loading { - if self.message_slate.is_none() && !self.message_edit.is_empty() { - // 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.response_edit.clear(); - self.message_error = None; - self.message_slate = None; - }); - } else if !self.response_edit.is_empty() && self.message_slate.is_some() { - // Draw cancel button. - let cancel_text = format!("{} {}", PROHIBIT, t!("modal.cancel")); - View::colored_text_button(ui, cancel_text, Colors::red(), Colors::button(), || { - let slate = self.message_slate.clone().unwrap(); - if let Some(tx) = wallet.tx_by_slate(&slate) { - wallet.cancel(tx.data.id); - self.message_edit.clear(); - self.response_edit.clear(); - self.message_slate = None; - } - }); - } else if self.message_slate.is_none() { - // Draw button to choose file. - let mut parsed_text = "".to_string(); - self.file_pick_button.ui(ui, cb, |text| { - parsed_text = text; - }); - if !parsed_text.is_empty() { - // Parse Slatepack message from file content. - self.message_edit = parsed_text; - self.parse_message(wallet); - } - } - } - }); - - // Draw setup of ability to post transaction with Dandelion. - if show_dandelion { - let dandelion_before = self.dandelion; - View::checkbox(ui, dandelion_before, t!("wallets.use_dandelion"), || { - self.dandelion = !dandelion_before; - wallet.update_use_dandelion(self.dandelion); - }); - } - } - - /// Show QR code Slatepack message [`Modal`]. - pub fn show_qr_message_modal(&mut self, text: String) { - self.qr_message_text = Some(text); - self.qr_message_content.clear_state(); - let slate = self.message_slate.clone().unwrap(); - let title = if slate.state == SlateState::Standard1 { - t!("wallets.receive") - } else { - t!("wallets.send") - }; - Modal::new(QR_SLATEPACK_MESSAGE_MODAL) - .position(ModalPosition::CenterTop) - .title(title) - .show(); - } - - /// Draw QR code Slatepack message image [`Modal`] content. - fn qr_message_modal_ui(&mut self, ui: &mut egui::Ui, m: &Modal, cb: &dyn PlatformCallbacks) { - ui.add_space(6.0); - - // Setup title for Slatepack message. - ui.vertical_centered(|ui| { - let slate = self.message_slate.clone().unwrap(); - let amount = amount_to_hr_string(slate.amount, true); - let title = if slate.state == SlateState::Standard1 { - t!("wallets.parse_s1_slatepack_desc","amount" => amount) - } else { - t!("wallets.parse_i1_slatepack_desc","amount" => amount) - }; - ui.label(RichText::new(title).size(16.0).color(Colors::inactive_text())); - }); - ui.add_space(6.0); - - // Draw QR code content. - let text = self.qr_message_text.clone().unwrap(); - self.qr_message_content.ui(ui, text.clone(), cb); - - ui.vertical_centered_justified(|ui| { - View::button(ui, t!("close"), Colors::white_or_black(false), || { - self.qr_message_text = None; - self.qr_message_content.clear_state(); - self.response_edit.clear(); - self.message_slate = None; - m.close(); - }); - }); - ui.add_space(6.0); - } - - /// 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(QR_SLATEPACK_MESSAGE_SCAN_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); - } - - /// Check Slatepack message request loading result. - fn check_message_loading_result(&mut self, wallet: &Wallet) { - // Check finalize post pay result. - let has_finalize_post_result = { - let r_res = self.final_post_result.read(); - r_res.is_some() - }; - if has_finalize_post_result { - let resp = { - let r_res = self.final_post_result.read(); - r_res.as_ref().unwrap().clone() - }; - if resp.is_ok() { - self.message_edit.clear(); - self.message_slate = None; - } else { - self.message_error = Some( - MessageError::Finalize( - t!("wallets.finalize_slatepack_err") - ) - ); - } - self.message_loading = false; - } - - // Check receive pay result. - let has_receive_pay_result = { - let r_res = self.receive_pay_result.read(); - r_res.is_some() - }; - if has_receive_pay_result { - let (slate, resp) = { - let r_res = self.receive_pay_result.read(); - r_res.as_ref().unwrap().clone() - }; - if resp.is_ok() { - self.response_edit = resp.as_ref().unwrap().clone(); - } else { - let err = resp.as_ref().err().unwrap(); - match err { - // Set already canceled transaction error message. - Error::TransactionWasCancelled {..} - => { - self.message_error = Some( - MessageError::Response( - 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 = Some(MessageError::Response(m)); - } - // Set default error message. - _ => { - self.message_error = Some( - MessageError::Response( - t!("wallets.resp_slatepack_err") - ) - ); - } - } - // Check if tx with same slate id already exists. - if self.message_error.is_none() { - let exists_tx = wallet.tx_by_slate(&slate).is_some(); - if exists_tx { - let mut sl = slate.clone(); - sl.state = if sl.state == SlateState::Standard1 { - SlateState::Standard2 - } else { - SlateState::Invoice2 - }; - match wallet.read_slatepack(&sl) { - None => { - self.message_error = Some( - MessageError::Response( - t!("wallets.resp_slatepack_err") - ) - ); - } - Some(sp) => { - self.response_edit = sp; - } - } - } - } - } - // Setup message slate. - if self.message_error.is_none() { - self.message_slate = Some(slate); - } - // Clear message loading result and status. - { - let mut w_res = self.receive_pay_result.write(); - *w_res = None; - } - self.message_loading = false; - } - } - - /// Parse message input into [`Slate`] updating slate and response input. - pub fn parse_message(&mut self, wallet: &Wallet) { - self.message_slate = None; - self.message_error = None; - if self.message_edit.is_empty() { - return; - } - // Trim message. - self.message_edit = self.message_edit.trim().to_string(); - - // Parse message. - 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::>(); - } - - if slate.amount == 0 { - self.message_error = Some( - MessageError::Response(t!("wallets.resp_slatepack_err")) - ); - return; - } - - // Make operation based on incoming state status. - match slate.state { - SlateState::Standard1 | SlateState::Invoice1 => { - let slate = slate.clone(); - let message = self.message_edit.clone(); - let message_result = self.receive_pay_result.clone(); - let wallet = wallet.clone(); - // Create response to sender or receiver at separate thread. - self.message_loading = true; - thread::spawn(move || { - let resp = if slate.state == SlateState::Standard1 { - wallet.receive(&message) - } else { - wallet.pay(&message) - }; - let mut w_res = message_result.write(); - *w_res = Some((slate, resp)); - }); - return; - } - SlateState::Standard2 | SlateState::Invoice2 => { - // Check if slatepack with same id and state already exists. - let mut sl = slate.clone(); - sl.state = if sl.state == SlateState::Standard2 { - SlateState::Standard1 - } else { - SlateState::Invoice1 - }; - match wallet.read_slatepack(&sl) { - None => { - match wallet.read_slatepack(&slate) { - None => { - self.message_error = Some( - MessageError::Response(t!("wallets.resp_slatepack_err")) - ); - } - Some(sp) => { - self.message_slate = Some(sl); - self.response_edit = sp; - return; - } - } - } - Some(_) => { - self.message_slate = Some(slate.clone()); - return; - } - } - } - _ => { - self.response_edit = "".to_string(); - } - } - self.message_slate = Some(slate); - } else { - self.message_slate = None; - self.message_error = Some(MessageError::Parse(t!("wallets.resp_slatepack_err"))); - } - } -} \ No newline at end of file diff --git a/src/gui/views/wallets/wallet/messages/content.rs b/src/gui/views/wallets/wallet/messages/content.rs new file mode 100644 index 0000000..5c02a60 --- /dev/null +++ b/src/gui/views/wallets/wallet/messages/content.rs @@ -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)>>>, + + /// Wallet transaction [`Modal`] content. + tx_info_content: Option, + + /// Invoice or sending request creation [`Modal`] content. + request_modal_content: Option, + + /// 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) -> 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::>(); + } + + // 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); + } +} \ No newline at end of file diff --git a/src/gui/views/wallets/wallet/messages/mod.rs b/src/gui/views/wallets/wallet/messages/mod.rs new file mode 100644 index 0000000..5199266 --- /dev/null +++ b/src/gui/views/wallets/wallet/messages/mod.rs @@ -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; \ No newline at end of file diff --git a/src/gui/views/wallets/wallet/messages/request.rs b/src/gui/views/wallets/wallet/messages/request.rs new file mode 100644 index 0000000..6517f7b --- /dev/null +++ b/src/gui/views/wallets/wallet/messages/request.rs @@ -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>>>, + /// Flag to check if there is an error happened on request creation. + request_error: Option, + + /// Request result transaction content. + result_tx_content: Option, +} + +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::>(); + 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; + } + } + } + } +} \ No newline at end of file diff --git a/src/gui/views/wallets/wallet/mod.rs b/src/gui/views/wallets/wallet/mod.rs index 0c82d31..34d3af7 100644 --- a/src/gui/views/wallets/wallet/mod.rs +++ b/src/gui/views/wallets/wallet/mod.rs @@ -13,10 +13,12 @@ // limitations under the License. pub mod types; -pub mod settings; + +mod settings; +pub use settings::*; mod txs; -pub use txs::WalletTransactions; +pub use txs::*; mod messages; pub use messages::WalletMessages; @@ -25,4 +27,6 @@ mod transport; pub use transport::WalletTransport; mod content; -pub use content::WalletContent; \ No newline at end of file +pub use content::WalletContent; + +mod modals; \ No newline at end of file diff --git a/src/gui/views/wallets/wallet/modals/accounts.rs b/src/gui/views/wallets/wallet/modals/accounts.rs new file mode 100644 index 0000000..2974214 --- /dev/null +++ b/src/gui/views/wallets/wallet/modals/accounts.rs @@ -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, + /// 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) -> 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); + }); + }); + }); + }); +} \ No newline at end of file diff --git a/src/gui/views/wallets/wallet/modals/mod.rs b/src/gui/views/wallets/wallet/modals/mod.rs new file mode 100644 index 0000000..7a350f7 --- /dev/null +++ b/src/gui/views/wallets/wallet/modals/mod.rs @@ -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::*; \ No newline at end of file diff --git a/src/gui/views/wallets/wallet/modals/scan.rs b/src/gui/views/wallets/wallet/modals/scan.rs new file mode 100644 index 0000000..3ac383c --- /dev/null +++ b/src/gui/views/wallets/wallet/modals/scan.rs @@ -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, + /// QR code scan result + qr_scan_result: Option, +} + +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); + } +} \ No newline at end of file diff --git a/src/gui/views/wallets/wallet/settings/common.rs b/src/gui/views/wallets/wallet/settings/common.rs index e312e70..f60d26e 100644 --- a/src/gui/views/wallets/wallet/settings/common.rs +++ b/src/gui/views/wallets/wallet/settings/common.rs @@ -36,7 +36,7 @@ pub struct CommonSettings { new_pass_edit: String, /// Minimum confirmations number value. - min_confirmations_edit: String + min_confirmations_edit: String, } /// Identifier for wallet name [`Modal`]. @@ -54,25 +54,26 @@ impl Default for CommonSettings { wrong_pass: false, old_pass_edit: "".to_string(), new_pass_edit: "".to_string(), - min_confirmations_edit: "".to_string() + min_confirmations_edit: "".to_string(), } } } impl CommonSettings { + /// Draw common wallet settings content. pub fn ui(&mut self, ui: &mut egui::Ui, wallet: &mut Wallet, cb: &dyn PlatformCallbacks) { // Show modal content for this ui container. self.modal_content_ui(ui, wallet, cb); ui.vertical_centered(|ui| { - let wallet_name = wallet.get_config().name; + let config = wallet.get_config(); // Show wallet name. ui.add_space(2.0); ui.label(RichText::new(t!("wallets.name")) .size(16.0) .color(Colors::gray())); ui.add_space(2.0); - ui.label(RichText::new(wallet_name.clone()) + ui.label(RichText::new(&config.name) .size(16.0) .color(Colors::white_or_black(true))); ui.add_space(8.0); @@ -80,7 +81,7 @@ impl CommonSettings { // Show wallet name setup. let name_text = format!("{} {}", PENCIL, t!("change")); View::button(ui, name_text, Colors::button(), || { - self.name_edit = wallet_name; + self.name_edit = config.name; // Show wallet name modal. Modal::new(NAME_EDIT_MODAL) .position(ModalPosition::CenterTop) @@ -118,10 +119,9 @@ impl CommonSettings { ui.add_space(6.0); // Show minimum amount of confirmations value setup. - let min_confirmations = wallet.get_config().min_confirmations; - let min_conf_text = format!("{} {}", CLOCK_COUNTDOWN, min_confirmations); + let min_conf_text = format!("{} {}", CLOCK_COUNTDOWN, config.min_confirmations); View::button(ui, min_conf_text, Colors::button(), || { - self.min_confirmations_edit = min_confirmations.to_string(); + self.min_confirmations_edit = config.min_confirmations.to_string(); // Show minimum amount of confirmations value modal. Modal::new(MIN_CONFIRMATIONS_EDIT_MODAL) .position(ModalPosition::CenterTop) @@ -131,8 +131,15 @@ impl CommonSettings { }); ui.add_space(12.0); + + // Setup ability to post wallet transactions with Dandelion. + View::checkbox(ui, wallet.can_use_dandelion(), t!("wallets.use_dandelion"), || { + wallet.update_use_dandelion(!wallet.can_use_dandelion()); + }); + + ui.add_space(6.0); View::horizontal_line(ui, Colors::stroke()); - ui.add_space(4.0); + ui.add_space(6.0); }); } diff --git a/src/gui/views/wallets/wallet/settings/content.rs b/src/gui/views/wallets/wallet/settings/content.rs index 085ff9b..4a77572 100644 --- a/src/gui/views/wallets/wallet/settings/content.rs +++ b/src/gui/views/wallets/wallet/settings/content.rs @@ -18,7 +18,7 @@ use egui::scroll_area::ScrollBarVisibility; use crate::gui::Colors; use crate::gui::platform::PlatformCallbacks; use crate::gui::views::{Content, View}; -use crate::gui::views::wallets::settings::{CommonSettings, ConnectionSettings, RecoverySettings}; +use crate::gui::views::wallets::{CommonSettings, ConnectionSettings, RecoverySettings}; use crate::gui::views::wallets::types::{WalletTab, WalletTabType}; use crate::gui::views::wallets::WalletContent; use crate::wallet::Wallet; diff --git a/src/gui/views/wallets/wallet/settings/recovery.rs b/src/gui/views/wallets/wallet/settings/recovery.rs index 969e2cd..686d059 100644 --- a/src/gui/views/wallets/wallet/settings/recovery.rs +++ b/src/gui/views/wallets/wallet/settings/recovery.rs @@ -232,7 +232,7 @@ impl RecoverySettings { }); }); columns[1].vertical_centered_justified(|ui| { - View::button(ui, "OK".to_owned(), Colors::white_or_black(false), || { + let mut on_next = || { match wallet.get_recovery(self.pass_edit.clone()) { Ok(phrase) => { self.wrong_pass = false; @@ -243,6 +243,12 @@ impl RecoverySettings { self.wrong_pass = true; } } + }; + View::on_enter_key(ui, || { + (on_next)(); + }); + View::button(ui, "OK".to_owned(), Colors::white_or_black(false), || { + on_next(); }); }); }); diff --git a/src/gui/views/wallets/wallet/transport.rs b/src/gui/views/wallets/wallet/transport.rs deleted file mode 100644 index e3192cd..0000000 --- a/src/gui/views/wallets/wallet/transport.rs +++ /dev/null @@ -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>, - /// Flag to check if error occurred during sending of transaction over Tor at [`Modal`]. - tor_send_error: Arc>, - /// Flag to check if transaction sent successfully over Tor [`Modal`]. - tor_success: Arc>, - /// 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) { - { - 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::>(); - 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(); - } - } - } -} \ No newline at end of file diff --git a/src/gui/views/wallets/wallet/transport/content.rs b/src/gui/views/wallets/wallet/transport/content.rs new file mode 100644 index 0000000..4876d1d --- /dev/null +++ b/src/gui/views/wallets/wallet/transport/content.rs @@ -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, + + /// QR code address image [`Modal`] content. + qr_address_content: Option, + + /// Tor settings [`Modal`] content. + settings_modal_content: Option, +} + +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) { + 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(); + } +} \ No newline at end of file diff --git a/src/gui/views/wallets/wallet/transport/mod.rs b/src/gui/views/wallets/wallet/transport/mod.rs new file mode 100644 index 0000000..845225a --- /dev/null +++ b/src/gui/views/wallets/wallet/transport/mod.rs @@ -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; \ No newline at end of file diff --git a/src/gui/views/wallets/wallet/transport/send.rs b/src/gui/views/wallets/wallet/transport/send.rs new file mode 100644 index 0000000..76e08b3 --- /dev/null +++ b/src/gui/views/wallets/wallet/transport/send.rs @@ -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>>>, + + /// 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, + + /// Transaction information content. + tx_info_content: Option, +} + +impl TransportSendModal { + /// Create new instance from provided address. + pub fn new(addr: Option) -> 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::>(); + 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; + } + } +} \ No newline at end of file diff --git a/src/gui/views/wallets/wallet/transport/settings.rs b/src/gui/views/wallets/wallet/transport/settings.rs new file mode 100644 index 0000000..377d2f0 --- /dev/null +++ b/src/gui/views/wallets/wallet/transport/settings.rs @@ -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, +} + +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); + } +} \ No newline at end of file diff --git a/src/gui/views/wallets/wallet/txs.rs b/src/gui/views/wallets/wallet/txs.rs deleted file mode 100644 index 1fd7053..0000000 --- a/src/gui/views/wallets/wallet/txs.rs +++ /dev/null @@ -1,1041 +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 std::time::{SystemTime, UNIX_EPOCH}; -use egui::{Align, Id, Layout, Margin, 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::{ARROW_CIRCLE_DOWN, ARROW_CIRCLE_UP, ARROW_CLOCKWISE, BRIDGE, BROOM, CALENDAR_CHECK, CHAT_CIRCLE_TEXT, CHECK, CHECK_CIRCLE, CLIPBOARD_TEXT, COPY, DOTS_THREE_CIRCLE, FILE_ARCHIVE, FILE_TEXT, GEAR_FINE, HASH_STRAIGHT, PROHIBIT, QR_CODE, SCAN, X_CIRCLE}; -use crate::gui::platform::PlatformCallbacks; -use crate::gui::views::{CameraContent, FilePickButton, Modal, PullToRefresh, QrCodeContent, Content, View}; -use crate::gui::views::types::ModalPosition; -use crate::gui::views::wallets::types::WalletTab; -use crate::gui::views::wallets::wallet::types::{GRIN, SLATEPACK_MESSAGE_HINT, WalletTabType}; -use crate::gui::views::wallets::wallet::WalletContent; -use crate::wallet::types::{WalletData, WalletTransaction}; -use crate::wallet::Wallet; - -/// Wallet transactions tab content. -pub struct WalletTransactions { - /// Transaction identifier to use at [`Modal`]. - tx_info_id: Option, - /// Identifier for [`Slate`] to use at [`Modal`]. - tx_info_slate_id: Option, - /// Response Slatepack message input value at [`Modal`]. - tx_info_response_edit: String, - /// Finalization Slatepack message input value at [`Modal`]. - tx_info_finalize_edit: String, - /// Flag to check if error happened during transaction finalization at [`Modal`]. - tx_info_finalize_error: bool, - /// Flag to check if tx finalization requested at [`Modal`]. - tx_info_finalize: bool, - /// Flag to check if tx is finalizing at [`Modal`]. - tx_info_finalizing: bool, - /// Transaction finalization result for [`Modal`]. - tx_info_final_result: Arc>>>, - /// Flag to check if QR code is showing at [`Modal`]. - tx_info_show_qr: bool, - /// QR code Slatepack message image [`Modal`] content. - tx_info_qr_code_content: QrCodeContent, - /// Flag to check if QR code scanner is showing at [`Modal`]. - tx_info_show_scanner: bool, - /// QR code scanner [`Modal`] content. - tx_info_scanner_content: CameraContent, - /// Button to parse picked file content at [`Modal`]. - tx_info_file_pick_button: FilePickButton, - - /// Transaction identifier to use at confirmation [`Modal`]. - confirm_cancel_tx_id: Option, - - /// Flag to check if sync of wallet was initiated manually at time. - manual_sync: Option -} - -impl Default for WalletTransactions { - fn default() -> Self { - Self { - tx_info_id: None, - tx_info_slate_id: None, - tx_info_response_edit: "".to_string(), - tx_info_finalize_edit: "".to_string(), - tx_info_finalize_error: false, - tx_info_finalize: false, - tx_info_finalizing: false, - tx_info_final_result: Arc::new(RwLock::new(None)), - tx_info_show_qr: false, - tx_info_qr_code_content: QrCodeContent::new("".to_string(), true), - tx_info_show_scanner: false, - tx_info_scanner_content: CameraContent::default(), - tx_info_file_pick_button: FilePickButton::default(), - 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"; - -/// Height of transaction list item. -const TX_ITEM_HEIGHT: f32 = 76.0; - -impl WalletTransactions { - /// 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, 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 r = View::item_rounding(index, txs.len(), false); - self.tx_item_ui(ui, tx, r, padding, true, &data, wallet, cb); - } - }); - }) - }); - - // Sync wallet on refresh. - if refresh_resp.should_refresh() { - self.manual_sync = Some(now); - if !wallet.syncing() { - wallet.sync(true); - } - } - } - - /// 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| { - self.tx_info_modal_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. - fn tx_item_ui(&mut self, - ui: &mut egui::Ui, - tx: &WalletTransaction, - mut rounding: Rounding, - extra_padding: bool, - can_show_info: bool, - data: &WalletData, - wallet: &mut Wallet, - cb: &dyn PlatformCallbacks) { - // Setup layout size. - let mut rect = ui.available_rect_before_wrap(); - if extra_padding { - rect.min += egui::emath::vec2(6.0, 0.0); - rect.max -= egui::emath::vec2(6.0, 0.0); - } - rect.set_height(TX_ITEM_HEIGHT); - - // Draw round background. - let bg_rect = rect.clone(); - let color = if can_show_info { - Colors::button() - } else { - Colors::fill() - }; - ui.painter().rect(bg_rect, rounding, color, View::item_stroke()); - - ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Max), |ui| { - ui.horizontal_centered(|ui| { - // Draw button to show transaction info. - if can_show_info && tx.data.tx_slate_id.is_some() { - rounding.nw = 0.0; - rounding.sw = 0.0; - View::item_button(ui, rounding, FILE_TEXT, None, || { - self.tx_info_finalize = false; - self.show_tx_info_modal(wallet, tx); - }); - } - - // Draw finalization button for tx that can be finalized. - let finalize = ((!can_show_info && !self.tx_info_finalizing) || can_show_info) - && tx.can_finalize; - if finalize { - let (icon, color) = if !can_show_info && self.tx_info_finalize { - (FILE_TEXT, None) - } else { - (CHECK, Some(Colors::green())) - }; - let final_rounding = if can_show_info { - Rounding::default() - } else { - rounding.nw = 0.0; - rounding.sw = 0.0; - rounding - }; - View::item_button(ui, final_rounding, icon, color, || { - cb.hide_keyboard(); - if !can_show_info && self.tx_info_finalize { - self.tx_info_finalize = false; - return; - } - self.tx_info_finalize = true; - // Show transaction information modal. - if can_show_info { - self.show_tx_info_modal(wallet, tx); - } - }); - } - - // Draw cancel button for tx that can be reposted and canceled. - let wallet_loaded = wallet.foreign_api_port().is_some(); - if wallet_loaded && ((!can_show_info && !self.tx_info_finalizing) || can_show_info) && - (tx.can_repost(data) || tx.can_cancel()) { - View::item_button(ui, Rounding::default(), PROHIBIT, Some(Colors::red()), || { - if can_show_info { - 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(); - } else { - cb.hide_keyboard(); - wallet.cancel(tx.data.id); - } - }); - } - - // Draw button to repost transaction. - if ((!can_show_info && !self.tx_info_finalizing) || can_show_info) && - tx.can_repost(data) { - let r = if finalize || can_show_info { - Rounding::default() - } else { - rounding.nw = 0.0; - rounding.sw = 0.0; - rounding - }; - View::item_button(ui, r, ARROW_CLOCKWISE, Some(Colors::green()), || { - cb.hide_keyboard(); - // Post tx after getting slate from slatepack file. - if let Some((s, _)) = wallet.read_slate_by_tx(tx) { - let _ = wallet.post(&s, wallet.can_use_dandelion()); - } - }); - } - }); - - 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.posting { - 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) { - self.tx_info_response_edit = "".to_string(); - self.tx_info_finalize_edit = "".to_string(); - self.tx_info_finalize_error = false; - self.tx_info_id = Some(tx.data.id); - self.tx_info_show_qr = false; - self.tx_info_slate_id = if let Some(id) = tx.data.tx_slate_id { - Some(id.to_string()) - } else { - None - }; - - // Setup slate and message from transaction. - self.tx_info_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() - }; - - // Show transaction information modal. - Modal::new(TX_INFO_MODAL) - .position(ModalPosition::CenterTop) - .title(t!("wallets.tx")) - .show(); - } - - /// Draw transaction info [`Modal`] content. - fn tx_info_modal_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 tx_id = self.tx_info_id.unwrap(); - let data_txs = data.txs.clone().unwrap(); - let txs = data_txs.into_iter() - .filter(|tx| tx.data.id == tx_id) - .collect::>(); - if txs.is_empty() { - cb.hide_keyboard(); - modal.close(); - return; - } - let tx = txs.get(0).unwrap(); - - if !self.tx_info_show_qr && !self.tx_info_show_scanner { - ui.add_space(6.0); - - // Show transaction amount status and time. - let rounding = View::item_rounding(0, 2, false); - self.tx_item_ui(ui, tx, rounding, false, false, &data, wallet, cb); - - // Show transaction ID info. - if let Some(id) = tx.data.tx_slate_id { - let label = format!("{} {}", HASH_STRAIGHT, t!("id")); - Self::tx_info_modal_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::tx_info_modal_item_ui(ui, kernel.0.to_hex(), label, true, cb); - } - } - - // Show Slatepack message or reset flag to show QR if not available. - if !tx.posting && !tx.data.confirmed && !tx.cancelling && - (tx.data.tx_type == TxLogEntryType::TxSent || - tx.data.tx_type == TxLogEntryType::TxReceived) { - self.tx_info_modal_slate_ui(ui, tx, wallet, modal, cb); - } else if self.tx_info_show_qr { - self.tx_info_qr_code_content.clear_state(); - self.tx_info_show_qr = false; - } - - if !self.tx_info_finalizing { - // Setup spacing between buttons. - ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); - - if self.tx_info_show_qr { - // 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.tx_info_qr_code_content.clear_state(); - self.tx_info_show_qr = false; - modal.close(); - }); - }); - cols[1].vertical_centered_justified(|ui| { - View::button(ui, t!("back"), Colors::white_or_black(false), || { - self.tx_info_qr_code_content.clear_state(); - self.tx_info_show_qr = false; - }); - }); - }); - } else if self.tx_info_show_scanner { - 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.tx_info_scanner_content.clear_state(); - self.tx_info_show_scanner = false; - modal.close(); - }); - }); - cols[1].vertical_centered_justified(|ui| { - View::button(ui, t!("back"), Colors::white_or_black(false), || { - cb.stop_camera(); - self.tx_info_scanner_content.clear_state(); - self.tx_info_show_scanner = false; - 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), || { - self.tx_info_id = None; - self.tx_info_finalize = 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.tx_info_final_result.read(); - r_res.is_some() - }; - if has_res { - let res = { - let r_res = self.tx_info_final_result.read(); - r_res.as_ref().unwrap().clone() - }; - if let Ok(_) = res { - self.tx_info_finalize = false; - self.tx_info_finalize_edit = "".to_string(); - } else { - self.tx_info_finalize_error = true; - } - // Clear status and result. - { - let mut w_res = self.tx_info_final_result.write(); - *w_res = None; - } - self.tx_info_finalizing = false; - modal.enable_closing(); - } - } - } - - /// Draw transaction information [`Modal`] item content. - fn tx_info_modal_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 Slate content to show response or generate payment proof. - fn tx_info_modal_slate_ui(&mut self, - ui: &mut egui::Ui, - tx: &WalletTransaction, - wallet: &Wallet, - modal: &Modal, - cb: &dyn PlatformCallbacks) { - if self.tx_info_slate_id.is_none() { - cb.hide_keyboard(); - modal.close(); - return; - } - ui.add_space(6.0); - - // Draw QR code scanner content if requested. - if self.tx_info_show_scanner { - if let Some(result) = self.tx_info_scanner_content.qr_scan_result() { - cb.stop_camera(); - self.tx_info_scanner_content.clear_state(); - - // Setup value to finalization input field. - self.tx_info_finalize_edit = result.text(); - self.on_finalization_input_change(tx, wallet, modal, cb); - - modal.enable_closing(); - self.tx_info_scanner_content.clear_state(); - self.tx_info_show_scanner = false; - } else { - self.tx_info_scanner_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.tx_info_finalize { - let desc_text = if self.tx_info_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.tx_info_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.tx_info_finalize { - &mut self.tx_info_finalize_edit - } else { - &mut self.tx_info_response_edit - }; - let message_before = message_edit.clone(); - - // Draw QR code content if requested. - if self.tx_info_show_qr { - let text = message_edit.clone(); - if text.is_empty() { - self.tx_info_qr_code_content.clear_state(); - self.tx_info_show_qr = false; - } else { - // Draw QR code content. - self.tx_info_qr_code_content.ui(ui, text.clone(), cb); - return; - } - } - - // Draw Slatepack message finalization input or request text. - ui.vertical_centered(|ui| { - let scroll_id = if self.tx_info_finalize { - Id::from("tx_info_message_finalize") - } else { - Id::from("tx_info_message_request") - }.with(self.tx_info_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.tx_info_finalize && !self.tx_info_finalizing) - .hint_text(SLATEPACK_MESSAGE_HINT) - .desired_width(f32::INFINITY) - .show(ui).response; - // Show soft keyboard on click. - if self.tx_info_finalize && resp.clicked() { - resp.request_focus(); - cb.show_keyboard(); - } - if self.tx_info_finalize && 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.tx_info_finalizing { - return; - } - - // Setup spacing between buttons. - ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); - - if self.tx_info_finalize { - 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.tx_info_show_scanner = true; - }); - }); - 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.tx_info_finalize_edit = cb.get_string_from_buffer(); - }); - }); - }); - ui.add_space(8.0); - ui.vertical_centered(|ui| { - if self.tx_info_finalize_error { - // Draw button to clear message input. - let clear_text = format!("{} {}", BROOM, t!("clear")); - View::button(ui, clear_text, Colors::button(), || { - self.tx_info_finalize_edit.clear(); - self.tx_info_finalize_error = false; - }); - } else { - // Draw button to choose file. - self.tx_info_file_pick_button.ui(ui, cb, |text| { - self.tx_info_finalize_edit = text; - }); - } - }); - - // Callback on finalization message input change. - if message_before != self.tx_info_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, Colors::button(), || { - cb.hide_keyboard(); - self.tx_info_show_qr = 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.tx_info_response_edit.clone()); - self.tx_info_finalize_edit = "".to_string(); - if tx.can_finalize { - self.tx_info_finalize = true; - } else { - cb.hide_keyboard(); - modal.close(); - } - }); - }); - }); - } - } - - /// 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.tx_info_finalize_edit; - if message.is_empty() { - self.tx_info_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.tx_info_final_result.clone(); - // Finalize transaction at separate thread. - cb.hide_keyboard(); - self.tx_info_finalizing = true; - modal.disable_closing(); - thread::spawn(move || { - let res = wallet.finalize(&message, wallet.can_use_dandelion()); - let mut w_res = final_res.write(); - *w_res = Some(res); - }); - } else { - self.tx_info_finalize_error = true; - } - } else { - self.tx_info_finalize_error = true; - } - } - } - - /// 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::>(); - 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); - }); - } -} \ No newline at end of file diff --git a/src/gui/views/wallets/wallet/txs/content.rs b/src/gui/views/wallets/wallet/txs/content.rs new file mode 100644 index 0000000..e7a63dd --- /dev/null +++ b/src/gui/views/wallets/wallet/txs/content.rs @@ -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, + + /// Transaction identifier to use at confirmation [`Modal`]. + confirm_cancel_tx_id: Option, + + /// Flag to check if sync of wallet was initiated manually at time. + manual_sync: Option +} + +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::>(); + 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); + }); + } +} \ No newline at end of file diff --git a/src/gui/views/wallets/wallet/txs/mod.rs b/src/gui/views/wallets/wallet/txs/mod.rs new file mode 100644 index 0000000..e283c72 --- /dev/null +++ b/src/gui/views/wallets/wallet/txs/mod.rs @@ -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::*; \ No newline at end of file diff --git a/src/gui/views/wallets/wallet/txs/tx.rs b/src/gui/views/wallets/wallet/txs/tx.rs new file mode 100644 index 0000000..cfc4ae7 --- /dev/null +++ b/src/gui/views/wallets/wallet/txs/tx.rs @@ -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, + + /// 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>>>, + + /// QR code Slatepack message image content. + qr_code_content: Option, + + /// QR code scanner content. + qr_scan_content: Option, + + /// 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::>(); + 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; + } + } + } +} \ No newline at end of file diff --git a/src/gui/views/wallets/wallet/types.rs b/src/gui/views/wallets/wallet/types.rs index e9f5a4d..750fc02 100644 --- a/src/gui/views/wallets/wallet/types.rs +++ b/src/gui/views/wallets/wallet/types.rs @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +use crate::gui::icons::{FOLDER_LOCK, FOLDER_OPEN, SPINNER, WARNING_CIRCLE}; use crate::gui::platform::PlatformCallbacks; use crate::wallet::Wallet; @@ -48,4 +49,39 @@ impl WalletTabType { WalletTabType::Settings => t!("wallets.settings") } } +} + +/// Get wallet status text. +pub fn status_text(wallet: &Wallet) -> String { + if wallet.is_open() { + if wallet.sync_error() { + format!("{} {}", WARNING_CIRCLE, t!("error")) + } else if wallet.is_closing() { + format!("{} {}", SPINNER, t!("wallets.closing")) + } else if wallet.is_repairing() { + let repair_progress = wallet.repairing_progress(); + if repair_progress == 0 { + format!("{} {}", SPINNER, t!("wallets.checking")) + } else { + format!("{} {}: {}%", + SPINNER, + t!("wallets.checking"), + repair_progress) + } + } else if wallet.syncing() { + let info_progress = wallet.info_sync_progress(); + if info_progress == 100 || info_progress == 0 { + format!("{} {}", SPINNER, t!("wallets.loading")) + } else { + format!("{} {}: {}%", + SPINNER, + t!("wallets.loading"), + info_progress) + } + } else { + format!("{} {}", FOLDER_OPEN, t!("wallets.unlocked")) + } + } else { + format!("{} {}", FOLDER_LOCK, t!("wallets.locked")) + } } \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 54ac58b..a6d8840 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -17,6 +17,9 @@ extern crate rust_i18n; use eframe::NativeOptions; use egui::{Context, Stroke}; +use lazy_static::lazy_static; +use std::sync::Arc; +use parking_lot::RwLock; #[cfg(target_os = "android")] use winit::platform::android::activity::AndroidApp; @@ -255,4 +258,55 @@ fn setup_i18n() { rust_i18n::set_locale(AppConfig::DEFAULT_LOCALE); } } +} + +/// Get data from deeplink or opened file. +pub fn consume_incoming_data() -> Option { + 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>> = 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(_) => {} + } + }; + } } \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index f50480e..a51b407 100644 --- a/src/main.rs +++ b/src/main.rs @@ -29,6 +29,18 @@ fn real_main() { .parse_default_env() .init(); + // Handle file path argument passing. + let args: Vec<_> = std::env::args().collect(); + let mut data = None; + if args.len() > 1 { + let path = std::path::PathBuf::from(&args[1]); + let content = match std::fs::read_to_string(path) { + Ok(s) => Some(s), + Err(_) => Some(args[1].clone()) + }; + data = content + } + // Setup callback on panic crash. std::panic::set_hook(Box::new(|info| { let backtrace = backtrace::Backtrace::new(); @@ -41,7 +53,7 @@ fn real_main() { // Save backtrace to file. let log = grim::Settings::crash_report_path(); if log.exists() { - std::fs::remove_file(log.clone()).unwrap(); + let _ = std::fs::remove_file(log.clone()); } std::fs::write(log, err.as_bytes()).unwrap(); // Setup flag to show crash after app restart. @@ -49,20 +61,28 @@ fn real_main() { })); // Start GUI. - let _ = std::panic::catch_unwind(|| { - start_desktop_gui(); - }); + match std::panic::catch_unwind(|| { + if is_app_running(&data) { + return; + } else if let Some(data) = data { + grim::on_data(data); + } + let platform = grim::gui::platform::Desktop::new(); + start_app_socket(platform.clone()); + start_desktop_gui(platform); + }) { + Ok(_) => {} + Err(e) => println!("{:?}", e) + } } -/// Start GUI with Desktop related setup. +/// Start GUI with Desktop related setup passing data from opening. #[allow(dead_code)] #[cfg(not(target_os = "android"))] -fn start_desktop_gui() { +fn start_desktop_gui(platform: grim::gui::platform::Desktop) { use grim::AppConfig; use dark_light::Mode; - let platform = grim::gui::platform::Desktop::default(); - // Setup system theme if not set. if let None = AppConfig::dark_theme() { let dark = match dark_light::detect() { @@ -73,12 +93,11 @@ fn start_desktop_gui() { AppConfig::set_dark_theme(dark); } - // Setup window size. let (width, height) = AppConfig::window_size(); - let mut viewport = egui::ViewportBuilder::default() .with_min_inner_size([AppConfig::MIN_WIDTH, AppConfig::MIN_HEIGHT]) .with_inner_size([width, height]); + // Setup an icon. if let Ok(icon) = eframe::icon_data::from_png_bytes(include_bytes!("../img/icon.png")) { viewport = viewport.with_icon(std::sync::Arc::new(icon)); @@ -90,6 +109,7 @@ fn start_desktop_gui() { // Setup window decorations. let is_mac = egui::os::OperatingSystem::from_target_os() == egui::os::OperatingSystem::Mac; viewport = viewport + .with_window_level(egui::WindowLevel::Normal) .with_fullsize_content_view(true) .with_title_shown(false) .with_titlebar_buttons_shown(false) @@ -110,7 +130,8 @@ fn start_desktop_gui() { }; // Start GUI. - match grim::start(options.clone(), grim::app_creator(grim::gui::App::new(platform.clone()))) { + let app = grim::gui::App::new(platform.clone()); + match grim::start(options.clone(), grim::app_creator(app)) { Ok(_) => {} Err(e) => { if win { @@ -118,7 +139,9 @@ fn start_desktop_gui() { } // Start with another renderer on error. options.renderer = eframe::Renderer::Glow; - match grim::start(options, grim::app_creator(grim::gui::App::new(platform))) { + + let app = grim::gui::App::new(platform); + match grim::start(options, grim::app_creator(app)) { Ok(_) => {} Err(e) => { panic!("{}", e); @@ -126,4 +149,117 @@ fn start_desktop_gui() { } } } +} + +/// Check if application is already running to pass data. +#[allow(dead_code)] +#[cfg(not(target_os = "android"))] +fn is_app_running(data: &Option) -> bool { + use tor_rtcompat::BlockOn; + let runtime = tor_rtcompat::tokio::TokioNativeTlsRuntime::create().unwrap(); + let res: Result<(), Box> = 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::()? + } else { + socket_path.clone().to_fs_name::()? + }; + // 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 { + 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::()? + } else { + socket_path.clone().to_fs_name::()? + }; + 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::(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(_) => {} + } + } + }); + }); } \ No newline at end of file diff --git a/src/settings/settings.rs b/src/settings/settings.rs index 0f7e947..c065f59 100644 --- a/src/settings/settings.rs +++ b/src/settings/settings.rs @@ -48,9 +48,10 @@ pub struct Settings { impl Settings { /// Main application directory name. pub const MAIN_DIR_NAME: &'static str = ".grim"; - /// Crash report file name. pub const CRASH_REPORT_FILE_NAME: &'static str = "crash.log"; + /// Application socket name. + pub const SOCKET_NAME: &'static str = "grim.sock"; /// Initialize settings with app and node configs. fn init() -> Self { @@ -141,6 +142,13 @@ impl Settings { path } + /// Get desktop application socket path. + pub fn socket_path() -> PathBuf { + let mut socket_path = Self::base_path(None); + socket_path.push(Self::SOCKET_NAME); + socket_path + } + /// Get configuration file path from provided name and sub-directory if needed. pub fn config_path(config_name: &str, sub_dir: Option) -> PathBuf { let mut path = Self::base_path(sub_dir); diff --git a/src/wallet/list.rs b/src/wallet/list.rs index 2298cc3..598fa3b 100644 --- a/src/wallet/list.rs +++ b/src/wallet/list.rs @@ -18,7 +18,8 @@ use grin_wallet_libwallet::Error; use crate::AppConfig; use crate::wallet::{Wallet, WalletConfig}; -/// Wrapper for [`Wallet`] list. +/// [`Wallet`] list container. +#[derive(Clone)] pub struct WalletList { /// List of wallets for [`ChainTypes::Mainnet`]. pub main_list: Vec, diff --git a/src/wallet/types.rs b/src/wallet/types.rs index 49885c0..067f1a6 100644 --- a/src/wallet/types.rs +++ b/src/wallet/types.rs @@ -158,14 +158,12 @@ pub struct WalletTransaction { pub amount: u64, /// Flag to check if transaction is cancelling. pub cancelling: bool, - /// Flag to check if transaction is posting after finalization. - pub posting: bool, /// Flag to check if transaction can be finalized based on Slatepack message state. pub can_finalize: bool, + /// Flag to check if transaction is finalizing. + pub finalizing: bool, /// Block height when tx was confirmed. pub conf_height: Option, - /// Block height when tx was reposted. - pub repost_height: Option, /// Flag to check if tx was received after sync from node. pub from_node: bool, } @@ -173,16 +171,8 @@ pub struct WalletTransaction { impl WalletTransaction { /// Check if transaction can be cancelled. pub fn can_cancel(&self) -> bool { - self.from_node && !self.cancelling && !self.posting && !self.data.confirmed && + self.from_node && !self.cancelling && !self.data.confirmed && self.data.tx_type != TxLogEntryType::TxReceivedCancelled && self.data.tx_type != TxLogEntryType::TxSentCancelled } - - /// Check if transaction can be reposted. - pub fn can_repost(&self, data: &WalletData) -> bool { - let last_height = data.info.last_confirmed_height; - let min_conf = data.info.minimum_confirmations; - self.from_node && self.posting && self.repost_height.is_some() && - last_height - self.repost_height.unwrap() > min_conf - } } \ No newline at end of file diff --git a/src/wallet/wallet.rs b/src/wallet/wallet.rs index 99d313e..56c1171 100644 --- a/src/wallet/wallet.rs +++ b/src/wallet/wallet.rs @@ -437,8 +437,8 @@ impl Wallet { // Mark wallet as not opened. wallet_close.closing.store(false, Ordering::Relaxed); wallet_close.is_open.store(false, Ordering::Relaxed); - // Wake up thread to exit. - wallet_close.sync(true); + // Start sync to exit from thread. + wallet_close.sync(); }); } @@ -464,14 +464,14 @@ impl Wallet { }); } - // Sync wallet data. - self.sync(false); + // Refresh wallet info. + sync_wallet_data(&self, false); Ok(()) }) } /// 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); controller::owner_single_use(None, None, Some(&mut api), |api, m| { api.set_active_account(m, label)?; @@ -498,7 +498,7 @@ impl Wallet { self.info_sync_progress.store(0, Ordering::Relaxed); // Sync wallet data. - self.sync(false); + self.sync(); Ok(()) } @@ -555,18 +555,11 @@ impl Wallet { r_data.clone() } - /// Sync wallet data from node or locally. - pub fn sync(&self, from_node: bool) { - if from_node { - let thread_r = self.sync_thread.read(); - if let Some(thread) = thread_r.as_ref() { - thread.unpark(); - } - } else { - let wallet = self.clone(); - thread::spawn(move || { - sync_wallet_data(&wallet, false); - }); + /// Sync wallet data from node at sync thread or locally synchronously. + pub fn sync(&self) { + let thread_r = self.sync_thread.read(); + if let Some(thread) = thread_r.as_ref() { + thread.unpark(); } } @@ -625,13 +618,7 @@ impl Wallet { let mut slate = None; if let Some(slate_id) = tx.data.tx_slate_id { // Get slate state based on tx state and status. - let state = if tx.posting { - 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 || + let state = if !tx.data.confirmed && (tx.data.tx_type == TxLogEntryType::TxSent || tx.data.tx_type == TxLogEntryType::TxReceived) { if tx.can_finalize { if tx.data.tx_type == TxLogEntryType::TxSent { @@ -681,7 +668,7 @@ impl Wallet { } /// 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 { let config = self.get_config(); let args = InitTxArgs { src_acct_name: Some(config.account), @@ -698,51 +685,35 @@ impl Wallet { api.tx_lock_outputs(None, &slate)?; // Create Slatepack message response. - let message_resp = self.create_slatepack_message(&slate)?; + let _ = self.create_slatepack_message(&slate)?; - // Sync wallet info. - self.sync(false); + // Refresh wallet info. + 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. - pub async fn send_tor(&mut self, amount: u64, addr: &SlatepackAddress) -> Option { + pub async fn send_tor(&mut self, + amount: u64, + addr: &SlatepackAddress) -> Result { // Initialize transaction. - let send_res = self.send(amount); - - if send_res.is_err() { - return None; + let tx = self.send(amount)?; + let slate_res = self.read_slate_by_tx(&tx); + if slate_res.is_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. let cancel_tx = || { let instance = self.instance.clone().unwrap(); let id = slate.clone().id; 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::>(); - data.txs = Some(txs); - *w_data = Some(data); - } + // Refresh wallet info to update statuses. - self.sync(false); + sync_wallet_data(&self, false); }; // Initialize parameters. @@ -764,17 +735,15 @@ impl Wallet { let req_res = Tor::post(body, url).await; if req_res.is_none() { 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(); if res["error"] != json!(null) { cancel_tx(); - return None; + return Err(Error::GenericError("Tx error".to_string())); } - - // Slatepack message json value. let slate_value = res["result"]["Ok"].clone(); let mut ret_slate = None; @@ -788,7 +757,7 @@ impl Wallet { // Save Slatepack message to file. let _ = self.create_slatepack_message(&slate).unwrap_or("".to_string()); // Post transaction to blockchain. - let result = self.post(&slate, self.can_use_dandelion()); + let result = self.post(&slate); match result { Ok(_) => { Ok(()) @@ -798,21 +767,25 @@ impl Wallet { } } } else { - Err(Error::GenericError("TX finalization error".to_string())) + Err(Error::GenericError("Tx finalization error".to_string())) }; - }).unwrap(); + })?; } Err(_) => {} }; + // Cancel transaction on error. if ret_slate.is_none() { 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. - pub fn issue_invoice(&self, amount: u64) -> Result<(Slate, String), Error> { + pub fn issue_invoice(&self, amount: u64) -> Result { let args = IssueInvoiceTxArgs { dest_acct_name: None, amount, @@ -822,16 +795,17 @@ impl Wallet { let slate = api.issue_invoice_tx(None, args)?; // Create Slatepack message response. - let response = self.create_slatepack_message(&slate.clone())?; + let _ = self.create_slatepack_message(&slate)?; - // Sync wallet info. - self.sync(false); + // Refresh wallet info. + 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. - pub fn pay(&self, message: &String) -> Result { + pub fn pay(&self, message: &String) -> Result { if let Ok(slate) = self.parse_slatepack(message) { let config = self.get_config(); let args = InitTxArgs { @@ -846,19 +820,19 @@ impl Wallet { api.tx_lock_outputs(None, &slate)?; // Create Slatepack message response. - let response = self.create_slatepack_message(&slate)?; + let _ = self.create_slatepack_message(&slate)?; - // Sync wallet info. - self.sync(false); + // Refresh wallet info. + sync_wallet_data(&self, false); - Ok(response) + Ok(self.tx_by_slate(&slate).ok_or(Error::GenericError("No tx found".to_string()))?) } else { Err(Error::SlatepackDeser("Slatepack parsing error".to_string())) } } /// Handle message to receive funds, return response to sender. - pub fn receive(&self, message: &String) -> Result { + pub fn receive(&self, message: &String) -> Result { if let Ok(mut slate) = self.parse_slatepack(message) { let api = Owner::new(self.instance.clone().unwrap(), None); controller::foreign_single_use(api.wallet_inst.clone(), None, |api| { @@ -866,61 +840,47 @@ impl Wallet { Ok(()) })?; // Create Slatepack message response. - let response = self.create_slatepack_message(&slate)?; + let _ = self.create_slatepack_message(&slate)?; - // Sync wallet info. - self.sync(false); + // Refresh wallet info. + sync_wallet_data(&self, false); - Ok(response) + Ok(self.tx_by_slate(&slate).ok_or(Error::GenericError("No tx found".to_string()))?) } else { Err(Error::SlatepackDeser("Slatepack parsing error".to_string())) } } /// Finalize transaction from provided message as sender or invoice issuer with Dandelion. - pub fn finalize(&self, message: &String, dandelion: bool) -> Result { + pub fn finalize(&self, message: &String) -> Result { if let Ok(mut slate) = self.parse_slatepack(message) { let api = Owner::new(self.instance.clone().unwrap(), None); slate = api.finalize_tx(None, &slate)?; // Save Slatepack message to file. let _ = self.create_slatepack_message(&slate)?; + // Post transaction to blockchain. - let _ = self.post(&slate, dandelion); - Ok(slate) + let tx = self.post(&slate)?; + + // Refresh wallet info. + sync_wallet_data(&self, false); + + Ok(tx) } else { Err(Error::SlatepackDeser("Slatepack parsing error".to_string())) } } /// Post transaction to blockchain. - pub fn post(&self, slate: &Slate, dandelion: bool) -> Result<(), Error> { + fn post(&self, slate: &Slate) -> Result { // Post transaction to blockchain. let api = Owner::new(self.instance.clone().unwrap(), None); - api.post_tx(None, slate, dandelion)?; - // Setup transaction repost height, posting flag and ability to finalize. - let mut slate = slate.clone(); - if slate.state == SlateState::Invoice2 { - slate.state = SlateState::Invoice3 - } else if slate.state == SlateState::Standard2 { - 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(()) + api.post_tx(None, slate, self.can_use_dandelion())?; + + // Refresh wallet info. + sync_wallet_data(&self, false); + + Ok(self.tx_by_slate(&slate).ok_or(Error::GenericError("No tx found".to_string()))?) } /// Cancel transaction. @@ -948,27 +908,7 @@ impl Wallet { } let instance = wallet.instance.clone().unwrap(); 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::>(); - data.txs = Some(txs); - *w_data = Some(data); - } + // Refresh wallet info to update statuses. sync_wallet_data(&wallet, false); }); @@ -985,7 +925,7 @@ impl Wallet { /// Initiate wallet repair by scanning its outputs. pub fn repair(&self) { self.repair_needed.store(true, Ordering::Relaxed); - self.sync(true); + self.sync(); } /// Check if wallet is repairing. @@ -1013,7 +953,7 @@ impl Wallet { // Remove wallet db files. let _ = fs::remove_dir_all(wallet_delete.get_config().get_db_path()); // Start sync to close thread. - wallet_delete.sync(true); + wallet_delete.sync(); // Mark wallet to reopen. wallet_delete.set_reopen(reopen); }); @@ -1046,7 +986,7 @@ impl Wallet { // Mark wallet as deleted. wallet_delete.deleted.store(true, Ordering::Relaxed); // 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(); // 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() { Ok(key) => { tx.parent_key_id == key @@ -1286,7 +1226,7 @@ fn sync_wallet_data(wallet: &Wallet, from_node: bool) { // Create wallet txs. let mut new_txs: Vec = vec![]; - for tx in &filter_txs { + for tx in &account_txs { // Setup transaction amount. let amount = if tx.amount_debited > tx.amount_credited { tx.amount_debited - tx.amount_credited @@ -1294,54 +1234,36 @@ fn sync_wallet_data(wallet: &Wallet, from_node: bool) { tx.amount_credited - tx.amount_debited }; + // Setup flag for ability to finalize transaction. let unconfirmed_sent_or_received = tx.tx_slate_id.is_some() && !tx.confirmed && (tx.tx_type == TxLogEntryType::TxSent || tx.tx_type == TxLogEntryType::TxReceived); - - // Setup transaction posting status based on slate state. - let posting = if unconfirmed_sent_or_received { - // 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 + let mut finalizing = false; + let can_finalize = if unconfirmed_sent_or_received { + let initial_state = { + let mut slate = Slate::blank(1, false); + slate.id = tx.tx_slate_id.unwrap(); + slate.state = match tx.tx_type { + TxLogEntryType::TxReceived => SlateState::Invoice1, + _ => SlateState::Standard1 + }; + wallet.read_slatepack(&slate).is_some() }; - - // 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() + 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 { 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); - slate.id = tx.tx_slate_id.unwrap(); - slate.state = match tx.tx_type { - TxLogEntryType::TxReceived => SlateState::Invoice1, - _ => SlateState::Standard1 - }; - wallet.read_slatepack(&slate).is_some() - } else { - false - }; - - // Setup confirmation, reposting height and cancelling status. + // Setup confirmation and cancelling status. let mut conf_height = None; let mut setup_conf_height = |t: &TxLogEntry, current_empty: bool| -> bool { if current_empty && t.kernel_lookup_min_height.is_some() && @@ -1376,7 +1298,6 @@ fn sync_wallet_data(wallet: &Wallet, from_node: bool) { false }; - let mut repost_height = None; let mut cancelling = false; if data_txs.is_empty() { setup_conf_height(tx, true); @@ -1387,7 +1308,6 @@ fn sync_wallet_data(wallet: &Wallet, from_node: bool) { t.conf_height.unwrap() == 0) { conf_height = t.conf_height; } - repost_height = t.repost_height; if t.cancelling && tx.tx_type != TxLogEntryType::TxReceivedCancelled && tx.tx_type != TxLogEntryType::TxSentCancelled { @@ -1403,10 +1323,9 @@ fn sync_wallet_data(wallet: &Wallet, from_node: bool) { data: tx.clone(), amount, cancelling, - posting, can_finalize, + finalizing, conf_height, - repost_height, from_node: !fresh_sync || from_node }); } diff --git a/wix/main.wxs b/wix/main.wxs index 2735064..ef83099 100644 --- a/wix/main.wxs +++ b/wix/main.wxs @@ -16,8 +16,8 @@ AllowSameVersionUpgrades = "yes" /> - - + + @@ -28,7 +28,7 @@ @@ -55,6 +55,12 @@ + + + + + + @@ -64,7 +70,7 @@