42 Commits

Author SHA1 Message Date
ardocrat 0026fc3717 build: fix android no_mangle attributes for rust 2024 2026-04-11 23:12:56 +03:00
ardocrat 0fd04f14a4 wallet: save last scanned block info to save progress on scan interruption 2026-04-11 22:52:43 +03:00
ardocrat 3338f51de5 tor: update to arti 0.41 2026-04-11 00:33:41 +03:00
ardocrat 0fa8963bd2 fix: wallet txs selection, wait starting tor service on send 2026-04-10 15:50:58 +03:00
ardocrat 70bba5d7ce pull_to_refresh: refresh when dragged far enough without release 2026-04-10 15:38:43 +03:00
ardocrat 0bb43e1e5d ui: show loader when fee is calculating 2026-04-10 15:18:23 +03:00
ardocrat fd52757549 build: version 0.3.4 2026-04-10 15:09:41 +03:00
ardocrat 6835bb1909 fix: do not send over tor when service not launched 2026-04-10 00:28:27 +03:00
ardocrat 31bc74529c build: update grin node 2026-04-09 20:56:45 +03:00
ardocrat 8d6943975b gui: glow renderer by default 2026-04-09 02:44:06 +03:00
ardocrat 4c5d8abe7b wix: update uuid 2026-04-09 02:44:01 +03:00
ardocrat 4dc42bce4a tor: fix multiline bridge connection 2026-03-30 01:37:36 +03:00
ardocrat e2d5d92f18 build: version 0.3.3 2026-03-30 01:36:45 +03:00
ardocrat b001eb4712 node: update to last git version (fix pibd stuck) 2026-03-29 21:18:36 +03:00
ardocrat f14bd902ea build: update win package guid 2026-03-24 14:52:20 +03:00
ardocrat 33ab11933a build: update wallet branch 2026-03-24 14:51:12 +03:00
ardocrat 6b05a2177e log: info level into file, crash report for android 2026-03-24 13:18:04 +03:00
ardocrat 7bbe637414 ui: do not show username at ext conn settings 2026-03-24 02:32:53 +03:00
ardocrat 9b6252de3a tor: fix connection with multiple bridges 2026-03-24 02:13:08 +03:00
ardocrat 26debcf51c ui: camera paddings, focus on password at wallet creation modal 2026-03-24 02:06:00 +03:00
ardocrat 497b967fd0 node: scan and share connection with qr code 2026-03-23 04:46:29 +03:00
ardocrat 05e18cf6c4 fix: show tx modal after message parse, cancel tx when slate not found 2026-03-23 02:49:23 +03:00
ardocrat 6e50b2b38a ui: make list items clickable, ability to delete tx 2026-03-23 01:21:09 +03:00
ardocrat 9bc96de398 ci: optimize release upload
- separate job telegram upload to avoid forgejo release upload repeat if failed
- upload artifacts to forgejo from another runner

