mirror of
https://code.gri.mw/GUI/grim.git
synced 2026-07-05 14:37:28 +00:00
Compare commits
42 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0026fc3717 | |||
| 0fd04f14a4 | |||
| 3338f51de5 | |||
| 0fa8963bd2 | |||
| 70bba5d7ce | |||
| 0bb43e1e5d | |||
| fd52757549 | |||
| 6835bb1909 | |||
| 31bc74529c | |||
| 8d6943975b | |||
| 4c5d8abe7b | |||
| 4dc42bce4a | |||
| e2d5d92f18 | |||
| b001eb4712 | |||
| f14bd902ea | |||
| 33ab11933a | |||
| 6b05a2177e | |||
| 7bbe637414 | |||
| 9b6252de3a | |||
| 26debcf51c | |||
| 497b967fd0 | |||
| 05e18cf6c4 | |||
| 6e50b2b38a | |||
| 9bc96de398 | |||
| 5a525c50e1 | |||
| ba0af0968d | |||
| a0947aa47c | |||
| 06c6b8b4f5 | |||
| b19335d0bc | |||
| 40eb30fb75 | |||
| 8223e52570 | |||
| 875bd11bdb | |||
| 19e4cb664d | |||
| 18bc327a99 | |||
| 88e2fb0715 | |||
| feb38dc7cf | |||
| 28ecb5b1f4 | |||
| 024a9d0098 | |||
| 59cf46e1cb | |||
| 22255e0f2a | |||
| 7fdb8d272b | |||
| d043562058 |
@@ -13,6 +13,7 @@ jobs:
|
||||
runs-on: ubuntu
|
||||
outputs:
|
||||
v: ${{ steps.version.outputs.v }}
|
||||
pre: ${{ steps.version.outputs.pre }}
|
||||
exists: ${{ steps.check.outputs.exists }}
|
||||
last_tag: ${{ steps.check_prev.outputs.last_tag }}
|
||||
steps:
|
||||
@@ -27,7 +28,7 @@ jobs:
|
||||
[[ ${{ forgejo.ref_type }} == 'tag' ]] && app_ver=${{ forgejo.ref_name }} || app_ver=v${ver}
|
||||
echo "v=${app_ver}" >> "$FORGEJO_OUTPUT"
|
||||
echo $app_ver
|
||||
[[ ${{ forgejo.ref_type }} == 'tag' ]] && pre=false || pre=true
|
||||
[[ ${{ forgejo.ref_type }} == 'tag' ]] && pre='false' || pre='true'
|
||||
echo "pre=${pre}" >> "$FORGEJO_OUTPUT"
|
||||
echo "pre-release: ${pre}"
|
||||
- name: Check existing release
|
||||
@@ -40,7 +41,7 @@ jobs:
|
||||
echo "exists=${exists}" >> "$FORGEJO_OUTPUT"
|
||||
echo ${exists}
|
||||
mkdir release
|
||||
- uses: ardocrat/forgejo-release@grim
|
||||
- uses: actions/forgejo-release@v2.11.3
|
||||
if: ${{ forgejo.ref_type == 'tag' && steps.check.outputs.exists == 'true' }}
|
||||
with:
|
||||
direction: download
|
||||
@@ -51,7 +52,7 @@ jobs:
|
||||
if: ${{ forgejo.ref_type == 'tag' && steps.check.outputs.exists == 'true' }}
|
||||
working-directory: release
|
||||
run: for f in *; do mv "$f" "$(echo "$f" | sed s/-dev-/-/)"; done
|
||||
- uses: ardocrat/forgejo-release@grim
|
||||
- uses: actions/forgejo-release@v2.11.3
|
||||
if: ${{ forgejo.ref_type == 'tag' && steps.check.outputs.exists == 'true' }}
|
||||
with:
|
||||
direction: upload
|
||||
@@ -331,11 +332,11 @@ jobs:
|
||||
- name: Archive Universal
|
||||
working-directory: macos
|
||||
run: |
|
||||
zip -r grim-${{ needs.version.outputs.v }}-macos.zip Grim.app
|
||||
mv grim-${{ needs.version.outputs.v }}-macos.zip ../release
|
||||
zip -r grim-${{ needs.version.outputs.v }}-macos-universal.zip Grim.app
|
||||
mv grim-${{ needs.version.outputs.v }}-macos-universal.zip ../release
|
||||
- name: Checksum Release Universal
|
||||
working-directory: release
|
||||
run: sha256sum grim-${{ needs.version.outputs.v }}-macos.zip > grim-${{ needs.version.outputs.v }}-macos-sha256sum.txt
|
||||
run: sha256sum grim-${{ needs.version.outputs.v }}-macos-universal.zip > grim-${{ needs.version.outputs.v }}-macos-universal-sha256sum.txt
|
||||
- name: Upload artifacts
|
||||
run: |
|
||||
tar -czf macos.tar.gz release
|
||||
@@ -369,7 +370,7 @@ jobs:
|
||||
|
||||
release:
|
||||
if: ${{ forgejo.ref_type == 'branch' || needs.version.outputs.exists == 'false' }}
|
||||
runs-on: debian-release
|
||||
runs-on: ubuntu
|
||||
needs: [version, android_release, linux, linux_x86, macos, windows]
|
||||
steps:
|
||||
- name: Download All Artifacts
|
||||
@@ -390,13 +391,13 @@ jobs:
|
||||
tar -xzf windows.tar.gz
|
||||
rm windows.tar.gz
|
||||
- name: Upload release to Forgejo
|
||||
uses: ardocrat/forgejo-release@grim
|
||||
uses: actions/forgejo-release@v2.11.3
|
||||
with:
|
||||
direction: upload
|
||||
token: ${{ secrets.RELEASE_TOKEN }}
|
||||
tag: ${{ needs.version.outputs.v }}
|
||||
override: true
|
||||
prerelease: ${{ needs.version.outputs.pre }}
|
||||
prerelease: ${{ needs.version.outputs.pre == 'true' }}
|
||||
release-dir: release
|
||||
release-notes: "Full Changelog: [${{ needs.version.outputs.last_tag }}...${{ needs.version.outputs.v }}](https://code.gri.mw/${{ forgejo.repository }}/compare/${{ needs.version.outputs.last_tag }}...${{ needs.version.outputs.v }})"
|
||||
- name: Telegram Notify Channel
|
||||
@@ -417,6 +418,22 @@ jobs:
|
||||
chat_id: ${{ secrets.TELEGRAM_GROUP_ID }}
|
||||
status: ${{ job.status }}
|
||||
notify_fields: "actor,repository,workflow,branch,commit"
|
||||
|
||||
release-telegram:
|
||||
runs-on: debian-release
|
||||
needs: [version, release]
|
||||
steps:
|
||||
- name: Download All Artifacts
|
||||
run: |
|
||||
mkdir release
|
||||
cd release
|
||||
wget ${{ secrets.REPO_HOST }}/${{ forgejo.repository }}/releases/download/${{ needs.version.outputs.v }}/grim-${{ needs.version.outputs.v }}-android.apk
|
||||
wget ${{ secrets.REPO_HOST }}/${{ forgejo.repository }}/releases/download/${{ needs.version.outputs.v }}/grim-${{ needs.version.outputs.v }}-android-x86_64.apk
|
||||
wget ${{ secrets.REPO_HOST }}/${{ forgejo.repository }}/releases/download/${{ needs.version.outputs.v }}/grim-${{ needs.version.outputs.v }}-linux-arm.AppImage
|
||||
wget ${{ secrets.REPO_HOST }}/${{ forgejo.repository }}/releases/download/${{ needs.version.outputs.v }}/grim-${{ needs.version.outputs.v }}-linux-x86_64.AppImage
|
||||
wget ${{ secrets.REPO_HOST }}/${{ forgejo.repository }}/releases/download/${{ needs.version.outputs.v }}/grim-${{ needs.version.outputs.v }}-macos-universal.zip
|
||||
wget ${{ secrets.REPO_HOST }}/${{ forgejo.repository }}/releases/download/${{ needs.version.outputs.v }}/grim-${{ needs.version.outputs.v }}-win-x86_64.msi
|
||||
wget ${{ secrets.REPO_HOST }}/${{ forgejo.repository }}/releases/download/${{ needs.version.outputs.v }}/grim-${{ needs.version.outputs.v }}-win-x86_64.zip
|
||||
- name: Upload files to Telegram
|
||||
uses: actions/telegram-send-file@main
|
||||
with:
|
||||
@@ -432,6 +449,6 @@ jobs:
|
||||
release/grim-${{ needs.version.outputs.v }}-android-x86_64.apk
|
||||
release/grim-${{ needs.version.outputs.v }}-linux-arm.AppImage
|
||||
release/grim-${{ needs.version.outputs.v }}-linux-x86_64.AppImage
|
||||
release/grim-${{ needs.version.outputs.v }}-macos.zip
|
||||
release/grim-${{ needs.version.outputs.v }}-macos-universal.zip
|
||||
release/grim-${{ needs.version.outputs.v }}-win-x86_64.msi
|
||||
release/grim-${{ needs.version.outputs.v }}-win-x86_64.zip
|
||||
@@ -5,7 +5,7 @@ REPO_NAME=$1
|
||||
TAG=$2
|
||||
DOWNLOAD_URL=${HOST}/${REPO_NAME}/releases/download/${TAG}
|
||||
|
||||
FILES=( "grim-${TAG}-android.apk" "grim-${TAG}-android-x86_64.apk" "grim-${TAG}-linux-arm.AppImage" "grim-${TAG}-linux-x86_64.AppImage" "grim-${TAG}-macos.zip" "grim-${TAG}-win-x86_64.msi" "grim-${TAG}-win-x86_64.zip" )
|
||||
FILES=( "grim-${TAG}-android.apk" "grim-${TAG}-android-x86_64.apk" "grim-${TAG}-linux-arm.AppImage" "grim-${TAG}-linux-x86_64.AppImage" "grim-${TAG}-macos-universal.zip" "grim-${TAG}-win-x86_64.msi" "grim-${TAG}-win-x86_64.zip" )
|
||||
|
||||
# Download release files
|
||||
for f in "${FILES[@]}"; do
|
||||
|
||||
@@ -26,6 +26,6 @@ jobs:
|
||||
grim-${{ github.ref_name }}-android-x86_64.apk
|
||||
grim-${{ github.ref_name }}-linux-arm.AppImage
|
||||
grim-${{ github.ref_name }}-linux-x86_64.AppImage
|
||||
grim-${{ github.ref_name }}-macos.zip
|
||||
grim-${{ github.ref_name }}-macos-universal.zip
|
||||
grim-${{ github.ref_name }}-win-x86_64.msi
|
||||
grim-${{ github.ref_name }}-win-x86_64.zip
|
||||
Generated
+286
-337
File diff suppressed because it is too large
Load Diff
+18
-14
@@ -1,12 +1,12 @@
|
||||
[package]
|
||||
name = "grim"
|
||||
version = "0.3.0"
|
||||
version = "0.3.4"
|
||||
authors = ["Ardocrat <ardocrat@gri.mw>"]
|
||||
description = "Cross-platform GUI for Grin with focus on usability and availability to be used by anyone, anywhere."
|
||||
license = "Apache-2.0"
|
||||
repository = "https://code.gri.mw/GUI/grim"
|
||||
keywords = [ "crypto", "grin", "mimblewimble" ]
|
||||
edition = "2021"
|
||||
edition = "2024"
|
||||
build = "build.rs"
|
||||
|
||||
[[bin]]
|
||||
@@ -52,6 +52,7 @@ egui-async = "0.3.4"
|
||||
rust-i18n = "3.1.5"
|
||||
|
||||
## other
|
||||
log4rs = "1.4.0"
|
||||
anyhow = "1.0.97"
|
||||
pin-project = "1.1.10"
|
||||
backtrace = "0.3.76"
|
||||
@@ -91,23 +92,23 @@ uuid = { version = "0.8.2", features = ["v4"] }
|
||||
num-bigint = "0.4.6"
|
||||
|
||||
## tor
|
||||
arti-client = { version = "0.38.0", features = ["pt-client", "static", "onion-service-service", "onion-service-client"] }
|
||||
tor-rtcompat = { version = "0.38.0", features = ["static"] }
|
||||
tor-config = "0.38.0"
|
||||
fs-mistrust = "0.13.1"
|
||||
tor-hsservice = "0.38.0"
|
||||
tor-hsrproxy = "0.38.0"
|
||||
tor-keymgr = "0.38.0"
|
||||
tor-llcrypto = "0.38.0"
|
||||
tor-hscrypto = "0.38.0"
|
||||
tor-error = "0.38.0"
|
||||
arti-client = { version = "0.41.0", features = ["pt-client", "static", "onion-service-service", "onion-service-client", "experimental-api", "bridge-client"] }
|
||||
tor-rtcompat = { version = "0.41.0", features = ["static"] }
|
||||
tor-config = "0.41.0"
|
||||
fs-mistrust = "0.14.1"
|
||||
tor-hsservice = "0.41.0"
|
||||
tor-hsrproxy = "0.41.0"
|
||||
tor-keymgr = "0.41.0"
|
||||
tor-llcrypto = "0.41.0"
|
||||
tor-hscrypto = "0.41.0"
|
||||
tor-error = "0.41.0"
|
||||
sha2 = "0.10.8"
|
||||
ed25519-dalek = "2.1.1"
|
||||
curve25519-dalek = "4.1.3"
|
||||
hyper-tor = { version = "0.14.32", features = ["full"], package = "hyper" }
|
||||
tls-api = "0.12.0"
|
||||
tls-api-native-tls = "0.12.1"
|
||||
safelog = "0.7.0"
|
||||
safelog = "0.8.1"
|
||||
|
||||
## stratum server
|
||||
tokio-old = { version = "0.2", features = ["full"], package = "tokio" }
|
||||
@@ -136,4 +137,7 @@ android_logger = "0.15.0"
|
||||
jni = "0.21.1"
|
||||
android-activity = { version = "0.6.0", features = ["game-activity"] }
|
||||
winit = { version = "0.30.12", features = ["android-game-activity"] }
|
||||
eframe = { version = "0.33.2", default-features = false, features = ["glow", "android-game-activity"] }
|
||||
eframe = { version = "0.33.2", default-features = false, features = ["glow", "android-game-activity"] }
|
||||
|
||||
[build-dependencies]
|
||||
built = "0.8.0"
|
||||
|
||||
@@ -12,7 +12,7 @@ android {
|
||||
minSdk 24
|
||||
targetSdk 36
|
||||
versionCode 5
|
||||
versionName "0.3.0"
|
||||
versionName "0.3.4"
|
||||
}
|
||||
|
||||
lint {
|
||||
|
||||
@@ -331,6 +331,17 @@ public class MainActivity extends GameActivity {
|
||||
onTextInput("9");
|
||||
return false;
|
||||
}
|
||||
} else if (event.getAction() == KeyEvent.ACTION_MULTIPLE && event.getKeyCode() == KeyEvent.KEYCODE_UNKNOWN) {
|
||||
if (!event.getCharacters().isEmpty()) {
|
||||
onTextInput(event.getCharacters());
|
||||
return false;
|
||||
}
|
||||
// Pass any other input values into native code.
|
||||
} else if (event.getAction() == KeyEvent.ACTION_UP &&
|
||||
event.getKeyCode() != KeyEvent.KEYCODE_ENTER &&
|
||||
event.getKeyCode() != KeyEvent.KEYCODE_BACK) {
|
||||
onTextInput(String.valueOf((char)event.getUnicodeChar()));
|
||||
return false;
|
||||
}
|
||||
return super.dispatchKeyEvent(event);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ use std::process::Command;
|
||||
use std::{env, fs};
|
||||
|
||||
fn main() {
|
||||
built::write_built_file().expect("Failed to acquire build-time information");
|
||||
|
||||
let out_dir = env::var("OUT_DIR").unwrap();
|
||||
let tor_out_dir = format!("{}/tor", out_dir);
|
||||
let mut webtunnel_file = format!("{}/webtunnel", tor_out_dir);
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 181 KiB After Width: | Height: | Size: 191 KiB |
@@ -142,6 +142,7 @@ wallets:
|
||||
payment_proof_desc: 'Geben Sie den erhaltenen Zahlungsnachweis ein, um die Transaktion zu verifizieren:'
|
||||
payment_proof_valid: 'Der eingegebene Zahlungsnachweis ist gültig:'
|
||||
payment_proof_error: 'Der eingetragene Zahlungsnachweis ist nicht gültig:'
|
||||
tx_delete_confirmation: Bist du sicher, dass du die Transaktion aus dem Verlauf löschen möchtest?
|
||||
transport:
|
||||
desc: 'Transport verwenden, um Nachrichten synchron zu empfangen oder zu senden:'
|
||||
tor_network: Tor Netzwek
|
||||
|
||||
@@ -142,6 +142,7 @@ wallets:
|
||||
payment_proof_desc: 'Enter received payment proof to verify transaction:'
|
||||
payment_proof_valid: 'Entered payment proof is valid:'
|
||||
payment_proof_error: 'Entered payment proof is not valid:'
|
||||
tx_delete_confirmation: Are you sure you want to delete the transaction from history?
|
||||
transport:
|
||||
desc: 'Use transport to receive or send messages synchronously:'
|
||||
tor_network: Tor network
|
||||
|
||||
@@ -142,6 +142,7 @@ wallets:
|
||||
payment_proof_desc: 'Saisissez la preuve de paiement reçue pour vérifier la transaction:'
|
||||
payment_proof_valid: 'La preuve de paiement saisie est valide:'
|
||||
payment_proof_error: "La preuve de paiement saisie n'est pas valide:"
|
||||
tx_delete_confirmation: Êtes-vous sûr de vouloir supprimer la transaction de l'historique?
|
||||
transport:
|
||||
desc: 'Utilisez le transport pour recevoir ou envoyer des messages de manière synchronisée:'
|
||||
tor_network: Réseau Tor
|
||||
|
||||
@@ -142,6 +142,7 @@ wallets:
|
||||
payment_proof_desc: 'Введите полученное подтверждение оплаты для проверки транзакции:'
|
||||
payment_proof_valid: 'Введённое подтверждение оплаты действительно:'
|
||||
payment_proof_error: 'Введённое подтверждение оплаты недействительно:'
|
||||
tx_delete_confirmation: Вы уверены, что хотите удалить транзакцию из истории?
|
||||
transport:
|
||||
desc: 'Используйте транспорт для синхронных получения или отправки сообщений:'
|
||||
tor_network: Сеть Tor
|
||||
|
||||
@@ -142,6 +142,7 @@ wallets:
|
||||
payment_proof_desc: 'Islemi doğrulamak için alinan ödeme kanitini girin:'
|
||||
payment_proof_valid: 'Girilen ödeme kaniti geçerlidir:'
|
||||
payment_proof_error: 'Girilen ödeme kaniti geçerli değildir:'
|
||||
tx_delete_confirmation: Islemi geçmişten silmek istediğinizden emin misiniz?
|
||||
transport:
|
||||
desc: 'Adresten senkronize GONDER veya AL:'
|
||||
tor_network: Tor network
|
||||
|
||||
@@ -142,6 +142,7 @@ wallets:
|
||||
payment_proof_desc: '輸入已收款證明以驗證交易:'
|
||||
payment_proof_valid: '輸入的付款證明有效:'
|
||||
payment_proof_error: '輸入的付款證明無效:'
|
||||
tx_delete_confirmation: 你確定要從歷史紀錄中刪除這筆交易嗎?
|
||||
transport:
|
||||
desc: '使用传输同步接收或发送消息:'
|
||||
tor_network: Tor 网络
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>0.2.3</string>
|
||||
<string>0.3.4</string>
|
||||
<key>CFBundleSupportedPlatforms</key>
|
||||
<array>
|
||||
<string>MacOSX</string>
|
||||
|
||||
+1
-1
Submodule node updated: 376c85bab5...42b928a42f
+5
-2
@@ -70,6 +70,9 @@ else
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Update MacOS version.
|
||||
sed -i '' -e 's/'"$GIT_TAG_LATEST"'/'"$VERSION_NEXT"'/' macos/Grim.app/Contents/Info.plist
|
||||
|
||||
# Update version for Windows installer.
|
||||
sed -i '' -e 's/" Version="[^\"]*"/" Version="'"$VERSION_NEXT"'"/g' wix/main.wxs
|
||||
sed -i '' -e 's/<Package Id="[^\"]*"/<Package Id="'"$(uuidgen)"'"/g' wix/main.wxs
|
||||
@@ -88,12 +91,12 @@ cargo update -p grim
|
||||
|
||||
# Commit the changes
|
||||
git add .
|
||||
git commit -m "release: v$VERSION_NEXT"
|
||||
git commit -m "build: version $VERSION_NEXT"
|
||||
|
||||
# ==================================
|
||||
# Create git tag for new version
|
||||
# ==================================
|
||||
|
||||
# Create a tag and push to master branch
|
||||
git tag "v$VERSION_NEXT" master
|
||||
#git tag "v$VERSION_NEXT" master
|
||||
#git push origin master --follow-tags
|
||||
|
||||
+1
-1
@@ -418,7 +418,7 @@ impl<Platform: PlatformCallbacks> eframe::App for App<Platform> {
|
||||
#[allow(dead_code)]
|
||||
#[cfg(target_os = "android")]
|
||||
#[allow(non_snake_case)]
|
||||
#[no_mangle]
|
||||
#[unsafe(no_mangle)]
|
||||
/// Handle Back key code event from Android.
|
||||
pub extern "C" fn Java_mw_gri_android_MainActivity_onBack(
|
||||
_env: jni::JNIEnv,
|
||||
|
||||
+2
-2
@@ -44,7 +44,6 @@ const BLUE_DARK: Color32 =
|
||||
const FILL: Color32 = Color32::from_gray(244);
|
||||
const FILL_DARK: Color32 = Color32::from_gray(26);
|
||||
|
||||
const FILL_DEEP: Color32 = Color32::from_gray(238);
|
||||
const FILL_DEEP_DARK: Color32 = Color32::from_gray(32);
|
||||
|
||||
const FILL_LITE: Color32 = Color32::from_gray(249);
|
||||
@@ -85,6 +84,7 @@ fn use_dark() -> bool {
|
||||
}
|
||||
|
||||
impl Colors {
|
||||
pub const FILL_DEEP: Color32 = Color32::from_gray(238);
|
||||
pub const TRANSPARENT: Color32 = Color32::from_rgba_premultiplied(0, 0, 0, 0);
|
||||
pub const STROKE: Color32 = Color32::from_gray(200);
|
||||
|
||||
@@ -172,7 +172,7 @@ impl Colors {
|
||||
if use_dark() {
|
||||
FILL_DEEP_DARK
|
||||
} else {
|
||||
FILL_DEEP
|
||||
Self::FILL_DEEP
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -203,7 +203,7 @@ lazy_static! {
|
||||
|
||||
/// Callback from Java code with last entered character from soft keyboard.
|
||||
#[allow(non_snake_case)]
|
||||
#[no_mangle]
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn Java_mw_gri_android_MainActivity_onCameraImage(
|
||||
env: JNIEnv,
|
||||
_class: JObject,
|
||||
@@ -218,7 +218,7 @@ pub extern "C" fn Java_mw_gri_android_MainActivity_onCameraImage(
|
||||
|
||||
/// Callback from Java code with picked file path.
|
||||
#[allow(non_snake_case)]
|
||||
#[no_mangle]
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn Java_mw_gri_android_MainActivity_onFilePick(
|
||||
_env: JNIEnv,
|
||||
_class: JObject,
|
||||
|
||||
+24
-18
@@ -12,21 +12,21 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::sync::Arc;
|
||||
use parking_lot::RwLock;
|
||||
use std::thread;
|
||||
use egui::load::SizedTexture;
|
||||
use egui::{Pos2, Rect, RichText, TextureOptions, UiBuilder, Widget};
|
||||
use image::{DynamicImage, EncodableLayout};
|
||||
use grin_keychain::mnemonic::WORDS;
|
||||
use grin_util::ZeroingString;
|
||||
use grin_wallet_libwallet::SlatepackAddress;
|
||||
use grin_keychain::mnemonic::WORDS;
|
||||
use image::{DynamicImage, EncodableLayout};
|
||||
use parking_lot::RwLock;
|
||||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
|
||||
use crate::gui::Colors;
|
||||
use crate::gui::icons::CAMERA_ROTATE;
|
||||
use crate::gui::platform::PlatformCallbacks;
|
||||
use crate::gui::views::types::{QrScanResult, QrScanState};
|
||||
use crate::gui::views::View;
|
||||
use crate::gui::Colors;
|
||||
use crate::wallet::types::PhraseSize;
|
||||
use crate::wallet::WalletUtils;
|
||||
|
||||
@@ -51,16 +51,13 @@ impl CameraContent {
|
||||
/// Draw camera content.
|
||||
pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||
let rect = if let Some(img_data) = cb.camera_image() {
|
||||
if let Ok(img) =
|
||||
image::load_from_memory(&*img_data.0) {
|
||||
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)
|
||||
@@ -69,6 +66,9 @@ impl CameraContent {
|
||||
self.loading_ui(ui)
|
||||
};
|
||||
|
||||
// Show UR scan progress.
|
||||
self.ur_progress_ui(ui, &rect);
|
||||
|
||||
// Show button to switch cameras.
|
||||
if cb.can_switch_camera() {
|
||||
let r = {
|
||||
@@ -84,6 +84,7 @@ impl CameraContent {
|
||||
});
|
||||
});
|
||||
}
|
||||
ui.add_space(6.0);
|
||||
ui.ctx().request_repaint();
|
||||
}
|
||||
|
||||
@@ -125,7 +126,11 @@ impl CameraContent {
|
||||
egui::Image::from_texture(sized_img)
|
||||
// Setup to crop image at square.
|
||||
.uv(Rect::from([
|
||||
Pos2::new(1.0 - (img_size.y / img_size.x), 0.0),
|
||||
if img_size.y > img_size.x {
|
||||
Pos2::new(0.0, 1.0 - (img_size.x / img_size.y))
|
||||
} else {
|
||||
Pos2::new(1.0 - (img_size.y / img_size.x), 0.0)
|
||||
},
|
||||
Pos2::new(1.0, 1.0)
|
||||
]))
|
||||
.max_height(ui.available_width())
|
||||
@@ -135,15 +140,17 @@ impl CameraContent {
|
||||
}
|
||||
|
||||
/// Draw animated QR code scanning progress.
|
||||
fn ur_progress_ui(&self, ui: &mut egui::Ui) {
|
||||
fn ur_progress_ui(&self, ui: &mut egui::Ui, rect: &Rect) {
|
||||
let show_ur_progress = {
|
||||
self.ur_data.as_ref().read().is_some()
|
||||
};
|
||||
if show_ur_progress {
|
||||
ui.centered_and_justified(|ui| {
|
||||
ui.label(RichText::new(format!("{}%", self.ur_progress()))
|
||||
.size(17.0)
|
||||
.color(Colors::green()));
|
||||
ui.scope_builder(UiBuilder::new().max_rect(rect.clone()), |ui| {
|
||||
ui.centered_and_justified(|ui| {
|
||||
ui.label(RichText::new(format!("{}%", self.ur_progress()))
|
||||
.size(32.0)
|
||||
.color(Colors::gold_dark()));
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -201,8 +208,7 @@ impl CameraContent {
|
||||
let on_scan = async move {
|
||||
// Prepare image data.
|
||||
let img = image_data.to_luma8();
|
||||
let mut img: rqrr::PreparedImage<image::GrayImage>
|
||||
= rqrr::PreparedImage::prepare(img);
|
||||
let mut img: rqrr::PreparedImage<image::GrayImage> = rqrr::PreparedImage::prepare(img);
|
||||
// Scan and save results.
|
||||
let grids = img.detect_grids();
|
||||
if let Some(g) = grids.get(0) {
|
||||
|
||||
@@ -117,7 +117,7 @@ impl ContentContainer for Content {
|
||||
|
||||
if self.first_draw {
|
||||
// Show crash report or integrated node Android warning.
|
||||
if Settings::crash_report_path().exists() {
|
||||
if Settings::crash_check_path().exists() {
|
||||
Modal::new(CRASH_REPORT_MODAL)
|
||||
.closeable(false)
|
||||
.position(ModalPosition::Center)
|
||||
@@ -276,14 +276,14 @@ impl Content {
|
||||
.size(16.0)
|
||||
.color(Colors::text(false)));
|
||||
ui.add_space(6.0);
|
||||
// Draw button to share crash report.
|
||||
// Draw button to share log file.
|
||||
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()) {
|
||||
let name = Settings::CRASH_REPORT_FILE_NAME.to_string();
|
||||
if let Ok(data) = fs::read_to_string(Settings::log_path()) {
|
||||
let name = Settings::LOG_FILE_NAME.to_string();
|
||||
let _ = cb.share_data(name, data.as_bytes().to_vec());
|
||||
}
|
||||
Settings::delete_crash_report();
|
||||
Settings::delete_crash_check();
|
||||
Modal::close();
|
||||
});
|
||||
});
|
||||
@@ -292,7 +292,7 @@ impl Content {
|
||||
ui.add_space(8.0);
|
||||
ui.vertical_centered_justified(|ui| {
|
||||
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
|
||||
Settings::delete_crash_report();
|
||||
Settings::delete_crash_check();
|
||||
Modal::close();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -404,7 +404,7 @@ lazy_static! {
|
||||
#[allow(dead_code)]
|
||||
#[cfg(target_os = "android")]
|
||||
#[allow(non_snake_case)]
|
||||
#[no_mangle]
|
||||
#[unsafe(no_mangle)]
|
||||
/// Callback from Java code with last entered character from soft keyboard.
|
||||
pub extern "C" fn Java_mw_gri_android_MainActivity_onTextInput(
|
||||
_env: jni::JNIEnv,
|
||||
@@ -429,7 +429,7 @@ pub extern "C" fn Java_mw_gri_android_MainActivity_onTextInput(
|
||||
#[allow(dead_code)]
|
||||
#[cfg(target_os = "android")]
|
||||
#[allow(non_snake_case)]
|
||||
#[no_mangle]
|
||||
#[unsafe(no_mangle)]
|
||||
/// Callback from Java code when Clear key was pressed at soft keyboard.
|
||||
pub extern "C" fn Java_mw_gri_android_MainActivity_onClearInput(
|
||||
_env: jni::JNIEnv,
|
||||
@@ -442,7 +442,7 @@ pub extern "C" fn Java_mw_gri_android_MainActivity_onClearInput(
|
||||
#[allow(dead_code)]
|
||||
#[cfg(target_os = "android")]
|
||||
#[allow(non_snake_case)]
|
||||
#[no_mangle]
|
||||
#[unsafe(no_mangle)]
|
||||
/// Callback from Java code when Enter key was pressed at soft keyboard.
|
||||
pub extern "C" fn Java_mw_gri_android_MainActivity_onEnterInput(
|
||||
_env: jni::JNIEnv,
|
||||
|
||||
+11
-2
@@ -14,7 +14,7 @@
|
||||
|
||||
use egui::epaint::{RectShape, Shadow};
|
||||
use egui::os::OperatingSystem;
|
||||
use egui::{Align2, CornerRadius, RichText, Stroke, StrokeKind, UiBuilder, Vec2};
|
||||
use egui::{Align2, Color32, CornerRadius, RichText, Stroke, StrokeKind, UiBuilder, Vec2};
|
||||
use lazy_static::lazy_static;
|
||||
use parking_lot::RwLock;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
@@ -43,6 +43,8 @@ pub struct Modal {
|
||||
title: Option<String>,
|
||||
/// Flag to check first content render.
|
||||
first_draw: Arc<AtomicBool>,
|
||||
/// Background color.
|
||||
fill: Option<Color32>,
|
||||
}
|
||||
|
||||
impl Modal {
|
||||
@@ -61,6 +63,7 @@ impl Modal {
|
||||
closeable: Arc::new(AtomicBool::new(true)),
|
||||
title: None,
|
||||
first_draw: Arc::new(AtomicBool::new(true)),
|
||||
fill: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -295,6 +298,12 @@ impl Modal {
|
||||
(align, offset)
|
||||
}
|
||||
|
||||
/// Set custom background color.
|
||||
pub fn set_background_color(&self, color: Color32) {
|
||||
let mut w_state = MODAL_STATE.write();
|
||||
w_state.modal.as_mut().unwrap().fill = Some(color);
|
||||
}
|
||||
|
||||
/// Draw provided content.
|
||||
fn content_ui(&self,
|
||||
ui: &mut egui::Ui,
|
||||
@@ -312,7 +321,7 @@ impl Modal {
|
||||
sw: 8.0 as u8,
|
||||
se: 8.0 as u8,
|
||||
}
|
||||
}, Colors::fill(), Stroke::NONE, StrokeKind::Outside);
|
||||
}, self.fill.unwrap_or(Colors::fill_lite()), Stroke::NONE, StrokeKind::Outside);
|
||||
let bg_idx = ui.painter().add(bg_shape.clone());
|
||||
|
||||
rect.min += egui::emath::vec2(6.0, 0.0);
|
||||
|
||||
@@ -12,40 +12,51 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use egui::{Align, Layout, RichText, CornerRadius, StrokeKind};
|
||||
use eframe::epaint::RectShape;
|
||||
use egui::{Align, Color32, CornerRadius, CursorIcon, Layout, RichText, Sense, StrokeKind, UiBuilder};
|
||||
|
||||
use crate::AppConfig;
|
||||
use crate::gui::Colors;
|
||||
use crate::gui::icons::{CARET_RIGHT, CHECK_CIRCLE, COMPUTER_TOWER, DOTS_THREE_CIRCLE, GLOBE_SIMPLE, PENCIL, PLUS_CIRCLE, POWER, TRASH, WARNING_CIRCLE, X_CIRCLE};
|
||||
use crate::gui::icons::{CHECK_CIRCLE, COMPUTER_TOWER, DOTS_THREE_CIRCLE, GLOBE_SIMPLE, PLUS_CIRCLE, POWER, QR_CODE, TRASH, WARNING_CIRCLE, X_CIRCLE};
|
||||
use crate::gui::platform::PlatformCallbacks;
|
||||
use crate::gui::views::{Modal, View};
|
||||
use crate::gui::views::network::modals::ExternalConnectionModal;
|
||||
use crate::gui::views::network::modals::{ExternalConnectionModal, ShareConnectionContent};
|
||||
use crate::gui::views::network::types::ShareConnection;
|
||||
use crate::gui::views::network::NodeSetup;
|
||||
use crate::gui::views::types::{ContentContainer, ModalPosition};
|
||||
use crate::gui::views::{Modal, View};
|
||||
use crate::gui::Colors;
|
||||
use crate::node::{Node, NodeConfig};
|
||||
use crate::wallet::{ConnectionsConfig, ExternalConnection};
|
||||
use crate::AppConfig;
|
||||
|
||||
/// Network connections content.
|
||||
pub struct ConnectionsContent {
|
||||
/// Flag to check connections state on first draw.
|
||||
first_draw: bool,
|
||||
|
||||
/// External connection [`Modal`] content.
|
||||
ext_conn_modal: ExternalConnectionModal,
|
||||
ext_conn_modal_content: ExternalConnectionModal,
|
||||
|
||||
/// [`Modal`] content to share connection with QR code.
|
||||
share_conn_modal_content: Option<ShareConnectionContent>
|
||||
}
|
||||
|
||||
impl Default for ConnectionsContent {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
first_draw: true,
|
||||
ext_conn_modal: ExternalConnectionModal::new(None),
|
||||
ext_conn_modal_content: ExternalConnectionModal::new(None),
|
||||
share_conn_modal_content: None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Identifier for [`Modal`] to share connection.
|
||||
const SHARE_CONN_QR_MODAL: &'static str = "share_conn_qr_modal";
|
||||
|
||||
impl ContentContainer for ConnectionsContent {
|
||||
fn modal_ids(&self) -> Vec<&'static str> {
|
||||
vec![
|
||||
ExternalConnectionModal::NETWORK_ID
|
||||
ExternalConnectionModal::NETWORK_ID,
|
||||
SHARE_CONN_QR_MODAL
|
||||
]
|
||||
}
|
||||
|
||||
@@ -55,8 +66,13 @@ impl ContentContainer for ConnectionsContent {
|
||||
cb: &dyn PlatformCallbacks) {
|
||||
match modal.id {
|
||||
ExternalConnectionModal::NETWORK_ID => {
|
||||
self.ext_conn_modal.ui(ui, cb, modal, |_| {});
|
||||
self.ext_conn_modal_content.ui(ui, cb, modal, |_| {});
|
||||
},
|
||||
SHARE_CONN_QR_MODAL => {
|
||||
if let Some(c) = self.share_conn_modal_content.as_mut() {
|
||||
c.ui(ui, modal, cb);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -81,11 +97,25 @@ impl ContentContainer for ConnectionsContent {
|
||||
}
|
||||
|
||||
// Show integrated node info content.
|
||||
Self::integrated_node_item_ui(ui, |ui| {
|
||||
// Draw button to show integrated node info.
|
||||
View::item_button(ui, View::item_rounding(0, 1, true), CARET_RIGHT, None, || {
|
||||
AppConfig::toggle_show_connections_network_panel();
|
||||
Self::integrated_node_item_ui(ui, Colors::fill_lite(), (true, || {
|
||||
AppConfig::toggle_show_connections_network_panel();
|
||||
}), |ui| {
|
||||
let r = View::item_rounding(0, 1, true);
|
||||
View::item_button(ui, r, QR_CODE, None, || {
|
||||
if let Ok(c) = ShareConnectionContent::new(ShareConnection {
|
||||
url: format!("http://{}", NodeConfig::get_api_address()),
|
||||
username: "grin".to_string(),
|
||||
secret: NodeConfig::get_api_secret(true).unwrap_or("".to_string()),
|
||||
}) {
|
||||
self.share_conn_modal_content = Some(c);
|
||||
// Show QR code to share integrated node connection.
|
||||
Modal::new(SHARE_CONN_QR_MODAL)
|
||||
.position(ModalPosition::Center)
|
||||
.title(t!("network.node"))
|
||||
.show();
|
||||
}
|
||||
});
|
||||
true
|
||||
});
|
||||
|
||||
// Show external connections.
|
||||
@@ -102,21 +132,42 @@ impl ContentContainer for ConnectionsContent {
|
||||
ui.add_space(4.0);
|
||||
|
||||
let ext_conn_list = ConnectionsConfig::ext_conn_list();
|
||||
let ext_conn_size = ext_conn_list.len();
|
||||
if ext_conn_size != 0 {
|
||||
let len = ext_conn_list.len();
|
||||
if len != 0 {
|
||||
ui.add_space(8.0);
|
||||
for (index, conn) in ext_conn_list.iter().enumerate() {
|
||||
for (i, c) in ext_conn_list.iter().enumerate() {
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
// Draw connection list item.
|
||||
Self::ext_conn_item_ui(ui, conn, index, ext_conn_size, |ui| {
|
||||
let button_rounding = View::item_rounding(index, ext_conn_size, true);
|
||||
View::item_button(ui, button_rounding, TRASH, None, || {
|
||||
ConnectionsConfig::remove_ext_conn(conn.id);
|
||||
let mut show_qr_content: Option<ShareConnectionContent> = None;
|
||||
// Draw external connection list item.
|
||||
let bg = Colors::fill_lite();
|
||||
Self::ext_conn_item_ui(ui, bg, c, i, len, (true, || {
|
||||
self.show_add_ext_conn_modal(Some(c.clone()));
|
||||
}), |ui| {
|
||||
// Draw button to delete connection.
|
||||
let r = View::item_rounding(i, len, true);
|
||||
View::item_button(ui, r, TRASH, Some(Colors::inactive_text()), || {
|
||||
ConnectionsConfig::remove_ext_conn(c.id);
|
||||
});
|
||||
View::item_button(ui, CornerRadius::default(), PENCIL, None, || {
|
||||
self.show_add_ext_conn_modal(Some(conn.clone()));
|
||||
// Draw button to share connection
|
||||
let r = CornerRadius::default();
|
||||
View::item_button(ui, r, QR_CODE, None, || {
|
||||
if let Ok(c) = ShareConnectionContent::new(ShareConnection {
|
||||
url: c.url.clone(),
|
||||
username: "grin".to_string(),
|
||||
secret: c.secret.clone().unwrap_or("".to_string()),
|
||||
}) {
|
||||
show_qr_content = Some(c);
|
||||
}
|
||||
});
|
||||
});
|
||||
if let Some(c) = show_qr_content {
|
||||
self.share_conn_modal_content = Some(c);
|
||||
// Show QR code to share external connection.
|
||||
Modal::new(SHARE_CONN_QR_MODAL)
|
||||
.position(ModalPosition::Center)
|
||||
.title(t!("wallets.ext_conn").replace(":", ""))
|
||||
.show();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -125,123 +176,161 @@ impl ContentContainer for ConnectionsContent {
|
||||
|
||||
impl ConnectionsContent {
|
||||
/// Draw integrated node connection item content.
|
||||
pub fn integrated_node_item_ui(ui: &mut egui::Ui, custom_button: impl FnOnce(&mut egui::Ui)) {
|
||||
pub fn integrated_node_item_ui(ui: &mut egui::Ui,
|
||||
bg: Color32,
|
||||
on_click: (bool, impl FnOnce()),
|
||||
custom_button: impl FnOnce(&mut egui::Ui) -> bool) {
|
||||
// Draw round background.
|
||||
let mut rect = ui.available_rect_before_wrap();
|
||||
rect.set_height(78.0);
|
||||
let rounding = View::item_rounding(0, 1, false);
|
||||
ui.painter().rect(rect, rounding, Colors::fill(), View::item_stroke(), StrokeKind::Outside);
|
||||
let r = View::item_rounding(0, 1, false);
|
||||
let mut bg_shape = RectShape::new(rect, r, bg, View::item_stroke(), StrokeKind::Outside);
|
||||
let bg_idx = ui.painter().add(bg_shape.clone());
|
||||
|
||||
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
|
||||
// Draw custom button.
|
||||
custom_button(ui);
|
||||
let res = ui.scope_builder(
|
||||
UiBuilder::new()
|
||||
.sense(Sense::click())
|
||||
.layout(Layout::right_to_left(Align::Center))
|
||||
.max_rect(rect), |ui| {
|
||||
// Draw custom button.
|
||||
let extra_button = custom_button(ui);
|
||||
|
||||
// Draw buttons to start/stop node.
|
||||
if Node::get_error().is_none() {
|
||||
if !Node::is_running() {
|
||||
View::item_button(ui, CornerRadius::default(), POWER, Some(Colors::green()), || {
|
||||
Node::start();
|
||||
});
|
||||
} else if !Node::is_starting() && !Node::is_stopping() && !Node::is_restarting() {
|
||||
View::item_button(ui, CornerRadius::default(), POWER, Some(Colors::red()), || {
|
||||
Node::stop(false);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let layout_size = ui.available_size();
|
||||
ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| {
|
||||
ui.add_space(6.0);
|
||||
ui.vertical(|ui| {
|
||||
ui.add_space(3.0);
|
||||
ui.with_layout(Layout::left_to_right(Align::Min), |ui| {
|
||||
ui.add_space(1.0);
|
||||
ui.label(RichText::new(t!("network.node"))
|
||||
.size(18.0)
|
||||
.color(Colors::title(false)));
|
||||
});
|
||||
|
||||
// Setup node status text.
|
||||
let has_error = Node::get_error().is_some();
|
||||
let status_icon = if has_error {
|
||||
WARNING_CIRCLE
|
||||
} else if !Node::is_running() {
|
||||
X_CIRCLE
|
||||
} else if Node::not_syncing() {
|
||||
CHECK_CIRCLE
|
||||
// Draw buttons to start/stop node.
|
||||
if Node::get_error().is_none() {
|
||||
let rounding = if extra_button {
|
||||
CornerRadius::default()
|
||||
} else {
|
||||
DOTS_THREE_CIRCLE
|
||||
View::item_rounding(0, 1, true)
|
||||
};
|
||||
let status_text = format!("{} {}", status_icon, if has_error {
|
||||
t!("error").into()
|
||||
} else {
|
||||
Node::get_sync_status_text()
|
||||
});
|
||||
View::ellipsize_text(ui, status_text, 15.0, Colors::text(false));
|
||||
ui.add_space(1.0);
|
||||
if !Node::is_running() {
|
||||
View::item_button(ui, rounding, POWER, Some(Colors::green()), || {
|
||||
Node::start();
|
||||
});
|
||||
} else if !Node::is_starting() && !Node::is_stopping() && !Node::is_restarting() {
|
||||
View::item_button(ui, rounding, POWER, Some(Colors::red()), || {
|
||||
Node::stop(false);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Setup node API address text.
|
||||
let api_address = NodeConfig::get_api_address();
|
||||
let address_text = format!("{} http://{}", COMPUTER_TOWER, api_address);
|
||||
ui.label(RichText::new(address_text).size(15.0).color(Colors::gray()));
|
||||
})
|
||||
});
|
||||
});
|
||||
let layout_size = ui.available_size();
|
||||
ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| {
|
||||
ui.add_space(6.0);
|
||||
ui.vertical(|ui| {
|
||||
ui.add_space(3.0);
|
||||
ui.with_layout(Layout::left_to_right(Align::Min), |ui| {
|
||||
ui.add_space(1.0);
|
||||
ui.label(RichText::new(t!("network.node"))
|
||||
.size(18.0)
|
||||
.color(Colors::title(false)));
|
||||
});
|
||||
|
||||
// Setup node status text.
|
||||
let has_error = Node::get_error().is_some();
|
||||
let status_icon = if has_error {
|
||||
WARNING_CIRCLE
|
||||
} else if !Node::is_running() {
|
||||
X_CIRCLE
|
||||
} else if Node::not_syncing() {
|
||||
CHECK_CIRCLE
|
||||
} else {
|
||||
DOTS_THREE_CIRCLE
|
||||
};
|
||||
let status_text = format!("{} {}", status_icon, if has_error {
|
||||
t!("error").into()
|
||||
} else {
|
||||
Node::get_sync_status_text()
|
||||
});
|
||||
View::ellipsize_text(ui, status_text, 15.0, Colors::text(false));
|
||||
ui.add_space(1.0);
|
||||
|
||||
// Setup node API address text.
|
||||
let api_address = NodeConfig::get_api_address();
|
||||
let address_text = format!("{} http://{}", COMPUTER_TOWER, api_address);
|
||||
ui.label(RichText::new(address_text).size(15.0).color(Colors::gray()));
|
||||
})
|
||||
});
|
||||
}).response;
|
||||
let (clickable, on_click) = on_click;
|
||||
let clicked = res.clicked() || res.long_touched();
|
||||
// Setup background and cursor.
|
||||
if clickable && res.hovered() {
|
||||
res.on_hover_cursor(CursorIcon::PointingHand);
|
||||
bg_shape.fill = Colors::fill();
|
||||
}
|
||||
ui.painter().set(bg_idx, bg_shape);
|
||||
// Handle clicks on layout.
|
||||
if clicked && clickable {
|
||||
on_click();
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw external connection item content.
|
||||
pub fn ext_conn_item_ui(ui: &mut egui::Ui,
|
||||
bg: Color32,
|
||||
conn: &ExternalConnection,
|
||||
index: usize,
|
||||
len: usize,
|
||||
buttons_ui: impl FnOnce(&mut egui::Ui)) {
|
||||
// Setup layout size.
|
||||
on_click: (bool, impl FnOnce()),
|
||||
custom_button: impl FnOnce(&mut egui::Ui)) {
|
||||
// Draw round background.
|
||||
let mut rect = ui.available_rect_before_wrap();
|
||||
rect.set_height(52.0);
|
||||
let r = View::item_rounding(index, len, false);
|
||||
let mut bg_shape = RectShape::new(rect, r, bg, View::item_stroke(), StrokeKind::Outside);
|
||||
let bg_idx = ui.painter().add(bg_shape.clone());
|
||||
|
||||
// Draw round background.
|
||||
let bg_rect = rect.clone();
|
||||
let item_rounding = View::item_rounding(index, len, false);
|
||||
ui.painter().rect(bg_rect,
|
||||
item_rounding,
|
||||
Colors::fill(),
|
||||
View::item_stroke(),
|
||||
StrokeKind::Outside);
|
||||
let res = ui.scope_builder(
|
||||
UiBuilder::new()
|
||||
.sense(Sense::click())
|
||||
.layout(Layout::right_to_left(Align::Center))
|
||||
.max_rect(rect), |ui| {
|
||||
// Draw custom button.
|
||||
custom_button(ui);
|
||||
|
||||
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
|
||||
// Draw provided buttons.
|
||||
buttons_ui(ui);
|
||||
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| {
|
||||
// Draw connections URL.
|
||||
ui.add_space(4.0);
|
||||
let conn_text = format!("{} {}", GLOBE_SIMPLE, conn.url);
|
||||
View::ellipsize_text(ui, conn_text, 15.0, Colors::title(false));
|
||||
ui.add_space(1.0);
|
||||
|
||||
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| {
|
||||
// Draw connections URL.
|
||||
ui.add_space(4.0);
|
||||
let conn_text = format!("{} {}", GLOBE_SIMPLE, conn.url);
|
||||
View::ellipsize_text(ui, conn_text, 15.0, Colors::title(false));
|
||||
ui.add_space(1.0);
|
||||
|
||||
// Setup connection status text.
|
||||
let status_text = if let Some(available) = conn.available {
|
||||
if available {
|
||||
format!("{} {}", CHECK_CIRCLE, t!("network.available"))
|
||||
// Setup connection status text.
|
||||
let status_text = if let Some(available) = conn.available {
|
||||
if available {
|
||||
format!("{} {}", CHECK_CIRCLE, t!("network.available"))
|
||||
} else {
|
||||
format!("{} {}", X_CIRCLE, t!("network.not_available"))
|
||||
}
|
||||
} else {
|
||||
format!("{} {}", X_CIRCLE, t!("network.not_available"))
|
||||
}
|
||||
} else {
|
||||
format!("{} {}", DOTS_THREE_CIRCLE, t!("network.availability_check"))
|
||||
};
|
||||
ui.label(RichText::new(status_text).size(15.0).color(Colors::gray()));
|
||||
ui.add_space(3.0);
|
||||
format!("{} {}", DOTS_THREE_CIRCLE, t!("network.availability_check"))
|
||||
};
|
||||
ui.label(RichText::new(status_text).size(15.0).color(Colors::gray()));
|
||||
ui.add_space(3.0);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
).response;
|
||||
let (clickable, on_click) = on_click;
|
||||
let clicked = res.clicked() || res.long_touched();
|
||||
// Setup background and cursor.
|
||||
if clickable && res.hovered() {
|
||||
res.on_hover_cursor(CursorIcon::PointingHand);
|
||||
bg_shape.fill = Colors::fill();
|
||||
}
|
||||
ui.painter().set(bg_idx, bg_shape);
|
||||
// Handle clicks on layout.
|
||||
if clicked && clickable {
|
||||
on_click();
|
||||
}
|
||||
}
|
||||
|
||||
/// Show [`Modal`] to add external connection.
|
||||
pub fn show_add_ext_conn_modal(&mut self, conn: Option<ExternalConnection>) {
|
||||
self.ext_conn_modal = ExternalConnectionModal::new(conn);
|
||||
self.ext_conn_modal_content = ExternalConnectionModal::new(conn);
|
||||
// Show modal.
|
||||
Modal::new(ExternalConnectionModal::NETWORK_ID)
|
||||
.position(ModalPosition::CenterTop)
|
||||
|
||||
@@ -152,7 +152,7 @@ fn block_item_ui(ui: &mut egui::Ui, db: &DiffBlock, rounding: CornerRadius) {
|
||||
rect.max -= vec2(8.0, 0.0);
|
||||
ui.painter().rect(rect,
|
||||
rounding,
|
||||
Colors::white_or_black(false),
|
||||
Colors::fill(),
|
||||
View::item_stroke(),
|
||||
StrokeKind::Outside);
|
||||
|
||||
|
||||
@@ -15,9 +15,11 @@
|
||||
use egui::{Id, RichText};
|
||||
use url::Url;
|
||||
|
||||
use crate::gui::Colors;
|
||||
use crate::gui::icons::SCAN;
|
||||
use crate::gui::platform::PlatformCallbacks;
|
||||
use crate::gui::views::{Modal, TextEdit, View};
|
||||
use crate::gui::views::network::types::ShareConnection;
|
||||
use crate::gui::views::{CameraContent, Modal, TextEdit, View};
|
||||
use crate::gui::Colors;
|
||||
use crate::wallet::{ConnectionsConfig, ExternalConnection};
|
||||
|
||||
/// Content to create or update external wallet connection.
|
||||
@@ -25,14 +27,21 @@ pub struct ExternalConnectionModal {
|
||||
/// Flag to check if content was just rendered.
|
||||
first_draw: bool,
|
||||
|
||||
/// External connection URL value for [`Modal`].
|
||||
ext_node_url_edit: String,
|
||||
/// External connection API secret value for [`Modal`].
|
||||
ext_node_secret_edit: String,
|
||||
/// Flag to show URL format error at [`Modal`].
|
||||
ext_node_url_error: bool,
|
||||
/// Editing external connection identifier for [`Modal`].
|
||||
ext_conn_id: Option<i64>,
|
||||
/// Editing external connection identifier.
|
||||
id: Option<i64>,
|
||||
|
||||
/// External connection URL.
|
||||
url_edit: String,
|
||||
/// Flag to show URL format error.
|
||||
url_error: bool,
|
||||
|
||||
// /// External connection username.
|
||||
// username_edit: String,
|
||||
/// External connection API secret.
|
||||
secret_edit: String,
|
||||
|
||||
/// QR code scanner content.
|
||||
scan_qr_content: Option<CameraContent>,
|
||||
}
|
||||
|
||||
impl ExternalConnectionModal {
|
||||
@@ -43,17 +52,20 @@ impl ExternalConnectionModal {
|
||||
|
||||
/// Create new instance from optional provided connection to update.
|
||||
pub fn new(conn: Option<ExternalConnection>) -> Self {
|
||||
let (ext_node_url_edit, ext_node_secret_edit, ext_conn_id) = if let Some(c) = conn {
|
||||
(c.url, c.secret.unwrap_or("".to_string()), Some(c.id))
|
||||
let (url_edit, secret_edit, id) = if let Some(c) = conn {
|
||||
// let username = c.username.unwrap_or("grin".to_string());
|
||||
let secret = c.secret.unwrap_or("".to_string());
|
||||
(c.url, secret, Some(c.id))
|
||||
} else {
|
||||
("".to_string(), "".to_string(), None)
|
||||
};
|
||||
Self {
|
||||
first_draw: true,
|
||||
ext_node_url_edit,
|
||||
ext_node_secret_edit,
|
||||
ext_node_url_error: false,
|
||||
ext_conn_id,
|
||||
url_edit,
|
||||
url_error: false,
|
||||
secret_edit,
|
||||
id,
|
||||
scan_qr_content: None
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,25 +75,70 @@ impl ExternalConnectionModal {
|
||||
cb: &dyn PlatformCallbacks,
|
||||
modal: &Modal,
|
||||
on_save: impl Fn(ExternalConnection)) {
|
||||
// Show QR code scanner content.
|
||||
if let Some(scan_content) = self.scan_qr_content.as_mut() {
|
||||
if let Some(result) = scan_content.qr_scan_result() {
|
||||
cb.stop_camera();
|
||||
modal.enable_closing();
|
||||
self.scan_qr_content = None;
|
||||
// Parse scan result.
|
||||
if let Ok(c) = serde_json::from_str::<ShareConnection>(&result.text()) {
|
||||
let ext_conn = ExternalConnection::new(c.url, Some(c.username), Some(c.secret));
|
||||
ConnectionsConfig::add_ext_conn(ext_conn.clone());
|
||||
ExternalConnection::check(Some(ext_conn.id), ui.ctx());
|
||||
Modal::close();
|
||||
}
|
||||
} else {
|
||||
scan_content.ui(ui, cb);
|
||||
}
|
||||
ui.add_space(8.0);
|
||||
|
||||
// Setup spacing between buttons.
|
||||
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.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.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.scan_qr_content = None;
|
||||
modal.enable_closing();
|
||||
});
|
||||
});
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
return;
|
||||
}
|
||||
// Add connection button callback.
|
||||
let on_add = |ui: &mut egui::Ui, m: &mut ExternalConnectionModal| {
|
||||
let url = if !m.ext_node_url_edit.starts_with("http") {
|
||||
format!("https://{}", m.ext_node_url_edit)
|
||||
let url = if !m.url_edit.starts_with("http") {
|
||||
format!("https://{}", m.url_edit)
|
||||
} else {
|
||||
m.ext_node_url_edit.clone()
|
||||
m.url_edit.clone()
|
||||
};
|
||||
let error = Url::parse(url.trim()).is_err();
|
||||
m.ext_node_url_error = error;
|
||||
m.url_error = error;
|
||||
if !error {
|
||||
let secret = if m.ext_node_secret_edit.is_empty() {
|
||||
let username = if m.secret_edit.is_empty() {
|
||||
Some("grin".to_string())
|
||||
} else {
|
||||
Some(m.secret_edit.clone())
|
||||
};
|
||||
let secret = if m.secret_edit.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(m.ext_node_secret_edit.clone())
|
||||
Some(m.secret_edit.clone())
|
||||
};
|
||||
|
||||
// Update or create new connection.
|
||||
let mut ext_conn = ExternalConnection::new(url, secret);
|
||||
if let Some(id) = m.ext_conn_id {
|
||||
let mut ext_conn = ExternalConnection::new(url, username, secret);
|
||||
if let Some(id) = m.id {
|
||||
ext_conn.id = id;
|
||||
}
|
||||
ConnectionsConfig::add_ext_conn(ext_conn.clone());
|
||||
@@ -89,9 +146,9 @@ impl ExternalConnectionModal {
|
||||
on_save(ext_conn);
|
||||
|
||||
// Close modal.
|
||||
m.ext_node_url_edit = "".to_string();
|
||||
m.ext_node_secret_edit = "".to_string();
|
||||
m.ext_node_url_error = false;
|
||||
m.url_edit = "".to_string();
|
||||
m.secret_edit = "".to_string();
|
||||
m.url_error = false;
|
||||
Modal::close();
|
||||
}
|
||||
};
|
||||
@@ -104,11 +161,24 @@ impl ExternalConnectionModal {
|
||||
ui.add_space(8.0);
|
||||
|
||||
// Draw node URL text edit.
|
||||
let url_edit_id = Id::from(modal.id).with(self.ext_conn_id).with("node_url");
|
||||
let mut url_edit = TextEdit::new(url_edit_id)
|
||||
.paste()
|
||||
.focus(self.first_draw);
|
||||
url_edit.ui(ui, &mut self.ext_node_url_edit, cb);
|
||||
let url_edit_id = Id::from(modal.id).with(self.id).with("node_url");
|
||||
let mut url_edit = TextEdit::new(url_edit_id).paste().focus(self.first_draw);
|
||||
let url_edit_before = self.url_edit.clone();
|
||||
url_edit.ui(ui, &mut self.url_edit, cb);
|
||||
if self.url_edit != url_edit_before {
|
||||
self.url_error = false;
|
||||
}
|
||||
|
||||
// ui.add_space(8.0);
|
||||
// ui.label(RichText::new(t!("wallets.name"))
|
||||
// .size(17.0)
|
||||
// .color(Colors::gray()));
|
||||
// ui.add_space(8.0);
|
||||
//
|
||||
// // Draw node username text edit (disabled by default).
|
||||
// let username_edit_id = Id::from(modal.id).with(self.id).with("node_username");
|
||||
// let mut username_edit = TextEdit::new(username_edit_id).focus(false).disable();
|
||||
// username_edit.ui(ui, &mut self.username_edit, cb);
|
||||
|
||||
ui.add_space(8.0);
|
||||
ui.label(RichText::new(t!("wallets.node_secret"))
|
||||
@@ -117,29 +187,41 @@ impl ExternalConnectionModal {
|
||||
ui.add_space(8.0);
|
||||
|
||||
// Draw node API secret text edit.
|
||||
let secret_edit_id = Id::from(modal.id).with(self.ext_conn_id).with("node_secret");
|
||||
let secret_edit_id = Id::from(modal.id).with(self.id).with("node_secret");
|
||||
let mut secret_edit = TextEdit::new(secret_edit_id)
|
||||
.h_center()
|
||||
.password()
|
||||
.paste()
|
||||
.focus(false);
|
||||
if url_edit.enter_pressed {
|
||||
secret_edit.focus_request();
|
||||
}
|
||||
secret_edit.ui(ui, &mut self.ext_node_secret_edit, cb);
|
||||
secret_edit.ui(ui, &mut self.secret_edit, cb);
|
||||
if secret_edit.enter_pressed {
|
||||
(on_add)(ui, self);
|
||||
on_add(ui, self);
|
||||
}
|
||||
|
||||
// Show error when specified URL is not valid.
|
||||
if self.ext_node_url_error {
|
||||
if self.url_error {
|
||||
ui.add_space(12.0);
|
||||
ui.label(RichText::new(t!("wallets.invalid_url"))
|
||||
.size(17.0)
|
||||
.color(Colors::red()));
|
||||
}
|
||||
ui.add_space(12.0);
|
||||
|
||||
let scan_text = format!("{} {}", SCAN, t!("scan"));
|
||||
View::button(ui, scan_text, Colors::white_or_black(false), || {
|
||||
modal.disable_closing();
|
||||
self.scan_qr_content = Some(CameraContent::default());
|
||||
cb.start_camera();
|
||||
});
|
||||
});
|
||||
|
||||
ui.add_space(8.0);
|
||||
View::horizontal_line(ui, Colors::item_stroke());
|
||||
ui.add_space(8.0);
|
||||
|
||||
// Show modal buttons.
|
||||
ui.scope(|ui| {
|
||||
// Setup spacing between buttons.
|
||||
@@ -149,14 +231,14 @@ impl ExternalConnectionModal {
|
||||
columns[0].vertical_centered_justified(|ui| {
|
||||
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
|
||||
// Close modal.
|
||||
self.ext_node_url_edit = "".to_string();
|
||||
self.ext_node_secret_edit = "".to_string();
|
||||
self.ext_node_url_error = false;
|
||||
self.url_edit = "".to_string();
|
||||
self.secret_edit = "".to_string();
|
||||
self.url_error = false;
|
||||
Modal::close();
|
||||
});
|
||||
});
|
||||
columns[1].vertical_centered_justified(|ui| {
|
||||
View::button_ui(ui, if self.ext_conn_id.is_some() {
|
||||
View::button_ui(ui, if self.id.is_some() {
|
||||
t!("modal.save")
|
||||
} else {
|
||||
t!("modal.add")
|
||||
|
||||
@@ -13,4 +13,7 @@
|
||||
// limitations under the License.
|
||||
|
||||
mod ext_conn;
|
||||
pub use ext_conn::*;
|
||||
pub use ext_conn::*;
|
||||
|
||||
mod share_conn;
|
||||
pub use share_conn::*;
|
||||
@@ -0,0 +1,57 @@
|
||||
// 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 crate::AppConfig;
|
||||
use crate::gui::Colors;
|
||||
use crate::gui::platform::PlatformCallbacks;
|
||||
use crate::gui::views::network::types::ShareConnection;
|
||||
use crate::gui::views::{Modal, QrCodeContent, View};
|
||||
|
||||
/// [`Modal`] content to share connection with QR code.
|
||||
pub struct ShareConnectionContent {
|
||||
/// QR code content.
|
||||
pub qr_details_content: QrCodeContent,
|
||||
}
|
||||
|
||||
impl ShareConnectionContent {
|
||||
/// Create new content instance from connection details.
|
||||
pub fn new(details: ShareConnection) -> Result<Self, serde_json::Error> {
|
||||
let details = serde_json::to_string_pretty(&details)?;
|
||||
let c = Self {
|
||||
qr_details_content: QrCodeContent::new(details, false).hide_text().no_copy(),
|
||||
};
|
||||
Ok(c)
|
||||
}
|
||||
|
||||
/// Draw QR code content.
|
||||
pub fn ui(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
|
||||
let dark_theme = AppConfig::dark_theme().unwrap_or(false);
|
||||
// Set light theme for better scanning.
|
||||
AppConfig::set_dark_theme(false);
|
||||
modal.set_background_color(Colors::FILL_DEEP);
|
||||
crate::setup_visuals(ui.ctx());
|
||||
// Draw QR code content.
|
||||
ui.add_space(6.0);
|
||||
self.qr_details_content.ui(ui, cb);
|
||||
ui.vertical_centered_justified(|ui| {
|
||||
View::button(ui, t!("close"), Colors::white_or_black(false), || {
|
||||
Modal::close();
|
||||
});
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
// Set color theme back.
|
||||
AppConfig::set_dark_theme(dark_theme);
|
||||
crate::setup_visuals(ui.ctx());
|
||||
}
|
||||
}
|
||||
@@ -176,7 +176,7 @@ 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: CornerRadius) {
|
||||
fn peer_item_ui(ui: &mut egui::Ui, peer: &PeerStats, r: CornerRadius) {
|
||||
let mut rect = ui.available_rect_before_wrap();
|
||||
rect.set_height(PEER_ITEM_HEIGHT);
|
||||
ui.allocate_ui(rect.size(), |ui| {
|
||||
@@ -184,7 +184,7 @@ fn peer_item_ui(ui: &mut egui::Ui, peer: &PeerStats, rounding: CornerRadius) {
|
||||
ui.add_space(4.0);
|
||||
|
||||
// Draw round background.
|
||||
ui.painter().rect(rect, rounding, Colors::fill_lite(), View::item_stroke(), StrokeKind::Outside);
|
||||
ui.painter().rect(rect, r, Colors::fill(), View::item_stroke(), StrokeKind::Outside);
|
||||
|
||||
// Draw IP address.
|
||||
ui.horizontal(|ui| {
|
||||
|
||||
@@ -13,11 +13,11 @@
|
||||
// limitations under the License.
|
||||
|
||||
use eframe::emath::Align;
|
||||
use eframe::epaint::StrokeKind;
|
||||
use egui::{Id, Layout, RichText};
|
||||
use eframe::epaint::{RectShape, StrokeKind};
|
||||
use egui::{CursorIcon, Id, Layout, RichText, Sense, UiBuilder};
|
||||
use grin_core::global::ChainTypes;
|
||||
|
||||
use crate::gui::icons::{CLOCK_CLOCKWISE, COMPUTER_TOWER, FOLDERS, PENCIL, PLUG, POWER, SHIELD, SHIELD_SLASH};
|
||||
use crate::gui::icons::{CLOCK_CLOCKWISE, COMPUTER_TOWER, FOLDERS, PLUG, POWER, SHIELD, SHIELD_SLASH};
|
||||
use crate::gui::platform::PlatformCallbacks;
|
||||
use crate::gui::views::network::settings::NetworkSettings;
|
||||
use crate::gui::views::network::NetworkContent;
|
||||
@@ -173,7 +173,7 @@ impl ContentContainer for NodeSetup {
|
||||
// Show data location selection for Desktop when it already started or turned off.
|
||||
if !Node::is_restarting() && !Node::is_stopping() && !Node::is_starting() &&
|
||||
View::is_desktop() {
|
||||
self.pick_data_dir_ui(ui, cb);
|
||||
self.data_dir_ui(ui, cb);
|
||||
ui.add_space(6.0);
|
||||
}
|
||||
|
||||
@@ -236,46 +236,56 @@ impl ContentContainer for NodeSetup {
|
||||
|
||||
impl NodeSetup {
|
||||
/// Draw content to change chain data directory.
|
||||
fn pick_data_dir_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||
// Setup layout size.
|
||||
fn data_dir_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||
// Draw round background.
|
||||
let mut rect = ui.available_rect_before_wrap();
|
||||
rect.set_height(56.0);
|
||||
let r = View::item_rounding(0, 1, false);
|
||||
let bg = Colors::fill_lite();
|
||||
let mut bg_shape = RectShape::new(rect, r, bg, View::item_stroke(), StrokeKind::Outside);
|
||||
let bg_idx = ui.painter().add(bg_shape.clone());
|
||||
|
||||
// Draw round background.
|
||||
let bg_rect = rect.clone();
|
||||
let item_rounding = View::item_rounding(0, 1, false);
|
||||
ui.painter().rect(bg_rect,
|
||||
item_rounding,
|
||||
Colors::fill(),
|
||||
View::item_stroke(),
|
||||
StrokeKind::Outside);
|
||||
|
||||
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
|
||||
self.pick_data_dir.ui(ui, cb, |path| {
|
||||
Node::change_data_dir(path);
|
||||
});
|
||||
View::item_button(ui, View::item_rounding(1, 3, true), PENCIL, None, || {
|
||||
self.data_path_edit = NodeConfig::get_chain_data_path();
|
||||
// Show chain data path edit modal.
|
||||
Modal::new(DATA_PATH_MODAL)
|
||||
.position(ModalPosition::CenterTop)
|
||||
.title(t!("network.node"))
|
||||
.show();
|
||||
});
|
||||
let layout_size = ui.available_size();
|
||||
ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| {
|
||||
ui.add_space(12.0);
|
||||
ui.vertical(|ui| {
|
||||
ui.add_space(4.0);
|
||||
let path = NodeConfig::get_chain_data_path();
|
||||
View::ellipsize_text(ui, path, 18.0, Colors::title(false));
|
||||
ui.add_space(1.0);
|
||||
let desc = format!("{} {}", FOLDERS, t!("files_location"));
|
||||
ui.label(RichText::new(desc).size(15.0).color(Colors::gray()));
|
||||
ui.add_space(8.0);
|
||||
let res = ui.scope_builder(
|
||||
UiBuilder::new()
|
||||
.sense(Sense::click())
|
||||
.layout(Layout::right_to_left(Align::Center))
|
||||
.max_rect(rect), |ui| {
|
||||
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
|
||||
self.pick_data_dir.ui(ui, cb, |path| {
|
||||
Node::change_data_dir(path);
|
||||
});
|
||||
let layout_size = ui.available_size();
|
||||
ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| {
|
||||
ui.add_space(12.0);
|
||||
ui.vertical(|ui| {
|
||||
ui.add_space(4.0);
|
||||
let path = NodeConfig::get_chain_data_path();
|
||||
View::ellipsize_text(ui, path, 18.0, Colors::title(false));
|
||||
ui.add_space(1.0);
|
||||
let desc = format!("{} {}", FOLDERS, t!("files_location"));
|
||||
ui.label(RichText::new(desc).size(15.0).color(Colors::gray()));
|
||||
ui.add_space(8.0);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
).response;
|
||||
let clicked = res.clicked() || res.long_touched();
|
||||
// Setup background and cursor.
|
||||
if res.hovered() {
|
||||
res.on_hover_cursor(CursorIcon::PointingHand);
|
||||
bg_shape.fill = Colors::fill();
|
||||
}
|
||||
ui.painter().set(bg_idx, bg_shape);
|
||||
// Handle clicks on layout.
|
||||
if clicked {
|
||||
self.data_path_edit = NodeConfig::get_chain_data_path();
|
||||
// Show chain data path edit modal.
|
||||
Modal::new(DATA_PATH_MODAL)
|
||||
.position(ModalPosition::CenterTop)
|
||||
.title(t!("network.node"))
|
||||
.show();
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw data path input [`Modal`] content.
|
||||
|
||||
@@ -786,7 +786,7 @@ fn peer_item_ui(ui: &mut egui::Ui,
|
||||
let item_rounding = View::item_rounding(index, len, false);
|
||||
ui.painter().rect(rect,
|
||||
item_rounding,
|
||||
Colors::white_or_black(false),
|
||||
Colors::fill(),
|
||||
View::item_stroke(),
|
||||
StrokeKind::Outside);
|
||||
|
||||
@@ -794,7 +794,8 @@ fn peer_item_ui(ui: &mut egui::Ui,
|
||||
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
|
||||
// Draw delete button for non-default seed peers.
|
||||
if peer_type != &PeerType::DefaultSeed {
|
||||
View::item_button(ui, View::item_rounding(index, len, true), TRASH, None, || {
|
||||
let r = View::item_rounding(index, len, true);
|
||||
View::item_button(ui, r, TRASH, Some(Colors::inactive_text()), || {
|
||||
match peer_type {
|
||||
PeerType::CustomSeed => {
|
||||
NodeConfig::remove_custom_seed(peer_addr);
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use crate::gui::platform::PlatformCallbacks;
|
||||
|
||||
/// Integrated node tab content interface.
|
||||
@@ -38,4 +39,13 @@ impl NodeTabType {
|
||||
NodeTabType::Settings => t!("network.settings").into()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Connection details to share.
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct ShareConnection {
|
||||
#[serde(rename(serialize = "ipPort", deserialize = "ipPort"))]
|
||||
pub url: String,
|
||||
pub username: String,
|
||||
pub secret: String
|
||||
}
|
||||
@@ -113,7 +113,7 @@ pub enum PullToRefreshState {
|
||||
/// `far_enough` is true if the user dragged far enough to trigger a refresh.
|
||||
far_enough: bool,
|
||||
},
|
||||
/// The user dragged far enough to trigger a refresh and released the pointer.
|
||||
/// The user dragged far enough to trigger a refresh.
|
||||
DoRefresh,
|
||||
/// The refresh is currently happening.
|
||||
Refreshing,
|
||||
@@ -298,6 +298,12 @@ impl PullToRefresh {
|
||||
} else {
|
||||
state = PullToRefreshState::Idle;
|
||||
}
|
||||
} else if let PullToRefreshState::Dragging {
|
||||
far_enough: enough, ..
|
||||
} = state.clone() {
|
||||
if enough {
|
||||
state = PullToRefreshState::DoRefresh;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
state = PullToRefreshState::Idle;
|
||||
|
||||
+55
-20
@@ -32,6 +32,10 @@ use crate::gui::Colors;
|
||||
pub struct QrCodeContent {
|
||||
/// QR code text.
|
||||
pub text: String,
|
||||
/// Flag to show text below QR code.
|
||||
show_text: bool,
|
||||
/// Flag to copy text below QR code.
|
||||
can_copy_text: bool,
|
||||
|
||||
/// Maximum QR code size.
|
||||
max_size: f32,
|
||||
@@ -56,6 +60,8 @@ impl QrCodeContent {
|
||||
pub fn new(text: String, animated: bool) -> Self {
|
||||
Self {
|
||||
text,
|
||||
show_text: true,
|
||||
can_copy_text: true,
|
||||
max_size: DEFAULT_QR_SIZE as f32,
|
||||
animated,
|
||||
animated_index: None,
|
||||
@@ -71,6 +77,18 @@ impl QrCodeContent {
|
||||
self
|
||||
}
|
||||
|
||||
/// Hide text below QR code.
|
||||
pub fn hide_text(mut self) -> Self {
|
||||
self.show_text = false;
|
||||
self
|
||||
}
|
||||
|
||||
/// Do not show button to copy QR code text.
|
||||
pub fn no_copy(mut self) -> Self {
|
||||
self.can_copy_text = false;
|
||||
self
|
||||
}
|
||||
|
||||
/// Draw QR code.
|
||||
pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||
if self.animated {
|
||||
@@ -123,7 +141,9 @@ impl QrCodeContent {
|
||||
self.qr_image_ui(svg, ui);
|
||||
|
||||
// Show QR code text.
|
||||
self.text_ui(ui);
|
||||
if self.show_text {
|
||||
self.text_ui(ui);
|
||||
}
|
||||
|
||||
ui.vertical_centered(|ui| {
|
||||
let sharing = {
|
||||
@@ -202,32 +222,47 @@ impl QrCodeContent {
|
||||
self.qr_image_ui(svg, ui);
|
||||
|
||||
// Show QR code text.
|
||||
self.text_ui(ui);
|
||||
if self.show_text {
|
||||
self.text_ui(ui);
|
||||
} else {
|
||||
ui.add_space(8.0);
|
||||
}
|
||||
|
||||
// Setup spacing between buttons.
|
||||
ui.spacing_mut().item_spacing = egui::Vec2::new(6.0, 0.0);
|
||||
if self.can_copy_text {
|
||||
// Setup spacing between buttons.
|
||||
ui.spacing_mut().item_spacing = egui::Vec2::new(6.0, 0.0);
|
||||
|
||||
ui.columns(2, |columns| {
|
||||
columns[0].vertical_centered_justified(|ui| {
|
||||
// Draw copy button.
|
||||
let copy_text = format!("{} {}", COPY, t!("copy"));
|
||||
View::button(ui, copy_text, Colors::white_or_black(false), || {
|
||||
cb.copy_string_to_buffer(self.text.clone());
|
||||
ui.columns(2, |columns| {
|
||||
columns[0].vertical_centered_justified(|ui| {
|
||||
// Draw copy button.
|
||||
let copy_text = format!("{} {}", COPY, t!("copy"));
|
||||
View::button(ui, copy_text, Colors::white_or_black(false), || {
|
||||
cb.copy_string_to_buffer(self.text.clone());
|
||||
});
|
||||
});
|
||||
columns[1].vertical_centered_justified(|ui| {
|
||||
self.share_static_button_ui(ui, cb);
|
||||
});
|
||||
});
|
||||
columns[1].vertical_centered_justified(|ui| {
|
||||
let share_text = format!("{} {}", IMAGES_SQUARE, t!("share"));
|
||||
View::colored_text_button(ui,
|
||||
share_text,
|
||||
Colors::blue(),
|
||||
Colors::white_or_black(false), || {
|
||||
self.share_static(cb);
|
||||
});
|
||||
} else {
|
||||
ui.vertical_centered(|ui| {
|
||||
self.share_static_button_ui(ui, cb);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw button to share static QR code.
|
||||
fn share_static_button_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||
let share_text = format!("{} {}", IMAGES_SQUARE, t!("share"));
|
||||
View::colored_text_button(ui,
|
||||
share_text,
|
||||
Colors::blue(),
|
||||
Colors::white_or_black(false), || {
|
||||
self.share_static(cb);
|
||||
});
|
||||
}
|
||||
|
||||
/// Share static QR code image.
|
||||
fn share_static(&self, cb: &dyn PlatformCallbacks) {
|
||||
let text = self.text.as_str();
|
||||
@@ -304,7 +339,7 @@ impl QrCodeContent {
|
||||
let qr_state = self.qr_image_state.clone();
|
||||
let text = self.text.clone();
|
||||
thread::spawn(move || {
|
||||
let mut encoder = ur::Encoder::bytes(text.as_bytes(), 100).unwrap();
|
||||
let mut encoder = ur::Encoder::bytes(text.as_bytes(), 64).unwrap();
|
||||
let mut data = Vec::with_capacity(encoder.fragment_count());
|
||||
for _ in 0..encoder.fragment_count() {
|
||||
let ur = encoder.next_part().unwrap();
|
||||
|
||||
@@ -12,12 +12,11 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use egui::scroll_area::ScrollBarVisibility;
|
||||
use egui::{Align, Layout, RichText, ScrollArea, StrokeKind};
|
||||
use eframe::epaint::RectShape;
|
||||
use egui::{Align, CursorIcon, Layout, RichText, Sense, StrokeKind, UiBuilder};
|
||||
|
||||
use crate::gui::icons::{CHECK, PENCIL, TRANSLATE};
|
||||
use crate::gui::platform::PlatformCallbacks;
|
||||
use crate::gui::views::types::{ContentContainer, ModalPosition};
|
||||
use crate::gui::views::types::ContentContainer;
|
||||
use crate::gui::views::{Modal, View};
|
||||
use crate::gui::Colors;
|
||||
use crate::AppConfig;
|
||||
@@ -28,22 +27,10 @@ pub struct InterfaceSettingsContent {
|
||||
locale: String,
|
||||
}
|
||||
|
||||
/// Identifier for language selection [`Modal`].
|
||||
const LANGUAGE_SELECTION_MODAL: &'static str = "language_selection_modal";
|
||||
|
||||
impl ContentContainer for InterfaceSettingsContent {
|
||||
fn modal_ids(&self) -> Vec<&'static str> {
|
||||
vec![
|
||||
LANGUAGE_SELECTION_MODAL
|
||||
]
|
||||
}
|
||||
fn modal_ids(&self) -> Vec<&'static str> { vec![] }
|
||||
|
||||
fn modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal, _: &dyn PlatformCallbacks) {
|
||||
match modal.id {
|
||||
LANGUAGE_SELECTION_MODAL => self.language_selection_ui(ui),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
fn modal_ui(&mut self, _: &mut egui::Ui, _: &Modal, _: &dyn PlatformCallbacks) {}
|
||||
|
||||
fn container_ui(&mut self, ui: &mut egui::Ui, _: &dyn PlatformCallbacks) {
|
||||
ui.add_space(5.0);
|
||||
@@ -72,7 +59,10 @@ impl ContentContainer for InterfaceSettingsContent {
|
||||
}
|
||||
|
||||
// Draw language selection.
|
||||
self.language_item_ui(self.locale.clone().as_str(), ui, true, 0, 1);
|
||||
let locales = rust_i18n::available_locales!();
|
||||
for (index, locale) in locales.iter().enumerate() {
|
||||
self.language_item_ui(locale, ui, index, locales.len());
|
||||
}
|
||||
ui.add_space(4.0);
|
||||
}
|
||||
}
|
||||
@@ -91,96 +81,29 @@ impl Default for InterfaceSettingsContent {
|
||||
}
|
||||
|
||||
impl InterfaceSettingsContent {
|
||||
/// Draw language selection content.
|
||||
fn language_selection_ui(&mut self, ui: &mut egui::Ui) {
|
||||
ui.add_space(4.0);
|
||||
ScrollArea::vertical()
|
||||
.max_height(373.0)
|
||||
.id_salt("select_language_scroll")
|
||||
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
|
||||
.auto_shrink([true; 2])
|
||||
.show(ui, |ui| {
|
||||
ui.add_space(2.0);
|
||||
ui.vertical_centered(|ui| {
|
||||
let locales = rust_i18n::available_locales!();
|
||||
for (index, locale) in locales.iter().enumerate() {
|
||||
self.language_item_ui(locale, ui, false, index, locales.len());
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
ui.add_space(6.0);
|
||||
View::horizontal_line(ui, Colors::item_stroke());
|
||||
ui.add_space(6.0);
|
||||
|
||||
// Show button to close modal.
|
||||
ui.vertical_centered_justified(|ui| {
|
||||
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
|
||||
Modal::close();
|
||||
});
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
}
|
||||
|
||||
/// Draw language selection item content.
|
||||
fn language_item_ui(&mut self, locale: &str, ui: &mut egui::Ui, edit: bool, index: usize, len: usize) {
|
||||
// Setup layout size.
|
||||
let mut rect = ui.available_rect_before_wrap();
|
||||
if edit {
|
||||
rect.set_height(56.0);
|
||||
} else {
|
||||
rect.set_height(50.0);
|
||||
}
|
||||
|
||||
fn language_item_ui(&mut self, locale: &str, ui: &mut egui::Ui, index: usize, len: usize) {
|
||||
let is_current = self.locale == locale;
|
||||
// Draw round background.
|
||||
let bg_rect = rect.clone();
|
||||
let item_rounding = View::item_rounding(index, len, false);
|
||||
ui.painter().rect(bg_rect,
|
||||
item_rounding,
|
||||
Colors::fill(),
|
||||
View::item_stroke(),
|
||||
StrokeKind::Outside);
|
||||
let mut rect = ui.available_rect_before_wrap();
|
||||
rect.set_height(50.0);
|
||||
let r = View::item_rounding(index, len, false);
|
||||
let bg = if is_current {
|
||||
Colors::fill()
|
||||
} else {
|
||||
Colors::fill_lite()
|
||||
};
|
||||
let mut bg_shape = RectShape::new(rect, r, bg, View::item_stroke(), StrokeKind::Outside);
|
||||
let bg_idx = ui.painter().add(bg_shape.clone());
|
||||
|
||||
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
|
||||
if edit {
|
||||
View::item_button(ui, View::item_rounding(index, len, true), PENCIL, None, || {
|
||||
// Show language selection modal.
|
||||
Modal::new(LANGUAGE_SELECTION_MODAL)
|
||||
.position(ModalPosition::Center)
|
||||
.title(t!("language"))
|
||||
.show();
|
||||
});
|
||||
let layout_size = ui.available_size();
|
||||
ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| {
|
||||
ui.add_space(12.0);
|
||||
ui.vertical(|ui| {
|
||||
ui.add_space(4.0);
|
||||
View::ellipsize_text(ui,
|
||||
t!("lang_name", locale = locale),
|
||||
18.0,
|
||||
Colors::title(false));
|
||||
ui.add_space(1.0);
|
||||
let value = format!("{} {}",
|
||||
TRANSLATE,
|
||||
t!("language"));
|
||||
ui.label(RichText::new(value).size(15.0).color(Colors::gray()));
|
||||
ui.add_space(3.0);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// Draw button to select language.
|
||||
let is_current = self.locale == locale;
|
||||
if !is_current {
|
||||
View::item_button(ui, View::item_rounding(index, len, true), CHECK, None, || {
|
||||
rust_i18n::set_locale(locale);
|
||||
AppConfig::save_locale(locale);
|
||||
self.locale = locale.to_string();
|
||||
Modal::close();
|
||||
});
|
||||
} else {
|
||||
let res = ui.scope_builder(
|
||||
UiBuilder::new()
|
||||
.sense(Sense::click())
|
||||
.layout(Layout::right_to_left(Align::Center))
|
||||
.max_rect(rect), |ui| {
|
||||
if is_current {
|
||||
View::selected_item_check(ui);
|
||||
}
|
||||
|
||||
let layout_size = ui.available_size();
|
||||
ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| {
|
||||
ui.add_space(12.0);
|
||||
@@ -195,10 +118,23 @@ impl InterfaceSettingsContent {
|
||||
ui.label(RichText::new(t!("lang_name", locale = locale))
|
||||
.size(17.0)
|
||||
.color(color));
|
||||
ui.add_space(3.0);
|
||||
ui.add_space(14.0);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
).response;
|
||||
let clicked = res.clicked() || res.long_touched();
|
||||
// Setup background and cursor.
|
||||
if res.hovered() && !is_current {
|
||||
res.on_hover_cursor(CursorIcon::PointingHand);
|
||||
bg_shape.fill = Colors::fill();
|
||||
}
|
||||
ui.painter().set(bg_idx, bg_shape);
|
||||
// Handle clicks on layout.
|
||||
if clicked && !is_current {
|
||||
rust_i18n::set_locale(locale);
|
||||
AppConfig::save_locale(locale);
|
||||
self.locale = locale.to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,10 +12,11 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use egui::{Align, Id, Layout, RichText, StrokeKind};
|
||||
use eframe::epaint::RectShape;
|
||||
use egui::{Align, CursorIcon, Id, Layout, RichText, Sense, StrokeKind, UiBuilder};
|
||||
use url::Url;
|
||||
|
||||
use crate::gui::icons::{CLOUD_CHECK, CLOUD_SLASH, PENCIL};
|
||||
use crate::gui::icons::{CLOUD_CHECK, CLOUD_SLASH};
|
||||
use crate::gui::platform::PlatformCallbacks;
|
||||
use crate::gui::views::types::{ContentContainer, ModalPosition};
|
||||
use crate::gui::views::{Modal, TextEdit, View};
|
||||
@@ -202,63 +203,71 @@ impl NetworkSettingsContent {
|
||||
|
||||
/// Draw proxy item content.
|
||||
fn proxy_item_ui(&mut self, ui: &mut egui::Ui) {
|
||||
// Setup layout size.
|
||||
// Draw round background.
|
||||
let mut rect = ui.available_rect_before_wrap();
|
||||
rect.set_height(56.0);
|
||||
let r = View::item_rounding(0, 1, false);
|
||||
let bg = Colors::fill_lite();
|
||||
let mut bg_shape = RectShape::new(rect, r, bg, View::item_stroke(), StrokeKind::Outside);
|
||||
let bg_idx = ui.painter().add(bg_shape.clone());
|
||||
|
||||
// Draw round background.
|
||||
let bg_rect = rect.clone();
|
||||
let item_rounding = View::item_rounding(0, 1, false);
|
||||
ui.painter().rect(bg_rect,
|
||||
item_rounding,
|
||||
Colors::fill(),
|
||||
View::item_stroke(),
|
||||
StrokeKind::Outside);
|
||||
let res = ui.scope_builder(
|
||||
UiBuilder::new()
|
||||
.sense(Sense::click())
|
||||
.layout(Layout::right_to_left(Align::Center))
|
||||
.max_rect(rect), |ui| {
|
||||
let layout_size = ui.available_size();
|
||||
ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| {
|
||||
ui.add_space(12.0);
|
||||
ui.vertical(|ui| {
|
||||
ui.add_space(4.0);
|
||||
let use_socks = AppConfig::use_socks_proxy();
|
||||
let proxy_url = if use_socks {
|
||||
AppConfig::socks_proxy_url()
|
||||
} else {
|
||||
AppConfig::http_proxy_url()
|
||||
};
|
||||
let (url, color, icon, text) = if let Some(url) = proxy_url {
|
||||
(url, Colors::title(false), CLOUD_CHECK, t!("network_settings.enabled"))
|
||||
} else {
|
||||
(
|
||||
t!("enter_url").into(),
|
||||
Colors::inactive_text(),
|
||||
CLOUD_SLASH,
|
||||
t!("network_settings.disabled")
|
||||
)
|
||||
};
|
||||
View::ellipsize_text(ui, url, 18.0, color);
|
||||
ui.add_space(1.0);
|
||||
|
||||
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
|
||||
View::item_button(ui, View::item_rounding(0, 1, true), PENCIL, None, || {
|
||||
let url = if AppConfig::use_socks_proxy() {
|
||||
AppConfig::socks_proxy_url().unwrap_or("".to_string())
|
||||
} else {
|
||||
AppConfig::http_proxy_url().unwrap_or("".to_string())
|
||||
};
|
||||
self.proxy_url_edit = url;
|
||||
// Show proxy URL edit modal.
|
||||
Modal::new(PROXY_URL_EDIT_MODAL)
|
||||
.position(ModalPosition::CenterTop)
|
||||
.title(t!("app_settings.proxy"))
|
||||
.show();
|
||||
});
|
||||
let layout_size = ui.available_size();
|
||||
ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| {
|
||||
ui.add_space(12.0);
|
||||
ui.vertical(|ui| {
|
||||
ui.add_space(4.0);
|
||||
let use_socks = AppConfig::use_socks_proxy();
|
||||
let proxy_url = if use_socks {
|
||||
AppConfig::socks_proxy_url()
|
||||
} else {
|
||||
AppConfig::http_proxy_url()
|
||||
};
|
||||
let (url, color, icon, text) = if let Some(url) = proxy_url {
|
||||
(url, Colors::title(false), CLOUD_CHECK, t!("network_settings.enabled"))
|
||||
} else {
|
||||
(
|
||||
t!("enter_url").into(),
|
||||
Colors::inactive_text(),
|
||||
CLOUD_SLASH,
|
||||
t!("network_settings.disabled")
|
||||
)
|
||||
};
|
||||
View::ellipsize_text(ui, url, 18.0, color);
|
||||
ui.add_space(1.0);
|
||||
|
||||
let value = format!("{} {}", icon, text);
|
||||
ui.label(RichText::new(value).size(15.0).color(Colors::gray()));
|
||||
ui.add_space(3.0);
|
||||
let value = format!("{} {}", icon, text);
|
||||
ui.label(RichText::new(value).size(15.0).color(Colors::gray()));
|
||||
ui.add_space(3.0);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
).response;
|
||||
let clicked = res.clicked() || res.long_touched();
|
||||
// Setup background and cursor.
|
||||
if res.hovered() {
|
||||
res.on_hover_cursor(CursorIcon::PointingHand);
|
||||
bg_shape.fill = Colors::fill();
|
||||
}
|
||||
ui.painter().set(bg_idx, bg_shape);
|
||||
// Handle clicks on layout.
|
||||
if clicked {
|
||||
let url = if AppConfig::use_socks_proxy() {
|
||||
AppConfig::socks_proxy_url().unwrap_or("".to_string())
|
||||
} else {
|
||||
AppConfig::http_proxy_url().unwrap_or("".to_string())
|
||||
};
|
||||
self.proxy_url_edit = url;
|
||||
// Show proxy URL edit modal.
|
||||
Modal::new(PROXY_URL_EDIT_MODAL)
|
||||
.position(ModalPosition::CenterTop)
|
||||
.title(t!("app_settings.proxy"))
|
||||
.show();
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw proxy type selection.
|
||||
|
||||
+216
-157
@@ -12,12 +12,13 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use eframe::epaint::RectShape;
|
||||
use egui::scroll_area::ScrollBarVisibility;
|
||||
use egui::{Align, CursorIcon, Id, Layout, RichText, ScrollArea, Sense, StrokeKind, UiBuilder};
|
||||
use std::fs;
|
||||
use egui::{Align, Id, Layout, RichText, StrokeKind};
|
||||
use egui::os::OperatingSystem;
|
||||
use url::Url;
|
||||
|
||||
use crate::gui::icons::{CLOUD_CHECK, NOTCHES, PENCIL, SCAN, TERMINAL};
|
||||
use crate::gui::icons::{CLIPBOARD_TEXT, CLOUD_CHECK, NOTCHES, PENCIL, SCAN, TERMINAL};
|
||||
use crate::gui::platform::PlatformCallbacks;
|
||||
use crate::gui::views::types::{ContentContainer, ModalPosition};
|
||||
use crate::gui::views::{CameraScanContent, FilePickContent, FilePickContentType, Modal, TextEdit, View};
|
||||
@@ -178,7 +179,6 @@ impl ContentContainer for TorSettingsContent {
|
||||
|
||||
// Draw checkbox to enable/disable bridges.
|
||||
View::checkbox(ui, bridge.is_some(), t!("transport.bridges"), || {
|
||||
// Save value.
|
||||
let value = if bridge.is_some() {
|
||||
None
|
||||
} else {
|
||||
@@ -194,9 +194,8 @@ impl ContentContainer for TorSettingsContent {
|
||||
|
||||
if bridge.is_some() {
|
||||
ui.add_space(6.0);
|
||||
// Show bridge selection for non-Android.
|
||||
let is_android = OperatingSystem::from_target_os() == OperatingSystem::Android;
|
||||
if !is_android {
|
||||
// Show bridge selection for desktop.
|
||||
if View::is_desktop() {
|
||||
let current_bridge = bridge.unwrap();
|
||||
let mut bridge = current_bridge.clone();
|
||||
|
||||
@@ -234,13 +233,12 @@ impl ContentContainer for TorSettingsContent {
|
||||
}
|
||||
|
||||
if let Some(br) = TorConfig::get_bridge().as_ref() {
|
||||
// Show bridge binary setup for non-Android.
|
||||
if !is_android {
|
||||
self.bridge_bin_ui(ui, br, cb);
|
||||
ui.add_space(10.0);
|
||||
}
|
||||
// Show bridge connection line setup.
|
||||
self.bridge_conn_line_ui(ui, br, cb);
|
||||
self.conn_line_ui(ui, br, cb);
|
||||
// Show bridge binary setup for desktop.
|
||||
if View::is_desktop() {
|
||||
self.bridge_bin_ui(ui, br, cb);
|
||||
}
|
||||
}
|
||||
|
||||
ui.add_space(8.0);
|
||||
@@ -361,153 +359,61 @@ impl TorSettingsContent {
|
||||
});
|
||||
}
|
||||
|
||||
/// Draw bridge binary setup content.
|
||||
fn bridge_bin_ui(&mut self, ui: &mut egui::Ui, bridge: &TorBridge, cb: &dyn PlatformCallbacks) {
|
||||
// Setup layout size.
|
||||
let mut rect = ui.available_rect_before_wrap();
|
||||
rect.set_height(56.0);
|
||||
|
||||
// Draw round background.
|
||||
let bg_rect = rect.clone();
|
||||
let item_rounding = View::item_rounding(0, 1, false);
|
||||
ui.painter().rect(bg_rect,
|
||||
item_rounding,
|
||||
Colors::fill(),
|
||||
View::item_stroke(),
|
||||
StrokeKind::Outside);
|
||||
|
||||
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
|
||||
self.bridge_bin_pick_file.ui(ui, cb, |path| {
|
||||
if bridge.binary_path() != path {
|
||||
TorBridge::save_bridge_bin_path(bridge, path);
|
||||
self.settings_changed = true;
|
||||
}
|
||||
});
|
||||
View::item_button(ui, View::item_rounding(1, 3, true), PENCIL, None, || {
|
||||
self.bridge_bin_path_edit = bridge.binary_path();
|
||||
// Show binary path edit modal.
|
||||
let title = bridge.protocol_name();
|
||||
Modal::new(BRIDGE_BIN_EDIT_MODAL)
|
||||
.position(ModalPosition::CenterTop)
|
||||
.title(title)
|
||||
.show();
|
||||
});
|
||||
let layout_size = ui.available_size();
|
||||
ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| {
|
||||
ui.add_space(12.0);
|
||||
ui.vertical(|ui| {
|
||||
ui.add_space(4.0);
|
||||
View::ellipsize_text(ui, bridge.binary_path(), 18.0, Colors::title(false));
|
||||
ui.add_space(1.0);
|
||||
let value = format!("{} {}",
|
||||
TERMINAL,
|
||||
t!("transport.bin_file").replace(":", ""));
|
||||
ui.label(RichText::new(value).size(15.0).color(Colors::gray()));
|
||||
ui.add_space(3.0);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// Draw bridge binary input [`Modal`] content.
|
||||
fn bridge_bin_edit_modal_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||
let on_save = |c: &mut TorSettingsContent| {
|
||||
let bridge = TorConfig::get_bridge().unwrap();
|
||||
let exists = fs::exists(&c.bridge_bin_path_edit).unwrap_or_default();
|
||||
if !exists {
|
||||
return;
|
||||
}
|
||||
if bridge.binary_path() != c.bridge_bin_path_edit {
|
||||
TorBridge::save_bridge_bin_path(&bridge, c.bridge_bin_path_edit.clone());
|
||||
c.settings_changed = true;
|
||||
}
|
||||
Modal::close();
|
||||
};
|
||||
|
||||
ui.add_space(6.0);
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.label(RichText::new(t!("transport.bin_file"))
|
||||
.size(17.0)
|
||||
.color(Colors::gray()));
|
||||
ui.add_space(8.0);
|
||||
|
||||
// Draw p2p port text edit.
|
||||
let mut edit = TextEdit::new(Id::from(BRIDGE_BIN_EDIT_MODAL)).paste();
|
||||
edit.ui(ui, &mut self.bridge_bin_path_edit, cb);
|
||||
if edit.enter_pressed {
|
||||
on_save(self);
|
||||
}
|
||||
ui.add_space(12.0);
|
||||
|
||||
// Show modal buttons.
|
||||
ui.scope(|ui| {
|
||||
// Setup spacing between buttons.
|
||||
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
|
||||
|
||||
ui.columns(2, |columns| {
|
||||
columns[0].vertical_centered_justified(|ui| {
|
||||
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
|
||||
// Close modal.
|
||||
Modal::close();
|
||||
});
|
||||
});
|
||||
columns[1].vertical_centered_justified(|ui| {
|
||||
View::button(ui, t!("modal.save"), Colors::white_or_black(false), || {
|
||||
on_save(self);
|
||||
});
|
||||
});
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// Draw bridge connection line setup content.
|
||||
fn bridge_conn_line_ui(&mut self,
|
||||
ui: &mut egui::Ui,
|
||||
bridge: &TorBridge,
|
||||
cb: &dyn PlatformCallbacks) {
|
||||
// Setup layout size.
|
||||
fn conn_line_ui(&mut self, ui: &mut egui::Ui, bridge: &TorBridge, cb: &dyn PlatformCallbacks) {
|
||||
// Draw round background.
|
||||
let mut rect = ui.available_rect_before_wrap();
|
||||
rect.set_height(56.0);
|
||||
let r = if View::is_desktop() {
|
||||
View::item_rounding(0, 2, false)
|
||||
} else {
|
||||
View::item_rounding(0, 1, false)
|
||||
};
|
||||
let bg = Colors::fill_lite();
|
||||
let mut bg_shape = RectShape::new(rect, r, bg, View::item_stroke(), StrokeKind::Outside);
|
||||
let bg_idx = ui.painter().add(bg_shape.clone());
|
||||
|
||||
// Draw round background.
|
||||
let bg_rect = rect.clone();
|
||||
let item_rounding = View::item_rounding(0, 1, false);
|
||||
ui.painter().rect(bg_rect,
|
||||
item_rounding,
|
||||
Colors::fill(),
|
||||
View::item_stroke(),
|
||||
StrokeKind::Outside);
|
||||
|
||||
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
|
||||
View::item_button(ui, View::item_rounding(0, 1, true), SCAN, None, || {
|
||||
self.show_qr_scan_bridge_modal(cb);
|
||||
});
|
||||
View::item_button(ui, View::item_rounding(1, 3 , true), PENCIL, None, || {
|
||||
self.bridge_conn_line_edit = bridge.connection_line();
|
||||
// Show connection line edit modal.
|
||||
let title = bridge.protocol_name();
|
||||
Modal::new(BRIDGE_CONN_LINE_EDIT_MODAL)
|
||||
.position(ModalPosition::CenterTop)
|
||||
.title(title)
|
||||
.show();
|
||||
});
|
||||
let layout_size = ui.available_size();
|
||||
ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| {
|
||||
ui.add_space(12.0);
|
||||
ui.vertical(|ui| {
|
||||
ui.add_space(4.0);
|
||||
View::ellipsize_text(ui, bridge.connection_line(), 18.0, Colors::title(false));
|
||||
ui.add_space(1.0);
|
||||
let value = format!("{} {}",
|
||||
NOTCHES,
|
||||
t!("transport.conn_line").replace(":", ""));
|
||||
ui.label(RichText::new(value).size(15.0).color(Colors::gray()));
|
||||
ui.add_space(3.0);
|
||||
let res = ui.scope_builder(
|
||||
UiBuilder::new()
|
||||
.sense(Sense::click())
|
||||
.layout(Layout::right_to_left(Align::Center))
|
||||
.max_rect(rect), |ui| {
|
||||
View::item_button(ui, View::item_rounding(0, 1, true), SCAN, None, || {
|
||||
self.show_qr_scan_bridge_modal(cb);
|
||||
});
|
||||
});
|
||||
});
|
||||
let layout_size = ui.available_size();
|
||||
ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| {
|
||||
ui.add_space(12.0);
|
||||
ui.vertical(|ui| {
|
||||
ui.add_space(4.0);
|
||||
let line_text = bridge.connection_line();
|
||||
View::ellipsize_text(ui, line_text, 18.0, Colors::title(false));
|
||||
ui.add_space(1.0);
|
||||
let line_desc = t!("transport.conn_line").replace(":", "");
|
||||
let value = format!("{} {}", NOTCHES, line_desc);
|
||||
ui.label(RichText::new(value).size(15.0).color(Colors::gray()));
|
||||
ui.add_space(3.0);
|
||||
});
|
||||
});
|
||||
}
|
||||
).response;
|
||||
let clicked = res.clicked() || res.long_touched();
|
||||
// Setup background and cursor.
|
||||
if res.hovered() {
|
||||
res.on_hover_cursor(CursorIcon::PointingHand);
|
||||
bg_shape.fill = Colors::fill();
|
||||
}
|
||||
ui.painter().set(bg_idx, bg_shape);
|
||||
// Handle clicks on layout.
|
||||
if clicked {
|
||||
self.bridge_conn_line_edit = bridge.connection_line();
|
||||
// Show connection line edit modal.
|
||||
let title = bridge.protocol_name();
|
||||
Modal::new(BRIDGE_CONN_LINE_EDIT_MODAL)
|
||||
.position(ModalPosition::Center)
|
||||
.title(title)
|
||||
.show();
|
||||
}
|
||||
}
|
||||
|
||||
/// Show bridge connection line QR code scanner.
|
||||
@@ -541,8 +447,161 @@ impl TorSettingsContent {
|
||||
ui.add_space(8.0);
|
||||
|
||||
// Draw connection line text edit.
|
||||
let mut edit = TextEdit::new(Id::from(BRIDGE_CONN_LINE_EDIT_MODAL)).paste();
|
||||
edit.ui(ui, &mut self.bridge_conn_line_edit, cb);
|
||||
ui.vertical_centered(|ui| {
|
||||
let scroll_id = Id::from(BRIDGE_CONN_LINE_EDIT_MODAL);
|
||||
View::horizontal_line(ui, Colors::item_stroke());
|
||||
ui.add_space(3.0);
|
||||
ScrollArea::both()
|
||||
.id_salt(scroll_id)
|
||||
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
|
||||
.max_height(128.0)
|
||||
.auto_shrink([false; 2])
|
||||
.show(ui, |ui| {
|
||||
ui.add_space(7.0);
|
||||
let input_id = scroll_id.with("_input");
|
||||
egui::TextEdit::multiline(&mut self.bridge_conn_line_edit)
|
||||
.id(input_id)
|
||||
.font(egui::TextStyle::Body)
|
||||
.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(8.0);
|
||||
|
||||
// Setup spacing between buttons.
|
||||
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
|
||||
|
||||
ui.columns(2, |columns| {
|
||||
columns[0].vertical_centered_justified(|ui| {
|
||||
// Draw paste button.
|
||||
let paste_text = format!("{} {}", CLIPBOARD_TEXT, t!("paste"));
|
||||
View::button(ui, paste_text, Colors::white_or_black(false), || {
|
||||
self.bridge_conn_line_edit = cb.get_string_from_buffer();
|
||||
});
|
||||
});
|
||||
columns[1].vertical_centered_justified(|ui| {
|
||||
// Draw button to scan bridge QR code.
|
||||
let scan_text = format!("{} {}", SCAN, t!("scan"));
|
||||
View::button(ui, scan_text, Colors::white_or_black(false), || {
|
||||
self.show_qr_scan_bridge_modal(cb);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
ui.add_space(8.0);
|
||||
View::horizontal_line(ui, Colors::item_stroke());
|
||||
ui.add_space(8.0);
|
||||
|
||||
// Show modal buttons.
|
||||
ui.scope(|ui| {
|
||||
// Setup spacing between buttons.
|
||||
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
|
||||
|
||||
ui.columns(2, |columns| {
|
||||
columns[0].vertical_centered_justified(|ui| {
|
||||
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
|
||||
// Close modal.
|
||||
Modal::close();
|
||||
});
|
||||
});
|
||||
columns[1].vertical_centered_justified(|ui| {
|
||||
View::button(ui, t!("modal.save"), Colors::white_or_black(false), || {
|
||||
on_save(self);
|
||||
});
|
||||
});
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// Draw bridge binary setup content.
|
||||
fn bridge_bin_ui(&mut self, ui: &mut egui::Ui, bridge: &TorBridge, cb: &dyn PlatformCallbacks) {
|
||||
// Draw round background.
|
||||
let mut rect = ui.available_rect_before_wrap();
|
||||
rect.set_height(56.0);
|
||||
let r = View::item_rounding(1, 2, false);
|
||||
let bg = Colors::fill_lite();
|
||||
let mut bg_shape = RectShape::new(rect, r, bg, View::item_stroke(), StrokeKind::Outside);
|
||||
let bg_idx = ui.painter().add(bg_shape.clone());
|
||||
|
||||
let res = ui.scope_builder(
|
||||
UiBuilder::new()
|
||||
.sense(Sense::click())
|
||||
.layout(Layout::right_to_left(Align::Center))
|
||||
.max_rect(rect), |ui| {
|
||||
self.bridge_bin_pick_file.ui(ui, cb, |path| {
|
||||
if bridge.binary_path() != path {
|
||||
TorBridge::save_bridge_bin_path(bridge, path);
|
||||
self.settings_changed = true;
|
||||
}
|
||||
});
|
||||
let layout_size = ui.available_size();
|
||||
ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| {
|
||||
ui.add_space(12.0);
|
||||
ui.vertical(|ui| {
|
||||
ui.add_space(4.0);
|
||||
let bin_text = bridge.binary_path();
|
||||
View::ellipsize_text(ui, bin_text, 18.0, Colors::title(false));
|
||||
ui.add_space(1.0);
|
||||
let bin_desc = t!("transport.bin_file").replace(":", "");
|
||||
let value = format!("{} {}", TERMINAL, bin_desc);
|
||||
ui.label(RichText::new(value).size(15.0).color(Colors::gray()));
|
||||
ui.add_space(3.0);
|
||||
});
|
||||
});
|
||||
}
|
||||
).response;
|
||||
let clicked = res.clicked() || res.long_touched();
|
||||
// Setup background and cursor.
|
||||
if res.hovered() {
|
||||
res.on_hover_cursor(CursorIcon::PointingHand);
|
||||
bg_shape.fill = Colors::fill();
|
||||
}
|
||||
ui.painter().set(bg_idx, bg_shape);
|
||||
// Handle clicks on layout.
|
||||
if clicked {
|
||||
self.bridge_bin_path_edit = bridge.binary_path();
|
||||
// Show binary path edit modal.
|
||||
let title = bridge.protocol_name();
|
||||
Modal::new(BRIDGE_BIN_EDIT_MODAL)
|
||||
.position(ModalPosition::CenterTop)
|
||||
.title(title)
|
||||
.show();
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw bridge binary input [`Modal`] content.
|
||||
fn bridge_bin_edit_modal_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||
let on_save = |c: &mut TorSettingsContent| {
|
||||
let bridge = TorConfig::get_bridge().unwrap();
|
||||
let exists = fs::exists(&c.bridge_bin_path_edit).unwrap_or_default();
|
||||
if !exists {
|
||||
return;
|
||||
}
|
||||
if bridge.binary_path() != c.bridge_bin_path_edit {
|
||||
TorBridge::save_bridge_bin_path(&bridge, c.bridge_bin_path_edit.clone());
|
||||
c.settings_changed = true;
|
||||
}
|
||||
Modal::close();
|
||||
};
|
||||
|
||||
ui.add_space(6.0);
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.label(RichText::new(t!("transport.bin_file"))
|
||||
.size(17.0)
|
||||
.color(Colors::gray()));
|
||||
ui.add_space(8.0);
|
||||
|
||||
// Draw bridge text edit.
|
||||
let mut edit = TextEdit::new(Id::from(BRIDGE_BIN_EDIT_MODAL)).paste();
|
||||
edit.ui(ui, &mut self.bridge_bin_path_edit, cb);
|
||||
if edit.enter_pressed {
|
||||
on_save(self);
|
||||
}
|
||||
|
||||
@@ -186,6 +186,8 @@ impl View {
|
||||
// Setup padding for title buttons.
|
||||
if !View::is_desktop() {
|
||||
ui.style_mut().spacing.button_padding = egui::vec2(20.0, 8.0);
|
||||
} else {
|
||||
ui.style_mut().spacing.button_padding = egui::vec2(16.0, 8.0);
|
||||
}
|
||||
// Disable strokes.
|
||||
ui.style_mut().visuals.widgets.inactive.bg_stroke = Stroke::NONE;
|
||||
@@ -358,7 +360,7 @@ impl View {
|
||||
ui.scope(|ui| {
|
||||
// Setup padding for item buttons.
|
||||
let padding = if Self::is_desktop() {
|
||||
14.0
|
||||
15.0
|
||||
} else {
|
||||
18.0
|
||||
};
|
||||
@@ -456,7 +458,7 @@ impl View {
|
||||
ne: if r[1] { 8.0 as u8 } else { 0.0 as u8 },
|
||||
sw: if r[2] { 8.0 as u8 } else { 0.0 as u8 },
|
||||
se: if r[3] { 8.0 as u8 } else { 0.0 as u8 },
|
||||
}, Colors::fill_lite(), Self::item_stroke(), StrokeKind::Outside);
|
||||
}, Colors::fill(), Self::item_stroke(), StrokeKind::Outside);
|
||||
let bg_idx = ui.painter().add(bg_shape.clone());
|
||||
|
||||
// Draw box content.
|
||||
@@ -707,7 +709,7 @@ lazy_static! {
|
||||
#[allow(dead_code)]
|
||||
#[cfg(target_os = "android")]
|
||||
#[allow(non_snake_case)]
|
||||
#[no_mangle]
|
||||
#[unsafe(no_mangle)]
|
||||
/// Callback from Java code to update display insets (cutouts).
|
||||
pub extern "C" fn Java_mw_gri_android_MainActivity_onDisplayInsets(
|
||||
_env: jni::JNIEnv,
|
||||
|
||||
@@ -12,27 +12,29 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::time::Duration;
|
||||
use egui::scroll_area::ScrollBarVisibility;
|
||||
use egui::{Align, CornerRadius, Id, Layout, Margin, OpenUrl, RichText, ScrollArea, StrokeKind};
|
||||
use egui::os::OperatingSystem;
|
||||
use egui::scroll_area::ScrollBarVisibility;
|
||||
use egui::{Align, CornerRadius, CursorIcon, Id, Layout, Margin, OpenUrl, RichText, ScrollArea, Sense, StrokeKind, UiBuilder};
|
||||
use egui_async::Bind;
|
||||
use crate::gui::icons::{ARROW_LEFT, BOOKMARKS, CALENDAR_CHECK, CARET_RIGHT, CLOUD_ARROW_DOWN, COMPUTER_TOWER, FOLDER_OPEN, FOLDER_PLUS, GEAR, GEAR_FINE, GLOBE, GLOBE_SIMPLE, LOCK_KEY, NOTEPAD, PLUS, SIDEBAR_SIMPLE, SUITCASE};
|
||||
use std::time::Duration;
|
||||
use eframe::epaint::RectShape;
|
||||
|
||||
use crate::gui::icons::{ARROW_LEFT, BOOKMARKS, CALENDAR_CHECK, CLOUD_ARROW_DOWN, COMPUTER_TOWER, FOLDER_PLUS, GEAR, GEAR_FINE, GLOBE, GLOBE_SIMPLE, LOCK_KEY, NOTEPAD, PLUS, SIDEBAR_SIMPLE, SUITCASE};
|
||||
use crate::gui::platform::PlatformCallbacks;
|
||||
use crate::gui::views::settings::SettingsContent;
|
||||
use crate::gui::views::types::{ContentContainer, LinePosition, ModalPosition, TitleContentType, TitleType};
|
||||
use crate::gui::views::wallets::creation::WalletCreationContent;
|
||||
use crate::gui::views::wallets::modals::{AddWalletModal, OpenWalletModal, WalletSettingsModal, WalletListModal, ChangelogContent};
|
||||
use crate::gui::views::wallets::modals::{AddWalletModal, ChangelogContent, OpenWalletModal, WalletListModal, WalletSettingsModal};
|
||||
use crate::gui::views::wallets::wallet::types::{wallet_status_text, WalletContentContainer};
|
||||
use crate::gui::views::wallets::wallet::RecoverySettings;
|
||||
use crate::gui::views::wallets::WalletContent;
|
||||
use crate::gui::views::{Content, Modal, TitlePanel, View};
|
||||
use crate::gui::Colors;
|
||||
use crate::http::{retrieve_release, ReleaseInfo};
|
||||
use crate::settings::AppUpdate;
|
||||
use crate::wallet::types::{ConnectionMethod, WalletTask};
|
||||
use crate::wallet::{Wallet, WalletList};
|
||||
use crate::AppConfig;
|
||||
use crate::gui::views::wallets::wallet::RecoverySettings;
|
||||
use crate::http::{retrieve_release, ReleaseInfo};
|
||||
use crate::settings::AppUpdate;
|
||||
|
||||
/// Wallets content.
|
||||
pub struct WalletsContent {
|
||||
@@ -160,9 +162,9 @@ impl ContentContainer for WalletsContent {
|
||||
}
|
||||
|
||||
fn container_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||
let is_android = OperatingSystem::from_target_os() == OperatingSystem::Android;
|
||||
let account_list_showing = self.wallet_content.account_content.list_content.is_some();
|
||||
// Small repaint delay is needed for Android back navigation and account list opening.
|
||||
let is_android = OperatingSystem::from_target_os() == OperatingSystem::Android;
|
||||
let account_list_showing = self.wallet_content.account_content.show_list;
|
||||
ui.ctx().request_repaint_after(Duration::from_millis(if account_list_showing {
|
||||
10
|
||||
} else if is_android {
|
||||
@@ -444,7 +446,12 @@ impl WalletsContent {
|
||||
|| (dual_panel && !show_list)) && !creating_wallet && !showing_settings {
|
||||
let title = self.wallet_content.title().into();
|
||||
let subtitle = self.wallets.selected().unwrap().get_config().name;
|
||||
TitleType::Single(TitleContentType::WithSubTitle(title, subtitle, false))
|
||||
let wallet_title_content = if self.wallet_content.settings_content.is_some() {
|
||||
TitleContentType::Title(title)
|
||||
} else {
|
||||
TitleContentType::WithSubTitle(title, subtitle, false)
|
||||
};
|
||||
TitleType::Single(wallet_title_content)
|
||||
} else {
|
||||
let title_text = if showing_settings {
|
||||
t!("settings")
|
||||
@@ -458,7 +465,11 @@ impl WalletsContent {
|
||||
if dual_title {
|
||||
let title = self.wallet_content.title().into();
|
||||
let subtitle = self.wallets.selected().unwrap().get_config().name;
|
||||
let wallet_title_content = TitleContentType::WithSubTitle(title, subtitle, false);
|
||||
let wallet_title_content = if self.wallet_content.settings_content.is_some() {
|
||||
TitleContentType::Title(title)
|
||||
} else {
|
||||
TitleContentType::WithSubTitle(title, subtitle, false)
|
||||
};
|
||||
TitleType::Dual(TitleContentType::Title(title_text), wallet_title_content)
|
||||
} else {
|
||||
TitleType::Single(TitleContentType::Title(title_text))
|
||||
@@ -516,6 +527,7 @@ impl WalletsContent {
|
||||
}
|
||||
}, ui);
|
||||
if show_settings {
|
||||
self.wallet_content.back(cb);
|
||||
self.settings_content = Some(SettingsContent::default());
|
||||
}
|
||||
}
|
||||
@@ -578,8 +590,12 @@ impl WalletsContent {
|
||||
} else {
|
||||
false
|
||||
};
|
||||
// Unselect wallet when opening or settings modal was closed.
|
||||
if current && !w.is_open() && Modal::opened().is_none() {
|
||||
self.wallets.select(None);
|
||||
}
|
||||
self.wallet_item_ui(ui, w, current, cb);
|
||||
ui.add_space(5.0);
|
||||
ui.add_space(6.0);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -592,88 +608,99 @@ impl WalletsContent {
|
||||
current: bool,
|
||||
cb: &dyn PlatformCallbacks) {
|
||||
let config = wallet.get_config();
|
||||
let can_open = !wallet.is_open() && !wallet.files_moving();
|
||||
|
||||
// Draw round background.
|
||||
let mut rect = ui.available_rect_before_wrap();
|
||||
rect.set_height(78.0);
|
||||
let rounding = View::item_rounding(0, 1, false);
|
||||
let r = View::item_rounding(0, 1, false);
|
||||
let bg = if current {
|
||||
Colors::fill_deep()
|
||||
} else {
|
||||
Colors::fill()
|
||||
};
|
||||
ui.painter().rect(rect, rounding, bg, View::item_stroke(), StrokeKind::Outside);
|
||||
let mut bg_shape = RectShape::new(rect, r, bg, View::item_stroke(), StrokeKind::Outside);
|
||||
let bg_idx = ui.painter().add(bg_shape.clone());
|
||||
|
||||
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
|
||||
if !wallet.is_open() && !wallet.files_moving() {
|
||||
// Show button to open closed wallet.
|
||||
View::item_button(ui, View::item_rounding(0, 1, true), FOLDER_OPEN, None, || {
|
||||
self.show_opening_modal(wallet, None, cb);
|
||||
});
|
||||
if !wallet.is_repairing() {
|
||||
View::item_button(ui, CornerRadius::default(), GEAR_FINE, None, || {
|
||||
self.select_wallet(wallet, None, cb);
|
||||
let conn = wallet.get_current_connection();
|
||||
self.wallet_settings_content = WalletSettingsModal::new(conn);
|
||||
// Show connection selection modal.
|
||||
Modal::new(WALLET_SETTINGS_MODAL)
|
||||
.position(ModalPosition::CenterTop)
|
||||
.title(t!("wallets.settings"))
|
||||
.show();
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if !current {
|
||||
// Show button to select opened wallet.
|
||||
View::item_button(ui, View::item_rounding(0, 1, true), CARET_RIGHT, None, || {
|
||||
self.select_wallet(wallet, None, cb);
|
||||
});
|
||||
}
|
||||
// Show button to close opened wallet.
|
||||
if !wallet.is_closing() {
|
||||
View::item_button(ui, if !current {
|
||||
CornerRadius::default()
|
||||
} else {
|
||||
View::item_rounding(0, 1, true)
|
||||
}, LOCK_KEY, None, || {
|
||||
let res = ui.scope_builder(
|
||||
UiBuilder::new()
|
||||
.sense(Sense::click())
|
||||
.layout(Layout::right_to_left(Align::Center))
|
||||
.max_rect(rect), |ui| {
|
||||
if can_open {
|
||||
if !wallet.is_repairing() {
|
||||
View::item_button(ui, View::item_rounding(0, 1, true), GEAR_FINE, None, || {
|
||||
self.select_wallet(wallet, None, cb);
|
||||
let conn = wallet.get_current_connection();
|
||||
self.wallet_settings_content = WalletSettingsModal::new(conn);
|
||||
// Show connection selection modal.
|
||||
Modal::new(WALLET_SETTINGS_MODAL)
|
||||
.position(ModalPosition::CenterTop)
|
||||
.title(t!("wallets.settings"))
|
||||
.show();
|
||||
});
|
||||
}
|
||||
} else if !wallet.is_closing() {
|
||||
// Show button to close opened wallet.
|
||||
View::item_button(ui, View::item_rounding(0, 1, true), LOCK_KEY, None, || {
|
||||
wallet.close();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let layout_size = ui.available_size();
|
||||
ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| {
|
||||
ui.add_space(6.0);
|
||||
ui.vertical(|ui| {
|
||||
ui.add_space(3.0);
|
||||
// Show wallet name text.
|
||||
let name_color = if current {
|
||||
Colors::white_or_black(true)
|
||||
} else {
|
||||
Colors::title(false)
|
||||
};
|
||||
ui.with_layout(Layout::left_to_right(Align::Min), |ui| {
|
||||
let layout_size = ui.available_size();
|
||||
ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| {
|
||||
ui.add_space(6.0);
|
||||
ui.vertical(|ui| {
|
||||
ui.add_space(3.0);
|
||||
// Show wallet name text.
|
||||
let name_color = if current {
|
||||
Colors::white_or_black(true)
|
||||
} else {
|
||||
Colors::title(false)
|
||||
};
|
||||
ui.with_layout(Layout::left_to_right(Align::Min), |ui| {
|
||||
ui.add_space(1.0);
|
||||
View::ellipsize_text(ui, config.name, 18.0, name_color);
|
||||
});
|
||||
|
||||
// Show wallet status text.
|
||||
let status_text = wallet_status_text(wallet);
|
||||
View::ellipsize_text(ui, status_text, 15.0, Colors::text(false));
|
||||
ui.add_space(1.0);
|
||||
|
||||
// Show wallet connection text.
|
||||
let connection = wallet.get_current_connection();
|
||||
let conn_text = match connection {
|
||||
ConnectionMethod::Integrated => {
|
||||
format!("{} {}", COMPUTER_TOWER, t!("network.node"))
|
||||
}
|
||||
ConnectionMethod::External(_, url) => {
|
||||
format!("{} {}", GLOBE_SIMPLE, url)
|
||||
}
|
||||
};
|
||||
View::ellipsize_text(ui, conn_text, 15.0, Colors::gray());
|
||||
ui.add_space(3.0);
|
||||
});
|
||||
|
||||
// Show wallet status text.
|
||||
View::ellipsize_text(ui, wallet_status_text(wallet), 15.0, Colors::text(false));
|
||||
ui.add_space(1.0);
|
||||
|
||||
// Show wallet connection text.
|
||||
let connection = wallet.get_current_connection();
|
||||
let conn_text = match connection {
|
||||
ConnectionMethod::Integrated => {
|
||||
format!("{} {}", COMPUTER_TOWER, t!("network.node"))
|
||||
}
|
||||
ConnectionMethod::External(_, url) => format!("{} {}", GLOBE_SIMPLE, url)
|
||||
};
|
||||
View::ellipsize_text(ui, conn_text, 15.0, Colors::gray());
|
||||
ui.add_space(3.0);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
).response;
|
||||
let clicked = res.clicked() || res.long_touched();
|
||||
// Setup background and cursor.
|
||||
if res.hovered() && (can_open || !current) {
|
||||
res.on_hover_cursor(CursorIcon::PointingHand);
|
||||
bg_shape.fill = Colors::fill_deep();
|
||||
}
|
||||
ui.painter().set(bg_idx, bg_shape);
|
||||
// Handle clicks on layout.
|
||||
if clicked {
|
||||
if can_open {
|
||||
// Show modal to open the wallet.
|
||||
self.show_opening_modal(wallet, None, cb);
|
||||
} else if !current {
|
||||
// Select opened wallet.
|
||||
self.select_wallet(wallet, None, cb);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw update information content.
|
||||
@@ -753,7 +780,7 @@ impl WalletsContent {
|
||||
|
||||
/// Select wallet to make some actions on it.
|
||||
fn select_wallet(&mut self, w: &Wallet, data: Option<String>, cb: &dyn PlatformCallbacks) {
|
||||
self.wallet_content.account_content.close_qr_scan(cb);
|
||||
self.wallet_content.back(cb);
|
||||
if let Some(data) = data {
|
||||
w.task(WalletTask::OpenMessage(data));
|
||||
}
|
||||
|
||||
@@ -232,9 +232,7 @@ impl WalletCreationContent {
|
||||
columns[0].vertical_centered_justified(|ui| {
|
||||
match self.mnemonic_setup.mnemonic.mode() {
|
||||
PhraseMode::Generate => {
|
||||
let c_t = format!("{} {}",
|
||||
COPY,
|
||||
t!("copy").to_uppercase());
|
||||
let c_t = format!("{} {}", COPY, t!("copy"));
|
||||
View::button(ui, c_t, Colors::white_or_black(false), || {
|
||||
cb.copy_string_to_buffer(self.mnemonic_setup
|
||||
.mnemonic
|
||||
@@ -242,9 +240,7 @@ impl WalletCreationContent {
|
||||
});
|
||||
}
|
||||
PhraseMode::Import => {
|
||||
let p_t = format!("{} {}",
|
||||
CLIPBOARD_TEXT,
|
||||
t!("paste").to_uppercase());
|
||||
let p_t = format!("{} {}", CLIPBOARD_TEXT, t!("paste"));
|
||||
View::button(ui, p_t, Colors::white_or_black(false), || {
|
||||
let data = ZeroingString::from(cb.get_string_from_buffer());
|
||||
self.mnemonic_setup.mnemonic.import(&data);
|
||||
@@ -257,9 +253,7 @@ impl WalletCreationContent {
|
||||
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"));
|
||||
View::button(ui, scan_text, Colors::white_or_black(false), || {
|
||||
self.scan_modal_content = Some(CameraScanContent::default());
|
||||
// Show QR code scan modal.
|
||||
@@ -279,7 +273,7 @@ impl WalletCreationContent {
|
||||
if next {
|
||||
self.next_step_button_ui(ui, on_create);
|
||||
} else {
|
||||
let paste_text = format!("{} {}", CLIPBOARD_TEXT, t!("paste").to_uppercase());
|
||||
let paste_text = format!("{} {}", CLIPBOARD_TEXT, t!("paste"));
|
||||
View::button(ui, paste_text, Colors::white_or_black(false), || {
|
||||
let data = ZeroingString::from(cb.get_string_from_buffer());
|
||||
self.mnemonic_setup.mnemonic.import(&data);
|
||||
|
||||
@@ -61,8 +61,7 @@ impl AddWalletModal {
|
||||
|
||||
// Show wallet name text edit.
|
||||
let mut name_input = TextEdit::new(Id::from(modal.id).with("name"))
|
||||
.focus(Modal::first_draw());
|
||||
|
||||
.focus(false);
|
||||
name_input.ui(ui, &mut self.name_edit, cb);
|
||||
|
||||
ui.add_space(8.0);
|
||||
@@ -74,13 +73,13 @@ impl AddWalletModal {
|
||||
// Show wallet password text edit.
|
||||
let mut pass_input = TextEdit::new(Id::from(modal.id).with("pass"))
|
||||
.password()
|
||||
.focus(false);
|
||||
.focus(Modal::first_draw());
|
||||
if name_input.enter_pressed {
|
||||
pass_input.focus_request();
|
||||
}
|
||||
pass_input.ui(ui, &mut self.pass_edit, cb);
|
||||
if pass_input.enter_pressed {
|
||||
(on_next)(self);
|
||||
on_next(self);
|
||||
}
|
||||
ui.add_space(12.0);
|
||||
});
|
||||
@@ -99,7 +98,7 @@ impl AddWalletModal {
|
||||
});
|
||||
columns[1].vertical_centered_justified(|ui| {
|
||||
View::button(ui, t!("continue"), Colors::white_or_black(false), || {
|
||||
(on_next)(self);
|
||||
on_next(self);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
use egui::scroll_area::ScrollBarVisibility;
|
||||
use egui::{RichText, ScrollArea};
|
||||
|
||||
use crate::gui::icons::{CHECK, PLUS_CIRCLE, TRASH};
|
||||
use crate::gui::icons::{PLUS_CIRCLE, TRASH};
|
||||
use crate::gui::platform::PlatformCallbacks;
|
||||
use crate::gui::views::network::modals::ExternalConnectionModal;
|
||||
use crate::gui::views::network::ConnectionsContent;
|
||||
@@ -79,18 +79,20 @@ impl WalletSettingsModal {
|
||||
ui.add_space(2.0);
|
||||
|
||||
// Show integrated node selection.
|
||||
ConnectionsContent::integrated_node_item_ui(ui, |ui| {
|
||||
match self.conn {
|
||||
ConnectionMethod::Integrated => {
|
||||
View::selected_item_check(ui);
|
||||
}
|
||||
_ => {
|
||||
View::item_button(ui, View::item_rounding(0, 1, true), CHECK, None, || {
|
||||
on_select(ConnectionMethod::Integrated);
|
||||
Modal::close();
|
||||
});
|
||||
}
|
||||
let cur_integrated = self.conn == ConnectionMethod::Integrated;
|
||||
let bg = if cur_integrated {
|
||||
Colors::fill()
|
||||
} else {
|
||||
Colors::fill_lite()
|
||||
};
|
||||
ConnectionsContent::integrated_node_item_ui(ui, bg, (!cur_integrated, || {
|
||||
on_select(ConnectionMethod::Integrated);
|
||||
Modal::close();
|
||||
}), |ui| {
|
||||
if cur_integrated {
|
||||
View::selected_item_check(ui);
|
||||
}
|
||||
cur_integrated
|
||||
});
|
||||
|
||||
ui.add_space(8.0);
|
||||
@@ -109,29 +111,31 @@ impl WalletSettingsModal {
|
||||
|
||||
if !ext_conn_list.is_empty() {
|
||||
ui.add_space(8.0);
|
||||
for (index, conn) in ext_conn_list.iter().enumerate() {
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
let len = ext_conn_list.len();
|
||||
ConnectionsContent::ext_conn_item_ui(ui, conn, index, len, |ui| {
|
||||
let current_ext_conn = match self.conn {
|
||||
ConnectionMethod::Integrated => false,
|
||||
ConnectionMethod::External(id, _) => id == conn.id
|
||||
};
|
||||
if current_ext_conn {
|
||||
View::selected_item_check(ui);
|
||||
} else {
|
||||
let button_rounding = View::item_rounding(index, len, true);
|
||||
View::item_button(ui, button_rounding, CHECK, None, || {
|
||||
on_select(
|
||||
ConnectionMethod::External(conn.id, conn.url.clone())
|
||||
);
|
||||
Modal::close();
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
for (i, c) in ext_conn_list.iter().enumerate() {
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
let len = ext_conn_list.len();
|
||||
let is_current = match self.conn {
|
||||
ConnectionMethod::External(id, _) => id == c.id,
|
||||
_ => false
|
||||
};
|
||||
let bg = if is_current {
|
||||
Colors::fill()
|
||||
} else {
|
||||
Colors::fill_lite()
|
||||
};
|
||||
ConnectionsContent::ext_conn_item_ui(ui, bg, c, i, len, (!is_current, || {
|
||||
on_select(
|
||||
ConnectionMethod::External(c.id, c.url.clone())
|
||||
);
|
||||
Modal::close();
|
||||
}), |ui| {
|
||||
if is_current {
|
||||
View::selected_item_check(ui);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
ui.add_space(4.0);
|
||||
});
|
||||
|
||||
|
||||
@@ -7,29 +7,30 @@
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distr1ibuted on an "AS IS" BASIS,
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use egui::{Align, Layout, RichText, StrokeKind};
|
||||
use egui::{Align, Layout, RichText, ScrollArea, StrokeKind};
|
||||
use egui::scroll_area::ScrollBarVisibility;
|
||||
use grin_core::core::amount_to_hr_string;
|
||||
|
||||
use crate::gui::icons::{FOLDER_USER, PACKAGE, SCAN, SPINNER, USERS_THREE, USER_PLUS};
|
||||
use crate::gui::icons::{CHECK, FOLDER_USER, PACKAGE, PATH, SCAN, SPINNER, USERS_THREE, USER_PLUS};
|
||||
use crate::gui::platform::PlatformCallbacks;
|
||||
use crate::gui::views::types::{ModalPosition, QrScanResult};
|
||||
use crate::gui::views::wallets::wallet::account::create::CreateAccountContent;
|
||||
use crate::gui::views::wallets::wallet::account::list::WalletAccountsContent;
|
||||
use crate::gui::views::wallets::wallet::types::{WalletContentContainer, GRIN};
|
||||
use crate::gui::views::{CameraContent, CameraScanContent, Content, Modal, View};
|
||||
use crate::gui::Colors;
|
||||
use crate::gui::views::wallets::wallet::request::SendRequestContent;
|
||||
use crate::wallet::{Wallet, WalletConfig};
|
||||
use crate::wallet::types::WalletTask;
|
||||
use crate::wallet::types::{WalletAccount, WalletTask};
|
||||
|
||||
/// Wallet account panel content.
|
||||
pub struct AccountContent {
|
||||
/// Account list content.
|
||||
pub list_content: Option<WalletAccountsContent>,
|
||||
pub struct WalletAccountContent {
|
||||
/// Flag to show account list content.
|
||||
pub show_list: bool,
|
||||
/// Account creation [`Modal`] content.
|
||||
create_account_content: CreateAccountContent,
|
||||
|
||||
@@ -37,15 +38,20 @@ pub struct AccountContent {
|
||||
qr_scan_content: Option<CameraContent>,
|
||||
/// QR code scan result
|
||||
qr_scan_result: Option<QrScanResult>,
|
||||
/// Send request creation [`Modal`] content.
|
||||
send_content: Option<SendRequestContent>,
|
||||
}
|
||||
|
||||
/// Account creation [`Modal`] identifier.
|
||||
const CREATE_MODAL_ID: &'static str = "create_account_modal";
|
||||
/// Identifier for sending request creation [`Modal`].
|
||||
const SEND_MODAL_ID: &'static str = "account_send_request_modal";
|
||||
|
||||
impl WalletContentContainer for AccountContent {
|
||||
impl WalletContentContainer for WalletAccountContent {
|
||||
fn modal_ids(&self) -> Vec<&'static str> {
|
||||
vec![
|
||||
CREATE_MODAL_ID
|
||||
CREATE_MODAL_ID,
|
||||
SEND_MODAL_ID
|
||||
]
|
||||
}
|
||||
|
||||
@@ -56,6 +62,11 @@ impl WalletContentContainer for AccountContent {
|
||||
cb: &dyn PlatformCallbacks) {
|
||||
match modal.id {
|
||||
CREATE_MODAL_ID => self.create_account_content.ui(ui, wallet, modal, cb),
|
||||
SEND_MODAL_ID => {
|
||||
if let Some(c) = self.send_content.as_mut() {
|
||||
c.modal_ui(ui, wallet, modal, cb);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -65,7 +76,7 @@ impl WalletContentContainer for AccountContent {
|
||||
self.qr_scan_ui(ui, wallet, cb);
|
||||
} else {
|
||||
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
|
||||
if self.list_content.is_some() {
|
||||
if self.show_list {
|
||||
self.list_ui(ui, wallet);
|
||||
} else {
|
||||
// Show account content.
|
||||
@@ -76,18 +87,21 @@ impl WalletContentContainer for AccountContent {
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for AccountContent {
|
||||
impl Default for WalletAccountContent {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
list_content: None,
|
||||
show_list: false,
|
||||
create_account_content: CreateAccountContent::default(),
|
||||
qr_scan_content: None,
|
||||
qr_scan_result: None,
|
||||
send_content: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AccountContent {
|
||||
const ACCOUNT_ITEM_HEIGHT: f32 = 75.0;
|
||||
|
||||
impl WalletAccountContent {
|
||||
/// Check if QR code scanner was opened.
|
||||
pub fn qr_scan_showing(&self) -> bool {
|
||||
self.qr_scan_content.is_some() || self.qr_scan_result.is_some()
|
||||
@@ -105,15 +119,15 @@ impl AccountContent {
|
||||
|
||||
/// Check if it's possible to go back at navigation stack.
|
||||
pub fn can_back(&self) -> bool {
|
||||
self.qr_scan_showing() || self.list_content.is_some()
|
||||
self.qr_scan_showing() || self.show_list
|
||||
}
|
||||
|
||||
/// Navigate back on navigation stack.
|
||||
pub fn back(&mut self, cb: &dyn PlatformCallbacks) {
|
||||
if self.qr_scan_showing() {
|
||||
self.close_qr_scan(cb);
|
||||
} else if self.list_content.is_some() {
|
||||
self.list_content = None;
|
||||
} else if self.show_list {
|
||||
self.show_list = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,13 +156,10 @@ impl AccountContent {
|
||||
|
||||
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
|
||||
// Draw button to show QR code scanner.
|
||||
let wallet_synced = wallet.synced_from_node();
|
||||
if wallet_synced {
|
||||
View::item_button(ui, View::item_rounding(0, 2, true), SCAN, None, || {
|
||||
self.qr_scan_content = Some(CameraContent::default());
|
||||
cb.start_camera();
|
||||
});
|
||||
}
|
||||
View::item_button(ui, View::item_rounding(0, 2, true), SCAN, None, || {
|
||||
self.qr_scan_content = Some(CameraContent::default());
|
||||
cb.start_camera();
|
||||
});
|
||||
|
||||
// Draw button to show list of accounts.
|
||||
let accounts = wallet.accounts();
|
||||
@@ -157,11 +168,7 @@ impl AccountContent {
|
||||
} else {
|
||||
USER_PLUS
|
||||
};
|
||||
let rounding = if wallet_synced {
|
||||
View::item_rounding(1, 3, true)
|
||||
} else {
|
||||
View::item_rounding(0, 2, true)
|
||||
};
|
||||
let rounding = View::item_rounding(1, 3, true);
|
||||
View::item_button(ui, rounding, accounts_icon, None, || {
|
||||
if accounts.len() == 1 {
|
||||
self.create_account_content = CreateAccountContent::default();
|
||||
@@ -170,9 +177,7 @@ impl AccountContent {
|
||||
.title(t!("wallets.accounts"))
|
||||
.show();
|
||||
} else {
|
||||
self.list_content = Some(
|
||||
WalletAccountsContent::new(accounts, wallet.get_config().account)
|
||||
);
|
||||
self.show_list = true;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -240,19 +245,26 @@ impl AccountContent {
|
||||
|
||||
/// Draw account list content.
|
||||
fn list_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet) {
|
||||
if let Some(accounts) = self.list_content.as_mut() {
|
||||
let mut selected = false;
|
||||
accounts.ui(ui, |acc| {
|
||||
let _ = wallet.set_active_account(&acc.label);
|
||||
selected = true;
|
||||
let accounts = wallet.accounts();
|
||||
let size = accounts.len();
|
||||
ScrollArea::vertical()
|
||||
.id_salt("account_list_scroll")
|
||||
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
|
||||
.max_height(411.0)
|
||||
.auto_shrink([true; 2])
|
||||
.show_rows(ui, ACCOUNT_ITEM_HEIGHT, size, |ui, row_range| {
|
||||
for index in row_range {
|
||||
let acc = accounts.get(index).unwrap().clone();
|
||||
let current = wallet.get_config().account == acc.label;
|
||||
account_item_ui(ui, &acc, current, index, size, || {
|
||||
let _ = wallet.set_active_account(&acc.label);
|
||||
self.show_list = false;
|
||||
});
|
||||
if index == size - 1 {
|
||||
ui.add_space(4.0);
|
||||
}
|
||||
}
|
||||
});
|
||||
if selected {
|
||||
self.list_content = None;
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
ui.add_space(2.0);
|
||||
View::horizontal_line(ui, Colors::item_stroke());
|
||||
@@ -265,12 +277,12 @@ impl AccountContent {
|
||||
ui.columns(2, |columns| {
|
||||
columns[0].vertical_centered_justified(|ui| {
|
||||
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
|
||||
self.list_content = None;
|
||||
self.show_list = false;
|
||||
});
|
||||
});
|
||||
columns[1].vertical_centered_justified(|ui| {
|
||||
View::button(ui, t!("modal.add"), Colors::white_or_black(false), || {
|
||||
self.list_content = None;
|
||||
self.show_list = false;
|
||||
self.create_account_content = CreateAccountContent::default();
|
||||
Modal::new(CREATE_MODAL_ID)
|
||||
.position(ModalPosition::CenterTop)
|
||||
@@ -290,8 +302,17 @@ impl AccountContent {
|
||||
cb.stop_camera();
|
||||
self.qr_scan_content = None;
|
||||
match result {
|
||||
QrScanResult::Address(_) => {
|
||||
//TODO: send with address
|
||||
QrScanResult::Address(a) => {
|
||||
if let Some(data) = wallet.get_data() {
|
||||
if data.info.amount_currently_spendable > 0 {
|
||||
let address = Some(a.to_string());
|
||||
self.send_content = Some(SendRequestContent::new(address));
|
||||
Modal::new(SEND_MODAL_ID)
|
||||
.position(ModalPosition::CenterTop)
|
||||
.title(t!("wallets.send"))
|
||||
.show();
|
||||
}
|
||||
}
|
||||
}
|
||||
QrScanResult::Slatepack(m) => {
|
||||
wallet.task(WalletTask::OpenMessage(m));
|
||||
@@ -321,4 +342,72 @@ impl AccountContent {
|
||||
ui.add_space(6.0);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw account item.
|
||||
fn account_item_ui(ui: &mut egui::Ui,
|
||||
acc: &WalletAccount,
|
||||
current: bool,
|
||||
index: usize,
|
||||
size: usize,
|
||||
mut on_select: impl FnMut()) {
|
||||
// Setup layout size.
|
||||
let mut rect = ui.available_rect_before_wrap();
|
||||
rect.set_height(ACCOUNT_ITEM_HEIGHT);
|
||||
|
||||
// Draw round background.
|
||||
let bg_rect = rect.clone();
|
||||
let item_rounding = View::item_rounding(index, size, false);
|
||||
ui.painter().rect(bg_rect,
|
||||
item_rounding,
|
||||
Colors::fill(),
|
||||
View::item_stroke(),
|
||||
StrokeKind::Outside);
|
||||
|
||||
ui.vertical(|ui| {
|
||||
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
|
||||
// Draw button to select account.
|
||||
if current {
|
||||
View::selected_item_check(ui);
|
||||
} else {
|
||||
let button_rounding = View::item_rounding(index, size, true);
|
||||
View::item_button(ui, button_rounding, CHECK, None, || {
|
||||
on_select();
|
||||
});
|
||||
}
|
||||
|
||||
let layout_size = ui.available_size();
|
||||
ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| {
|
||||
ui.add_space(8.0);
|
||||
ui.vertical(|ui| {
|
||||
ui.add_space(3.0);
|
||||
// Show spendable amount.
|
||||
let amount = amount_to_hr_string(acc.spendable_amount, true);
|
||||
let amount_text = format!("{} {}", amount, GRIN);
|
||||
ui.with_layout(Layout::left_to_right(Align::Min), |ui| {
|
||||
ui.add_space(1.0);
|
||||
ui.label(RichText::new(amount_text)
|
||||
.size(18.0)
|
||||
.color(Colors::white_or_black(true)));
|
||||
});
|
||||
ui.add_space(-2.0);
|
||||
|
||||
// Show account name.
|
||||
let default_acc_label = WalletConfig::DEFAULT_ACCOUNT_LABEL.to_string();
|
||||
let acc_label = if acc.label == default_acc_label {
|
||||
t!("wallets.default_account").into()
|
||||
} else {
|
||||
acc.label.to_owned()
|
||||
};
|
||||
let acc_name = format!("{} {}", FOLDER_USER, acc_label);
|
||||
View::ellipsize_text(ui, acc_name, 15.0, Colors::text(false));
|
||||
|
||||
// Show account BIP32 derivation path.
|
||||
let acc_path = format!("{} {}", PATH, acc.path);
|
||||
ui.label(RichText::new(acc_path).size(15.0).color(Colors::gray()));
|
||||
ui.add_space(3.0);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,130 +0,0 @@
|
||||
// Copyright 2025 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::{Align, Layout, RichText, ScrollArea, StrokeKind};
|
||||
use grin_core::core::amount_to_hr_string;
|
||||
|
||||
use crate::gui::icons::{CHECK, FOLDER_USER, PATH};
|
||||
use crate::gui::views::wallets::wallet::types::GRIN;
|
||||
use crate::gui::views::View;
|
||||
use crate::gui::Colors;
|
||||
use crate::wallet::types::WalletAccount;
|
||||
use crate::wallet::WalletConfig;
|
||||
|
||||
/// Wallet account list content.
|
||||
pub struct WalletAccountsContent {
|
||||
/// List of wallet accounts.
|
||||
accounts: Vec<WalletAccount>,
|
||||
/// Current wallet account label.
|
||||
current_label: String,
|
||||
}
|
||||
|
||||
const ACCOUNT_ITEM_HEIGHT: f32 = 75.0;
|
||||
|
||||
impl WalletAccountsContent {
|
||||
/// Create new accounts content.
|
||||
pub fn new(accounts: Vec<WalletAccount>, current: String) -> Self {
|
||||
Self { accounts, current_label: current }
|
||||
}
|
||||
|
||||
/// Draw account list content.
|
||||
pub fn ui(&mut self, ui: &mut egui::Ui, mut on_select: impl FnMut(WalletAccount)) {
|
||||
let size = self.accounts.len();
|
||||
ScrollArea::vertical()
|
||||
.id_salt("account_list_scroll")
|
||||
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
|
||||
.max_height(411.0)
|
||||
.auto_shrink([true; 2])
|
||||
.show_rows(ui, ACCOUNT_ITEM_HEIGHT, size, |ui, row_range| {
|
||||
for index in row_range {
|
||||
let acc = self.accounts.get(index).unwrap().clone();
|
||||
self.account_item_ui(ui, &acc, index, size, || {
|
||||
on_select(acc.clone());
|
||||
});
|
||||
if index == size - 1 {
|
||||
ui.add_space(4.0);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Draw account item.
|
||||
fn account_item_ui(&mut self,
|
||||
ui: &mut egui::Ui,
|
||||
acc: &WalletAccount,
|
||||
index: usize,
|
||||
size: usize,
|
||||
mut on_select: impl FnMut()) {
|
||||
// Setup layout size.
|
||||
let mut rect = ui.available_rect_before_wrap();
|
||||
rect.set_height(ACCOUNT_ITEM_HEIGHT);
|
||||
|
||||
// Draw round background.
|
||||
let bg_rect = rect.clone();
|
||||
let item_rounding = View::item_rounding(index, size, false);
|
||||
ui.painter().rect(bg_rect,
|
||||
item_rounding,
|
||||
Colors::fill(),
|
||||
View::item_stroke(),
|
||||
StrokeKind::Outside);
|
||||
|
||||
ui.vertical(|ui| {
|
||||
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
|
||||
// Draw button to select account.
|
||||
if self.current_label == acc.label {
|
||||
View::selected_item_check(ui);
|
||||
} else {
|
||||
let button_rounding = View::item_rounding(index, size, true);
|
||||
View::item_button(ui, button_rounding, CHECK, None, || {
|
||||
on_select();
|
||||
});
|
||||
}
|
||||
|
||||
let layout_size = ui.available_size();
|
||||
ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| {
|
||||
ui.add_space(8.0);
|
||||
ui.vertical(|ui| {
|
||||
ui.add_space(3.0);
|
||||
// Show spendable amount.
|
||||
let amount = amount_to_hr_string(acc.spendable_amount, true);
|
||||
let amount_text = format!("{} {}", amount, GRIN);
|
||||
ui.with_layout(Layout::left_to_right(Align::Min), |ui| {
|
||||
ui.add_space(1.0);
|
||||
ui.label(RichText::new(amount_text)
|
||||
.size(18.0)
|
||||
.color(Colors::white_or_black(true)));
|
||||
});
|
||||
ui.add_space(-2.0);
|
||||
|
||||
// Show account name.
|
||||
let default_acc_label = WalletConfig::DEFAULT_ACCOUNT_LABEL.to_string();
|
||||
let acc_label = if acc.label == default_acc_label {
|
||||
t!("wallets.default_account").into()
|
||||
} else {
|
||||
acc.label.to_owned()
|
||||
};
|
||||
let acc_name = format!("{} {}", FOLDER_USER, acc_label);
|
||||
View::ellipsize_text(ui, acc_name, 15.0, Colors::text(false));
|
||||
|
||||
// Show account BIP32 derivation path.
|
||||
let acc_path = format!("{} {}", PATH, acc.path);
|
||||
ui.label(RichText::new(acc_path).size(15.0).color(Colors::gray()));
|
||||
ui.add_space(3.0);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,6 @@
|
||||
// limitations under the License.
|
||||
|
||||
mod content;
|
||||
mod list;
|
||||
mod create;
|
||||
pub use content::*;
|
||||
|
||||
pub use content::*;
|
||||
mod create;
|
||||
@@ -19,7 +19,7 @@ use grin_chain::SyncStatus;
|
||||
use crate::gui::icons::{ARROWS_CLOCKWISE, FILE_ARROW_DOWN, FILE_ARROW_UP, FILE_TEXT, GEAR_FINE, POWER, STACK};
|
||||
use crate::gui::platform::PlatformCallbacks;
|
||||
use crate::gui::views::types::{LinePosition, ModalPosition};
|
||||
use crate::gui::views::wallets::wallet::account::AccountContent;
|
||||
use crate::gui::views::wallets::wallet::account::WalletAccountContent;
|
||||
use crate::gui::views::wallets::wallet::message::MessageInputContent;
|
||||
use crate::gui::views::wallets::wallet::request::{InvoiceRequestContent, SendRequestContent};
|
||||
use crate::gui::views::wallets::wallet::transport::WalletTransportContent;
|
||||
@@ -42,7 +42,7 @@ pub struct WalletContent {
|
||||
pub settings_content: Option<WalletSettingsContent>,
|
||||
|
||||
/// Account panel content.
|
||||
pub account_content: AccountContent,
|
||||
pub account_content: WalletAccountContent,
|
||||
/// Transport panel content.
|
||||
pub transport_content: WalletTransportContent,
|
||||
|
||||
@@ -150,9 +150,13 @@ impl WalletContentContainer for WalletContent {
|
||||
}
|
||||
});
|
||||
|
||||
// Close scanner when account panel got hidden.
|
||||
if !show_account && self.account_content.qr_scan_showing() {
|
||||
self.account_content.close_qr_scan(cb);
|
||||
// Close scanner or account list when account panel got hidden.
|
||||
if !show_account {
|
||||
if self.account_content.qr_scan_showing() {
|
||||
self.account_content.close_qr_scan(cb);
|
||||
} else {
|
||||
self.account_content.show_list = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Flag to check if account panel is opened.
|
||||
@@ -204,7 +208,11 @@ impl WalletContentContainer for WalletContent {
|
||||
bottom: 1.0 as i8,
|
||||
},
|
||||
fill: if top_panel_expanded {
|
||||
Colors::fill_lite()
|
||||
if self.transport_content.qr_address_content.is_some() {
|
||||
Colors::FILL_DEEP
|
||||
} else {
|
||||
Colors::fill_lite()
|
||||
}
|
||||
} else {
|
||||
Colors::TRANSPARENT
|
||||
},
|
||||
@@ -248,7 +256,7 @@ impl WalletContentContainer for WalletContent {
|
||||
.show_inside(ui, |ui| {
|
||||
let rect = ui.available_rect_before_wrap();
|
||||
let show_settings = self.settings_content.is_some();
|
||||
let show_txs = self.txs_content.is_some();
|
||||
let show_txs = self.txs_content.is_some() && !top_panel_expanded;
|
||||
let show_sync = (!show_settings || block_nav) &&
|
||||
sync_ui(ui, &wallet);
|
||||
if !show_sync {
|
||||
@@ -302,7 +310,7 @@ impl Default for WalletContent {
|
||||
Self {
|
||||
txs_content: Some(WalletTransactionsContent::new(None)),
|
||||
settings_content: None,
|
||||
account_content: AccountContent::default(),
|
||||
account_content: WalletAccountContent::default(),
|
||||
transport_content: WalletTransportContent::default(),
|
||||
invoice_content: None,
|
||||
send_content: None,
|
||||
@@ -316,7 +324,7 @@ impl WalletContent {
|
||||
pub fn title(&self) -> impl Into<String> {
|
||||
if self.account_content.qr_scan_showing() {
|
||||
t!("scan_qr")
|
||||
} else if self.account_content.list_content.is_some() {
|
||||
} else if self.account_content.show_list {
|
||||
t!("wallets.accounts")
|
||||
} else if self.transport_content.settings_content.is_some() {
|
||||
t!("wallets.transport")
|
||||
@@ -378,8 +386,7 @@ impl WalletContent {
|
||||
self.settings_content = None;
|
||||
});
|
||||
});
|
||||
let active = if wallet.synced_from_node() &&
|
||||
has_wallet_data { Some(false) } else { None };
|
||||
let active = if has_wallet_data { Some(false) } else { None };
|
||||
columns[1].vertical_centered_justified(|ui| {
|
||||
if wallet.invoice_creating() {
|
||||
ui.add_space(4.0);
|
||||
@@ -450,7 +457,7 @@ impl WalletContent {
|
||||
None => {
|
||||
// Show transaction modal on wallet task result.
|
||||
if let Some(id) = id {
|
||||
let tx = wallet.get_data().unwrap().tx_by_slate_id(id);
|
||||
let tx = wallet.get_data().unwrap().tx_by_id(id);
|
||||
if tx.is_some() {
|
||||
self.txs_content = Some(WalletTransactionsContent::new(tx));
|
||||
self.settings_content = None;
|
||||
|
||||
@@ -28,11 +28,12 @@ pub struct MessageInputContent {
|
||||
message_edit: String,
|
||||
/// Flag to check if error happened at Slatepack message parsing.
|
||||
parse_error: bool,
|
||||
/// QR code scanner content.
|
||||
scan_qr_content: Option<CameraContent>,
|
||||
/// Button to parse picked file content.
|
||||
file_pick_button: FilePickContent,
|
||||
|
||||
/// QR code scanner content.
|
||||
scan_qr_content: Option<CameraContent>,
|
||||
|
||||
/// Payment proof input content.
|
||||
pub proof_content: Option<PaymentProofContent>,
|
||||
}
|
||||
@@ -45,10 +46,10 @@ impl Default for MessageInputContent {
|
||||
Self {
|
||||
message_edit: "".to_string(),
|
||||
parse_error: false,
|
||||
scan_qr_content: None,
|
||||
file_pick_button: FilePickContent::new(
|
||||
FilePickContentType::Button(t!("choose_file").into())
|
||||
),
|
||||
scan_qr_content: None,
|
||||
proof_content: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ use crate::gui::icons::{BROOM, CLIPBOARD_TEXT, COPY, FILE_TEXT, SEAL_CHECK};
|
||||
use crate::gui::platform::PlatformCallbacks;
|
||||
use crate::gui::views::{FilePickContent, FilePickContentType, Modal, View};
|
||||
use crate::gui::Colors;
|
||||
use crate::wallet::types::{WalletTask, WalletTransaction};
|
||||
use crate::wallet::types::{WalletTask, WalletTx};
|
||||
use crate::wallet::Wallet;
|
||||
|
||||
pub struct PaymentProofContent {
|
||||
@@ -154,7 +154,7 @@ impl PaymentProofContent {
|
||||
/// Draw transaction payment proof content to share.
|
||||
pub fn share_ui(&mut self,
|
||||
ui: &mut egui::Ui,
|
||||
tx: &WalletTransaction,
|
||||
tx: &WalletTx,
|
||||
cb: &dyn PlatformCallbacks) {
|
||||
ui.add_space(6.0);
|
||||
ui.vertical_centered(|ui| {
|
||||
|
||||
@@ -287,9 +287,14 @@ impl SendRequestContent {
|
||||
});
|
||||
columns[1].vertical_centered_justified(|ui| {
|
||||
// Button to create Slatepack message request.
|
||||
View::button(ui, t!("continue"), Colors::white_or_black(false), || {
|
||||
self.on_continue(wallet);
|
||||
});
|
||||
if self.max_calculating || wallet.fee_calculating() {
|
||||
ui.add_space(4.0);
|
||||
View::small_loading_spinner(ui);
|
||||
} else {
|
||||
View::button(ui, t!("continue"), Colors::white_or_black(false), || {
|
||||
self.on_continue(wallet);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
@@ -297,7 +302,7 @@ impl SendRequestContent {
|
||||
|
||||
/// Callback when Continue button was pressed.
|
||||
fn on_continue(&mut self, wallet: &Wallet) {
|
||||
if self.amount_edit.is_empty() || self.max_calculating || wallet.fee_calculating() {
|
||||
if self.amount_edit.is_empty() {
|
||||
return;
|
||||
}
|
||||
// Check address to send over Tor if enabled.
|
||||
|
||||
@@ -13,10 +13,10 @@
|
||||
// limitations under the License.
|
||||
|
||||
use eframe::emath::Align;
|
||||
use eframe::epaint::StrokeKind;
|
||||
use egui::{Id, Layout, RichText};
|
||||
use eframe::epaint::{RectShape, StrokeKind};
|
||||
use egui::{CursorIcon, Id, Layout, RichText, Sense, UiBuilder};
|
||||
|
||||
use crate::gui::icons::{CLOCK_COUNTDOWN, FOLDERS, FOLDER_USER, PASSWORD, PENCIL};
|
||||
use crate::gui::icons::{CLOCK_COUNTDOWN, FOLDERS, FOLDER_USER, PASSWORD};
|
||||
use crate::gui::platform::PlatformCallbacks;
|
||||
use crate::gui::views::types::ModalPosition;
|
||||
use crate::gui::views::wallets::wallet::types::WalletContentContainer;
|
||||
@@ -79,7 +79,11 @@ impl WalletContentContainer for CommonSettings {
|
||||
}
|
||||
|
||||
fn container_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) {
|
||||
ui.add_space(8.0);
|
||||
if View::is_desktop() {
|
||||
ui.add_space(1.0);
|
||||
} else {
|
||||
ui.add_space(8.0);
|
||||
}
|
||||
ui.vertical_centered(|ui| {
|
||||
let config = wallet.get_config();
|
||||
// Show wallet name.
|
||||
@@ -106,7 +110,7 @@ impl WalletContentContainer for CommonSettings {
|
||||
|
||||
ui.add_space(8.0);
|
||||
|
||||
// Setup ability to post wallet transactions with Dandelion.
|
||||
// Ability to post wallet transactions with Dandelion.
|
||||
View::checkbox(ui, wallet.can_use_dandelion(), t!("wallets.use_dandelion"), || {
|
||||
wallet.update_use_dandelion(!wallet.can_use_dandelion());
|
||||
});
|
||||
@@ -137,60 +141,68 @@ impl Default for CommonSettings {
|
||||
impl CommonSettings {
|
||||
/// Draw content to change wallet name and password.
|
||||
fn name_ui(&mut self, ui: &mut egui::Ui, name: String) {
|
||||
// Setup layout size.
|
||||
// Draw round background.
|
||||
let mut rect = ui.available_rect_before_wrap();
|
||||
rect.set_height(56.0);
|
||||
|
||||
// Draw round background.
|
||||
let bg_rect = rect.clone();
|
||||
let item_rounding = if View::is_desktop() {
|
||||
let r = if View::is_desktop() {
|
||||
View::item_rounding(0, 2, false)
|
||||
} else {
|
||||
View::item_rounding(0, 1, false)
|
||||
};
|
||||
ui.painter().rect(bg_rect,
|
||||
item_rounding,
|
||||
Colors::fill(),
|
||||
View::item_stroke(),
|
||||
StrokeKind::Outside);
|
||||
let bg = Colors::fill_lite();
|
||||
let mut bg_shape = RectShape::new(rect, r, bg, View::item_stroke(), StrokeKind::Outside);
|
||||
let bg_idx = ui.painter().add(bg_shape.clone());
|
||||
|
||||
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
|
||||
let r = if View::is_desktop() {
|
||||
View::item_rounding(0, 2, true)
|
||||
} else {
|
||||
View::item_rounding(0, 1, true)
|
||||
};
|
||||
View::item_button(ui, r, PASSWORD, None, || {
|
||||
self.old_pass_edit = "".to_string();
|
||||
self.new_pass_edit = "".to_string();
|
||||
self.wrong_pass = false;
|
||||
// Show wallet password modal.
|
||||
Modal::new(PASS_EDIT_MODAL)
|
||||
.position(ModalPosition::CenterTop)
|
||||
.title(t!("wallets.wallet"))
|
||||
.show();
|
||||
});
|
||||
View::item_button(ui, View::item_rounding(1, 3, true), PENCIL, None, || {
|
||||
self.name_edit = name.clone();
|
||||
// Show wallet name modal.
|
||||
Modal::new(NAME_EDIT_MODAL)
|
||||
.position(ModalPosition::CenterTop)
|
||||
.title(t!("wallets.wallet"))
|
||||
.show();
|
||||
});
|
||||
let layout_size = ui.available_size();
|
||||
ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| {
|
||||
ui.add_space(12.0);
|
||||
ui.vertical(|ui| {
|
||||
ui.add_space(4.0);
|
||||
View::ellipsize_text(ui, name, 18.0, Colors::title(false));
|
||||
ui.add_space(1.0);
|
||||
let desc = format!("{} {}", FOLDER_USER, t!("wallets.name").replace(":", ""));
|
||||
ui.label(RichText::new(desc).size(15.0).color(Colors::gray()));
|
||||
ui.add_space(8.0);
|
||||
let res = ui.scope_builder(
|
||||
UiBuilder::new()
|
||||
.sense(Sense::click())
|
||||
.layout(Layout::right_to_left(Align::Center))
|
||||
.max_rect(rect), |ui| {
|
||||
let r = if View::is_desktop() {
|
||||
View::item_rounding(0, 2, true)
|
||||
} else {
|
||||
View::item_rounding(0, 1, true)
|
||||
};
|
||||
View::item_button(ui, r, PASSWORD, None, || {
|
||||
self.old_pass_edit = "".to_string();
|
||||
self.new_pass_edit = "".to_string();
|
||||
self.wrong_pass = false;
|
||||
// Show wallet password modal.
|
||||
Modal::new(PASS_EDIT_MODAL)
|
||||
.position(ModalPosition::CenterTop)
|
||||
.title(t!("wallets.wallet"))
|
||||
.show();
|
||||
});
|
||||
});
|
||||
});
|
||||
let layout_size = ui.available_size();
|
||||
ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| {
|
||||
ui.add_space(12.0);
|
||||
ui.vertical(|ui| {
|
||||
ui.add_space(4.0);
|
||||
View::ellipsize_text(ui, name.clone(), 18.0, Colors::title(false));
|
||||
ui.add_space(1.0);
|
||||
let desc = format!("{} {}", FOLDER_USER, t!("wallets.name").replace(":", ""));
|
||||
ui.label(RichText::new(desc).size(15.0).color(Colors::gray()));
|
||||
ui.add_space(8.0);
|
||||
});
|
||||
});
|
||||
}
|
||||
).response;
|
||||
let clicked = res.clicked() || res.long_touched();
|
||||
// Setup background and cursor.
|
||||
if res.hovered() {
|
||||
res.on_hover_cursor(CursorIcon::PointingHand);
|
||||
bg_shape.fill = Colors::fill();
|
||||
}
|
||||
ui.painter().set(bg_idx, bg_shape);
|
||||
// Handle clicks on layout.
|
||||
if clicked {
|
||||
self.name_edit = name;
|
||||
// Show wallet name modal.
|
||||
Modal::new(NAME_EDIT_MODAL)
|
||||
.position(ModalPosition::CenterTop)
|
||||
.title(t!("wallets.wallet"))
|
||||
.show();
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw wallet name [`Modal`] content.
|
||||
@@ -340,45 +352,53 @@ impl CommonSettings {
|
||||
|
||||
/// Draw content to change wallet data directory.
|
||||
fn data_dir_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) {
|
||||
// Setup layout size.
|
||||
// Draw round background.
|
||||
let mut rect = ui.available_rect_before_wrap();
|
||||
rect.set_height(56.0);
|
||||
let r = View::item_rounding(1, 2, false);
|
||||
let bg = Colors::fill_lite();
|
||||
let mut bg_shape = RectShape::new(rect, r, bg, View::item_stroke(), StrokeKind::Outside);
|
||||
let bg_idx = ui.painter().add(bg_shape.clone());
|
||||
|
||||
// Draw round background.
|
||||
let bg_rect = rect.clone();
|
||||
let item_rounding = View::item_rounding(1, 2, false);
|
||||
ui.painter().rect(bg_rect,
|
||||
item_rounding,
|
||||
Colors::fill(),
|
||||
View::item_stroke(),
|
||||
StrokeKind::Outside);
|
||||
|
||||
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
|
||||
self.pick_data_dir.ui(ui, cb, |path| {
|
||||
wallet.change_data_path(path);
|
||||
});
|
||||
View::item_button(ui, View::item_rounding(1, 3, true), PENCIL, None, || {
|
||||
self.data_path_edit = wallet.get_config().data_path.unwrap_or_default();
|
||||
// Show chain data path edit modal.
|
||||
Modal::new(DATA_PATH_MODAL)
|
||||
.position(ModalPosition::CenterTop)
|
||||
.title(t!("wallets.wallet"))
|
||||
.show();
|
||||
});
|
||||
let layout_size = ui.available_size();
|
||||
ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| {
|
||||
ui.add_space(12.0);
|
||||
ui.vertical(|ui| {
|
||||
ui.add_space(4.0);
|
||||
let path = wallet.get_config().data_path.unwrap_or_default();
|
||||
View::ellipsize_text(ui, path, 18.0, Colors::title(false));
|
||||
ui.add_space(1.0);
|
||||
let desc = format!("{} {}", FOLDERS, t!("files_location"));
|
||||
ui.label(RichText::new(desc).size(15.0).color(Colors::gray()));
|
||||
ui.add_space(8.0);
|
||||
let res = ui.scope_builder(
|
||||
UiBuilder::new()
|
||||
.sense(Sense::click())
|
||||
.layout(Layout::right_to_left(Align::Center))
|
||||
.max_rect(rect), |ui| {
|
||||
self.pick_data_dir.ui(ui, cb, |path| {
|
||||
wallet.change_data_path(path);
|
||||
});
|
||||
});
|
||||
});
|
||||
let layout_size = ui.available_size();
|
||||
ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| {
|
||||
ui.add_space(12.0);
|
||||
ui.vertical(|ui| {
|
||||
ui.add_space(4.0);
|
||||
let path = wallet.get_config().data_path.unwrap_or_default();
|
||||
View::ellipsize_text(ui, path, 18.0, Colors::title(false));
|
||||
ui.add_space(1.0);
|
||||
let desc = format!("{} {}", FOLDERS, t!("files_location"));
|
||||
ui.label(RichText::new(desc).size(15.0).color(Colors::gray()));
|
||||
ui.add_space(8.0);
|
||||
});
|
||||
});
|
||||
}
|
||||
).response;
|
||||
let clicked = res.clicked() || res.long_touched();
|
||||
// Setup background and cursor.
|
||||
if res.hovered() {
|
||||
res.on_hover_cursor(CursorIcon::PointingHand);
|
||||
bg_shape.fill = Colors::fill();
|
||||
}
|
||||
ui.painter().set(bg_idx, bg_shape);
|
||||
// Handle clicks on layout.
|
||||
if clicked {
|
||||
self.data_path_edit = wallet.get_config().data_path.unwrap_or_default();
|
||||
// Show chain data path edit modal.
|
||||
Modal::new(DATA_PATH_MODAL)
|
||||
.position(ModalPosition::CenterTop)
|
||||
.title(t!("wallets.wallet"))
|
||||
.show();
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw data path input [`Modal`] content.
|
||||
|
||||
@@ -12,9 +12,9 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use egui::{Align, Layout, RichText, StrokeKind};
|
||||
use egui::RichText;
|
||||
|
||||
use crate::gui::icons::{CHECK, CHECK_CIRCLE, DOTS_THREE_CIRCLE, GLOBE, GLOBE_SIMPLE, PLUS_CIRCLE, X_CIRCLE};
|
||||
use crate::gui::icons::{GLOBE, PLUS_CIRCLE};
|
||||
use crate::gui::platform::PlatformCallbacks;
|
||||
use crate::gui::views::network::modals::ExternalConnectionModal;
|
||||
use crate::gui::views::network::ConnectionsContent;
|
||||
@@ -70,15 +70,19 @@ impl ContentContainer for ConnectionSettings {
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.add_space(6.0);
|
||||
// Show integrated node selection.
|
||||
ConnectionsContent::integrated_node_item_ui(ui, |ui| {
|
||||
let is_current_method = self.method == ConnectionMethod::Integrated;
|
||||
if !is_current_method {
|
||||
View::item_button(ui, View::item_rounding(0, 1, true), CHECK, None, || {
|
||||
self.method = ConnectionMethod::Integrated;
|
||||
});
|
||||
} else {
|
||||
let cur_integrated = self.method == ConnectionMethod::Integrated;
|
||||
let bg = if cur_integrated {
|
||||
Colors::fill_deep()
|
||||
} else {
|
||||
Colors::fill_lite()
|
||||
};
|
||||
ConnectionsContent::integrated_node_item_ui(ui, bg, (!cur_integrated, || {
|
||||
self.method = ConnectionMethod::Integrated;
|
||||
}), |ui| {
|
||||
if cur_integrated {
|
||||
View::selected_item_check(ui);
|
||||
}
|
||||
cur_integrated
|
||||
});
|
||||
|
||||
ui.add_space(8.0);
|
||||
@@ -111,6 +115,7 @@ impl ContentContainer for ConnectionSettings {
|
||||
ext_conn_list.push(ExternalConnection {
|
||||
id: *id,
|
||||
url: url.clone(),
|
||||
username: Some("grin".to_string()),
|
||||
secret: None,
|
||||
available: Some(true),
|
||||
})
|
||||
@@ -119,8 +124,8 @@ impl ContentContainer for ConnectionSettings {
|
||||
}
|
||||
}
|
||||
|
||||
let ext_size = ext_conn_list.len();
|
||||
if ext_size != 0 {
|
||||
let len = ext_conn_list.len();
|
||||
if len != 0 {
|
||||
ui.add_space(8.0);
|
||||
for (i, c) in ext_conn_list.iter().enumerate() {
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
@@ -129,73 +134,21 @@ impl ContentContainer for ConnectionSettings {
|
||||
ConnectionMethod::External(id, url) => id == &c.id || url == &c.url,
|
||||
_ => false
|
||||
};
|
||||
Self::ext_conn_item_ui(ui, c, is_current, i, ext_size, || {
|
||||
let bg = if is_current {
|
||||
Colors::fill()
|
||||
} else {
|
||||
Colors::fill_lite()
|
||||
};
|
||||
ConnectionsContent::ext_conn_item_ui(ui, bg, c, i, len, (!is_current, || {
|
||||
self.method = ConnectionMethod::External(c.id, c.url.clone());
|
||||
}), |ui| {
|
||||
if is_current {
|
||||
View::selected_item_check(ui);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl ConnectionSettings {
|
||||
/// Draw external connection item content.
|
||||
fn ext_conn_item_ui(ui: &mut egui::Ui,
|
||||
conn: &ExternalConnection,
|
||||
is_current: bool,
|
||||
index: usize,
|
||||
len: usize,
|
||||
mut on_select: impl FnMut()) {
|
||||
// Setup layout size.
|
||||
let mut rect = ui.available_rect_before_wrap();
|
||||
rect.set_height(52.0);
|
||||
|
||||
// Draw round background.
|
||||
let bg_rect = rect.clone();
|
||||
let item_rounding = View::item_rounding(index, len, false);
|
||||
ui.painter().rect(bg_rect,
|
||||
item_rounding,
|
||||
Colors::fill(),
|
||||
View::item_stroke(),
|
||||
StrokeKind::Outside);
|
||||
|
||||
ui.vertical(|ui| {
|
||||
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
|
||||
if is_current {
|
||||
View::selected_item_check(ui);
|
||||
} else {
|
||||
// Draw button to select connection.
|
||||
let button_rounding = View::item_rounding(index, len, true);
|
||||
View::item_button(ui, button_rounding, CHECK, None, || {
|
||||
on_select();
|
||||
});
|
||||
}
|
||||
|
||||
let layout_size = ui.available_size();
|
||||
ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| {
|
||||
ui.add_space(6.0);
|
||||
ui.vertical(|ui| {
|
||||
// Draw connections URL.
|
||||
ui.add_space(4.0);
|
||||
let conn_text = format!("{} {}", GLOBE_SIMPLE, conn.url);
|
||||
View::ellipsize_text(ui, conn_text, 15.0, Colors::title(false));
|
||||
ui.add_space(1.0);
|
||||
// Setup connection status text.
|
||||
let status_text = if let Some(available) = conn.available {
|
||||
if available {
|
||||
format!("{} {}", CHECK_CIRCLE, t!("network.available"))
|
||||
} else {
|
||||
format!("{} {}", X_CIRCLE, t!("network.not_available"))
|
||||
}
|
||||
} else {
|
||||
format!("{} {}", DOTS_THREE_CIRCLE, t!("network.availability_check"))
|
||||
};
|
||||
ui.label(RichText::new(status_text).size(15.0).color(Colors::gray()));
|
||||
ui.add_space(3.0);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
use egui::{Align, CornerRadius, Layout, RichText, StrokeKind};
|
||||
|
||||
use crate::AppConfig;
|
||||
use crate::gui::icons::{CIRCLE_HALF, DOTS_THREE_CIRCLE, PLUGS, PLUGS_CONNECTED, POWER, QR_CODE, SHIELD_CHECKERED, SHIELD_SLASH, WARNING_CIRCLE, WRENCH};
|
||||
use crate::gui::platform::PlatformCallbacks;
|
||||
use crate::gui::views::wallets::wallet::transport::settings::WalletTransportSettingsContent;
|
||||
@@ -40,13 +41,10 @@ impl WalletContentContainer for WalletTransportContent {
|
||||
|
||||
fn container_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) {
|
||||
if let Some(content) = self.qr_address_content.as_mut() {
|
||||
// Close panel on wallet change.
|
||||
if let Some(address) = wallet.slatepack_address() {
|
||||
if address != content.text {
|
||||
self.qr_address_content = None;
|
||||
return;
|
||||
}
|
||||
}
|
||||
let dark_theme = AppConfig::dark_theme().unwrap_or(false);
|
||||
// Set light theme for better scanning.
|
||||
AppConfig::set_dark_theme(false);
|
||||
crate::setup_visuals(ui.ctx());
|
||||
// Draw QR code content.
|
||||
ui.add_space(6.0);
|
||||
content.ui(ui, cb);
|
||||
@@ -56,6 +54,9 @@ impl WalletContentContainer for WalletTransportContent {
|
||||
});
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
// Set color theme back.
|
||||
AppConfig::set_dark_theme(dark_theme);
|
||||
crate::setup_visuals(ui.ctx());
|
||||
} else if let Some(content) = self.settings_content.as_mut() {
|
||||
let mut closed = false;
|
||||
content.ui(ui, wallet, cb, || {
|
||||
@@ -89,7 +90,7 @@ impl WalletTransportContent {
|
||||
pub fn back(&mut self) {
|
||||
if let Some(content) = self.settings_content.as_ref() {
|
||||
if content.tor_settings_content.settings_changed {
|
||||
Tor::restart_services();
|
||||
Tor::restart();
|
||||
}
|
||||
self.settings_content = None;
|
||||
} else if self.qr_address_content.is_some() {
|
||||
@@ -142,7 +143,7 @@ impl WalletTransportContent {
|
||||
if wallet.foreign_api_port().is_some() && wallet.secret_key().is_some() {
|
||||
let port = wallet.foreign_api_port().unwrap();
|
||||
let key = wallet.secret_key().unwrap();
|
||||
if !Tor::is_service_starting(service_id) {
|
||||
if !Tor::is_service_starting(service_id) {
|
||||
if !Tor::is_service_running(service_id) {
|
||||
let r = CornerRadius::default();
|
||||
View::item_button(ui, r, POWER, Some(Colors::green()), || {
|
||||
|
||||
@@ -63,7 +63,7 @@ impl WalletTransportSettingsContent {
|
||||
ui.vertical_centered_justified(|ui| {
|
||||
View::button(ui, t!("close"), Colors::white_or_black(false), || {
|
||||
if self.tor_settings_content.settings_changed {
|
||||
Tor::restart_services();
|
||||
Tor::restart();
|
||||
}
|
||||
on_close();
|
||||
});
|
||||
|
||||
@@ -14,21 +14,21 @@
|
||||
|
||||
use egui::epaint::RectShape;
|
||||
use egui::scroll_area::ScrollBarVisibility;
|
||||
use egui::{Align, CornerRadius, Id, Layout, Rect, RichText, ScrollArea, StrokeKind};
|
||||
use egui::{Align, Color32, CornerRadius, CursorIcon, Id, Layout, Rect, RichText, ScrollArea, Sense, StrokeKind, UiBuilder};
|
||||
use grin_core::consensus::COINBASE_MATURITY;
|
||||
use grin_core::core::amount_to_hr_string;
|
||||
use grin_wallet_libwallet::TxLogEntryType;
|
||||
use std::ops::Range;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use crate::gui::icons::{ARROWS_CLOCKWISE, ARROW_CIRCLE_DOWN, ARROW_CIRCLE_UP, CALENDAR_CHECK, DOTS_THREE_CIRCLE, FILE_ARROW_DOWN, FILE_TEXT, GEAR_FINE, PROHIBIT, WARNING, X_CIRCLE};
|
||||
use crate::gui::icons::{ARROWS_CLOCKWISE, ARROW_CIRCLE_DOWN, ARROW_CIRCLE_UP, CALENDAR_CHECK, DOTS_THREE_CIRCLE, FILE_ARROW_DOWN, FILE_TEXT, FILE_X, GEAR_FINE, PROHIBIT, WARNING, X_CIRCLE};
|
||||
use crate::gui::platform::PlatformCallbacks;
|
||||
use crate::gui::views::types::{LinePosition, ModalPosition};
|
||||
use crate::gui::views::wallets::wallet::types::{WalletContentContainer, GRIN};
|
||||
use crate::gui::views::wallets::wallet::WalletTransactionContent;
|
||||
use crate::gui::views::{Content, Modal, PullToRefresh, View};
|
||||
use crate::gui::Colors;
|
||||
use crate::wallet::types::{WalletData, WalletTask, WalletTransaction, WalletTransactionAction};
|
||||
use crate::wallet::types::{WalletData, WalletTask, WalletTx, WalletTxAction};
|
||||
use crate::wallet::Wallet;
|
||||
|
||||
/// Wallet transactions tab content.
|
||||
@@ -36,8 +36,10 @@ pub struct WalletTransactionsContent {
|
||||
/// Transaction information [`Modal`] content.
|
||||
pub tx_info_content: Option<WalletTransactionContent>,
|
||||
|
||||
/// Transaction identifier to use at confirmation [`Modal`].
|
||||
/// Transaction identifier to use at confirmation [`Modal`] to cancel.
|
||||
confirm_cancel_tx_id: Option<u32>,
|
||||
/// Transaction identifier to use at confirmation [`Modal`] to delete.
|
||||
confirm_delete_tx_id: Option<u32>,
|
||||
|
||||
/// Flag to check if sync of wallet was initiated manually at time.
|
||||
manual_sync: Option<u128>
|
||||
@@ -45,19 +47,28 @@ pub struct WalletTransactionsContent {
|
||||
|
||||
impl WalletContentContainer for WalletTransactionsContent {
|
||||
fn modal_ids(&self) -> Vec<&'static str> {
|
||||
vec![TX_INFO_MODAL, CANCEL_TX_CONFIRMATION_MODAL]
|
||||
vec![TX_INFO_MODAL, CANCEL_TX_CONFIRMATION_MODAL, DELETE_TX_CONFIRMATION_MODAL]
|
||||
}
|
||||
|
||||
fn modal_ui(&mut self, ui: &mut egui::Ui, w: &Wallet, m: &Modal, cb: &dyn PlatformCallbacks) {
|
||||
match m.id {
|
||||
TX_INFO_MODAL => {
|
||||
if let Some(content) = self.tx_info_content.as_mut() {
|
||||
content.ui(ui, w, cb);
|
||||
let mut on_delete_id = None;
|
||||
content.ui(ui, m, w, cb, |id| {
|
||||
on_delete_id = Some(id);
|
||||
});
|
||||
if let Some(id) = on_delete_id {
|
||||
self.show_delete_confirmation_modal(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
CANCEL_TX_CONFIRMATION_MODAL => {
|
||||
self.cancel_confirmation_modal(ui, w);
|
||||
}
|
||||
DELETE_TX_CONFIRMATION_MODAL => {
|
||||
self.delete_confirmation_modal(ui, w);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -69,19 +80,21 @@ impl WalletContentContainer for WalletTransactionsContent {
|
||||
|
||||
/// Identifier for transaction information [`Modal`].
|
||||
const TX_INFO_MODAL: &'static str = "tx_info_modal";
|
||||
|
||||
/// Identifier for transaction cancellation confirmation [`Modal`].
|
||||
const CANCEL_TX_CONFIRMATION_MODAL: &'static str = "cancel_tx_conf_modal";
|
||||
/// Identifier for transaction deletion confirmation [`Modal`].
|
||||
const DELETE_TX_CONFIRMATION_MODAL: &'static str = "delete_tx_conf_modal";
|
||||
|
||||
impl WalletTransactionsContent {
|
||||
/// Height of transaction list item.
|
||||
pub const TX_ITEM_HEIGHT: f32 = 75.0;
|
||||
|
||||
/// Create new content instance with opening tx info.
|
||||
pub fn new(tx: Option<WalletTransaction>) -> Self {
|
||||
pub fn new(tx: Option<WalletTx>) -> Self {
|
||||
let mut content = Self {
|
||||
tx_info_content: None,
|
||||
confirm_cancel_tx_id: None,
|
||||
confirm_delete_tx_id: None,
|
||||
manual_sync: None,
|
||||
};
|
||||
if let Some(tx) = &tx {
|
||||
@@ -100,7 +113,10 @@ impl WalletTransactionsContent {
|
||||
});
|
||||
return;
|
||||
}
|
||||
let txs = data.txs.as_ref().unwrap();
|
||||
let txs = data.txs.as_ref().unwrap()
|
||||
.iter()
|
||||
.filter(|tx| !tx.deleting())
|
||||
.collect::<Vec<&WalletTx>>();
|
||||
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
|
||||
// Show message when txs are empty.
|
||||
if txs.is_empty() {
|
||||
@@ -162,7 +178,7 @@ impl WalletTransactionsContent {
|
||||
ui: &mut egui::Ui,
|
||||
row_range: Range<usize>,
|
||||
wallet: &Wallet,
|
||||
txs: &Vec<WalletTransaction>) {
|
||||
txs: Vec<&WalletTx>) {
|
||||
let data = wallet.get_data().unwrap();
|
||||
for index in row_range {
|
||||
if index == txs.len() && ui.is_visible() {
|
||||
@@ -183,34 +199,40 @@ impl WalletTransactionsContent {
|
||||
}
|
||||
return;
|
||||
}
|
||||
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);
|
||||
rect.set_height(Self::TX_ITEM_HEIGHT);
|
||||
|
||||
// Draw tx item background.
|
||||
let mut r = View::item_rounding(index, txs.len(), false);
|
||||
let p = ui.painter();
|
||||
p.rect(rect, r, Colors::fill(), View::item_stroke(), StrokeKind::Outside);
|
||||
|
||||
// Transaction item background setup.
|
||||
let rect = {
|
||||
let mut r = ui.available_rect_before_wrap();
|
||||
r.min += egui::emath::vec2(6.0, 0.0);
|
||||
r.max -= egui::emath::vec2(6.0, 0.0);
|
||||
r.set_height(Self::TX_ITEM_HEIGHT);
|
||||
r
|
||||
};
|
||||
let rounding = View::item_rounding(index, txs.len(), false);
|
||||
let bg = Colors::fill();
|
||||
let tx = txs.get(index).unwrap();
|
||||
Self::tx_item_ui(ui, tx, rect, &data, |ui| {
|
||||
// Draw button to show transaction info.
|
||||
if tx.data.tx_slate_id.is_some() || tx.data.payment_proof.is_some() {
|
||||
let mut show_tx_info = false;
|
||||
// Draw transaction list item.
|
||||
Self::tx_item_ui(ui, tx, rect, bg, rounding, &data, (true, || {
|
||||
show_tx_info = true;
|
||||
}), |ui| {
|
||||
let btn_rounding = {
|
||||
let mut r = rounding.clone();
|
||||
r.nw = 0.0 as u8;
|
||||
r.sw = 0.0 as u8;
|
||||
View::item_button(ui, r, FILE_TEXT, None, || {
|
||||
self.show_tx_info_modal(tx.data.id);
|
||||
r
|
||||
};
|
||||
// Draw button to delete transaction.
|
||||
if tx.data.confirmed || tx.cancelled() {
|
||||
View::item_button(ui, btn_rounding, FILE_X, Some(Colors::inactive_text()), || {
|
||||
self.show_delete_confirmation_modal(tx.data.id);
|
||||
});
|
||||
}
|
||||
|
||||
if wallet.synced_from_node() && !tx.cancelled() && !tx.cancelling() && !tx.posting() {
|
||||
let resend = tx.broadcasting_timed_out(wallet);
|
||||
|
||||
} else if !tx.cancelled() && !tx.cancelling() && !tx.posting() &&
|
||||
wallet.synced_from_node() {
|
||||
let repeat = tx.broadcasting_timed_out(wallet);
|
||||
// Draw button to cancel transaction.
|
||||
if tx.can_cancel() || resend {
|
||||
if tx.can_cancel() || repeat {
|
||||
let (icon, color) = (PROHIBIT, Some(Colors::red()));
|
||||
View::item_button(ui, CornerRadius::default(), icon, color, || {
|
||||
View::item_button(ui, btn_rounding, icon, color, || {
|
||||
self.confirm_cancel_tx_id = Some(tx.data.id);
|
||||
// Show transaction cancellation confirmation modal.
|
||||
Modal::new(CANCEL_TX_CONFIRMATION_MODAL)
|
||||
@@ -219,13 +241,16 @@ impl WalletTransactionsContent {
|
||||
.show();
|
||||
});
|
||||
}
|
||||
|
||||
// Draw button to repeat transaction action.
|
||||
if tx.can_repeat_action() || resend {
|
||||
Self::tx_repeat_button_ui(ui, CornerRadius::default(), tx, wallet, resend);
|
||||
if tx.can_repeat_action(wallet) || repeat {
|
||||
Self::tx_repeat_button_ui(ui, CornerRadius::default(), tx, wallet, repeat);
|
||||
}
|
||||
}
|
||||
});
|
||||
// Show transaction info on click.
|
||||
if show_tx_info {
|
||||
self.show_tx_info_modal(tx.data.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -272,92 +297,135 @@ impl WalletTransactionsContent {
|
||||
|
||||
/// Draw transaction item.
|
||||
pub fn tx_item_ui(ui: &mut egui::Ui,
|
||||
tx: &WalletTransaction,
|
||||
tx: &WalletTx,
|
||||
rect: Rect,
|
||||
bg: Color32,
|
||||
r: CornerRadius,
|
||||
data: &WalletData,
|
||||
on_click: (bool, impl FnOnce()),
|
||||
buttons_ui: impl FnOnce(&mut egui::Ui)) {
|
||||
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Max), |ui| {
|
||||
ui.horizontal_centered(|ui| {
|
||||
// Draw buttons.
|
||||
buttons_ui(ui);
|
||||
});
|
||||
// Draw background.
|
||||
let mut bg_shape = RectShape::new(rect, r, bg, View::item_stroke(), StrokeKind::Outside);
|
||||
let bg_idx = ui.painter().add(bg_shape.clone());
|
||||
let res = ui.scope_builder(
|
||||
UiBuilder::new()
|
||||
.sense(Sense::click())
|
||||
.layout(Layout::right_to_left(Align::Center))
|
||||
.max_rect(rect), |ui| {
|
||||
ui.horizontal_centered(|ui| {
|
||||
// Draw buttons.
|
||||
buttons_ui(ui);
|
||||
});
|
||||
|
||||
ui.with_layout(Layout::left_to_right(Align::Min), |ui| {
|
||||
ui.add_space(6.0);
|
||||
ui.vertical(|ui| {
|
||||
ui.add_space(3.0);
|
||||
ui.with_layout(Layout::left_to_right(Align::Min), |ui| {
|
||||
ui.add_space(6.0);
|
||||
ui.vertical(|ui| {
|
||||
ui.add_space(3.0);
|
||||
|
||||
// Setup transaction amount.
|
||||
let mut amount_text = if tx.data.tx_type == TxLogEntryType::TxSent ||
|
||||
tx.data.tx_type == TxLogEntryType::TxSentCancelled {
|
||||
"-"
|
||||
} else if tx.data.tx_type == TxLogEntryType::TxReceived ||
|
||||
tx.data.tx_type == TxLogEntryType::TxReceivedCancelled {
|
||||
"+"
|
||||
} else {
|
||||
""
|
||||
}.to_string();
|
||||
amount_text = format!("{}{} {}",
|
||||
amount_text,
|
||||
amount_to_hr_string(tx.amount, true),
|
||||
GRIN);
|
||||
// Setup transaction amount.
|
||||
let mut amount_text = if tx.data.tx_type == TxLogEntryType::TxSent ||
|
||||
tx.data.tx_type == TxLogEntryType::TxSentCancelled {
|
||||
"-"
|
||||
} else if tx.data.tx_type == TxLogEntryType::TxReceived ||
|
||||
tx.data.tx_type == TxLogEntryType::TxReceivedCancelled {
|
||||
"+"
|
||||
} else {
|
||||
""
|
||||
}.to_string();
|
||||
amount_text = format!("{}{} {}",
|
||||
amount_text,
|
||||
amount_to_hr_string(tx.amount, true),
|
||||
GRIN);
|
||||
|
||||
// Setup amount color.
|
||||
let amount_color = match tx.data.tx_type {
|
||||
TxLogEntryType::ConfirmedCoinbase => Colors::white_or_black(true),
|
||||
TxLogEntryType::TxReceived => Colors::white_or_black(true),
|
||||
TxLogEntryType::TxSent => Colors::white_or_black(true),
|
||||
TxLogEntryType::TxReceivedCancelled => Colors::text(false),
|
||||
TxLogEntryType::TxSentCancelled => Colors::text(false),
|
||||
TxLogEntryType::TxReverted => Colors::text(false)
|
||||
};
|
||||
ui.with_layout(Layout::left_to_right(Align::Min), |ui| {
|
||||
ui.add_space(1.0);
|
||||
View::ellipsize_text(ui, amount_text, 18.0, amount_color);
|
||||
});
|
||||
ui.add_space(-2.0);
|
||||
// Setup amount color.
|
||||
let amount_color = match tx.data.tx_type {
|
||||
TxLogEntryType::ConfirmedCoinbase => Colors::white_or_black(true),
|
||||
TxLogEntryType::TxReceived => Colors::white_or_black(true),
|
||||
TxLogEntryType::TxSent => Colors::white_or_black(true),
|
||||
TxLogEntryType::TxReceivedCancelled => Colors::text(false),
|
||||
TxLogEntryType::TxSentCancelled => Colors::text(false),
|
||||
TxLogEntryType::TxReverted => Colors::text(false)
|
||||
};
|
||||
ui.with_layout(Layout::left_to_right(Align::Min), |ui| {
|
||||
ui.add_space(1.0);
|
||||
View::ellipsize_text(ui, amount_text, 18.0, amount_color);
|
||||
});
|
||||
ui.add_space(-2.0);
|
||||
|
||||
// Setup transaction status text.
|
||||
let 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;
|
||||
if is_canceled {
|
||||
format!("{} {}", X_CIRCLE, t!("wallets.tx_canceled"))
|
||||
} else if let Some(a) = &tx.action {
|
||||
let error = if tx.action_error.is_none() {
|
||||
"".to_string()
|
||||
// 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;
|
||||
if is_canceled {
|
||||
format!("{} {}", X_CIRCLE, t!("wallets.tx_canceled"))
|
||||
} else if let Some(action) = &tx.action {
|
||||
let error = if tx.action_error.is_none() {
|
||||
"".to_string()
|
||||
} else {
|
||||
format!("{}: ", t!("error"))
|
||||
};
|
||||
let status = match action {
|
||||
WalletTxAction::Finalizing => t!("wallets.tx_finalizing"),
|
||||
WalletTxAction::Posting => t!("wallets.tx_posting"),
|
||||
WalletTxAction::SendingTor => t!("transport.tor_sending"),
|
||||
_ => t!("wallets.tx_cancelling")
|
||||
};
|
||||
let icon = if error.is_empty() {
|
||||
DOTS_THREE_CIRCLE
|
||||
} else {
|
||||
WARNING
|
||||
};
|
||||
format!("{} {}{}", icon, error, status)
|
||||
} else {
|
||||
format!("{}: ", t!("error"))
|
||||
};
|
||||
let status = match a {
|
||||
WalletTransactionAction::Cancelling => t!("wallets.tx_cancelling"),
|
||||
WalletTransactionAction::Finalizing => t!("wallets.tx_finalizing"),
|
||||
WalletTransactionAction::Posting => t!("wallets.tx_posting"),
|
||||
WalletTransactionAction::SendingTor => t!("transport.tor_sending")
|
||||
};
|
||||
let icon = if error.is_empty() {
|
||||
DOTS_THREE_CIRCLE
|
||||
} else {
|
||||
WARNING
|
||||
};
|
||||
format!("{} {}{}", icon, error, status)
|
||||
match tx.data.tx_type {
|
||||
TxLogEntryType::TxReceived => {
|
||||
let text = match tx.finalized() {
|
||||
true => t!("wallets.await_fin_amount"),
|
||||
false => t!("wallets.tx_receiving")
|
||||
};
|
||||
format!("{} {}", DOTS_THREE_CIRCLE, text)
|
||||
},
|
||||
TxLogEntryType::TxSent => {
|
||||
let text = match tx.finalized() {
|
||||
true => t!("wallets.await_fin_amount"),
|
||||
false => t!("wallets.tx_sending")
|
||||
};
|
||||
format!("{} {}", DOTS_THREE_CIRCLE, text)
|
||||
},
|
||||
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_confirming"))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
match tx.data.tx_type {
|
||||
TxLogEntryType::TxReceived => {
|
||||
let text = match tx.finalized() {
|
||||
true => t!("wallets.await_fin_amount"),
|
||||
false => t!("wallets.tx_receiving")
|
||||
};
|
||||
format!("{} {}", DOTS_THREE_CIRCLE, text)
|
||||
},
|
||||
TxLogEntryType::TxSent => {
|
||||
let text = match tx.finalized() {
|
||||
true => t!("wallets.await_fin_amount"),
|
||||
false => t!("wallets.tx_sending")
|
||||
};
|
||||
format!("{} {}", DOTS_THREE_CIRCLE, text)
|
||||
},
|
||||
TxLogEntryType::ConfirmedCoinbase => {
|
||||
let tx_h = tx.height.unwrap_or(1) - 1;
|
||||
if tx_h != 0 {
|
||||
@@ -374,134 +442,115 @@ impl WalletTransactionsContent {
|
||||
} else {
|
||||
format!("{} {}",
|
||||
DOTS_THREE_CIRCLE,
|
||||
t!("wallets.tx_confirming"))
|
||||
t!("wallets.tx_confirmed"))
|
||||
}
|
||||
} else {
|
||||
format!("{} {}",
|
||||
DOTS_THREE_CIRCLE,
|
||||
t!("wallets.tx_confirming"))
|
||||
}
|
||||
},
|
||||
_ => {
|
||||
format!("{} {}",
|
||||
DOTS_THREE_CIRCLE,
|
||||
t!("wallets.tx_confirming"))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
match tx.data.tx_type {
|
||||
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_confirmed"))
|
||||
}
|
||||
} else {
|
||||
format!("{} {}",
|
||||
DOTS_THREE_CIRCLE,
|
||||
t!("wallets.tx_confirmed"))
|
||||
}
|
||||
|
||||
},
|
||||
TxLogEntryType::TxSent | TxLogEntryType::TxReceived => {
|
||||
let min_conf = data.info.minimum_confirmations;
|
||||
if tx.height.is_none() || (tx.height.unwrap() != 0 &&
|
||||
height - tx.height.unwrap() >= min_conf - 1) {
|
||||
let (i, t) = if tx.data.tx_type == TxLogEntryType::TxSent {
|
||||
(ARROW_CIRCLE_UP, t!("wallets.tx_sent"))
|
||||
},
|
||||
TxLogEntryType::TxSent | TxLogEntryType::TxReceived => {
|
||||
let min_conf = data.info.minimum_confirmations;
|
||||
if tx.height.is_none() || (tx.height.unwrap() != 0 &&
|
||||
height - tx.height.unwrap() >= min_conf - 1) {
|
||||
let (i, t) = if tx.data.tx_type == TxLogEntryType::TxSent {
|
||||
(ARROW_CIRCLE_UP, t!("wallets.tx_sent"))
|
||||
} else {
|
||||
(ARROW_CIRCLE_DOWN, t!("wallets.tx_received"))
|
||||
};
|
||||
format!("{} {}", i, t)
|
||||
} else {
|
||||
(ARROW_CIRCLE_DOWN, t!("wallets.tx_received"))
|
||||
};
|
||||
format!("{} {}", i, t)
|
||||
} else {
|
||||
let tx_height = tx.height.unwrap() - 1;
|
||||
let left_conf = height - tx_height;
|
||||
let conf_info = if tx_height != 0 && height >= tx_height &&
|
||||
left_conf < min_conf {
|
||||
format!("{}/{}", left_conf, min_conf)
|
||||
} else {
|
||||
"".to_string()
|
||||
};
|
||||
format!("{} {} {}",
|
||||
DOTS_THREE_CIRCLE,
|
||||
t!("wallets.tx_confirming"),
|
||||
conf_info
|
||||
)
|
||||
}
|
||||
let tx_height = tx.height.unwrap() - 1;
|
||||
let left_conf = height - tx_height;
|
||||
let conf_info = if tx_height != 0 && height >= tx_height &&
|
||||
left_conf < min_conf {
|
||||
format!("{}/{}", left_conf, min_conf)
|
||||
} else {
|
||||
"".to_string()
|
||||
};
|
||||
format!("{} {} {}",
|
||||
DOTS_THREE_CIRCLE,
|
||||
t!("wallets.tx_confirming"),
|
||||
conf_info
|
||||
)
|
||||
}
|
||||
},
|
||||
_ => format!("{} {}", X_CIRCLE, t!("wallets.canceled"))
|
||||
}
|
||||
};
|
||||
|
||||
// Setup status text color.
|
||||
let status_color = match tx.data.tx_type {
|
||||
TxLogEntryType::ConfirmedCoinbase => Colors::text(false),
|
||||
TxLogEntryType::TxReceived => if tx.data.confirmed {
|
||||
Colors::green()
|
||||
} else {
|
||||
Colors::text(false)
|
||||
},
|
||||
_ => format!("{} {}", X_CIRCLE, t!("wallets.canceled"))
|
||||
}
|
||||
};
|
||||
TxLogEntryType::TxSent => if tx.data.confirmed {
|
||||
Colors::red()
|
||||
} else {
|
||||
Colors::text(false)
|
||||
},
|
||||
TxLogEntryType::TxReceivedCancelled => Colors::inactive_text(),
|
||||
TxLogEntryType::TxSentCancelled => Colors::inactive_text(),
|
||||
TxLogEntryType::TxReverted => Colors::inactive_text(),
|
||||
};
|
||||
View::ellipsize_text(ui, status_text, 15.0, status_color);
|
||||
|
||||
// Setup status text color.
|
||||
let status_color = match tx.data.tx_type {
|
||||
TxLogEntryType::ConfirmedCoinbase => Colors::text(false),
|
||||
TxLogEntryType::TxReceived => if tx.data.confirmed {
|
||||
Colors::green()
|
||||
} else {
|
||||
Colors::text(false)
|
||||
},
|
||||
TxLogEntryType::TxSent => if tx.data.confirmed {
|
||||
Colors::red()
|
||||
} else {
|
||||
Colors::text(false)
|
||||
},
|
||||
TxLogEntryType::TxReceivedCancelled => Colors::inactive_text(),
|
||||
TxLogEntryType::TxSentCancelled => Colors::inactive_text(),
|
||||
TxLogEntryType::TxReverted => Colors::inactive_text(),
|
||||
};
|
||||
View::ellipsize_text(ui, status_text, 15.0, status_color);
|
||||
|
||||
// Setup transaction time.
|
||||
let tx_time = View::format_time(tx.data.creation_ts.timestamp());
|
||||
let tx_time_text = format!("{} {}", CALENDAR_CHECK, tx_time);
|
||||
ui.label(RichText::new(tx_time_text).size(15.0).color(Colors::gray()));
|
||||
ui.add_space(3.0);
|
||||
// Setup transaction time.
|
||||
let tx_time = View::format_time(tx.data.creation_ts.timestamp());
|
||||
let tx_time_text = format!("{} {}", CALENDAR_CHECK, tx_time);
|
||||
ui.label(RichText::new(tx_time_text).size(15.0).color(Colors::gray()));
|
||||
ui.add_space(4.0);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
).response;
|
||||
let (clickable, on_click) = on_click;
|
||||
let clicked = res.clicked() || res.long_touched();
|
||||
// Setup background and cursor.
|
||||
if clickable && res.hovered() {
|
||||
res.on_hover_cursor(CursorIcon::PointingHand);
|
||||
bg_shape.fill = Colors::TRANSPARENT;
|
||||
}
|
||||
ui.painter().set(bg_idx, bg_shape);
|
||||
// Handle clicks on layout.
|
||||
if clicked && clickable {
|
||||
on_click();
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw button to repeat transaction action on error or repost.
|
||||
pub fn tx_repeat_button_ui(ui: &mut egui::Ui,
|
||||
rounding: CornerRadius,
|
||||
tx: &WalletTransaction,
|
||||
tx: &WalletTx,
|
||||
wallet: &Wallet,
|
||||
repost: bool) {
|
||||
let (icon, color) = (ARROWS_CLOCKWISE, Some(Colors::green()));
|
||||
View::item_button(ui, rounding, icon, color, || {
|
||||
if repost {
|
||||
wallet.task(WalletTask::Post(tx.data.id));
|
||||
} else {
|
||||
match tx.action.as_ref().unwrap() {
|
||||
WalletTransactionAction::Cancelling => {
|
||||
wallet.task(WalletTask::Cancel(tx.data.clone()));
|
||||
}
|
||||
WalletTransactionAction::Finalizing => {
|
||||
} else if let Some(action) = tx.action.as_ref() {
|
||||
match action {
|
||||
WalletTxAction::Finalizing => {
|
||||
wallet.task(WalletTask::Finalize(tx.data.id));
|
||||
}
|
||||
WalletTransactionAction::Posting => {
|
||||
WalletTxAction::Posting => {
|
||||
wallet.task(WalletTask::Post(tx.data.id));
|
||||
}
|
||||
WalletTransactionAction::SendingTor => {
|
||||
_ => {
|
||||
if let Some(a) = &tx.receiver {
|
||||
wallet.task(WalletTask::SendTor(tx.data.id, a.clone()));
|
||||
wallet.task(WalletTask::SendTor(tx.data.clone(), a.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if let Some(a) = &tx.receiver {
|
||||
wallet.task(WalletTask::SendTor(tx.data.clone(), a.clone()));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -521,8 +570,8 @@ impl WalletTransactionsContent {
|
||||
let data = wallet.get_data().unwrap();
|
||||
let data_txs = data.txs.unwrap();
|
||||
let txs = data_txs.into_iter()
|
||||
.filter(|tx| tx.data.id == self.confirm_cancel_tx_id.unwrap())
|
||||
.collect::<Vec<WalletTransaction>>();
|
||||
.filter(|tx| tx.data.id == self.confirm_cancel_tx_id.unwrap_or_default())
|
||||
.collect::<Vec<WalletTx>>();
|
||||
if txs.is_empty() {
|
||||
Modal::close();
|
||||
return;
|
||||
@@ -561,7 +610,7 @@ impl WalletTransactionsContent {
|
||||
});
|
||||
columns[1].vertical_centered_justified(|ui| {
|
||||
View::button(ui, "OK".to_string(), Colors::white_or_black(false), || {
|
||||
wallet.task(WalletTask::Cancel(tx.data.clone()));
|
||||
wallet.task(WalletTask::Cancel(tx.data.id));
|
||||
self.confirm_cancel_tx_id = None;
|
||||
Modal::close();
|
||||
});
|
||||
@@ -570,6 +619,62 @@ impl WalletTransactionsContent {
|
||||
ui.add_space(6.0);
|
||||
});
|
||||
}
|
||||
|
||||
/// Show transaction deletion confirmation [`Modal`].
|
||||
fn show_delete_confirmation_modal(&mut self, id: u32) {
|
||||
self.confirm_delete_tx_id = Some(id);
|
||||
// Show transaction deletion confirmation modal.
|
||||
Modal::new(DELETE_TX_CONFIRMATION_MODAL)
|
||||
.position(ModalPosition::Center)
|
||||
.title(t!("confirmation"))
|
||||
.show();
|
||||
}
|
||||
|
||||
/// Confirmation [`Modal`] to delete transaction.
|
||||
fn delete_confirmation_modal(&mut self, ui: &mut egui::Ui, wallet: &Wallet) {
|
||||
let data = wallet.get_data().unwrap();
|
||||
let data_txs = data.txs.unwrap();
|
||||
let txs = data_txs.into_iter()
|
||||
.filter(|tx| tx.data.id == self.confirm_delete_tx_id.unwrap_or_default())
|
||||
.collect::<Vec<WalletTx>>();
|
||||
if txs.is_empty() {
|
||||
Modal::close();
|
||||
return;
|
||||
}
|
||||
let tx = txs.get(0).unwrap();
|
||||
|
||||
// Show confirmation text.
|
||||
ui.add_space(6.0);
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.label(RichText::new(t!("wallets.tx_delete_confirmation"))
|
||||
.size(17.0)
|
||||
.color(Colors::text(false)));
|
||||
ui.add_space(8.0);
|
||||
});
|
||||
|
||||
// Show modal buttons.
|
||||
ui.scope(|ui| {
|
||||
// Setup spacing between buttons.
|
||||
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
|
||||
|
||||
ui.columns(2, |columns| {
|
||||
columns[0].vertical_centered_justified(|ui| {
|
||||
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
|
||||
self.confirm_delete_tx_id = None;
|
||||
Modal::close();
|
||||
});
|
||||
});
|
||||
columns[1].vertical_centered_justified(|ui| {
|
||||
View::button(ui, "OK".to_string(), Colors::white_or_black(false), || {
|
||||
wallet.task(WalletTask::Delete(tx.data.id));
|
||||
self.confirm_delete_tx_id = None;
|
||||
Modal::close();
|
||||
});
|
||||
});
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw awaiting balance item content.
|
||||
|
||||
@@ -19,14 +19,15 @@ use grin_util::ToHex;
|
||||
use grin_wallet_libwallet::TxLogEntryType;
|
||||
use std::fs;
|
||||
|
||||
use crate::gui::icons::{CIRCLE_HALF, COPY, CUBE, FILE_ARCHIVE, FILE_TEXT, HASH_STRAIGHT, PROHIBIT, QR_CODE, SEAL_CHECK};
|
||||
use crate::gui::icons::{CIRCLE_HALF, COPY, CUBE, FILE_ARCHIVE, FILE_TEXT, FILE_X, HASH_STRAIGHT, PROHIBIT, QR_CODE, SEAL_CHECK};
|
||||
use crate::gui::platform::PlatformCallbacks;
|
||||
use crate::gui::views::wallets::wallet::proof::PaymentProofContent;
|
||||
use crate::gui::views::wallets::wallet::txs::WalletTransactionsContent;
|
||||
use crate::gui::views::{Modal, QrCodeContent, View};
|
||||
use crate::gui::Colors;
|
||||
use crate::wallet::types::{WalletTask, WalletTransaction};
|
||||
use crate::wallet::types::{WalletTask, WalletTx};
|
||||
use crate::wallet::Wallet;
|
||||
use crate::AppConfig;
|
||||
|
||||
/// Transaction information [`Modal`] content.
|
||||
pub struct WalletTransactionContent {
|
||||
@@ -43,7 +44,7 @@ pub struct WalletTransactionContent {
|
||||
}
|
||||
|
||||
impl WalletTransactionContent {
|
||||
/// Create new content instance with [`Wallet`] from provided [`WalletTransaction`].
|
||||
/// Create new content instance with [`Wallet`] from provided [`WalletTx`].
|
||||
pub fn new(tx_id: u32) -> Self {
|
||||
Self {
|
||||
tx_id,
|
||||
@@ -54,7 +55,12 @@ impl WalletTransactionContent {
|
||||
}
|
||||
|
||||
/// Draw [`Modal`] content.
|
||||
pub fn ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) {
|
||||
pub fn ui(&mut self,
|
||||
ui: &mut egui::Ui,
|
||||
modal: &Modal,
|
||||
wallet: &Wallet,
|
||||
cb: &dyn PlatformCallbacks,
|
||||
on_delete: impl FnOnce(u32)) {
|
||||
// Check values and setup transaction data.
|
||||
let wallet_data = wallet.get_data();
|
||||
if wallet_data.is_none() {
|
||||
@@ -65,7 +71,7 @@ impl WalletTransactionContent {
|
||||
let data_txs = data.txs.clone().unwrap();
|
||||
let txs = data_txs.into_iter()
|
||||
.filter(|tx| tx.data.id == self.tx_id)
|
||||
.collect::<Vec<WalletTransaction>>();
|
||||
.collect::<Vec<WalletTx>>();
|
||||
if txs.is_empty() {
|
||||
Modal::close();
|
||||
return;
|
||||
@@ -73,6 +79,12 @@ impl WalletTransactionContent {
|
||||
let tx = txs.get(0).unwrap();
|
||||
|
||||
if let Some(content) = self.qr_code_content.as_mut() {
|
||||
let dark_theme = AppConfig::dark_theme().unwrap_or(false);
|
||||
// Set light theme for better scanning.
|
||||
AppConfig::set_dark_theme(false);
|
||||
crate::setup_visuals(ui.ctx());
|
||||
modal.set_background_color(Colors::FILL_DEEP);
|
||||
|
||||
ui.add_space(6.0);
|
||||
content.ui(ui, cb);
|
||||
|
||||
@@ -93,9 +105,13 @@ impl WalletTransactionContent {
|
||||
});
|
||||
});
|
||||
});
|
||||
// Set color theme back.
|
||||
AppConfig::set_dark_theme(dark_theme);
|
||||
crate::setup_visuals(ui.ctx());
|
||||
} else {
|
||||
modal.set_background_color(Colors::fill());
|
||||
// Show transaction information.
|
||||
self.info_ui(ui, tx, wallet, cb);
|
||||
self.info_ui(ui, tx, wallet, cb, on_delete);
|
||||
|
||||
// Show transaction sharing content or payment proof.
|
||||
if self.proof_content.is_none() && tx.can_cancel() && !tx.finalized() {
|
||||
@@ -140,7 +156,7 @@ impl WalletTransactionContent {
|
||||
fn share_ui(&mut self,
|
||||
ui: &mut egui::Ui,
|
||||
wallet: &Wallet,
|
||||
tx: &WalletTransaction,
|
||||
tx: &WalletTx,
|
||||
cb: &dyn PlatformCallbacks) {
|
||||
if self.message.is_none() {
|
||||
let slatepack_path = wallet.get_config().get_tx_slate_path(tx);
|
||||
@@ -243,22 +259,27 @@ impl WalletTransactionContent {
|
||||
/// Draw transaction information content.
|
||||
fn info_ui(&mut self,
|
||||
ui: &mut egui::Ui,
|
||||
tx: &WalletTransaction,
|
||||
tx: &WalletTx,
|
||||
wallet: &Wallet,
|
||||
cb: &dyn PlatformCallbacks) {
|
||||
cb: &dyn PlatformCallbacks,
|
||||
on_delete: impl FnOnce(u32)) {
|
||||
ui.add_space(6.0);
|
||||
|
||||
// Transaction item background setup.
|
||||
let mut rect = ui.available_rect_before_wrap();
|
||||
rect.set_height(WalletTransactionsContent::TX_ITEM_HEIGHT);
|
||||
|
||||
// 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(), StrokeKind::Outside);
|
||||
|
||||
let rounding = View::item_rounding(0, 2, false);
|
||||
let bg = Colors::TRANSPARENT;
|
||||
// Show transaction amount status and time.
|
||||
let data = wallet.get_data().unwrap();
|
||||
WalletTransactionsContent::tx_item_ui(ui, tx, rect, &data, |ui| {
|
||||
let on_click = (false, || {});
|
||||
WalletTransactionsContent::tx_item_ui(ui, tx, rect, bg, rounding, &data, on_click, |ui| {
|
||||
// Show button to delete transaction from database.
|
||||
if tx.data.confirmed || tx.cancelled() {
|
||||
let r = View::item_rounding(0, 2, true);
|
||||
View::item_button(ui, r, FILE_X, Some(Colors::inactive_text()), || {
|
||||
on_delete(tx.data.id);
|
||||
});
|
||||
}
|
||||
// Show block height or buttons.
|
||||
if let Some(h) = tx.height {
|
||||
if h != 0 {
|
||||
@@ -273,27 +294,24 @@ impl WalletTransactionContent {
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if wallet.synced_from_node() && !tx.cancelled() && !tx.cancelling() && !tx.posting() {
|
||||
let rebroadcast = tx.broadcasting_timed_out(&wallet);
|
||||
|
||||
let repeat = tx.broadcasting_timed_out(&wallet);
|
||||
// Draw button to cancel transaction.
|
||||
if tx.can_cancel() || rebroadcast {
|
||||
if tx.can_cancel() || repeat {
|
||||
let r = View::item_rounding(0, 2, true);
|
||||
View::item_button(ui, r, PROHIBIT, Some(Colors::red()), || {
|
||||
wallet.task(WalletTask::Cancel(tx.data.clone()));
|
||||
wallet.task(WalletTask::Cancel(tx.data.id));
|
||||
Modal::close();
|
||||
});
|
||||
}
|
||||
|
||||
// Draw button to repeat transaction action.
|
||||
if tx.can_repeat_action() || rebroadcast {
|
||||
if tx.can_repeat_action(wallet) || repeat {
|
||||
let r = if tx.can_finalize() || tx.can_cancel() {
|
||||
CornerRadius::default()
|
||||
} else {
|
||||
View::item_rounding(0, 2, true)
|
||||
};
|
||||
WalletTransactionsContent::tx_repeat_button_ui(ui, r, tx, wallet, rebroadcast);
|
||||
WalletTransactionsContent::tx_repeat_button_ui(ui, r, tx, wallet, repeat);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -44,7 +44,7 @@ pub trait WalletContentContainer {
|
||||
|
||||
/// Get wallet status text.
|
||||
pub fn wallet_status_text(wallet: &Wallet) -> String {
|
||||
if wallet.sync_error() {
|
||||
if wallet.sync_error() && wallet.is_open() {
|
||||
format!("{} {}", WARNING_CIRCLE, t!("error"))
|
||||
} else if wallet.is_closing() {
|
||||
format!("{} {}", SPINNER, t!("wallets.closing"))
|
||||
|
||||
+7
-12
@@ -19,8 +19,8 @@ rust_i18n::i18n!("locales");
|
||||
use eframe::NativeOptions;
|
||||
use egui::{Context, Stroke, Theme};
|
||||
use lazy_static::lazy_static;
|
||||
use std::sync::Arc;
|
||||
use parking_lot::RwLock;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
use winit::platform::android::activity::AndroidApp;
|
||||
@@ -28,9 +28,9 @@ use winit::platform::android::activity::AndroidApp;
|
||||
pub use settings::AppConfig;
|
||||
pub use settings::Settings;
|
||||
|
||||
use crate::gui::{Colors, App};
|
||||
use crate::gui::platform::PlatformCallbacks;
|
||||
use crate::gui::views::View;
|
||||
use crate::gui::{App, Colors};
|
||||
use crate::node::Node;
|
||||
|
||||
mod node;
|
||||
@@ -39,22 +39,17 @@ mod tor;
|
||||
mod settings;
|
||||
mod http;
|
||||
pub mod gui;
|
||||
pub mod logger;
|
||||
|
||||
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
/// Android platform entry point.
|
||||
#[allow(dead_code)]
|
||||
#[cfg(target_os = "android")]
|
||||
#[no_mangle]
|
||||
#[unsafe(no_mangle)]
|
||||
fn android_main(app: AndroidApp) {
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
std::env::set_var("RUST_BACKTRACE", "full");
|
||||
let log_config = android_logger::Config::default()
|
||||
.with_max_level(log::LevelFilter::Info)
|
||||
.with_tag("grim");
|
||||
android_logger::init_once(log_config);
|
||||
}
|
||||
// Setup logger.
|
||||
logger::init_logger();
|
||||
|
||||
use gui::platform::Android;
|
||||
let platform = Android::new(app.clone());
|
||||
@@ -278,7 +273,7 @@ lazy_static! {
|
||||
#[allow(dead_code)]
|
||||
#[allow(non_snake_case)]
|
||||
#[cfg(target_os = "android")]
|
||||
#[no_mangle]
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn Java_mw_gri_android_MainActivity_onData(
|
||||
_env: jni::JNIEnv,
|
||||
_class: jni::objects::JObject,
|
||||
|
||||
+144
@@ -0,0 +1,144 @@
|
||||
// Copyright 2026 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::{panic, thread};
|
||||
use std::fs::File;
|
||||
use backtrace::Backtrace;
|
||||
use log4rs::append::Append;
|
||||
use log4rs::append::console::ConsoleAppender;
|
||||
use log4rs::append::rolling_file::policy::compound::CompoundPolicy;
|
||||
use log4rs::append::rolling_file::policy::compound::roll::fixed_window::FixedWindowRoller;
|
||||
use log4rs::append::rolling_file::policy::compound::trigger::size::SizeTrigger;
|
||||
use log4rs::append::rolling_file::RollingFileAppender;
|
||||
use log4rs::Config;
|
||||
use log4rs::config::{Appender, Root};
|
||||
use log4rs::encode::pattern::PatternEncoder;
|
||||
use log4rs::filter::threshold::ThresholdFilter;
|
||||
use log::{error, LevelFilter};
|
||||
|
||||
use crate::Settings;
|
||||
|
||||
const LOGGING_PATTERN: &str = "{d(%Y%m%d %H:%M:%S%.3f)} {h({l})} {M} - {m}{n}";
|
||||
|
||||
/// 32 log files to rotate over by default.
|
||||
const ROTATE_LOG_FILES: u32 = 32;
|
||||
/// Size of the log in bytes to rotate over (6 megabytes).
|
||||
const MAX_FILE_SIZE: u64 = 1024 * 1024 * 6;
|
||||
|
||||
/// Include build information.
|
||||
pub mod built_info {
|
||||
include!(concat!(env!("OUT_DIR"), "/built.rs"));
|
||||
}
|
||||
|
||||
/// Initialize the logger.
|
||||
pub fn init_logger() {
|
||||
let stdout = ConsoleAppender::builder()
|
||||
.encoder(Box::new(PatternEncoder::new(&LOGGING_PATTERN)))
|
||||
.build();
|
||||
|
||||
let mut root = Root::builder();
|
||||
|
||||
let mut app = vec![];
|
||||
app.push(
|
||||
Appender::builder()
|
||||
.filter(Box::new(ThresholdFilter::new(LevelFilter::Info)))
|
||||
.build("stdout", Box::new(stdout)),
|
||||
);
|
||||
root = root.appender("stdout");
|
||||
|
||||
// Setup file logging.
|
||||
let filter = Box::new(ThresholdFilter::new(LevelFilter::Info));
|
||||
let file: Box<dyn Append> = {
|
||||
let path = Settings::log_path();
|
||||
let roller = FixedWindowRoller::builder()
|
||||
.build(&format!("{}.{{}}.gz", path), ROTATE_LOG_FILES)
|
||||
.unwrap();
|
||||
let trigger = SizeTrigger::new(MAX_FILE_SIZE);
|
||||
let policy = CompoundPolicy::new(Box::new(trigger), Box::new(roller));
|
||||
Box::new(
|
||||
RollingFileAppender::builder()
|
||||
.append(true)
|
||||
.encoder(Box::new(PatternEncoder::new(&LOGGING_PATTERN)))
|
||||
.build(path, Box::new(policy))
|
||||
.expect("Failed to create logfile"),
|
||||
)
|
||||
};
|
||||
app.push(
|
||||
Appender::builder()
|
||||
.filter(filter)
|
||||
.build("file", file),
|
||||
);
|
||||
root = root.appender("file");
|
||||
|
||||
let config = Config::builder()
|
||||
.appenders(app)
|
||||
.build(root.build(LevelFilter::Info))
|
||||
.unwrap();
|
||||
let _ = log4rs::init_config(config).unwrap();
|
||||
|
||||
log::info!("{}", build_info());
|
||||
|
||||
send_panic_to_log();
|
||||
}
|
||||
|
||||
/// Get information about application build.
|
||||
fn build_info() -> String {
|
||||
format!(
|
||||
"This is Grim version {}, built for {} by {}.",
|
||||
built_info::PKG_VERSION,
|
||||
built_info::TARGET,
|
||||
built_info::RUSTC_VERSION,
|
||||
)
|
||||
}
|
||||
|
||||
/// Hook to send panics to logs as well as stderr.
|
||||
fn send_panic_to_log() {
|
||||
panic::set_hook(Box::new(|info| {
|
||||
let backtrace = Backtrace::new();
|
||||
|
||||
let thread = thread::current();
|
||||
let thread = thread.name().unwrap_or("unnamed");
|
||||
|
||||
let msg = match info.payload().downcast_ref::<&'static str>() {
|
||||
Some(s) => *s,
|
||||
None => match info.payload().downcast_ref::<String>() {
|
||||
Some(s) => &**s,
|
||||
None => "Box<Any>",
|
||||
},
|
||||
};
|
||||
|
||||
match info.location() {
|
||||
Some(location) => {
|
||||
error!(
|
||||
"{}\nThread '{}' panicked at '{}': {}:{}{:?}\n\n",
|
||||
build_info(),
|
||||
thread,
|
||||
msg,
|
||||
location.file(),
|
||||
location.line(),
|
||||
backtrace
|
||||
);
|
||||
}
|
||||
None => error!("Thread '{}' panicked at '{}'{:?}", thread, msg, backtrace),
|
||||
}
|
||||
// Also print to stderr.
|
||||
eprintln!(
|
||||
"Thread '{}' panicked with message:\n\"{}\"\nSee {} for further details.",
|
||||
thread, msg, Settings::log_path()
|
||||
);
|
||||
// Create file to show report send on launch.
|
||||
let log = Settings::crash_check_path();
|
||||
let _ = File::create(log);
|
||||
}));
|
||||
}
|
||||
+12
-60
@@ -23,11 +23,8 @@ pub fn main() {
|
||||
#[allow(dead_code)]
|
||||
#[cfg(not(target_os = "android"))]
|
||||
fn real_main() {
|
||||
env_logger::builder()
|
||||
.filter_level(log::LevelFilter::Info)
|
||||
.parse_default_env()
|
||||
.init();
|
||||
|
||||
// Initialize logger.
|
||||
grim::logger::init_logger();
|
||||
// Handle file path argument passing.
|
||||
let args: Vec<_> = std::env::args().collect();
|
||||
let mut data = None;
|
||||
@@ -40,50 +37,15 @@ fn real_main() {
|
||||
data = content
|
||||
}
|
||||
|
||||
// Setup callback on panic crash.
|
||||
std::panic::set_hook(Box::new(|info| {
|
||||
// Format error.
|
||||
let backtrace = backtrace::Backtrace::new();
|
||||
let time = grim::gui::views::View::format_time(chrono::Utc::now().timestamp());
|
||||
let os = egui::os::OperatingSystem::from_target_os();
|
||||
let ver = grim::VERSION;
|
||||
let msg = panic_info_message(info);
|
||||
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() {
|
||||
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();
|
||||
}
|
||||
} else {
|
||||
std::fs::write(log, err.as_bytes()).unwrap_or_default();
|
||||
}
|
||||
// Print message error.
|
||||
println!("{}\n{}", msg, loc);
|
||||
}));
|
||||
|
||||
// Start GUI.
|
||||
let _ = std::panic::catch_unwind(|| {
|
||||
if is_app_running(&data) {
|
||||
return;
|
||||
} else if let Some(data) = data {
|
||||
grim::on_data(data);
|
||||
}
|
||||
let platform = grim::gui::platform::Desktop::new();
|
||||
start_app_socket(platform.clone());
|
||||
start_desktop_gui(platform);
|
||||
});
|
||||
if is_app_running(&data) {
|
||||
return;
|
||||
} else if let Some(data) = data {
|
||||
grim::on_data(data);
|
||||
}
|
||||
let platform = grim::gui::platform::Desktop::new();
|
||||
start_app_socket(platform.clone());
|
||||
start_desktop_gui(platform);
|
||||
}
|
||||
|
||||
/// Get panic message from crash payload.
|
||||
@@ -133,14 +95,8 @@ fn start_desktop_gui(platform: grim::gui::platform::Desktop) {
|
||||
.with_transparent(true)
|
||||
.with_decorations(is_mac || is_win);
|
||||
|
||||
let renderer = if is_mac {
|
||||
eframe::Renderer::Glow
|
||||
} else {
|
||||
eframe::Renderer::Wgpu
|
||||
};
|
||||
|
||||
let mut options = eframe::NativeOptions {
|
||||
renderer,
|
||||
renderer: eframe::Renderer::Glow,
|
||||
viewport,
|
||||
..Default::default()
|
||||
};
|
||||
@@ -151,11 +107,7 @@ fn start_desktop_gui(platform: grim::gui::platform::Desktop) {
|
||||
Ok(_) => {}
|
||||
Err(_) => {
|
||||
// Start with another renderer on error.
|
||||
if is_mac {
|
||||
options.renderer = eframe::Renderer::Wgpu;
|
||||
} else {
|
||||
options.renderer = eframe::Renderer::Glow;
|
||||
}
|
||||
options.renderer = eframe::Renderer::Wgpu;
|
||||
|
||||
let app = grim::gui::App::new(platform);
|
||||
match grim::start(options, grim::app_creator(app)) {
|
||||
|
||||
+12
-12
@@ -710,7 +710,7 @@ pub fn start_stratum_mining_server(server: &Server, config: StratumServerConfig)
|
||||
#[allow(dead_code)]
|
||||
#[cfg(target_os = "android")]
|
||||
#[allow(non_snake_case)]
|
||||
#[no_mangle]
|
||||
#[unsafe(no_mangle)]
|
||||
/// Get sync status text for Android notification from [`NODE_STATE`] in Java string format.
|
||||
pub extern "C" fn Java_mw_gri_android_BackgroundService_getSyncStatusText(
|
||||
_env: jni::JNIEnv,
|
||||
@@ -725,7 +725,7 @@ pub extern "C" fn Java_mw_gri_android_BackgroundService_getSyncStatusText(
|
||||
#[allow(dead_code)]
|
||||
#[cfg(target_os = "android")]
|
||||
#[allow(non_snake_case)]
|
||||
#[no_mangle]
|
||||
#[unsafe(no_mangle)]
|
||||
/// Get sync title for Android notification in Java string format.
|
||||
pub extern "C" fn Java_mw_gri_android_BackgroundService_getSyncTitle(
|
||||
_env: jni::JNIEnv,
|
||||
@@ -739,7 +739,7 @@ pub extern "C" fn Java_mw_gri_android_BackgroundService_getSyncTitle(
|
||||
#[allow(dead_code)]
|
||||
#[cfg(target_os = "android")]
|
||||
#[allow(non_snake_case)]
|
||||
#[no_mangle]
|
||||
#[unsafe(no_mangle)]
|
||||
/// Get start text for Android notification in Java string format.
|
||||
pub extern "C" fn Java_mw_gri_android_BackgroundService_getStartText(
|
||||
_env: jni::JNIEnv,
|
||||
@@ -753,7 +753,7 @@ pub extern "C" fn Java_mw_gri_android_BackgroundService_getStartText(
|
||||
#[allow(dead_code)]
|
||||
#[cfg(target_os = "android")]
|
||||
#[allow(non_snake_case)]
|
||||
#[no_mangle]
|
||||
#[unsafe(no_mangle)]
|
||||
/// Get stop text for Android notification in Java string format.
|
||||
pub extern "C" fn Java_mw_gri_android_BackgroundService_getStopText(
|
||||
_env: jni::JNIEnv,
|
||||
@@ -767,7 +767,7 @@ pub extern "C" fn Java_mw_gri_android_BackgroundService_getStopText(
|
||||
#[allow(dead_code)]
|
||||
#[cfg(target_os = "android")]
|
||||
#[allow(non_snake_case)]
|
||||
#[no_mangle]
|
||||
#[unsafe(no_mangle)]
|
||||
/// Get exit text for Android notification in Java string format.
|
||||
pub extern "C" fn Java_mw_gri_android_BackgroundService_getExitText(
|
||||
_env: jni::JNIEnv,
|
||||
@@ -781,7 +781,7 @@ pub extern "C" fn Java_mw_gri_android_BackgroundService_getExitText(
|
||||
#[allow(dead_code)]
|
||||
#[cfg(target_os = "android")]
|
||||
#[allow(non_snake_case)]
|
||||
#[no_mangle]
|
||||
#[unsafe(no_mangle)]
|
||||
/// Check if node launch is possible.
|
||||
pub extern "C" fn Java_mw_gri_android_BackgroundService_canStartNode(
|
||||
_env: jni::JNIEnv,
|
||||
@@ -795,7 +795,7 @@ pub extern "C" fn Java_mw_gri_android_BackgroundService_canStartNode(
|
||||
#[allow(dead_code)]
|
||||
#[cfg(target_os = "android")]
|
||||
#[allow(non_snake_case)]
|
||||
#[no_mangle]
|
||||
#[unsafe(no_mangle)]
|
||||
/// Check if node stop is possible.
|
||||
pub extern "C" fn Java_mw_gri_android_BackgroundService_canStopNode(
|
||||
_env: jni::JNIEnv,
|
||||
@@ -809,7 +809,7 @@ pub extern "C" fn Java_mw_gri_android_BackgroundService_canStopNode(
|
||||
#[allow(dead_code)]
|
||||
#[cfg(target_os = "android")]
|
||||
#[allow(non_snake_case)]
|
||||
#[no_mangle]
|
||||
#[unsafe(no_mangle)]
|
||||
/// Start node from Android Java code.
|
||||
pub extern "C" fn Java_mw_gri_android_NotificationActionsReceiver_startNode(
|
||||
_env: jni::JNIEnv,
|
||||
@@ -822,7 +822,7 @@ pub extern "C" fn Java_mw_gri_android_NotificationActionsReceiver_startNode(
|
||||
#[allow(dead_code)]
|
||||
#[cfg(target_os = "android")]
|
||||
#[allow(non_snake_case)]
|
||||
#[no_mangle]
|
||||
#[unsafe(no_mangle)]
|
||||
/// Stop node from Android Java code.
|
||||
pub extern "C" fn Java_mw_gri_android_NotificationActionsReceiver_stopNode(
|
||||
_env: jni::JNIEnv,
|
||||
@@ -835,7 +835,7 @@ pub extern "C" fn Java_mw_gri_android_NotificationActionsReceiver_stopNode(
|
||||
#[allow(dead_code)]
|
||||
#[cfg(target_os = "android")]
|
||||
#[allow(non_snake_case)]
|
||||
#[no_mangle]
|
||||
#[unsafe(no_mangle)]
|
||||
/// Stop node from Android Java code.
|
||||
pub extern "C" fn Java_mw_gri_android_NotificationActionsReceiver_stopNodeToExit(
|
||||
_env: jni::JNIEnv,
|
||||
@@ -852,7 +852,7 @@ pub extern "C" fn Java_mw_gri_android_NotificationActionsReceiver_stopNodeToExit
|
||||
#[allow(dead_code)]
|
||||
#[cfg(target_os = "android")]
|
||||
#[allow(non_snake_case)]
|
||||
#[no_mangle]
|
||||
#[unsafe(no_mangle)]
|
||||
/// Check if app exit is needed after node stop to finish Android app at background.
|
||||
pub extern "C" fn Java_mw_gri_android_BackgroundService_exitAppAfterNodeStop(
|
||||
_env: jni::JNIEnv,
|
||||
@@ -866,7 +866,7 @@ pub extern "C" fn Java_mw_gri_android_BackgroundService_exitAppAfterNodeStop(
|
||||
#[allow(dead_code)]
|
||||
#[cfg(target_os = "android")]
|
||||
#[allow(non_snake_case)]
|
||||
#[no_mangle]
|
||||
#[unsafe(no_mangle)]
|
||||
/// Handle unexpected application termination on Android (removal from recent apps).
|
||||
pub extern "C" fn Java_mw_gri_android_MainActivity_onTermination(
|
||||
_env: jni::JNIEnv,
|
||||
|
||||
@@ -48,10 +48,10 @@ pub struct Settings {
|
||||
impl Settings {
|
||||
/// Main application directory name.
|
||||
pub const MAIN_DIR_NAME: &'static str = ".grim";
|
||||
/// Crash report file name.
|
||||
pub const CRASH_REPORT_FILE_NAME: &'static str = "crash.log";
|
||||
/// Application socket name.
|
||||
pub const SOCKET_NAME: &'static str = "grim.sock";
|
||||
/// Log file name.
|
||||
pub const LOG_FILE_NAME: &'static str = "grim.log";
|
||||
|
||||
/// Initialize settings with app and node configs.
|
||||
fn init() -> Self {
|
||||
@@ -148,6 +148,13 @@ impl Settings {
|
||||
path
|
||||
}
|
||||
|
||||
/// Get log file path.
|
||||
pub fn log_path() -> String {
|
||||
let mut log_path = Self::base_path(None);
|
||||
log_path.push(Self::LOG_FILE_NAME);
|
||||
log_path.to_str().unwrap().into()
|
||||
}
|
||||
|
||||
/// Get desktop application socket path.
|
||||
pub fn socket_path() -> PathBuf {
|
||||
let mut socket_path = Self::base_path(None);
|
||||
@@ -162,16 +169,16 @@ impl Settings {
|
||||
path
|
||||
}
|
||||
|
||||
/// Get configuration file path from provided name and subdirectory if needed.
|
||||
pub fn crash_report_path() -> PathBuf {
|
||||
/// Get path of file created when application crashed.
|
||||
pub fn crash_check_path() -> PathBuf {
|
||||
let mut path = Self::base_path(None);
|
||||
path.push(Self::CRASH_REPORT_FILE_NAME);
|
||||
path.push("crashed");
|
||||
path
|
||||
}
|
||||
|
||||
/// Delete crash report file.
|
||||
pub fn delete_crash_report() {
|
||||
let log = Self::crash_report_path();
|
||||
/// Delete crash file.
|
||||
pub fn delete_crash_check() {
|
||||
let log = Self::crash_check_path();
|
||||
if log.exists() {
|
||||
let _ = fs::remove_file(log.clone());
|
||||
}
|
||||
|
||||
+2
-1
@@ -126,7 +126,8 @@ impl TorConfig {
|
||||
} else {
|
||||
TorConfig::webtunnel_path()
|
||||
},
|
||||
TorBridge::DEFAULT_WEBTUNNEL_CONN_LINE.to_string()
|
||||
serde_json::to_string(&TorBridge::DEFAULT_WEBTUNNEL_CONN_LINES)
|
||||
.unwrap_or(TorBridge::DEFAULT_WEBTUNNEL_CONN_LINE.to_string())
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+334
-163
@@ -18,7 +18,6 @@ use arti_client::{TorClient, TorClientConfig};
|
||||
use curve25519_dalek::digest::Digest;
|
||||
use ed25519_dalek::hazmat::ExpandedSecretKey;
|
||||
use fs_mistrust::Mistrust;
|
||||
use futures::task::SpawnExt;
|
||||
use grin_util::secp::SecretKey;
|
||||
use http_body_util::{BodyExt, Full};
|
||||
use lazy_static::lazy_static;
|
||||
@@ -30,6 +29,7 @@ use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use std::{fs, thread};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use bytes::Bytes;
|
||||
use log::error;
|
||||
use safelog::DisplayRedacted;
|
||||
@@ -46,6 +46,7 @@ use tor_hsservice::{
|
||||
};
|
||||
use tor_keymgr::{ArtiNativeKeystore, KeyMgrBuilder, KeystoreSelector};
|
||||
use tor_llcrypto::pk::ed25519::ExpandedKeypair;
|
||||
use tor_rtcompat::SpawnExt;
|
||||
use tor_rtcompat::tokio::TokioNativeTlsRuntime;
|
||||
|
||||
use crate::http::HttpClient;
|
||||
@@ -53,24 +54,25 @@ use crate::tor::http::ArtiHttpConnector;
|
||||
use crate::tor::{TorBridge, TorConfig, TorProxy};
|
||||
|
||||
lazy_static! {
|
||||
/// Static thread-aware state of [`Node`] to be updated from separate thread.
|
||||
static ref TOR_SERVER_STATE: Arc<Tor> = Arc::new(Tor::default());
|
||||
/// Static thread-aware state of Tor to be updated from separate thread.
|
||||
static ref TOR_STATE: Arc<Tor> = Arc::new(Tor::default());
|
||||
}
|
||||
|
||||
/// Tor server to use as SOCKS proxy for requests and to launch Onion services.
|
||||
/// Tor client to use as SOCKS proxy for requests and to launch Onion services.
|
||||
pub struct Tor {
|
||||
/// Tor client and config.
|
||||
client_config: Arc<RwLock<(TorClient<TokioNativeTlsRuntime>, TorClientConfig)>>,
|
||||
/// Client to check services availability.
|
||||
check_client: Arc<RwLock<
|
||||
Option<hyper_tor::Client<ArtiHttpConnector<TokioNativeTlsRuntime, TlsConnector>>>
|
||||
>>,
|
||||
client_config: Arc<RwLock<Option<(TorClient<TokioNativeTlsRuntime>, TorClientConfig)>>>,
|
||||
/// Flag to check if client is launching.
|
||||
client_launching: Arc<AtomicBool>,
|
||||
|
||||
/// Mapping of running Onion services identifiers to proxy.
|
||||
run: Arc<RwLock<
|
||||
BTreeMap<String, (u16, SecretKey, Arc<RunningOnionService>, Arc<OnionServiceReverseProxy>)>
|
||||
>>,
|
||||
/// Starting Onion services identifiers.
|
||||
start: Arc<RwLock<BTreeSet<String>>>,
|
||||
/// Mapping of starting Onion services identifiers.
|
||||
start: Arc<RwLock<
|
||||
BTreeMap<String, (u16, SecretKey)>
|
||||
>>,
|
||||
/// Failed Onion services identifiers.
|
||||
fail: Arc<RwLock<BTreeSet<String>>>,
|
||||
/// Checking Onion services identifiers.
|
||||
@@ -86,18 +88,11 @@ impl Default for Tor {
|
||||
fs::write(TorConfig::webtunnel_path(), webtunnel).unwrap_or_default();
|
||||
}
|
||||
}
|
||||
// Create Tor client.
|
||||
let runtime = TokioNativeTlsRuntime::create().unwrap();
|
||||
let config = Self::build_config(true);
|
||||
let client = TorClient::with_runtime(runtime)
|
||||
.config(config.clone())
|
||||
.create_unbootstrapped()
|
||||
.unwrap();
|
||||
Self {
|
||||
client_config: Arc::new(RwLock::new((client, config))),
|
||||
check_client: Arc::new(RwLock::new(None)),
|
||||
client_config: Arc::new(RwLock::new(None)),
|
||||
client_launching: Arc::new(AtomicBool::new(false)),
|
||||
run: Arc::new(RwLock::new(BTreeMap::new())),
|
||||
start: Arc::new(RwLock::new(BTreeSet::new())),
|
||||
start: Arc::new(RwLock::new(BTreeMap::new())),
|
||||
fail: Arc::new(RwLock::new(BTreeSet::new())),
|
||||
check: Arc::new(RwLock::new(BTreeSet::new())),
|
||||
}
|
||||
@@ -105,28 +100,159 @@ impl Default for Tor {
|
||||
}
|
||||
|
||||
impl Tor {
|
||||
/// Create Tor client configuration.
|
||||
fn build_config(clean: bool) -> TorClientConfig {
|
||||
// Cleanup keys, state and cache.
|
||||
if clean {
|
||||
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 config.
|
||||
/// Create Tor configuration returning unused bridges (exclude more than 2 to avoid stuck).
|
||||
fn build_config(bridges: Option<Vec<TorBridge>>)
|
||||
-> (TorClientConfig, Vec<TorBridge>, Vec<TorBridge>) {
|
||||
let mut builder = TorClientConfigBuilder::from_directories(
|
||||
TorConfig::state_path(),
|
||||
TorConfig::cache_path(),
|
||||
);
|
||||
builder.address_filter().allow_onion_addrs(true);
|
||||
// Setup bridges.
|
||||
let bridge = TorConfig::get_bridge();
|
||||
if let Some(b) = bridge {
|
||||
// Build bridges.
|
||||
let mut bridges = bridges.unwrap_or(vec![]);
|
||||
let max_two_bridges = if bridges.len() > 2 {
|
||||
let two_bridges = bridges.iter().take(2)
|
||||
.cloned()
|
||||
.collect::<Vec<TorBridge>>();
|
||||
bridges = bridges.iter().filter(|b| !two_bridges.contains(b))
|
||||
.cloned()
|
||||
.collect::<Vec<TorBridge>>();
|
||||
two_bridges
|
||||
} else {
|
||||
bridges.clone()
|
||||
};
|
||||
for b in max_two_bridges.clone() {
|
||||
Self::build_bridge(&mut builder, b);
|
||||
}
|
||||
builder.address_filter().allow_onion_addrs(true);
|
||||
// Create config.
|
||||
let config = builder.build().unwrap();
|
||||
config
|
||||
(config, bridges, max_two_bridges)
|
||||
}
|
||||
|
||||
/// Build bootstrapped client from provided config.
|
||||
fn build_client_bootstrap(config: TorClientConfig) -> Option<TorClient<TokioNativeTlsRuntime>> {
|
||||
let runtime = TokioNativeTlsRuntime::create().unwrap();
|
||||
let client_res = TorClient::with_runtime(runtime)
|
||||
.config(config.clone())
|
||||
.create_unbootstrapped();
|
||||
if client_res.is_err() {
|
||||
return None;
|
||||
}
|
||||
let client = client_res.unwrap();
|
||||
let bootstrapping = Arc::new(AtomicBool::new(true));
|
||||
let bootstrap_success = Arc::new(AtomicBool::new(false));
|
||||
let bootstrapping_t = bootstrapping.clone();
|
||||
let bootstrap_success_t = bootstrap_success.clone();
|
||||
let c = client.clone();
|
||||
client.runtime().spawn(async move {
|
||||
let task = c.bootstrap();
|
||||
// Bootstrap client with 60s timeout.
|
||||
if tokio::time::timeout(Duration::from_millis(60000), task).await.is_ok() {
|
||||
bootstrap_success_t.store(true, Ordering::Relaxed);
|
||||
}
|
||||
bootstrapping_t.store(false, Ordering::Relaxed);
|
||||
}).unwrap();
|
||||
// Wait client to finish bootstrap.
|
||||
while bootstrapping.load(Ordering::Relaxed) {
|
||||
thread::sleep(Duration::from_millis(1000));
|
||||
}
|
||||
if bootstrap_success.load(Ordering::Relaxed) {
|
||||
Some(client)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Launch Tor client.
|
||||
fn launch() {
|
||||
// Wait client to finish launch and exit on success.
|
||||
while TOR_STATE.client_launching.load(Ordering::Relaxed) {
|
||||
thread::sleep(Duration::from_millis(1000));
|
||||
}
|
||||
{
|
||||
if TOR_STATE.client_config.read().is_some() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Cleanup keys, state and cache.
|
||||
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();
|
||||
TOR_STATE.client_launching.store(true, Ordering::Relaxed);
|
||||
// Get initial bridges.
|
||||
let initial_bridges = if let Some(b) = TorConfig::get_bridge() {
|
||||
let lines_parse = serde_json::from_str::<Vec<String>>(&b.connection_line());
|
||||
let bridges = lines_parse.unwrap_or_else(|_| b.connection_line()
|
||||
.lines()
|
||||
.map(|l| l.to_string()).collect()
|
||||
).iter().map(|l| {
|
||||
let mut bridge = b.clone();
|
||||
bridge.update_conn_line(l.clone());
|
||||
bridge
|
||||
}).collect::<Vec<TorBridge>>();
|
||||
Some(bridges)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
// Bootstrap client in the loop, trying different bridges.
|
||||
let mut default_attempt = false;
|
||||
let mut config_bridges = Self::build_config(initial_bridges);
|
||||
loop {
|
||||
let (config, unused_bridges, used_bridges) = config_bridges.clone();
|
||||
let client = Self::build_client_bootstrap(config.clone());
|
||||
if let Some(c) = client {
|
||||
// Update bridges order.
|
||||
if let Some(b) = TorConfig::get_bridge() {
|
||||
let lines_parse = serde_json::from_str::<Vec<String>>(&b.connection_line());
|
||||
match lines_parse {
|
||||
Ok(mut lines) => {
|
||||
lines.sort_by_key(|l| {
|
||||
let mut bridge = b.clone();
|
||||
bridge.update_conn_line(l.clone());
|
||||
!used_bridges.contains(&bridge)
|
||||
});
|
||||
let lines_str = serde_json::to_string(&lines)
|
||||
.unwrap_or_else(|_| {
|
||||
TorConfig::default_webtunnel_bridge().connection_line()
|
||||
});
|
||||
TorBridge::save_bridge_conn_line(&b, lines_str);
|
||||
}
|
||||
Err(_) => {},
|
||||
}
|
||||
}
|
||||
TOR_STATE.client_config.write().replace((c, config.clone()));
|
||||
break;
|
||||
} else {
|
||||
// Cleanup state and cache.
|
||||
fs::remove_dir_all(TorConfig::state_path()).unwrap_or_default();
|
||||
fs::remove_dir_all(TorConfig::cache_path()).unwrap_or_default();
|
||||
// Check unused bridges to check another part.
|
||||
if !unused_bridges.is_empty() {
|
||||
config_bridges = Self::build_config(Some(unused_bridges));
|
||||
continue;
|
||||
}
|
||||
if !default_attempt {
|
||||
default_attempt = true;
|
||||
// Launch client with default Webtunnel bridges if failed.
|
||||
let add_bridges = TorBridge::DEFAULT_WEBTUNNEL_CONN_LINES.iter()
|
||||
.map(|b| TorBridge::Webtunnel(TorConfig::webtunnel_path(), b.to_string()))
|
||||
.collect::<Vec<_>>();
|
||||
config_bridges = Self::build_config(Some(add_bridges));
|
||||
continue;
|
||||
} else if TorConfig::get_bridge().is_some() {
|
||||
// Launch without bridges if all attempts failed.
|
||||
let (config, _, _) = Self::build_config(None);
|
||||
let client = Self::build_client_bootstrap(config.clone());
|
||||
if let Some(c) = client {
|
||||
TOR_STATE.client_config.write().replace((c, config));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
// Wait 5s after launch.
|
||||
thread::sleep(Duration::from_millis(5000));
|
||||
TOR_STATE.client_launching.store(false, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// Send post request using Tor.
|
||||
@@ -172,13 +298,16 @@ impl Tor {
|
||||
} else {
|
||||
if let Some(b) = TorConfig::get_bridge() {
|
||||
if !fs::exists(b.binary_path()).unwrap() {
|
||||
error!("Tor: bridge binary not exists");
|
||||
return None;
|
||||
}
|
||||
}
|
||||
// Bootstrap client.
|
||||
let (client, _) = Self::client_config();
|
||||
client.bootstrap().await.unwrap_or_default();
|
||||
if Self::client_config().is_none() {
|
||||
error!("Tor: client not launched");
|
||||
return None;
|
||||
}
|
||||
// Create http tor-powered client to post data.
|
||||
let client = Self::client_config().unwrap().0.isolated_client();
|
||||
let tls_conn = TlsConnector::builder().unwrap().build().unwrap();
|
||||
let conn = ArtiHttpConnector::new(client, tls_conn);
|
||||
let http = hyper_tor::Client::builder().build::<_, hyper_tor::Body>(conn);
|
||||
@@ -205,52 +334,82 @@ impl Tor {
|
||||
}
|
||||
}
|
||||
|
||||
fn client_config() -> (TorClient<TokioNativeTlsRuntime>, TorClientConfig) {
|
||||
let r_client_config = TOR_SERVER_STATE.client_config.read();
|
||||
fn client_config() -> Option<(TorClient<TokioNativeTlsRuntime>, TorClientConfig)> {
|
||||
let r_client_config = TOR_STATE.client_config.read();
|
||||
r_client_config.clone()
|
||||
}
|
||||
|
||||
/// Check if Onion service is starting.
|
||||
pub fn is_service_starting(id: &String) -> bool {
|
||||
let r_services = TOR_SERVER_STATE.start.read();
|
||||
r_services.contains(id)
|
||||
let r_services = TOR_STATE.start.read();
|
||||
r_services.contains_key(id)
|
||||
}
|
||||
|
||||
/// Check if Onion service is running.
|
||||
pub fn is_service_running(id: &String) -> bool {
|
||||
let r_services = TOR_SERVER_STATE.run.read();
|
||||
let r_services = TOR_STATE.run.read();
|
||||
r_services.contains_key(id)
|
||||
}
|
||||
|
||||
/// Check if Onion service failed on start.
|
||||
pub fn is_service_failed(id: &String) -> bool {
|
||||
let r_services = TOR_SERVER_STATE.fail.read();
|
||||
let r_services = TOR_STATE.fail.read();
|
||||
r_services.contains(id)
|
||||
}
|
||||
|
||||
/// Check if Onion service is checking.
|
||||
pub fn is_service_checking(id: &String) -> bool {
|
||||
let r_services = TOR_SERVER_STATE.check.read();
|
||||
let r_services = TOR_STATE.check.read();
|
||||
r_services.contains(id)
|
||||
}
|
||||
|
||||
/// Restart Tor client at separate thread.
|
||||
pub fn restart() {
|
||||
thread::spawn(|| {
|
||||
// Exit if client was not launched.
|
||||
if Self::client_config().is_none() {
|
||||
return;
|
||||
}
|
||||
Self::restart_services();
|
||||
});
|
||||
}
|
||||
|
||||
/// Restart running Onion services.
|
||||
pub fn restart_services() {
|
||||
// Stop all services saving port key for relaunch.
|
||||
fn restart_services() {
|
||||
// Stop all services saving keys to relaunch.
|
||||
let service_ids = {
|
||||
let r_services = TOR_SERVER_STATE.run.read().clone();
|
||||
let r_services = TOR_STATE.run.read().clone();
|
||||
r_services.keys().map(|s| s.to_string()).collect::<Vec<String>>()
|
||||
};
|
||||
let mut services: BTreeMap<String, (u16, SecretKey)> = BTreeMap::new();
|
||||
let mut services: BTreeMap<String, (u16, SecretKey)> = TOR_STATE.start.read().clone();
|
||||
for id in service_ids.clone() {
|
||||
if let Some(res) = Self::stop_service(&id) {
|
||||
services.insert(id, res);
|
||||
}
|
||||
}
|
||||
// Reconfigure client.
|
||||
let config = Self::build_config(false);
|
||||
let r_client = TOR_SERVER_STATE.client_config.read();
|
||||
r_client.0.reconfigure(&config, tor_config::Reconfigure::WarnOnFailures).unwrap();
|
||||
// Put stopped services to start.
|
||||
{
|
||||
let mut w_services = TOR_STATE.start.write();
|
||||
*w_services = services.clone();
|
||||
}
|
||||
// Cleanup keys, state and cache.
|
||||
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();
|
||||
{
|
||||
let mut w_client = TOR_STATE.client_config.write();
|
||||
*w_client = None;
|
||||
}
|
||||
// Relaunch client.
|
||||
Self::launch();
|
||||
// Save failed services if client was not created.
|
||||
if Self::client_config().is_none() {
|
||||
for id in service_ids {
|
||||
TOR_STATE.start.write().remove(&id);
|
||||
TOR_STATE.fail.write().insert(id);
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Start services.
|
||||
for id in services.keys() {
|
||||
let (port, key) = services.get(id).unwrap();
|
||||
@@ -260,43 +419,62 @@ impl Tor {
|
||||
|
||||
/// Stop running Onion service returning port and key.
|
||||
pub fn stop_service(id: &String) -> Option<(u16, SecretKey)> {
|
||||
let mut port_key = None;
|
||||
{
|
||||
// Remove service from starting.
|
||||
let mut w_services = TOR_SERVER_STATE.start.write();
|
||||
// Remove service from checking.
|
||||
let mut w_services = TOR_STATE.check.write();
|
||||
w_services.remove(id);
|
||||
}
|
||||
let mut w_services = TOR_SERVER_STATE.run.write();
|
||||
if let Some((port, key, svc, proxy)) = w_services.remove(id) {
|
||||
proxy.shutdown();
|
||||
drop(proxy);
|
||||
drop(svc);
|
||||
return Some((port, key));
|
||||
// Remove service from starting.
|
||||
{
|
||||
let mut w_services = TOR_STATE.start.write();
|
||||
if let Some((port, key)) = w_services.remove(id) {
|
||||
port_key = Some((port, key));
|
||||
}
|
||||
}
|
||||
None
|
||||
// Remove service from running.
|
||||
{
|
||||
let mut w_services = TOR_STATE.run.write();
|
||||
if let Some((port, key, svc, proxy)) = w_services.remove(id) {
|
||||
proxy.shutdown();
|
||||
drop(proxy);
|
||||
drop(svc);
|
||||
port_key = Some((port, key));
|
||||
}
|
||||
}
|
||||
// Remove client when no running services left.
|
||||
if TOR_STATE.start.read().is_empty() && TOR_STATE.run.read().is_empty() {
|
||||
let mut w_client = TOR_STATE.client_config.write();
|
||||
*w_client = None;
|
||||
// Clear state.
|
||||
fs::remove_dir_all(TorConfig::state_path()).unwrap_or_default();
|
||||
}
|
||||
port_key
|
||||
}
|
||||
|
||||
/// Start Onion service from listening local port and [`SecretKey`].
|
||||
pub fn start_service(port: u16, key: SecretKey, id: &String) {
|
||||
// Check if service is already running.
|
||||
if Self::is_service_running(id) || Self::is_service_starting(id) {
|
||||
if Self::is_service_running(id) {
|
||||
return;
|
||||
} else {
|
||||
// Save starting service.
|
||||
let mut w_services = TOR_SERVER_STATE.start.write();
|
||||
w_services.insert(id.clone());
|
||||
// Remove service from failed.
|
||||
let mut w_services = TOR_SERVER_STATE.fail.write();
|
||||
w_services.remove(id);
|
||||
}
|
||||
|
||||
let service_id = id.clone();
|
||||
thread::spawn(move || {
|
||||
{
|
||||
// Save starting service.
|
||||
let mut w_services = TOR_STATE.start.write();
|
||||
w_services.insert(service_id.clone(), (port, key.clone()));
|
||||
// Remove service from failed.
|
||||
let mut w_services = TOR_STATE.fail.write();
|
||||
w_services.remove(&service_id);
|
||||
}
|
||||
|
||||
let on_error = |service_id: String| {
|
||||
// Remove service from starting.
|
||||
let mut w_services = TOR_SERVER_STATE.start.write();
|
||||
let mut w_services = TOR_STATE.start.write();
|
||||
w_services.remove(&service_id);
|
||||
// Save failed service.
|
||||
let mut w_services = TOR_SERVER_STATE.fail.write();
|
||||
let mut w_services = TOR_STATE.fail.write();
|
||||
w_services.insert(service_id);
|
||||
};
|
||||
|
||||
@@ -319,7 +497,14 @@ impl Tor {
|
||||
}
|
||||
}
|
||||
|
||||
let (client, config) = Self::client_config();
|
||||
// Launch client if not exists.
|
||||
Self::launch();
|
||||
let client_config = Self::client_config();
|
||||
if client_config.is_none() {
|
||||
on_error(service_id);
|
||||
return;
|
||||
}
|
||||
let (client, config) = client_config.unwrap();
|
||||
client
|
||||
.runtime()
|
||||
.spawn(async move {
|
||||
@@ -329,40 +514,34 @@ impl Tor {
|
||||
on_error(service_id);
|
||||
return;
|
||||
}
|
||||
let (c, _) = Self::client_config();
|
||||
// Bootstrap client.
|
||||
if c.bootstrap_status().as_frac() == 0.0 {
|
||||
if let Err(_) = c.bootstrap().await {
|
||||
on_error(service_id);
|
||||
return;
|
||||
}
|
||||
// Launch first time after delay.
|
||||
thread::sleep(Duration::from_millis(10000));
|
||||
}
|
||||
// Launch Onion service.
|
||||
let service_config = OnionServiceConfigBuilder::default()
|
||||
.nickname(hs.clone())
|
||||
.build()
|
||||
.unwrap();
|
||||
let client_config = Self::client_config();
|
||||
if client_config.is_none() {
|
||||
on_error(service_id.clone());
|
||||
}
|
||||
let c = client_config.unwrap().0.isolated_client();
|
||||
if let Ok(res) = c.launch_onion_service(service_config) {
|
||||
if let Some((service, request)) = res {
|
||||
let onion_addr = service.onion_address();
|
||||
// Launch service proxy.
|
||||
let addr = SocketAddr::new(IpAddr::from(Ipv4Addr::LOCALHOST), port);
|
||||
let proxy = tokio::spawn(Self::run_service_proxy(
|
||||
addr,
|
||||
request,
|
||||
hs.clone(),
|
||||
)).await.unwrap();
|
||||
let run = Self::run_service_proxy(c, addr, request, hs.clone());
|
||||
let proxy = tokio::spawn(run).await.unwrap();
|
||||
let onion_addr = service.onion_address();
|
||||
// Save running service.
|
||||
let mut w_services = TOR_SERVER_STATE.run.write();
|
||||
let id = service_id.clone();
|
||||
w_services.insert(id, (port, key.clone(), service, proxy));
|
||||
{
|
||||
let mut w_services = TOR_STATE.run.write();
|
||||
let id = service_id.clone();
|
||||
w_services.insert(id, (port, key.clone(), service, proxy));
|
||||
}
|
||||
// Check service availability.
|
||||
let addr = onion_addr.unwrap().display_unredacted().to_string();
|
||||
let url = format!("http://{}/", addr);
|
||||
if !Self::is_service_checking(&service_id) {
|
||||
Self::check_service(service_id, url, port, key)
|
||||
Self::check_service(service_id, url)
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -373,93 +552,86 @@ impl Tor {
|
||||
});
|
||||
}
|
||||
|
||||
/// Check is service is running on check.
|
||||
fn check_running(service_id: &String) -> bool {
|
||||
let running = Tor::is_service_running(service_id) && Self::client_config().is_some();
|
||||
if !running {
|
||||
// Remove service from checking.
|
||||
let mut w_services = TOR_STATE.check.write();
|
||||
w_services.remove(service_id);
|
||||
}
|
||||
running
|
||||
}
|
||||
|
||||
/// Check service availability.
|
||||
fn check_service(service_id: String,
|
||||
url: String,
|
||||
port: u16,
|
||||
key: SecretKey) {
|
||||
fn check_service(service_id: String, url: String) {
|
||||
{
|
||||
let mut w_services = TOR_SERVER_STATE.check.write();
|
||||
let mut w_services = TOR_STATE.check.write();
|
||||
w_services.insert(service_id.clone());
|
||||
}
|
||||
let (client, _) = Self::client_config();
|
||||
thread::spawn(move || {
|
||||
// Wait 5 sec before check.
|
||||
thread::sleep(Duration::from_millis(5000));
|
||||
// Request client setup.
|
||||
if TOR_SERVER_STATE.check_client.read().is_none() {
|
||||
let (client_check, _) = Self::client_config();
|
||||
let tls_conn = TlsConnector::builder().unwrap().build().unwrap();
|
||||
let conn = ArtiHttpConnector::new(client_check, tls_conn);
|
||||
let http = hyper_tor::Client::builder().build::<_, hyper_tor::Body>(conn);
|
||||
let mut w_client = TOR_SERVER_STATE.check_client.write();
|
||||
*w_client = Some(http);
|
||||
}
|
||||
let r_client = TOR_SERVER_STATE.check_client.read();
|
||||
let http = r_client.clone().unwrap();
|
||||
let runtime = client.runtime();
|
||||
runtime
|
||||
.spawn(async move {
|
||||
tokio::runtime::Builder::new_multi_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap()
|
||||
.block_on(async {
|
||||
const MAX_ERRORS: i32 = 16;
|
||||
let mut errors_count = 0;
|
||||
// Wait 5 seconds.
|
||||
tokio::time::sleep(Duration::from_millis(5000)).await;
|
||||
loop {
|
||||
// Check if service is running.
|
||||
fn is_running(service_id: &String) -> bool {
|
||||
let running = Tor::is_service_running(service_id);
|
||||
if !running {
|
||||
// Remove service from checking.
|
||||
let mut w_services =
|
||||
TOR_SERVER_STATE.check.write();
|
||||
w_services.remove(service_id);
|
||||
}
|
||||
running
|
||||
}
|
||||
if !is_running(&service_id) {
|
||||
if !Self::check_running(&service_id) {
|
||||
break;
|
||||
}
|
||||
let duration = {
|
||||
// Send request.
|
||||
let tls_conn = TlsConnector::builder().unwrap().build().unwrap();
|
||||
let client_config = Self::client_config();
|
||||
if client_config.is_none() {
|
||||
return;
|
||||
}
|
||||
let client = client_config.unwrap().0.isolated_client();
|
||||
let conn = ArtiHttpConnector::new(client, tls_conn);
|
||||
let http = hyper_tor::Client::builder()
|
||||
.build::<_, hyper_tor::Body>(conn);
|
||||
let uri = hyper_tor::Uri::from_str(url.clone().as_str()).unwrap();
|
||||
let check = http.get(uri.clone());
|
||||
// Setup error callback.
|
||||
let mut on_error = |service_id: &String| -> bool {
|
||||
if !is_running(service_id) {
|
||||
if !Self::check_running(service_id) {
|
||||
return true;
|
||||
}
|
||||
// Restart service after maximum amount of errors.
|
||||
errors_count += 1;
|
||||
if errors_count == MAX_ERRORS {
|
||||
// Remove service from checking.
|
||||
let mut w_services =
|
||||
TOR_SERVER_STATE.check.write();
|
||||
w_services.remove(service_id);
|
||||
// Remove service from starting.
|
||||
let mut w_services = TOR_SERVER_STATE.start.write();
|
||||
w_services.remove(service_id);
|
||||
// Restart service.
|
||||
let key = key.clone();
|
||||
let id = service_id.clone();
|
||||
thread::spawn(move || {
|
||||
Self::stop_service(&id);
|
||||
Self::start_service(port, key, &id);
|
||||
});
|
||||
return true;
|
||||
let max_errors = errors_count >= MAX_ERRORS;
|
||||
if max_errors {
|
||||
{
|
||||
// Remove service from checking.
|
||||
let mut w_services = TOR_STATE.check.write();
|
||||
w_services.remove(service_id);
|
||||
// Remove service from starting.
|
||||
let mut w_services = TOR_STATE.start.write();
|
||||
w_services.remove(service_id);
|
||||
}
|
||||
// Restart services.
|
||||
Self::restart();
|
||||
}
|
||||
false
|
||||
max_errors
|
||||
};
|
||||
// Send request.
|
||||
let uri = hyper_tor::Uri::from_str(url.clone().as_str()).unwrap();
|
||||
let check = http.get(uri);
|
||||
// Check with timeout of 20s.
|
||||
match tokio::time::timeout(Duration::from_millis(20000), check).await {
|
||||
// Check with timeout of 30s.
|
||||
match tokio::time::timeout(Duration::from_millis(30000), check).await {
|
||||
Ok(resp) => {
|
||||
match resp {
|
||||
Ok(_) => {
|
||||
if !is_running(&service_id) {
|
||||
if !Self::check_running(&service_id) {
|
||||
break;
|
||||
}
|
||||
// Remove service from starting.
|
||||
let mut w_services = TOR_SERVER_STATE.start.write();
|
||||
let mut w_services = TOR_STATE.start.write();
|
||||
w_services.remove(&service_id);
|
||||
errors_count = 0;
|
||||
// Check again after 20s.
|
||||
Duration::from_millis(20000)
|
||||
// Check again after 60s.
|
||||
Duration::from_millis(60000)
|
||||
}
|
||||
Err(e) => {
|
||||
if on_error(&service_id) {
|
||||
@@ -484,15 +656,15 @@ impl Tor {
|
||||
}
|
||||
};
|
||||
// Wait to check service again.
|
||||
thread::sleep(duration);
|
||||
tokio::time::sleep(duration).await;
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// Launch Onion service proxy.
|
||||
async fn run_service_proxy<S>(
|
||||
client: TorClient<TokioNativeTlsRuntime>,
|
||||
addr: SocketAddr,
|
||||
request: S,
|
||||
nickname: HsNickname,
|
||||
@@ -500,7 +672,6 @@ impl Tor {
|
||||
S: futures::Stream<Item = tor_hsservice::RendRequest> + Unpin + Send + 'static,
|
||||
{
|
||||
let id = nickname.to_string();
|
||||
let (client, _) = Self::client_config();
|
||||
let runtime = client.runtime().clone();
|
||||
|
||||
// Setup proxy to forward request from Tor address to local address.
|
||||
@@ -513,7 +684,7 @@ impl Tor {
|
||||
let proxy = OnionServiceReverseProxy::new(proxy_cfg_builder.build().unwrap());
|
||||
|
||||
// Remove service from failed.
|
||||
let mut w_services = TOR_SERVER_STATE.fail.write();
|
||||
let mut w_services = TOR_STATE.fail.write();
|
||||
w_services.remove(&id);
|
||||
|
||||
// Start proxy for launched service.
|
||||
@@ -524,16 +695,16 @@ impl Tor {
|
||||
match p.handle_requests(runtime, nickname.clone(), request).await {
|
||||
Ok(()) => {
|
||||
// Remove service from running.
|
||||
let mut w_services = TOR_SERVER_STATE.run.write();
|
||||
let mut w_services = TOR_STATE.run.write();
|
||||
w_services.remove(&id);
|
||||
}
|
||||
Err(_) => {
|
||||
if Self::is_service_running(&id) {
|
||||
// Remove service from running.
|
||||
let mut w_services = TOR_SERVER_STATE.run.write();
|
||||
let mut w_services = TOR_STATE.run.write();
|
||||
w_services.remove(&id);
|
||||
// Save failed service.
|
||||
let mut w_services = TOR_SERVER_STATE.fail.write();
|
||||
let mut w_services = TOR_STATE.fail.write();
|
||||
w_services.insert(id);
|
||||
}
|
||||
}
|
||||
|
||||
+36
-2
@@ -41,7 +41,7 @@ impl TorProxy {
|
||||
}
|
||||
|
||||
/// Tor network bridge type.
|
||||
#[derive(Serialize, Deserialize, Clone, PartialEq)]
|
||||
#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)]
|
||||
pub enum TorBridge {
|
||||
/// Obfs4 bridge with binary path and connection line.
|
||||
Webtunnel(String, String),
|
||||
@@ -59,8 +59,27 @@ impl TorBridge {
|
||||
|
||||
/// Default webtunnel protocol connection line.
|
||||
pub const DEFAULT_WEBTUNNEL_CONN_LINE: &'static str = "webtunnel [2001:db8:beb:5884:ffcc:bfe3:2858:b06b]:443 1E242C749707B4A68A269F0D31311CE36CDFEC28 url=https://wt.gri.mw/74Fm0lKUWWMMjZpKf6iSC0UH";
|
||||
pub const ADDITIONAL_WEBTUNNEL_CONN_LINE_1: &'static str = "webtunnel [2001:db8:1640:379c:ad30:db5f:bff5:37d0]:443 AF8F7548C886D6F53A652411DBB71D089517085A url=https://app05.oneclickhost.eu/alpfZGTB9FckCgOkOOA0OHlh";
|
||||
pub const ADDITIONAL_WEBTUNNEL_CONN_LINE_2: &'static str = "webtunnel [2001:db8:eedb:cae7:a345:4f72:f9cc:5de0]:443 B3C81E7A0CA474270DAA4A2C8633E1CA8935C37D url=https://wordpress.far-east-investment.ru/sORes7268CEUSRD7hAWvJU5A";
|
||||
pub const ADDITIONAL_WEBTUNNEL_CONN_LINE_3: &'static str = "webtunnel [2001:db8:945c:e0b9:7e4c:c974:ff00:d4c5]:443 91937F3EFB3BE5169788AC7C8BF07460B7E306DB url=https://kabel.entreri.de/YXbp1dNrJeOF8giAFFYWxvmf";
|
||||
pub const ADDITIONAL_WEBTUNNEL_CONN_LINE_4: &'static str = "webtunnel [2001:db8:4767:7aa2:df21:1b2b:d7f9:caee]:443 CD193CF0D0C29551928C01FCB28D1200D9F27CFA url=https://occurrence.pics/68SzSlQCRgnfSo32eLyjC1V3";
|
||||
pub const ADDITIONAL_WEBTUNNEL_CONN_LINE_5: &'static str = "webtunnel [2001:db8:ce90:3593:272e:4975:a031:55b]:443 12382A2F3912AD1983A97C8709CBAE47ADB60BE3 url=https://miranda.today/LWwxIXDHCyyScn7oDauPMTmX";
|
||||
pub const ADDITIONAL_WEBTUNNEL_CONN_LINE_6: &'static str = "webtunnel [2001:db8:a12b:ff8:8a1a:a05b:5f21:2ccc]:443 F2A9C5AEE0A420EB9D55F9497B3C0FA243A2A770 url=https://bridge.lovecloud.me/wss-wc3p0euqrlne98t9";
|
||||
pub const ADDITIONAL_WEBTUNNEL_CONN_LINE_7: &'static str = "webtunnel [2001:db8:8ed6:e6c9:5fc9:9f20:a373:2374]:443 1636A2EFFBAA4B162F5FF461A1663EB55C41AE11 url=https://hanoi.delivery/roQFPLtlspWT6yIKeXD6lEci";
|
||||
|
||||
pub const DEFAULT_WEBTUNNEL_CONN_LINES: [&'static str; 8] = [
|
||||
TorBridge::DEFAULT_WEBTUNNEL_CONN_LINE,
|
||||
TorBridge::ADDITIONAL_WEBTUNNEL_CONN_LINE_1,
|
||||
TorBridge::ADDITIONAL_WEBTUNNEL_CONN_LINE_2,
|
||||
TorBridge::ADDITIONAL_WEBTUNNEL_CONN_LINE_3,
|
||||
TorBridge::ADDITIONAL_WEBTUNNEL_CONN_LINE_4,
|
||||
TorBridge::ADDITIONAL_WEBTUNNEL_CONN_LINE_5,
|
||||
TorBridge::ADDITIONAL_WEBTUNNEL_CONN_LINE_6,
|
||||
TorBridge::ADDITIONAL_WEBTUNNEL_CONN_LINE_7,
|
||||
];
|
||||
|
||||
/// Default Obfs4 protocol connection line.
|
||||
pub const DEFAULT_OBFS4_CONN_LINE: &'static str = "obfs4 45.76.43.226:3479 7AAFDC594147E72635DD64DB47A8CD8781F463F6 cert=bJ720bjXkmFGGAD77BsCMopkDzQ/cXDj0QntOmsBYw7Fqohq7Y7yZMV7FlECQNB1tyq1AA iat-mode=0";
|
||||
pub const DEFAULT_OBFS4_CONN_LINE: &'static str = "obfs4 51.83.248.35:25981 D08B4760D128C1A65506577E063D9D26C2A71815 cert=UJWUh+sIDdOKja/byBM2+qP9AFNl86hkGRFJ/lM1GWKP79eCu3PT4WTXI2gdXYULbQ0EMg iat-mode=0";
|
||||
/// Default Snowflake protocol connection line.
|
||||
pub const DEFAULT_SNOWFLAKE_CONN_LINE: &'static str = "snowflake 192.0.2.4:80 8838024498816A039FCBBAB14E6F40A0843051FA fingerprint=8838024498816A039FCBBAB14E6F40A0843051FA url=https://1098762253.rsc.cdn77.org/ fronts=www.cdn77.com,www.phpmyadmin.net ice=stun:stun.l.google.com:19302,stun:stun.antisip.com:3478,stun:stun.bluesip.net:3478,stun:stun.dus.net:3478,stun:stun.epygi.com:3478,stun:stun.sonetel.net:3478,stun:stun.uls.co.za:3478,stun:stun.voipgate.com:3478,stun:stun.voys.nl:3478 utls-imitate=hellorandomizedalpn";
|
||||
|
||||
@@ -87,6 +106,12 @@ impl TorBridge {
|
||||
}
|
||||
}
|
||||
|
||||
/// Get bridge client binary name.
|
||||
pub fn binary_name(&self) -> String {
|
||||
let path = self.binary_path();
|
||||
path.split(std::path::MAIN_SEPARATOR_STR).last().unwrap().to_string()
|
||||
}
|
||||
|
||||
/// Get bridge client connection line.
|
||||
pub fn connection_line(&self) -> String {
|
||||
match self {
|
||||
@@ -96,6 +121,15 @@ impl TorBridge {
|
||||
}
|
||||
}
|
||||
|
||||
/// Update bridge connection line.
|
||||
pub fn update_conn_line(&mut self, l: String) {
|
||||
*self = match TorConfig::get_bridge().unwrap() {
|
||||
TorBridge::Webtunnel(bin, _) => TorBridge::Webtunnel(bin, l.clone()),
|
||||
TorBridge::Obfs4(bin, _) => TorBridge::Obfs4(bin, l.clone()),
|
||||
TorBridge::Snowflake(bin, _) => TorBridge::Snowflake(bin, l.clone()),
|
||||
};
|
||||
}
|
||||
|
||||
/// Save binary path to provided bridge.
|
||||
pub fn save_bridge_bin_path(bridge: &TorBridge, path: String) {
|
||||
match bridge {
|
||||
|
||||
@@ -23,7 +23,7 @@ use serde_derive::{Deserialize, Serialize};
|
||||
|
||||
use crate::{AppConfig, Settings};
|
||||
use crate::wallet::ConnectionsConfig;
|
||||
use crate::wallet::types::{ConnectionMethod, WalletTransaction};
|
||||
use crate::wallet::types::{ConnectionMethod, WalletTx};
|
||||
|
||||
/// Wallet configuration.
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
@@ -228,7 +228,7 @@ impl WalletConfig {
|
||||
}
|
||||
|
||||
/// Get Slatepack file path for transaction.
|
||||
pub fn get_tx_slate_path(&self, tx: &WalletTransaction) -> PathBuf {
|
||||
pub fn get_tx_slate_path(&self, tx: &WalletTx) -> PathBuf {
|
||||
let mut path = PathBuf::from(self.get_base_data_path());
|
||||
path.push(SLATEPACKS_DIR_NAME);
|
||||
if !path.exists() {
|
||||
|
||||
@@ -64,7 +64,7 @@ impl ConnectionsConfig {
|
||||
pub fn add_ext_conn(conn: ExternalConnection) {
|
||||
let mut w_config = Settings::conn_config_to_update();
|
||||
if let Some(pos) = w_config.external.iter().position(|c| {
|
||||
c.id == conn.id
|
||||
c.id == conn.id || c.url == conn.url
|
||||
}) {
|
||||
w_config.external.remove(pos);
|
||||
w_config.external.insert(pos, conn);
|
||||
|
||||
@@ -28,6 +28,8 @@ pub struct ExternalConnection {
|
||||
pub id: i64,
|
||||
/// Node URL.
|
||||
pub url: String,
|
||||
/// Optional username.
|
||||
pub username: Option<String>,
|
||||
/// Optional API secret key.
|
||||
pub secret: Option<String>,
|
||||
|
||||
@@ -61,6 +63,7 @@ impl ExternalConnection {
|
||||
ExternalConnection {
|
||||
id: index as i64,
|
||||
url: url.to_string(),
|
||||
username: Some("grin".to_string()),
|
||||
secret: None,
|
||||
available: None,
|
||||
}
|
||||
@@ -68,11 +71,12 @@ impl ExternalConnection {
|
||||
}
|
||||
|
||||
/// Create new external connection.
|
||||
pub fn new(url: String, secret: Option<String>) -> Self {
|
||||
pub fn new(url: String, username: Option<String>, secret: Option<String>) -> Self {
|
||||
let id = chrono::Utc::now().timestamp();
|
||||
Self {
|
||||
id,
|
||||
url,
|
||||
username,
|
||||
secret,
|
||||
available: None,
|
||||
}
|
||||
@@ -128,7 +132,6 @@ fn check_ext_conn(conn: &ExternalConnection, ui_ctx: &egui::Context) {
|
||||
return;
|
||||
}
|
||||
let url = url_res.unwrap();
|
||||
|
||||
if let Ok(_) = url.socket_addrs(|| None) {
|
||||
let addr = format!("{}v2/foreign", url.to_string());
|
||||
let mut req_setup = hyper::Request::builder()
|
||||
@@ -136,9 +139,10 @@ fn check_ext_conn(conn: &ExternalConnection, ui_ctx: &egui::Context) {
|
||||
.uri(addr.clone());
|
||||
// Setup secret key auth.
|
||||
if let Some(key) = conn.secret {
|
||||
let username = conn.username.unwrap_or("grin".to_string());
|
||||
let basic_auth = format!(
|
||||
"Basic {}",
|
||||
to_base64(&format!("grin:{}", key))
|
||||
to_base64(&format!("{}:{}", username, key))
|
||||
);
|
||||
req_setup = req_setup
|
||||
.header(hyper::header::AUTHORIZATION, basic_auth.clone());
|
||||
|
||||
+46
-49
@@ -20,6 +20,7 @@ use grin_wallet_util::OnionV3Address;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::tor::Tor;
|
||||
use crate::wallet::Wallet;
|
||||
|
||||
/// Mnemonic phrase word.
|
||||
@@ -150,7 +151,7 @@ pub struct WalletData {
|
||||
pub info: WalletInfo,
|
||||
|
||||
/// Transactions data.
|
||||
pub txs: Option<Vec<WalletTransaction>>,
|
||||
pub txs: Option<Vec<WalletTx>>,
|
||||
/// Number of txs to show on select from database.
|
||||
pub txs_limit: u32,
|
||||
}
|
||||
@@ -160,53 +161,34 @@ impl WalletData {
|
||||
pub const TXS_LIMIT: u32 = 30;
|
||||
|
||||
/// Update transaction action status.
|
||||
pub fn on_tx_action(&mut self, id: String, action: Option<WalletTransactionAction>) {
|
||||
pub fn on_tx_action(&mut self, id: u32, action: Option<WalletTxAction>) {
|
||||
if self.txs.is_none() {
|
||||
return;
|
||||
}
|
||||
for tx in self.txs.as_mut().unwrap() {
|
||||
if let Some(slate_id) = tx.data.tx_slate_id {
|
||||
if slate_id.to_string() == id {
|
||||
tx.action = action;
|
||||
tx.action_error = None;
|
||||
break;
|
||||
}
|
||||
if id == tx.data.id {
|
||||
tx.action = action;
|
||||
tx.action_error = None;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Update transaction action error status.
|
||||
pub fn on_tx_error(&mut self, id: String, err: Option<Error>) {
|
||||
pub fn on_tx_error(&mut self, id: u32, err: Option<Error>) {
|
||||
if self.txs.is_none() {
|
||||
return;
|
||||
}
|
||||
for tx in self.txs.as_mut().unwrap() {
|
||||
if let Some(slate_id) = tx.data.tx_slate_id {
|
||||
if slate_id.to_string() == id {
|
||||
tx.action_error = err;
|
||||
break;
|
||||
}
|
||||
if id == tx.data.id {
|
||||
tx.action_error = err;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get transaction by slate identifier.
|
||||
pub fn tx_by_slate_id(&self, id: String) -> Option<WalletTransaction> {
|
||||
if self.txs.is_none() {
|
||||
return None;
|
||||
}
|
||||
for tx in self.txs.as_ref().unwrap() {
|
||||
if let Some(slate_id) = tx.data.tx_slate_id {
|
||||
if slate_id.to_string() == id {
|
||||
return Some(tx.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Get transaction by identifier.
|
||||
pub fn tx_by_id(&self, id: u32) -> Option<WalletTransaction> {
|
||||
pub fn tx_by_id(&self, id: u32) -> Option<WalletTx> {
|
||||
if self.txs.is_none() {
|
||||
return None;
|
||||
}
|
||||
@@ -221,13 +203,13 @@ impl WalletData {
|
||||
|
||||
/// Wallet transaction action.
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub enum WalletTransactionAction {
|
||||
Cancelling, Finalizing, Posting, SendingTor
|
||||
pub enum WalletTxAction {
|
||||
Cancelling, Finalizing, Posting, SendingTor, Deleting
|
||||
}
|
||||
|
||||
/// Wallet transaction data.
|
||||
#[derive(Clone)]
|
||||
pub struct WalletTransaction {
|
||||
pub struct WalletTx {
|
||||
/// Information from database.
|
||||
pub data: TxLogEntry,
|
||||
/// State of transaction Slate.
|
||||
@@ -247,19 +229,19 @@ pub struct WalletTransaction {
|
||||
pub broadcasting_height: Option<u64>,
|
||||
|
||||
/// Action on transaction.
|
||||
pub action: Option<WalletTransactionAction>,
|
||||
pub action: Option<WalletTxAction>,
|
||||
/// Action result error.
|
||||
pub action_error: Option<Error>
|
||||
}
|
||||
|
||||
impl WalletTransaction {
|
||||
impl WalletTx {
|
||||
/// Create new wallet transaction.
|
||||
pub fn new(tx: TxLogEntry,
|
||||
proof: Option<PaymentProof>,
|
||||
wallet: &Wallet,
|
||||
height: Option<u64>,
|
||||
broadcasting_height: Option<u64>,
|
||||
action: Option<WalletTransactionAction>,
|
||||
action: Option<WalletTxAction>,
|
||||
action_error: Option<Error>) -> Self {
|
||||
let amount = if tx.amount_debited > tx.amount_credited {
|
||||
tx.amount_debited - tx.amount_credited
|
||||
@@ -352,7 +334,7 @@ impl WalletTransaction {
|
||||
/// Check if transaction is sending over Tor.
|
||||
pub fn sending_tor(&self) -> bool {
|
||||
if let Some(a) = self.action.as_ref() {
|
||||
return a == &WalletTransactionAction::SendingTor;
|
||||
return a == &WalletTxAction::SendingTor;
|
||||
}
|
||||
false
|
||||
}
|
||||
@@ -360,7 +342,7 @@ impl WalletTransaction {
|
||||
/// Check if transaction is cancelling.
|
||||
pub fn cancelling(&self) -> bool {
|
||||
if let Some(a) = self.action.as_ref() {
|
||||
return a == &WalletTransactionAction::Cancelling;
|
||||
return a == &WalletTxAction::Cancelling;
|
||||
}
|
||||
false
|
||||
}
|
||||
@@ -368,7 +350,7 @@ impl WalletTransaction {
|
||||
/// Check if transaction is posting.
|
||||
pub fn posting(&self) -> bool {
|
||||
if let Some(a) = self.action.as_ref() {
|
||||
return a == &WalletTransactionAction::Posting;
|
||||
return a == &WalletTxAction::Posting;
|
||||
}
|
||||
false
|
||||
}
|
||||
@@ -390,17 +372,21 @@ impl WalletTransaction {
|
||||
/// Check if transaction is finalizing.
|
||||
pub fn finalizing(&self) -> bool {
|
||||
if let Some(a) = self.action.as_ref() {
|
||||
return a == &WalletTransactionAction::Finalizing;
|
||||
return a == &WalletTxAction::Finalizing;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Check if possible to repeat transaction action.
|
||||
pub fn can_repeat_action(&self) -> bool {
|
||||
pub fn can_repeat_action(&self, wallet: &Wallet) -> bool {
|
||||
if let Some(a) = &self.action {
|
||||
return self.action_error.is_some() && a != &WalletTransactionAction::Cancelling
|
||||
self.action_error.is_some() && a != &WalletTxAction::Cancelling
|
||||
} else {
|
||||
// Can resend over Tor.
|
||||
!self.data.confirmed && !self.sending_tor() &&
|
||||
Tor::is_service_running(&wallet.identifier()) && !self.broadcasting() &&
|
||||
self.receiver.is_some()
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Check if transaction is broadcasting after finalization.
|
||||
@@ -423,6 +409,14 @@ impl WalletTransaction {
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Check if transaction is deleting.
|
||||
pub fn deleting(&self) -> bool {
|
||||
if let Some(a) = self.action.as_ref() {
|
||||
return a == &WalletTxAction::Deleting;
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Task for the wallet.
|
||||
@@ -443,19 +437,22 @@ pub enum WalletTask {
|
||||
/// * receiver
|
||||
Send(u64, Option<SlatepackAddress>),
|
||||
/// Send request over Tor.
|
||||
/// * local tx id
|
||||
/// * tx
|
||||
/// * receiver
|
||||
SendTor(u32, SlatepackAddress),
|
||||
SendTor(TxLogEntry, SlatepackAddress),
|
||||
/// Invoice creation.
|
||||
/// * amount
|
||||
Receive(u64),
|
||||
/// Transaction finalization.
|
||||
/// * local tx id
|
||||
/// * tx id
|
||||
Finalize(u32),
|
||||
/// Post transaction to blockchain.
|
||||
/// * local tx id
|
||||
/// * tx id
|
||||
Post(u32),
|
||||
/// Cancel transaction.
|
||||
/// * tx
|
||||
Cancel(TxLogEntry),
|
||||
/// * tx id
|
||||
Cancel(u32),
|
||||
/// Delete transaction.
|
||||
/// * tx id
|
||||
Delete(u32)
|
||||
}
|
||||
+268
-216
@@ -16,7 +16,7 @@ use crate::node::{Node, NodeConfig};
|
||||
use crate::tor::Tor;
|
||||
use crate::wallet::seed::WalletSeed;
|
||||
use crate::wallet::store::TxHeightStore;
|
||||
use crate::wallet::types::{ConnectionMethod, PhraseMode, WalletAccount, WalletData, WalletInstance, WalletTask, WalletTransaction, WalletTransactionAction};
|
||||
use crate::wallet::types::{ConnectionMethod, PhraseMode, WalletAccount, WalletData, WalletInstance, WalletTask, WalletTx, WalletTxAction};
|
||||
use crate::wallet::{ConnectionsConfig, Mnemonic, WalletConfig};
|
||||
use crate::AppConfig;
|
||||
|
||||
@@ -48,10 +48,11 @@ use std::sync::mpsc::Sender;
|
||||
use std::sync::{mpsc, Arc};
|
||||
use std::thread::Thread;
|
||||
use std::time::Duration;
|
||||
use std::{fs, thread};
|
||||
use std::{fs, path, thread};
|
||||
use chrono::Utc;
|
||||
use log::error;
|
||||
use num_bigint::BigInt;
|
||||
use tor_config::deps::Itertools;
|
||||
use uuid::Uuid;
|
||||
|
||||
/// Contains wallet instance, configuration and state, handles wallet commands.
|
||||
@@ -63,7 +64,7 @@ pub struct Wallet {
|
||||
instance: Arc<RwLock<Option<WalletInstance>>>,
|
||||
/// Connection of current wallet instance.
|
||||
connection: Arc<RwLock<ConnectionMethod>>,
|
||||
/// Wallet secret key for transport service.
|
||||
/// Keychain mask for API calls.
|
||||
keychain_mask: Arc<RwLock<Option<SecretKey>>>,
|
||||
|
||||
/// Wallet Slatepack address to receive txs at transport.
|
||||
@@ -131,7 +132,7 @@ pub struct Wallet {
|
||||
/// Tasks sender.
|
||||
tasks_sender: Arc<RwLock<Option<Sender<WalletTask>>>>,
|
||||
/// Task result with optional transaction identifier.
|
||||
task_result: Arc<RwLock<Option<(Option<String>, WalletTask)>>>,
|
||||
task_result: Arc<RwLock<Option<(Option<u32>, WalletTask)>>>,
|
||||
}
|
||||
|
||||
impl Wallet {
|
||||
@@ -379,13 +380,8 @@ impl Wallet {
|
||||
}
|
||||
}
|
||||
|
||||
// Set Slatepack address and secret key.
|
||||
if let Ok((key, addr)) = self.get_secret_key_addr() {
|
||||
let mut w_key = self.secret_key.write();
|
||||
*w_key = Some(key);
|
||||
let mut w_address = self.slatepack_address.write();
|
||||
*w_address = Some(addr.to_string());
|
||||
}
|
||||
// Update Slatepack address and secret key.
|
||||
self.update_secret_key_addr()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -403,7 +399,7 @@ impl Wallet {
|
||||
}
|
||||
|
||||
/// Retrieve wallet [`SecretKey`] and Slatepack address for transport.
|
||||
fn get_secret_key_addr(&self) -> Result<(SecretKey, SlatepackAddress), Error> {
|
||||
fn update_secret_key_addr(&self) -> Result<(), Error> {
|
||||
let r_inst = self.instance.as_ref().read();
|
||||
let instance = r_inst.clone().unwrap();
|
||||
let mut w_lock = instance.lock();
|
||||
@@ -414,7 +410,11 @@ impl Wallet {
|
||||
let sec_key = address::address_from_derivation_path(&k, &parent_key_id, 0)
|
||||
.map_err(|e| Error::TorConfig(format!("{:?}", e)))?;
|
||||
let addr = SlatepackAddress::try_from(&sec_key)?;
|
||||
Ok((sec_key, addr))
|
||||
let mut w_key = self.secret_key.write();
|
||||
*w_key = Some(sec_key);
|
||||
let mut w_address = self.slatepack_address.write();
|
||||
*w_address = Some(addr.to_string());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get unique opened wallet identifier, including current account.
|
||||
@@ -533,13 +533,16 @@ impl Wallet {
|
||||
if !self.is_open() || !has_instance {
|
||||
return;
|
||||
}
|
||||
self.closing.store(true, Ordering::Relaxed);
|
||||
|
||||
// Stop repairing.
|
||||
if self.is_repairing() {
|
||||
self.repair_needed.store(false, Ordering::Relaxed);
|
||||
}
|
||||
// Close wallet at separate thread.
|
||||
let wallet_close = self.clone();
|
||||
let service_id = wallet_close.identifier();
|
||||
let conn = wallet_close.connection.clone();
|
||||
thread::spawn(move || {
|
||||
wallet_close.closing.store(true, Ordering::Relaxed);
|
||||
// Wait common operations to finish.
|
||||
while wallet_close.message_opening() || wallet_close.send_creating() ||
|
||||
wallet_close.invoice_creating() {
|
||||
@@ -628,12 +631,11 @@ impl Wallet {
|
||||
}
|
||||
|
||||
/// Select transaction by slate id.
|
||||
fn retrieve_tx_by_id(&self, id: Uuid) -> Option<TxLogEntry> {
|
||||
fn retrieve_tx_by_id(&self, id: Option<u32>, slate_id: Option<Uuid>) -> Option<TxLogEntry> {
|
||||
let r_inst = self.instance.as_ref().read();
|
||||
let inst = r_inst.clone().unwrap();
|
||||
let mask = self.keychain_mask();
|
||||
let tx_id = Some(id);
|
||||
if let Ok((_, txs)) = retrieve_txs(inst, mask.as_ref(), &None, false, None, tx_id, None) {
|
||||
if let Ok((_, txs)) = retrieve_txs(inst, mask.as_ref(), &None, false, id, slate_id, None) {
|
||||
if !txs.is_empty() {
|
||||
return Some(txs.get(0).unwrap().clone())
|
||||
}
|
||||
@@ -650,8 +652,12 @@ impl Wallet {
|
||||
let w = lc.wallet_inst()?;
|
||||
let parent_key_id = w.parent_key_id();
|
||||
// Retrieve txs from database.
|
||||
let txs_iter = w.tx_log_iter()
|
||||
let txs: Vec<TxLogEntry> = w.tx_log_iter()
|
||||
.filter(|tx_entry| tx_entry.parent_key_id == parent_key_id)
|
||||
// Filter transactions to not show txs without slate (usually unspent outputs).
|
||||
.filter(|tx| {
|
||||
tx.tx_slate_id.is_some() || (tx.tx_slate_id.is_none() && tx.payment_proof.is_some())
|
||||
})
|
||||
.filter(|tx_entry| {
|
||||
if tx_entry.tx_type == TxLogEntryType::TxSent
|
||||
|| tx_entry.tx_type == TxLogEntryType::TxSentCancelled {
|
||||
@@ -663,20 +669,26 @@ impl Wallet {
|
||||
- BigInt::from(tx_entry.amount_debited)
|
||||
>= BigInt::from(1)
|
||||
}
|
||||
});
|
||||
let mut return_txs: Vec<TxLogEntry> = txs_iter.collect();
|
||||
// Sort txs by creation date and confirmation status reversing an order.
|
||||
return_txs.sort_by_key(|tx| if !tx.confirmed && (tx.tx_type == TxLogEntryType::TxSent ||
|
||||
tx.tx_type == TxLogEntryType::TxReceived) {
|
||||
i64::MAX
|
||||
} else {
|
||||
tx.creation_ts.timestamp()
|
||||
});
|
||||
// return_txs.sort_by_key(|tx| tx.confirmed);
|
||||
return_txs.reverse();
|
||||
// Apply limit.
|
||||
return_txs = return_txs.into_iter().take(limit as usize).collect();
|
||||
Ok(return_txs)
|
||||
})
|
||||
// Sort txs by creation date and confirmation status.
|
||||
.sorted_by_key(|tx| if !tx.confirmed && (tx.tx_type == TxLogEntryType::TxSent ||
|
||||
tx.tx_type == TxLogEntryType::TxReceived) {
|
||||
-i64::MAX
|
||||
} else {
|
||||
-tx.creation_ts.timestamp()
|
||||
})
|
||||
// Sort to show unconfirmed at top.
|
||||
.sorted_by_key(|tx| {
|
||||
tx.confirmed || tx.tx_type == TxLogEntryType::TxReceivedCancelled ||
|
||||
tx.tx_type == TxLogEntryType::TxSentCancelled ||
|
||||
tx.tx_type == TxLogEntryType::TxReverted
|
||||
})
|
||||
// Apply limit.
|
||||
.take(limit as usize)
|
||||
.collect();
|
||||
// Reverse an order.
|
||||
// txs.reverse();
|
||||
Ok(txs)
|
||||
}
|
||||
|
||||
/// Send a task to the wallet.
|
||||
@@ -740,13 +752,8 @@ impl Wallet {
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
// Setup secret key and Slatepack address.
|
||||
if let Ok((key, addr)) = self.get_secret_key_addr() {
|
||||
let mut w_key = self.secret_key.write();
|
||||
*w_key = Some(key);
|
||||
let mut w_address = self.slatepack_address.write();
|
||||
*w_address = Some(addr.to_string());
|
||||
}
|
||||
// Update Slatepack address and secret key.
|
||||
self.update_secret_key_addr()?;
|
||||
|
||||
// Save account label into config.
|
||||
let mut w_config = self.config.write();
|
||||
@@ -806,7 +813,7 @@ impl Wallet {
|
||||
let wallet = self.clone();
|
||||
thread::spawn(move || {
|
||||
// Wait when current sync will be finished.
|
||||
if wallet.syncing() {
|
||||
while wallet.syncing() {
|
||||
thread::sleep(Duration::from_secs(1));
|
||||
}
|
||||
// Sync wallet data with new limit.
|
||||
@@ -893,7 +900,7 @@ impl Wallet {
|
||||
/// Check if Slatepack file exists.
|
||||
pub fn slatepack_exists(&self, slate: &Slate) -> bool {
|
||||
let slatepack_path = self.get_config().get_slate_path(slate);
|
||||
fs::exists(slatepack_path).unwrap()
|
||||
fs::exists(slatepack_path).unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Calculate transaction fee for provided amount.
|
||||
@@ -969,12 +976,12 @@ impl Wallet {
|
||||
}
|
||||
|
||||
/// Send slate to Tor address.
|
||||
async fn send_tor(&self, slate: &Slate, addr: &SlatepackAddress) -> Result<Slate, Error> {
|
||||
self.on_tx_action(slate.id.to_string(), Some(WalletTransactionAction::SendingTor));
|
||||
async fn send_tor(&self, id: u32, s: &Slate, addr: &SlatepackAddress) -> Result<Slate, Error> {
|
||||
self.on_tx_action(id, Some(WalletTxAction::SendingTor));
|
||||
|
||||
let tor_addr = OnionV3Address::try_from(addr).unwrap().to_http_str();
|
||||
let url = format!("{}/v2/foreign", tor_addr);
|
||||
let slate_send = VersionedSlate::into_version(slate.clone(), SlateVersion::V4)?;
|
||||
let slate_send = VersionedSlate::into_version(s.clone(), SlateVersion::V4)?;
|
||||
let body = json!({
|
||||
"jsonrpc": "2.0",
|
||||
"method": "receive_tx",
|
||||
@@ -985,7 +992,10 @@ impl Wallet {
|
||||
null
|
||||
]
|
||||
}).to_string();
|
||||
|
||||
// Wait Tor service to launch.
|
||||
while Tor::is_service_starting(&self.identifier()) {
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
}
|
||||
// Send request to receiver.
|
||||
let req_res = Tor::post(body, url).await;
|
||||
if req_res.is_none() {
|
||||
@@ -1074,8 +1084,8 @@ impl Wallet {
|
||||
}
|
||||
|
||||
/// Finalize transaction from provided message as sender or invoice issuer.
|
||||
fn finalize(&self, slate: &Slate) -> Result<Slate, Error> {
|
||||
self.on_tx_action(slate.id.to_string(), Some(WalletTransactionAction::Finalizing));
|
||||
fn finalize(&self, slate: &Slate, id: u32) -> Result<Slate, Error> {
|
||||
self.on_tx_action(id, Some(WalletTxAction::Finalizing));
|
||||
|
||||
let r_inst = self.instance.as_ref().read();
|
||||
let instance = r_inst.clone().unwrap();
|
||||
@@ -1090,14 +1100,16 @@ impl Wallet {
|
||||
let _ = self.create_slatepack_message(&slate, None)?;
|
||||
|
||||
// Clear tx action.
|
||||
self.on_tx_action(slate.id.to_string(), None);
|
||||
self.on_tx_action(id, None);
|
||||
|
||||
Ok(slate)
|
||||
}
|
||||
|
||||
/// Post transaction to blockchain.
|
||||
fn post(&self, slate: &Slate) -> Result<(), Error> {
|
||||
self.on_tx_action(slate.id.to_string(), Some(WalletTransactionAction::Posting));
|
||||
fn post(&self, slate: &Slate, id: Option<u32>) -> Result<(), Error> {
|
||||
if let Some(id) = id {
|
||||
self.on_tx_action(id, Some(WalletTxAction::Posting));
|
||||
}
|
||||
|
||||
let r_inst = self.instance.as_ref().read();
|
||||
let instance = r_inst.clone().unwrap();
|
||||
@@ -1108,19 +1120,19 @@ impl Wallet {
|
||||
})?;
|
||||
|
||||
// Clear tx action.
|
||||
self.on_tx_action(slate.id.to_string(), None);
|
||||
|
||||
if let Some(id) = id {
|
||||
self.on_tx_action(id, None);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Cancel transaction.
|
||||
fn cancel(&self, tx: &TxLogEntry) -> Result<(), Error> {
|
||||
let id = tx.tx_slate_id.unwrap().to_string();
|
||||
self.on_tx_action(id.clone(), Some(WalletTransactionAction::Cancelling));
|
||||
fn cancel(&self, id: u32) -> Result<(), Error> {
|
||||
self.on_tx_action(id, Some(WalletTxAction::Cancelling));
|
||||
|
||||
let r_inst = self.instance.as_ref().read();
|
||||
let instance = r_inst.clone().unwrap();
|
||||
cancel_tx(instance, self.keychain_mask().as_ref(), &None, Some(tx.id), None)?;
|
||||
cancel_tx(instance, self.keychain_mask().as_ref(), &None, Some(id), None)?;
|
||||
|
||||
// Clear tx action.
|
||||
self.on_tx_action(id, None);
|
||||
@@ -1129,25 +1141,34 @@ impl Wallet {
|
||||
}
|
||||
|
||||
/// Update transaction action status.
|
||||
fn on_tx_action(&self, id: String, action: Option<WalletTransactionAction>) {
|
||||
fn on_tx_action(&self, id: u32, action: Option<WalletTxAction>) {
|
||||
let mut w_data = self.data.write();
|
||||
w_data.as_mut().unwrap().on_tx_action(id, action);
|
||||
if let Some(data) = w_data.as_mut() {
|
||||
data.on_tx_action(id, action);
|
||||
}
|
||||
}
|
||||
|
||||
/// Update transaction action error status.
|
||||
fn on_tx_error(&self, id: String, err: Option<Error>) {
|
||||
fn on_tx_error(&self, id: u32, err: Option<Error>) {
|
||||
let mut w_data = self.data.write();
|
||||
w_data.as_mut().unwrap().on_tx_error(id, err);
|
||||
if let Some(data) = w_data.as_mut() {
|
||||
data.on_tx_error(id, err);
|
||||
}
|
||||
}
|
||||
|
||||
/// Save task result to consume later.
|
||||
fn on_task_result(&self, id: Option<String>, task: &WalletTask) {
|
||||
fn on_task_result(&self, tx: Option<TxLogEntry>, task: &WalletTask) {
|
||||
let mut w_res = self.task_result.write();
|
||||
let id = if let Some(t) = tx {
|
||||
Some(t.id)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
*w_res = Some((id, task.clone()));
|
||||
}
|
||||
|
||||
/// Consume result of successful task.
|
||||
pub fn consume_task_result(&self) -> Option<(Option<String>, WalletTask)> {
|
||||
pub fn consume_task_result(&self) -> Option<(Option<u32>, WalletTask)> {
|
||||
let res = {
|
||||
let r_res = self.task_result.read();
|
||||
r_res.clone()
|
||||
@@ -1159,7 +1180,7 @@ impl Wallet {
|
||||
}
|
||||
|
||||
/// Get possible transaction confirmation height.
|
||||
fn tx_height(&self, tx: &WalletTransaction) -> Result<Option<u64>, Error> {
|
||||
fn tx_height(&self, tx: &WalletTx) -> Result<Option<u64>, Error> {
|
||||
let mut tx_height = None;
|
||||
if tx.data.confirmed && tx.data.kernel_excess.is_some() {
|
||||
let r_inst = self.instance.as_ref().read();
|
||||
@@ -1185,17 +1206,45 @@ impl Wallet {
|
||||
Ok(tx_height)
|
||||
}
|
||||
|
||||
/// Get transaction Slate from database.
|
||||
fn get_tx(&self, tx_id: u32) -> Option<Slate> {
|
||||
/// Get stored transaction Slate.
|
||||
fn get_tx_slate(&self, tx_id: Option<u32>, slate_id: Option<&Uuid>) -> Option<Slate> {
|
||||
let r_inst = self.instance.as_ref().read();
|
||||
let instance = r_inst.clone().unwrap();
|
||||
let api = Owner::new(instance, None);
|
||||
if let Ok(s) = api.get_stored_tx(self.keychain_mask().as_ref(), Some(tx_id), None) {
|
||||
if let Ok(s) = api.get_stored_tx(self.keychain_mask().as_ref(), tx_id, slate_id) {
|
||||
return s;
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Delete transaction from database.
|
||||
fn delete_tx(&self, id: u32) -> Result<(), Error> {
|
||||
self.on_tx_action(id, Some(WalletTxAction::Deleting));
|
||||
|
||||
let slate = self.get_tx_slate(Some(id), None);
|
||||
let r_inst = self.instance.as_ref().read();
|
||||
let instance = r_inst.clone().unwrap();
|
||||
let keychain_mask = self.keychain_mask();
|
||||
let mut wallet_lock = instance.lock();
|
||||
let lc = wallet_lock.lc_provider()?;
|
||||
let w = lc.wallet_inst()?;
|
||||
let parent_key = w.parent_key_id();
|
||||
let mut batch = w.batch(keychain_mask.as_ref())?;
|
||||
batch.delete_tx_log_entry(id, &parent_key)?;
|
||||
batch.commit()?;
|
||||
|
||||
// Delete transaction files.
|
||||
if let Some(s) = slate {
|
||||
let slatepack_path = self.get_config().get_slate_path(&s);
|
||||
fs::remove_file(&slatepack_path).unwrap_or_default();
|
||||
let path = path::Path::new(&self.get_config().get_data_path())
|
||||
.join("saved_txs")
|
||||
.join(format!("{}.grintx", s.id));
|
||||
fs::remove_file(&path).unwrap_or_default();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Change wallet password.
|
||||
pub fn change_password(&self, old: String, new: String) -> Result<(), Error> {
|
||||
let r_inst = self.instance.as_ref().read();
|
||||
@@ -1577,37 +1626,32 @@ fn start_sync(wallet: Wallet) -> Thread {
|
||||
|
||||
/// Handle wallet task.
|
||||
async fn handle_task(w: &Wallet, t: WalletTask) {
|
||||
let send_tor = async |s: &Slate, r: &SlatepackAddress| {
|
||||
let id = s.id.to_string();
|
||||
match w.send_tor(&s, r).await {
|
||||
let send_tor = async |tx: TxLogEntry, s: &Slate, r: &SlatepackAddress| {
|
||||
match w.send_tor(tx.id, &s, r).await {
|
||||
Ok(s) => {
|
||||
match w.finalize(&s) {
|
||||
match w.finalize(&s, tx.id) {
|
||||
Ok(s) => {
|
||||
match w.post(&s) {
|
||||
match w.post(&s, Some(tx.id)) {
|
||||
Ok(_) => {
|
||||
sync_wallet_data(&w, false);
|
||||
w.on_task_result(Some(id), &t);
|
||||
w.on_task_result(Some(tx), &t);
|
||||
}
|
||||
Err(e) => {
|
||||
error!("send tor post error: {:?}", e);
|
||||
w.on_tx_error(id, Some(e));
|
||||
w.on_tx_error(tx.id, Some(e));
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!("send tor finalize error: {:?}", e);
|
||||
sync_wallet_data(&w, false);
|
||||
if let Some(tx) = w.retrieve_tx_by_id(s.id) {
|
||||
let _ = w.cancel(&tx);
|
||||
}
|
||||
w.on_tx_error(id, Some(e));
|
||||
w.task(WalletTask::Cancel(tx.id));
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!("send tor error: {:?}", e);
|
||||
w.on_tx_error(id.clone(), Some(e));
|
||||
w.on_task_result(Some(id), &t);
|
||||
w.on_tx_error(tx.id, Some(e));
|
||||
w.on_task_result(Some(tx), &t);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1617,74 +1661,72 @@ async fn handle_task(w: &Wallet, t: WalletTask) {
|
||||
return;
|
||||
}
|
||||
let w = w.clone();
|
||||
let load = w.message_opening.clone();
|
||||
let msg = m.clone();
|
||||
thread::spawn(move || {
|
||||
load.store(true, Ordering::Relaxed);
|
||||
if let Ok((s, dest)) = w.parse_slatepack(&msg) {
|
||||
let id = s.id.to_string();
|
||||
// Check if message already exists.
|
||||
let exists = {
|
||||
let mut exists = w.slatepack_exists(&s);
|
||||
if !exists && (s.state == SlateState::Invoice2 ||
|
||||
s.state == SlateState::Standard2) {
|
||||
let mut slate = s.clone();
|
||||
slate.state = if s.state == SlateState::Standard2 {
|
||||
SlateState::Standard3
|
||||
} else {
|
||||
SlateState::Invoice3
|
||||
};
|
||||
exists = w.slatepack_exists(&slate);
|
||||
}
|
||||
exists
|
||||
};
|
||||
if exists {
|
||||
w.on_task_result(Some(id), &t);
|
||||
load.store(false, Ordering::Relaxed);
|
||||
return;
|
||||
w.message_opening.store(true, Ordering::Relaxed);
|
||||
if let Ok((s, dest)) = w.parse_slatepack(&msg) {
|
||||
let tx = w.retrieve_tx_by_id(None, Some(s.id));
|
||||
// Check if message already exists.
|
||||
let exists = {
|
||||
let mut exists = w.slatepack_exists(&s);
|
||||
if !exists && (s.state == SlateState::Invoice2 ||
|
||||
s.state == SlateState::Standard2) {
|
||||
let mut slate = s.clone();
|
||||
slate.state = if s.state == SlateState::Standard2 {
|
||||
SlateState::Standard3
|
||||
} else {
|
||||
SlateState::Invoice3
|
||||
};
|
||||
exists = w.slatepack_exists(&slate);
|
||||
}
|
||||
// Create response or finalize.
|
||||
match s.state {
|
||||
SlateState::Standard1 | SlateState::Invoice1 => {
|
||||
if s.state != SlateState::Standard1 {
|
||||
if let Ok(_) = w.pay(&s) {
|
||||
sync_wallet_data(&w, false);
|
||||
w.on_task_result(Some(id), &t);
|
||||
}
|
||||
} else {
|
||||
if let Ok(_) = w.receive(&s, dest) {
|
||||
sync_wallet_data(&w, false);
|
||||
w.on_task_result(Some(id), &t);
|
||||
}
|
||||
exists
|
||||
};
|
||||
if exists {
|
||||
w.on_task_result(tx, &t);
|
||||
w.message_opening.store(false, Ordering::Relaxed);
|
||||
return;
|
||||
}
|
||||
// Create response or finalize.
|
||||
match s.state {
|
||||
SlateState::Standard1 | SlateState::Invoice1 => {
|
||||
if s.state != SlateState::Standard1 {
|
||||
if let Ok(_) = w.pay(&s) {
|
||||
sync_wallet_data(&w, false);
|
||||
let tx = w.retrieve_tx_by_id(None, Some(s.id));
|
||||
w.on_task_result(tx, &t);
|
||||
}
|
||||
} else {
|
||||
if let Ok(_) = w.receive(&s, dest) {
|
||||
sync_wallet_data(&w, false);
|
||||
let tx = w.retrieve_tx_by_id(None, Some(s.id));
|
||||
w.on_task_result(tx, &t);
|
||||
}
|
||||
}
|
||||
SlateState::Standard2 | SlateState::Invoice2 => {
|
||||
match w.finalize(&s) {
|
||||
}
|
||||
SlateState::Standard2 | SlateState::Invoice2 => {
|
||||
if let Some(tx) = tx {
|
||||
match w.finalize(&s, tx.id) {
|
||||
Ok(s) => {
|
||||
match w.post(&s) {
|
||||
match w.post(&s, Some(tx.id)) {
|
||||
Ok(_) => {
|
||||
sync_wallet_data(&w, false);
|
||||
}
|
||||
Err(e) => {
|
||||
error!("message tx post error: {:?}", e);
|
||||
w.on_tx_error(id, Some(e));
|
||||
w.on_tx_error(tx.id, Some(e));
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
if let Some(tx) = w.retrieve_tx_by_id(s.id) {
|
||||
let _ = w.cancel(&tx);
|
||||
}
|
||||
error!("message tx finalize error: {:?}", e);
|
||||
w.on_tx_error(id, Some(e));
|
||||
w.task(WalletTask::Cancel(tx.id));
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
load.store(false, Ordering::Relaxed);
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
w.message_opening.store(false, Ordering::Relaxed);
|
||||
}
|
||||
WalletTask::CalculateFee(a, _) => {
|
||||
// Wait if there are no more fee tasks or handle next input value.
|
||||
@@ -1711,96 +1753,107 @@ async fn handle_task(w: &Wallet, t: WalletTask) {
|
||||
w.send_creating.store(true, Ordering::Relaxed);
|
||||
if let Ok(s) = w.send(*a, r.clone()) {
|
||||
sync_wallet_data(&w, false);
|
||||
if r.is_some() && Tor::is_service_running(&w.identifier()) {
|
||||
w.send_creating.store(false, Ordering::Relaxed);
|
||||
let addr = r.as_ref().unwrap();
|
||||
send_tor(&s, addr).await;
|
||||
return;
|
||||
} else {
|
||||
w.on_task_result(Some(s.id.to_string()), &t);
|
||||
let tx = w.retrieve_tx_by_id(None, Some(s.id));
|
||||
if let Some(tx) = tx {
|
||||
if let Some(addr) = r {
|
||||
let id = w.identifier();
|
||||
if Tor::is_service_running(&id) || Tor::is_service_starting(&id) {
|
||||
w.send_creating.store(false, Ordering::Relaxed);
|
||||
send_tor(tx, &s, addr).await;
|
||||
return;
|
||||
} else {
|
||||
w.on_task_result(Some(tx), &t);
|
||||
}
|
||||
} else {
|
||||
w.on_task_result(Some(tx), &t);
|
||||
}
|
||||
}
|
||||
}
|
||||
w.send_creating.store(false, Ordering::Relaxed);
|
||||
}
|
||||
WalletTask::SendTor(id, r) => {
|
||||
if let Some(s) = w.get_tx(*id) {
|
||||
send_tor(&s, r).await;
|
||||
WalletTask::SendTor(tx, r) => {
|
||||
if let Some(s) = w.get_tx_slate(Some(tx.id), None) {
|
||||
send_tor(tx.clone(), &s, r).await;
|
||||
}
|
||||
}
|
||||
WalletTask::Receive(a) => {
|
||||
w.invoice_creating.store(true, Ordering::Relaxed);
|
||||
if let Ok(s) = w.issue_invoice(*a) {
|
||||
sync_wallet_data(&w, false);
|
||||
w.on_task_result(Some(s.id.to_string()), &t);
|
||||
let tx = w.retrieve_tx_by_id(None, Some(s.id));
|
||||
if let Some(tx) = tx {
|
||||
w.on_task_result(Some(tx), &t);
|
||||
}
|
||||
}
|
||||
w.invoice_creating.store(false, Ordering::Relaxed);
|
||||
},
|
||||
WalletTask::Finalize(id) => {
|
||||
let slate = &w.get_tx(*id).unwrap();
|
||||
w.on_tx_error(slate.id.to_string(), None);
|
||||
match w.finalize(slate) {
|
||||
Ok(s) => {
|
||||
match w.post(&s) {
|
||||
Ok(_) => {
|
||||
sync_wallet_data(&w, false);
|
||||
}
|
||||
Err(e) => {
|
||||
error!("tx finalize post error: {:?}", e);
|
||||
w.on_tx_error(slate.id.to_string(), Some(e));
|
||||
if let Some(s) = w.get_tx_slate(Some(*id), None) {
|
||||
w.on_tx_error(*id, None);
|
||||
match w.finalize(&s, *id) {
|
||||
Ok(s) => {
|
||||
match w.post(&s, Some(*id)) {
|
||||
Ok(_) => {
|
||||
sync_wallet_data(&w, false);
|
||||
}
|
||||
Err(e) => {
|
||||
error!("tx finalize post error: {:?}", e);
|
||||
w.on_tx_error(*id, Some(e));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
if let Some(tx) = w.retrieve_tx_by_id(slate.id) {
|
||||
let _ = w.cancel(&tx);
|
||||
Err(e) => {
|
||||
error!("tx finalize error: {:?}", e);
|
||||
w.task(WalletTask::Cancel(*id));
|
||||
}
|
||||
error!("tx finalize error: {:?}", e);
|
||||
w.on_tx_error(slate.id.to_string(), Some(e));
|
||||
}
|
||||
} else {
|
||||
error!("tx finalize: slate not found");
|
||||
w.task(WalletTask::Cancel(*id));
|
||||
}
|
||||
}
|
||||
WalletTask::Post(id) => {
|
||||
let slate = &w.get_tx(*id).unwrap();
|
||||
w.on_tx_error(slate.id.to_string(), None);
|
||||
|
||||
// Cleanup broadcasting tx height.
|
||||
let tx_height_store = TxHeightStore::new(w.get_config().get_extra_db_path());
|
||||
tx_height_store.delete_broadcasting_height(&slate.id.to_string());
|
||||
|
||||
let has_data = {
|
||||
let r_data = w.data.read();
|
||||
r_data.is_some()
|
||||
};
|
||||
if has_data {
|
||||
let mut w_data = w.data.write();
|
||||
for tx in w_data.as_mut().unwrap().txs.as_mut().unwrap() {
|
||||
if tx.data.id == *id {
|
||||
tx.broadcasting_height = None;
|
||||
break;
|
||||
if let Some(s) = w.get_tx_slate(Some(*id), None) {
|
||||
w.on_tx_error(*id, None);
|
||||
// Cleanup broadcasting tx height.
|
||||
let tx_height_store = TxHeightStore::new(w.get_config().get_extra_db_path());
|
||||
tx_height_store.delete_broadcasting_height(&id.to_string());
|
||||
let has_data = {
|
||||
let r_data = w.data.read();
|
||||
r_data.is_some()
|
||||
};
|
||||
if has_data {
|
||||
let mut w_data = w.data.write();
|
||||
for tx in w_data.as_mut().unwrap().txs.as_mut().unwrap() {
|
||||
if tx.data.id == *id {
|
||||
tx.broadcasting_height = None;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match w.post(slate) {
|
||||
Ok(_) => {
|
||||
sync_wallet_data(&w, false);
|
||||
}
|
||||
Err(e) => {
|
||||
error!("tx post error: {:?}", e);
|
||||
w.on_tx_error(slate.id.to_string(), Some(e));
|
||||
// Post transaction.
|
||||
match w.post(&s, Some(*id)) {
|
||||
Ok(_) => {
|
||||
sync_wallet_data(&w, false);
|
||||
}
|
||||
Err(e) => {
|
||||
error!("tx post error: {:?}", e);
|
||||
w.on_tx_error(*id, Some(e));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
error!("tx post: slate not found");
|
||||
w.task(WalletTask::Cancel(*id));
|
||||
}
|
||||
|
||||
}
|
||||
WalletTask::Cancel(tx) => {
|
||||
match w.cancel(tx) {
|
||||
WalletTask::Cancel(id) => {
|
||||
match w.cancel(*id) {
|
||||
Ok(_) => {
|
||||
sync_wallet_data(&w, false);
|
||||
}
|
||||
Err(e) => {
|
||||
error!("tx cancel error: {:?}", e);
|
||||
let id = tx.tx_slate_id.unwrap().to_string();
|
||||
w.on_tx_error(id, Some(e));
|
||||
w.on_tx_error(*id, Some(e));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1810,6 +1863,15 @@ async fn handle_task(w: &Wallet, t: WalletTask) {
|
||||
w.proof_verifying.store(false, Ordering::Relaxed);
|
||||
w.on_task_result(None, &WalletTask::VerifyProof(p.clone(), Some(res)));
|
||||
}
|
||||
WalletTask::Delete(id) => {
|
||||
match w.delete_tx(*id) {
|
||||
Ok(_) => sync_wallet_data(&w, false),
|
||||
Err(e) => {
|
||||
error!("tx delete error: {:?}", e);
|
||||
w.on_tx_error(*id, Some(e));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1926,21 +1988,10 @@ fn update_txs(wallet: &Wallet, mut txs_limit: u32) -> Result<(), Error> {
|
||||
return Err(Error::GenericError("Wallet is not open".to_string()));
|
||||
}
|
||||
|
||||
// Filter transactions to not show txs without slate (usually unspent outputs).
|
||||
let mut filter_txs = txs.iter().map(|v| v.clone()).filter(|tx| {
|
||||
tx.tx_slate_id.is_some() || (tx.tx_slate_id.is_none() && tx.payment_proof.is_some())
|
||||
}).collect::<Vec<TxLogEntry>>();
|
||||
|
||||
// Sort to show unconfirmed at top.
|
||||
filter_txs.sort_by_key(|tx| {
|
||||
tx.confirmed || tx.tx_type == TxLogEntryType::TxReceivedCancelled ||
|
||||
tx.tx_type == TxLogEntryType::TxSentCancelled ||
|
||||
tx.tx_type == TxLogEntryType::TxReverted
|
||||
});
|
||||
|
||||
// Update limit with actual length.
|
||||
let txs_size = txs.len() as u32;
|
||||
let filter_size = filter_txs.len() as u32;
|
||||
let filter_size = txs.len() as u32;
|
||||
|
||||
if txs_size > filter_size && txs_limit >= filter_size {
|
||||
txs_limit = txs_limit - (txs_size - filter_size);
|
||||
}
|
||||
@@ -1949,11 +2000,11 @@ fn update_txs(wallet: &Wallet, mut txs_limit: u32) -> Result<(), Error> {
|
||||
let tx_height_store = TxHeightStore::new(wallet.get_config().get_extra_db_path());
|
||||
let data = wallet.get_data().unwrap();
|
||||
let data_txs = data.txs.unwrap_or(vec![]);
|
||||
let mut new_txs: Vec<WalletTransaction> = vec![];
|
||||
for tx in &filter_txs {
|
||||
let mut new_txs: Vec<WalletTx> = vec![];
|
||||
for tx in &txs {
|
||||
let mut height: Option<u64> = None;
|
||||
let mut broadcasting_height: Option<u64> = None;
|
||||
let mut action: Option<WalletTransactionAction> = None;
|
||||
let mut action: Option<WalletTxAction> = None;
|
||||
let mut action_error: Option<Error> = None;
|
||||
let mut proof: Option<PaymentProof> = None;
|
||||
for t in &data_txs {
|
||||
@@ -1966,13 +2017,13 @@ fn update_txs(wallet: &Wallet, mut txs_limit: u32) -> Result<(), Error> {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let mut new = WalletTransaction::new(tx.clone(),
|
||||
proof.clone(),
|
||||
wallet,
|
||||
height,
|
||||
broadcasting_height,
|
||||
action,
|
||||
action_error);
|
||||
let mut new = WalletTx::new(tx.clone(),
|
||||
proof.clone(),
|
||||
wallet,
|
||||
height,
|
||||
broadcasting_height,
|
||||
action,
|
||||
action_error);
|
||||
// Update Slate state for unconfirmed.
|
||||
let unconfirmed = !tx.confirmed && (tx.tx_type == TxLogEntryType::TxSent ||
|
||||
tx.tx_type == TxLogEntryType::TxReceived);
|
||||
@@ -2016,8 +2067,9 @@ fn update_txs(wallet: &Wallet, mut txs_limit: u32) -> Result<(), Error> {
|
||||
new.broadcasting_height = broadcasting_height;
|
||||
}
|
||||
}
|
||||
|
||||
new_txs.push(new);
|
||||
if !new.deleting() {
|
||||
new_txs.push(new);
|
||||
}
|
||||
}
|
||||
// Update wallet txs.
|
||||
let mut w_data = wallet.data.write();
|
||||
@@ -2142,10 +2194,10 @@ fn repair_wallet(wallet: &Wallet) {
|
||||
}
|
||||
});
|
||||
|
||||
// Start wallet scanning.
|
||||
let r_inst = wallet.instance.as_ref().read();
|
||||
let instance = r_inst.clone().unwrap();
|
||||
let api = Owner::new(instance, Some(info_tx));
|
||||
// Start wallet scanning.
|
||||
match api.scan(wallet.keychain_mask().as_ref(), Some(1), false) {
|
||||
Ok(()) => {
|
||||
// Set sync error if scanning was not complete and wallet is open.
|
||||
|
||||
+1
-1
Submodule wallet updated: a6e0cdae0c...4f3d9aac25
+2
-2
@@ -7,8 +7,8 @@
|
||||
<?endif ?>
|
||||
|
||||
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
|
||||
<Product Id="*" Version="0.3.0" UpgradeCode="C19F9B41-CD13-4F0E-B27D-E0EF8CF1CE91" Language="1033" Name="Grim" Manufacturer="Ardocrat">
|
||||
<Package Id="FA6823B7-7FB1-49A4-BF64-0442BCD2724B" InstallerVersion="300" Compressed="yes"/>
|
||||
<Product Id="*" Version="0.3.4" UpgradeCode="C19F9B41-CD13-4F0E-B27D-E0EF8CF1CE91" Language="1033" Name="Grim" Manufacturer="Ardocrat">
|
||||
<Package Id="ECF79CAD-4441-45A7-B80C-7CC5773FAD82" InstallerVersion="300" Compressed="yes"/>
|
||||
<Media Id="1" Cabinet="grim.cab" EmbedCab="yes" />
|
||||
|
||||
<MajorUpgrade AllowDowngrades = "yes"/>
|
||||
|
||||
Reference in New Issue
Block a user