Compare commits
53 commits
Author | SHA1 | Date | |
---|---|---|---|
1d9b7d9698 | |||
82c05588bc | |||
1cddd05bc0 | |||
8ad0d1c461 | |||
a22a75913c | |||
e797da0ed8 | |||
6936c14ed2 | |||
c626ed5a48 | |||
d79d05ef5a | |||
|
094a5b8969 | ||
|
12a75f8370 | ||
|
1c14b9aa93 | ||
|
8ea388554a | ||
|
1531c201bb | ||
|
ed522c56ae | ||
|
4b454ab2f3 | ||
|
f6fbf7226e | ||
|
ebd09ab1c8 | ||
|
75cf7edc96 | ||
|
5c8b9c40be | ||
|
dcaf9945c8 | ||
|
f9426287d5 | ||
|
77281e3ab9 | ||
|
64439ad3d3 | ||
|
9494c1292e | ||
|
accf123d49 | ||
|
d77598c259 | ||
|
4e6dff52fe | ||
|
92d0aac250 | ||
|
5ef310558a | ||
|
683821b667 | ||
|
da4cf71fac | ||
|
f81ceae940 | ||
|
fa6301a1db | ||
|
442fc425f7 | ||
|
ea61588ede | ||
|
7f67aa134a | ||
|
d7d1c53c52 | ||
|
18f52f877a | ||
|
c13195bd61 | ||
|
e40d5b6474 | ||
|
92e5d38755 | ||
|
ec7e795ba9 | ||
|
af220b2a09 | ||
|
846e30cb38 | ||
|
d371d4368b | ||
|
85fc8101e4 | ||
|
e2f58a8938 | ||
|
7e6954afd9 | ||
|
bed041a1c3 | ||
|
f955f720d2 | ||
|
b627ac1ca6 | ||
|
ac0b218376 |
73 changed files with 4657 additions and 4682 deletions
16
.github/workflows/release.yml
vendored
16
.github/workflows/release.yml
vendored
|
@ -94,20 +94,26 @@ jobs:
|
|||
|
||||
macos_release:
|
||||
name: MacOS Release
|
||||
runs-on: macos-latest
|
||||
runs-on: macos-12
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Install coreutils
|
||||
run: brew install coreutils
|
||||
- name: Zig Setup
|
||||
uses: goto-bus-stop/setup-zig@v2
|
||||
with:
|
||||
version: 0.12.1
|
||||
- name: Install coreutils
|
||||
run: brew install coreutils
|
||||
- name: Install cargo-zigbuild
|
||||
run: cargo install cargo-zigbuild
|
||||
- name: Download SDK
|
||||
run: wget https://github.com/phracker/MacOSX-SDKs/releases/download/11.3/MacOSX11.0.sdk.tar.xz
|
||||
- name: Setup SDK env
|
||||
run: tar xf ${{ github.workspace }}/MacOSX11.0.sdk.tar.xz && echo "SDKROOT=${{ github.workspace }}/MacOSX11.0.sdk" >> $GITHUB_ENV
|
||||
- name: Setup platform env
|
||||
run: echo "MACOSX_DEPLOYMENT_TARGET=11.0" >> $GITHUB_ENV
|
||||
- name: Release x86
|
||||
run: |
|
||||
rustup target add x86_64-apple-darwin
|
||||
|
@ -140,8 +146,6 @@ jobs:
|
|||
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
|
||||
rustup target add x86_64-apple-darwin
|
||||
cargo zigbuild --release --target universal2-apple-darwin
|
||||
yes | cp -rf target/universal2-apple-darwin/release/grim macos/Grim.app/Contents/MacOS
|
||||
- name: Archive Universal
|
||||
|
@ -152,7 +156,7 @@ jobs:
|
|||
cd ..
|
||||
- name: Checksum Release Universal
|
||||
working-directory: target/universal2-apple-darwin/release
|
||||
shell: bash
|
||||
shell: pwsh
|
||||
run: sha256sum grim-${{ github.ref_name }}-macos-universal.zip > grim-${{ github.ref_name }}-macos-universal-sha256sum.txt
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
|
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -1,9 +1,12 @@
|
|||
*.iml
|
||||
android/.idea
|
||||
android/.gradle
|
||||
android/local.properties
|
||||
android/keystore
|
||||
android/keystore.asc
|
||||
android/keystore.properties
|
||||
android/*.apk
|
||||
android/*sha256sum.txt
|
||||
/.idea
|
||||
.DS_Store
|
||||
/captures
|
||||
|
|
4400
Cargo.lock
generated
4400
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
132
Cargo.toml
132
Cargo.toml
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "grim"
|
||||
version = "0.2.0"
|
||||
version = "0.2.3"
|
||||
authors = ["Ardocrat <ardocrat@proton.me>"]
|
||||
description = "Cross-platform GUI for Grin with focus on usability and availability to be used by anyone, anywhere."
|
||||
license = "Apache-2.0"
|
||||
|
@ -25,108 +25,110 @@ codegen-units = 1
|
|||
panic = "abort"
|
||||
|
||||
[dependencies]
|
||||
log = "0.4"
|
||||
log = "0.4.22"
|
||||
|
||||
## node
|
||||
openssl-sys = { version = "0.9.82", features = ["vendored"] }
|
||||
grin_api = "5.3.1"
|
||||
grin_chain = "5.3.1"
|
||||
grin_config = "5.3.1"
|
||||
grin_core = "5.3.1"
|
||||
grin_p2p = "5.3.1"
|
||||
grin_servers = "5.3.1"
|
||||
grin_keychain = "5.3.1"
|
||||
grin_util = "5.3.1"
|
||||
openssl-sys = { version = "0.9.103", features = ["vendored"] }
|
||||
grin_api = "5.3.3"
|
||||
grin_chain = "5.3.3"
|
||||
grin_config = "5.3.3"
|
||||
grin_core = "5.3.3"
|
||||
grin_p2p = "5.3.3"
|
||||
grin_servers = "5.3.3"
|
||||
grin_keychain = "5.3.3"
|
||||
grin_util = "5.3.3"
|
||||
|
||||
## wallet
|
||||
grin_wallet_impls = "5.3.1"
|
||||
grin_wallet_api = "5.3.1"
|
||||
grin_wallet_libwallet = "5.3.1"
|
||||
grin_wallet_util = "5.3.1"
|
||||
grin_wallet_controller = "5.3.1"
|
||||
grin_wallet_impls = "5.3.3"
|
||||
grin_wallet_api = "5.3.3"
|
||||
grin_wallet_libwallet = "5.3.3"
|
||||
grin_wallet_util = "5.3.3"
|
||||
grin_wallet_controller = "5.3.3"
|
||||
|
||||
## ui
|
||||
egui = { version = "0.28.1", default-features = false }
|
||||
egui_extras = { version = "0.28.1", features = ["image", "svg"] }
|
||||
egui = { version = "0.29.1", default-features = false }
|
||||
egui_extras = { version = "0.29.1", features = ["image", "svg"] }
|
||||
rust-i18n = "2.3.1"
|
||||
|
||||
## other
|
||||
backtrace = "0.3"
|
||||
thiserror = "1.0.58"
|
||||
futures = "0.3"
|
||||
anyhow = "1.0.89"
|
||||
pin-project = "1.1.6"
|
||||
backtrace = "0.3.74"
|
||||
thiserror = "1.0.64"
|
||||
futures = "0.3.31"
|
||||
dirs = "5.0.1"
|
||||
sys-locale = "0.3.0"
|
||||
chrono = "0.4.31"
|
||||
parking_lot = "0.12.1"
|
||||
lazy_static = "1.4.0"
|
||||
toml = "0.8.2"
|
||||
serde = "1.0.170"
|
||||
local-ip-address = "0.6.1"
|
||||
url = "2.4.0"
|
||||
sys-locale = "0.3.1"
|
||||
chrono = "0.4.38"
|
||||
parking_lot = "0.12.3"
|
||||
lazy_static = "1.5.0"
|
||||
toml = "0.8.19"
|
||||
serde = "1.0.210"
|
||||
local-ip-address = "0.6.3"
|
||||
url = "2.5.2"
|
||||
rand = "0.8.5"
|
||||
serde_derive = "1.0.197"
|
||||
serde_json = "1.0.115"
|
||||
tokio = { version = "1.37.0", features = ["full"] }
|
||||
image = "0.25.1"
|
||||
rqrr = "0.7.1"
|
||||
serde_derive = "1.0.210"
|
||||
serde_json = "1.0.128"
|
||||
tokio = { version = "1.40.0", features = ["full"] }
|
||||
image = "0.25.2"
|
||||
rqrr = "0.8.0"
|
||||
qrcodegen = "1.8.0"
|
||||
qrcode = "0.14.0"
|
||||
qrcode = "0.14.1"
|
||||
ur = "0.4.1"
|
||||
gif = "0.13.1"
|
||||
rkv = { version = "0.19.0", features = ["lmdb"] }
|
||||
|
||||
## tor
|
||||
arti-client = { version = "0.19.0", features = ["pt-client", "static", "onion-service-service", "onion-service-client"] }
|
||||
tor-rtcompat = { version = "0.19.0", features = ["static"] }
|
||||
tor-config = "0.19.0"
|
||||
fs-mistrust = "0.7.9"
|
||||
tor-hsservice = "0.19.0"
|
||||
tor-hsrproxy = "0.19.0"
|
||||
tor-keymgr = "0.19.0"
|
||||
tor-llcrypto = "0.19.0"
|
||||
tor-hscrypto = "0.19.0"
|
||||
arti-hyper = "0.19.0"
|
||||
sha2 = "0.10.0"
|
||||
arti-client = { version = "0.26.0", features = ["pt-client", "static", "onion-service-service", "onion-service-client"] }
|
||||
tor-rtcompat = { version = "0.26.0", features = ["static"] }
|
||||
tor-config = "0.26.0"
|
||||
fs-mistrust = "0.8.0"
|
||||
tor-hsservice = "0.26.0"
|
||||
tor-hsrproxy = "0.26.0"
|
||||
tor-keymgr = "0.26.0"
|
||||
tor-llcrypto = "0.26.0"
|
||||
tor-hscrypto = "0.26.0"
|
||||
tor-error = "0.26.0"
|
||||
sha2 = "0.10.8"
|
||||
ed25519-dalek = "2.1.1"
|
||||
curve25519-dalek = "4.1.2"
|
||||
hyper = { version = "0.14.28", features = ["full"] }
|
||||
curve25519-dalek = "4.1.3"
|
||||
hyper = { version = "0.14.30", features = ["full"] }
|
||||
hyper-tls = "0.5.0"
|
||||
tls-api = "0.9.0"
|
||||
tls-api-native-tls = "0.9.0"
|
||||
tls-api = "0.12.0"
|
||||
tls-api-native-tls = "0.12.1"
|
||||
|
||||
## stratum server
|
||||
tokio-old = {version = "0.2", features = ["full"], package = "tokio" }
|
||||
tokio-util-old = { version = "0.2", features = ["codec"], package = "tokio-util" }
|
||||
|
||||
[target.'cfg(all(not(target_os = "windows"), not(target_os = "android")))'.dependencies]
|
||||
eye = { version = "0.5.0", default-features = false }
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
nokhwa = { version = "0.10.5", default-features = false, features = ["input-v4l"] }
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
nokhwa = { version = "0.10.4", default-features = false, features = ["input-msmf"] }
|
||||
nokhwa = { version = "0.10.5", default-features = false, features = ["input-msmf"] }
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
tls-api-openssl = "0.9.0"
|
||||
openpnp_capture_sys = "0.4.0"
|
||||
eye = { git = "https://github.com/raymanfx/eye-rs", rev = "5b7e3f7a1e79966091692896c568aab042e449ef", default-features = false }
|
||||
|
||||
[target.'cfg(not(target_os = "android"))'.dependencies]
|
||||
env_logger = "0.11.3"
|
||||
winit = { version = "0.29.15" }
|
||||
eframe = { version = "0.28.1", features = ["wgpu", "glow"] }
|
||||
winit = { version = "0.30.5" }
|
||||
eframe = { version = "0.29.1", features = ["wgpu", "glow"] }
|
||||
arboard = "3.2.0"
|
||||
rfd = "0.14.1"
|
||||
dark-light = "1.1.1"
|
||||
rfd = "0.15.0"
|
||||
interprocess = { version = "2.2.1", features = ["tokio"] }
|
||||
|
||||
[target.'cfg(target_os = "android")'.dependencies]
|
||||
android_logger = "0.13.1"
|
||||
android_logger = "0.14.1"
|
||||
jni = "0.21.1"
|
||||
wgpu = "22.1.0"
|
||||
android-activity = { version = "0.6.0", features = ["game-activity"] }
|
||||
wgpu = "0.20.1"
|
||||
winit = { version = "0.29.15", features = ["android-game-activity"] }
|
||||
eframe = { version = "0.28.1", features = ["wgpu", "android-game-activity"] }
|
||||
winit = { version = "0.30.5", features = ["android-game-activity"] }
|
||||
eframe = { version = "0.29.1", features = ["wgpu", "android-game-activity"] }
|
||||
|
||||
[patch.crates-io]
|
||||
openpnp_capture = { git = "https://github.com/ardocrat/openpnp-capture-rs", rev = "f9b06f627c5e5d42c672d117650af700846ca6cf" }
|
||||
egui_extras = { git = "https://github.com/emilk/egui", rev = "5b846b4554fe47269affb43efef2cad8710a8a47" }
|
||||
egui = { git = "https://github.com/emilk/egui", rev = "5b846b4554fe47269affb43efef2cad8710a8a47" }
|
||||
eframe = { git = "https://github.com/emilk/egui", rev = "5b846b4554fe47269affb43efef2cad8710a8a47" }
|
||||
### patch grin store
|
||||
#grin_store = { path = "../grin-store" }
|
||||
### fix cross-compilation support for macos
|
||||
openpnp_capture_sys = { git = "https://github.com/ardocrat/openpnp-capture-rs", branch = "cross_compilation_support" }
|
|
@ -1,11 +1,11 @@
|
|||
# <img height="22" src="https://github.com/ardocrat/grim/blob/master/android/app/src/main/ic_launcher-playstore.png?raw=true"> Grim <img height="20" src="https://github.com/mimblewimble/site/blob/master/assets/images/grin-logo.png?raw=true"> <img height="20" src="https://github.com/ardocrat/grim/blob/master/img/logo.png?raw=true">
|
||||
# Grim <img height="20" src="https://gri.mw/code/GUI/grim/raw/branch/master/img/grin-logo.png"/> <img height="20" src="https://gri.mw/code/GUI/grim/raw/branch/master/img/logo.png"/>
|
||||
Cross-platform GUI for [GRiN ツ](https://grin.mw) in [Rust](https://www.rust-lang.org/)
|
||||
for maximum compatibility with original [Mimblewimble](https://github.com/mimblewimble/grin) implementation.
|
||||
Initially supported platforms are Linux, Mac, Windows, limited Android and possible web support with help of [egui](https://github.com/emilk/egui) - immediate mode GUI library in pure Rust.
|
||||
|
||||
Named by the character [Grim](http://harrypotter.wikia.com/wiki/Grim) - the shape of a large, black, menacing, spectral giant dog.
|
||||
|
||||
![image](https://github.com/user-attachments/assets/a925b1c8-02c9-4b08-b888-0315d11138b6)
|
||||
![image](https://gri.mw/code/GUI/grim/raw/branch/master/img/cover.png)
|
||||
|
||||
|
||||
## Build instructions
|
||||
|
|
|
@ -11,7 +11,7 @@ android {
|
|||
minSdk 24
|
||||
targetSdk 33
|
||||
versionCode 3
|
||||
versionName "0.2.0"
|
||||
versionName "0.2.3"
|
||||
}
|
||||
|
||||
def keystorePropertiesFile = rootProject.file("keystore.properties")
|
||||
|
|
|
@ -1,10 +1,6 @@
|
|||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
plugins {
|
||||
id 'com.android.application' version '8.1.1' apply false
|
||||
id 'com.android.library' version '8.1.1' apply false
|
||||
}
|
||||
|
||||
task clean(type: Delete) {
|
||||
delete rootProject.buildDir
|
||||
id 'com.android.application' version '8.6.1' apply false
|
||||
id 'com.android.library' version '8.6.1' apply false
|
||||
}
|
||||
|
||||
|
|
|
@ -19,5 +19,4 @@ android.useAndroidX=true
|
|||
# resources declared in the library itself and none from the library's dependencies,
|
||||
# thereby reducing the size of the R class for that library
|
||||
android.nonTransitiveRClass=true
|
||||
android.defaults.buildfeatures.buildconfig=true
|
||||
android.nonFinalResIds=false
|
|
@ -1,6 +1,6 @@
|
|||
#Mon May 02 15:39:12 BST 2022
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
|
||||
distributionPath=wrapper/dists
|
||||
zipStorePath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
|
BIN
img/cover.png
Normal file
BIN
img/cover.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 181 KiB |
BIN
img/grin-logo.png
Normal file
BIN
img/grin-logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 37 KiB |
|
@ -1,45 +1,35 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Grim</string>
|
||||
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>grim</string>
|
||||
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>AppIcon</string>
|
||||
|
||||
<key>CFBundleIconName</key>
|
||||
<string>AppIcon</string>
|
||||
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>mw.gri.macos</string>
|
||||
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
|
||||
<key>CFBundleName</key>
|
||||
<string>Grim</string>
|
||||
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>0.1.0</string>
|
||||
|
||||
<string>0.2.3</string>
|
||||
<key>CFBundleSupportedPlatforms</key>
|
||||
<array>
|
||||
<string>MacOSX</string>
|
||||
</array>
|
||||
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>Grim needs an access to your camera to scan QR code.</string>
|
||||
<key>CFBundleDocumentTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
|
@ -67,11 +57,9 @@
|
|||
<string>Document</string>
|
||||
</dict>
|
||||
</array>
|
||||
|
||||
<key>LSApplicationCategoryType</key>
|
||||
<string>public.app-category.finance</string>
|
||||
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>2024</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
1
macos/Grim.app/Contents/MacOS/.gitignore
vendored
1
macos/Grim.app/Contents/MacOS/.gitignore
vendored
|
@ -1 +1,2 @@
|
|||
!.gitignore
|
||||
grim
|
|
@ -1,11 +1,11 @@
|
|||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
case $2 in
|
||||
case $1 in
|
||||
x86_64|arm|universal)
|
||||
;;
|
||||
*)
|
||||
echo "Usage: release_macos.sh [version] [platform]\n - platform: 'x86_64', 'arm', 'universal'" >&2
|
||||
echo "Usage: release_macos.sh [platform] [version]\n - platform: 'x86_64', 'arm', 'universal'" >&2
|
||||
exit 1
|
||||
esac
|
||||
|
||||
|
@ -24,17 +24,28 @@ cd ${BASEDIR}
|
|||
cd ..
|
||||
|
||||
# Setup platform
|
||||
rustup target add x86_64-apple-darwin
|
||||
rustup target add aarch64-apple-darwin
|
||||
[[ $1 == "x86_64" ]] && arch+=(x86_64-apple-darwin)
|
||||
[[ $1 == "arm" ]] && arch+=(aarch64-apple-darwin)
|
||||
|
||||
[[ $2 == "x86_64" ]] && arch+=(x86_64-apple-darwin)
|
||||
[[ $2 == "arm" ]] && arch+=(aarch64-apple-darwin)
|
||||
[[ $2 == "universal" ]]; arch+=(universal2-apple-darwin)
|
||||
if [[ "$OSTYPE" != "darwin"* ]]; then
|
||||
# Start release build on non-MacOS with zig linker, requires zig 0.12.1
|
||||
rustup target add x86_64-apple-darwin
|
||||
rustup target add aarch64-apple-darwin
|
||||
[[ $1 == "universal" ]]; arch+=(universal2-apple-darwin)
|
||||
cargo install cargo-zigbuild
|
||||
cargo zigbuild --release --target ${arch}
|
||||
else
|
||||
rustup target add ${arch}
|
||||
if [[ $1 == "universal" ]]; then
|
||||
cargo build --release --target x86_64-apple-darwin
|
||||
cargo build --release --target aarch64-apple-darwin
|
||||
lipo -create -output target/grim target/aarch64-apple-darwin/release/grim target/x86_64-apple-darwin/release/grim
|
||||
else
|
||||
cargo build --release --target ${arch}
|
||||
fi
|
||||
fi
|
||||
|
||||
# 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
|
||||
rm -f .intentionally-empty-file.o
|
||||
|
||||
yes | cp -rf target/${arch}/release/grim macos/Grim.app/Contents/MacOS
|
||||
|
||||
|
@ -43,8 +54,7 @@ yes | cp -rf target/${arch}/release/grim macos/Grim.app/Contents/MacOS
|
|||
#rcodesign sign --pem-file cert.pem macos/Grim.app
|
||||
|
||||
# Create release package
|
||||
FILE_NAME=grim-v$1-macos-$2.zip
|
||||
rm -rf target/${arch}/release/${FILE_NAME}
|
||||
FILE_NAME=grim-v$2-macos-$1.zip
|
||||
cd macos
|
||||
zip -r ${FILE_NAME} Grim.app
|
||||
mv ${FILE_NAME} ../target/${arch}/release
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
#!/bin/bash
|
||||
|
||||
usage="Usage: android.sh [type] [platform]\n - type: 'build', 'release', ''\n - platform, for build type: 'v7', 'v8', 'x86'"
|
||||
usage="Usage: android.sh [type] [platform|version]\n - type: 'build', 'release'\n - platform, for 'build' type: 'v7', 'v8', 'x86'\n - optional version for 'release' (needed on MacOS), example: '0.2.2'"
|
||||
case $1 in
|
||||
build|release)
|
||||
;;
|
||||
|
@ -20,8 +20,8 @@ if [[ $1 == "build" ]]; then
|
|||
fi
|
||||
|
||||
# Setup build directory
|
||||
BASEDIR=$(cd $(dirname $0) && pwd)
|
||||
cd ${BASEDIR}
|
||||
BASEDIR=$(cd "$(dirname "$0")" && pwd)
|
||||
cd "${BASEDIR}" || exit 1
|
||||
cd ..
|
||||
|
||||
# Install platforms and tools
|
||||
|
@ -34,35 +34,37 @@ success=1
|
|||
|
||||
### Build native code
|
||||
function build_lib() {
|
||||
[[ $1 == "v7" ]] && arch=(armeabi-v7a)
|
||||
[[ $1 == "v8" ]] && arch=(arm64-v8a)
|
||||
[[ $1 == "x86" ]] && arch=(x86_64)
|
||||
[[ $1 == "v7" ]] && arch=armeabi-v7a
|
||||
[[ $1 == "v8" ]] && arch=arm64-v8a
|
||||
[[ $1 == "x86" ]] && arch=x86_64
|
||||
|
||||
sed -i -e 's/"rlib"/"cdylib","rlib"/g' Cargo.toml
|
||||
|
||||
# Fix for https://stackoverflow.com/questions/57193895/error-use-of-undeclared-identifier-pthread-mutex-robust-cargo-build-liblmdb-s
|
||||
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
|
||||
# Uncomment lines below for the 1st build:
|
||||
#export CPPFLAGS="-DMDB_USE_ROBUST=0" && export CFLAGS="-DMDB_USE_ROBUST=0"
|
||||
#cargo ndk -t ${arch} build --profile release-apk
|
||||
#unset CPPFLAGS && unset CFLAGS
|
||||
cargo ndk -t "${arch}" -o android/app/src/main/jniLibs build --profile release-apk
|
||||
if [ $? -eq 0 ]
|
||||
then
|
||||
success=1
|
||||
else
|
||||
success=0
|
||||
fi
|
||||
|
||||
sed -i -e 's/"cdylib","rlib"/"rlib"/g' Cargo.toml
|
||||
rm -f Cargo.toml-e
|
||||
}
|
||||
|
||||
### Build application
|
||||
function build_apk() {
|
||||
version=$(grep -m 1 -Po 'version = "\K[^"]*' Cargo.toml)
|
||||
|
||||
cd android
|
||||
cd android || exit 1
|
||||
./gradlew clean
|
||||
# Build signed apk if keystore exists
|
||||
if [ ! -f keystore.properties ]; then
|
||||
./gradlew assembleRelease
|
||||
apk_path=app/build/outputs/apk/release/app-release.apk
|
||||
./gradlew assembleDebug
|
||||
apk_path=app/build/outputs/apk/debug/app-debug.apk
|
||||
else
|
||||
./gradlew assembleSignedRelease
|
||||
apk_path=app/build/outputs/apk/signedRelease/app-signedRelease.apk
|
||||
|
@ -72,22 +74,27 @@ function build_apk() {
|
|||
# Launch application at all connected devices.
|
||||
for SERIAL in $(adb devices | grep -v List | cut -f 1);
|
||||
do
|
||||
adb -s $SERIAL install ${apk_path}
|
||||
adb -s "$SERIAL" install ${apk_path}
|
||||
sleep 1s
|
||||
adb -s $SERIAL shell am start -n mw.gri.android/.MainActivity;
|
||||
adb -s "$SERIAL" shell am start -n mw.gri.android/.MainActivity;
|
||||
done
|
||||
else
|
||||
if [[ "$OSTYPE" != "darwin"* ]]; then
|
||||
version=$(grep -m 1 -Po 'version = "\K[^"]*' Cargo.toml)
|
||||
else
|
||||
version=v$2
|
||||
fi
|
||||
# Setup release file name
|
||||
name=grim-${version}-android-$1.apk
|
||||
[[ $1 == "arm" ]] && name=grim-${version}-android.apk
|
||||
rm -rf ${name}
|
||||
mv ${apk_path} ${name}
|
||||
rm -f "${name}"
|
||||
mv ${apk_path} "${name}"
|
||||
|
||||
# Calculate checksum
|
||||
checksum=grim-${version}-android-$1-sha256sum.txt
|
||||
[[ $1 == "arm" ]] && checksum=grim-${version}-android-sha256sum.txt
|
||||
rm -rf ${checksum}
|
||||
sha256sum ${name} > ${checksum}
|
||||
rm -f "${checksum}"
|
||||
sha256sum "${name}" > "${checksum}"
|
||||
fi
|
||||
|
||||
cd ..
|
||||
|
@ -96,7 +103,7 @@ function build_apk() {
|
|||
rm -rf android/app/src/main/jniLibs/*
|
||||
|
||||
if [[ $1 == "build" ]]; then
|
||||
build_lib $2
|
||||
build_lib "$2"
|
||||
[ $success -eq 1 ] && build_apk
|
||||
else
|
||||
rm -rf target/release-apk
|
||||
|
@ -106,8 +113,8 @@ else
|
|||
|
||||
build_lib "v7"
|
||||
[ $success -eq 1 ] && build_lib "v8"
|
||||
[ $success -eq 1 ] && build_apk "arm"
|
||||
[ $success -eq 1 ] && build_apk "arm" "$2"
|
||||
rm -rf android/app/src/main/jniLibs/*
|
||||
[ $success -eq 1 ] && build_lib "x86"
|
||||
[ $success -eq 1 ] && build_apk "x86_64"
|
||||
[ $success -eq 1 ] && build_apk "x86_64" "$2"
|
||||
fi
|
|
@ -1,25 +1,27 @@
|
|||
#!/bin/bash
|
||||
|
||||
case $1 in
|
||||
debug|release)
|
||||
debug|build)
|
||||
;;
|
||||
*)
|
||||
echo "Usage: build_run.sh [type] where is type is 'debug' or 'release'" >&2
|
||||
echo "Usage: build_run.sh [type] where is type is 'debug' or 'build'" >&2
|
||||
exit 1
|
||||
esac
|
||||
|
||||
# Setup build directory
|
||||
BASEDIR=$(cd $(dirname $0) && pwd)
|
||||
cd ${BASEDIR}
|
||||
BASEDIR=$(cd "$(dirname $0)" && pwd)
|
||||
cd "${BASEDIR}" || return
|
||||
cd ..
|
||||
|
||||
# Build application
|
||||
type=$1
|
||||
[[ ${type} == "release" ]] && release_param+=(--release)
|
||||
cargo build ${release_param[@]}
|
||||
[[ ${type} == "build" ]] && release_param+=(--release)
|
||||
cargo --config profile.release.incremental=true build "${release_param[@]}"
|
||||
|
||||
# Start application
|
||||
if [ $? -eq 0 ]
|
||||
then
|
||||
./target/${type}/grim
|
||||
path=${type}
|
||||
[[ ${type} == "build" ]] && path="release"
|
||||
./target/"${path}"/grim
|
||||
fi
|
|
@ -77,10 +77,12 @@ fi
|
|||
|
||||
# Update version in build.gradle
|
||||
|
||||
sed -i 's/versionName [0-9a-zA-Z -_]*/versionName "'"$VERSION_NEXT"'"/' android/app/build.gradle
|
||||
sed -i'.bak' -e 's/versionName [0-9a-zA-Z -_]*/versionName "'"$VERSION_NEXT"'"/' android/app/build.gradle
|
||||
rm -f android/app/build.gradle.bak
|
||||
|
||||
# Update version in Cargo.toml
|
||||
sed -i "s/^version = .*/version = \"$VERSION_NEXT\"/" Cargo.toml
|
||||
sed -i'.bak' -e "s/^version = .*/version = \"$VERSION_NEXT\"/" Cargo.toml
|
||||
rm -f Cargo.toml.bak
|
||||
|
||||
# Update Cargo.lock as this changes when
|
||||
# updating the version in your manifest
|
||||
|
@ -96,4 +98,4 @@ git commit -m "release: v$VERSION_NEXT"
|
|||
|
||||
# Create a tag and push to master branch
|
||||
git tag "v$VERSION_NEXT" master
|
||||
git push origin master --follow-tags
|
||||
#git push origin master --follow-tags
|
||||
|
|
235
src/gui/app.rs
235
src/gui/app.rs
|
@ -14,7 +14,7 @@
|
|||
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use lazy_static::lazy_static;
|
||||
use egui::{Align, Context, CursorIcon, Layout, Modifiers, Rect, ResizeDirection, Rounding, Stroke, ViewportCommand};
|
||||
use egui::{Align, Context, CursorIcon, Layout, Modifiers, ResizeDirection, Rounding, Stroke, UiBuilder, ViewportCommand};
|
||||
use egui::epaint::{RectShape};
|
||||
use egui::os::OperatingSystem;
|
||||
|
||||
|
@ -22,7 +22,7 @@ use crate::AppConfig;
|
|||
use crate::gui::Colors;
|
||||
use crate::gui::icons::{ARROWS_IN, ARROWS_OUT, CARET_DOWN, MOON, SUN, X};
|
||||
use crate::gui::platform::PlatformCallbacks;
|
||||
use crate::gui::views::{Content, TitlePanel, View};
|
||||
use crate::gui::views::{Content, Modal, TitlePanel, View};
|
||||
use crate::wallet::ExternalConnection;
|
||||
|
||||
lazy_static! {
|
||||
|
@ -32,17 +32,14 @@ lazy_static! {
|
|||
|
||||
/// Implements ui entry point and contains platform-specific callbacks.
|
||||
pub struct App<Platform> {
|
||||
/// Platform specific callbacks handler.
|
||||
/// Handles platform-specific functionality.
|
||||
pub platform: Platform,
|
||||
|
||||
/// Main content.
|
||||
content: Content,
|
||||
|
||||
/// Last window resize direction.
|
||||
resize_direction: Option<ResizeDirection>,
|
||||
|
||||
/// Flag to check if it's first draw.
|
||||
first_draw: bool,
|
||||
first_draw: bool
|
||||
}
|
||||
|
||||
impl<Platform: PlatformCallbacks> App<Platform> {
|
||||
|
@ -55,24 +52,29 @@ impl<Platform: PlatformCallbacks> App<Platform> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Draw application content.
|
||||
pub fn ui(&mut self, ctx: &Context) {
|
||||
if self.first_draw {
|
||||
/// Called of first content draw.
|
||||
fn on_first_draw(&mut self, ctx: &Context) {
|
||||
// Set platform context.
|
||||
if View::is_desktop() {
|
||||
self.platform.set_context(ctx);
|
||||
}
|
||||
|
||||
// Check external connections availability.
|
||||
// Check connections availability.
|
||||
ExternalConnection::check(None, ctx);
|
||||
// Setup visuals.
|
||||
crate::setup_visuals(ctx);
|
||||
}
|
||||
|
||||
/// Draw application content.
|
||||
pub fn ui(&mut self, ctx: &Context) {
|
||||
if self.first_draw {
|
||||
self.on_first_draw(ctx);
|
||||
self.first_draw = false;
|
||||
}
|
||||
|
||||
// Handle Esc keyboard key event and platform Back button key event.
|
||||
let back_pressed = BACK_BUTTON_PRESSED.load(Ordering::Relaxed);
|
||||
if back_pressed || ctx.input_mut(|i| i.consume_key(Modifiers::NONE, egui::Key::Escape)) {
|
||||
self.content.on_back();
|
||||
self.content.on_back(&self.platform);
|
||||
if back_pressed {
|
||||
BACK_BUTTON_PRESSED.store(false, Ordering::Relaxed);
|
||||
}
|
||||
|
@ -86,10 +88,9 @@ impl<Platform: PlatformCallbacks> App<Platform> {
|
|||
ctx.send_viewport_cmd(ViewportCommand::CancelClose);
|
||||
Content::show_exit_modal();
|
||||
} else {
|
||||
let (w, h) = View::window_size(ctx);
|
||||
AppConfig::save_window_size(w, h);
|
||||
ctx.input(|i| {
|
||||
if let Some(rect) = i.viewport().inner_rect {
|
||||
AppConfig::save_window_size(rect.width(), rect.height());
|
||||
}
|
||||
if let Some(rect) = i.viewport().outer_rect {
|
||||
AppConfig::save_window_pos(rect.left(), rect.top());
|
||||
}
|
||||
|
@ -97,22 +98,27 @@ impl<Platform: PlatformCallbacks> App<Platform> {
|
|||
}
|
||||
}
|
||||
|
||||
// Show main content with custom frame on desktop.
|
||||
// Show main content.
|
||||
egui::CentralPanel::default()
|
||||
.frame(egui::Frame {
|
||||
..Default::default()
|
||||
})
|
||||
.show(ctx, |ui| {
|
||||
let is_mac_os = OperatingSystem::from_target_os() == OperatingSystem::Mac;
|
||||
if View::is_desktop() && !is_mac_os {
|
||||
self.desktop_window_ui(ui);
|
||||
if View::is_desktop() {
|
||||
let is_fullscreen = ui.ctx().input(|i| {
|
||||
i.viewport().fullscreen.unwrap_or(false)
|
||||
});
|
||||
if OperatingSystem::from_target_os() != OperatingSystem::Mac {
|
||||
self.desktop_window_ui(ui, is_fullscreen);
|
||||
} else {
|
||||
if is_mac_os {
|
||||
self.window_title_ui(ui);
|
||||
self.window_title_ui(ui, is_fullscreen);
|
||||
ui.add_space(-1.0);
|
||||
}
|
||||
Self::title_panel_bg(ui);
|
||||
self.content.ui(ui, &self.platform);
|
||||
}
|
||||
} else {
|
||||
self.mobile_window_ui(ui);
|
||||
}
|
||||
|
||||
// Provide incoming data to wallets.
|
||||
if let Some(data) = crate::consume_incoming_data() {
|
||||
|
@ -129,76 +135,52 @@ impl<Platform: PlatformCallbacks> App<Platform> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Draw custom resizeable window content.
|
||||
fn desktop_window_ui(&mut self, ui: &mut egui::Ui) {
|
||||
let is_fullscreen = ui.ctx().input(|i| {
|
||||
i.viewport().fullscreen.unwrap_or(false)
|
||||
});
|
||||
|
||||
let title_stroke_rect = {
|
||||
let mut rect = ui.max_rect();
|
||||
if !is_fullscreen {
|
||||
rect = rect.shrink(Content::WINDOW_FRAME_MARGIN);
|
||||
/// Draw mobile platform window content.
|
||||
fn mobile_window_ui(&mut self, ui: &mut egui::Ui) {
|
||||
Self::title_panel_bg(ui);
|
||||
self.content.ui(ui, &self.platform);
|
||||
}
|
||||
rect.max.y = if !is_fullscreen {
|
||||
Content::WINDOW_FRAME_MARGIN
|
||||
} else {
|
||||
0.0
|
||||
} + Content::WINDOW_TITLE_HEIGHT + TitlePanel::DEFAULT_HEIGHT + 0.5;
|
||||
rect
|
||||
};
|
||||
let title_stroke = RectShape {
|
||||
rect: title_stroke_rect,
|
||||
rounding: Rounding {
|
||||
nw: 8.0,
|
||||
ne: 8.0,
|
||||
sw: 0.0,
|
||||
se: 0.0,
|
||||
},
|
||||
fill: Colors::yellow(),
|
||||
stroke: Stroke {
|
||||
width: 1.0,
|
||||
color: egui::Color32::from_gray(200)
|
||||
},
|
||||
blur_width: 0.0,
|
||||
fill_texture_id: Default::default(),
|
||||
uv: Rect::ZERO
|
||||
};
|
||||
// Draw title stroke.
|
||||
ui.painter().add(title_stroke);
|
||||
|
||||
let content_stroke_rect = {
|
||||
let mut rect = ui.max_rect();
|
||||
/// Draw desktop platform window content.
|
||||
fn desktop_window_ui(&mut self, ui: &mut egui::Ui, is_fullscreen: bool) {
|
||||
let content_bg_rect = {
|
||||
let mut r = ui.max_rect();
|
||||
if !is_fullscreen {
|
||||
rect = rect.shrink(Content::WINDOW_FRAME_MARGIN);
|
||||
r = r.shrink(Content::WINDOW_FRAME_MARGIN);
|
||||
}
|
||||
let top = Content::WINDOW_TITLE_HEIGHT + TitlePanel::DEFAULT_HEIGHT + 0.5;
|
||||
rect.min += egui::vec2(0.0, top);
|
||||
rect
|
||||
r.min.y += Content::WINDOW_TITLE_HEIGHT + TitlePanel::HEIGHT;
|
||||
r
|
||||
};
|
||||
let content_stroke = RectShape {
|
||||
rect: content_stroke_rect,
|
||||
rounding: Rounding::ZERO,
|
||||
fill: Colors::fill(),
|
||||
stroke: Stroke {
|
||||
width: 1.0,
|
||||
color: Colors::stroke()
|
||||
},
|
||||
blur_width: 0.0,
|
||||
fill_texture_id: Default::default(),
|
||||
uv: Rect::ZERO
|
||||
};
|
||||
// Draw content stroke.
|
||||
ui.painter().add(content_stroke);
|
||||
let content_bg = RectShape::new(content_bg_rect,
|
||||
Rounding::ZERO,
|
||||
Colors::fill_lite(),
|
||||
View::default_stroke());
|
||||
// Draw content background.
|
||||
ui.painter().add(content_bg);
|
||||
|
||||
// Draw window content.
|
||||
let mut content_rect = ui.max_rect();
|
||||
if !is_fullscreen {
|
||||
content_rect = content_rect.shrink(Content::WINDOW_FRAME_MARGIN);
|
||||
}
|
||||
ui.allocate_ui_at_rect(content_rect, |ui| {
|
||||
self.window_title_ui(ui);
|
||||
self.window_content(ui);
|
||||
// Draw window content.
|
||||
ui.allocate_new_ui(UiBuilder::new().max_rect(content_rect), |ui| {
|
||||
// Draw window title.
|
||||
self.window_title_ui(ui, is_fullscreen);
|
||||
ui.add_space(-1.0);
|
||||
|
||||
// Draw title panel background.
|
||||
Self::title_panel_bg(ui);
|
||||
|
||||
let content_rect = {
|
||||
let mut rect = ui.max_rect();
|
||||
rect.min.y += Content::WINDOW_TITLE_HEIGHT;
|
||||
rect
|
||||
};
|
||||
let mut content_ui = ui.new_child(UiBuilder::new()
|
||||
.max_rect(content_rect)
|
||||
.layout(*ui.layout()));
|
||||
// Draw main content.
|
||||
self.content.ui(&mut content_ui, &self.platform);
|
||||
});
|
||||
|
||||
// Setup resize areas.
|
||||
|
@ -214,35 +196,35 @@ impl<Platform: PlatformCallbacks> App<Platform> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Draw window content for desktop.
|
||||
fn window_content(&mut self, ui: &mut egui::Ui) {
|
||||
let content_rect = {
|
||||
/// Draw title panel background.
|
||||
fn title_panel_bg(ui: &mut egui::Ui) {
|
||||
let title_rect = {
|
||||
let mut rect = ui.max_rect();
|
||||
rect.min.y += Content::WINDOW_TITLE_HEIGHT;
|
||||
if View::is_desktop() {
|
||||
rect.min.y += Content::WINDOW_TITLE_HEIGHT - 0.5;
|
||||
}
|
||||
rect.max.y = rect.min.y + View::get_top_inset() + TitlePanel::HEIGHT;
|
||||
rect
|
||||
};
|
||||
// Draw main content.
|
||||
let mut content_ui = ui.child_ui(content_rect, *ui.layout(), None);
|
||||
self.content.ui(&mut content_ui, &self.platform);
|
||||
let title_bg = RectShape::filled(title_rect, Rounding::ZERO, Colors::yellow());
|
||||
ui.painter().add(title_bg);
|
||||
}
|
||||
|
||||
/// Draw custom window title content.
|
||||
fn window_title_ui(&self, ui: &mut egui::Ui) {
|
||||
let content_rect = ui.max_rect();
|
||||
|
||||
fn window_title_ui(&self, ui: &mut egui::Ui, is_fullscreen: bool) {
|
||||
let title_rect = {
|
||||
let mut rect = content_rect;
|
||||
let mut rect = ui.max_rect();
|
||||
rect.max.y = rect.min.y + Content::WINDOW_TITLE_HEIGHT;
|
||||
rect
|
||||
};
|
||||
|
||||
let is_fullscreen = ui.ctx().input(|i| {
|
||||
i.viewport().fullscreen.unwrap_or(false)
|
||||
});
|
||||
|
||||
let window_title_bg = RectShape {
|
||||
rect: title_rect,
|
||||
rounding: if is_fullscreen {
|
||||
let title_bg_rect = {
|
||||
let mut r = title_rect.clone();
|
||||
r.max.y += TitlePanel::HEIGHT - 1.0;
|
||||
r
|
||||
};
|
||||
let is_mac = OperatingSystem::from_target_os() == OperatingSystem::Mac;
|
||||
let window_title_bg = RectShape::new(title_bg_rect, if is_fullscreen || is_mac {
|
||||
Rounding::ZERO
|
||||
} else {
|
||||
Rounding {
|
||||
|
@ -251,20 +233,16 @@ impl<Platform: PlatformCallbacks> App<Platform> {
|
|||
sw: 0.0,
|
||||
se: 0.0,
|
||||
}
|
||||
},
|
||||
fill: Colors::yellow_dark(),
|
||||
stroke: Stroke::NONE,
|
||||
blur_width: 0.0,
|
||||
fill_texture_id: Default::default(),
|
||||
uv: Rect::ZERO
|
||||
};
|
||||
}, Colors::yellow_dark(), Stroke::new(1.0, Colors::STROKE));
|
||||
// Draw title background.
|
||||
ui.painter().add(window_title_bg);
|
||||
|
||||
let painter = ui.painter();
|
||||
|
||||
let interact_rect = {
|
||||
let mut rect = title_rect;
|
||||
let mut rect = title_rect.clone();
|
||||
rect.max.x -= 128.0;
|
||||
rect.min.x += 85.0;
|
||||
if !is_fullscreen {
|
||||
rect.min.y += Content::WINDOW_FRAME_MARGIN;
|
||||
}
|
||||
|
@ -273,17 +251,20 @@ impl<Platform: PlatformCallbacks> App<Platform> {
|
|||
let title_resp = ui.interact(
|
||||
interact_rect,
|
||||
egui::Id::new("window_title"),
|
||||
egui::Sense::click_and_drag(),
|
||||
egui::Sense::drag(),
|
||||
);
|
||||
// Interact with the window title (drag to move window):
|
||||
if !is_fullscreen && title_resp.drag_started_by(egui::PointerButton::Primary) {
|
||||
ui.ctx().send_viewport_cmd(ViewportCommand::StartDrag);
|
||||
}
|
||||
|
||||
// Paint the title.
|
||||
let dual_wallets_panel =
|
||||
ui.available_width() >= (Content::SIDE_PANEL_WIDTH * 3.0)
|
||||
+ View::get_right_inset() + View::get_left_inset();
|
||||
let dual_wallets_panel = ui.available_width() >= (Content::SIDE_PANEL_WIDTH * 3.0) +
|
||||
View::get_right_inset() + View::get_left_inset();
|
||||
let wallet_panel_opened = self.content.wallets.showing_wallet();
|
||||
let show_app_name = if dual_wallets_panel {
|
||||
wallet_panel_opened && !AppConfig::show_wallets_at_dual_panel()
|
||||
} else if Content::is_dual_panel_mode(ui) {
|
||||
} else if Content::is_dual_panel_mode(ui.ctx()) {
|
||||
wallet_panel_opened
|
||||
} else {
|
||||
Content::is_network_panel_open() || wallet_panel_opened
|
||||
|
@ -302,20 +283,13 @@ impl<Platform: PlatformCallbacks> App<Platform> {
|
|||
Colors::title(true),
|
||||
);
|
||||
|
||||
// Interact with the window title (drag to move window):
|
||||
if !is_fullscreen && title_resp.double_clicked() {
|
||||
ui.ctx().send_viewport_cmd(ViewportCommand::Fullscreen(!is_fullscreen));
|
||||
}
|
||||
|
||||
if !is_fullscreen && title_resp.drag_started_by(egui::PointerButton::Primary) {
|
||||
ui.ctx().send_viewport_cmd(ViewportCommand::StartDrag);
|
||||
}
|
||||
|
||||
ui.allocate_ui_at_rect(title_rect, |ui| {
|
||||
ui.allocate_new_ui(UiBuilder::new().max_rect(title_rect), |ui| {
|
||||
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
|
||||
// Draw button to close window.
|
||||
View::title_button_small(ui, X, |_| {
|
||||
if Modal::opened().is_none() || Modal::opened_closeable() {
|
||||
Content::show_exit_modal();
|
||||
}
|
||||
});
|
||||
|
||||
// Draw fullscreen button.
|
||||
|
@ -427,16 +401,11 @@ impl<Platform: PlatformCallbacks> eframe::App for App<Platform> {
|
|||
}
|
||||
|
||||
fn clear_color(&self, _visuals: &egui::Visuals) -> [f32; 4] {
|
||||
if View::is_desktop() {
|
||||
let is_mac_os = OperatingSystem::from_target_os() == OperatingSystem::Mac;
|
||||
if is_mac_os {
|
||||
Colors::fill().to_normalized_gamma_f32()
|
||||
} else {
|
||||
egui::Rgba::TRANSPARENT.to_array()
|
||||
}
|
||||
} else {
|
||||
Colors::fill().to_normalized_gamma_f32()
|
||||
let is_mac = OperatingSystem::from_target_os() == OperatingSystem::Mac;
|
||||
if !View::is_desktop() || is_mac {
|
||||
return Colors::fill_lite().to_normalized_gamma_f32();
|
||||
}
|
||||
Colors::TRANSPARENT.to_normalized_gamma_f32()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -46,6 +46,9 @@ const FILL_DARK: Color32 = Color32::from_gray(24);
|
|||
const FILL_DEEP: Color32 = Color32::from_gray(238);
|
||||
const FILL_DEEP_DARK: Color32 = Color32::from_gray(18);
|
||||
|
||||
const FILL_LITE: Color32 = Color32::from_gray(249);
|
||||
const FILL_LITE_DARK: Color32 = Color32::from_gray(16);
|
||||
|
||||
const TEXT: Color32 = Color32::from_gray(80);
|
||||
const TEXT_DARK: Color32 = Color32::from_gray(185);
|
||||
|
||||
|
@ -58,13 +61,9 @@ const TEXT_BUTTON_DARK: Color32 = Color32::from_gray(195);
|
|||
const TITLE: Color32 = Color32::from_gray(60);
|
||||
const TITLE_DARK: Color32 = Color32::from_gray(205);
|
||||
|
||||
const BUTTON: Color32 = Color32::from_gray(249);
|
||||
const BUTTON_DARK: Color32 = Color32::from_gray(16);
|
||||
|
||||
const GRAY: Color32 = Color32::from_gray(120);
|
||||
const GRAY_DARK: Color32 = Color32::from_gray(145);
|
||||
|
||||
const STROKE: Color32 = Color32::from_gray(200);
|
||||
const STROKE_DARK: Color32 = Color32::from_gray(50);
|
||||
|
||||
const INACTIVE_TEXT: Color32 = Color32::from_gray(150);
|
||||
|
@ -86,6 +85,7 @@ fn use_dark() -> bool {
|
|||
|
||||
impl Colors {
|
||||
pub const TRANSPARENT: Color32 = Color32::from_rgba_premultiplied(0, 0, 0, 0);
|
||||
pub const STROKE: Color32 = Color32::from_gray(200);
|
||||
|
||||
pub fn white_or_black(black_in_white: bool) -> Color32 {
|
||||
if use_dark() {
|
||||
|
@ -167,6 +167,14 @@ impl Colors {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn fill_lite() -> Color32 {
|
||||
if use_dark() {
|
||||
FILL_LITE_DARK
|
||||
} else {
|
||||
FILL_LITE
|
||||
}
|
||||
}
|
||||
|
||||
pub fn checkbox() -> Color32 {
|
||||
if use_dark() {
|
||||
CHECKBOX_DARK
|
||||
|
@ -199,14 +207,6 @@ impl Colors {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn button() -> Color32 {
|
||||
if use_dark() {
|
||||
BUTTON_DARK
|
||||
} else {
|
||||
BUTTON
|
||||
}
|
||||
}
|
||||
|
||||
pub fn gray() -> Color32 {
|
||||
if use_dark() {
|
||||
GRAY_DARK
|
||||
|
@ -219,7 +219,7 @@ impl Colors {
|
|||
if use_dark() {
|
||||
STROKE_DARK
|
||||
} else {
|
||||
STROKE
|
||||
Self::STROKE
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::thread;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
||||
use std::sync::Arc;
|
||||
use parking_lot::RwLock;
|
||||
use lazy_static::lazy_static;
|
||||
|
@ -30,6 +30,10 @@ pub struct Desktop {
|
|||
/// Context to repaint content and handle viewport commands.
|
||||
ctx: Arc<RwLock<Option<egui::Context>>>,
|
||||
|
||||
/// Cameras amount.
|
||||
cameras_amount: Arc<AtomicUsize>,
|
||||
/// Camera index.
|
||||
camera_index: Arc<AtomicUsize>,
|
||||
/// Flag to check if camera stop is needed.
|
||||
stop_camera: Arc<AtomicBool>,
|
||||
|
||||
|
@ -37,6 +41,127 @@ pub struct Desktop {
|
|||
attention_required: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl Desktop {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
ctx: Arc::new(RwLock::new(None)),
|
||||
cameras_amount: Arc::new(AtomicUsize::new(0)),
|
||||
camera_index: Arc::new(AtomicUsize::new(0)),
|
||||
stop_camera: Arc::new(AtomicBool::new(false)),
|
||||
attention_required: Arc::new(AtomicBool::new(false)),
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
fn start_camera_capture(cameras_amount: Arc<AtomicUsize>,
|
||||
camera_index: Arc<AtomicUsize>,
|
||||
stop_camera: Arc<AtomicBool>) {
|
||||
use nokhwa::Camera;
|
||||
use nokhwa::pixel_format::RgbFormat;
|
||||
use nokhwa::utils::{CameraIndex, RequestedFormat, RequestedFormatType};
|
||||
use nokhwa::utils::ApiBackend;
|
||||
|
||||
let devices = nokhwa::query(ApiBackend::Auto).unwrap();
|
||||
cameras_amount.store(devices.len(), Ordering::Relaxed);
|
||||
let index = camera_index.load(Ordering::Relaxed);
|
||||
if devices.is_empty() || index >= devices.len() {
|
||||
return;
|
||||
}
|
||||
|
||||
thread::spawn(move || {
|
||||
let index = CameraIndex::Index(camera_index.load(Ordering::Relaxed) as u32);
|
||||
let requested = RequestedFormat::new::<RgbFormat>(
|
||||
RequestedFormatType::AbsoluteHighestFrameRate
|
||||
);
|
||||
// Create and open camera.
|
||||
if let Ok(mut camera) = Camera::new(index, requested) {
|
||||
if let Ok(_) = camera.open_stream() {
|
||||
loop {
|
||||
// Stop if camera was stopped.
|
||||
if stop_camera.load(Ordering::Relaxed) {
|
||||
stop_camera.store(false, Ordering::Relaxed);
|
||||
// Clear image.
|
||||
let mut w_image = LAST_CAMERA_IMAGE.write();
|
||||
*w_image = None;
|
||||
break;
|
||||
}
|
||||
// Get a frame.
|
||||
if let Ok(frame) = camera.frame() {
|
||||
// Save image.
|
||||
let mut w_image = LAST_CAMERA_IMAGE.write();
|
||||
*w_image = Some((frame.buffer().to_vec(), 0));
|
||||
} else {
|
||||
// Clear image.
|
||||
let mut w_image = LAST_CAMERA_IMAGE.write();
|
||||
*w_image = None;
|
||||
break;
|
||||
}
|
||||
}
|
||||
camera.stop_stream().unwrap();
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[cfg(target_os = "macos")]
|
||||
fn start_camera_capture(cameras_amount: Arc<AtomicUsize>,
|
||||
camera_index: Arc<AtomicUsize>,
|
||||
stop_camera: Arc<AtomicBool>) {
|
||||
use image::{ExtendedColorType, ImageBuffer, ImageEncoder, Rgb};
|
||||
use eye::hal::{traits::{Context, Device, Stream}, PlatformContext};
|
||||
use image::codecs::jpeg::JpegEncoder;
|
||||
|
||||
let index = camera_index.load(Ordering::Relaxed);
|
||||
let devices = PlatformContext::default().devices().unwrap_or(vec![]);
|
||||
cameras_amount.store(devices.len(), Ordering::Relaxed);
|
||||
if devices.is_empty() || index >= devices.len() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Capture images at separate thread.
|
||||
let uri = devices[camera_index.load(Ordering::Relaxed)].uri.clone();
|
||||
thread::spawn(move || {
|
||||
if let Ok(dev) = PlatformContext::default().open_device(&uri) {
|
||||
let streams = dev.streams().unwrap_or(vec![]);
|
||||
if streams.is_empty() {
|
||||
return;
|
||||
}
|
||||
let stream_desc = streams[0].clone();
|
||||
let w = stream_desc.width;
|
||||
let h = stream_desc.height;
|
||||
if let Ok(mut stream) = dev.start_stream(&stream_desc) {
|
||||
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) = ImageBuffer::<Rgb<u8>, &[u8]>::from_raw(w, h, &frame) {
|
||||
JpegEncoder::new(&mut out)
|
||||
.write_image(buf.as_raw(), w, h, ExtendedColorType::Rgb8)
|
||||
.unwrap_or_default();
|
||||
} else {
|
||||
out = frame.to_vec();
|
||||
}
|
||||
// Save image.
|
||||
let mut w_image = LAST_CAMERA_IMAGE.write();
|
||||
*w_image = Some((out, 0));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl PlatformCallbacks for Desktop {
|
||||
fn set_context(&mut self, ctx: &egui::Context) {
|
||||
let mut w_ctx = self.ctx.write();
|
||||
|
@ -47,7 +172,7 @@ impl PlatformCallbacks for Desktop {
|
|||
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);
|
||||
ctx.send_viewport_cmd(ViewportCommand::Close);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -71,15 +196,13 @@ impl PlatformCallbacks for Desktop {
|
|||
let mut w_image = LAST_CAMERA_IMAGE.write();
|
||||
*w_image = None;
|
||||
}
|
||||
|
||||
// Setup stop camera flag.
|
||||
let stop_camera = self.stop_camera.clone();
|
||||
stop_camera.store(false, Ordering::Relaxed);
|
||||
|
||||
// Capture images at separate thread.
|
||||
thread::spawn(move || {
|
||||
Self::start_camera_capture(stop_camera);
|
||||
});
|
||||
Self::start_camera_capture(self.cameras_amount.clone(),
|
||||
self.camera_index.clone(),
|
||||
stop_camera);
|
||||
}
|
||||
|
||||
fn stop_camera(&self) {
|
||||
|
@ -96,11 +219,20 @@ impl PlatformCallbacks for Desktop {
|
|||
}
|
||||
|
||||
fn can_switch_camera(&self) -> bool {
|
||||
false
|
||||
let amount = self.cameras_amount.load(Ordering::Relaxed);
|
||||
amount > 1
|
||||
}
|
||||
|
||||
fn switch_camera(&self) {
|
||||
return;
|
||||
self.stop_camera();
|
||||
let index = self.camera_index.load(Ordering::Relaxed);
|
||||
let amount = self.cameras_amount.load(Ordering::Relaxed);
|
||||
if index == amount - 1 {
|
||||
self.camera_index.store(0, Ordering::Relaxed);
|
||||
} else {
|
||||
self.camera_index.store(index + 1, Ordering::Relaxed);
|
||||
}
|
||||
self.start_camera();
|
||||
}
|
||||
|
||||
fn share_data(&self, name: String, data: Vec<u8>) -> Result<(), std::io::Error> {
|
||||
|
@ -171,94 +303,6 @@ impl PlatformCallbacks for Desktop {
|
|||
}
|
||||
}
|
||||
|
||||
impl Desktop {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
stop_camera: Arc::new(AtomicBool::new(false)),
|
||||
ctx: Arc::new(RwLock::new(None)),
|
||||
attention_required: Arc::new(AtomicBool::new(false)),
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[cfg(target_os = "windows")]
|
||||
fn start_camera_capture(stop_camera: Arc<AtomicBool>) {
|
||||
use nokhwa::Camera;
|
||||
use nokhwa::pixel_format::RgbFormat;
|
||||
use nokhwa::utils::{CameraIndex, RequestedFormat, RequestedFormatType};
|
||||
let index = CameraIndex::Index(0);
|
||||
let requested = RequestedFormat::new::<RgbFormat>(
|
||||
RequestedFormatType::AbsoluteHighestFrameRate
|
||||
);
|
||||
// Create and open camera.
|
||||
let mut camera = Camera::new(index, requested).unwrap();
|
||||
if let Ok(_) = camera.open_stream() {
|
||||
loop {
|
||||
// Stop if camera was stopped.
|
||||
if stop_camera.load(Ordering::Relaxed) {
|
||||
stop_camera.store(false, Ordering::Relaxed);
|
||||
// Clear image.
|
||||
let mut w_image = LAST_CAMERA_IMAGE.write();
|
||||
*w_image = None;
|
||||
break;
|
||||
}
|
||||
// Get a frame.
|
||||
if let Ok(frame) = camera.frame() {
|
||||
// Save image.
|
||||
let mut w_image = LAST_CAMERA_IMAGE.write();
|
||||
*w_image = Some((frame.buffer().to_vec(), 0));
|
||||
} else {
|
||||
// Clear image.
|
||||
let mut w_image = LAST_CAMERA_IMAGE.write();
|
||||
*w_image = None;
|
||||
break;
|
||||
}
|
||||
}
|
||||
camera.stop_stream().unwrap();
|
||||
};
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
fn start_camera_capture(stop_camera: Arc<AtomicBool>) {
|
||||
use eye::hal::{traits::{Context, Device, Stream}, PlatformContext};
|
||||
use image::ImageEncoder;
|
||||
|
||||
let ctx = PlatformContext::default();
|
||||
let devices = ctx.devices().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 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);
|
||||
let mut w_image = LAST_CAMERA_IMAGE.write();
|
||||
*w_image = None;
|
||||
break;
|
||||
}
|
||||
// Get a frame.
|
||||
let frame = stream.next().expect("Stream is dead").expect("Failed to capture a frame");
|
||||
let mut out = vec![];
|
||||
if let Some(buf) = image::ImageBuffer::<image::Rgb<u8>, &[u8]>::from_raw(w, h, &frame) {
|
||||
image::codecs::jpeg::JpegEncoder::new(&mut out)
|
||||
.write_image(buf.as_raw(), w, h, image::ExtendedColorType::Rgb8).unwrap();
|
||||
} else {
|
||||
out = frame.to_vec();
|
||||
}
|
||||
// Save image.
|
||||
let mut w_image = LAST_CAMERA_IMAGE.write();
|
||||
*w_image = Some((out, 0));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
/// Last captured image from started camera.
|
||||
static ref LAST_CAMERA_IMAGE: Arc<RwLock<Option<(Vec<u8>, u32)>>> = Arc::new(RwLock::new(None));
|
||||
|
|
|
@ -15,11 +15,9 @@
|
|||
use std::sync::Arc;
|
||||
use parking_lot::RwLock;
|
||||
use std::thread;
|
||||
use eframe::emath::Align;
|
||||
use egui::load::SizedTexture;
|
||||
use egui::{Layout, Pos2, Rect, RichText, TextureOptions, Widget};
|
||||
use image::{DynamicImage, EncodableLayout, ImageFormat};
|
||||
|
||||
use egui::{Pos2, Rect, RichText, TextureOptions, UiBuilder, Widget};
|
||||
use image::{DynamicImage, EncodableLayout};
|
||||
use grin_util::ZeroingString;
|
||||
use grin_wallet_libwallet::SlatepackAddress;
|
||||
use grin_keychain::mnemonic::WORDS;
|
||||
|
@ -36,16 +34,15 @@ use crate::wallet::WalletUtils;
|
|||
pub struct CameraContent {
|
||||
/// QR code scanning progress and result.
|
||||
qr_scan_state: Arc<RwLock<QrScanState>>,
|
||||
|
||||
/// Uniform Resources URIs collected from QR code scanning.
|
||||
ur_data: Arc<RwLock<Option<(Vec<String>, usize)>>>,
|
||||
ur_data: Arc<RwLock<Option<(Vec<String>, usize)>>>
|
||||
}
|
||||
|
||||
impl Default for CameraContent {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
qr_scan_state: Arc::new(RwLock::new(QrScanState::default())),
|
||||
ur_data: Arc::new(RwLock::new(None)),
|
||||
ur_data: Arc::new(RwLock::new(None))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -53,21 +50,56 @@ impl Default for CameraContent {
|
|||
impl CameraContent {
|
||||
/// Draw camera content.
|
||||
pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||
// Draw last image from camera or loader.
|
||||
if let Some(img_data) = cb.camera_image() {
|
||||
// Load image to draw.
|
||||
if let Ok(mut img) =
|
||||
image::load_from_memory_with_format(&*img_data.0, ImageFormat::Jpeg) {
|
||||
ui.ctx().request_repaint();
|
||||
let rect = if let Some(img_data) = cb.camera_image() {
|
||||
if let Ok(img) =
|
||||
image::load_from_memory(&*img_data.0) {
|
||||
// Process image to find QR code.
|
||||
self.scan_qr(&img);
|
||||
|
||||
// Draw image.
|
||||
let img_rect = self.image_ui(ui, img, img_data.1);
|
||||
|
||||
// Show UR scan progress.
|
||||
self.ur_progress_ui(ui);
|
||||
img_rect
|
||||
} else {
|
||||
self.loading_ui(ui)
|
||||
}
|
||||
} else {
|
||||
self.loading_ui(ui)
|
||||
};
|
||||
|
||||
// Show button to switch cameras.
|
||||
if cb.can_switch_camera() {
|
||||
let r = {
|
||||
let mut r = rect.clone();
|
||||
r.min.y = r.max.y - 52.0;
|
||||
r.min.x = r.max.x - 52.0;
|
||||
r
|
||||
};
|
||||
ui.allocate_new_ui(UiBuilder::new().max_rect(r), |ui| {
|
||||
let rotate_img = CAMERA_ROTATE.to_string();
|
||||
View::button(ui, rotate_img, Colors::white_or_black(false), || {
|
||||
cb.switch_camera();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw camera image.
|
||||
fn image_ui(&mut self, ui: &mut egui::Ui, mut img: DynamicImage, rotation: u32) -> Rect {
|
||||
// Setup image rotation.
|
||||
img = match img_data.1 {
|
||||
img = match rotation {
|
||||
90 => img.rotate90(),
|
||||
180 => img.rotate180(),
|
||||
270 => img.rotate270(),
|
||||
_ => img
|
||||
};
|
||||
// Convert to ColorImage to add at content.
|
||||
if View::is_desktop() {
|
||||
img = img.fliph();
|
||||
}
|
||||
// Convert to ColorImage.
|
||||
let color_img = match &img {
|
||||
DynamicImage::ImageRgb8(image) => {
|
||||
egui::ColorImage::from_rgb(
|
||||
|
@ -90,8 +122,6 @@ impl CameraContent {
|
|||
let img_size = egui::emath::vec2(color_img.width() as f32,
|
||||
color_img.height() as f32);
|
||||
let sized_img = SizedTexture::new(texture.id(), img_size);
|
||||
// Add image to content.
|
||||
ui.vertical_centered(|ui| {
|
||||
egui::Image::from_texture(sized_img)
|
||||
// Setup to crop image at square.
|
||||
.uv(Rect::from([
|
||||
|
@ -101,54 +131,31 @@ impl CameraContent {
|
|||
.max_height(ui.available_width())
|
||||
.maintain_aspect_ratio(false)
|
||||
.shrink_to_fit()
|
||||
.ui(ui);
|
||||
});
|
||||
.ui(ui).rect
|
||||
}
|
||||
|
||||
// Show UR scan progress.
|
||||
/// Draw animated QR code scanning progress.
|
||||
fn ur_progress_ui(&self, ui: &mut egui::Ui) {
|
||||
let show_ur_progress = {
|
||||
self.ur_data.clone().read().is_some()
|
||||
self.ur_data.as_ref().read().is_some()
|
||||
};
|
||||
let ur_progress = self.ur_progress();
|
||||
if show_ur_progress && ur_progress != 0 {
|
||||
ui.add_space(-52.0);
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.label(RichText::new(format!("{}%", ur_progress))
|
||||
.size(16.0)
|
||||
.color(Colors::yellow()));
|
||||
if show_ur_progress {
|
||||
ui.centered_and_justified(|ui| {
|
||||
ui.label(RichText::new(format!("{}%", self.ur_progress()))
|
||||
.size(17.0)
|
||||
.color(Colors::green()));
|
||||
});
|
||||
}
|
||||
|
||||
// Show button to switch cameras.
|
||||
if cb.can_switch_camera() {
|
||||
ui.add_space(-52.0);
|
||||
let mut size = ui.available_size();
|
||||
size.y = 48.0;
|
||||
ui.allocate_ui_with_layout(size, Layout::right_to_left(Align::Max), |ui| {
|
||||
ui.add_space(4.0);
|
||||
View::button(ui, CAMERA_ROTATE.to_string(), Colors::white_or_black(false), || {
|
||||
cb.switch_camera();
|
||||
});
|
||||
});
|
||||
}
|
||||
} else {
|
||||
self.loading_content_ui(ui);
|
||||
}
|
||||
} else {
|
||||
self.loading_content_ui(ui);
|
||||
}
|
||||
|
||||
// Request redraw.
|
||||
ui.ctx().request_repaint();
|
||||
}
|
||||
|
||||
/// Draw camera loading progress content.
|
||||
fn loading_content_ui(&self, ui: &mut egui::Ui) {
|
||||
fn loading_ui(&self, ui: &mut egui::Ui) -> Rect {
|
||||
let space = (ui.available_width() - View::BIG_SPINNER_SIZE) / 2.0;
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.add_space(space);
|
||||
View::big_loading_spinner(ui);
|
||||
ui.add_space(space);
|
||||
});
|
||||
}).response.rect
|
||||
}
|
||||
|
||||
/// Check if image is processing to find QR code.
|
||||
|
|
|
@ -52,6 +52,11 @@ pub struct Content {
|
|||
allowed_modal_ids: Vec<&'static str>
|
||||
}
|
||||
|
||||
/// Identifier for integrated node warning [`Modal`] on Android.
|
||||
const ANDROID_INTEGRATED_NODE_WARNING_MODAL: &'static str = "android_node_warning_modal";
|
||||
/// Identifier for crash report [`Modal`].
|
||||
const CRASH_REPORT_MODAL: &'static str = "crash_report_modal";
|
||||
|
||||
impl Default for Content {
|
||||
fn default() -> Self {
|
||||
// Exit from eframe only for non-mobile platforms.
|
||||
|
@ -66,8 +71,8 @@ impl Default for Content {
|
|||
allowed_modal_ids: vec![
|
||||
Self::EXIT_CONFIRMATION_MODAL,
|
||||
Self::SETTINGS_MODAL,
|
||||
Self::ANDROID_INTEGRATED_NODE_WARNING_MODAL,
|
||||
Self::CRASH_REPORT_MODAL
|
||||
ANDROID_INTEGRATED_NODE_WARNING_MODAL,
|
||||
CRASH_REPORT_MODAL
|
||||
],
|
||||
}
|
||||
}
|
||||
|
@ -85,8 +90,8 @@ impl ModalContainer for Content {
|
|||
match modal.id {
|
||||
Self::EXIT_CONFIRMATION_MODAL => self.exit_modal_content(ui, modal, cb),
|
||||
Self::SETTINGS_MODAL => self.settings_modal_ui(ui, modal),
|
||||
Self::ANDROID_INTEGRATED_NODE_WARNING_MODAL => self.android_warning_modal_ui(ui, modal),
|
||||
Self::CRASH_REPORT_MODAL => self.crash_report_modal_ui(ui, modal, cb),
|
||||
ANDROID_INTEGRATED_NODE_WARNING_MODAL => self.android_warning_modal_ui(ui, modal),
|
||||
CRASH_REPORT_MODAL => self.crash_report_modal_ui(ui, modal, cb),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
@ -97,10 +102,6 @@ impl Content {
|
|||
pub const EXIT_CONFIRMATION_MODAL: &'static str = "exit_confirmation_modal";
|
||||
/// Identifier for wallet opening [`Modal`].
|
||||
pub const SETTINGS_MODAL: &'static str = "settings_modal";
|
||||
/// Identifier for integrated node warning [`Modal`] on Android.
|
||||
const ANDROID_INTEGRATED_NODE_WARNING_MODAL: &'static str = "android_node_warning_modal";
|
||||
/// Identifier for crash report [`Modal`].
|
||||
const CRASH_REPORT_MODAL: &'static str = "crash_report_modal";
|
||||
|
||||
/// Default width of side panel at application UI.
|
||||
pub const SIDE_PANEL_WIDTH: f32 = 400.0;
|
||||
|
@ -110,11 +111,10 @@ impl Content {
|
|||
pub const WINDOW_FRAME_MARGIN: f32 = 6.0;
|
||||
|
||||
pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||
// Draw modal content for current ui container.
|
||||
self.current_modal_ui(ui, cb);
|
||||
|
||||
let dual_panel = Self::is_dual_panel_mode(ui);
|
||||
let (is_panel_open, panel_width) = Self::network_panel_state_width(ui, dual_panel);
|
||||
let dual_panel = Self::is_dual_panel_mode(ui.ctx());
|
||||
let (is_panel_open, panel_width) = network_panel_state_width(ui.ctx(), dual_panel);
|
||||
|
||||
// Show network content.
|
||||
egui::SidePanel::left("network_panel")
|
||||
|
@ -137,48 +137,26 @@ impl Content {
|
|||
});
|
||||
|
||||
if self.first_draw {
|
||||
// Show crash report if needed.
|
||||
if AppConfig::show_crash() {
|
||||
Modal::new(Self::CRASH_REPORT_MODAL)
|
||||
// Show crash report or integrated node Android warning.
|
||||
if Settings::crash_report_path().exists() {
|
||||
Modal::new(CRASH_REPORT_MODAL)
|
||||
.closeable(false)
|
||||
.position(ModalPosition::Center)
|
||||
.title(t!("crash_report"))
|
||||
.show();
|
||||
} else {
|
||||
// Show integrated node warning on Android if needed.
|
||||
if OperatingSystem::from_target_os() == OperatingSystem::Android &&
|
||||
} else if OperatingSystem::from_target_os() == OperatingSystem::Android &&
|
||||
AppConfig::android_integrated_node_warning_needed() {
|
||||
Modal::new(Self::ANDROID_INTEGRATED_NODE_WARNING_MODAL)
|
||||
Modal::new(ANDROID_INTEGRATED_NODE_WARNING_MODAL)
|
||||
.title(t!("network.node"))
|
||||
.show();
|
||||
}
|
||||
}
|
||||
self.first_draw = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Get [`NetworkContent`] panel state and width.
|
||||
fn network_panel_state_width(ui: &mut egui::Ui, dual_panel: bool) -> (bool, f32) {
|
||||
let is_panel_open = dual_panel || Self::is_network_panel_open();
|
||||
let panel_width = if dual_panel {
|
||||
Self::SIDE_PANEL_WIDTH + View::get_left_inset()
|
||||
} else {
|
||||
let is_fullscreen = ui.ctx().input(|i| {
|
||||
i.viewport().fullscreen.unwrap_or(false)
|
||||
});
|
||||
View::window_size(ui).0 - if View::is_desktop() && !is_fullscreen &&
|
||||
OperatingSystem::from_target_os() != OperatingSystem::Mac {
|
||||
Self::WINDOW_FRAME_MARGIN * 2.0
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
};
|
||||
(is_panel_open, panel_width)
|
||||
}
|
||||
|
||||
/// Check if ui can show [`NetworkContent`] and [`WalletsContent`] at same time.
|
||||
pub fn is_dual_panel_mode(ui: &egui::Ui) -> bool {
|
||||
let (w, h) = View::window_size(ui);
|
||||
pub fn is_dual_panel_mode(ctx: &egui::Context) -> bool {
|
||||
let (w, h) = View::window_size(ctx);
|
||||
// Screen is wide if width is greater than height or just 20% smaller.
|
||||
let is_wide_screen = w > h || w + (w * 0.2) >= h;
|
||||
// Dual panel mode is available when window is wide and its width is at least 2 times
|
||||
|
@ -260,9 +238,9 @@ impl Content {
|
|||
}
|
||||
|
||||
/// Handle Back key event.
|
||||
pub fn on_back(&mut self) {
|
||||
pub fn on_back(&mut self, cb: &dyn PlatformCallbacks) {
|
||||
if Modal::on_back() {
|
||||
if self.wallets.on_back() {
|
||||
if self.wallets.on_back(cb) {
|
||||
Self::show_exit_modal()
|
||||
}
|
||||
}
|
||||
|
@ -341,7 +319,6 @@ impl Content {
|
|||
let item_rounding = View::item_rounding(index, len, false);
|
||||
ui.painter().rect(bg_rect, item_rounding, Colors::fill(), View::item_stroke());
|
||||
|
||||
ui.vertical(|ui| {
|
||||
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
|
||||
// Draw button to select language.
|
||||
let is_current = if let Some(lang) = AppConfig::locale() {
|
||||
|
@ -379,7 +356,6 @@ impl Content {
|
|||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// Draw content for integrated node warning [`Modal`] on Android.
|
||||
|
@ -415,10 +391,10 @@ impl Content {
|
|||
let text = format!("{} {}", FILE_X, t!("share"));
|
||||
View::colored_text_button(ui, text, Colors::blue(), Colors::white_or_black(false), || {
|
||||
if let Ok(data) = fs::read_to_string(Settings::crash_report_path()) {
|
||||
cb.share_data(Settings::CRASH_REPORT_FILE_NAME.to_string(),
|
||||
data.as_bytes().to_vec()).unwrap_or_default()
|
||||
let name = Settings::CRASH_REPORT_FILE_NAME.to_string();
|
||||
let _ = cb.share_data(name, data.as_bytes().to_vec());
|
||||
}
|
||||
AppConfig::set_show_crash(false);
|
||||
Settings::delete_crash_report();
|
||||
modal.close();
|
||||
});
|
||||
});
|
||||
|
@ -427,10 +403,29 @@ impl Content {
|
|||
ui.add_space(8.0);
|
||||
ui.vertical_centered_justified(|ui| {
|
||||
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
|
||||
AppConfig::set_show_crash(false);
|
||||
Settings::delete_crash_report();
|
||||
modal.close();
|
||||
});
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
}
|
||||
}
|
||||
|
||||
/// Get [`NetworkContent`] panel state and width.
|
||||
fn network_panel_state_width(ctx: &egui::Context, dual_panel: bool) -> (bool, f32) {
|
||||
let is_panel_open = dual_panel || Content::is_network_panel_open();
|
||||
let panel_width = if dual_panel {
|
||||
Content::SIDE_PANEL_WIDTH + View::get_left_inset()
|
||||
} else {
|
||||
let is_fullscreen = ctx.input(|i| {
|
||||
i.viewport().fullscreen.unwrap_or(false)
|
||||
});
|
||||
View::window_size(ctx).0 - if View::is_desktop() && !is_fullscreen &&
|
||||
OperatingSystem::from_target_os() != OperatingSystem::Mac {
|
||||
Content::WINDOW_FRAME_MARGIN * 2.0
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
};
|
||||
(is_panel_open, panel_width)
|
||||
}
|
|
@ -78,8 +78,8 @@ impl FilePickButton {
|
|||
}
|
||||
} else {
|
||||
// Draw button to pick file.
|
||||
let file_text = format!("{} {}", ARCHIVE_BOX, t!("choose_file"));
|
||||
View::colored_text_button(ui, file_text, Colors::blue(), Colors::button(), || {
|
||||
let text = format!("{} {}", ARCHIVE_BOX, t!("choose_file"));
|
||||
View::colored_text_button(ui, text, Colors::blue(), Colors::white_or_black(false), || {
|
||||
if let Some(path) = cb.pick_file() {
|
||||
self.on_file_pick(path);
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ use lazy_static::lazy_static;
|
|||
use std::sync::Arc;
|
||||
use parking_lot::RwLock;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use egui::{Align2, Rect, RichText, Rounding, Stroke, Vec2};
|
||||
use egui::{Align2, RichText, Rounding, Stroke, UiBuilder, Vec2};
|
||||
use egui::epaint::{RectShape, Shadow};
|
||||
use egui::os::OperatingSystem;
|
||||
|
||||
|
@ -29,17 +29,17 @@ lazy_static! {
|
|||
static ref MODAL_STATE: Arc<RwLock<ModalState>> = Arc::new(RwLock::new(ModalState::default()));
|
||||
}
|
||||
|
||||
/// Stores data to draw modal [`egui::Window`] at ui.
|
||||
/// Modal [`egui::Window`] container.
|
||||
#[derive(Clone)]
|
||||
pub struct Modal {
|
||||
/// Identifier for modal.
|
||||
pub(crate) id: &'static str,
|
||||
/// Position on the screen.
|
||||
pub position: ModalPosition,
|
||||
/// To check if it can be closed.
|
||||
/// Flag to check if modal can be closed by keys.
|
||||
closeable: Arc<AtomicBool>,
|
||||
/// Title text
|
||||
title: Option<String>
|
||||
/// Title text.
|
||||
title: Option<String>,
|
||||
}
|
||||
|
||||
impl Modal {
|
||||
|
@ -54,7 +54,7 @@ impl Modal {
|
|||
id,
|
||||
position: ModalPosition::Center,
|
||||
closeable: Arc::new(AtomicBool::new(true)),
|
||||
title: None
|
||||
title: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -110,7 +110,7 @@ impl Modal {
|
|||
}
|
||||
|
||||
/// Remove [`Modal`] from [`ModalState`] if it's showing and can be closed.
|
||||
/// Return `false` if Modal existed in [`ModalState`] before call.
|
||||
/// Return `false` if modal existed in state before call.
|
||||
pub fn on_back() -> bool {
|
||||
let mut w_state = MODAL_STATE.write();
|
||||
|
||||
|
@ -125,7 +125,7 @@ impl Modal {
|
|||
true
|
||||
}
|
||||
|
||||
/// Return id of opened [`Modal`].
|
||||
/// Return identifier of opened [`Modal`].
|
||||
pub fn opened() -> Option<&'static str> {
|
||||
// Check if modal is showing.
|
||||
{
|
||||
|
@ -140,6 +140,19 @@ impl Modal {
|
|||
Some(modal.id)
|
||||
}
|
||||
|
||||
/// Check if [`Modal`] is opened and can be closed.
|
||||
pub fn opened_closeable() -> bool {
|
||||
// Check if modal is showing.
|
||||
{
|
||||
if MODAL_STATE.read().modal.is_none() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
let r_state = MODAL_STATE.read();
|
||||
let modal = r_state.modal.as_ref().unwrap();
|
||||
modal.closeable.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// Set title text for current opened [`Modal`].
|
||||
pub fn set_title(title: String) {
|
||||
// Save state.
|
||||
|
@ -170,40 +183,42 @@ impl Modal {
|
|||
let is_fullscreen = ctx.input(|i| {
|
||||
i.viewport().fullscreen.unwrap_or(false)
|
||||
});
|
||||
let is_mac_os = OperatingSystem::from_target_os() == OperatingSystem::Mac;
|
||||
|
||||
let mut rect = ctx.screen_rect();
|
||||
if View::is_desktop() && !is_mac_os {
|
||||
let margin = if !is_fullscreen {
|
||||
Content::WINDOW_FRAME_MARGIN
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
rect = rect.shrink(margin - 0.5);
|
||||
rect.min += egui::vec2(0.0, Content::WINDOW_TITLE_HEIGHT + 0.5);
|
||||
rect.max.x += 0.5;
|
||||
// Setup background rect.
|
||||
let bg_rect = if View::is_desktop() {
|
||||
let mut r = ctx.screen_rect();
|
||||
let is_mac = OperatingSystem::Mac == OperatingSystem::from_target_os();
|
||||
if !is_mac && !is_fullscreen {
|
||||
r = r.shrink(Content::WINDOW_FRAME_MARGIN - 1.0);
|
||||
}
|
||||
r.min.y += Content::WINDOW_TITLE_HEIGHT;
|
||||
r
|
||||
} else {
|
||||
ctx.screen_rect()
|
||||
};
|
||||
|
||||
// Draw modal background.
|
||||
egui::Window::new("modal_bg_window")
|
||||
.title_bar(false)
|
||||
.resizable(false)
|
||||
.collapsible(false)
|
||||
.fixed_rect(rect)
|
||||
.fixed_rect(bg_rect)
|
||||
.frame(egui::Frame {
|
||||
fill: Colors::semi_transparent(),
|
||||
..Default::default()
|
||||
})
|
||||
.show(ctx, |ui| {
|
||||
ui.set_min_size(rect.size());
|
||||
ui.set_min_size(bg_rect.size());
|
||||
});
|
||||
|
||||
// Setup width of modal content.
|
||||
let side_insets = View::get_left_inset() + View::get_right_inset();
|
||||
let available_width = rect.width() - (side_insets + Self::DEFAULT_MARGIN);
|
||||
let available_width = ctx.screen_rect().width() - (side_insets + Self::DEFAULT_MARGIN);
|
||||
let width = f32::min(available_width, Self::DEFAULT_WIDTH);
|
||||
|
||||
// Show main content Window at given position.
|
||||
let (content_align, content_offset) = self.modal_position(is_fullscreen);
|
||||
let layer_id = egui::Window::new(format!("modal_window_{}", self.id))
|
||||
// Show main content window at given position.
|
||||
let (content_align, content_offset) = self.modal_position();
|
||||
let layer_id = egui::Window::new("modal_window")
|
||||
.title_bar(false)
|
||||
.resizable(false)
|
||||
.collapsible(false)
|
||||
|
@ -218,33 +233,30 @@ impl Modal {
|
|||
color: egui::Color32::from_black_alpha(32),
|
||||
},
|
||||
rounding: Rounding::same(8.0),
|
||||
fill: Colors::fill(),
|
||||
..Default::default()
|
||||
})
|
||||
.show(ctx, |ui| {
|
||||
if self.title.is_some() {
|
||||
self.title_ui(ui);
|
||||
if let Some(title) = &self.title {
|
||||
title_ui(title, ui);
|
||||
}
|
||||
self.content_ui(ui, add_content);
|
||||
}).unwrap().response.layer_id;
|
||||
|
||||
// Always show main content Window above background Window.
|
||||
// Always show main content window above background window.
|
||||
ctx.move_to_top(layer_id);
|
||||
|
||||
}
|
||||
|
||||
/// Get [`egui::Window`] position based on [`ModalPosition`].
|
||||
fn modal_position(&self, is_fullscreen: bool) -> (Align2, Vec2) {
|
||||
fn modal_position(&self) -> (Align2, Vec2) {
|
||||
let align = match self.position {
|
||||
ModalPosition::CenterTop => Align2::CENTER_TOP,
|
||||
ModalPosition::Center => Align2::CENTER_CENTER
|
||||
};
|
||||
|
||||
let x_align = View::get_left_inset() - View::get_right_inset();
|
||||
|
||||
let is_mac_os = OperatingSystem::from_target_os() == OperatingSystem::Mac;
|
||||
let extra_y = if View::is_desktop() && !is_mac_os {
|
||||
Content::WINDOW_TITLE_HEIGHT + if !is_fullscreen {
|
||||
let is_mac = OperatingSystem::Mac == OperatingSystem::from_target_os();
|
||||
let extra_y = if View::is_desktop() {
|
||||
Content::WINDOW_TITLE_HEIGHT + if !is_mac {
|
||||
Content::WINDOW_FRAME_MARGIN
|
||||
} else {
|
||||
0.0
|
||||
|
@ -252,7 +264,7 @@ impl Modal {
|
|||
} else {
|
||||
0.0
|
||||
};
|
||||
let y_align = View::get_top_inset() + Self::DEFAULT_MARGIN + extra_y;
|
||||
let y_align = View::get_top_inset() + Self::DEFAULT_MARGIN / 2.0 + extra_y;
|
||||
|
||||
let offset = match self.position {
|
||||
ModalPosition::CenterTop => Vec2::new(x_align, y_align),
|
||||
|
@ -264,80 +276,64 @@ impl Modal {
|
|||
/// Draw provided content.
|
||||
fn content_ui(&self, ui: &mut egui::Ui, add_content: impl FnOnce(&mut egui::Ui, &Modal)) {
|
||||
let mut rect = ui.available_rect_before_wrap();
|
||||
rect.min += egui::emath::vec2(6.0, 0.0);
|
||||
rect.max -= egui::emath::vec2(6.0, 0.0);
|
||||
|
||||
// Create background shape.
|
||||
let rounding = if self.title.is_some() {
|
||||
let mut bg_shape = RectShape::new(rect, if self.title.is_none() {
|
||||
Rounding::same(8.0)
|
||||
} else {
|
||||
Rounding {
|
||||
nw: 0.0,
|
||||
ne: 0.0,
|
||||
sw: 8.0,
|
||||
se: 8.0,
|
||||
}
|
||||
} else {
|
||||
Rounding::same(8.0)
|
||||
};
|
||||
let mut bg_shape = RectShape {
|
||||
rect,
|
||||
rounding,
|
||||
fill: Colors::fill(),
|
||||
stroke: Stroke::NONE,
|
||||
blur_width: 0.0,
|
||||
fill_texture_id: Default::default(),
|
||||
uv: Rect::ZERO
|
||||
};
|
||||
}, Colors::fill(), Stroke::NONE);
|
||||
let bg_idx = ui.painter().add(bg_shape);
|
||||
|
||||
// Draw main content.
|
||||
let mut content_rect = ui.allocate_ui_at_rect(rect, |ui| {
|
||||
rect.min += egui::emath::vec2(6.0, 0.0);
|
||||
rect.max -= egui::emath::vec2(6.0, 0.0);
|
||||
let resp = ui.allocate_new_ui(UiBuilder::new().max_rect(rect), |ui| {
|
||||
(add_content)(ui, self);
|
||||
}).response.rect;
|
||||
}).response;
|
||||
|
||||
// Setup background shape to be painted behind main content.
|
||||
content_rect.min -= egui::emath::vec2(6.0, 0.0);
|
||||
content_rect.max += egui::emath::vec2(6.0, 0.0);
|
||||
bg_shape.rect = content_rect;
|
||||
// Setup background size.
|
||||
let bg_rect = {
|
||||
let mut r = resp.rect.clone();
|
||||
r.min -= egui::emath::vec2(6.0, 0.0);
|
||||
r.max += egui::emath::vec2(6.0, 0.0);
|
||||
r
|
||||
};
|
||||
bg_shape.rect = bg_rect;
|
||||
ui.painter().set(bg_idx, bg_shape);
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw title content.
|
||||
fn title_ui(&self, ui: &mut egui::Ui) {
|
||||
/// Draw title content.
|
||||
fn title_ui(title: &String, ui: &mut egui::Ui) {
|
||||
let rect = ui.available_rect_before_wrap();
|
||||
|
||||
// Create background shape.
|
||||
let mut bg_shape = RectShape {
|
||||
rect,
|
||||
rounding: Rounding {
|
||||
let mut bg_shape = RectShape::new(rect, Rounding {
|
||||
nw: 8.0,
|
||||
ne: 8.0,
|
||||
sw: 0.0,
|
||||
se: 0.0,
|
||||
},
|
||||
fill: Colors::yellow(),
|
||||
stroke: Stroke::NONE,
|
||||
blur_width: 0.0,
|
||||
fill_texture_id: Default::default(),
|
||||
uv: Rect::ZERO
|
||||
};
|
||||
}, Colors::yellow(), Stroke::NONE);
|
||||
let bg_idx = ui.painter().add(bg_shape);
|
||||
|
||||
// Draw title content.
|
||||
let title_resp = ui.allocate_ui_at_rect(rect, |ui| {
|
||||
ui.vertical_centered_justified(|ui| {
|
||||
ui.add_space(Self::DEFAULT_MARGIN + 1.0);
|
||||
ui.label(RichText::new(self.title.as_ref().unwrap())
|
||||
let resp = ui.vertical_centered(|ui| {
|
||||
ui.add_space(Modal::DEFAULT_MARGIN + 2.0);
|
||||
ui.label(RichText::new(title)
|
||||
.size(19.0)
|
||||
.color(Colors::title(true))
|
||||
);
|
||||
ui.add_space(Self::DEFAULT_MARGIN);
|
||||
ui.add_space(Modal::DEFAULT_MARGIN + 1.0);
|
||||
// Draw line below title.
|
||||
View::horizontal_line(ui, Colors::item_stroke());
|
||||
});
|
||||
}).response;
|
||||
|
||||
// Setup background shape to be painted behind title content.
|
||||
bg_shape.rect = title_resp.rect;
|
||||
// Setup background size.
|
||||
bg_shape.rect = resp.rect;
|
||||
ui.painter().set(bg_idx, bg_shape);
|
||||
}
|
||||
}
|
|
@ -202,7 +202,6 @@ impl ConnectionsContent {
|
|||
let item_rounding = View::item_rounding(index, len, false);
|
||||
ui.painter().rect(bg_rect, item_rounding, Colors::fill(), View::item_stroke());
|
||||
|
||||
ui.vertical(|ui| {
|
||||
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
|
||||
// Draw provided buttons.
|
||||
buttons_ui(ui);
|
||||
|
@ -232,7 +231,6 @@ impl ConnectionsContent {
|
|||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// Show [`Modal`] to add external connection.
|
||||
|
|
|
@ -21,15 +21,15 @@ use crate::gui::icons::{ARROWS_COUNTER_CLOCKWISE, BRIEFCASE, DATABASE, DOTS_THRE
|
|||
use crate::gui::platform::PlatformCallbacks;
|
||||
use crate::gui::views::{Content, TitlePanel, View};
|
||||
use crate::gui::views::network::{ConnectionsContent, NetworkMetrics, NetworkMining, NetworkNode, NetworkSettings};
|
||||
use crate::gui::views::network::types::{NetworkTab, NetworkTabType};
|
||||
use crate::gui::views::types::{TitleContentType, TitleType};
|
||||
use crate::gui::views::network::types::{NodeTab, NodeTabType};
|
||||
use crate::gui::views::types::{LinePosition, TitleContentType, TitleType};
|
||||
use crate::node::{Node, NodeConfig, NodeError};
|
||||
use crate::wallet::ExternalConnection;
|
||||
|
||||
/// Network content.
|
||||
pub struct NetworkContent {
|
||||
/// Current integrated node tab content.
|
||||
node_tab_content: Box<dyn NetworkTab>,
|
||||
node_tab_content: Box<dyn NodeTab>,
|
||||
/// Connections content.
|
||||
connections: ConnectionsContent,
|
||||
}
|
||||
|
@ -46,14 +46,14 @@ impl Default for NetworkContent {
|
|||
impl NetworkContent {
|
||||
pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||
let show_connections = AppConfig::show_connections_network_panel();
|
||||
let dual_panel = Content::is_dual_panel_mode(ui);
|
||||
let dual_panel = Content::is_dual_panel_mode(ui.ctx());
|
||||
|
||||
// Show title panel.
|
||||
self.title_ui(ui, show_connections);
|
||||
self.title_ui(ui, dual_panel, show_connections);
|
||||
|
||||
// Show integrated node tabs content.
|
||||
if !show_connections {
|
||||
egui::TopBottomPanel::bottom("node_tabs_content")
|
||||
egui::TopBottomPanel::bottom("node_tabs")
|
||||
.min_height(0.5)
|
||||
.resizable(false)
|
||||
.frame(egui::Frame {
|
||||
|
@ -67,15 +67,23 @@ impl NetworkContent {
|
|||
..Default::default()
|
||||
})
|
||||
.show_inside(ui, |ui| {
|
||||
ui.vertical_centered(|ui| {
|
||||
let rect = ui.available_rect_before_wrap();
|
||||
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
|
||||
self.tabs_ui(ui);
|
||||
});
|
||||
});
|
||||
// Draw content divider line.
|
||||
let r = {
|
||||
let mut r = rect.clone();
|
||||
r.min.x -= View::get_left_inset() + View::TAB_ITEMS_PADDING;
|
||||
r.min.y -= View::TAB_ITEMS_PADDING;
|
||||
r.max.x += View::far_right_inset_margin(ui) + View::TAB_ITEMS_PADDING;
|
||||
r
|
||||
};
|
||||
View::line(ui, LinePosition::TOP, &r, Colors::stroke());
|
||||
});
|
||||
}
|
||||
|
||||
// Show current node tab content.
|
||||
// Show integrated node tab content.
|
||||
egui::SidePanel::right("node_tab_content")
|
||||
.resizable(false)
|
||||
.exact_width(ui.available_width())
|
||||
|
@ -85,8 +93,6 @@ impl NetworkContent {
|
|||
.show_animated_inside(ui, !show_connections, |ui| {
|
||||
egui::CentralPanel::default()
|
||||
.frame(egui::Frame {
|
||||
fill: Colors::white_or_black(false),
|
||||
stroke: View::item_stroke(),
|
||||
inner_margin: Margin {
|
||||
left: View::get_left_inset() + 4.0,
|
||||
right: View::far_right_inset_margin(ui) + 4.0,
|
||||
|
@ -96,14 +102,42 @@ impl NetworkContent {
|
|||
..Default::default()
|
||||
})
|
||||
.show_inside(ui, |ui| {
|
||||
let rect = ui.available_rect_before_wrap();
|
||||
if self.node_tab_content.get_type() != NodeTabType::Settings {
|
||||
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
|
||||
let node_err = Node::get_error();
|
||||
if let Some(err) = node_err {
|
||||
node_error_ui(ui, err);
|
||||
} else if !Node::is_running() {
|
||||
disabled_node_ui(ui);
|
||||
} else if Node::get_stats().is_none() || Node::is_restarting() ||
|
||||
Node::is_stopping() {
|
||||
NetworkContent::loading_ui(ui, None);
|
||||
} else {
|
||||
self.node_tab_content.ui(ui, cb);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
self.node_tab_content.ui(ui, cb);
|
||||
}
|
||||
|
||||
// Draw content divider line.
|
||||
let r = {
|
||||
let mut r = rect.clone();
|
||||
r.min.y -= 3.0;
|
||||
r.max.x += 4.0;
|
||||
r.max.y += 4.0;
|
||||
r
|
||||
};
|
||||
if dual_panel {
|
||||
View::line(ui, LinePosition::RIGHT, &r, Colors::item_stroke());
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Show connections content.
|
||||
egui::CentralPanel::default()
|
||||
.frame(egui::Frame {
|
||||
stroke: View::item_stroke(),
|
||||
inner_margin: Margin {
|
||||
left: if show_connections {
|
||||
View::get_left_inset() + 4.0
|
||||
|
@ -116,18 +150,14 @@ impl NetworkContent {
|
|||
0.0
|
||||
},
|
||||
top: 3.0,
|
||||
bottom: if View::is_desktop() && show_connections {
|
||||
6.0
|
||||
} else {
|
||||
4.0
|
||||
bottom: 4.0 + View::get_bottom_inset(),
|
||||
},
|
||||
},
|
||||
fill: Colors::button(),
|
||||
..Default::default()
|
||||
})
|
||||
.show_inside(ui, |ui| {
|
||||
let rect = ui.available_rect_before_wrap();
|
||||
ScrollArea::vertical()
|
||||
.id_source("connections_content")
|
||||
.id_salt("connections_scroll")
|
||||
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
|
||||
.auto_shrink([false; 2])
|
||||
.show(ui, |ui| {
|
||||
|
@ -143,15 +173,26 @@ impl NetworkContent {
|
|||
});
|
||||
});
|
||||
});
|
||||
// Draw content divider line.
|
||||
let r = {
|
||||
let mut r = rect.clone();
|
||||
r.min.y -= 3.0;
|
||||
r.max.x += 4.0;
|
||||
r.max.y += 4.0 + View::get_bottom_inset();
|
||||
r
|
||||
};
|
||||
if show_connections && dual_panel {
|
||||
View::line(ui, LinePosition::RIGHT, &r, Colors::item_stroke());
|
||||
}
|
||||
});
|
||||
|
||||
// Redraw after delay.
|
||||
if Node::is_running() {
|
||||
// Redraw after delay if node is running at non-dual-panel mode.
|
||||
if !dual_panel && Content::is_network_panel_open() && Node::is_running() {
|
||||
ui.ctx().request_repaint_after(Node::STATS_UPDATE_DELAY);
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw tab buttons in the bottom of the screen.
|
||||
/// Draw tab buttons at bottom of the screen.
|
||||
fn tabs_ui(&mut self, ui: &mut egui::Ui) {
|
||||
ui.vertical_centered(|ui| {
|
||||
// Setup spacing between tabs.
|
||||
|
@ -163,22 +204,22 @@ impl NetworkContent {
|
|||
let current_type = self.node_tab_content.get_type();
|
||||
ui.columns(4, |columns| {
|
||||
columns[0].vertical_centered_justified(|ui| {
|
||||
View::tab_button(ui, DATABASE, current_type == NetworkTabType::Node, |_| {
|
||||
View::tab_button(ui, DATABASE, current_type == NodeTabType::Info, |_| {
|
||||
self.node_tab_content = Box::new(NetworkNode::default());
|
||||
});
|
||||
});
|
||||
columns[1].vertical_centered_justified(|ui| {
|
||||
View::tab_button(ui, GAUGE, current_type == NetworkTabType::Metrics, |_| {
|
||||
View::tab_button(ui, GAUGE, current_type == NodeTabType::Metrics, |_| {
|
||||
self.node_tab_content = Box::new(NetworkMetrics::default());
|
||||
});
|
||||
});
|
||||
columns[2].vertical_centered_justified(|ui| {
|
||||
View::tab_button(ui, FACTORY, current_type == NetworkTabType::Mining, |_| {
|
||||
View::tab_button(ui, FACTORY, current_type == NodeTabType::Mining, |_| {
|
||||
self.node_tab_content = Box::new(NetworkMining::default());
|
||||
});
|
||||
});
|
||||
columns[3].vertical_centered_justified(|ui| {
|
||||
View::tab_button(ui, FADERS, current_type == NetworkTabType::Settings, |_| {
|
||||
View::tab_button(ui, FADERS, current_type == NodeTabType::Settings, |_| {
|
||||
self.node_tab_content = Box::new(NetworkSettings::default());
|
||||
});
|
||||
});
|
||||
|
@ -187,15 +228,15 @@ impl NetworkContent {
|
|||
}
|
||||
|
||||
/// Draw title content.
|
||||
fn title_ui(&mut self, ui: &mut egui::Ui, show_connections: bool) {
|
||||
fn title_ui(&mut self, ui: &mut egui::Ui, dual_panel: bool, show_connections: bool) {
|
||||
// Setup values for title panel.
|
||||
let title_text = self.node_tab_content.get_type().title().to_uppercase();
|
||||
let title_text = self.node_tab_content.get_type().title();
|
||||
let subtitle_text = Node::get_sync_status_text();
|
||||
let not_syncing = Node::not_syncing();
|
||||
let title_content = if !show_connections {
|
||||
TitleContentType::WithSubTitle(title_text, subtitle_text, !not_syncing)
|
||||
} else {
|
||||
TitleContentType::Title(t!("network.connections").to_uppercase())
|
||||
TitleContentType::Title(t!("network.connections"))
|
||||
};
|
||||
|
||||
// Draw title panel.
|
||||
|
@ -209,7 +250,7 @@ impl NetworkContent {
|
|||
});
|
||||
}
|
||||
}, |ui| {
|
||||
if !Content::is_dual_panel_mode(ui) {
|
||||
if !dual_panel {
|
||||
View::title_button_big(ui, BRIEFCASE, |_| {
|
||||
Content::toggle_network_panel();
|
||||
});
|
||||
|
@ -217,23 +258,6 @@ impl NetworkContent {
|
|||
}, ui);
|
||||
}
|
||||
|
||||
/// Content to draw when node is disabled.
|
||||
pub fn disabled_node_ui(ui: &mut egui::Ui) {
|
||||
View::center_content(ui, 156.0, |ui| {
|
||||
let text = t!("network.disabled_server", "dots" => DOTS_THREE_OUTLINE_VERTICAL);
|
||||
ui.label(RichText::new(text)
|
||||
.size(16.0)
|
||||
.color(Colors::inactive_text())
|
||||
);
|
||||
ui.add_space(8.0);
|
||||
View::action_button(ui, format!("{} {}", POWER, t!("network.enable_node")), || {
|
||||
Node::start();
|
||||
});
|
||||
ui.add_space(2.0);
|
||||
Self::autorun_node_ui(ui);
|
||||
});
|
||||
}
|
||||
|
||||
/// Content to draw on loading.
|
||||
pub fn loading_ui(ui: &mut egui::Ui, text: Option<String>) {
|
||||
match text {
|
||||
|
@ -262,9 +286,27 @@ impl NetworkContent {
|
|||
AppConfig::toggle_node_autostart();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw integrated node error content.
|
||||
pub fn node_error_ui(ui: &mut egui::Ui, e: NodeError) {
|
||||
/// Content to draw when node is disabled.
|
||||
fn disabled_node_ui(ui: &mut egui::Ui) {
|
||||
View::center_content(ui, 156.0, |ui| {
|
||||
let text = t!("network.disabled_server", "dots" => DOTS_THREE_OUTLINE_VERTICAL);
|
||||
ui.label(RichText::new(text)
|
||||
.size(16.0)
|
||||
.color(Colors::inactive_text())
|
||||
);
|
||||
ui.add_space(8.0);
|
||||
View::action_button(ui, format!("{} {}", POWER, t!("network.enable_node")), || {
|
||||
Node::start();
|
||||
});
|
||||
ui.add_space(2.0);
|
||||
NetworkContent::autorun_node_ui(ui);
|
||||
});
|
||||
}
|
||||
|
||||
/// Draw integrated node error content.
|
||||
pub fn node_error_ui(ui: &mut egui::Ui, e: NodeError) {
|
||||
match e {
|
||||
NodeError::Storage => {
|
||||
View::center_content(ui, 156.0, |ui| {
|
||||
|
@ -338,5 +380,4 @@ impl NetworkContent {
|
|||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -14,6 +14,7 @@
|
|||
|
||||
use egui::{RichText, Rounding, ScrollArea, vec2};
|
||||
use egui::scroll_area::ScrollBarVisibility;
|
||||
use grin_core::consensus::{DAY_HEIGHT, GRIN_BASE, HOUR_SEC, REWARD};
|
||||
use grin_servers::{DiffBlock, ServerStats};
|
||||
|
||||
use crate::gui::Colors;
|
||||
|
@ -21,87 +22,61 @@ use crate::gui::icons::{AT, COINS, CUBE_TRANSPARENT, HOURGLASS_LOW, HOURGLASS_ME
|
|||
use crate::gui::platform::PlatformCallbacks;
|
||||
use crate::gui::views::{Content, View};
|
||||
use crate::gui::views::network::NetworkContent;
|
||||
use crate::gui::views::network::types::{NetworkTab, NetworkTabType};
|
||||
use crate::gui::views::network::types::{NodeTab, NodeTabType};
|
||||
use crate::node::Node;
|
||||
|
||||
/// Chain metrics tab content.
|
||||
#[derive(Default)]
|
||||
pub struct NetworkMetrics;
|
||||
|
||||
const BLOCK_REWARD: f64 = 60.0;
|
||||
// 1 year is calculated as 365 days and 6 hours (31557600).
|
||||
const YEARLY_SUPPLY: f64 = ((60 * 60 * 24 * 365) + 6 * 60 * 60) as f64;
|
||||
const BLOCK_REWARD: u64 = REWARD / GRIN_BASE;
|
||||
// 1 year as 365 days and 6 hours (31557600).
|
||||
const YEARLY_SUPPLY: u64 = (BLOCK_REWARD * DAY_HEIGHT * 365) + 6 * HOUR_SEC;
|
||||
|
||||
impl NetworkTab for NetworkMetrics {
|
||||
fn get_type(&self) -> NetworkTabType {
|
||||
NetworkTabType::Metrics
|
||||
impl NodeTab for NetworkMetrics {
|
||||
fn get_type(&self) -> NodeTabType {
|
||||
NodeTabType::Metrics
|
||||
}
|
||||
|
||||
fn ui(&mut self, ui: &mut egui::Ui, _: &dyn PlatformCallbacks) {
|
||||
// Show an error content when available.
|
||||
let node_err = Node::get_error();
|
||||
if node_err.is_some() {
|
||||
NetworkContent::node_error_ui(ui, node_err.unwrap());
|
||||
return;
|
||||
}
|
||||
|
||||
// Show message to enable node when it's not running.
|
||||
if !Node::is_running() {
|
||||
NetworkContent::disabled_node_ui(ui);
|
||||
return;
|
||||
}
|
||||
|
||||
// Show loading spinner when node is stopping.
|
||||
if Node::is_stopping() {
|
||||
NetworkContent::loading_ui(ui, None);
|
||||
return;
|
||||
}
|
||||
|
||||
// Show message when metrics are not available.
|
||||
let server_stats = Node::get_stats();
|
||||
if server_stats.is_none() || Node::is_restarting()
|
||||
|| server_stats.as_ref().unwrap().diff_stats.height == 0 {
|
||||
let stats = server_stats.as_ref().unwrap();
|
||||
if stats.diff_stats.height == 0 {
|
||||
NetworkContent::loading_ui(ui, Some(t!("network_metrics.loading")));
|
||||
return;
|
||||
}
|
||||
|
||||
ui.add_space(1.0);
|
||||
ui.vertical_centered(|ui| {
|
||||
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
|
||||
let stats = server_stats.as_ref().unwrap();
|
||||
// Show emission and difficulty info.
|
||||
info_ui(ui, stats);
|
||||
// Show difficulty adjustment window blocks.
|
||||
blocks_ui(ui, stats);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const BLOCK_ITEM_HEIGHT: f32 = 78.0;
|
||||
|
||||
/// Draw emission and difficulty info.
|
||||
fn info_ui(ui: &mut egui::Ui, stats: &ServerStats) {
|
||||
// Show emission info.
|
||||
View::sub_title(ui, format!("{} {}", COINS, t!("network_metrics.emission")));
|
||||
ui.columns(3, |columns| {
|
||||
let supply = stats.header_stats.height as f64 * BLOCK_REWARD;
|
||||
let rate = (YEARLY_SUPPLY * 100.0) / supply;
|
||||
let supply = stats.header_stats.height * BLOCK_REWARD;
|
||||
let rate = (YEARLY_SUPPLY * 100) / supply;
|
||||
|
||||
columns[0].vertical_centered(|ui| {
|
||||
View::rounded_box(ui,
|
||||
View::label_box(ui,
|
||||
format!("{}ツ", BLOCK_REWARD),
|
||||
t!("network_metrics.reward"),
|
||||
[true, false, true, false]);
|
||||
});
|
||||
columns[1].vertical_centered(|ui| {
|
||||
View::rounded_box(ui,
|
||||
View::label_box(ui,
|
||||
format!("{:.2}%", rate),
|
||||
t!("network_metrics.inflation"),
|
||||
[false, false, false, false]);
|
||||
});
|
||||
columns[2].vertical_centered(|ui| {
|
||||
View::rounded_box(ui,
|
||||
View::label_box(ui,
|
||||
supply.to_string(),
|
||||
t!("network_metrics.supply"),
|
||||
[false, true, false, true]);
|
||||
|
@ -117,19 +92,19 @@ fn info_ui(ui: &mut egui::Ui, stats: &ServerStats) {
|
|||
View::sub_title(ui, format!("{} {}", HOURGLASS_MEDIUM, difficulty_title));
|
||||
ui.columns(3, |columns| {
|
||||
columns[0].vertical_centered(|ui| {
|
||||
View::rounded_box(ui,
|
||||
View::label_box(ui,
|
||||
stats.diff_stats.height.to_string(),
|
||||
t!("network_node.height"),
|
||||
[true, false, true, false]);
|
||||
});
|
||||
columns[1].vertical_centered(|ui| {
|
||||
View::rounded_box(ui,
|
||||
View::label_box(ui,
|
||||
format!("{}s", stats.diff_stats.average_block_time),
|
||||
t!("network_metrics.block_time"),
|
||||
[false, false, false, false]);
|
||||
});
|
||||
columns[2].vertical_centered(|ui| {
|
||||
View::rounded_box(ui,
|
||||
View::label_box(ui,
|
||||
stats.diff_stats.average_difficulty.to_string(),
|
||||
t!("network_node.difficulty"),
|
||||
[false, true, false, true]);
|
||||
|
@ -137,12 +112,14 @@ fn info_ui(ui: &mut egui::Ui, stats: &ServerStats) {
|
|||
});
|
||||
}
|
||||
|
||||
const BLOCK_ITEM_HEIGHT: f32 = 77.0;
|
||||
|
||||
/// Draw difficulty adjustment window blocks content.
|
||||
fn blocks_ui(ui: &mut egui::Ui, stats: &ServerStats) {
|
||||
let blocks_size = stats.diff_stats.last_blocks.len();
|
||||
ui.add_space(4.0);
|
||||
ScrollArea::vertical()
|
||||
.id_source("difficulty_scroll")
|
||||
.id_salt("mining_difficulty_scroll")
|
||||
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
|
||||
.auto_shrink([false; 2])
|
||||
.stick_to_bottom(true)
|
||||
|
@ -151,11 +128,8 @@ fn blocks_ui(ui: &mut egui::Ui, stats: &ServerStats) {
|
|||
BLOCK_ITEM_HEIGHT,
|
||||
blocks_size,
|
||||
|ui, row_range| {
|
||||
for index in row_range {
|
||||
// Add space before the first item.
|
||||
if index == 0 {
|
||||
ui.add_space(4.0);
|
||||
}
|
||||
for index in row_range {
|
||||
let db = stats.diff_stats.last_blocks.get(index).unwrap();
|
||||
block_item_ui(ui, db, View::item_rounding(index, blocks_size, false));
|
||||
}
|
||||
|
@ -167,11 +141,11 @@ fn blocks_ui(ui: &mut egui::Ui, stats: &ServerStats) {
|
|||
fn block_item_ui(ui: &mut egui::Ui, db: &DiffBlock, rounding: Rounding) {
|
||||
let mut rect = ui.available_rect_before_wrap();
|
||||
rect.set_height(BLOCK_ITEM_HEIGHT);
|
||||
ui.allocate_ui_at_rect(rect, |ui| {
|
||||
ui.allocate_ui(rect.size(), |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.add_space(6.0);
|
||||
ui.vertical(|ui| {
|
||||
ui.add_space(3.0);
|
||||
ui.add_space(4.0);
|
||||
|
||||
// Draw round background.
|
||||
rect.min += vec2(8.0, 0.0);
|
||||
|
@ -180,24 +154,26 @@ fn block_item_ui(ui: &mut egui::Ui, db: &DiffBlock, rounding: Rounding) {
|
|||
|
||||
// Draw block hash.
|
||||
ui.horizontal(|ui| {
|
||||
ui.add_space(7.0);
|
||||
ui.add_space(8.0);
|
||||
ui.label(RichText::new(db.block_hash.to_string())
|
||||
.color(Colors::white_or_black(true))
|
||||
.size(17.0));
|
||||
});
|
||||
// Draw block difficulty and height.
|
||||
ui.horizontal(|ui| {
|
||||
ui.add_space(6.0);
|
||||
ui.add_space(7.0);
|
||||
let diff_text = format!("{} {} {} {}",
|
||||
CUBE_TRANSPARENT,
|
||||
db.difficulty,
|
||||
AT,
|
||||
db.block_height);
|
||||
ui.label(RichText::new(diff_text).color(Colors::title(false)).size(16.0));
|
||||
ui.label(RichText::new(diff_text)
|
||||
.color(Colors::title(false))
|
||||
.size(15.0));
|
||||
});
|
||||
// Draw block date.
|
||||
ui.horizontal(|ui| {
|
||||
ui.add_space(6.0);
|
||||
ui.add_space(7.0);
|
||||
let block_time = View::format_time(db.time as i64);
|
||||
ui.label(RichText::new(format!("{} {}s {} {}",
|
||||
TIMER,
|
||||
|
@ -205,7 +181,7 @@ fn block_item_ui(ui: &mut egui::Ui, db: &DiffBlock, rounding: Rounding) {
|
|||
HOURGLASS_LOW,
|
||||
block_time))
|
||||
.color(Colors::gray())
|
||||
.size(16.0));
|
||||
.size(15.0));
|
||||
});
|
||||
ui.add_space(3.0);
|
||||
});
|
||||
|
|
|
@ -23,62 +23,30 @@ use crate::gui::platform::PlatformCallbacks;
|
|||
use crate::gui::views::{Content, View};
|
||||
use crate::gui::views::network::NetworkContent;
|
||||
use crate::gui::views::network::setup::StratumSetup;
|
||||
use crate::gui::views::network::types::{NetworkTab, NetworkTabType};
|
||||
use crate::gui::views::network::types::{NodeTab, NodeTabType};
|
||||
use crate::node::{Node, NodeConfig};
|
||||
use crate::wallet::WalletConfig;
|
||||
|
||||
/// Mining tab content.
|
||||
pub struct NetworkMining {
|
||||
/// Stratum server setup content.
|
||||
stratum_server_setup: StratumSetup,
|
||||
|
||||
/// Wallet name for rewards.
|
||||
wallet_name: String,
|
||||
}
|
||||
|
||||
impl Default for NetworkMining {
|
||||
fn default() -> Self {
|
||||
let wallet_name = if let Some(id) = NodeConfig::get_stratum_wallet_id() {
|
||||
WalletConfig::name_by_id(id).unwrap_or("-".to_string())
|
||||
} else {
|
||||
"-".to_string()
|
||||
};
|
||||
Self {
|
||||
stratum_server_setup: StratumSetup::default(),
|
||||
wallet_name,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl NetworkTab for NetworkMining {
|
||||
fn get_type(&self) -> NetworkTabType {
|
||||
NetworkTabType::Mining
|
||||
impl NodeTab for NetworkMining {
|
||||
fn get_type(&self) -> NodeTabType {
|
||||
NodeTabType::Mining
|
||||
}
|
||||
|
||||
fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||
// Show an error content when available.
|
||||
let node_err = Node::get_error();
|
||||
if node_err.is_some() {
|
||||
NetworkContent::node_error_ui(ui, node_err.unwrap());
|
||||
return;
|
||||
}
|
||||
|
||||
// Show message to enable node when it's not running.
|
||||
if !Node::is_running() {
|
||||
NetworkContent::disabled_node_ui(ui);
|
||||
return;
|
||||
}
|
||||
|
||||
// Show loading spinner when node is stopping or stratum server is starting.
|
||||
if Node::is_stopping() || Node::is_stratum_starting() {
|
||||
NetworkContent::loading_ui(ui, None);
|
||||
return;
|
||||
}
|
||||
|
||||
// Show message when mining is not available.
|
||||
let server_stats = Node::get_stats();
|
||||
if server_stats.is_none() || Node::is_restarting()
|
||||
|| Node::get_sync_status().unwrap() != SyncStatus::NoSync {
|
||||
if Node::is_stratum_starting() || Node::get_sync_status().unwrap() != SyncStatus::NoSync {
|
||||
NetworkContent::loading_ui(ui, Some(t!("network_mining.loading")));
|
||||
return;
|
||||
}
|
||||
|
@ -87,17 +55,15 @@ impl NetworkTab for NetworkMining {
|
|||
let stratum_stats = Node::get_stratum_stats();
|
||||
if !stratum_stats.is_running {
|
||||
ScrollArea::vertical()
|
||||
.id_source("stratum_setup_scroll")
|
||||
.id_salt("stratum_setup_scroll")
|
||||
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
|
||||
.auto_shrink([false; 2])
|
||||
.show(ui, |ui| {
|
||||
ui.add_space(1.0);
|
||||
ui.vertical_centered(|ui| {
|
||||
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
|
||||
self.stratum_server_setup.ui(ui, cb);
|
||||
});
|
||||
});
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -108,14 +74,17 @@ impl NetworkTab for NetworkMining {
|
|||
ui.columns(2, |columns| {
|
||||
columns[0].vertical_centered(|ui| {
|
||||
let (stratum_addr, stratum_port) = NodeConfig::get_stratum_address();
|
||||
View::rounded_box(ui,
|
||||
View::label_box(ui,
|
||||
format!("{}:{}", stratum_addr, stratum_port),
|
||||
t!("network_mining.address"),
|
||||
[true, false, true, false]);
|
||||
});
|
||||
columns[1].vertical_centered(|ui| {
|
||||
View::rounded_box(ui,
|
||||
self.wallet_name.clone(),
|
||||
View::label_box(ui,
|
||||
self.stratum_server_setup
|
||||
.wallet_name
|
||||
.clone()
|
||||
.unwrap_or("-".to_string()),
|
||||
t!("network_mining.rewards_wallet"),
|
||||
[false, true, false, true]);
|
||||
});
|
||||
|
@ -131,7 +100,7 @@ impl NetworkTab for NetworkMining {
|
|||
} else {
|
||||
"-".into()
|
||||
};
|
||||
View::rounded_box(ui,
|
||||
View::label_box(ui,
|
||||
difficulty,
|
||||
t!("network_node.difficulty"),
|
||||
[true, false, true, false]);
|
||||
|
@ -142,7 +111,7 @@ impl NetworkTab for NetworkMining {
|
|||
} else {
|
||||
"-".into()
|
||||
};
|
||||
View::rounded_box(ui,
|
||||
View::label_box(ui,
|
||||
block_height,
|
||||
t!("network_node.header"),
|
||||
[false, false, false, false]);
|
||||
|
@ -153,7 +122,7 @@ impl NetworkTab for NetworkMining {
|
|||
} else {
|
||||
"-".into()
|
||||
};
|
||||
View::rounded_box(ui,
|
||||
View::label_box(ui,
|
||||
hashrate,
|
||||
t!("network_mining.hashrate", "bits" => stratum_stats.edge_bits),
|
||||
[false, true, false, true]);
|
||||
|
@ -165,14 +134,14 @@ impl NetworkTab for NetworkMining {
|
|||
View::sub_title(ui, format!("{} {}", CPU, t!("network_mining.miners")));
|
||||
ui.columns(2, |columns| {
|
||||
columns[0].vertical_centered(|ui| {
|
||||
View::rounded_box(ui,
|
||||
View::label_box(ui,
|
||||
stratum_stats.num_workers.to_string(),
|
||||
t!("network_mining.devices"),
|
||||
[true, false, true, false]);
|
||||
});
|
||||
|
||||
columns[1].vertical_centered(|ui| {
|
||||
View::rounded_box(ui,
|
||||
View::label_box(ui,
|
||||
stratum_stats.blocks_found.to_string(),
|
||||
t!("network_mining.blocks_found"),
|
||||
[false, true, false, true]);
|
||||
|
@ -187,7 +156,7 @@ impl NetworkTab for NetworkMining {
|
|||
View::horizontal_line(ui, Colors::item_stroke());
|
||||
ui.add_space(4.0);
|
||||
ScrollArea::vertical()
|
||||
.id_source("stratum_workers_scroll")
|
||||
.id_salt("stratum_workers_scroll")
|
||||
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
|
||||
.auto_shrink([false; 2])
|
||||
.show_rows(
|
||||
|
|
|
@ -20,53 +20,30 @@ use crate::gui::Colors;
|
|||
use crate::gui::icons::{AT, CUBE, DEVICES, FLOW_ARROW, HANDSHAKE, PACKAGE, SHARE_NETWORK};
|
||||
use crate::gui::platform::PlatformCallbacks;
|
||||
use crate::gui::views::{Content, View};
|
||||
use crate::gui::views::network::NetworkContent;
|
||||
use crate::gui::views::network::types::{NetworkTab, NetworkTabType};
|
||||
use crate::gui::views::network::types::{NodeTab, NodeTabType};
|
||||
use crate::node::{Node, NodeConfig};
|
||||
|
||||
/// Integrated node tab content.
|
||||
#[derive(Default)]
|
||||
pub struct NetworkNode;
|
||||
|
||||
impl NetworkTab for NetworkNode {
|
||||
fn get_type(&self) -> NetworkTabType {
|
||||
NetworkTabType::Node
|
||||
impl NodeTab for NetworkNode {
|
||||
fn get_type(&self) -> NodeTabType {
|
||||
NodeTabType::Info
|
||||
}
|
||||
|
||||
fn ui(&mut self, ui: &mut egui::Ui, _: &dyn PlatformCallbacks) {
|
||||
// Show an error content when available.
|
||||
let node_err = Node::get_error();
|
||||
if node_err.is_some() {
|
||||
NetworkContent::node_error_ui(ui, node_err.unwrap());
|
||||
return;
|
||||
}
|
||||
|
||||
// Show message to enable node when it's not running.
|
||||
if !Node::is_running() {
|
||||
NetworkContent::disabled_node_ui(ui);
|
||||
return;
|
||||
}
|
||||
|
||||
// Show loading spinner when stats are not available.
|
||||
let server_stats = Node::get_stats();
|
||||
if server_stats.is_none() || Node::is_restarting() || Node::is_stopping() {
|
||||
NetworkContent::loading_ui(ui, None);
|
||||
return;
|
||||
}
|
||||
|
||||
ScrollArea::vertical()
|
||||
.id_source("integrated_node")
|
||||
.id_salt("integrated_node_info_scroll")
|
||||
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
|
||||
.auto_shrink([false; 2])
|
||||
.show(ui, |ui| {
|
||||
ui.add_space(2.0);
|
||||
ui.vertical_centered(|ui| {
|
||||
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
|
||||
// Show node stats content.
|
||||
node_stats_ui(ui);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -79,13 +56,13 @@ fn node_stats_ui(ui: &mut egui::Ui) {
|
|||
View::sub_title(ui, format!("{} {}", FLOW_ARROW, t!("network_node.header")));
|
||||
ui.columns(2, |columns| {
|
||||
columns[0].vertical_centered(|ui| {
|
||||
View::rounded_box(ui,
|
||||
View::label_box(ui,
|
||||
stats.header_stats.last_block_h.to_string(),
|
||||
t!("network_node.hash"),
|
||||
[true, false, false, false]);
|
||||
});
|
||||
columns[1].vertical_centered(|ui| {
|
||||
View::rounded_box(ui,
|
||||
View::label_box(ui,
|
||||
stats.header_stats.height.to_string(),
|
||||
t!("network_node.height"),
|
||||
[false, true, false, false]);
|
||||
|
@ -93,7 +70,7 @@ fn node_stats_ui(ui: &mut egui::Ui) {
|
|||
});
|
||||
ui.columns(2, |columns| {
|
||||
columns[0].vertical_centered(|ui| {
|
||||
View::rounded_box(ui,
|
||||
View::label_box(ui,
|
||||
stats.header_stats.total_difficulty.to_string(),
|
||||
t!("network_node.difficulty"),
|
||||
[false, false, true, false]);
|
||||
|
@ -101,7 +78,7 @@ fn node_stats_ui(ui: &mut egui::Ui) {
|
|||
columns[1].vertical_centered(|ui| {
|
||||
let h_ts = stats.header_stats.latest_timestamp.timestamp();
|
||||
let h_time = View::format_time(h_ts);
|
||||
View::rounded_box(ui,
|
||||
View::label_box(ui,
|
||||
h_time,
|
||||
t!("network_node.time"),
|
||||
[false, false, false, true]);
|
||||
|
@ -113,13 +90,13 @@ fn node_stats_ui(ui: &mut egui::Ui) {
|
|||
View::sub_title(ui, format!("{} {}", CUBE, t!("network_node.block")));
|
||||
ui.columns(2, |columns| {
|
||||
columns[0].vertical_centered(|ui| {
|
||||
View::rounded_box(ui,
|
||||
View::label_box(ui,
|
||||
stats.chain_stats.last_block_h.to_string(),
|
||||
t!("network_node.hash"),
|
||||
[true, false, false, false]);
|
||||
});
|
||||
columns[1].vertical_centered(|ui| {
|
||||
View::rounded_box(ui,
|
||||
View::label_box(ui,
|
||||
stats.chain_stats.height.to_string(),
|
||||
t!("network_node.height"),
|
||||
[false, true, false, false]);
|
||||
|
@ -127,7 +104,7 @@ fn node_stats_ui(ui: &mut egui::Ui) {
|
|||
});
|
||||
ui.columns(2, |columns| {
|
||||
columns[0].vertical_centered(|ui| {
|
||||
View::rounded_box(ui,
|
||||
View::label_box(ui,
|
||||
stats.chain_stats.total_difficulty.to_string(),
|
||||
t!("network_node.difficulty"),
|
||||
[false, false, true, false]);
|
||||
|
@ -135,7 +112,7 @@ fn node_stats_ui(ui: &mut egui::Ui) {
|
|||
columns[1].vertical_centered(|ui| {
|
||||
let b_ts = stats.chain_stats.latest_timestamp.timestamp();
|
||||
let b_time = View::format_time(b_ts);
|
||||
View::rounded_box(ui,
|
||||
View::label_box(ui,
|
||||
b_time,
|
||||
t!("network_node.time"),
|
||||
[false, false, false, true]);
|
||||
|
@ -151,7 +128,7 @@ fn node_stats_ui(ui: &mut egui::Ui) {
|
|||
None => "0 (0)".to_string(),
|
||||
Some(tx) => format!("{} ({})", tx.tx_pool_size, tx.tx_pool_kernels)
|
||||
};
|
||||
View::rounded_box(ui,
|
||||
View::label_box(ui,
|
||||
tx_stat,
|
||||
t!("network_node.main_pool"),
|
||||
[true, false, false, false]);
|
||||
|
@ -163,7 +140,7 @@ fn node_stats_ui(ui: &mut egui::Ui) {
|
|||
stx.stem_pool_size,
|
||||
stx.stem_pool_kernels)
|
||||
};
|
||||
View::rounded_box(ui,
|
||||
View::label_box(ui,
|
||||
stem_tx_stat,
|
||||
t!("network_node.stem_pool"),
|
||||
[false, true, false, false]);
|
||||
|
@ -171,7 +148,7 @@ fn node_stats_ui(ui: &mut egui::Ui) {
|
|||
});
|
||||
ui.columns(2, |columns| {
|
||||
columns[0].vertical_centered(|ui| {
|
||||
View::rounded_box(ui,
|
||||
View::label_box(ui,
|
||||
stats.disk_usage_gb.to_string(),
|
||||
t!("network_node.size"),
|
||||
[false, false, true, false]);
|
||||
|
@ -180,7 +157,7 @@ fn node_stats_ui(ui: &mut egui::Ui) {
|
|||
let peers_txt = format!("{} ({})",
|
||||
stats.peer_count,
|
||||
NodeConfig::get_max_outbound_peers());
|
||||
View::rounded_box(ui, peers_txt, t!("network_node.peers"), [false, false, false, true]);
|
||||
View::label_box(ui, peers_txt, t!("network_node.peers"), [false, false, false, true]);
|
||||
});
|
||||
});
|
||||
ui.add_space(5.0);
|
||||
|
@ -196,23 +173,27 @@ fn node_stats_ui(ui: &mut egui::Ui) {
|
|||
}
|
||||
}
|
||||
|
||||
const PEER_ITEM_HEIGHT: f32 = 77.0;
|
||||
|
||||
/// Draw connected peer info item.
|
||||
fn peer_item_ui(ui: &mut egui::Ui, peer: &PeerStats, rounding: Rounding) {
|
||||
let mut rect = ui.available_rect_before_wrap();
|
||||
rect.set_height(79.0);
|
||||
ui.allocate_ui_at_rect(rect, |ui| {
|
||||
rect.set_height(PEER_ITEM_HEIGHT);
|
||||
ui.allocate_ui(rect.size(), |ui| {
|
||||
ui.vertical(|ui| {
|
||||
ui.add_space(4.0);
|
||||
|
||||
// Draw round background.
|
||||
ui.painter().rect(rect, rounding, Colors::white_or_black(false), View::item_stroke());
|
||||
ui.painter().rect(rect, rounding, Colors::fill_lite(), View::item_stroke());
|
||||
|
||||
// Draw peer address
|
||||
// Draw IP address.
|
||||
ui.horizontal(|ui| {
|
||||
ui.add_space(7.0);
|
||||
ui.label(RichText::new(&peer.addr).color(Colors::white_or_black(true)).size(17.0));
|
||||
ui.label(RichText::new(&peer.addr)
|
||||
.color(Colors::white_or_black(true))
|
||||
.size(17.0));
|
||||
});
|
||||
// Draw peer difficulty and height
|
||||
// Draw difficulty and height.
|
||||
ui.horizontal(|ui| {
|
||||
ui.add_space(6.0);
|
||||
let diff_text = format!("{} {} {} {}",
|
||||
|
@ -220,13 +201,17 @@ fn peer_item_ui(ui: &mut egui::Ui, peer: &PeerStats, rounding: Rounding) {
|
|||
peer.total_difficulty,
|
||||
AT,
|
||||
peer.height);
|
||||
ui.label(RichText::new(diff_text).color(Colors::title(false)).size(16.0));
|
||||
ui.label(RichText::new(diff_text)
|
||||
.color(Colors::title(false))
|
||||
.size(15.0));
|
||||
});
|
||||
// Draw peer user-agent
|
||||
// Draw user-agent.
|
||||
ui.horizontal(|ui| {
|
||||
ui.add_space(6.0);
|
||||
let agent_text = format!("{} {}", DEVICES, &peer.user_agent);
|
||||
ui.label(RichText::new(agent_text).color(Colors::gray()).size(16.0));
|
||||
ui.label(RichText::new(agent_text)
|
||||
.color(Colors::gray())
|
||||
.size(15.0));
|
||||
});
|
||||
|
||||
ui.add_space(3.0);
|
||||
|
|
|
@ -20,7 +20,7 @@ use crate::gui::icons::ARROW_COUNTER_CLOCKWISE;
|
|||
use crate::gui::platform::PlatformCallbacks;
|
||||
use crate::gui::views::{Modal, Content, View};
|
||||
use crate::gui::views::network::setup::{DandelionSetup, NodeSetup, P2PSetup, PoolSetup, StratumSetup};
|
||||
use crate::gui::views::network::types::{NetworkTab, NetworkTabType};
|
||||
use crate::gui::views::network::types::{NodeTab, NodeTabType};
|
||||
use crate::gui::views::types::{ModalContainer, ModalPosition};
|
||||
use crate::node::{Node, NodeConfig};
|
||||
|
||||
|
@ -42,7 +42,7 @@ pub struct NetworkSettings {
|
|||
}
|
||||
|
||||
/// Identifier for settings reset confirmation [`Modal`].
|
||||
pub const RESET_SETTINGS_MODAL: &'static str = "reset_settings";
|
||||
pub const RESET_SETTINGS_CONFIRMATION_MODAL: &'static str = "reset_settings_confirmation";
|
||||
|
||||
impl Default for NetworkSettings {
|
||||
fn default() -> Self {
|
||||
|
@ -53,7 +53,7 @@ impl Default for NetworkSettings {
|
|||
pool: PoolSetup::default(),
|
||||
dandelion: DandelionSetup::default(),
|
||||
modal_ids: vec![
|
||||
RESET_SETTINGS_MODAL
|
||||
RESET_SETTINGS_CONFIRMATION_MODAL
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -69,15 +69,15 @@ impl ModalContainer for NetworkSettings {
|
|||
modal: &Modal,
|
||||
_: &dyn PlatformCallbacks) {
|
||||
match modal.id {
|
||||
RESET_SETTINGS_MODAL => reset_settings_confirmation_modal(ui, modal),
|
||||
RESET_SETTINGS_CONFIRMATION_MODAL => reset_settings_confirmation_modal(ui, modal),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl NetworkTab for NetworkSettings {
|
||||
fn get_type(&self) -> NetworkTabType {
|
||||
NetworkTabType::Settings
|
||||
impl NodeTab for NetworkSettings {
|
||||
fn get_type(&self) -> NodeTabType {
|
||||
NodeTabType::Settings
|
||||
}
|
||||
|
||||
fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||
|
@ -85,7 +85,7 @@ impl NetworkTab for NetworkSettings {
|
|||
self.current_modal_ui(ui, cb);
|
||||
|
||||
ScrollArea::vertical()
|
||||
.id_source("network_settings")
|
||||
.id_salt("node_settings_scroll")
|
||||
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
|
||||
.auto_shrink([false; 2])
|
||||
.show(ui, |ui| {
|
||||
|
@ -210,7 +210,7 @@ fn reset_settings_ui(ui: &mut egui::Ui) {
|
|||
t!("network_settings.reset_settings"));
|
||||
View::action_button(ui, button_text, || {
|
||||
// Show modal to confirm settings reset.
|
||||
Modal::new(RESET_SETTINGS_MODAL)
|
||||
Modal::new(RESET_SETTINGS_CONFIRMATION_MODAL)
|
||||
.position(ModalPosition::Center)
|
||||
.title(t!("confirmation"))
|
||||
.show();
|
||||
|
|
|
@ -141,7 +141,7 @@ impl DandelionSetup {
|
|||
ui.add_space(6.0);
|
||||
|
||||
let epoch = NodeConfig::get_dandelion_epoch();
|
||||
View::button(ui, format!("{} {}", WATCH, epoch.clone()), Colors::button(), || {
|
||||
View::button(ui, format!("{} {}", WATCH, &epoch), Colors::white_or_black(false), || {
|
||||
// Setup values for modal.
|
||||
self.epoch_edit = epoch;
|
||||
// Show epoch setup modal.
|
||||
|
@ -218,8 +218,7 @@ impl DandelionSetup {
|
|||
ui.add_space(6.0);
|
||||
|
||||
let embargo = NodeConfig::get_dandelion_embargo();
|
||||
View::button(ui, format!("{} {}", TIMER, embargo.clone()), Colors::button(), || {
|
||||
// Setup values for modal.
|
||||
View::button(ui, format!("{} {}", TIMER, &embargo), Colors::white_or_black(false), || {
|
||||
self.embargo_edit = embargo;
|
||||
// Show embargo setup modal.
|
||||
Modal::new(EMBARGO_MODAL)
|
||||
|
@ -294,10 +293,10 @@ impl DandelionSetup {
|
|||
);
|
||||
ui.add_space(6.0);
|
||||
|
||||
let agg = NodeConfig::get_dandelion_aggregation();
|
||||
View::button(ui, format!("{} {}", CLOCK_COUNTDOWN, agg.clone()), Colors::button(), || {
|
||||
let ag = NodeConfig::get_dandelion_aggregation();
|
||||
View::button(ui, format!("{} {}", CLOCK_COUNTDOWN, &ag), Colors::white_or_black(false), || {
|
||||
// Setup values for modal.
|
||||
self.aggregation_edit = agg;
|
||||
self.aggregation_edit = ag;
|
||||
// Show aggregation setup modal.
|
||||
Modal::new(AGGREGATION_MODAL)
|
||||
.position(ModalPosition::CenterTop)
|
||||
|
@ -372,7 +371,7 @@ impl DandelionSetup {
|
|||
ui.add_space(6.0);
|
||||
|
||||
let stem_prob = NodeConfig::get_stem_probability();
|
||||
View::button(ui, format!("{}%", stem_prob.clone()), Colors::button(), || {
|
||||
View::button(ui, format!("{}%", &stem_prob), Colors::white_or_black(false), || {
|
||||
// Setup values for modal.
|
||||
self.stem_prob_edit = stem_prob;
|
||||
// Show stem probability setup modal.
|
||||
|
|
|
@ -255,7 +255,7 @@ impl NodeSetup {
|
|||
ui.add_space(6.0);
|
||||
|
||||
let (_, port) = NodeConfig::get_api_ip_port();
|
||||
View::button(ui, format!("{} {}", PLUG, port.clone()), Colors::button(), || {
|
||||
View::button(ui, format!("{} {}", PLUG, &port), Colors::white_or_black(false), || {
|
||||
// Setup values for modal.
|
||||
self.api_port_edit = port;
|
||||
self.api_port_available_edit = self.is_api_port_available;
|
||||
|
@ -283,7 +283,9 @@ impl NodeSetup {
|
|||
fn api_port_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
|
||||
ui.add_space(6.0);
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.label(RichText::new(t!("network_settings.api_port")).size(17.0).color(Colors::gray()));
|
||||
ui.label(RichText::new(t!("network_settings.api_port"))
|
||||
.size(17.0)
|
||||
.color(Colors::gray()));
|
||||
ui.add_space(6.0);
|
||||
|
||||
// Draw API port text edit.
|
||||
|
@ -356,8 +358,8 @@ impl NodeSetup {
|
|||
ui.add_space(6.0);
|
||||
|
||||
let secret_value = match modal_id {
|
||||
API_SECRET_MODAL => NodeConfig::get_api_secret(),
|
||||
_ => NodeConfig::get_foreign_api_secret()
|
||||
API_SECRET_MODAL => NodeConfig::get_api_secret(false),
|
||||
_ => NodeConfig::get_api_secret(true)
|
||||
};
|
||||
|
||||
let secret_text = if secret_value.is_some() {
|
||||
|
@ -366,7 +368,7 @@ impl NodeSetup {
|
|||
format!("{} {}", SHIELD_SLASH, t!("network_settings.disabled"))
|
||||
};
|
||||
|
||||
View::button(ui, secret_text, Colors::button(), || {
|
||||
View::button(ui, secret_text, Colors::white_or_black(false), || {
|
||||
// Setup values for modal.
|
||||
self.secret_edit = secret_value.unwrap_or("".to_string());
|
||||
// Show secret edit modal.
|
||||
|
@ -449,7 +451,9 @@ impl NodeSetup {
|
|||
ui.add_space(6.0);
|
||||
|
||||
let ftl = NodeConfig::get_ftl();
|
||||
View::button(ui, format!("{} {}", CLOCK_CLOCKWISE, ftl.clone()), Colors::button(), || {
|
||||
View::button(ui,
|
||||
format!("{} {}", CLOCK_CLOCKWISE, &ftl),
|
||||
Colors::white_or_black(false), || {
|
||||
// Setup values for modal.
|
||||
self.ftl_edit = ftl;
|
||||
// Show ftl value setup modal.
|
||||
|
|
|
@ -246,7 +246,9 @@ impl P2PSetup {
|
|||
ui.add_space(6.0);
|
||||
|
||||
let port = NodeConfig::get_p2p_port();
|
||||
View::button(ui, format!("{} {}", PLUG, port.clone()), Colors::button(), || {
|
||||
View::button(ui,
|
||||
format!("{} {}", PLUG, &port),
|
||||
Colors::white_or_black(false), || {
|
||||
// Setup values for modal.
|
||||
self.port_edit = port;
|
||||
self.port_available_edit = self.is_port_available;
|
||||
|
@ -306,11 +308,9 @@ impl P2PSetup {
|
|||
// Save port at config if it's available.
|
||||
if available {
|
||||
NodeConfig::save_p2p_port(self.port_edit.parse::<u16>().unwrap());
|
||||
|
||||
if Node::is_running() {
|
||||
Node::restart();
|
||||
}
|
||||
|
||||
self.is_port_available = true;
|
||||
cb.hide_keyboard();
|
||||
modal.close();
|
||||
|
@ -371,9 +371,7 @@ impl P2PSetup {
|
|||
.size(16.0)
|
||||
.color(Colors::inactive_text()));
|
||||
}
|
||||
if !peers.is_empty() {
|
||||
ui.add_space(12.0);
|
||||
}
|
||||
|
||||
let add_text = if peer_type == &PeerType::CustomSeed {
|
||||
format!("{} {}", PLUS_CIRCLE, t!("network_settings.add_seed"))
|
||||
|
@ -381,7 +379,7 @@ impl P2PSetup {
|
|||
format!("{} {}", PLUS_CIRCLE, t!("network_settings.add_peer"))
|
||||
|
||||
};
|
||||
View::button(ui, add_text, Colors::button(), || {
|
||||
View::button(ui, add_text, Colors::white_or_black(false), || {
|
||||
// Setup values for modal.
|
||||
self.is_correct_address_edit = true;
|
||||
self.peer_edit = "".to_string();
|
||||
|
@ -508,7 +506,9 @@ impl P2PSetup {
|
|||
ui.add_space(6.0);
|
||||
|
||||
let ban_window = NodeConfig::get_p2p_ban_window();
|
||||
View::button(ui, format!("{} {}", PROHIBIT_INSET, ban_window.clone()), Colors::button(), || {
|
||||
View::button(ui,
|
||||
format!("{} {}", PROHIBIT_INSET, &ban_window),
|
||||
Colors::white_or_black(false), || {
|
||||
// Setup values for modal.
|
||||
self.ban_window_edit = ban_window;
|
||||
// Show ban window period setup modal.
|
||||
|
@ -590,8 +590,9 @@ impl P2PSetup {
|
|||
ui.add_space(6.0);
|
||||
|
||||
let max_inbound = NodeConfig::get_max_inbound_peers();
|
||||
let button_text = format!("{} {}", ARROW_FAT_LINES_DOWN, max_inbound.clone());
|
||||
View::button(ui, button_text, Colors::button(), || {
|
||||
View::button(ui,
|
||||
format!("{} {}", ARROW_FAT_LINES_DOWN, &max_inbound),
|
||||
Colors::white_or_black(false), || {
|
||||
// Setup values for modal.
|
||||
self.max_inbound_count = max_inbound;
|
||||
// Show maximum number of inbound peers setup modal.
|
||||
|
@ -666,10 +667,10 @@ impl P2PSetup {
|
|||
.color(Colors::gray())
|
||||
);
|
||||
ui.add_space(6.0);
|
||||
|
||||
let max_outbound = NodeConfig::get_max_outbound_peers();
|
||||
let button_text = format!("{} {}", ARROW_FAT_LINES_UP, max_outbound.clone());
|
||||
View::button(ui, button_text, Colors::button(), || {
|
||||
View::button(ui,
|
||||
format!("{} {}", ARROW_FAT_LINES_UP, &max_outbound),
|
||||
Colors::white_or_black(false), || {
|
||||
// Setup values for modal.
|
||||
self.max_outbound_count = max_outbound;
|
||||
// Show maximum number of outbound peers setup modal.
|
||||
|
@ -740,9 +741,10 @@ impl P2PSetup {
|
|||
/// Draw content to reset peers data.
|
||||
fn reset_peers_ui(&mut self, ui: &mut egui::Ui) {
|
||||
ui.add_space(4.0);
|
||||
|
||||
let button_text = format!("{} {}", TRASH, t!("network_settings.reset_peers"));
|
||||
View::colored_text_button(ui, button_text, Colors::red(), Colors::button(), || {
|
||||
View::colored_text_button(ui,
|
||||
format!("{} {}", TRASH, t!("network_settings.reset_peers")),
|
||||
Colors::red(),
|
||||
Colors::white_or_black(false), || {
|
||||
Node::reset_peers(false);
|
||||
self.peers_reset = true;
|
||||
});
|
||||
|
|
|
@ -145,7 +145,7 @@ impl PoolSetup {
|
|||
ui.add_space(6.0);
|
||||
|
||||
let fee = NodeConfig::get_base_fee();
|
||||
View::button(ui, format!("{} {}", HAND_COINS, fee.clone()), Colors::button(), || {
|
||||
View::button(ui, format!("{} {}", HAND_COINS, &fee), Colors::white_or_black(false), || {
|
||||
// Setup values for modal.
|
||||
self.fee_base_edit = fee;
|
||||
// Show fee setup modal.
|
||||
|
@ -195,7 +195,6 @@ impl PoolSetup {
|
|||
modal.close();
|
||||
}
|
||||
};
|
||||
|
||||
ui.columns(2, |columns| {
|
||||
columns[0].vertical_centered_justified(|ui| {
|
||||
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
|
||||
|
@ -220,9 +219,10 @@ impl PoolSetup {
|
|||
.color(Colors::gray())
|
||||
);
|
||||
ui.add_space(6.0);
|
||||
|
||||
let period = NodeConfig::get_reorg_cache_period();
|
||||
View::button(ui, format!("{} {}", CLOCK_COUNTDOWN, period.clone()), Colors::button(), || {
|
||||
View::button(ui,
|
||||
format!("{} {}", CLOCK_COUNTDOWN, &period),
|
||||
Colors::white_or_black(false), || {
|
||||
// Setup values for modal.
|
||||
self.reorg_period_edit = period;
|
||||
// Show reorg period setup modal.
|
||||
|
@ -299,7 +299,7 @@ impl PoolSetup {
|
|||
ui.add_space(6.0);
|
||||
|
||||
let size = NodeConfig::get_max_pool_size();
|
||||
View::button(ui, format!("{} {}", CIRCLES_THREE, size.clone()), Colors::button(), || {
|
||||
View::button(ui, format!("{} {}", CIRCLES_THREE, size), Colors::white_or_black(false), || {
|
||||
// Setup values for modal.
|
||||
self.pool_size_edit = size;
|
||||
// Show pool size setup modal.
|
||||
|
@ -376,7 +376,9 @@ impl PoolSetup {
|
|||
ui.add_space(6.0);
|
||||
|
||||
let size = NodeConfig::get_max_stempool_size();
|
||||
View::button(ui, format!("{} {}", BEZIER_CURVE, size.clone()), Colors::button(), || {
|
||||
View::button(ui,
|
||||
format!("{} {}", BEZIER_CURVE, &size),
|
||||
Colors::white_or_black(false), || {
|
||||
// Setup values for modal.
|
||||
self.stempool_size_edit = size;
|
||||
// Show stempool size setup modal.
|
||||
|
@ -453,7 +455,9 @@ impl PoolSetup {
|
|||
ui.add_space(6.0);
|
||||
|
||||
let weight = NodeConfig::get_mineable_max_weight();
|
||||
View::button(ui, format!("{} {}", BOUNDING_BOX, weight.clone()), Colors::button(), || {
|
||||
View::button(ui,
|
||||
format!("{} {}", BOUNDING_BOX, &weight),
|
||||
Colors::white_or_black(false), || {
|
||||
// Setup values for modal.
|
||||
self.max_weight_edit = weight;
|
||||
// Show total tx weight setup modal.
|
||||
|
|
|
@ -44,7 +44,7 @@ pub struct StratumSetup {
|
|||
is_port_available: bool,
|
||||
|
||||
/// Wallet name to receive rewards.
|
||||
wallet_name: Option<String>,
|
||||
pub wallet_name: Option<String>,
|
||||
|
||||
/// Attempt time value in seconds to mine on a particular header.
|
||||
attempt_time_edit: String,
|
||||
|
@ -189,7 +189,9 @@ impl StratumSetup {
|
|||
ui.add_space(8.0);
|
||||
|
||||
// Show button to select wallet.
|
||||
View::button(ui, t!("network_settings.choose_wallet"), Colors::button(), || {
|
||||
View::button(ui,
|
||||
t!("network_settings.choose_wallet"),
|
||||
Colors::white_or_black(false), || {
|
||||
self.show_wallets_modal();
|
||||
});
|
||||
ui.add_space(12.0);
|
||||
|
@ -260,7 +262,7 @@ impl StratumSetup {
|
|||
ui.add_space(6.0);
|
||||
|
||||
let (_, port) = NodeConfig::get_stratum_address();
|
||||
View::button(ui, format!("{} {}", PLUG, port.clone()), Colors::button(), || {
|
||||
View::button(ui, format!("{} {}", PLUG, &port), Colors::white_or_black(false), || {
|
||||
// Setup values for modal.
|
||||
self.stratum_port_edit = port;
|
||||
self.stratum_port_available_edit = self.is_port_available;
|
||||
|
@ -359,7 +361,7 @@ impl StratumSetup {
|
|||
ui.add_space(6.0);
|
||||
|
||||
let time = NodeConfig::get_stratum_attempt_time();
|
||||
View::button(ui, format!("{} {}", TIMER, time.clone()), Colors::button(), || {
|
||||
View::button(ui, format!("{} {}", TIMER, &time), Colors::white_or_black(false), || {
|
||||
// Setup values for modal.
|
||||
self.attempt_time_edit = time;
|
||||
|
||||
|
@ -442,7 +444,7 @@ impl StratumSetup {
|
|||
ui.add_space(6.0);
|
||||
|
||||
let diff = NodeConfig::get_stratum_min_share_diff();
|
||||
View::button(ui, format!("{} {}", BARBELL, diff.clone()), Colors::button(), || {
|
||||
View::button(ui, format!("{} {}", BARBELL, &diff), Colors::white_or_black(false), || {
|
||||
// Setup values for modal.
|
||||
self.min_share_diff_edit = diff;
|
||||
|
||||
|
|
|
@ -14,28 +14,28 @@
|
|||
|
||||
use crate::gui::platform::PlatformCallbacks;
|
||||
|
||||
/// Network tab content interface.
|
||||
pub trait NetworkTab {
|
||||
fn get_type(&self) -> NetworkTabType;
|
||||
/// Integrated node tab content interface.
|
||||
pub trait NodeTab {
|
||||
fn get_type(&self) -> NodeTabType;
|
||||
fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks);
|
||||
}
|
||||
|
||||
/// Type of [`NetworkTab`] content.
|
||||
/// Type of [`NodeTab`] content.
|
||||
#[derive(PartialEq)]
|
||||
pub enum NetworkTabType {
|
||||
Node,
|
||||
pub enum NodeTabType {
|
||||
Info,
|
||||
Metrics,
|
||||
Mining,
|
||||
Settings
|
||||
}
|
||||
|
||||
impl NetworkTabType {
|
||||
impl NodeTabType {
|
||||
pub fn title(&self) -> String {
|
||||
match *self {
|
||||
NetworkTabType::Node => { t!("network.node") }
|
||||
NetworkTabType::Metrics => { t!("network.metrics") }
|
||||
NetworkTabType::Mining => { t!("network.mining") }
|
||||
NetworkTabType::Settings => { t!("network.settings") }
|
||||
NodeTabType::Info => { t!("network.node") }
|
||||
NodeTabType::Metrics => { t!("network.metrics") }
|
||||
NodeTabType::Mining => { t!("network.mining") }
|
||||
NodeTabType::Settings => { t!("network.settings") }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -13,7 +13,7 @@
|
|||
// limitations under the License.
|
||||
|
||||
use egui::scroll_area::ScrollAreaOutput;
|
||||
use egui::{Sense, Align2, Area, Color32, Id, Rect, Response, Widget, Vec2};
|
||||
use egui::{Sense, Align2, Area, Color32, Id, Rect, Response, Widget, Vec2, UiBuilder};
|
||||
use egui::epaint::{emath::lerp, vec2, Pos2, Shape, Stroke};
|
||||
|
||||
/// A spinner widget used to indicate loading.
|
||||
|
@ -195,7 +195,9 @@ impl PullToRefresh {
|
|||
ui: &mut egui::Ui,
|
||||
content: impl FnOnce(&mut egui::Ui) -> T,
|
||||
) -> PullToRefreshResponse<T> {
|
||||
let mut child = ui.child_ui(ui.available_rect_before_wrap(), *ui.layout(), None);
|
||||
let mut child = ui.new_child(UiBuilder::new()
|
||||
.max_rect(ui.available_rect_before_wrap())
|
||||
.layout(*ui.layout()));
|
||||
|
||||
let output = content(&mut child);
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ use std::mem::size_of;
|
|||
use std::sync::Arc;
|
||||
use parking_lot::RwLock;
|
||||
use std::thread;
|
||||
use egui::{SizeHint, TextureHandle};
|
||||
use egui::{SizeHint, TextureHandle, UiBuilder};
|
||||
use egui::epaint::RectShape;
|
||||
use image::{ExtendedColorType, ImageEncoder};
|
||||
use image::codecs::png::{CompressionType, FilterType, PngEncoder};
|
||||
|
@ -235,26 +235,23 @@ impl QrCodeContent {
|
|||
rect.max -= egui::emath::vec2(10.0, 0.0);
|
||||
|
||||
// Create background shape.
|
||||
let mut bg_shape = RectShape {
|
||||
let mut bg_shape = RectShape::new(
|
||||
rect,
|
||||
rounding: egui::Rounding::default(),
|
||||
fill: egui::Color32::WHITE,
|
||||
stroke: egui::Stroke::NONE,
|
||||
blur_width: 0.0,
|
||||
fill_texture_id: Default::default(),
|
||||
uv: egui::Rect::ZERO
|
||||
};
|
||||
egui::Rounding::default(),
|
||||
egui::Color32::WHITE,
|
||||
egui::Stroke::NONE
|
||||
);
|
||||
let bg_idx = ui.painter().add(bg_shape);
|
||||
|
||||
// Draw QR code image content.
|
||||
let mut content_rect = ui.allocate_ui_at_rect(rect, |ui| {
|
||||
let mut content_rect = ui.allocate_new_ui(UiBuilder::new().max_rect(rect), |ui| {
|
||||
ui.add_space(10.0);
|
||||
let size = SizeHint::Size(ui.available_width() as u32, ui.available_width() as u32);
|
||||
self.texture_handle = Some(View::svg_image(ui, "qr_code", svg.as_slice(), Some(size)));
|
||||
ui.add_space(10.0);
|
||||
}).response.rect;
|
||||
|
||||
// Setup background shape to be painted behind content.
|
||||
// Setup background size.
|
||||
content_rect.min -= egui::emath::vec2(10.0, 0.0);
|
||||
content_rect.max += egui::emath::vec2(10.0, 0.0);
|
||||
bg_shape.rect = content_rect;
|
||||
|
|
|
@ -32,7 +32,7 @@ pub struct CameraScanModal {
|
|||
impl Default for CameraScanModal {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
camera_content: None,
|
||||
camera_content: Some(CameraContent::default()),
|
||||
qr_scan_result: None,
|
||||
}
|
||||
}
|
||||
|
@ -51,7 +51,7 @@ impl CameraScanModal {
|
|||
View::horizontal_line(ui, Colors::item_stroke());
|
||||
ui.add_space(3.0);
|
||||
ScrollArea::vertical()
|
||||
.id_source(Id::from("qr_scan_result_input"))
|
||||
.id_salt(Id::from("qr_scan_result_input"))
|
||||
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
|
||||
.max_height(128.0)
|
||||
.auto_shrink([false; 2])
|
||||
|
@ -72,7 +72,7 @@ impl CameraScanModal {
|
|||
// Show copy button.
|
||||
ui.vertical_centered(|ui| {
|
||||
let copy_text = format!("{} {}", COPY, t!("copy"));
|
||||
View::button(ui, copy_text, Colors::button(), || {
|
||||
View::button(ui, copy_text, Colors::white_or_black(false), || {
|
||||
cb.copy_string_to_buffer(result_text.to_string());
|
||||
self.qr_scan_result = None;
|
||||
modal.close();
|
||||
|
@ -81,22 +81,7 @@ impl CameraScanModal {
|
|||
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);
|
||||
|
||||
|
@ -117,7 +102,20 @@ impl CameraScanModal {
|
|||
});
|
||||
});
|
||||
});
|
||||
} else if let Some(camera_content) = self.camera_content.as_mut() {
|
||||
if let Some(result) = camera_content.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 {
|
||||
// Draw camera content.
|
||||
ui.add_space(6.0);
|
||||
self.camera_content.as_mut().unwrap().ui(ui, cb);
|
||||
ui.add_space(12.0);
|
||||
ui.vertical_centered_justified(|ui| {
|
||||
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
|
||||
cb.stop_camera();
|
||||
|
@ -126,6 +124,7 @@ impl CameraScanModal {
|
|||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
ui.add_space(6.0);
|
||||
}
|
||||
}
|
|
@ -12,11 +12,11 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use egui::{Margin, Id, Layout, Align};
|
||||
use egui::{Margin, Id, Layout, Align, UiBuilder};
|
||||
|
||||
use crate::gui::Colors;
|
||||
use crate::gui::views::{Content, View};
|
||||
use crate::gui::views::types::{TitleContentType, TitleType};
|
||||
use crate::gui::views::types::{LinePosition, TitleContentType, TitleType};
|
||||
|
||||
/// Title panel with left/right action buttons and text in the middle.
|
||||
pub struct TitlePanel {
|
||||
|
@ -25,8 +25,8 @@ pub struct TitlePanel {
|
|||
}
|
||||
|
||||
impl TitlePanel {
|
||||
/// Default [`TitlePanel`] content height.
|
||||
pub const DEFAULT_HEIGHT: f32 = 54.0;
|
||||
/// Content height.
|
||||
pub const HEIGHT: f32 = 54.0;
|
||||
|
||||
/// Create new title panel with provided identifier.
|
||||
pub fn new(id: Id) -> Self {
|
||||
|
@ -43,7 +43,7 @@ impl TitlePanel {
|
|||
// Draw title panel.
|
||||
egui::TopBottomPanel::top(self.id)
|
||||
.resizable(false)
|
||||
.exact_height(Self::DEFAULT_HEIGHT + View::get_top_inset())
|
||||
.exact_height(Self::HEIGHT + View::get_top_inset())
|
||||
.frame(egui::Frame {
|
||||
inner_margin: Margin {
|
||||
left: View::far_left_inset_margin(ui),
|
||||
|
@ -51,7 +51,6 @@ impl TitlePanel {
|
|||
top: View::get_top_inset(),
|
||||
bottom: 0.0,
|
||||
},
|
||||
fill: Colors::yellow(),
|
||||
..Default::default()
|
||||
})
|
||||
.show_inside(ui, |ui| {
|
||||
|
@ -68,40 +67,51 @@ impl TitlePanel {
|
|||
match title {
|
||||
TitleType::Single(content) => {
|
||||
let content_rect = {
|
||||
let mut r = rect;
|
||||
r.min.x += Self::DEFAULT_HEIGHT;
|
||||
r.max.x -= Self::DEFAULT_HEIGHT;
|
||||
let mut r = rect.clone();
|
||||
r.min.x += Self::HEIGHT;
|
||||
r.max.x -= Self::HEIGHT;
|
||||
r
|
||||
};
|
||||
ui.allocate_ui_at_rect(content_rect, |ui| {
|
||||
ui.allocate_new_ui(UiBuilder::new().max_rect(content_rect), |ui| {
|
||||
Self::title_text_content(ui, content);
|
||||
});
|
||||
}
|
||||
TitleType::Dual(first, second) => {
|
||||
let first_rect = {
|
||||
let mut r = rect;
|
||||
r.max.x = r.min.x + Content::SIDE_PANEL_WIDTH - Self::DEFAULT_HEIGHT;
|
||||
r.min.x += Self::DEFAULT_HEIGHT;
|
||||
let mut r = rect.clone();
|
||||
r.max.x = r.min.x + Content::SIDE_PANEL_WIDTH - Self::HEIGHT;
|
||||
r.min.x += Self::HEIGHT;
|
||||
r
|
||||
};
|
||||
// Draw first title content.
|
||||
ui.allocate_ui_at_rect(first_rect, |ui| {
|
||||
ui.allocate_new_ui(UiBuilder::new().max_rect(first_rect), |ui| {
|
||||
Self::title_text_content(ui, first);
|
||||
});
|
||||
|
||||
let second_rect = {
|
||||
let mut r = rect;
|
||||
r.min.x = first_rect.max.x + 2.0 * Self::DEFAULT_HEIGHT;
|
||||
r.max.x -= Self::DEFAULT_HEIGHT;
|
||||
let mut r = rect.clone();
|
||||
r.min.x = first_rect.max.x + 2.0 * Self::HEIGHT;
|
||||
r.max.x -= Self::HEIGHT;
|
||||
r
|
||||
};
|
||||
// Draw second title content.
|
||||
ui.allocate_ui_at_rect(second_rect, |ui| {
|
||||
ui.allocate_new_ui(UiBuilder::new().max_rect(second_rect), |ui| {
|
||||
Self::title_text_content(ui, second);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Draw content divider line.
|
||||
let r = {
|
||||
let mut r = rect.clone();
|
||||
r.min.x -= View::far_left_inset_margin(ui);
|
||||
r.max.x += View::far_right_inset_margin(ui);
|
||||
r
|
||||
};
|
||||
if Content::is_dual_panel_mode(ui.ctx()) {
|
||||
View::line(ui, LinePosition::BOTTOM, &r, Colors::stroke());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -115,11 +125,11 @@ impl TitlePanel {
|
|||
} else {
|
||||
0.0
|
||||
});
|
||||
View::ellipsize_text(ui, text, 19.0, Colors::title(true));
|
||||
View::ellipsize_text(ui, text.to_uppercase(), 19.0, Colors::title(true));
|
||||
}
|
||||
TitleContentType::WithSubTitle(text, subtitle, animate) => {
|
||||
ui.add_space(4.0);
|
||||
View::ellipsize_text(ui, text, 18.0, Colors::title(true));
|
||||
View::ellipsize_text(ui, text.to_uppercase(), 18.0, Colors::title(true));
|
||||
ui.add_space(-2.0);
|
||||
View::animate_text(ui, subtitle, 15.0, Colors::text(true), animate)
|
||||
}
|
||||
|
|
|
@ -32,6 +32,11 @@ pub enum TitleContentType {
|
|||
WithSubTitle(String, String, bool)
|
||||
}
|
||||
|
||||
/// Stroke position against content.
|
||||
pub enum LinePosition {
|
||||
TOP, LEFT, RIGHT, BOTTOM
|
||||
}
|
||||
|
||||
/// Position of [`Modal`] on the screen.
|
||||
#[derive(Clone)]
|
||||
pub enum ModalPosition {
|
||||
|
|
|
@ -17,8 +17,8 @@ use std::sync::Arc;
|
|||
use parking_lot::RwLock;
|
||||
use lazy_static::lazy_static;
|
||||
|
||||
use egui::{Align, Button, CursorIcon, Layout, lerp, PointerState, Rect, Response, Rgba, RichText, Sense, SizeHint, Spinner, TextBuffer, TextStyle, TextureHandle, TextureOptions, Widget};
|
||||
use egui::epaint::{Color32, FontId, RectShape, Rounding, Stroke};
|
||||
use egui::{Align, Button, CursorIcon, Layout, lerp, PointerState, Rect, Response, Rgba, RichText, Sense, SizeHint, Spinner, TextBuffer, TextStyle, TextureHandle, TextureOptions, Widget, UiBuilder};
|
||||
use egui::epaint::{Color32, FontId, PathShape, PathStroke, RectShape, Rounding, Stroke};
|
||||
use egui::epaint::text::TextWrapping;
|
||||
use egui::load::SizedTexture;
|
||||
use egui::os::OperatingSystem;
|
||||
|
@ -30,7 +30,7 @@ use crate::AppConfig;
|
|||
use crate::gui::Colors;
|
||||
use crate::gui::icons::{CHECK_SQUARE, CLIPBOARD_TEXT, COPY, EYE, EYE_SLASH, SCAN, SQUARE};
|
||||
use crate::gui::platform::PlatformCallbacks;
|
||||
use crate::gui::views::types::TextEditOptions;
|
||||
use crate::gui::views::types::{LinePosition, TextEditOptions};
|
||||
|
||||
pub struct View;
|
||||
|
||||
|
@ -78,14 +78,16 @@ impl View {
|
|||
rect.set_width(width);
|
||||
|
||||
// Draw content.
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.allocate_ui(rect.size(), |ui| {
|
||||
(add_content)(ui);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// Get width and height of app window.
|
||||
pub fn window_size(ui: &egui::Ui) -> (f32, f32) {
|
||||
let rect = ui.ctx().screen_rect();
|
||||
pub fn window_size(ctx: &egui::Context) -> (f32, f32) {
|
||||
let rect = ctx.screen_rect();
|
||||
(rect.width(), rect.height())
|
||||
}
|
||||
|
||||
|
@ -108,7 +110,7 @@ impl View {
|
|||
/// Calculate margin for far left view based on display insets (cutouts).
|
||||
pub fn far_right_inset_margin(ui: &mut egui::Ui) -> f32 {
|
||||
let container_width = ui.available_rect_before_wrap().max.x as i32;
|
||||
let window_size = Self::window_size(ui);
|
||||
let window_size = Self::window_size(ui.ctx());
|
||||
let display_width = window_size.0 as i32;
|
||||
// Means end of the screen.
|
||||
if container_width == display_width {
|
||||
|
@ -235,7 +237,7 @@ impl View {
|
|||
ui.style_mut().visuals.widgets.active.expansion = 0.0;
|
||||
// Setup fill colors.
|
||||
ui.visuals_mut().widgets.inactive.weak_bg_fill = Colors::white_or_black(false);
|
||||
ui.visuals_mut().widgets.hovered.weak_bg_fill = Colors::button();
|
||||
ui.visuals_mut().widgets.hovered.weak_bg_fill = Colors::fill_lite();
|
||||
ui.visuals_mut().widgets.active.weak_bg_fill = Colors::fill();
|
||||
// Setup stroke colors.
|
||||
ui.visuals_mut().widgets.inactive.bg_stroke = Self::default_stroke();
|
||||
|
@ -325,7 +327,7 @@ impl View {
|
|||
action: impl FnOnce()) {
|
||||
// Setup button size.
|
||||
let mut rect = ui.available_rect_before_wrap();
|
||||
rect.set_width(32.0);
|
||||
rect.set_width(42.0);
|
||||
let button_size = rect.size();
|
||||
|
||||
ui.scope(|ui| {
|
||||
|
@ -336,12 +338,12 @@ impl View {
|
|||
ui.style_mut().visuals.widgets.active.expansion = 0.0;
|
||||
// Setup fill colors.
|
||||
ui.visuals_mut().widgets.inactive.weak_bg_fill = Colors::white_or_black(false);
|
||||
ui.visuals_mut().widgets.hovered.weak_bg_fill = Colors::button();
|
||||
ui.visuals_mut().widgets.hovered.weak_bg_fill = Colors::fill_lite();
|
||||
ui.visuals_mut().widgets.active.weak_bg_fill = Colors::fill();
|
||||
// Setup stroke colors.
|
||||
ui.visuals_mut().widgets.inactive.bg_stroke = Self::default_stroke();
|
||||
ui.visuals_mut().widgets.hovered.bg_stroke = Self::hover_stroke();
|
||||
ui.visuals_mut().widgets.active.bg_stroke = Self::item_stroke();
|
||||
// Disable strokes.
|
||||
ui.visuals_mut().widgets.inactive.bg_stroke = Stroke::NONE;
|
||||
ui.visuals_mut().widgets.hovered.bg_stroke = Stroke::NONE;
|
||||
ui.visuals_mut().widgets.active.bg_stroke = Stroke::NONE;
|
||||
|
||||
// Setup button text color.
|
||||
let text_color = if let Some(c) = color { c } else { Colors::item_button() };
|
||||
|
@ -353,9 +355,18 @@ impl View {
|
|||
.ui(ui)
|
||||
.on_hover_cursor(CursorIcon::PointingHand);
|
||||
br.surrender_focus();
|
||||
if Self::touched(ui, br) {
|
||||
if Self::touched(ui, br.clone()) {
|
||||
(action)();
|
||||
}
|
||||
|
||||
// Draw stroke.
|
||||
let r = {
|
||||
let mut r = ui.available_rect_before_wrap();
|
||||
r.min = br.rect.min;
|
||||
r.min.x += 0.5;
|
||||
r
|
||||
};
|
||||
Self::line(ui, LinePosition::LEFT, &r, Colors::item_stroke());
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -529,28 +540,20 @@ impl View {
|
|||
/// where is r = (top_left, top_right, bottom_left, bottom_right).
|
||||
/// | VALUE |
|
||||
/// | label |
|
||||
pub fn rounded_box(ui: &mut egui::Ui, value: String, label: String, r: [bool; 4]) {
|
||||
pub fn label_box(ui: &mut egui::Ui, text: String, label: String, r: [bool; 4]) {
|
||||
let rect = ui.available_rect_before_wrap();
|
||||
|
||||
// Create background shape.
|
||||
let mut bg_shape = RectShape {
|
||||
rect,
|
||||
rounding: Rounding {
|
||||
let mut bg_shape = RectShape::new(rect, Rounding {
|
||||
nw: if r[0] { 8.0 } else { 0.0 },
|
||||
ne: if r[1] { 8.0 } else { 0.0 },
|
||||
sw: if r[2] { 8.0 } else { 0.0 },
|
||||
se: if r[3] { 8.0 } else { 0.0 },
|
||||
},
|
||||
fill: Colors::TRANSPARENT,
|
||||
stroke: Self::item_stroke(),
|
||||
blur_width: 0.0,
|
||||
fill_texture_id: Default::default(),
|
||||
uv: Rect::ZERO
|
||||
};
|
||||
}, Colors::fill_lite(), Self::item_stroke());
|
||||
let bg_idx = ui.painter().add(bg_shape);
|
||||
|
||||
// Draw box content.
|
||||
let content_resp = ui.allocate_ui_at_rect(rect, |ui| {
|
||||
let content_resp = ui.allocate_new_ui(UiBuilder::new().max_rect(rect), |ui| {
|
||||
ui.vertical_centered_justified(|ui| {
|
||||
ui.add_space(4.0);
|
||||
ui.scope(|ui| {
|
||||
|
@ -558,7 +561,7 @@ impl View {
|
|||
ui.style_mut().spacing.item_spacing.y = -3.0;
|
||||
|
||||
// Draw box value.
|
||||
let mut job = LayoutJob::single_section(value, TextFormat {
|
||||
let mut job = LayoutJob::single_section(text, TextFormat {
|
||||
font_id: FontId::proportional(17.0),
|
||||
color: Colors::white_or_black(true),
|
||||
..Default::default()
|
||||
|
@ -578,7 +581,7 @@ impl View {
|
|||
});
|
||||
}).response;
|
||||
|
||||
// Setup background shape to be painted behind box content.
|
||||
// Setup background shape size.
|
||||
bg_shape.rect = content_resp.rect;
|
||||
ui.painter().set(bg_idx, bg_shape);
|
||||
}
|
||||
|
@ -590,7 +593,7 @@ impl View {
|
|||
let side_margin = 28.0;
|
||||
rect.min += egui::emath::vec2(side_margin, ui.available_height() / 2.0 - height / 2.0);
|
||||
rect.max -= egui::emath::vec2(side_margin, 0.0);
|
||||
ui.allocate_ui_at_rect(rect, |ui| {
|
||||
ui.allocate_new_ui(UiBuilder::new().max_rect(rect), |ui| {
|
||||
(content)(ui);
|
||||
});
|
||||
});
|
||||
|
@ -653,6 +656,47 @@ impl View {
|
|||
Stroke { width: 1.0, color });
|
||||
}
|
||||
|
||||
/// Draw line for panel content.
|
||||
pub fn line(ui: &mut egui::Ui, pos: LinePosition, rect: &Rect, color: Color32) {
|
||||
let points = match pos {
|
||||
LinePosition::RIGHT => {
|
||||
vec![{
|
||||
let mut r = rect.clone();
|
||||
r.min.x = r.max.x;
|
||||
r.min
|
||||
}, rect.max]
|
||||
}
|
||||
LinePosition::BOTTOM => {
|
||||
vec![{
|
||||
let mut r = rect.clone();
|
||||
r.min.y = r.max.y;
|
||||
r.min
|
||||
}, rect.max]
|
||||
}
|
||||
LinePosition::LEFT => {
|
||||
vec![rect.min, {
|
||||
let mut r = rect.clone();
|
||||
r.max.x = r.min.x;
|
||||
r.max
|
||||
}]
|
||||
}
|
||||
LinePosition::TOP => {
|
||||
vec![rect.min, {
|
||||
let mut r = rect.clone();
|
||||
r.max.y = r.min.y;
|
||||
r.max
|
||||
}]
|
||||
}
|
||||
};
|
||||
let stroke = PathShape {
|
||||
points,
|
||||
closed: false,
|
||||
fill: Default::default(),
|
||||
stroke: PathStroke::new(1.0, color),
|
||||
};
|
||||
ui.painter().add(stroke);
|
||||
}
|
||||
|
||||
/// Draw SVG image from provided data with optional provided size.
|
||||
pub fn svg_image(ui: &mut egui::Ui,
|
||||
name: &str,
|
||||
|
@ -698,6 +742,19 @@ impl View {
|
|||
);
|
||||
}
|
||||
|
||||
/// Draw semi-transparent cover at specified area.
|
||||
pub fn content_cover_ui(ui: &mut egui::Ui,
|
||||
rect: Rect,
|
||||
id: impl std::hash::Hash,
|
||||
mut on_click: impl FnMut()) {
|
||||
let resp = ui.interact(rect, egui::Id::new(id), Sense::click_and_drag());
|
||||
if resp.clicked() || resp.dragged() {
|
||||
on_click();
|
||||
}
|
||||
let shape = RectShape::filled(resp.rect, Rounding::ZERO, Colors::semi_transparent());
|
||||
ui.painter().add(shape);
|
||||
}
|
||||
|
||||
/// Get top display inset (cutout) size.
|
||||
pub fn get_top_inset() -> f32 {
|
||||
TOP_DISPLAY_INSET.load(Ordering::Relaxed) as f32
|
||||
|
|
|
@ -21,7 +21,7 @@ use crate::gui::Colors;
|
|||
use crate::gui::icons::{ARROW_LEFT, CARET_RIGHT, COMPUTER_TOWER, FOLDER_OPEN, FOLDER_PLUS, GEAR, GLOBE, GLOBE_SIMPLE, LOCK_KEY, PLUS, SIDEBAR_SIMPLE, SUITCASE};
|
||||
use crate::gui::platform::PlatformCallbacks;
|
||||
use crate::gui::views::{Modal, Content, TitlePanel, View};
|
||||
use crate::gui::views::types::{ModalContainer, ModalPosition, TitleContentType, TitleType};
|
||||
use crate::gui::views::types::{ModalContainer, ModalPosition, LinePosition, TitleContentType, TitleType};
|
||||
use crate::gui::views::wallets::creation::WalletCreation;
|
||||
use crate::gui::views::wallets::modals::{AddWalletModal, OpenWalletModal, WalletConnectionModal, WalletsModal};
|
||||
use crate::gui::views::wallets::types::WalletTabType;
|
||||
|
@ -149,14 +149,15 @@ impl WalletsContent {
|
|||
|
||||
let creating_wallet = self.creating_wallet();
|
||||
let showing_wallet = self.showing_wallet() && !creating_wallet;
|
||||
let dual_panel = is_dual_panel_mode(ui);
|
||||
let dual_panel = Self::is_dual_panel_mode(ui);
|
||||
let content_width = ui.available_width();
|
||||
let list_hidden = creating_wallet || self.wallets.list().is_empty()
|
||||
|| (showing_wallet && self.wallet_content.as_ref().unwrap().qr_scan_content.is_some())
|
||||
|| (dual_panel && showing_wallet && !self.show_wallets_at_dual_panel)
|
||||
|| (!dual_panel && showing_wallet);
|
||||
|
||||
// Show title panel.
|
||||
self.title_ui(ui, dual_panel, showing_wallet);
|
||||
self.title_ui(ui, dual_panel, showing_wallet, cb);
|
||||
|
||||
if showing_wallet {
|
||||
egui::SidePanel::right("wallet_panel")
|
||||
|
@ -167,7 +168,6 @@ impl WalletsContent {
|
|||
content_width - Content::SIDE_PANEL_WIDTH
|
||||
})
|
||||
.frame(egui::Frame {
|
||||
fill: Colors::fill_deep(),
|
||||
..Default::default()
|
||||
})
|
||||
.show_inside(ui, |ui| {
|
||||
|
@ -181,17 +181,19 @@ impl WalletsContent {
|
|||
if !list_hidden {
|
||||
egui::TopBottomPanel::bottom("wallets_bottom_panel")
|
||||
.frame(egui::Frame {
|
||||
fill: Colors::fill(),
|
||||
inner_margin: Margin {
|
||||
left: View::far_left_inset_margin(ui) + View::TAB_ITEMS_PADDING,
|
||||
right: View::far_right_inset_margin(ui) + View::TAB_ITEMS_PADDING,
|
||||
top: View::TAB_ITEMS_PADDING,
|
||||
bottom: View::get_bottom_inset() + View::TAB_ITEMS_PADDING,
|
||||
},
|
||||
fill: Colors::fill(),
|
||||
..Default::default()
|
||||
})
|
||||
.resizable(false)
|
||||
.show_inside(ui, |ui| {
|
||||
let rect = ui.available_rect_before_wrap();
|
||||
|
||||
// Setup spacing between tabs.
|
||||
ui.style_mut().spacing.item_spacing = egui::vec2(View::TAB_ITEMS_PADDING, 0.0);
|
||||
// Setup vertical padding inside buttons.
|
||||
|
@ -203,6 +205,16 @@ impl WalletsContent {
|
|||
self.show_add_wallet_modal(cb);
|
||||
});
|
||||
});
|
||||
|
||||
// Draw content divider line.
|
||||
let r = {
|
||||
let mut r = rect.clone();
|
||||
r.min.y -= View::TAB_ITEMS_PADDING;
|
||||
r.min.x -= View::TAB_ITEMS_PADDING;
|
||||
r.max.x += View::TAB_ITEMS_PADDING;
|
||||
r
|
||||
};
|
||||
View::line(ui, LinePosition::TOP, &r, Colors::stroke());
|
||||
});
|
||||
|
||||
egui::SidePanel::left("wallet_list_panel")
|
||||
|
@ -213,18 +225,17 @@ impl WalletsContent {
|
|||
})
|
||||
.resizable(false)
|
||||
.frame(egui::Frame {
|
||||
stroke: View::item_stroke(),
|
||||
fill: Colors::fill_deep(),
|
||||
inner_margin: Margin {
|
||||
left: View::far_left_inset_margin(ui) + 4.0,
|
||||
right: View::far_right_inset_margin(ui) + 4.0,
|
||||
top: 3.0,
|
||||
bottom: 4.0,
|
||||
},
|
||||
fill: Colors::fill_deep(),
|
||||
..Default::default()
|
||||
})
|
||||
.show_inside(ui, |ui| {
|
||||
if !list_hidden && !dual_panel && !showing_wallet && !creating_wallet {
|
||||
if !dual_panel && !showing_wallet {
|
||||
ui.ctx().request_repaint_after(Duration::from_millis(1000));
|
||||
}
|
||||
// Show wallet list.
|
||||
|
@ -232,12 +243,10 @@ impl WalletsContent {
|
|||
});
|
||||
}
|
||||
|
||||
// Show central panel with wallet creation.
|
||||
egui::CentralPanel::default()
|
||||
.frame(egui::Frame {
|
||||
stroke: View::item_stroke(),
|
||||
fill: if self.creation_content.is_some() {
|
||||
Colors::white_or_black(false)
|
||||
fill: if creating_wallet {
|
||||
Colors::TRANSPARENT
|
||||
} else {
|
||||
Colors::fill_deep()
|
||||
},
|
||||
|
@ -276,6 +285,8 @@ impl WalletsContent {
|
|||
self.show_add_wallet_modal(cb);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -284,7 +295,8 @@ impl WalletsContent {
|
|||
pub fn showing_wallet(&self) -> bool {
|
||||
if let Some(wallet_content) = &self.wallet_content {
|
||||
let w = &wallet_content.wallet;
|
||||
return w.is_open() && w.get_config().chain_type == AppConfig::chain_type();
|
||||
return w.is_open() && !w.is_deleted() &&
|
||||
w.get_config().chain_type == AppConfig::chain_type();
|
||||
}
|
||||
false
|
||||
}
|
||||
|
@ -301,27 +313,20 @@ impl WalletsContent {
|
|||
return;
|
||||
}
|
||||
// Close network panel on single panel mode.
|
||||
if !Content::is_dual_panel_mode(ui) && Content::is_network_panel_open() {
|
||||
if !Content::is_dual_panel_mode(ui.ctx()) && Content::is_network_panel_open() {
|
||||
Content::toggle_network_panel();
|
||||
}
|
||||
// Pass data to opened selected wallet or show wallets selection.
|
||||
if self.wallet_content.is_some() {
|
||||
if self.showing_wallet() {
|
||||
// Pass data to single wallet or show wallets selection.
|
||||
if wallets_size == 1 {
|
||||
let wallet_content = self.wallet_content.as_mut().unwrap();
|
||||
wallet_content.on_data(data);
|
||||
let w = self.wallets.list()[0].clone();
|
||||
if w.is_open() {
|
||||
self.wallet_content = Some(WalletContent::new(w, data));
|
||||
} else {
|
||||
self.show_opening_modal(w, data, cb);
|
||||
}
|
||||
} else {
|
||||
self.show_wallet_selection_modal(data);
|
||||
}
|
||||
} else {
|
||||
if wallets_size == 1 {
|
||||
let wallet_content = self.wallet_content.as_ref().unwrap();
|
||||
self.show_opening_modal(wallet_content.wallet.clone(), data, cb);
|
||||
} else {
|
||||
self.show_wallet_selection_modal(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Show initial wallet creation [`Modal`].
|
||||
|
@ -344,19 +349,26 @@ impl WalletsContent {
|
|||
}
|
||||
|
||||
/// Handle Back key event returning `false` when event was handled.
|
||||
pub fn on_back(&mut self) -> bool {
|
||||
pub fn on_back(&mut self, cb: &dyn PlatformCallbacks) -> bool {
|
||||
if self.creation_content.is_some() {
|
||||
// Close wallet creation.
|
||||
let creation = self.creation_content.as_mut().unwrap();
|
||||
if creation.on_back() {
|
||||
self.creation_content = None;
|
||||
}
|
||||
return false
|
||||
return false;
|
||||
} else {
|
||||
// Close opened wallet.
|
||||
if self.showing_wallet() {
|
||||
let content = self.wallet_content.as_mut().unwrap();
|
||||
// Close opened QR code scanner.
|
||||
if content.qr_scan_content.is_some() {
|
||||
cb.stop_camera();
|
||||
content.qr_scan_content = None;
|
||||
return false;
|
||||
}
|
||||
// Close opened wallet.
|
||||
self.wallet_content = None;
|
||||
return false
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
|
@ -366,15 +378,27 @@ impl WalletsContent {
|
|||
fn title_ui(&mut self,
|
||||
ui: &mut egui::Ui,
|
||||
dual_panel: bool,
|
||||
show_wallet: bool) {
|
||||
show_wallet: bool,
|
||||
cb: &dyn PlatformCallbacks) {
|
||||
let show_list = self.show_wallets_at_dual_panel;
|
||||
let creating_wallet = self.creating_wallet();
|
||||
let qr_scan = {
|
||||
let mut scan = false;
|
||||
if show_wallet {
|
||||
scan = self.wallet_content.as_mut().unwrap().qr_scan_content.is_some();
|
||||
}
|
||||
scan
|
||||
};
|
||||
// Setup title.
|
||||
let title_content = if show_wallet && (!dual_panel
|
||||
|| (dual_panel && !show_list)) && !creating_wallet {
|
||||
let wallet_content = self.wallet_content.as_ref().unwrap();
|
||||
let wallet_tab_type = wallet_content.current_tab.get_type();
|
||||
let title_text = wallet_tab_type.name().to_uppercase();
|
||||
let title_text = if qr_scan {
|
||||
t!("scan_qr")
|
||||
} else {
|
||||
wallet_tab_type.name()
|
||||
};
|
||||
if wallet_tab_type == WalletTabType::Settings {
|
||||
TitleType::Single(TitleContentType::Title(title_text))
|
||||
} else {
|
||||
|
@ -382,16 +406,18 @@ impl WalletsContent {
|
|||
TitleType::Single(TitleContentType::WithSubTitle(title_text, subtitle_text, false))
|
||||
}
|
||||
} else {
|
||||
let title_text = if creating_wallet {
|
||||
let title_text = if qr_scan {
|
||||
t!("scan_qr")
|
||||
} else if creating_wallet {
|
||||
t!("wallets.add")
|
||||
} else {
|
||||
t!("wallets.title")
|
||||
}.to_uppercase();
|
||||
let dual_title = !creating_wallet && show_wallet && dual_panel;
|
||||
};
|
||||
let dual_title = !qr_scan && !creating_wallet && show_wallet && dual_panel;
|
||||
if dual_title {
|
||||
let wallet_content = self.wallet_content.as_ref().unwrap();
|
||||
let wallet_tab_type = wallet_content.current_tab.get_type();
|
||||
let wallet_title_text = wallet_tab_type.name().to_uppercase();
|
||||
let wallet_title_text = wallet_tab_type.name();
|
||||
let wallet_title_content = if wallet_tab_type == WalletTabType::Settings {
|
||||
TitleContentType::Title(wallet_title_text)
|
||||
} else {
|
||||
|
@ -408,6 +434,16 @@ impl WalletsContent {
|
|||
TitlePanel::new(Id::new("wallets_title_panel")).ui(title_content, |ui| {
|
||||
if show_wallet && !dual_panel {
|
||||
View::title_button_big(ui, ARROW_LEFT, |_| {
|
||||
let wallet_qr_scan = self.wallet_content
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.qr_scan_content
|
||||
.is_some();
|
||||
if wallet_qr_scan {
|
||||
cb.stop_camera();
|
||||
self.wallet_content.as_mut().unwrap().qr_scan_content = None;
|
||||
return;
|
||||
}
|
||||
self.wallet_content = None;
|
||||
});
|
||||
} else if self.creation_content.is_some() {
|
||||
|
@ -423,6 +459,12 @@ impl WalletsContent {
|
|||
self.creation_content = None;
|
||||
}
|
||||
} else if show_wallet && dual_panel {
|
||||
if qr_scan {
|
||||
View::title_button_big(ui, ARROW_LEFT, |_| {
|
||||
cb.stop_camera();
|
||||
self.wallet_content.as_mut().unwrap().qr_scan_content = None;
|
||||
});
|
||||
} else {
|
||||
let list_icon = if show_list {
|
||||
SIDEBAR_SIMPLE
|
||||
} else {
|
||||
|
@ -432,7 +474,8 @@ impl WalletsContent {
|
|||
self.show_wallets_at_dual_panel = !show_list;
|
||||
AppConfig::toggle_show_wallets_at_dual_panel();
|
||||
});
|
||||
} else if !Content::is_dual_panel_mode(ui) {
|
||||
}
|
||||
} else if !Content::is_dual_panel_mode(ui.ctx()) {
|
||||
View::title_button_big(ui, GLOBE, |_| {
|
||||
Content::toggle_network_panel();
|
||||
});
|
||||
|
@ -453,40 +496,34 @@ impl WalletsContent {
|
|||
ui: &mut egui::Ui,
|
||||
cb: &dyn PlatformCallbacks) {
|
||||
ScrollArea::vertical()
|
||||
.id_source("wallet_list")
|
||||
.id_salt("wallet_list_scroll")
|
||||
.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| {
|
||||
// Show application logo and name.
|
||||
View::app_logo_name_version(ui);
|
||||
ui.add_space(15.0);
|
||||
|
||||
let mut list = self.wallets.list().clone();
|
||||
// Remove deleted wallet from the list.
|
||||
list.retain(|w| {
|
||||
let deleted = w.is_deleted();
|
||||
if deleted {
|
||||
let list = self.wallets.list().clone();
|
||||
for w in &list {
|
||||
// Remove deleted.
|
||||
if w.is_deleted() {
|
||||
self.wallet_content = None;
|
||||
self.wallets.remove(w.get_config().id);
|
||||
ui.ctx().request_repaint();
|
||||
continue;
|
||||
}
|
||||
!deleted
|
||||
});
|
||||
for wallet in &list {
|
||||
// Check if wallet reopen is needed.
|
||||
if wallet.reopen_needed() && !wallet.is_open() {
|
||||
wallet.set_reopen(false);
|
||||
self.show_opening_modal(wallet.clone(), None, cb);
|
||||
if w.reopen_needed() && !w.is_open() {
|
||||
w.set_reopen(false);
|
||||
self.show_opening_modal(w.clone(), None, cb);
|
||||
}
|
||||
// Draw wallet list item.
|
||||
self.wallet_item_ui(ui, wallet, cb);
|
||||
self.wallet_item_ui(ui, w, cb);
|
||||
ui.add_space(5.0);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// Draw wallet list item.
|
||||
|
@ -533,7 +570,6 @@ impl WalletsContent {
|
|||
show_selection = true;
|
||||
});
|
||||
if show_selection {
|
||||
self.conn_selection_content = None;
|
||||
ExternalConnection::check(None, ui.ctx());
|
||||
}
|
||||
}
|
||||
|
@ -604,11 +640,11 @@ impl WalletsContent {
|
|||
.show();
|
||||
cb.show_keyboard();
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if it's possible to show [`WalletsContent`] and [`WalletContent`] panels at same time.
|
||||
fn is_dual_panel_mode(ui: &mut egui::Ui) -> bool {
|
||||
let dual_panel_root = Content::is_dual_panel_mode(ui);
|
||||
/// Check if it's possible to show [`WalletsContent`] and [`WalletContent`] panels at same time.
|
||||
fn is_dual_panel_mode(ui: &mut egui::Ui) -> bool {
|
||||
let dual_panel_root = Content::is_dual_panel_mode(ui.ctx());
|
||||
let max_width = ui.available_width();
|
||||
dual_panel_root && max_width >= (Content::SIDE_PANEL_WIDTH * 2.0) + View::get_right_inset()
|
||||
}
|
||||
}
|
|
@ -20,7 +20,7 @@ use crate::gui::Colors;
|
|||
use crate::gui::icons::{CHECK, CLIPBOARD_TEXT, COPY, SCAN};
|
||||
use crate::gui::platform::PlatformCallbacks;
|
||||
use crate::gui::views::{Modal, Content, View, CameraScanModal};
|
||||
use crate::gui::views::types::{ModalContainer, ModalPosition, QrScanResult};
|
||||
use crate::gui::views::types::{LinePosition, ModalContainer, ModalPosition, QrScanResult};
|
||||
use crate::gui::views::wallets::creation::MnemonicSetup;
|
||||
use crate::gui::views::wallets::creation::types::Step;
|
||||
use crate::gui::views::wallets::ConnectionSettings;
|
||||
|
@ -112,30 +112,33 @@ impl WalletCreation {
|
|||
on_create: impl FnMut(Wallet)) {
|
||||
self.current_modal_ui(ui, cb);
|
||||
|
||||
// Show wallet creation step description and confirmation panel.
|
||||
egui::TopBottomPanel::bottom("wallet_creation_step_panel")
|
||||
.frame(egui::Frame {
|
||||
stroke: View::item_stroke(),
|
||||
fill: Colors::fill_deep(),
|
||||
inner_margin: Margin {
|
||||
left: View::far_left_inset_margin(ui) + 8.0,
|
||||
right: View::get_right_inset() + 8.0,
|
||||
top: 4.0,
|
||||
bottom: View::get_bottom_inset(),
|
||||
left: View::far_left_inset_margin(ui) + View::TAB_ITEMS_PADDING,
|
||||
right: View::get_right_inset() + View::TAB_ITEMS_PADDING,
|
||||
top: View::TAB_ITEMS_PADDING,
|
||||
bottom: View::get_bottom_inset() + View::TAB_ITEMS_PADDING,
|
||||
},
|
||||
fill: Colors::fill_deep(),
|
||||
..Default::default()
|
||||
})
|
||||
.show_inside(ui, |ui| {
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.vertical_centered(|ui| {
|
||||
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 2.0, |ui| {
|
||||
// Draw divider line.
|
||||
let rect = {
|
||||
let mut r = ui.available_rect_before_wrap();
|
||||
r.min.y -= View::TAB_ITEMS_PADDING;
|
||||
r.min.x -= View::far_left_inset_margin(ui) + View::TAB_ITEMS_PADDING;
|
||||
r.max.x += View::get_right_inset() + View::TAB_ITEMS_PADDING;
|
||||
r
|
||||
};
|
||||
View::line(ui, LinePosition::TOP, &rect, Colors::item_stroke());
|
||||
// Show step control content.
|
||||
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
|
||||
self.step_control_ui(ui, on_create, cb);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
// Show wallet creation step content panel.
|
||||
egui::CentralPanel::default()
|
||||
.frame(egui::Frame {
|
||||
|
@ -149,11 +152,10 @@ impl WalletCreation {
|
|||
})
|
||||
.show_inside(ui, |ui| {
|
||||
ScrollArea::vertical()
|
||||
.id_source(Id::from(format!("creation_step_scroll_{}", self.step.name())))
|
||||
.id_salt(Id::from(format!("creation_step_scroll_{}", self.step.name())))
|
||||
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
|
||||
.auto_shrink([false; 2])
|
||||
.show(ui, |ui| {
|
||||
ui.vertical_centered(|ui| {
|
||||
let max_width = if self.step == Step::SetupConnection {
|
||||
Content::SIDE_PANEL_WIDTH * 1.3
|
||||
} else {
|
||||
|
@ -164,7 +166,6 @@ impl WalletCreation {
|
|||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// Draw [`Step`] description and confirmation control.
|
||||
|
@ -201,9 +202,8 @@ impl WalletCreation {
|
|||
self.mnemonic_setup.mnemonic.mode() == PhraseMode::Generate;
|
||||
if (self.mnemonic_setup.mnemonic.valid() && self.creation_error.is_none()) ||
|
||||
generate_step {
|
||||
ui.add_space(2.0);
|
||||
ui.label(RichText::new(step_text).size(16.0).color(Colors::gray()));
|
||||
ui.add_space(2.0);
|
||||
ui.add_space(6.0);
|
||||
} else {
|
||||
next = false;
|
||||
// Show error text.
|
||||
|
@ -214,39 +214,35 @@ impl WalletCreation {
|
|||
.color(Colors::red()));
|
||||
ui.add_space(10.0);
|
||||
} else {
|
||||
ui.add_space(2.0);
|
||||
ui.label(RichText::new(&t!("wallets.not_valid_phrase"))
|
||||
.size(16.0)
|
||||
.color(Colors::red()));
|
||||
ui.add_space(2.0);
|
||||
ui.add_space(4.0);
|
||||
};
|
||||
}
|
||||
|
||||
// Setup buttons.
|
||||
// Setup spacing between buttons.
|
||||
ui.style_mut().spacing.item_spacing = egui::vec2(8.0, 0.0);
|
||||
// Setup vertical padding inside button.
|
||||
ui.style_mut().spacing.button_padding = egui::vec2(10.0, 7.0);
|
||||
|
||||
match step {
|
||||
Step::EnterMnemonic => {
|
||||
ui.add_space(4.0);
|
||||
|
||||
// Setup spacing between buttons.
|
||||
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
|
||||
|
||||
ui.columns(2, |columns| {
|
||||
// Show copy or paste button for mnemonic phrase step.
|
||||
columns[0].vertical_centered_justified(|ui| {
|
||||
match self.mnemonic_setup.mnemonic.mode() {
|
||||
PhraseMode::Generate => {
|
||||
// Show copy button.
|
||||
let c_t = format!("{} {}", COPY, t!("copy").to_uppercase());
|
||||
View::button(ui,
|
||||
c_t.to_uppercase(),
|
||||
Colors::white_or_black(false), || {
|
||||
let c_t = format!("{} {}",
|
||||
COPY,
|
||||
t!("copy").to_uppercase());
|
||||
View::button(ui, c_t, Colors::white_or_black(false), || {
|
||||
cb.copy_string_to_buffer(self.mnemonic_setup
|
||||
.mnemonic
|
||||
.get_phrase());
|
||||
});
|
||||
}
|
||||
PhraseMode::Import => {
|
||||
// Show paste button.
|
||||
let p_t = format!("{} {}",
|
||||
CLIPBOARD_TEXT,
|
||||
t!("paste").to_uppercase());
|
||||
|
@ -262,7 +258,9 @@ impl WalletCreation {
|
|||
if next {
|
||||
self.next_step_button_ui(ui, on_create);
|
||||
} else {
|
||||
let scan_text = format!("{} {}", SCAN, t!("scan").to_uppercase());
|
||||
let scan_text = format!("{} {}",
|
||||
SCAN,
|
||||
t!("scan").to_uppercase());
|
||||
View::button(ui, scan_text, Colors::white_or_black(false), || {
|
||||
self.scan_modal_content = Some(CameraScanModal::default());
|
||||
// Show QR code scan modal.
|
||||
|
@ -276,10 +274,8 @@ impl WalletCreation {
|
|||
}
|
||||
});
|
||||
});
|
||||
ui.add_space(4.0);
|
||||
}
|
||||
Step::ConfirmMnemonic => {
|
||||
ui.add_space(4.0);
|
||||
// Show next step or paste button.
|
||||
if next {
|
||||
self.next_step_button_ui(ui, on_create);
|
||||
|
@ -290,17 +286,14 @@ impl WalletCreation {
|
|||
self.mnemonic_setup.mnemonic.import(&data);
|
||||
});
|
||||
}
|
||||
ui.add_space(4.0);
|
||||
}
|
||||
Step::SetupConnection => {
|
||||
if next {
|
||||
ui.add_space(4.0);
|
||||
self.next_step_button_ui(ui, on_create);
|
||||
ui.add_space(4.0);
|
||||
ui.add_space(2.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
ui.add_space(3.0);
|
||||
}
|
||||
|
||||
/// Draw button to go to next [`Step`].
|
||||
|
@ -311,7 +304,7 @@ impl WalletCreation {
|
|||
let (next_text, text_color, bg_color) = if self.step == Step::SetupConnection {
|
||||
(format!("{} {}", CHECK, t!("complete")), Colors::title(true), Colors::gold())
|
||||
} else {
|
||||
(t!("continue"), Colors::text_button(), Colors::white_or_black(false))
|
||||
(t!("continue"), Colors::green(), Colors::white_or_black(false))
|
||||
};
|
||||
|
||||
// Show next step button.
|
||||
|
@ -361,7 +354,7 @@ impl WalletCreation {
|
|||
Step::ConfirmMnemonic => self.mnemonic_setup.confirm_ui(ui, cb),
|
||||
Step::SetupConnection => {
|
||||
// Redraw if node is running.
|
||||
if Node::is_running() && !Content::is_dual_panel_mode(ui) {
|
||||
if Node::is_running() && !Content::is_dual_panel_mode(ui.ctx()) {
|
||||
ui.ctx().request_repaint_after(Node::STATS_UPDATE_DELAY);
|
||||
}
|
||||
self.network_setup.create_ui(ui, cb)
|
||||
|
|
|
@ -216,7 +216,7 @@ impl MnemonicSetup {
|
|||
};
|
||||
if edit {
|
||||
ui.add_space(6.0);
|
||||
View::button(ui, PENCIL.to_string(), Colors::button(), || {
|
||||
View::button(ui, PENCIL.to_string(), Colors::white_or_black(false), || {
|
||||
self.word_index_edit = num - 1;
|
||||
self.word_edit = word.text.clone();
|
||||
self.valid_word_edit = word.valid;
|
||||
|
|
|
@ -65,7 +65,7 @@ impl WalletConnectionModal {
|
|||
} else {
|
||||
350.0
|
||||
})
|
||||
.id_source("integrated_node")
|
||||
.id_salt("connections_scroll")
|
||||
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
|
||||
.auto_shrink([true; 2])
|
||||
.show(ui, |ui| {
|
||||
|
@ -96,7 +96,7 @@ impl WalletConnectionModal {
|
|||
ui.add_space(6.0);
|
||||
// Show button to add new external node connection.
|
||||
let add_node_text = format!("{} {}", PLUS_CIRCLE, t!("wallets.add_node"));
|
||||
View::button(ui, add_node_text, Colors::button(), || {
|
||||
View::button(ui, add_node_text, Colors::white_or_black(false), || {
|
||||
self.ext_conn_content = Some(ExternalConnectionModal::new(None));
|
||||
});
|
||||
});
|
||||
|
@ -137,7 +137,7 @@ impl WalletConnectionModal {
|
|||
});
|
||||
|
||||
ui.add_space(2.0);
|
||||
View::horizontal_line(ui, Colors::stroke());
|
||||
View::horizontal_line(ui, Colors::item_stroke());
|
||||
ui.add_space(6.0);
|
||||
|
||||
// Show button to close modal.
|
||||
|
|
|
@ -64,7 +64,7 @@ impl WalletsModal {
|
|||
ui.add_space(4.0);
|
||||
ScrollArea::vertical()
|
||||
.max_height(373.0)
|
||||
.id_source("select_wallet_list")
|
||||
.id_salt("select_wallet_list_scroll")
|
||||
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
|
||||
.auto_shrink([true; 2])
|
||||
.show(ui, |ui| {
|
||||
|
@ -83,7 +83,7 @@ impl WalletsModal {
|
|||
});
|
||||
|
||||
ui.add_space(2.0);
|
||||
View::horizontal_line(ui, Colors::stroke());
|
||||
View::horizontal_line(ui, Colors::item_stroke());
|
||||
ui.add_space(6.0);
|
||||
|
||||
// Show button to close modal.
|
||||
|
|
|
@ -13,16 +13,17 @@
|
|||
// limitations under the License.
|
||||
|
||||
use std::time::Duration;
|
||||
use egui::{Align, Id, Layout, Margin, RichText};
|
||||
use egui::{Align, Id, Layout, Margin, RichText, ScrollArea};
|
||||
use egui::scroll_area::ScrollBarVisibility;
|
||||
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, FOLDER_USER, GEAR_FINE, GRAPH, PACKAGE, POWER, SCAN, SPINNER, USERS_THREE};
|
||||
use crate::gui::icons::{ARROWS_CLOCKWISE, BRIDGE, CAMERA_ROTATE, CHAT_CIRCLE_TEXT, FOLDER_USER, GEAR_FINE, GRAPH, PACKAGE, POWER, SCAN, SPINNER, USERS_THREE};
|
||||
use crate::gui::platform::PlatformCallbacks;
|
||||
use crate::gui::views::{Modal, Content, View, CameraScanModal};
|
||||
use crate::gui::views::types::{ModalPosition, QrScanResult};
|
||||
use crate::gui::views::{Modal, Content, View, CameraContent};
|
||||
use crate::gui::views::types::{LinePosition, ModalContainer, ModalPosition};
|
||||
use crate::gui::views::wallets::{WalletTransactions, WalletMessages, WalletTransport};
|
||||
use crate::gui::views::wallets::types::{GRIN, WalletTab, WalletTabType};
|
||||
use crate::gui::views::wallets::wallet::modals::WalletAccountsModal;
|
||||
|
@ -35,21 +36,40 @@ use crate::wallet::types::{ConnectionMethod, WalletData};
|
|||
pub struct WalletContent {
|
||||
/// Selected and opened wallet.
|
||||
pub wallet: Wallet,
|
||||
/// Current tab content to show.
|
||||
pub current_tab: Box<dyn WalletTab>,
|
||||
|
||||
/// Wallet accounts [`Modal`] content.
|
||||
accounts_modal_content: Option<WalletAccountsModal>,
|
||||
/// QR code scan [`Modal`] content.
|
||||
scan_modal_content: Option<CameraScanModal>,
|
||||
|
||||
/// Current tab content to show.
|
||||
pub current_tab: Box<dyn WalletTab>,
|
||||
/// QR code scan content.
|
||||
pub qr_scan_content: Option<CameraContent>,
|
||||
|
||||
/// List of allowed [`Modal`] ids for this [`ModalContainer`].
|
||||
allowed_modal_ids: Vec<&'static str>
|
||||
}
|
||||
|
||||
/// Identifier for account list [`Modal`].
|
||||
const ACCOUNT_LIST_MODAL: &'static str = "account_list_modal";
|
||||
|
||||
/// Identifier for QR code scan [`Modal`].
|
||||
const QR_CODE_SCAN_MODAL: &'static str = "qr_code_scan_modal";
|
||||
impl ModalContainer for WalletContent {
|
||||
fn modal_ids(&self) -> &Vec<&'static str> {
|
||||
&self.allowed_modal_ids
|
||||
}
|
||||
|
||||
fn modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
|
||||
match modal.id {
|
||||
ACCOUNT_LIST_MODAL => {
|
||||
if let Some(content) = self.accounts_modal_content.as_mut() {
|
||||
Modal::ui(ui.ctx(), |ui, modal| {
|
||||
content.ui(ui, &self.wallet, modal, cb);
|
||||
});
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl WalletContent {
|
||||
/// Create new instance with optional data.
|
||||
|
@ -57,8 +77,11 @@ impl WalletContent {
|
|||
let mut content = Self {
|
||||
wallet,
|
||||
accounts_modal_content: None,
|
||||
scan_modal_content: None,
|
||||
qr_scan_content: None,
|
||||
current_tab: Box::new(WalletTransactions::default()),
|
||||
allowed_modal_ids: vec![
|
||||
ACCOUNT_LIST_MODAL,
|
||||
],
|
||||
};
|
||||
if data.is_some() {
|
||||
content.on_data(data);
|
||||
|
@ -68,115 +91,163 @@ impl WalletContent {
|
|||
|
||||
/// Handle data from deeplink or opened file.
|
||||
pub fn on_data(&mut self, data: Option<String>) {
|
||||
// Provide data to messages.
|
||||
self.current_tab = Box::new(WalletMessages::new(data));
|
||||
}
|
||||
|
||||
/// Draw wallet content.
|
||||
pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||
self.modal_content_ui(ui, cb);
|
||||
ui.ctx().request_repaint_after(Duration::from_millis(1000));
|
||||
self.current_modal_ui(ui, cb);
|
||||
|
||||
let dual_panel = Content::is_dual_panel_mode(ui);
|
||||
let dual_panel = Content::is_dual_panel_mode(ui.ctx());
|
||||
let show_wallets_dual = AppConfig::show_wallets_at_dual_panel();
|
||||
|
||||
let wallet = &self.wallet;
|
||||
let wallet_id = wallet.identifier();
|
||||
let data = wallet.get_data();
|
||||
let data_empty = data.is_none();
|
||||
let show_qr_scan = self.qr_scan_content.is_some();
|
||||
let hide_tabs = Self::block_navigation_on_sync(wallet);
|
||||
|
||||
// Show wallet balance panel not on Settings tab with selected non-repairing
|
||||
// wallet, when there is no error and data is not empty.
|
||||
let mut show_balance = self.current_tab.get_type() != WalletTabType::Settings && !data_empty
|
||||
&& !wallet.sync_error() && !wallet.is_repairing() && !wallet.is_closing();
|
||||
// Show wallet account panel not on settings tab when navigation is not blocked and QR code
|
||||
// scanner is not showing and wallet data is not empty.
|
||||
let mut show_account = self.current_tab.get_type() != WalletTabType::Settings && !hide_tabs
|
||||
&& !wallet.sync_error() && data.is_some();
|
||||
if wallet.get_current_connection() == ConnectionMethod::Integrated && !Node::is_running() {
|
||||
show_balance = false;
|
||||
show_account = false;
|
||||
}
|
||||
egui::TopBottomPanel::top(Id::from("wallet_balance").with(wallet.identifier()))
|
||||
// Close scanner when balance got hidden.
|
||||
if !show_account && show_qr_scan {
|
||||
cb.stop_camera();
|
||||
self.qr_scan_content = None;
|
||||
}
|
||||
egui::TopBottomPanel::top(Id::from("wallet_account").with(wallet.identifier()))
|
||||
.frame(egui::Frame {
|
||||
fill: Colors::fill(),
|
||||
stroke: View::item_stroke(),
|
||||
inner_margin: Margin {
|
||||
left: View::far_left_inset_margin(ui) + 4.0,
|
||||
right: View::get_right_inset() + 4.0,
|
||||
top: 4.0,
|
||||
bottom: 0.0,
|
||||
},
|
||||
outer_margin: Margin {
|
||||
left: if dual_panel {
|
||||
-0.5
|
||||
} else {
|
||||
0.0
|
||||
},
|
||||
right: 0.0,
|
||||
top: 0.0,
|
||||
bottom: if dual_panel {
|
||||
-1.0
|
||||
} else {
|
||||
-0.5
|
||||
},
|
||||
},
|
||||
fill: Colors::fill(),
|
||||
..Default::default()
|
||||
})
|
||||
.show_animated_inside(ui, show_balance, |ui| {
|
||||
ui.vertical_centered(|ui| {
|
||||
if !dual_panel {
|
||||
ui.add_space(1.0);
|
||||
}
|
||||
// Draw account info.
|
||||
.show_animated_inside(ui, show_account, |ui| {
|
||||
let rect = ui.available_rect_before_wrap();
|
||||
if show_qr_scan {
|
||||
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH, |ui| {
|
||||
self.qr_scan_content.as_mut().unwrap().ui(ui, cb);
|
||||
ui.add_space(6.0);
|
||||
ui.vertical_centered_justified(|ui| {
|
||||
View::button(ui, t!("close"), Colors::white_or_black(false), || {
|
||||
cb.stop_camera();
|
||||
self.qr_scan_content = None;
|
||||
});
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
});
|
||||
} else {
|
||||
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
|
||||
self.account_ui(ui, data.unwrap(), cb);
|
||||
});
|
||||
});
|
||||
}
|
||||
// Draw content divider lines.
|
||||
let r = {
|
||||
let mut r = rect.clone();
|
||||
r.min.x -= 4.0 + View::far_left_inset_margin(ui);
|
||||
r.min.y -= 4.0;
|
||||
r.max.x += 4.0 + View::get_right_inset();
|
||||
r
|
||||
};
|
||||
View::line(ui, LinePosition::BOTTOM, &r, Colors::item_stroke());
|
||||
if dual_panel && show_wallets_dual && !show_qr_scan {
|
||||
View::line(ui, LinePosition::LEFT, &r, Colors::item_stroke());
|
||||
}
|
||||
});
|
||||
|
||||
// Show wallet tabs panel.
|
||||
egui::TopBottomPanel::bottom("wallet_tabs_content")
|
||||
// Show wallet tabs.
|
||||
let show_tabs = !hide_tabs && self.qr_scan_content.is_none();
|
||||
egui::TopBottomPanel::bottom("wallet_tabs")
|
||||
.frame(egui::Frame {
|
||||
fill: Colors::fill(),
|
||||
inner_margin: Margin {
|
||||
left: View::far_left_inset_margin(ui) + View::TAB_ITEMS_PADDING,
|
||||
right: View::get_right_inset() + View::TAB_ITEMS_PADDING,
|
||||
top: View::TAB_ITEMS_PADDING,
|
||||
bottom: View::get_bottom_inset() + View::TAB_ITEMS_PADDING,
|
||||
},
|
||||
fill: Colors::fill(),
|
||||
..Default::default()
|
||||
})
|
||||
.show_animated_inside(ui, !hide_tabs, |ui| {
|
||||
ui.vertical_centered(|ui| {
|
||||
// Draw wallet tabs.
|
||||
.show_animated_inside(ui, show_tabs, |ui| {
|
||||
let rect = ui.available_rect_before_wrap();
|
||||
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
|
||||
self.tabs_ui(ui);
|
||||
});
|
||||
self.tabs_ui(ui, cb);
|
||||
});
|
||||
let rect = {
|
||||
let mut r = rect.clone();
|
||||
r.min.x -= View::far_left_inset_margin(ui) + View::TAB_ITEMS_PADDING;
|
||||
r.min.y -= View::TAB_ITEMS_PADDING;
|
||||
r.max.x += View::get_right_inset() + View::TAB_ITEMS_PADDING;
|
||||
r.max.y += View::get_bottom_inset() + View::TAB_ITEMS_PADDING;
|
||||
r
|
||||
};
|
||||
// Draw content divider line.
|
||||
View::line(ui, LinePosition::TOP, &rect, Colors::stroke());
|
||||
});
|
||||
|
||||
// Show tab content panel.
|
||||
// Show tab content.
|
||||
egui::CentralPanel::default()
|
||||
.frame(egui::Frame {
|
||||
outer_margin: Margin {
|
||||
left: if dual_panel {
|
||||
-0.5
|
||||
} else {
|
||||
0.0
|
||||
},
|
||||
right: 0.0,
|
||||
inner_margin: Margin {
|
||||
left: View::far_left_inset_margin(ui) + 4.0,
|
||||
right: View::get_right_inset() + 4.0,
|
||||
top: 0.0,
|
||||
bottom: 0.0,
|
||||
bottom: 4.0,
|
||||
},
|
||||
stroke: View::item_stroke(),
|
||||
fill: Colors::white_or_black(false),
|
||||
..Default::default()
|
||||
})
|
||||
.show_inside(ui, |ui| {
|
||||
let rect = ui.available_rect_before_wrap();
|
||||
let tab_type = self.current_tab.get_type();
|
||||
let show_sync = (tab_type != WalletTabType::Settings || hide_tabs) &&
|
||||
sync_ui(ui, &self.wallet);
|
||||
if !show_sync {
|
||||
if tab_type != WalletTabType::Txs {
|
||||
ui.add_space(3.0);
|
||||
ScrollArea::vertical()
|
||||
.id_salt(Id::from("wallet_scroll")
|
||||
.with(tab_type.name())
|
||||
.with(wallet_id))
|
||||
.auto_shrink([false; 2])
|
||||
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
|
||||
.show(ui, |ui| {
|
||||
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
|
||||
self.current_tab.ui(ui, &self.wallet, cb);
|
||||
});
|
||||
|
||||
// Refresh content after 1 second for synced wallet.
|
||||
if !data_empty {
|
||||
ui.ctx().request_repaint_after(Duration::from_millis(1000));
|
||||
});
|
||||
} else {
|
||||
ui.ctx().request_repaint();
|
||||
self.current_tab.ui(ui, &self.wallet, cb);
|
||||
}
|
||||
}
|
||||
let rect = {
|
||||
let mut r = rect.clone();
|
||||
r.min.x -= View::far_left_inset_margin(ui) + 4.0;
|
||||
r.max.x += View::get_right_inset() + 4.0;
|
||||
r.max.y += 4.0;
|
||||
r
|
||||
};
|
||||
// Draw cover when QR code scanner is active.
|
||||
if show_qr_scan {
|
||||
View::content_cover_ui(ui, rect, "wallet_tab", || {
|
||||
cb.stop_camera();
|
||||
self.qr_scan_content = None;
|
||||
});
|
||||
}
|
||||
// Draw content divider line.
|
||||
if dual_panel && show_wallets_dual {
|
||||
View::line(ui, LinePosition::LEFT, &rect, Colors::item_stroke());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Check when to block tabs navigation on sync progress.
|
||||
pub fn block_navigation_on_sync(wallet: &Wallet) -> bool {
|
||||
|
@ -191,64 +262,6 @@ impl WalletContent {
|
|||
(!integrated_node || integrated_node_ready))
|
||||
}
|
||||
|
||||
/// Draw [`Modal`] content for this ui container.
|
||||
fn modal_content_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||
match Modal::opened() {
|
||||
None => {}
|
||||
Some(id) => {
|
||||
match id {
|
||||
ACCOUNT_LIST_MODAL => {
|
||||
if let Some(content) = self.accounts_modal_content.as_mut() {
|
||||
Modal::ui(ui.ctx(), |ui, modal| {
|
||||
content.ui(ui, &self.wallet, modal, cb);
|
||||
});
|
||||
}
|
||||
}
|
||||
QR_CODE_SCAN_MODAL => {
|
||||
let mut success = false;
|
||||
if let Some(content) = self.scan_modal_content.as_mut() {
|
||||
Modal::ui(ui.ctx(), |ui, modal| {
|
||||
content.ui(ui, modal, cb, |result| {
|
||||
match result {
|
||||
QrScanResult::Slatepack(message) => {
|
||||
success = true;
|
||||
let msg = Some(message.to_string());
|
||||
let messages = WalletMessages::new(msg);
|
||||
self.current_tab = Box::new(messages);
|
||||
return;
|
||||
}
|
||||
QrScanResult::Address(receiver) => {
|
||||
success = true;
|
||||
let balance = self.wallet.get_data()
|
||||
.unwrap()
|
||||
.info
|
||||
.amount_currently_spendable;
|
||||
if balance > 0 {
|
||||
let mut transport = WalletTransport::default();
|
||||
let rec = Some(receiver.to_string());
|
||||
transport.show_send_tor_modal(cb, rec);
|
||||
self.current_tab = Box::new(transport);
|
||||
return;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
if success {
|
||||
modal.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
if success {
|
||||
self.scan_modal_content = None;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw wallet account content.
|
||||
fn account_ui(&mut self,
|
||||
ui: &mut egui::Ui,
|
||||
|
@ -258,17 +271,12 @@ impl WalletContent {
|
|||
rect.set_height(75.0);
|
||||
// Draw round background.
|
||||
let rounding = View::item_rounding(0, 2, false);
|
||||
ui.painter().rect(rect, rounding, Colors::button(), View::hover_stroke());
|
||||
ui.painter().rect(rect, rounding, Colors::fill_lite(), View::item_stroke());
|
||||
|
||||
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
|
||||
// Draw button to show QR code scanner.
|
||||
View::item_button(ui, View::item_rounding(0, 2, true), SCAN, None, || {
|
||||
self.scan_modal_content = Some(CameraScanModal::default());
|
||||
Modal::new(QR_CODE_SCAN_MODAL)
|
||||
.position(ModalPosition::CenterTop)
|
||||
.title(t!("scan_qr"))
|
||||
.closeable(false)
|
||||
.show();
|
||||
self.qr_scan_content = Some(CameraContent::default());
|
||||
cb.start_camera();
|
||||
});
|
||||
|
||||
|
@ -346,15 +354,28 @@ impl WalletContent {
|
|||
});
|
||||
}
|
||||
|
||||
/// Draw tab buttons in the bottom of the screen.
|
||||
fn tabs_ui(&mut self, ui: &mut egui::Ui) {
|
||||
/// Draw tab buttons at the bottom of the screen.
|
||||
fn tabs_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||
ui.scope(|ui| {
|
||||
// Setup spacing between tabs.
|
||||
ui.style_mut().spacing.item_spacing = egui::vec2(View::TAB_ITEMS_PADDING, 0.0);
|
||||
|
||||
// Show camera switch button at QR code scan.
|
||||
if self.qr_scan_content.is_some() && cb.can_switch_camera() {
|
||||
// Setup vertical padding inside tab button.
|
||||
ui.style_mut().spacing.button_padding = egui::vec2(10.0, 4.0);
|
||||
|
||||
ui.vertical_centered(|ui| {
|
||||
View::tab_button(ui, CAMERA_ROTATE, false, |_| {
|
||||
cb.switch_camera();
|
||||
});
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Setup vertical padding inside buttons.
|
||||
ui.style_mut().spacing.button_padding = egui::vec2(0.0, 4.0);
|
||||
|
||||
// Draw tab buttons.
|
||||
let current_type = self.current_tab.get_type();
|
||||
ui.columns(4, |columns| {
|
||||
columns[0].vertical_centered_justified(|ui| {
|
||||
|
@ -384,26 +405,27 @@ impl WalletContent {
|
|||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw content when wallet is syncing and not ready to use, returns `true` at this case.
|
||||
pub fn sync_ui(ui: &mut egui::Ui, wallet: &Wallet) -> bool {
|
||||
/// Draw content when wallet is syncing and not ready to use, returns `true` at this case.
|
||||
fn sync_ui(ui: &mut egui::Ui, wallet: &Wallet) -> bool {
|
||||
if wallet.is_repairing() && !wallet.sync_error() {
|
||||
Self::sync_progress_ui(ui, wallet);
|
||||
sync_progress_ui(ui, wallet);
|
||||
return true;
|
||||
} else if wallet.is_closing() {
|
||||
Self::sync_progress_ui(ui, wallet);
|
||||
sync_progress_ui(ui, wallet);
|
||||
return true;
|
||||
} else if wallet.get_current_connection() == ConnectionMethod::Integrated {
|
||||
if !Node::is_running() || Node::is_stopping() {
|
||||
View::center_content(ui, 108.0, |ui| {
|
||||
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.5, |ui| {
|
||||
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
|
||||
let text = t!("wallets.enable_node", "settings" => GEAR_FINE);
|
||||
ui.label(RichText::new(text).size(16.0).color(Colors::inactive_text()));
|
||||
ui.add_space(8.0);
|
||||
// Show button to enable integrated node at non-dual root panel mode
|
||||
// or when network connections are not showing and node is not stopping
|
||||
let dual_panel_root = Content::is_dual_panel_mode(ui);
|
||||
if (!dual_panel_root || AppConfig::show_connections_network_panel())
|
||||
let dual_panel = Content::is_dual_panel_mode(ui.ctx());
|
||||
if (!dual_panel || AppConfig::show_connections_network_panel())
|
||||
&& !Node::is_stopping() {
|
||||
let enable_text = format!("{} {}", POWER, t!("network.enable_node"));
|
||||
View::action_button(ui, enable_text, || {
|
||||
|
@ -415,24 +437,24 @@ impl WalletContent {
|
|||
return true
|
||||
} else if wallet.sync_error()
|
||||
&& Node::get_sync_status() == Some(SyncStatus::NoSync) {
|
||||
Self::sync_error_ui(ui, wallet);
|
||||
sync_error_ui(ui, wallet);
|
||||
return true;
|
||||
} else if wallet.get_data().is_none() {
|
||||
Self::sync_progress_ui(ui, wallet);
|
||||
sync_progress_ui(ui, wallet);
|
||||
return true;
|
||||
}
|
||||
} else if wallet.sync_error() {
|
||||
Self::sync_error_ui(ui, wallet);
|
||||
sync_error_ui(ui, wallet);
|
||||
return true;
|
||||
} else if wallet.get_data().is_none() {
|
||||
Self::sync_progress_ui(ui, wallet);
|
||||
sync_progress_ui(ui, wallet);
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw wallet sync error content.
|
||||
fn sync_error_ui(ui: &mut egui::Ui, wallet: &Wallet) {
|
||||
/// Draw wallet sync error content.
|
||||
fn sync_error_ui(ui: &mut egui::Ui, wallet: &Wallet) {
|
||||
View::center_content(ui, 108.0, |ui| {
|
||||
let text = t!("wallets.wallet_loading_err", "settings" => GEAR_FINE);
|
||||
ui.label(RichText::new(text).size(16.0).color(Colors::inactive_text()));
|
||||
|
@ -442,12 +464,12 @@ impl WalletContent {
|
|||
wallet.set_sync_error(false);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw wallet sync progress content.
|
||||
pub fn sync_progress_ui(ui: &mut egui::Ui, wallet: &Wallet) {
|
||||
/// Draw wallet sync progress content.
|
||||
fn sync_progress_ui(ui: &mut egui::Ui, wallet: &Wallet) {
|
||||
View::center_content(ui, 162.0, |ui| {
|
||||
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.5, |ui| {
|
||||
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
|
||||
View::big_loading_spinner(ui);
|
||||
ui.add_space(18.0);
|
||||
// Setup sync progress text.
|
||||
|
@ -480,5 +502,4 @@ impl WalletContent {
|
|||
ui.label(RichText::new(text).size(16.0).color(Colors::inactive_text()));
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
|
@ -14,7 +14,7 @@
|
|||
|
||||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
use egui::{Id, Margin, RichText, ScrollArea};
|
||||
use egui::{Id, RichText, ScrollArea};
|
||||
use egui::scroll_area::ScrollBarVisibility;
|
||||
use grin_core::core::amount_to_hr_string;
|
||||
use grin_wallet_libwallet::{Error, Slate, SlateState};
|
||||
|
@ -23,11 +23,11 @@ use parking_lot::RwLock;
|
|||
use crate::gui::Colors;
|
||||
use crate::gui::icons::{BROOM, CLIPBOARD_TEXT, DOWNLOAD_SIMPLE, SCAN, UPLOAD_SIMPLE};
|
||||
use crate::gui::platform::PlatformCallbacks;
|
||||
use crate::gui::views::{FilePickButton, Modal, Content, View, CameraScanModal};
|
||||
use crate::gui::views::{FilePickButton, Modal, View, CameraScanModal};
|
||||
use crate::gui::views::types::{ModalPosition, QrScanResult};
|
||||
use crate::gui::views::wallets::wallet::messages::request::MessageRequestModal;
|
||||
use crate::gui::views::wallets::wallet::types::{SLATEPACK_MESSAGE_HINT, WalletTab, WalletTabType};
|
||||
use crate::gui::views::wallets::wallet::{WalletContent, WalletTransactionModal};
|
||||
use crate::gui::views::wallets::wallet::WalletTransactionModal;
|
||||
use crate::wallet::types::WalletTransaction;
|
||||
use crate::wallet::Wallet;
|
||||
|
||||
|
@ -60,10 +60,8 @@ pub struct WalletMessages {
|
|||
|
||||
/// Identifier for amount input [`Modal`] to create invoice or sending request.
|
||||
const REQUEST_MODAL: &'static str = "messages_request_modal";
|
||||
|
||||
/// Identifier for [`Modal`] modal to show transaction information.
|
||||
const TX_INFO_MODAL: &'static str = "messages_tx_info_modal";
|
||||
|
||||
/// Identifier for [`Modal`] to scan Slatepack message from QR code.
|
||||
const SCAN_QR_MODAL: &'static str = "messages_scan_qr_modal";
|
||||
|
||||
|
@ -73,38 +71,8 @@ impl WalletTab for WalletMessages {
|
|||
}
|
||||
|
||||
fn ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) {
|
||||
if WalletContent::sync_ui(ui, wallet) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Show modal content for this ui container.
|
||||
self.modal_content_ui(ui, wallet, cb);
|
||||
|
||||
egui::CentralPanel::default()
|
||||
.frame(egui::Frame {
|
||||
stroke: View::item_stroke(),
|
||||
fill: Colors::white_or_black(false),
|
||||
inner_margin: Margin {
|
||||
left: View::far_left_inset_margin(ui) + 4.0,
|
||||
right: View::get_right_inset() + 4.0,
|
||||
top: 3.0,
|
||||
bottom: 4.0,
|
||||
},
|
||||
..Default::default()
|
||||
})
|
||||
.show_inside(ui, |ui| {
|
||||
ScrollArea::vertical()
|
||||
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
|
||||
.id_source(Id::from("wallet_messages").with(wallet.get_config().id))
|
||||
.auto_shrink([false; 2])
|
||||
.show(ui, |ui| {
|
||||
ui.vertical_centered(|ui| {
|
||||
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
|
||||
self.ui(ui, wallet, cb);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
self.messages_ui(ui, wallet, cb);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -124,8 +92,8 @@ impl WalletMessages {
|
|||
}
|
||||
}
|
||||
|
||||
/// Draw manual wallet transaction interaction content.
|
||||
pub fn ui(&mut self,
|
||||
/// Draw messages content.
|
||||
fn messages_ui(&mut self,
|
||||
ui: &mut egui::Ui,
|
||||
wallet: &Wallet,
|
||||
cb: &dyn PlatformCallbacks) {
|
||||
|
@ -136,7 +104,6 @@ impl WalletMessages {
|
|||
}
|
||||
self.first_draw = false;
|
||||
}
|
||||
|
||||
ui.add_space(3.0);
|
||||
|
||||
// Show creation of request to send or receive funds.
|
||||
|
@ -224,7 +191,10 @@ impl WalletMessages {
|
|||
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(), || {
|
||||
View::colored_text_button(ui,
|
||||
send_text,
|
||||
Colors::red(),
|
||||
Colors::white_or_black(false), || {
|
||||
self.show_request_modal(false, cb);
|
||||
});
|
||||
});
|
||||
|
@ -240,7 +210,10 @@ impl WalletMessages {
|
|||
/// 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(), || {
|
||||
View::colored_text_button(ui,
|
||||
receive_text,
|
||||
Colors::green(),
|
||||
Colors::white_or_black(false), || {
|
||||
self.show_request_modal(true, cb);
|
||||
});
|
||||
}
|
||||
|
@ -253,7 +226,10 @@ impl WalletMessages {
|
|||
} else {
|
||||
t!("wallets.send")
|
||||
};
|
||||
Modal::new(REQUEST_MODAL).position(ModalPosition::CenterTop).title(title).show();
|
||||
Modal::new(REQUEST_MODAL)
|
||||
.position(ModalPosition::CenterTop)
|
||||
.title(title)
|
||||
.show();
|
||||
cb.show_keyboard();
|
||||
}
|
||||
|
||||
|
@ -264,7 +240,9 @@ impl WalletMessages {
|
|||
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()));
|
||||
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)
|
||||
|
@ -280,7 +258,7 @@ impl WalletMessages {
|
|||
|
||||
let scroll_id = Id::from("message_input_scroll").with(wallet.get_config().id);
|
||||
ScrollArea::vertical()
|
||||
.id_source(scroll_id)
|
||||
.id_salt(scroll_id)
|
||||
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
|
||||
.max_height(128.0)
|
||||
.auto_shrink([false; 2])
|
||||
|
@ -340,11 +318,9 @@ impl WalletMessages {
|
|||
}
|
||||
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",
|
||||
|
@ -352,8 +328,18 @@ impl WalletMessages {
|
|||
);
|
||||
self.message_error = m;
|
||||
}
|
||||
// Set default error message.
|
||||
_ => {
|
||||
// Show tx modal or show default error message.
|
||||
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 {
|
||||
let finalize = slate.state == SlateState::Standard2 ||
|
||||
slate.state == SlateState::Invoice2;
|
||||
self.message_error = if finalize {
|
||||
|
@ -365,6 +351,7 @@ impl WalletMessages {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.message_loading = false;
|
||||
}
|
||||
return;
|
||||
|
@ -377,7 +364,7 @@ impl WalletMessages {
|
|||
ui.columns(2, |columns| {
|
||||
columns[0].vertical_centered_justified(|ui| {
|
||||
let scan_text = format!("{} {}", SCAN, t!("scan"));
|
||||
View::button(ui, scan_text, Colors::button(), || {
|
||||
View::button(ui, scan_text, Colors::white_or_black(false), || {
|
||||
self.message_edit.clear();
|
||||
self.message_error.clear();
|
||||
self.scan_modal_content = Some(CameraScanModal::default());
|
||||
|
@ -393,7 +380,7 @@ impl WalletMessages {
|
|||
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(), || {
|
||||
View::button(ui, paste, Colors::white_or_black(false), || {
|
||||
let buf = cb.get_string_from_buffer();
|
||||
let previous = self.message_edit.clone();
|
||||
self.message_edit = buf.clone().trim().to_string();
|
||||
|
@ -418,7 +405,7 @@ impl WalletMessages {
|
|||
} else {
|
||||
// Draw button to clear message input.
|
||||
let clear_text = format!("{} {}", BROOM, t!("clear"));
|
||||
View::button(ui, clear_text, Colors::button(), || {
|
||||
View::button(ui, clear_text, Colors::white_or_black(false), || {
|
||||
self.message_edit.clear();
|
||||
self.message_error.clear();
|
||||
});
|
||||
|
@ -455,11 +442,9 @@ impl WalletMessages {
|
|||
.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();
|
||||
|
|
|
@ -130,7 +130,7 @@ impl WalletAccountsModal {
|
|||
// Show list of accounts.
|
||||
let size = self.accounts.len();
|
||||
ScrollArea::vertical()
|
||||
.id_source("account_list_modal_scroll")
|
||||
.id_salt("account_list_modal_scroll")
|
||||
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
|
||||
.max_height(266.0)
|
||||
.auto_shrink([true; 2])
|
||||
|
@ -149,7 +149,7 @@ impl WalletAccountsModal {
|
|||
});
|
||||
|
||||
ui.add_space(2.0);
|
||||
View::horizontal_line(ui, Colors::stroke());
|
||||
View::horizontal_line(ui, Colors::item_stroke());
|
||||
ui.add_space(6.0);
|
||||
|
||||
// Setup spacing between buttons.
|
||||
|
|
|
@ -1,133 +0,0 @@
|
|||
// Copyright 2024 The Grim Developers
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use egui::scroll_area::ScrollBarVisibility;
|
||||
use egui::{Id, ScrollArea};
|
||||
|
||||
use crate::gui::Colors;
|
||||
use crate::gui::icons::COPY;
|
||||
use crate::gui::platform::PlatformCallbacks;
|
||||
use crate::gui::views::{CameraContent, Modal, View};
|
||||
use crate::gui::views::types::QrScanResult;
|
||||
use crate::wallet::Wallet;
|
||||
|
||||
/// QR code scan [`Modal`] content.
|
||||
pub struct WalletScanModal {
|
||||
/// Camera content for QR scan [`Modal`].
|
||||
camera_content: Option<CameraContent>,
|
||||
/// QR code scan result
|
||||
qr_scan_result: Option<QrScanResult>,
|
||||
}
|
||||
|
||||
impl Default for WalletScanModal {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
camera_content: None,
|
||||
qr_scan_result: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl WalletScanModal {
|
||||
/// Draw [`Modal`] content.
|
||||
pub fn ui(&mut self,
|
||||
ui: &mut egui::Ui,
|
||||
wallet: &Wallet,
|
||||
modal: &Modal,
|
||||
cb: &dyn PlatformCallbacks,
|
||||
mut on_result: impl FnMut(&QrScanResult)) {
|
||||
// Show scan result if exists or show camera content while scanning.
|
||||
if let Some(result) = &self.qr_scan_result {
|
||||
let mut result_text = result.text();
|
||||
View::horizontal_line(ui, Colors::item_stroke());
|
||||
ui.add_space(3.0);
|
||||
ScrollArea::vertical()
|
||||
.id_source(Id::from("qr_scan_result_input").with(wallet.get_config().id))
|
||||
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
|
||||
.max_height(128.0)
|
||||
.auto_shrink([false; 2])
|
||||
.show(ui, |ui| {
|
||||
ui.add_space(7.0);
|
||||
egui::TextEdit::multiline(&mut result_text)
|
||||
.font(egui::TextStyle::Small)
|
||||
.desired_rows(5)
|
||||
.interactive(false)
|
||||
.desired_width(f32::INFINITY)
|
||||
.show(ui);
|
||||
ui.add_space(6.0);
|
||||
});
|
||||
ui.add_space(2.0);
|
||||
View::horizontal_line(ui, Colors::item_stroke());
|
||||
ui.add_space(10.0);
|
||||
|
||||
// Show copy button.
|
||||
ui.vertical_centered(|ui| {
|
||||
let copy_text = format!("{} {}", COPY, t!("copy"));
|
||||
View::button(ui, copy_text, Colors::button(), || {
|
||||
cb.copy_string_to_buffer(result_text.to_string());
|
||||
self.qr_scan_result = None;
|
||||
modal.close();
|
||||
});
|
||||
});
|
||||
ui.add_space(10.0);
|
||||
View::horizontal_line(ui, Colors::item_stroke());
|
||||
ui.add_space(6.0);
|
||||
} else if let Some(result) = self.camera_content.get_or_insert(CameraContent::default())
|
||||
.qr_scan_result() {
|
||||
cb.stop_camera();
|
||||
self.camera_content = None;
|
||||
on_result(&result);
|
||||
|
||||
// Set result and rename modal title.
|
||||
self.qr_scan_result = Some(result);
|
||||
Modal::set_title(t!("scan_result"));
|
||||
} else {
|
||||
ui.add_space(6.0);
|
||||
self.camera_content.as_mut().unwrap().ui(ui, cb);
|
||||
ui.add_space(6.0);
|
||||
}
|
||||
|
||||
if self.qr_scan_result.is_some() {
|
||||
// Setup spacing between buttons.
|
||||
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
|
||||
|
||||
ui.columns(2, |columns| {
|
||||
columns[0].vertical_centered_justified(|ui| {
|
||||
View::button(ui, t!("close"), Colors::white_or_black(false), || {
|
||||
self.qr_scan_result = None;
|
||||
self.camera_content = None;
|
||||
modal.close();
|
||||
});
|
||||
});
|
||||
columns[1].vertical_centered_justified(|ui| {
|
||||
View::button(ui, t!("repeat"), Colors::white_or_black(false), || {
|
||||
Modal::set_title(t!("scan_qr"));
|
||||
self.qr_scan_result = None;
|
||||
self.camera_content = Some(CameraContent::default());
|
||||
cb.start_camera();
|
||||
});
|
||||
});
|
||||
});
|
||||
} else {
|
||||
ui.vertical_centered_justified(|ui| {
|
||||
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
|
||||
cb.stop_camera();
|
||||
self.camera_content = None;
|
||||
modal.close();
|
||||
});
|
||||
});
|
||||
}
|
||||
ui.add_space(6.0);
|
||||
}
|
||||
}
|
|
@ -62,7 +62,7 @@ impl Default for CommonSettings {
|
|||
impl CommonSettings {
|
||||
/// Draw common wallet settings content.
|
||||
pub fn ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) {
|
||||
// Show modal content for this ui container.
|
||||
// Show modal content for this container.
|
||||
self.modal_content_ui(ui, wallet, cb);
|
||||
|
||||
ui.vertical_centered(|ui| {
|
||||
|
@ -80,7 +80,7 @@ impl CommonSettings {
|
|||
|
||||
// Show wallet name setup.
|
||||
let name_text = format!("{} {}", PENCIL, t!("change"));
|
||||
View::button(ui, name_text, Colors::button(), || {
|
||||
View::button(ui, name_text, Colors::white_or_black(false), || {
|
||||
self.name_edit = config.name;
|
||||
// Show wallet name modal.
|
||||
Modal::new(NAME_EDIT_MODAL)
|
||||
|
@ -98,7 +98,7 @@ impl CommonSettings {
|
|||
|
||||
// Show wallet password setup.
|
||||
let pass_text = format!("{} {}", PASSWORD, t!("change"));
|
||||
View::button(ui, pass_text, Colors::button(), || {
|
||||
View::button(ui, pass_text, Colors::white_or_black(false), || {
|
||||
// Setup modal values.
|
||||
self.first_edit_pass_opening = true;
|
||||
self.old_pass_edit = "".to_string();
|
||||
|
@ -120,7 +120,7 @@ impl CommonSettings {
|
|||
|
||||
// Show minimum amount of confirmations value setup.
|
||||
let min_conf_text = format!("{} {}", CLOCK_COUNTDOWN, config.min_confirmations);
|
||||
View::button(ui, min_conf_text, Colors::button(), || {
|
||||
View::button(ui, min_conf_text, Colors::white_or_black(false), || {
|
||||
self.min_confirmations_edit = config.min_confirmations.to_string();
|
||||
// Show minimum amount of confirmations value modal.
|
||||
Modal::new(MIN_CONFIRMATIONS_EDIT_MODAL)
|
||||
|
|
|
@ -123,7 +123,7 @@ impl ConnectionSettings {
|
|||
|
||||
// Show button to add new external node connection.
|
||||
let add_node_text = format!("{} {}", PLUS_CIRCLE, t!("wallets.add_node"));
|
||||
View::button(ui, add_node_text, Colors::button(), || {
|
||||
View::button(ui, add_node_text, Colors::white_or_black(false), || {
|
||||
self.ext_conn_modal = ExternalConnectionModal::new(None);
|
||||
Modal::new(ExternalConnectionModal::WALLET_ID)
|
||||
.position(ModalPosition::CenterTop)
|
||||
|
|
|
@ -12,15 +12,9 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use egui::{Id, Margin, ScrollArea};
|
||||
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::{CommonSettings, ConnectionSettings, RecoverySettings};
|
||||
use crate::gui::views::wallets::types::{WalletTab, WalletTabType};
|
||||
use crate::gui::views::wallets::WalletContent;
|
||||
use crate::wallet::Wallet;
|
||||
|
||||
/// Wallet settings tab content.
|
||||
|
@ -52,42 +46,11 @@ impl WalletTab for WalletSettings {
|
|||
ui: &mut egui::Ui,
|
||||
wallet: &Wallet,
|
||||
cb: &dyn PlatformCallbacks) {
|
||||
// Show loading progress if navigation is blocked.
|
||||
if WalletContent::block_navigation_on_sync(wallet) {
|
||||
WalletContent::sync_progress_ui(ui, wallet);
|
||||
return;
|
||||
}
|
||||
|
||||
// Show settings 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_settings_scroll").with(wallet.get_config().id))
|
||||
.auto_shrink([false; 2])
|
||||
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
|
||||
.show(ui, |ui| {
|
||||
ui.vertical_centered(|ui| {
|
||||
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
|
||||
// Show common wallet setup.
|
||||
self.common_setup.ui(ui, wallet, cb);
|
||||
// Show wallet connections setup.
|
||||
self.conn_setup.wallet_ui(ui, wallet, cb);
|
||||
// Show wallet recovery setup.
|
||||
self.recovery_setup.ui(ui, wallet, cb);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
|
@ -92,9 +92,11 @@ impl RecoverySettings {
|
|||
ui.add_space(6.0);
|
||||
|
||||
// Draw button to restore the wallet.
|
||||
let recover_text = format!("{} {}", LIFEBUOY, t!("wallets.recover"));
|
||||
ui.add_space(4.0);
|
||||
View::colored_text_button(ui, recover_text, Colors::green(), Colors::button(), || {
|
||||
View::colored_text_button(ui,
|
||||
format!("{} {}", LIFEBUOY, t!("wallets.recover")),
|
||||
Colors::green(),
|
||||
Colors::white_or_black(false), || {
|
||||
wallet.delete_db(true);
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
|
@ -112,7 +114,7 @@ impl RecoverySettings {
|
|||
|
||||
// Draw button to show recovery phrase.
|
||||
let show_text = format!("{} {}", EYE, t!("show"));
|
||||
View::button(ui, show_text, Colors::button(), || {
|
||||
View::button(ui, show_text, Colors::white_or_black(false), || {
|
||||
self.show_recovery_phrase_modal(cb);
|
||||
});
|
||||
|
||||
|
@ -123,8 +125,10 @@ impl RecoverySettings {
|
|||
ui.add_space(6.0);
|
||||
|
||||
// Draw button to delete the wallet.
|
||||
let delete_text = format!("{} {}", TRASH, t!("wallets.delete"));
|
||||
View::colored_text_button(ui, delete_text, Colors::red(), Colors::button(), || {
|
||||
View::colored_text_button(ui,
|
||||
format!("{} {}", TRASH, t!("wallets.delete")),
|
||||
Colors::red(),
|
||||
Colors::white_or_black(false), || {
|
||||
Modal::new(DELETE_CONFIRMATION_MODAL)
|
||||
.position(ModalPosition::Center)
|
||||
.title(t!("confirmation"))
|
||||
|
|
|
@ -12,18 +12,16 @@
|
|||
// 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 egui::{Align, Layout, RichText, Rounding};
|
||||
|
||||
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::{Modal, QrCodeContent, 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;
|
||||
|
@ -49,39 +47,8 @@ impl WalletTab for WalletTransport {
|
|||
ui: &mut egui::Ui,
|
||||
wallet: &Wallet,
|
||||
cb: &dyn PlatformCallbacks) {
|
||||
if WalletContent::sync_ui(ui, wallet) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Show modal content for this ui container.
|
||||
self.modal_content_ui(ui, wallet, cb);
|
||||
|
||||
// Show transport content panel.
|
||||
egui::CentralPanel::default()
|
||||
.frame(egui::Frame {
|
||||
stroke: View::item_stroke(),
|
||||
fill: Colors::white_or_black(false),
|
||||
inner_margin: Margin {
|
||||
left: View::far_left_inset_margin(ui) + 4.0,
|
||||
right: View::get_right_inset() + 4.0,
|
||||
top: 3.0,
|
||||
bottom: 4.0,
|
||||
},
|
||||
..Default::default()
|
||||
})
|
||||
.show_inside(ui, |ui| {
|
||||
ScrollArea::vertical()
|
||||
.id_source(Id::from("wallet_transport").with(wallet.get_config().id))
|
||||
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
|
||||
.auto_shrink([false; 2])
|
||||
.show(ui, |ui| {
|
||||
ui.vertical_centered(|ui| {
|
||||
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
|
||||
self.ui(ui, wallet, cb);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
self.transport_ui(ui, wallet, cb);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -106,7 +73,7 @@ impl Default for WalletTransport {
|
|||
|
||||
impl WalletTransport {
|
||||
/// Draw wallet transport content.
|
||||
pub fn ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) {
|
||||
fn transport_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) {
|
||||
ui.add_space(3.0);
|
||||
ui.label(RichText::new(t!("transport.desc"))
|
||||
.size(16.0)
|
||||
|
@ -180,7 +147,7 @@ impl WalletTransport {
|
|||
// 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.painter().rect(bg_rect, item_rounding, Colors::fill_lite(), View::item_stroke());
|
||||
|
||||
ui.vertical(|ui| {
|
||||
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
|
||||
|
@ -286,7 +253,7 @@ impl WalletTransport {
|
|||
} else {
|
||||
View::item_rounding(1, 2, false)
|
||||
};
|
||||
ui.painter().rect(bg_rect, item_rounding, Colors::button(), View::item_stroke());
|
||||
ui.painter().rect(bg_rect, item_rounding, Colors::fill_lite(), View::item_stroke());
|
||||
|
||||
ui.vertical(|ui| {
|
||||
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
|
||||
|
|
|
@ -96,6 +96,7 @@ impl TransportSendModal {
|
|||
/// Draw content to send.
|
||||
fn content_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.address_scan_content.as_mut() {
|
||||
let mut on_stop = || {
|
||||
|
@ -249,7 +250,11 @@ impl TransportSendModal {
|
|||
}
|
||||
|
||||
/// Draw error content.
|
||||
fn error_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, modal: &Modal, cb: &dyn PlatformCallbacks) {
|
||||
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"))
|
||||
|
@ -342,7 +347,8 @@ impl TransportSendModal {
|
|||
let res = self.send_result.read().clone().unwrap();
|
||||
match res {
|
||||
Ok(tx) => {
|
||||
self.tx_info_content = Some(WalletTransactionModal::new(wallet, &tx, false));
|
||||
self.tx_info_content =
|
||||
Some(WalletTransactionModal::new(wallet, &tx, false));
|
||||
}
|
||||
Err(_) => {
|
||||
self.error = true;
|
||||
|
|
|
@ -12,20 +12,23 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::ops::Range;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use egui::{Align, Id, Layout, Margin, Rect, RichText, Rounding, ScrollArea};
|
||||
use egui::{Align, Id, Layout, Rect, RichText, Rounding, ScrollArea};
|
||||
use egui::epaint::RectShape;
|
||||
use egui::scroll_area::ScrollBarVisibility;
|
||||
use grin_core::consensus::COINBASE_MATURITY;
|
||||
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::icons::{ARROW_CIRCLE_DOWN, ARROW_CIRCLE_UP, BRIDGE, CALENDAR_CHECK, CHAT_CIRCLE_TEXT, CHECK, 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::types::{LinePosition, 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::gui::views::wallets::wallet::WalletTransactionModal;
|
||||
use crate::wallet::types::{WalletData, WalletTransaction};
|
||||
use crate::wallet::Wallet;
|
||||
|
||||
|
@ -57,32 +60,8 @@ impl WalletTab for WalletTransactions {
|
|||
}
|
||||
|
||||
fn ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) {
|
||||
if WalletContent::sync_ui(ui, wallet) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Show modal content for this ui container.
|
||||
self.modal_content_ui(ui, wallet, cb);
|
||||
|
||||
// Show wallet transactions content.
|
||||
egui::CentralPanel::default()
|
||||
.frame(egui::Frame {
|
||||
stroke: View::item_stroke(),
|
||||
fill: Colors::button(),
|
||||
inner_margin: Margin {
|
||||
left: View::far_left_inset_margin(ui) + 4.0,
|
||||
right: View::get_right_inset() + 4.0,
|
||||
top: 0.0,
|
||||
bottom: 4.0,
|
||||
},
|
||||
..Default::default()
|
||||
})
|
||||
.show_inside(ui, |ui| {
|
||||
ui.vertical_centered(|ui| {
|
||||
let data = wallet.get_data().unwrap();
|
||||
self.txs_ui(ui, wallet, &data, cb);
|
||||
});
|
||||
});
|
||||
self.txs_ui(ui, wallet, cb);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -91,59 +70,26 @@ 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;
|
||||
pub const TX_ITEM_HEIGHT: f32 = 75.0;
|
||||
|
||||
/// Draw transactions content.
|
||||
fn txs_ui(&mut self,
|
||||
ui: &mut egui::Ui,
|
||||
wallet: &Wallet,
|
||||
data: &WalletData,
|
||||
cb: &dyn PlatformCallbacks) {
|
||||
let amount_conf = data.info.amount_awaiting_confirmation;
|
||||
let amount_fin = data.info.amount_awaiting_finalization;
|
||||
let amount_locked = data.info.amount_locked;
|
||||
let data = wallet.get_data().unwrap();
|
||||
if data.txs.is_none() {
|
||||
ui.centered_and_justified(|ui| {
|
||||
View::big_loading_spinner(ui);
|
||||
});
|
||||
return;
|
||||
}
|
||||
let txs = data.txs.as_ref().unwrap();
|
||||
let mut awaiting_amount = false;
|
||||
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!(
|
||||
|
@ -152,25 +98,18 @@ impl WalletTransactions {
|
|||
"transport" => BRIDGE,
|
||||
"settings" => GEAR_FINE
|
||||
);
|
||||
ui.label(RichText::new(empty_text).size(16.0).color(Colors::inactive_text()));
|
||||
ui.label(RichText::new(empty_text)
|
||||
.size(16.0)
|
||||
.color(Colors::inactive_text()));
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Draw awaiting amount info if exists.
|
||||
awaiting_amount = self.awaiting_info_ui(ui, &data);
|
||||
});
|
||||
|
||||
// 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)
|
||||
|
@ -178,23 +117,53 @@ impl WalletTransactions {
|
|||
.min_refresh_distance(70.0)
|
||||
.scroll_area_ui(ui, |ui| {
|
||||
ScrollArea::vertical()
|
||||
.id_source(Id::from("txs_content").with(wallet.get_config().id))
|
||||
.id_salt(Id::from("wallet_tx_list_scroll").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;
|
||||
self.tx_list_ui(ui, awaiting_amount, row_range, wallet, txs, cb);
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
// Sync wallet on refresh.
|
||||
if refresh_resp.should_refresh() {
|
||||
self.manual_sync = Some(now);
|
||||
if !wallet.syncing() {
|
||||
wallet.sync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw transaction list content.
|
||||
fn tx_list_ui(&mut self,
|
||||
ui: &mut egui::Ui,
|
||||
awaiting: bool,
|
||||
row_range: Range<usize>,
|
||||
wallet: &Wallet,
|
||||
txs: &Vec<WalletTransaction>,
|
||||
cb: &dyn PlatformCallbacks) {
|
||||
for index in row_range {
|
||||
let tx = txs.get(index).unwrap();
|
||||
let mut r = View::item_rounding(index, txs.len(), false);
|
||||
let mut rect = if awaiting {
|
||||
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
|
||||
} else {
|
||||
ui.available_rect_before_wrap()
|
||||
};
|
||||
rect.set_height(Self::TX_ITEM_HEIGHT);
|
||||
Self::tx_item_ui(ui, tx, rect, r, &data, |ui| {
|
||||
|
||||
// Draw tx item background.
|
||||
let mut r = View::item_rounding(index, txs.len(), false);
|
||||
let p = ui.painter();
|
||||
p.rect(rect, r, Colors::fill_lite(), View::item_stroke());
|
||||
|
||||
let tx = txs.get(index).unwrap();
|
||||
let data = wallet.get_data().unwrap();
|
||||
Self::tx_item_ui(ui, tx, rect, &data, |ui| {
|
||||
// Draw button to show transaction info.
|
||||
if tx.data.tx_slate_id.is_some() {
|
||||
r.nw = 0.0;
|
||||
|
@ -229,17 +198,47 @@ impl WalletTransactions {
|
|||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
// Sync wallet on refresh.
|
||||
if refresh_resp.should_refresh() {
|
||||
self.manual_sync = Some(now);
|
||||
if !wallet.syncing() {
|
||||
wallet.sync();
|
||||
/// Draw information about locked, finalizing or confirming balance, return `true` if exists.
|
||||
fn awaiting_info_ui(&mut self, ui: &mut egui::Ui, data: &WalletData) -> bool {
|
||||
let amount_conf = data.info.amount_awaiting_confirmation;
|
||||
let amount_fin = data.info.amount_awaiting_finalization;
|
||||
let amount_locked = data.info.amount_locked;
|
||||
if amount_conf == 0 && amount_fin == 0 && amount_locked == 0 {
|
||||
return false;
|
||||
}
|
||||
let rect = ui.available_rect_before_wrap();
|
||||
// Draw background.
|
||||
let mut bg = RectShape::new(rect, Rounding {
|
||||
nw: 0.0,
|
||||
ne: 0.0,
|
||||
sw: 8.0,
|
||||
se: 8.0,
|
||||
}, Colors::TRANSPARENT, View::item_stroke());
|
||||
let bg_idx = ui.painter().add(bg);
|
||||
let resp = ui.allocate_ui(rect.size(), |ui| {
|
||||
ui.vertical_centered_justified(|ui| {
|
||||
// Correct vertical spacing between items.
|
||||
ui.style_mut().spacing.item_spacing.y = -3.0;
|
||||
if amount_conf != 0 {
|
||||
// Draw awaiting confirmation amount.
|
||||
awaiting_item_ui(ui, amount_conf, t!("wallets.await_conf_amount"));
|
||||
}
|
||||
if amount_fin != 0 {
|
||||
// Draw awaiting confirmation amount.
|
||||
awaiting_item_ui(ui, amount_fin, t!("wallets.await_fin_amount"));
|
||||
}
|
||||
if amount_locked != 0 {
|
||||
// Draw locked amount.
|
||||
awaiting_item_ui(ui, amount_locked, t!("wallets.locked_amount"));
|
||||
}
|
||||
});
|
||||
}).response;
|
||||
// Setup background size.
|
||||
bg.rect = resp.rect;
|
||||
ui.painter().set(bg_idx, bg);
|
||||
true
|
||||
}
|
||||
|
||||
/// Draw [`Modal`] content for this ui container.
|
||||
|
@ -273,13 +272,8 @@ impl WalletTransactions {
|
|||
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.
|
||||
|
@ -322,6 +316,7 @@ impl WalletTransactions {
|
|||
ui.add_space(-2.0);
|
||||
|
||||
// Setup transaction status text.
|
||||
let height = data.info.last_confirmed_height;
|
||||
let status_text = if !tx.data.confirmed {
|
||||
let is_canceled = tx.data.tx_type == TxLogEntryType::TxSentCancelled
|
||||
|| tx.data.tx_type == TxLogEntryType::TxReceivedCancelled;
|
||||
|
@ -344,10 +339,34 @@ impl WalletTransactions {
|
|||
DOTS_THREE_CIRCLE,
|
||||
t!("wallets.tx_sending"))
|
||||
},
|
||||
TxLogEntryType::ConfirmedCoinbase => {
|
||||
let tx_h = tx.height.unwrap_or(1) - 1;
|
||||
if tx_h != 0 {
|
||||
let left_conf = height - tx_h;
|
||||
if height >= tx_h && left_conf < COINBASE_MATURITY {
|
||||
let conf_info = format!("{}/{}",
|
||||
left_conf,
|
||||
COINBASE_MATURITY);
|
||||
format!("{} {} {}",
|
||||
DOTS_THREE_CIRCLE,
|
||||
t!("wallets.tx_confirming"),
|
||||
conf_info
|
||||
)
|
||||
} else {
|
||||
format!("{} {}",
|
||||
DOTS_THREE_CIRCLE,
|
||||
t!("wallets.tx_confirming"))
|
||||
}
|
||||
} else {
|
||||
format!("{} {}",
|
||||
DOTS_THREE_CIRCLE,
|
||||
t!("wallets.tx_confirming"))
|
||||
}
|
||||
},
|
||||
_ => {
|
||||
format!("{} {}",
|
||||
DOTS_THREE_CIRCLE,
|
||||
t!("wallets.tx_confirmed"))
|
||||
t!("wallets.tx_confirming"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -355,10 +374,31 @@ impl WalletTransactions {
|
|||
} else {
|
||||
match tx.data.tx_type {
|
||||
TxLogEntryType::ConfirmedCoinbase => {
|
||||
format!("{} {}", CHECK_CIRCLE, t!("wallets.tx_confirmed"))
|
||||
let tx_h = tx.height.unwrap_or(1) - 1;
|
||||
if tx_h != 0 {
|
||||
let left_conf = height - tx_h;
|
||||
if height >= tx_h && left_conf < COINBASE_MATURITY {
|
||||
let conf_info = format!("{}/{}",
|
||||
left_conf,
|
||||
COINBASE_MATURITY);
|
||||
format!("{} {} {}",
|
||||
DOTS_THREE_CIRCLE,
|
||||
t!("wallets.tx_confirming"),
|
||||
conf_info
|
||||
)
|
||||
} else {
|
||||
format!("{} {}",
|
||||
DOTS_THREE_CIRCLE,
|
||||
t!("wallets.tx_confirmed"))
|
||||
}
|
||||
} else {
|
||||
format!("{} {}",
|
||||
DOTS_THREE_CIRCLE,
|
||||
t!("wallets.tx_confirmed"))
|
||||
}
|
||||
|
||||
},
|
||||
TxLogEntryType::TxSent | TxLogEntryType::TxReceived => {
|
||||
let height = data.info.last_confirmed_height;
|
||||
let min_conf = data.info.minimum_confirmations;
|
||||
if tx.height.is_none() || (tx.height.unwrap() != 0 &&
|
||||
height - tx.height.unwrap() > min_conf - 1) {
|
||||
|
@ -481,3 +521,18 @@ impl WalletTransactions {
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw awaiting balance item content.
|
||||
fn awaiting_item_ui(ui: &mut egui::Ui, amount: u64, label: String) {
|
||||
let rect = ui.available_rect_before_wrap();
|
||||
View::line(ui, LinePosition::TOP, &rect, Colors::item_stroke());
|
||||
ui.add_space(4.0);
|
||||
let amount_format = amount_to_hr_string(amount, true);
|
||||
ui.label(RichText::new(format!("{} ツ", amount_format))
|
||||
.color(Colors::white_or_black(true))
|
||||
.size(17.0));
|
||||
ui.label(RichText::new(label)
|
||||
.color(Colors::gray())
|
||||
.size(15.0));
|
||||
ui.add_space(4.0);
|
||||
}
|
|
@ -53,7 +53,7 @@ pub struct WalletTransactionModal {
|
|||
qr_code_content: Option<QrCodeContent>,
|
||||
|
||||
/// QR code scanner content.
|
||||
qr_scan_content: Option<CameraContent>,
|
||||
scan_qr_content: Option<CameraContent>,
|
||||
|
||||
/// Button to parse picked file content.
|
||||
file_pick_button: FilePickButton,
|
||||
|
@ -93,7 +93,7 @@ impl WalletTransactionModal {
|
|||
finalizing: false,
|
||||
final_result: Arc::new(RwLock::new(None)),
|
||||
qr_code_content: None,
|
||||
qr_scan_content: None,
|
||||
scan_qr_content: None,
|
||||
file_pick_button: FilePickButton::default(),
|
||||
}
|
||||
}
|
||||
|
@ -122,78 +122,9 @@ impl WalletTransactionModal {
|
|||
}
|
||||
let tx = txs.get(0).unwrap();
|
||||
|
||||
if self.qr_code_content.is_none() && self.qr_scan_content.is_none() {
|
||||
ui.add_space(6.0);
|
||||
|
||||
let r = View::item_rounding(0, 2, false);
|
||||
let mut rect = ui.available_rect_before_wrap();
|
||||
rect.set_height(WalletTransactions::TX_ITEM_HEIGHT);
|
||||
// Show transaction amount status and time.
|
||||
WalletTransactions::tx_item_ui(ui, tx, rect, r, &data, |ui| {
|
||||
if self.finalizing {
|
||||
return;
|
||||
}
|
||||
// Show block height or buttons.
|
||||
if let Some(h) = tx.height {
|
||||
if h != 0 {
|
||||
ui.add_space(6.0);
|
||||
let height = format!("{} {}", CUBE, h.to_string());
|
||||
ui.with_layout(Layout::bottom_up(Align::Max), |ui| {
|
||||
ui.add_space(3.0);
|
||||
ui.label(RichText::new(height)
|
||||
.size(15.0)
|
||||
.color(Colors::text(false)));
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let wallet_loaded = wallet.foreign_api_port().is_some();
|
||||
|
||||
// Draw button to show transaction finalization or transaction info.
|
||||
if wallet_loaded && tx.can_finalize {
|
||||
let (icon, color) = if self.show_finalization {
|
||||
(FILE_TEXT, None)
|
||||
} else {
|
||||
(CHECK, Some(Colors::green()))
|
||||
};
|
||||
let mut r = r.clone();
|
||||
r.nw = 0.0;
|
||||
r.sw = 0.0;
|
||||
View::item_button(ui, r, icon, color, || {
|
||||
cb.hide_keyboard();
|
||||
if self.show_finalization {
|
||||
self.show_finalization = false;
|
||||
return;
|
||||
}
|
||||
self.show_finalization = true;
|
||||
});
|
||||
}
|
||||
|
||||
// Draw button to cancel transaction.
|
||||
if wallet_loaded && tx.can_cancel() {
|
||||
View::item_button(ui, Rounding::default(), PROHIBIT, Some(Colors::red()), || {
|
||||
cb.hide_keyboard();
|
||||
wallet.cancel(tx.data.id);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Show identifier.
|
||||
if let Some(id) = tx.data.tx_slate_id {
|
||||
let label = format!("{} {}", HASH_STRAIGHT, t!("id"));
|
||||
Self::info_item_ui(ui, id.to_string(), label, true, cb);
|
||||
}
|
||||
// Show kernel.
|
||||
if let Some(kernel) = tx.data.kernel_excess {
|
||||
let label = format!("{} {}", FILE_ARCHIVE, t!("kernel"));
|
||||
Self::info_item_ui(ui, kernel.0.to_hex(), label, true, cb);
|
||||
}
|
||||
// Show receiver address.
|
||||
if let Some(rec) = tx.receiver() {
|
||||
let label = format!("{} {}", CUBE, t!("network_mining.address"));
|
||||
Self::info_item_ui(ui, rec.to_string(), label, true, cb);
|
||||
}
|
||||
// Show transaction information.
|
||||
if self.qr_code_content.is_none() && self.scan_qr_content.is_none() {
|
||||
self.info_ui(ui, tx, wallet, cb);
|
||||
}
|
||||
|
||||
// Show Slatepack message interaction.
|
||||
|
@ -220,21 +151,21 @@ impl WalletTransactionModal {
|
|||
});
|
||||
});
|
||||
});
|
||||
} else if self.qr_scan_content.is_some() {
|
||||
} else if self.scan_qr_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;
|
||||
self.scan_qr_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;
|
||||
self.scan_qr_content = None;
|
||||
modal.enable_closing();
|
||||
});
|
||||
});
|
||||
|
@ -272,6 +203,7 @@ impl WalletTransactionModal {
|
|||
if let Ok(_) = res {
|
||||
self.show_finalization = false;
|
||||
self.finalize_edit = "".to_string();
|
||||
self.response_edit = "".to_string();
|
||||
} else {
|
||||
self.finalize_error = true;
|
||||
}
|
||||
|
@ -286,44 +218,91 @@ impl WalletTransactionModal {
|
|||
}
|
||||
}
|
||||
|
||||
/// Draw transaction information item content.
|
||||
fn info_item_ui(ui: &mut egui::Ui,
|
||||
value: String,
|
||||
label: String,
|
||||
copy: bool,
|
||||
/// Draw transaction information content.
|
||||
fn info_ui(&mut self,
|
||||
ui: &mut egui::Ui,
|
||||
tx: &WalletTransaction,
|
||||
wallet: &Wallet,
|
||||
cb: &dyn PlatformCallbacks) {
|
||||
// Setup layout size.
|
||||
ui.add_space(6.0);
|
||||
|
||||
let mut rect = ui.available_rect_before_wrap();
|
||||
rect.set_height(50.0);
|
||||
rect.set_height(WalletTransactions::TX_ITEM_HEIGHT);
|
||||
|
||||
// Draw round background.
|
||||
let bg_rect = rect.clone();
|
||||
let mut rounding = View::item_rounding(1, 3, false);
|
||||
// Draw tx item background.
|
||||
let p = ui.painter();
|
||||
let r = View::item_rounding(0, 2, false);
|
||||
p.rect(rect, r, Colors::TRANSPARENT, View::item_stroke());
|
||||
|
||||
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());
|
||||
// Show transaction amount status and time.
|
||||
let data = wallet.get_data().unwrap();
|
||||
WalletTransactions::tx_item_ui(ui, tx, rect, &data, |ui| {
|
||||
if self.finalizing {
|
||||
return;
|
||||
}
|
||||
// Show block height or buttons.
|
||||
if let Some(h) = tx.height {
|
||||
if h != 0 {
|
||||
ui.add_space(6.0);
|
||||
let height = format!("{} {}", CUBE, h.to_string());
|
||||
ui.with_layout(Layout::bottom_up(Align::Max), |ui| {
|
||||
ui.add_space(3.0);
|
||||
ui.label(RichText::new(height)
|
||||
.size(15.0)
|
||||
.color(Colors::text(false)));
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 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);
|
||||
let wallet_loaded = wallet.foreign_api_port().is_some();
|
||||
|
||||
// Draw button to show transaction finalization or request info.
|
||||
if wallet_loaded && tx.can_finalize {
|
||||
let (icon, color) = if self.show_finalization {
|
||||
(FILE_TEXT, None)
|
||||
} else {
|
||||
(CHECK, Some(Colors::green()))
|
||||
};
|
||||
let r = View::item_rounding(0, 2, true);
|
||||
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() {
|
||||
let r = if tx.can_finalize {
|
||||
Rounding::default()
|
||||
} else {
|
||||
View::item_rounding(0, 2, true)
|
||||
};
|
||||
View::item_button(ui, r, PROHIBIT, Some(Colors::red()), || {
|
||||
cb.hide_keyboard();
|
||||
wallet.cancel(tx.data.id);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Show identifier.
|
||||
if let Some(id) = tx.data.tx_slate_id {
|
||||
let label = format!("{} {}", HASH_STRAIGHT, t!("id"));
|
||||
info_item_ui(ui, id.to_string(), label, true, cb);
|
||||
}
|
||||
// Show kernel.
|
||||
if let Some(kernel) = tx.data.kernel_excess {
|
||||
let label = format!("{} {}", FILE_ARCHIVE, t!("kernel"));
|
||||
info_item_ui(ui, kernel.0.to_hex(), label, true, cb);
|
||||
}
|
||||
// Show receiver address.
|
||||
if let Some(rec) = tx.receiver() {
|
||||
let label = format!("{} {}", CUBE, t!("network_mining.address"));
|
||||
info_item_ui(ui, rec.to_string(), label, true, cb);
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw Slatepack message content.
|
||||
|
@ -336,8 +315,8 @@ impl WalletTransactionModal {
|
|||
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() {
|
||||
if let Some(scan_content) = self.scan_qr_content.as_mut() {
|
||||
if let Some(result) = scan_content.qr_scan_result() {
|
||||
cb.stop_camera();
|
||||
|
||||
// Setup value to finalization input field.
|
||||
|
@ -345,9 +324,9 @@ impl WalletTransactionModal {
|
|||
self.on_finalization_input_change(tx, wallet, modal, cb);
|
||||
|
||||
modal.enable_closing();
|
||||
self.qr_scan_content = None;
|
||||
self.scan_qr_content = None;
|
||||
} else {
|
||||
qr_scan_content.ui(ui, cb);
|
||||
scan_content.ui(ui, cb);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
@ -415,7 +394,7 @@ impl WalletTransactionModal {
|
|||
View::horizontal_line(ui, Colors::item_stroke());
|
||||
ui.add_space(3.0);
|
||||
ScrollArea::vertical()
|
||||
.id_source(scroll_id)
|
||||
.id_salt(scroll_id)
|
||||
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
|
||||
.max_height(128.0)
|
||||
.auto_shrink([false; 2])
|
||||
|
@ -460,17 +439,17 @@ impl WalletTransactionModal {
|
|||
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(), || {
|
||||
View::button(ui, qr_text, Colors::fill_lite(), || {
|
||||
cb.hide_keyboard();
|
||||
modal.disable_closing();
|
||||
cb.start_camera();
|
||||
self.qr_scan_content = Some(CameraContent::default());
|
||||
self.scan_qr_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(), || {
|
||||
View::button(ui, paste_text, Colors::fill_lite(), || {
|
||||
self.finalize_edit = cb.get_string_from_buffer();
|
||||
});
|
||||
});
|
||||
|
@ -480,7 +459,7 @@ impl WalletTransactionModal {
|
|||
if self.finalize_error {
|
||||
// Draw button to clear message input.
|
||||
let clear_text = format!("{} {}", BROOM, t!("clear"));
|
||||
View::button(ui, clear_text, Colors::button(), || {
|
||||
View::button(ui, clear_text, Colors::fill_lite(), || {
|
||||
self.finalize_edit.clear();
|
||||
self.finalize_error = false;
|
||||
});
|
||||
|
@ -501,7 +480,7 @@ impl WalletTransactionModal {
|
|||
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(), || {
|
||||
View::button(ui, qr_text.clone(), Colors::white_or_black(false), || {
|
||||
cb.hide_keyboard();
|
||||
let text = self.response_edit.clone();
|
||||
self.qr_code_content = Some(QrCodeContent::new(text, true));
|
||||
|
@ -510,7 +489,7 @@ impl WalletTransactionModal {
|
|||
columns[1].vertical_centered_justified(|ui| {
|
||||
// Draw copy button.
|
||||
let copy_text = format!("{} {}", COPY, t!("copy"));
|
||||
View::button(ui, copy_text, Colors::button(), || {
|
||||
View::button(ui, copy_text, Colors::white_or_black(false), || {
|
||||
cb.copy_string_to_buffer(self.response_edit.clone());
|
||||
self.finalize_edit = "".to_string();
|
||||
if tx.can_finalize {
|
||||
|
@ -579,3 +558,43 @@ impl WalletTransactionModal {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
29
src/lib.rs
29
src/lib.rs
|
@ -16,7 +16,7 @@
|
|||
extern crate rust_i18n;
|
||||
|
||||
use eframe::NativeOptions;
|
||||
use egui::{Context, Stroke};
|
||||
use egui::{Context, Stroke, Theme};
|
||||
use lazy_static::lazy_static;
|
||||
use std::sync::Arc;
|
||||
use parking_lot::RwLock;
|
||||
|
@ -77,6 +77,7 @@ fn android_main(app: AndroidApp) {
|
|||
options.wgpu_options.device_descriptor = std::sync::Arc::new(|_| {
|
||||
let base_limits = wgpu::Limits::downlevel_webgl2_defaults();
|
||||
wgpu::DeviceDescriptor {
|
||||
memory_hints: wgpu::MemoryHints::default(),
|
||||
label: Some("egui wgpu device"),
|
||||
required_features: wgpu::Features::default(),
|
||||
required_limits: wgpu::Limits {
|
||||
|
@ -102,31 +103,22 @@ fn use_dark_theme(platform: &gui::platform::Android) -> bool {
|
|||
}
|
||||
|
||||
/// [`App`] setup for [`eframe`].
|
||||
pub fn app_creator<T: 'static>(app: App<T>) -> eframe::AppCreator
|
||||
pub fn app_creator<T: 'static>(app: App<T>) -> eframe::AppCreator<'static>
|
||||
where App<T>: eframe::App, T: PlatformCallbacks {
|
||||
Box::new(|cc| {
|
||||
setup_fonts(&cc.egui_ctx);
|
||||
// Setup images support.
|
||||
egui_extras::install_image_loaders(&cc.egui_ctx);
|
||||
// Setup visuals.
|
||||
setup_visuals(&cc.egui_ctx);
|
||||
// Setup fonts.
|
||||
setup_fonts(&cc.egui_ctx);
|
||||
// Return app instance.
|
||||
Ok(Box::new(app))
|
||||
})
|
||||
}
|
||||
|
||||
/// Entry point to start ui with [`eframe`].
|
||||
pub fn start(mut options: NativeOptions, app_creator: eframe::AppCreator) -> eframe::Result<()> {
|
||||
options.default_theme = if AppConfig::dark_theme().unwrap_or(false) {
|
||||
eframe::Theme::Dark
|
||||
} else {
|
||||
eframe::Theme::Light
|
||||
};
|
||||
pub fn start(options: NativeOptions, app_creator: eframe::AppCreator) -> eframe::Result<()> {
|
||||
// Setup translations.
|
||||
setup_i18n();
|
||||
// Start integrated node if needed.
|
||||
if Settings::app_config_to_read().auto_start_node {
|
||||
if AppConfig::autostart_node() {
|
||||
Node::start();
|
||||
}
|
||||
// Launch graphical interface.
|
||||
|
@ -135,6 +127,10 @@ pub fn start(mut options: NativeOptions, app_creator: eframe::AppCreator) -> efr
|
|||
|
||||
/// Setup application [`egui::Style`] and [`egui::Visuals`].
|
||||
pub fn setup_visuals(ctx: &Context) {
|
||||
let use_dark = AppConfig::dark_theme().unwrap_or_else(|| {
|
||||
ctx.system_theme().unwrap_or(Theme::Dark) == Theme::Dark
|
||||
});
|
||||
|
||||
let mut style = (*ctx.style()).clone();
|
||||
// Setup selection.
|
||||
style.interaction.selectable_labels = false;
|
||||
|
@ -155,7 +151,6 @@ pub fn setup_visuals(ctx: &Context) {
|
|||
ctx.set_style(style);
|
||||
|
||||
// Setup visuals based on app color theme.
|
||||
let use_dark = AppConfig::dark_theme().unwrap_or(false);
|
||||
let mut visuals = if use_dark {
|
||||
egui::Visuals::dark()
|
||||
} else {
|
||||
|
@ -191,9 +186,9 @@ pub fn setup_fonts(ctx: &Context) {
|
|||
"../fonts/phosphor.ttf"
|
||||
)).tweak(egui::FontTweak {
|
||||
scale: 1.0,
|
||||
y_offset_factor: -0.30,
|
||||
y_offset_factor: -0.20,
|
||||
y_offset: 0.0,
|
||||
baseline_offset_factor: 0.50,
|
||||
baseline_offset_factor: 0.16,
|
||||
}),
|
||||
);
|
||||
fonts
|
||||
|
|
100
src/main.rs
100
src/main.rs
|
@ -43,25 +43,39 @@ fn real_main() {
|
|||
|
||||
// Setup callback on panic crash.
|
||||
std::panic::set_hook(Box::new(|info| {
|
||||
let backtrace = backtrace::Backtrace::new();
|
||||
// Format error.
|
||||
let backtrace = backtrace::Backtrace::new();
|
||||
let time = grim::gui::views::View::format_time(chrono::Utc::now().timestamp());
|
||||
let target = egui::os::OperatingSystem::from_target_os();
|
||||
let os = egui::os::OperatingSystem::from_target_os();
|
||||
let ver = grim::VERSION;
|
||||
let msg = panic_info_message(info);
|
||||
let err = format!("{} - {:?} - v{}\n\n{}\n\n{:?}", time, target, ver, msg, backtrace);
|
||||
let loc = if let Some(location) = info.location() {
|
||||
format!("{}:{}:{}", location.file(), location.line(), location.column())
|
||||
} else {
|
||||
"no location found.".parse().unwrap()
|
||||
};
|
||||
let err = format!("{} - {:?} - v{}\n{}\n{}\n\n{:?}", time, os, ver, msg, loc, backtrace);
|
||||
// Save backtrace to file.
|
||||
let log = grim::Settings::crash_report_path();
|
||||
if log.exists() {
|
||||
let _ = std::fs::remove_file(log.clone());
|
||||
use std::io::{Seek, SeekFrom, Write};
|
||||
let mut file = std::fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.append(true)
|
||||
.open(log)
|
||||
.unwrap();
|
||||
if file.seek(SeekFrom::End(0)).is_ok() {
|
||||
file.write(err.as_bytes()).unwrap_or_default();
|
||||
}
|
||||
std::fs::write(log, err.as_bytes()).unwrap();
|
||||
// Setup flag to show crash after app restart.
|
||||
grim::AppConfig::set_show_crash(true);
|
||||
} else {
|
||||
std::fs::write(log, err.as_bytes()).unwrap_or_default();
|
||||
}
|
||||
// Print message error.
|
||||
println!("{}\n{}", msg, loc);
|
||||
}));
|
||||
|
||||
// Start GUI.
|
||||
match std::panic::catch_unwind(|| {
|
||||
let _ = std::panic::catch_unwind(|| {
|
||||
if is_app_running(&data) {
|
||||
return;
|
||||
} else if let Some(data) = data {
|
||||
|
@ -70,16 +84,13 @@ fn real_main() {
|
|||
let platform = grim::gui::platform::Desktop::new();
|
||||
start_app_socket(platform.clone());
|
||||
start_desktop_gui(platform);
|
||||
}) {
|
||||
Ok(_) => {}
|
||||
Err(e) => println!("{:?}", e)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Get panic message from crash payload.
|
||||
#[allow(dead_code)]
|
||||
#[cfg(not(target_os = "android"))]
|
||||
fn panic_info_message<'pi>(panic_info: &'pi std::panic::PanicInfo<'_>) -> &'pi str {
|
||||
fn panic_info_message<'pi>(panic_info: &'pi std::panic::PanicHookInfo<'_>) -> &'pi str {
|
||||
let payload = panic_info.payload();
|
||||
// taken from: https://github.com/rust-lang/rust/blob/4b9f4b221b92193c7e95b1beb502c6eb32c3b613/library/std/src/panicking.rs#L194-L200
|
||||
match payload.downcast_ref::<&'static str>() {
|
||||
|
@ -97,24 +108,14 @@ fn panic_info_message<'pi>(panic_info: &'pi std::panic::PanicInfo<'_>) -> &'pi s
|
|||
#[cfg(not(target_os = "android"))]
|
||||
fn start_desktop_gui(platform: grim::gui::platform::Desktop) {
|
||||
use grim::AppConfig;
|
||||
use dark_light::Mode;
|
||||
|
||||
// Setup system theme if not set.
|
||||
if let None = AppConfig::dark_theme() {
|
||||
let dark = match dark_light::detect() {
|
||||
Mode::Dark => true,
|
||||
Mode::Light => false,
|
||||
Mode::Default => false
|
||||
};
|
||||
AppConfig::set_dark_theme(dark);
|
||||
}
|
||||
|
||||
let os = egui::os::OperatingSystem::from_target_os();
|
||||
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.
|
||||
// Setup 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));
|
||||
}
|
||||
|
@ -123,10 +124,10 @@ fn start_desktop_gui(platform: grim::gui::platform::Desktop) {
|
|||
viewport = viewport.with_position(egui::pos2(x, y));
|
||||
}
|
||||
// Setup window decorations.
|
||||
let is_mac = egui::os::OperatingSystem::from_target_os() == egui::os::OperatingSystem::Mac;
|
||||
let is_mac = os == egui::os::OperatingSystem::Mac;
|
||||
viewport = viewport
|
||||
.with_window_level(egui::WindowLevel::Normal)
|
||||
.with_fullsize_content_view(true)
|
||||
.with_window_level(egui::WindowLevel::Normal)
|
||||
.with_title_shown(false)
|
||||
.with_titlebar_buttons_shown(false)
|
||||
.with_titlebar_shown(false)
|
||||
|
@ -138,8 +139,8 @@ fn start_desktop_gui(platform: grim::gui::platform::Desktop) {
|
|||
..Default::default()
|
||||
};
|
||||
// Use Glow renderer for Windows.
|
||||
let win = egui::os::OperatingSystem::from_target_os() == egui::os::OperatingSystem::Windows;
|
||||
options.renderer = if win {
|
||||
let is_win = os == egui::os::OperatingSystem::Windows;
|
||||
options.renderer = if is_win {
|
||||
eframe::Renderer::Glow
|
||||
} else {
|
||||
eframe::Renderer::Wgpu
|
||||
|
@ -150,7 +151,7 @@ fn start_desktop_gui(platform: grim::gui::platform::Desktop) {
|
|||
match grim::start(options.clone(), grim::app_creator(app)) {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
if win {
|
||||
if is_win {
|
||||
panic!("{}", e);
|
||||
}
|
||||
// Start with another renderer on error.
|
||||
|
@ -176,19 +177,15 @@ fn is_app_running(data: &Option<String>) -> bool {
|
|||
let res: Result<(), Box<dyn std::error::Error>> = runtime
|
||||
.block_on(async {
|
||||
use interprocess::local_socket::{
|
||||
tokio::{prelude::*, Stream},
|
||||
GenericFilePath, GenericNamespaced
|
||||
tokio::{prelude::*, Stream}
|
||||
};
|
||||
use tokio::{
|
||||
io::AsyncWriteExt,
|
||||
};
|
||||
|
||||
let socket_path = grim::Settings::socket_path();
|
||||
let name = if GenericNamespaced::is_supported() {
|
||||
grim::Settings::SOCKET_NAME.to_ns_name::<GenericNamespaced>()?
|
||||
} else {
|
||||
socket_path.clone().to_fs_name::<GenericFilePath>()?
|
||||
};
|
||||
let name = socket_name(&socket_path)?;
|
||||
|
||||
// Connect to running application socket.
|
||||
let conn = Stream::connect(name).await?;
|
||||
let data = data.clone().unwrap_or("".to_string());
|
||||
|
@ -203,7 +200,7 @@ fn is_app_running(data: &Option<String>) -> bool {
|
|||
drop((rec, sen));
|
||||
Ok(())
|
||||
});
|
||||
return match res {
|
||||
match res {
|
||||
Ok(_) => true,
|
||||
Err(_) => false
|
||||
}
|
||||
|
@ -220,7 +217,7 @@ fn start_app_socket(platform: grim::gui::platform::Desktop) {
|
|||
.block_on(async {
|
||||
use interprocess::local_socket::{
|
||||
tokio::{prelude::*, Stream},
|
||||
GenericFilePath, GenericNamespaced, Listener, ListenerOptions,
|
||||
Listener, ListenerOptions,
|
||||
};
|
||||
use std::io;
|
||||
use tokio::{
|
||||
|
@ -238,15 +235,12 @@ fn start_app_socket(platform: grim::gui::platform::Desktop) {
|
|||
Ok(buffer)
|
||||
}
|
||||
|
||||
// Setup socket name.
|
||||
let socket_path = grim::Settings::socket_path();
|
||||
let name = if GenericNamespaced::is_supported() {
|
||||
grim::Settings::SOCKET_NAME.to_ns_name::<GenericNamespaced>()?
|
||||
} else {
|
||||
socket_path.clone().to_fs_name::<GenericFilePath>()?
|
||||
};
|
||||
if socket_path.exists() {
|
||||
let _ = std::fs::remove_file(socket_path);
|
||||
let _ = std::fs::remove_file(&socket_path);
|
||||
}
|
||||
let name = socket_name(&socket_path)?;
|
||||
|
||||
// Create listener.
|
||||
let opts = ListenerOptions::new().name(name);
|
||||
|
@ -279,3 +273,17 @@ fn start_app_socket(platform: grim::gui::platform::Desktop) {
|
|||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// Get application socket name from provided path.
|
||||
#[allow(dead_code)]
|
||||
#[cfg(not(target_os = "android"))]
|
||||
fn socket_name(path: &std::path::PathBuf) -> std::io::Result<interprocess::local_socket::Name> {
|
||||
use interprocess::local_socket::{NameType, ToFsName, ToNsName};
|
||||
let name = if egui::os::OperatingSystem::Mac != egui::os::OperatingSystem::from_target_os() &&
|
||||
interprocess::local_socket::GenericNamespaced::is_supported() {
|
||||
grim::Settings::SOCKET_NAME.to_ns_name::<interprocess::local_socket::GenericNamespaced>()?
|
||||
} else {
|
||||
path.clone().to_fs_name::<interprocess::local_socket::GenericFilePath>()?
|
||||
};
|
||||
Ok(name)
|
||||
}
|
|
@ -505,22 +505,29 @@ impl NodeConfig {
|
|||
}
|
||||
|
||||
/// Get API secret text.
|
||||
pub fn get_api_secret() -> Option<String> {
|
||||
pub fn get_api_secret(foreign: bool) -> Option<String> {
|
||||
let r_config = Settings::node_config_to_read();
|
||||
let api_secret_path = r_config
|
||||
let api_secret_path = if foreign {
|
||||
&r_config
|
||||
.node
|
||||
.server
|
||||
.foreign_api_secret_path
|
||||
} else {
|
||||
&r_config
|
||||
.node
|
||||
.server
|
||||
.api_secret_path
|
||||
.clone();
|
||||
return if let Some(secret_path) = api_secret_path {
|
||||
let api_secret_file = File::open(secret_path).unwrap();
|
||||
let buf_reader = BufReader::new(api_secret_file);
|
||||
}.clone();
|
||||
if let Some(secret_path) = api_secret_path {
|
||||
if let Ok(file) = File::open(secret_path) {
|
||||
let buf_reader = BufReader::new(file);
|
||||
let mut lines_iter = buf_reader.lines();
|
||||
let first_line = lines_iter.next().unwrap();
|
||||
Some(first_line.unwrap())
|
||||
} else {
|
||||
if let Some(Ok(line)) = lines_iter.next() {
|
||||
return Some(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
};
|
||||
}
|
||||
|
||||
/// Save API secret text.
|
||||
|
@ -528,25 +535,6 @@ impl NodeConfig {
|
|||
Self::save_secret(api_secret, API_SECRET_FILE_NAME);
|
||||
}
|
||||
|
||||
/// Get Foreign API secret text.
|
||||
pub fn get_foreign_api_secret() -> Option<String> {
|
||||
let r_config = Settings::node_config_to_read();
|
||||
let foreign_secret_path = r_config
|
||||
.node
|
||||
.server
|
||||
.foreign_api_secret_path
|
||||
.clone();
|
||||
return if let Some(secret_path) = foreign_secret_path {
|
||||
let foreign_secret_file = File::open(secret_path).unwrap();
|
||||
let buf_reader = BufReader::new(foreign_secret_file);
|
||||
let mut lines_iter = buf_reader.lines();
|
||||
let first_line = lines_iter.next().unwrap();
|
||||
Some(first_line.unwrap())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
}
|
||||
|
||||
/// Update Foreign API secret.
|
||||
pub fn save_foreign_api_secret(api_secret: &String) {
|
||||
Self::save_secret(api_secret, FOREIGN_API_SECRET_FILE_NAME);
|
||||
|
|
|
@ -15,7 +15,6 @@
|
|||
//! Build a block to mine: gathers transactions from the pool, assembles
|
||||
//! them into a block and returns it.
|
||||
|
||||
use std::panic::panic_any;
|
||||
use chrono::prelude::{DateTime, Utc};
|
||||
use rand::{thread_rng, Rng};
|
||||
use serde_json::{json, Value};
|
||||
|
@ -77,7 +76,7 @@ pub fn get_block(
|
|||
key_id: Option<Identifier>,
|
||||
wallet_listener_url: Option<String>,
|
||||
stop_state: &Arc<StratumStopState>
|
||||
) -> (core::Block, BlockFees) {
|
||||
) -> Option<(core::Block, BlockFees)> {
|
||||
let wallet_retry_interval = 5;
|
||||
// get the latest chain state and build a block on top of it
|
||||
let mut result = build_block(chain, tx_pool, key_id.clone(), wallet_listener_url.clone());
|
||||
|
@ -116,12 +115,11 @@ pub fn get_block(
|
|||
|
||||
// Stop attempts to build a block on stop.
|
||||
if stop_state.is_stopped() {
|
||||
panic_any("Stopped");
|
||||
return None;
|
||||
}
|
||||
|
||||
result = build_block(chain, tx_pool, new_key_id, wallet_listener_url.clone());
|
||||
}
|
||||
return result.unwrap();
|
||||
Some(result.unwrap())
|
||||
}
|
||||
|
||||
/// Builds a new block with the chain head as previous and eligible
|
||||
|
|
|
@ -627,10 +627,6 @@ fn start_node_server() -> Result<Server, Error> {
|
|||
let api_chan: &'static mut (oneshot::Sender<()>, oneshot::Receiver<()>) =
|
||||
Box::leak(Box::new(oneshot::channel::<()>()));
|
||||
let server_result = Server::new(server_config, None, api_chan);
|
||||
|
||||
// Delay after server start.
|
||||
thread::sleep(Duration::from_millis(5000));
|
||||
|
||||
server_result
|
||||
}
|
||||
|
||||
|
|
|
@ -604,14 +604,13 @@ impl Handler {
|
|||
let clear_blocks = current_hash != latest_hash;
|
||||
|
||||
// Build the new block (version)
|
||||
let (new_block, block_fees) = get_block(
|
||||
if let Some((new_block, block_fees)) = get_block(
|
||||
&self.chain,
|
||||
tx_pool,
|
||||
state.current_key_id.clone(),
|
||||
wallet_listener_url,
|
||||
&stop_state
|
||||
);
|
||||
|
||||
) {
|
||||
// scaled difficulty
|
||||
state.current_difficulty =
|
||||
(new_block.header.total_difficulty() - head.total_difficulty).to_num();
|
||||
|
@ -636,8 +635,12 @@ impl Handler {
|
|||
self.workers.update_network_difficulty(difficulty.to_num());
|
||||
self.workers.update_network_hashrate();
|
||||
|
||||
// Add this new block candidate onto our list of block versions for this height
|
||||
// Add this new block candidate onto our list of block versions for height
|
||||
state.current_block_versions.push(new_block);
|
||||
} else {
|
||||
thread::sleep(Duration::from_millis(1500));
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Send this job to all connected workers
|
||||
self.broadcast_job();
|
||||
|
@ -716,7 +719,7 @@ fn accept_connections(listen_addr: SocketAddr,
|
|||
let mut rt = Runtime::new().unwrap();
|
||||
let (task, handle) = abortable(task);
|
||||
rt.spawn(check_stop_state(stop_state, handle));
|
||||
rt.block_on(task).unwrap();
|
||||
rt.block_on(task).unwrap_or_default();
|
||||
}
|
||||
|
||||
async fn check_stop_state(stop_state: Arc<StratumStopState>, handle: AbortHandle) {
|
||||
|
|
|
@ -49,9 +49,6 @@ pub struct AppConfig {
|
|||
|
||||
/// Flag to check if dark theme should be used, use system settings if not set.
|
||||
use_dark_theme: Option<bool>,
|
||||
|
||||
/// Flag to show crash report when happened.
|
||||
show_crash: Option<bool>
|
||||
}
|
||||
|
||||
impl Default for AppConfig {
|
||||
|
@ -68,7 +65,6 @@ impl Default for AppConfig {
|
|||
y: None,
|
||||
lang: None,
|
||||
use_dark_theme: None,
|
||||
show_crash: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -241,17 +237,4 @@ impl AppConfig {
|
|||
w_config.use_dark_theme = Some(use_dark);
|
||||
w_config.save();
|
||||
}
|
||||
|
||||
/// Check if crash report should be shown on application start.
|
||||
pub fn show_crash() -> bool {
|
||||
let r_config = Settings::app_config_to_read();
|
||||
r_config.show_crash.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Setup flag to show crash report on application start.
|
||||
pub fn set_show_crash(show: bool) {
|
||||
let mut w_config = Settings::app_config_to_update();
|
||||
w_config.show_crash = Some(show);
|
||||
w_config.save();
|
||||
}
|
||||
}
|
|
@ -20,7 +20,6 @@ use lazy_static::lazy_static;
|
|||
use parking_lot::{RwLock, RwLockReadGuard, RwLockWriteGuard};
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::Serialize;
|
||||
|
||||
use grin_config::ConfigError;
|
||||
|
||||
use crate::node::NodeConfig;
|
||||
|
@ -127,10 +126,7 @@ impl Settings {
|
|||
/// Get base directory path for configuration.
|
||||
pub fn base_path(sub_dir: Option<String>) -> PathBuf {
|
||||
// Check if dir exists.
|
||||
let mut path = match dirs::home_dir() {
|
||||
Some(p) => p,
|
||||
None => PathBuf::new(),
|
||||
};
|
||||
let mut path = dirs::home_dir().unwrap_or_else(|| PathBuf::new());
|
||||
path.push(Self::MAIN_DIR_NAME);
|
||||
if sub_dir.is_some() {
|
||||
path.push(sub_dir.unwrap());
|
||||
|
@ -149,20 +145,28 @@ impl Settings {
|
|||
socket_path
|
||||
}
|
||||
|
||||
/// Get configuration file path from provided name and sub-directory if needed.
|
||||
/// Get configuration file path from provided name and subdirectory if needed.
|
||||
pub fn config_path(config_name: &str, sub_dir: Option<String>) -> PathBuf {
|
||||
let mut path = Self::base_path(sub_dir);
|
||||
path.push(config_name);
|
||||
path
|
||||
}
|
||||
|
||||
/// Get configuration file path from provided name and sub-directory if needed.
|
||||
/// Get configuration file path from provided name and subdirectory if needed.
|
||||
pub fn crash_report_path() -> PathBuf {
|
||||
let mut path = Self::base_path(None);
|
||||
path.push(Self::CRASH_REPORT_FILE_NAME);
|
||||
path
|
||||
}
|
||||
|
||||
/// Delete crash report file.
|
||||
pub fn delete_crash_report() {
|
||||
let log = Self::crash_report_path();
|
||||
if log.exists() {
|
||||
let _ = fs::remove_file(log.clone());
|
||||
}
|
||||
}
|
||||
|
||||
/// Read configuration from the file.
|
||||
pub fn read_from_file<T: DeserializeOwned>(config_path: PathBuf) -> Result<T, ConfigError> {
|
||||
let file_content = fs::read_to_string(config_path.clone())?;
|
||||
|
|
258
src/tor/http.rs
Normal file
258
src/tor/http.rs
Normal file
|
@ -0,0 +1,258 @@
|
|||
// Copyright 2024 The Grim Developers
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::future::Future;
|
||||
use std::io::Error;
|
||||
use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
use std::task::{Context, Poll};
|
||||
|
||||
use arti_client::{DataStream, IntoTorAddr, TorClient};
|
||||
use hyper::client::connect::{Connected, Connection};
|
||||
use hyper::http::uri::Scheme;
|
||||
use hyper::http::Uri;
|
||||
use hyper::service::Service;
|
||||
use pin_project::pin_project;
|
||||
use thiserror::Error;
|
||||
use tls_api::TlsConnector as TlsConn; // This is different from tor_rtcompat::TlsConnector
|
||||
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
|
||||
use tor_config::deps::educe::Educe;
|
||||
use tor_rtcompat::Runtime;
|
||||
|
||||
/// Error making or using http connection
|
||||
///
|
||||
/// This error ends up being passed to hyper and bundled up into a [`hyper::Error`]
|
||||
#[derive(Error, Clone, Debug)]
|
||||
#[non_exhaustive]
|
||||
pub enum ConnectionError {
|
||||
/// Unsupported URI scheme
|
||||
#[error("unsupported URI scheme in {uri:?}")]
|
||||
UnsupportedUriScheme {
|
||||
/// URI
|
||||
uri: Uri,
|
||||
},
|
||||
|
||||
/// Missing hostname
|
||||
#[error("Missing hostname in {uri:?}")]
|
||||
MissingHostname {
|
||||
/// URI
|
||||
uri: Uri,
|
||||
},
|
||||
|
||||
/// Tor connection failed
|
||||
#[error("Tor connection failed")]
|
||||
Arti(#[from] arti_client::Error),
|
||||
|
||||
/// TLS connection failed
|
||||
#[error("TLS connection failed")]
|
||||
TLS(#[source] Arc<anyhow::Error>),
|
||||
}
|
||||
|
||||
/// We implement this for form's sake
|
||||
impl tor_error::HasKind for ConnectionError {
|
||||
#[rustfmt::skip]
|
||||
fn kind(&self) -> tor_error::ErrorKind {
|
||||
use ConnectionError as CE;
|
||||
use tor_error::ErrorKind as EK;
|
||||
match self {
|
||||
CE::UnsupportedUriScheme{..} => EK::NotImplemented,
|
||||
CE::MissingHostname{..} => EK::BadApiUsage,
|
||||
CE::Arti(e) => e.kind(),
|
||||
CE::TLS(_) => EK::RemoteProtocolViolation,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// **Main entrypoint**: `hyper` connector to make HTTP\[S] connections via Tor, using Arti.
|
||||
///
|
||||
/// An `ArtiHttpConnector` combines an Arti Tor client, and a TLS implementation,
|
||||
/// in a form that can be provided to hyper
|
||||
/// (e.g. to [`hyper::client::Builder`]'s `build` method)
|
||||
/// so that hyper can speak HTTP and HTTPS to origin servers via Tor.
|
||||
///
|
||||
/// TC is the TLS to used *across* Tor to connect to the origin server.
|
||||
/// For example, it could be a [`tls_api_native_tls::TlsConnector`].
|
||||
/// This is a different Rust type to the TLS used *by* Tor to connect to relays etc.
|
||||
/// It might even be a different underlying TLS implementation
|
||||
/// (although that is usually not a particularly good idea).
|
||||
#[derive(Educe)]
|
||||
#[educe(Clone)] // #[derive(Debug)] infers an unwanted bound TC: Clone
|
||||
pub struct ArtiHttpConnector<R: Runtime, TC: TlsConn> {
|
||||
/// The client
|
||||
client: TorClient<R>,
|
||||
|
||||
/// TLS for using across Tor.
|
||||
tls_conn: Arc<TC>,
|
||||
}
|
||||
|
||||
// #[derive(Clone)] infers a TC: Clone bound
|
||||
|
||||
impl<R: Runtime, TC: TlsConn> ArtiHttpConnector<R, TC> {
|
||||
/// Make a new `ArtiHttpConnector` using an Arti `TorClient` object.
|
||||
pub fn new(client: TorClient<R>, tls_conn: TC) -> Self {
|
||||
let tls_conn = tls_conn.into();
|
||||
Self { client, tls_conn }
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrapper type that makes an Arti `DataStream` implement necessary traits to be used as
|
||||
/// a `hyper` connection object (mainly `Connection`).
|
||||
///
|
||||
/// This might represent a bare HTTP connection across Tor,
|
||||
/// or it might represent an HTTPS connection through Tor to an origin server,
|
||||
/// `TC::TlsStream` as the TLS layer.
|
||||
///
|
||||
/// An `ArtiHttpConnection` is constructed by hyper's use of the [`ArtiHttpConnector`]
|
||||
/// implementation of [`hyper::service::Service`],
|
||||
/// and then used by hyper as the transport for hyper's HTTP implementation.
|
||||
#[pin_project]
|
||||
pub struct ArtiHttpConnection<TC: TlsConn> {
|
||||
/// The stream
|
||||
#[pin]
|
||||
inner: MaybeHttpsStream<TC>,
|
||||
}
|
||||
|
||||
/// The actual stream; might be TLS, might not
|
||||
#[pin_project(project = MaybeHttpsStreamProj)]
|
||||
enum MaybeHttpsStream<TC: TlsConn> {
|
||||
/// http
|
||||
Http(Pin<Box<DataStream>>), // Tc:TlsStream is generally boxed; box this one too
|
||||
|
||||
/// https
|
||||
Https(#[pin] TC::TlsStream),
|
||||
}
|
||||
|
||||
impl<TC: TlsConn> Connection for ArtiHttpConnection<TC> {
|
||||
fn connected(&self) -> Connected {
|
||||
Connected::new()
|
||||
}
|
||||
}
|
||||
|
||||
// These trait implementations just defer to the inner `DataStream`; the wrapper type is just
|
||||
// there to implement the `Connection` trait.
|
||||
impl<TC: TlsConn> AsyncRead for ArtiHttpConnection<TC> {
|
||||
fn poll_read(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &mut ReadBuf<'_>,
|
||||
) -> Poll<Result<(), std::io::Error>> {
|
||||
match self.project().inner.project() {
|
||||
MaybeHttpsStreamProj::Http(ds) => ds.as_mut().poll_read(cx, buf),
|
||||
MaybeHttpsStreamProj::Https(t) => t.poll_read(cx, buf),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<TC: TlsConn> AsyncWrite for ArtiHttpConnection<TC> {
|
||||
fn poll_write(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &[u8],
|
||||
) -> Poll<Result<usize, Error>> {
|
||||
match self.project().inner.project() {
|
||||
MaybeHttpsStreamProj::Http(ds) => ds.as_mut().poll_write(cx, buf),
|
||||
MaybeHttpsStreamProj::Https(t) => t.poll_write(cx, buf),
|
||||
}
|
||||
}
|
||||
|
||||
fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Error>> {
|
||||
match self.project().inner.project() {
|
||||
MaybeHttpsStreamProj::Http(ds) => ds.as_mut().poll_flush(cx),
|
||||
MaybeHttpsStreamProj::Https(t) => t.poll_flush(cx),
|
||||
}
|
||||
}
|
||||
|
||||
fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Error>> {
|
||||
match self.project().inner.project() {
|
||||
MaybeHttpsStreamProj::Http(ds) => ds.as_mut().poll_shutdown(cx),
|
||||
MaybeHttpsStreamProj::Https(t) => t.poll_shutdown(cx),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||
/// Are we doing TLS?
|
||||
enum UseTls {
|
||||
/// No
|
||||
Bare,
|
||||
|
||||
/// Yes
|
||||
Tls,
|
||||
}
|
||||
|
||||
/// Convert uri to http\[s\] host and port, and whether to do tls
|
||||
fn uri_to_host_port_tls(uri: Uri) -> Result<(String, u16, UseTls), ConnectionError> {
|
||||
let use_tls = {
|
||||
// Scheme doesn't derive PartialEq so can't be matched on
|
||||
let scheme = uri.scheme();
|
||||
if scheme == Some(&Scheme::HTTP) {
|
||||
UseTls::Bare
|
||||
} else if scheme == Some(&Scheme::HTTPS) {
|
||||
UseTls::Tls
|
||||
} else {
|
||||
return Err(ConnectionError::UnsupportedUriScheme { uri });
|
||||
}
|
||||
};
|
||||
let host = match uri.host() {
|
||||
Some(h) => h,
|
||||
_ => return Err(ConnectionError::MissingHostname { uri }),
|
||||
};
|
||||
let port = uri.port().map(|x| x.as_u16()).unwrap_or(match use_tls {
|
||||
UseTls::Tls => 443,
|
||||
UseTls::Bare => 80,
|
||||
});
|
||||
|
||||
Ok((host.to_owned(), port, use_tls))
|
||||
}
|
||||
|
||||
impl<R: Runtime, TC: TlsConn> Service<Uri> for ArtiHttpConnector<R, TC> {
|
||||
type Response = ArtiHttpConnection<TC>;
|
||||
type Error = ConnectionError;
|
||||
#[allow(clippy::type_complexity)]
|
||||
type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
|
||||
|
||||
fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
|
||||
fn call(&mut self, req: Uri) -> Self::Future {
|
||||
// `TorClient` objects can be cloned cheaply (the cloned objects refer to the same
|
||||
// underlying handles required to make Tor connections internally).
|
||||
// We use this to avoid the returned future having to borrow `self`.
|
||||
let client = self.client.clone();
|
||||
let tls_conn = self.tls_conn.clone();
|
||||
Box::pin(async move {
|
||||
// Extract the host and port to connect to from the URI.
|
||||
let (host, port, use_tls) = uri_to_host_port_tls(req)?;
|
||||
// Initiate a new Tor connection, producing a `DataStream` if successful.
|
||||
let addr = (&host as &str, port)
|
||||
.into_tor_addr()
|
||||
.map_err(arti_client::Error::from)?;
|
||||
let ds = client.connect(addr).await?;
|
||||
|
||||
let inner = match use_tls {
|
||||
UseTls::Tls => {
|
||||
let conn = tls_conn
|
||||
.connect_impl_tls_stream(&host, ds)
|
||||
.await
|
||||
.map_err(|e| ConnectionError::TLS(e.into()))?;
|
||||
MaybeHttpsStream::Https(conn)
|
||||
}
|
||||
UseTls::Bare => MaybeHttpsStream::Http(Box::new(ds).into()),
|
||||
};
|
||||
|
||||
Ok(ArtiHttpConnection { inner })
|
||||
})
|
||||
}
|
||||
}
|
|
@ -20,3 +20,5 @@ pub use tor::Tor;
|
|||
|
||||
mod types;
|
||||
pub use types::*;
|
||||
|
||||
mod http;
|
|
@ -24,7 +24,6 @@ use std::time::Duration;
|
|||
|
||||
use arti_client::config::{CfgPath, TorClientConfigBuilder};
|
||||
use arti_client::{TorClient, TorClientConfig};
|
||||
use arti_hyper::ArtiHttpConnector;
|
||||
use curve25519_dalek::digest::Digest;
|
||||
use ed25519_dalek::hazmat::ExpandedSecretKey;
|
||||
use fs_mistrust::Mistrust;
|
||||
|
@ -32,6 +31,7 @@ use grin_util::secp::SecretKey;
|
|||
use hyper::{Body, Uri};
|
||||
use parking_lot::RwLock;
|
||||
use sha2::Sha512;
|
||||
use tls_api_native_tls::TlsConnector;
|
||||
use tls_api::{TlsConnector as TlsConnectorTrait, TlsConnectorBuilder};
|
||||
use tokio::time::sleep;
|
||||
use tor_hscrypto::pk::{HsIdKey, HsIdKeypair};
|
||||
|
@ -48,15 +48,7 @@ use tor_llcrypto::pk::ed25519::ExpandedKeypair;
|
|||
use tor_rtcompat::tokio::TokioNativeTlsRuntime;
|
||||
use tor_rtcompat::Runtime;
|
||||
|
||||
// On aarch64-apple-darwin targets there is an issue with the native and rustls
|
||||
// tls implementation so this makes it fall back to the openssl variant.
|
||||
//
|
||||
// https://gitlab.torproject.org/tpo/core/arti/-/issues/715
|
||||
#[cfg(not(all(target_vendor = "apple", target_arch = "aarch64")))]
|
||||
use tls_api_native_tls::TlsConnector;
|
||||
#[cfg(all(target_vendor = "apple", target_arch = "aarch64"))]
|
||||
use tls_api_openssl::TlsConnector;
|
||||
|
||||
use crate::tor::http::ArtiHttpConnector;
|
||||
use crate::tor::TorConfig;
|
||||
|
||||
lazy_static! {
|
||||
|
@ -81,20 +73,16 @@ pub struct Tor {
|
|||
|
||||
impl Default for Tor {
|
||||
fn default() -> Self {
|
||||
// Cleanup keys, state and cache on start.
|
||||
fs::remove_dir_all(TorConfig::keystore_path()).unwrap_or_default();
|
||||
fs::remove_dir_all(TorConfig::state_path()).unwrap_or_default();
|
||||
fs::remove_dir_all(TorConfig::cache_path()).unwrap_or_default();
|
||||
// Create Tor client.
|
||||
let runtime = TokioNativeTlsRuntime::create().unwrap();
|
||||
let config = Self::build_config();
|
||||
let client = if let Ok(c) = TorClient::with_runtime(runtime)
|
||||
let client = TorClient::with_runtime(runtime)
|
||||
.config(config.clone())
|
||||
.create_unbootstrapped() {
|
||||
c
|
||||
} else {
|
||||
fs::remove_dir_all(TorConfig::state_path()).unwrap();
|
||||
fs::remove_dir_all(TorConfig::cache_path()).unwrap();
|
||||
let runtime = TokioNativeTlsRuntime::create().unwrap();
|
||||
TorClient::with_runtime(runtime)
|
||||
.config(config.clone())
|
||||
.create_unbootstrapped().unwrap()
|
||||
};
|
||||
.create_unbootstrapped().unwrap();
|
||||
Self {
|
||||
running_services: Arc::new(RwLock::new(BTreeMap::new())),
|
||||
starting_services: Arc::new(RwLock::new(BTreeSet::new())),
|
||||
|
@ -314,8 +302,8 @@ impl Tor {
|
|||
let mut w_services =
|
||||
TOR_SERVER_STATE.starting_services.write();
|
||||
w_services.remove(&service_id);
|
||||
// Check again after 15 seconds.
|
||||
Duration::from_millis(15000)
|
||||
// Check again after 50 seconds.
|
||||
Duration::from_millis(50000)
|
||||
}
|
||||
Err(_) => {
|
||||
// Restart service on 3rd error.
|
||||
|
@ -409,10 +397,10 @@ impl Tor {
|
|||
hs_nickname: &HsNickname,
|
||||
) -> tor_keymgr::Result<()> {
|
||||
let arti_store =
|
||||
ArtiNativeKeystore::from_path_and_mistrust(TorConfig::keystore_path(), &mistrust)?;
|
||||
ArtiNativeKeystore::from_path_and_mistrust(TorConfig::keystore_path(), mistrust)?;
|
||||
|
||||
let key_manager = KeyMgrBuilder::default()
|
||||
.default_store(Box::new(arti_store))
|
||||
.primary_store(Box::new(arti_store))
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
|
@ -427,13 +415,15 @@ impl Tor {
|
|||
key_manager.insert(
|
||||
HsIdKey::from(expanded_kp.public().clone()),
|
||||
&HsIdPublicKeySpecifier::new(hs_nickname.clone()),
|
||||
KeystoreSelector::Default,
|
||||
KeystoreSelector::Primary,
|
||||
true
|
||||
)?;
|
||||
|
||||
key_manager.insert(
|
||||
HsIdKeypair::from(expanded_kp),
|
||||
&HsIdKeypairSpecifier::new(hs_nickname.clone()),
|
||||
KeystoreSelector::Default,
|
||||
KeystoreSelector::Primary,
|
||||
true
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
@ -444,7 +434,7 @@ impl Tor {
|
|||
builder.bridges().bridges().push(bridge);
|
||||
}
|
||||
|
||||
// Now configure an snowflake transport. (Requires the "pt-client" feature)
|
||||
// Now configure a snowflake transport. (Requires the "pt-client" feature)
|
||||
let mut transport = TransportConfigBuilder::default();
|
||||
transport
|
||||
.protocols(vec!["snowflake".parse().unwrap()])
|
||||
|
|
|
@ -80,7 +80,7 @@ impl WalletConfig {
|
|||
},
|
||||
min_confirmations: MIN_CONFIRMATIONS_DEFAULT,
|
||||
use_dandelion: Some(true),
|
||||
enable_tor_listener: Some(true),
|
||||
enable_tor_listener: Some(false),
|
||||
api_port: Some(rand::thread_rng().gen_range(10000..30000)),
|
||||
};
|
||||
Settings::write_to_file(&config, config_path);
|
||||
|
|
|
@ -195,6 +195,7 @@ impl Mnemonic {
|
|||
// Setup phrase size.
|
||||
let confirm = self.mode == PhraseMode::Generate;
|
||||
if !confirm {
|
||||
self.words = Self::empty_words(&size);
|
||||
self.size = size;
|
||||
} else if self.size != size {
|
||||
return;
|
||||
|
|
|
@ -171,7 +171,7 @@ impl Wallet {
|
|||
// Setup node client.
|
||||
let integrated = || {
|
||||
let api_url = format!("http://{}", NodeConfig::get_api_address());
|
||||
let api_secret = NodeConfig::get_foreign_api_secret();
|
||||
let api_secret = NodeConfig::get_api_secret(true);
|
||||
(api_url, api_secret)
|
||||
};
|
||||
let (node_api_url, node_secret) = if let Some(id) = config.ext_conn_id {
|
||||
|
@ -617,7 +617,7 @@ impl Wallet {
|
|||
let r_inst = self.instance.as_ref().read();
|
||||
let instance = r_inst.clone().unwrap();
|
||||
let mut api = Owner::new(instance, None);
|
||||
return match parse_slatepack(&mut api, None, None, Some(text.clone())) {
|
||||
match parse_slatepack(&mut api, None, None, Some(text.clone())) {
|
||||
Ok(s) => Ok(s.0),
|
||||
Err(e) => Err(e)
|
||||
}
|
||||
|
@ -714,7 +714,7 @@ impl Wallet {
|
|||
amount,
|
||||
minimum_confirmations: config.min_confirmations,
|
||||
num_change_outputs: 1,
|
||||
selection_strategy_is_use_all: true,
|
||||
selection_strategy_is_use_all: false,
|
||||
..Default::default()
|
||||
};
|
||||
let r_inst = self.instance.as_ref().read();
|
||||
|
@ -857,7 +857,7 @@ impl Wallet {
|
|||
src_acct_name: None,
|
||||
amount: slate.amount,
|
||||
minimum_confirmations: config.min_confirmations,
|
||||
selection_strategy_is_use_all: true,
|
||||
selection_strategy_is_use_all: false,
|
||||
..Default::default()
|
||||
};
|
||||
let r_inst = self.instance.as_ref().read();
|
||||
|
|
Loading…
Reference in a new issue