Reviewed-on: https://code.gri.mw/GUI/grim/pulls/55
2026-03-22 22:14:04 +00:00
ardocrat 5a525c50e1 img: update cover 2026-03-19 10:37:45 +03:00
ardocrat ba0af0968d tor: multiline bridges input, optimize tor connection check, add multiple default webtunnel bridges, fix tx cancel on finalization error 2026-03-18 15:44:32 +03:00
ardocrat a0947aa47c ci: fix pre-release check 2026-03-15 21:18:42 +00:00
ardocrat 06c6b8b4f5 android: fix text input on some devices 2026-03-15 23:26:57 +03:00
ardocrat b19335d0bc build: version 0.3.2 2026-03-15 23:25:46 +03:00
ardocrat 40eb30fb75 macos: fix version 2026-03-15 23:25:42 +03:00
ardocrat 8223e52570 build: version script 2026-03-15 23:25:08 +03:00
ardocrat 875bd11bdb ci: macos universal release name 2026-03-10 02:02:15 +03:00
ardocrat 19e4cb664d ci: fix pre-release check 2026-03-10 01:58:28 +03:00
ardocrat 18bc327a99 build: update msi and android version 2026-03-10 01:43:15 +03:00
ardocrat 88e2fb0715 node: update 5.4.0 release 2026-03-10 01:09:53 +03:00
ardocrat feb38dc7cf ui: show scan and wallet actions before getting data from node 2026-03-10 00:49:18 +03:00
ardocrat 28ecb5b1f4 fix: camera image square crop and animate qr code scanning progress 2026-03-10 00:39:42 +03:00
ardocrat 024a9d0098 ui: make qr codes background lighter for better scanning 2026-03-10 00:06:57 +03:00
ardocrat 59cf46e1cb feat: open modal to send amount on address scan 2026-03-09 21:33:11 +03:00
ardocrat 22255e0f2a fix: close wallet panels when settings are open 2026-03-09 20:55:11 +03:00
ardocrat 7fdb8d272b fix: hide account list panel on wallet change, do not store account list at content 2026-03-09 20:48:00 +03:00
ardocrat d043562058 build: bump version 2026-03-09 20:10:01 +03:00
73 changed files with 2968 additions and 2213 deletions
+27 -10
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+18 -14
View File
@@ -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"
+1 -1
View File
@@ -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
View File
@@ -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);
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 181 KiB

After

Width:  |  Height:  |  Size: 191 KiB

+1
View File
@@ -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
+1
View File
@@ -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
+1
View File
@@ -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
+1
View File
@@ -142,6 +142,7 @@ wallets:
payment_proof_desc: 'Введите полученное подтверждение оплаты для проверки транзакции:'
payment_proof_valid: 'Введённое подтверждение оплаты действительно:'
payment_proof_error: 'Введённое подтверждение оплаты недействительно:'
tx_delete_confirmation: Вы уверены, что хотите удалить транзакцию из истории?
transport:
desc: 'Используйте транспорт для синхронных получения или отправки сообщений:'
tor_network: Сеть Tor
+1
View File
@@ -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
+1
View File
@@ -142,6 +142,7 @@ wallets:
payment_proof_desc: '輸入已收款證明以驗證交易:'
payment_proof_valid: '輸入的付款證明有效:'
payment_proof_error: '輸入的付款證明無效:'
tx_delete_confirmation: 你確定要從歷史紀錄中刪除這筆交易嗎?
transport:
desc: '使用传输同步接收或发送消息:'
tor_network: Tor 网络
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
}
}
+2 -2
View File
@@ -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
View File
@@ -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) {
+6 -6
View File
@@ -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();
});
});
+3 -3
View File
@@ -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
View File
@@ -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);
+204 -115
View File
@@ -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)
+1 -1
View File
@@ -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);
+123 -41
View File
@@ -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")
+4 -1
View File
@@ -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());
}
}
+2 -2
View File
@@ -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| {
+50 -40
View File
@@ -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.
+3 -2
View File
@@ -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);
+10
View File
@@ -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
}
+7 -1
View File
@@ -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
View File
@@ -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();
+42 -106
View File
@@ -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();
}
}
}
+63 -54
View File
@@ -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
View File
@@ -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);
}
+5 -3
View File
@@ -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,
+105 -78
View File
@@ -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));
}
+4 -10
View File
@@ -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);
+4 -5
View File
@@ -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);
});
});
});
+38 -34
View File
@@ -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);
});
+137 -48
View File
@@ -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);
});
});
});
});
}
}
+2 -3
View File
@@ -13,7 +13,6 @@
// limitations under the License.
mod content;
mod list;
mod create;
pub use content::*;
pub use content::*;
mod create;
+19 -12
View File
@@ -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;
+4 -3
View File
@@ -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,
}
}
+2 -2
View File
@@ -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| {
+9 -4
View File
@@ -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.
+106 -86
View File
@@ -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();
});
+317 -212
View File
@@ -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.
+43 -25
View File
@@ -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);
}
}
});
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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,
+15 -8
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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 {
+2 -2
View File
@@ -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() {
+1 -1
View File
@@ -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);
+7 -3
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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"/>