Compare commits

...

135 commits

Author SHA1 Message Date
fb159c17a0 i18n: chinese
Some checks failed
Build / Linux Build (push) Has been cancelled
Build / Windows Build (push) Has been cancelled
Build / MacOS Build (push) Has been cancelled
2025-04-27 21:22:08 +03:00
f7eb6580cc tor: trim address on send
Some checks are pending
Build / Linux Build (push) Waiting to run
Build / Windows Build (push) Waiting to run
Build / MacOS Build (push) Waiting to run
2025-04-27 19:51:06 +03:00
43720b34ba fix: external connection deletion
Some checks failed
Build / Linux Build (push) Has been cancelled
Build / Windows Build (push) Has been cancelled
Build / MacOS Build (push) Has been cancelled
2025-04-23 15:10:48 +03:00
f1f0f002ce fix: content redraw at connections 2025-04-23 13:09:31 +03:00
86afa21a60 node: do not remove lock file on cleanup
Some checks are pending
Build / Linux Build (push) Waiting to run
Build / Windows Build (push) Waiting to run
Build / MacOS Build (push) Waiting to run
2025-04-23 12:38:23 +03:00
0169acba81 build: use zig linker for macos and linux for arm on x86
Some checks failed
Build / Linux Build (push) Has been cancelled
Build / Windows Build (push) Has been cancelled
Build / MacOS Build (push) Has been cancelled
2025-04-02 22:30:59 +03:00
073d950d41 github: disable release build
Some checks are pending
Build / Linux Build (push) Waiting to run
Build / Windows Build (push) Waiting to run
Build / MacOS Build (push) Waiting to run
2025-04-02 21:10:45 +03:00
4eaaebd739 release: v0.2.4
Some checks failed
Build / Linux Build (push) Has been cancelled
Build / Windows Build (push) Has been cancelled
Build / MacOS Build (push) Has been cancelled
2025-04-02 20:48:58 +03:00
a9e2106fda git: ignore cargo parse result file
Some checks are pending
Build / Linux Build (push) Waiting to run
Build / Windows Build (push) Waiting to run
Build / MacOS Build (push) Waiting to run
2025-04-02 20:48:11 +03:00
8b427989c5 github: disable release build
Some checks are pending
Build / Linux Build (push) Waiting to run
Build / Windows Build (push) Waiting to run
Build / MacOS Build (push) Waiting to run
2025-04-02 20:37:47 +03:00
f16ce3c69b fix: transparent background on desktop 2025-04-02 20:37:23 +03:00
a1b3330e5e async: use tokio for thread block calls
Some checks are pending
Build / Linux Build (push) Waiting to run
Build / Windows Build (push) Waiting to run
Build / MacOS Build (push) Waiting to run
2025-04-02 19:15:20 +03:00
3da8f5420b build: update tor arti 0.29.0
Some checks are pending
Build / Linux Build (push) Waiting to run
Build / Windows Build (push) Waiting to run
Build / MacOS Build (push) Waiting to run
2025-04-02 17:05:20 +03:00
109e896506 tor: clean error after start
Some checks are pending
Build / Linux Build (push) Waiting to run
Build / Windows Build (push) Waiting to run
Build / MacOS Build (push) Waiting to run
2025-04-02 16:47:07 +03:00
8ad38f381e ui: change values on enter press at node settings modals
Some checks are pending
Build / Linux Build (push) Waiting to run
Build / Windows Build (push) Waiting to run
Build / MacOS Build (push) Waiting to run
2025-04-02 15:49:07 +03:00
1e32315346 win: use system window frame
Some checks are pending
Build / Linux Build (push) Waiting to run
Build / Windows Build (push) Waiting to run
Build / MacOS Build (push) Waiting to run
2025-04-02 15:22:15 +03:00
ef8c645a6a win: allow downgrade install
Some checks are pending
Build / Linux Build (push) Waiting to run
Build / Windows Build (push) Waiting to run
Build / MacOS Build (push) Waiting to run
2025-04-02 14:32:00 +03:00
15ecdf1e57 build: update guid for win installer
Some checks are pending
Build / Linux Build (push) Waiting to run
Build / Windows Build (push) Waiting to run
Build / MacOS Build (push) Waiting to run
2025-04-02 13:31:04 +03:00
587b00c93a build: version for windows
Some checks failed
Build / Windows Build (push) Has been cancelled
Build / Linux Build (push) Has been cancelled
Build / MacOS Build (push) Has been cancelled
2025-04-01 00:26:59 +03:00
aba2bead27 build: update package info, other dependencies
Some checks are pending
Build / Linux Build (push) Waiting to run
Build / Windows Build (push) Waiting to run
Build / MacOS Build (push) Waiting to run
2025-03-31 21:21:51 +03:00
85ce58f69c fix: parse result from scan on top panel
Some checks are pending
Build / Linux Build (push) Waiting to run
Build / Windows Build (push) Waiting to run
Build / MacOS Build (push) Waiting to run
2025-03-31 20:46:23 +03:00
bb7e00b0eb fix: initial color theme setup
Some checks are pending
Build / Linux Build (push) Waiting to run
Build / Windows Build (push) Waiting to run
Build / MacOS Build (push) Waiting to run
2025-03-29 21:52:10 +03:00
d60b35ebef Merge pull request 'macos: use nokhwa camera dependency' (#16) from macos_camera_fix into master
Some checks are pending
Build / Linux Build (push) Waiting to run
Build / Windows Build (push) Waiting to run
Build / MacOS Build (push) Waiting to run
Reviewed-on: https://gri.mw/code/code/GUI/grim/pulls/16
2025-03-29 21:36:25 +03:00
eb60c52224 macos: use nokhwa camera dependency
Some checks failed
Build / Linux Build (push) Has been cancelled
Build / Windows Build (push) Has been cancelled
Build / MacOS Build (push) Has been cancelled
Build / Linux Build (pull_request) Has been cancelled
Build / Windows Build (pull_request) Has been cancelled
Build / MacOS Build (pull_request) Has been cancelled
2025-03-29 21:18:53 +03:00
61828ea2db build: update tor lib
Some checks failed
Build / Windows Build (push) Has been cancelled
Build / MacOS Build (push) Has been cancelled
Build / Linux Build (push) Has been cancelled
2025-03-15 20:41:30 +03:00
7e819e14d1 node: fix peers config saving
Some checks are pending
Build / MacOS Build (push) Waiting to run
Build / Linux Build (push) Waiting to run
Build / Windows Build (push) Waiting to run
2025-03-15 20:35:10 +03:00
1d9b7d9698 wallet: do not lock whole balance on send
Some checks failed
Build / Linux Build (push) Has been cancelled
Build / Windows Build (push) Has been cancelled
Build / MacOS Build (push) Has been cancelled
2025-01-14 17:55:50 +03:00
82c05588bc readme: update title
Some checks are pending
Build / Linux Build (push) Waiting to run
Build / Windows Build (push) Waiting to run
Build / MacOS Build (push) Waiting to run
2025-01-13 21:59:22 +03:00
1cddd05bc0 readme: update img tag
Some checks are pending
Build / Linux Build (push) Waiting to run
Build / Windows Build (push) Waiting to run
Build / MacOS Build (push) Waiting to run
2025-01-13 21:58:29 +03:00
8ad0d1c461 readme: update images
Some checks are pending
Build / Linux Build (push) Waiting to run
Build / Windows Build (push) Waiting to run
Build / MacOS Build (push) Waiting to run
2025-01-13 21:56:48 +03:00
a22a75913c img: add grin logo
Some checks are pending
Build / Linux Build (push) Waiting to run
Build / Windows Build (push) Waiting to run
Build / MacOS Build (push) Waiting to run
2025-01-13 21:55:55 +03:00
e797da0ed8 img: add cover
Some checks are pending
Build / Linux Build (push) Waiting to run
Build / Windows Build (push) Waiting to run
Build / MacOS Build (push) Waiting to run
2025-01-13 21:26:00 +03:00
6936c14ed2 tor: remove macos tls fix
Some checks are pending
Build / Linux Build (push) Waiting to run
Build / Windows Build (push) Waiting to run
Build / MacOS Build (push) Waiting to run
2025-01-13 21:06:34 +03:00
c626ed5a48 tor: clear data on launch, update arti to 0.26.0
Some checks failed
Build / Linux Build (push) Has been cancelled
Build / Windows Build (push) Has been cancelled
Build / MacOS Build (push) Has been cancelled
2025-01-13 19:40:09 +03:00
d79d05ef5a android: debug build without keystore 2025-01-13 16:54:27 +03:00
ardocrat
094a5b8969 release: v0.2.3 2024-10-27 20:12:12 +03:00
ardocrat
12a75f8370 macos: future version update 2024-10-27 19:45:00 +03:00
ardocrat
1c14b9aa93 tx: fix confirmation status for new block, do not show Slatepack message after finalization 2024-10-27 19:02:17 +03:00
ardocrat
8ea388554a github: macos target 11.0 2024-10-27 18:07:22 +03:00
ardocrat
1531c201bb github: macos 12 2024-10-27 00:46:53 +03:00
ardocrat
ed522c56ae github: macos zig linker 2024-10-27 00:40:58 +03:00
ardocrat
4b454ab2f3 github: macos last os 2024-10-27 00:29:27 +03:00
ardocrat
f6fbf7226e fix: window size saving 2024-10-26 23:54:47 +03:00
ardocrat
ebd09ab1c8 camera: update nokhwa, eye for macos, ability to switch camera when another camera not loaded 2024-10-26 23:25:55 +03:00
ardocrat
75cf7edc96 fix: modal padding and window border on desktop 2024-10-26 23:23:39 +03:00
ardocrat
5c8b9c40be build: provide version for android release 2024-10-26 02:17:28 +03:00
ardocrat
dcaf9945c8 ui: wgpu renderer for macos, desktop content background fix, do not show left line for camera content at dual panel mode 2024-10-26 02:16:47 +03:00
ardocrat
f9426287d5 macos: release on darwin without zig, info.plist camera usage description and version update 2024-10-25 20:03:57 +03:00
ardocrat
77281e3ab9 github: fix macos arm sdk 2024-10-23 00:37:41 +03:00
ardocrat
64439ad3d3 github: fix macos deployment target 2024-10-23 00:11:45 +03:00
ardocrat
9494c1292e github: macos coreutils 2024-10-22 23:47:19 +03:00
ardocrat
accf123d49 github: macos build 2024-10-22 23:24:02 +03:00
ardocrat
d77598c259 github: fix macos sdk env 2024-10-22 04:39:08 +03:00
ardocrat
4e6dff52fe github: macos install zig 2024-10-22 04:14:21 +03:00
ardocrat
92d0aac250 github: fix macos sdk unzip 2024-10-22 04:08:26 +03:00
ardocrat
5ef310558a release: v0.2.2 2024-10-22 03:51:01 +03:00
ardocrat
683821b667 build: fix version script 2024-10-22 03:50:48 +03:00
ardocrat
da4cf71fac github: build macos on linux with SDK 10.15 2024-10-22 03:19:34 +03:00
ardocrat
f81ceae940 txs: new block confirmation time 2024-10-22 02:11:25 +03:00
ardocrat
fa6301a1db stratum: fix wallet name after selection, do not panic after stop 2024-10-22 00:12:13 +03:00
ardocrat
442fc425f7 ui: update to egui 0.29.1, wallet qr scan content, panels strokes and colors refactoring, check closeable modal at desktop title, fix app socket name 2024-10-21 12:03:09 +03:00
ardocrat
ea61588ede build: check android lib result 2024-10-12 19:58:14 +03:00
ardocrat
7f67aa134a build: increment on android development 2024-10-12 15:33:23 +03:00
ardocrat
d7d1c53c52 build: incremental release on desktop development 2024-10-12 15:26:01 +03:00
ardocrat
18f52f877a node: remove delay after server start 2024-10-12 15:24:15 +03:00
ardocrat
c13195bd61 stratum: prevent crash at connections thread 2024-10-10 21:51:28 +03:00
ardocrat
e40d5b6474 node: single function to get api secrets 2024-10-10 21:11:50 +03:00
ardocrat
92e5d38755 build: update grin 5.3.3, arti 0.23.0 (fork arti-hyper crate) and non-egui dependencies 2024-10-09 12:58:59 +03:00
ardocrat
ec7e795ba9 build: camera features 2024-10-09 10:13:55 +03:00
ardocrat
af220b2a09 camera: remove eye-rs to fix build for mac, horizontally flip image 2024-10-08 23:23:04 +03:00
ardocrat
846e30cb38 app: better panic handling, macos single app instance 2024-10-08 17:11:45 +03:00
ardocrat
d371d4368b wallet: disable tor listener by default 2024-10-08 14:59:51 +03:00
ardocrat
85fc8101e4 ui: show tx modal on error if exists 2024-10-08 02:37:51 +03:00
ardocrat
e2f58a8938 android: update gradle 2024-10-07 20:55:23 +03:00
ardocrat
7e6954afd9 fix: opened file data providing 2024-10-07 19:45:29 +03:00
ardocrat
bed041a1c3 git: ignore android release artifacts 2024-09-21 00:33:46 +03:00
ardocrat
f955f720d2 release: v0.2.1 2024-09-20 23:33:08 +03:00
ardocrat
b627ac1ca6 fix: mnemonic import 2024-09-20 23:30:41 +03:00
ardocrat
ac0b218376 fix: connection selection 2024-09-20 23:12:44 +03:00
ardocrat
04bf5a5349 github: coreutils for macos 2024-09-20 21:46:17 +03:00
ardocrat
9cce52a7d9 github: fix sha256sum 2024-09-20 20:38:05 +03:00
ardocrat
51e0d87d27 github: fix release 2024-09-20 15:17:41 +03:00
ardocrat
d6f7e2e976 github: release sha256sum 2024-09-20 15:15:18 +03:00
ardocrat
0bbf395a62 build: android warning fix 2024-09-20 15:03:56 +03:00
ardocrat
609d7ceb7a build: remove panic message dependency 2024-09-20 14:45:40 +03:00
ardocrat
b91605864d github: fix macos release 2024-09-20 14:42:37 +03:00
ardocrat
7857b708c9 release: v0.2.0 2024-09-20 14:17:03 +03:00
ardocrat
a0f85538e9 ui: tx modal height 2024-09-20 14:09:53 +03:00
ardocrat
c52da4f479 wallet: accounts balance calculating optimization, payment proof support on send, selection_strategy_is_use_all 2024-09-20 13:56:25 +03:00
ardocrat
af597df7b1 i18n: move confirmation word 2024-09-20 13:49:31 +03:00
ardocrat
2adb29f4ee ui: external connection check and ui repaint fix, tab button callback argument 2024-09-20 13:42:45 +03:00
ardocrat
2b83944f34 ui: show node error status on connection item 2024-09-20 11:10:05 +03:00
ardocrat
71e80f6df7 ui: reset node config from ui on error 2024-09-20 10:58:52 +03:00
ardocrat
0ead11ec6c tx: receiver address 2024-09-20 02:39:06 +03:00
ardocrat
3e249c5314 android: share file type 2024-09-20 00:16:12 +03:00
ardocrat
bacc87945c messages: qr scan modal 2024-09-20 00:09:08 +03:00
ardocrat
2cfd428c4c ui: do not clear qr state 2024-09-19 21:39:59 +03:00
ardocrat
c155deedb5 wallet: qr scan modal, connections content and default list, wallet creation and list refactoring, tx height 2024-09-19 15:56:53 +03:00
Ardocrat
3bc8c407b4
Merge pull request #13 from ardocrat/slatepack_ext_file
Open .slatepack file with the app
2024-09-16 16:08:27 +00:00
ardocrat
c3fae38d5c desktop: open camera check 2024-09-15 15:54:07 +03:00
ardocrat
d6ec4213ab ui: ability to finalize tx only when wallet is loaded 2024-09-14 21:21:03 +03:00
ardocrat
150a0de1c4 android: always build with release-apk profile 2024-09-14 21:17:43 +03:00
ardocrat
7cedebc70e ui: qr scan and accounts modals module, parsing messages fix 2024-09-14 21:11:52 +03:00
ardocrat
fe5aca6f0e build: remove debug from release profile 2024-09-14 16:08:40 +03:00
ardocrat
5d83710fed ui: dark colors fix 2024-09-14 16:02:20 +03:00
ardocrat
1431e307ee ui: separate wallet accounts modal 2024-09-14 15:21:08 +03:00
ardocrat
1934dc3377 desktop: args text 2024-09-14 15:04:11 +03:00
ardocrat
8af06d8860 build: android fix 2024-09-14 13:07:48 +03:00
ardocrat
9ea0da95b7 build: release sha256sum 2024-09-14 12:12:50 +03:00
ardocrat
d39e2ec21e build: android signed release 2024-09-14 02:06:35 +03:00
ardocrat
68c9c9df04 build: local android release 2024-09-14 01:47:06 +03:00
ardocrat
6f7156ef17 github: android secrets 2024-09-13 22:31:28 +03:00
ardocrat
50638ff54e github: android keystore 2024-09-13 22:00:59 +03:00
ardocrat
8594279b98 android: java call result fixes 2024-09-13 21:08:14 +03:00
ardocrat
0205e01b3c build: macos fix 2024-09-13 19:51:33 +03:00
ardocrat
17545c1b7c macos: platform build 2024-09-13 18:57:09 +03:00
ardocrat
bcf821c06a macos: initial file type association 2024-09-13 15:21:43 +03:00
ardocrat
34376d3490 build: fix macos 2024-09-13 14:56:04 +03:00
ardocrat
8ed2308340 macos: build, warn fix 2024-09-13 14:53:22 +03:00
ardocrat
c73cd58eed platform: android file opening, better exit 2024-09-13 14:22:15 +03:00
ardocrat
d78ec570b0 platform: passed data at lib, desktop user attention, check existing file on share at android 2024-09-12 21:27:37 +03:00
ardocrat
dd45f7ce38 desktop: platform socket fix, file extension association for windows 2024-09-12 18:02:02 +03:00
ardocrat
fb7312cb80 desktop: request window focus on data 2024-09-11 21:13:52 +03:00
ardocrat
dbc28205e8 desktop: parse file content from argument on launch, single app instance, wallets selection and opening modals refactoring 2024-09-11 17:01:05 +03:00
ardocrat
a3ed3bd234 build: linux release 2024-09-07 12:45:05 +03:00
ardocrat
21ecf200b8 wallet + ui: optimize sync after tx actions, remove tx repost, share message as file from tx modal, show tx info after tor sending and message creation or finalization, messages and transport modules refactoring, qr code text optimization, wallet dandelion setting, recovery phrase modal next step on enter 2024-09-07 00:11:17 +03:00
ardocrat
c8bca08bdc txs: share message as file from modal, module refactoring 2024-08-15 23:09:42 +03:00
ardocrat
68bd2b81ec peers: fix config edit and load, default mainnet dnsseed 2024-08-13 02:31:38 +03:00
ardocrat
09cfb84b94 fix: ellipsized sync status text at connections 2024-08-12 18:30:10 +03:00
ardocrat
5c1ffb5636 build: push version 2024-08-10 12:15:40 +03:00
ardocrat
7f79cc0708 release: v0.1.3 2024-08-10 12:08:20 +03:00
ardocrat
b0b4f9068a build: version release 2024-08-10 11:59:12 +03:00
ardocrat
cb9e86750c mnemonic: words import and errors check refactoring 2024-08-10 02:35:42 +03:00
ardocrat
86fbf2e14f github: fix android build 2024-08-08 03:08:10 +03:00
ardocrat
e0351cea84 fix: mnemonic words size on creation, wallet creation errors 2024-08-08 03:01:08 +03:00
109 changed files with 11227 additions and 10062 deletions

View file

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

View file

@ -1,260 +0,0 @@
name: Release
on:
push:
tags:
- "v*.*.*"
jobs:
android_release:
name: Android Release
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
cache: gradle
- name: Setup Rust build
run: |
cargo install cargo-ndk
rustup target add aarch64-linux-android
rustup target add armv7-linux-androideabi
rustup target add x86_64-linux-android
- name: Setup Java build
run: |
chmod +x android/gradlew
echo "${{ secrets.ANDROID_RELEASE_KEYSTORE }}" > release.keystore.asc
gpg -d --passphrase "${{ secrets.ANDROID_RELEASE_SECRET }}" --batch release.keystore.asc > android/keystore
echo -e "storePassword=${{ secrets.ANDROID_PASS }}\nkeyPassword=${{ secrets.ANDROID_PASS }}\nkeyAlias=grim\nstoreFile=../keystore" > android/keystore.properties
- name: Build lib ARMv8 1/2
continue-on-error: true
run: |
sed -i -e 's/"rlib"/"cdylib","rlib"/g' Cargo.toml
export CPPFLAGS="-DMDB_USE_ROBUST=0" && export CFLAGS="-DMDB_USE_ROBUST=0" && cargo ndk -t arm64-v8a build --profile release-apk
- name: Build lib ARMv8 2/2
run: |
unset CPPFLAGS && unset CFLAGS && cargo ndk -t arm64-v8a -o android/app/src/main/jniLibs build --profile release-apk
sed -i -e 's/"cdylib","rlib"/"rlib"/g' Cargo.toml
- name: Build APK ARMv8
working-directory: android
run: |
./gradlew assembleRelease
mv app/build/outputs/apk/release/app-release.apk grim-${{ github.ref_name }}-android-armv8.apk
- name: Checksum APK ARMv8
working-directory: android
shell: pwsh
run: get-filehash -algorithm sha256 grim-${{ github.ref_name }}-android-armv8.apk | Format-List | Out-String | ForEach-Object { $_.Trim() } > grim-${{ github.ref_name }}-android-armv8-sha256sum.txt
- name: Build lib ARMv7 1/2
continue-on-error: true
run: |
sed -i -e 's/"rlib"/"cdylib","rlib"/g' Cargo.toml
export CPPFLAGS="-DMDB_USE_ROBUST=0" && export CFLAGS="-DMDB_USE_ROBUST=0" && cargo ndk -t armeabi-v7a build --profile release-apk
- name: Build lib ARMv7 2/2
run: |
unset CPPFLAGS && unset CFLAGS && cargo ndk -t armeabi-v7a -o android/app/src/main/jniLibs build --profile release-apk
sed -i -e 's/"cdylib","rlib"/"rlib"/g' Cargo.toml
- name: Build APK ARMv7
working-directory: android
run: |
rm -rf app/build
rm -rf app/src/main/jniLibs/*
./gradlew assembleRelease
mv app/build/outputs/apk/release/app-release.apk grim-${{ github.ref_name }}-android-armv7.apk
- name: Checksum APK ARMv7
working-directory: android
shell: pwsh
run: get-filehash -algorithm sha256 grim-${{ github.ref_name }}-android-armv7.apk | Format-List | Out-String | ForEach-Object { $_.Trim() } > grim-${{ github.ref_name }}-android-armv7-sha256sum.txt
- name: Build lib x86 1/2
continue-on-error: true
run: |
sed -i -e 's/"rlib"/"cdylib","rlib"/g' Cargo.toml
export CPPFLAGS="-DMDB_USE_ROBUST=0" && export CFLAGS="-DMDB_USE_ROBUST=0" && cargo ndk -t x86_64 build --profile release-apk
- name: Build lib x86 2/2
run: |
unset CPPFLAGS && unset CFLAGS && cargo ndk -t x86_64 -o android/app/src/main/jniLibs build --profile release-apk
sed -i -e 's/"cdylib","rlib"/"rlib"/g' Cargo.toml
- name: Build APK x86
working-directory: android
run: |
rm -rf app/build
rm -rf app/src/main/jniLibs/*
./gradlew assembleRelease
mv app/build/outputs/apk/release/app-release.apk grim-${{ github.ref_name }}-android-x86_64.apk
- name: Checksum APK x86
working-directory: android
shell: pwsh
run: get-filehash -algorithm sha256 grim-${{ github.ref_name }}-android-x86_64.apk | Format-List | Out-String | ForEach-Object { $_.Trim() } > grim-${{ github.ref_name }}-android-x86_64-sha256sum.txt
- name: Release
uses: softprops/action-gh-release@v1
with:
files: |
android/grim-${{ github.ref_name }}-android-armv8.apk
android/grim-${{ github.ref_name }}-android-armv8-sha256sum.txt
android/grim-${{ github.ref_name }}-android-armv7.apk
android/grim-${{ github.ref_name }}-android-armv7-sha256sum.txt
android/grim-${{ github.ref_name }}-android-x86_64.apk
android/grim-${{ github.ref_name }}-android-x86_64-sha256sum.txt
linux_release:
name: Linux Release
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Download appimagetools
run: |
wget https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage
chmod +x appimagetool-x86_64.AppImage
sudo apt install libfuse2
- name: Zig Setup
uses: goto-bus-stop/setup-zig@v2
with:
version: 0.12.1
- name: Install cargo-zigbuild
run: cargo install cargo-zigbuild
- name: Release x86
run: cargo zigbuild --release --target x86_64-unknown-linux-gnu
- name: Release ARM
run: |
rustup target add aarch64-unknown-linux-gnu
cargo zigbuild --release --target aarch64-unknown-linux-gnu
- name: AppImage x86
run: |
cp target/x86_64-unknown-linux-gnu/release/grim linux/Grim.AppDir/AppRun
./appimagetool-x86_64.AppImage linux/Grim.AppDir target/x86_64-unknown-linux-gnu/release/grim-${{ github.ref_name }}-linux-x86_64.AppImage
- name: Checksum AppImage x86
working-directory: target/x86_64-unknown-linux-gnu/release
shell: pwsh
run: get-filehash -algorithm sha256 grim-${{ github.ref_name }}-linux-x86_64.AppImage | Format-List | Out-String | ForEach-Object { $_.Trim() } > grim-${{ github.ref_name }}-linux-x86_64-appimage-sha256sum.txt
- name: AppImage ARM
run: |
cp target/aarch64-unknown-linux-gnu/release/grim linux/Grim.AppDir/AppRun
./appimagetool-x86_64.AppImage linux/Grim.AppDir target/aarch64-unknown-linux-gnu/release/grim-${{ github.ref_name }}-linux-arm.AppImage
- name: Checksum AppImage ARM
working-directory: target/aarch64-unknown-linux-gnu/release
shell: pwsh
run: get-filehash -algorithm sha256 grim-${{ github.ref_name }}-linux-arm.AppImage | Format-List | Out-String | ForEach-Object { $_.Trim() } > grim-${{ github.ref_name }}-linux-arm-appimage-sha256sum.txt
- name: Release
uses: softprops/action-gh-release@v1
with:
files: |
target/x86_64-unknown-linux-gnu/release/grim-${{ github.ref_name }}-linux-x86_64.AppImage
target/x86_64-unknown-linux-gnu/release/grim-${{ github.ref_name }}-linux-x86_64-appimage-sha256sum.txt
target/aarch64-unknown-linux-gnu/release/grim-${{ github.ref_name }}-linux-arm.AppImage
target/aarch64-unknown-linux-gnu/release/grim-${{ github.ref_name }}-linux-arm-appimage-sha256sum.txt
windows_release:
name: Windows Release
runs-on: windows-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Build release
run: cargo build --release
- name: Archive release
uses: vimtor/action-zip@v1
with:
files: target/release/grim.exe
dest: target/release/grim-${{ github.ref_name }}-win-x86_64.zip
- name: Checksum release
working-directory: target/release
shell: pwsh
run: get-filehash -algorithm sha256 grim-${{ github.ref_name }}-win-x86_64.zip | Format-List | Out-String | ForEach-Object { $_.Trim() } > grim-${{ github.ref_name }}-win-x86_64-sha256sum.txt
- name: Install cargo-wix
run: cargo install cargo-wix
- name: Run cargo-wix
run: cargo wix -p grim -o ./target/wix/grim-${{ github.ref_name }}-win-x86_64.msi --nocapture
- name: Checksum msi
working-directory: target/wix
shell: pwsh
run: get-filehash -algorithm sha256 grim-${{ github.ref_name }}-win-x86_64.msi | Format-List | Out-String | ForEach-Object { $_.Trim() } > grim-${{ github.ref_name }}-win-x86_64-msi-sha256sum.txt
- name: Release
uses: softprops/action-gh-release@v1
with:
files: |
target/release/grim-${{ github.ref_name }}-win-x86_64.zip
target/release/grim-${{ github.ref_name }}-win-x86_64-sha256sum.txt
target/wix/grim-${{ github.ref_name }}-win-x86_64.msi
target/wix/grim-${{ github.ref_name }}-win-x86_64-msi-sha256sum.txt
macos_release:
name: MacOS Release
runs-on: macos-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Zig Setup
uses: goto-bus-stop/setup-zig@v2
with:
version: 0.12.1
- name: Install cargo-zigbuild
run: cargo install cargo-zigbuild
- name: Release x86
run: |
rustup target add x86_64-apple-darwin
cargo zigbuild --release --target x86_64-apple-darwin
mkdir macos/Grim.app/Contents/MacOS
yes | cp -rf target/x86_64-apple-darwin/release/grim macos/Grim.app/Contents/MacOS
- name: Archive x86
run: |
cd macos
zip -r grim-${{ github.ref_name }}-macos-x86_64.zip Grim.app
mv grim-${{ github.ref_name }}-macos-x86_64.zip ../target/x86_64-apple-darwin/release
cd ..
- name: Checksum Release x86
working-directory: target/x86_64-apple-darwin/release
shell: pwsh
run: get-filehash -algorithm sha256 grim-${{ github.ref_name }}-macos-x86_64.zip | Format-List | Out-String | ForEach-Object { $_.Trim() } > grim-${{ github.ref_name }}-macos-x86_64-sha256sum.txt
- name: Release ARM
run: |
rustup target add aarch64-apple-darwin
cargo zigbuild --release --target aarch64-apple-darwin
yes | cp -rf target/aarch64-apple-darwin/release/grim macos/Grim.app/Contents/MacOS
- name: Archive ARM
run: |
cd macos
zip -r grim-${{ github.ref_name }}-macos-arm.zip Grim.app
mv grim-${{ github.ref_name }}-macos-arm.zip ../target/aarch64-apple-darwin/release
cd ..
- name: Checksum Release ARM
working-directory: target/aarch64-apple-darwin/release
shell: pwsh
run: get-filehash -algorithm sha256 grim-${{ github.ref_name }}-macos-arm.zip | Format-List | Out-String | ForEach-Object { $_.Trim() } > grim-${{ github.ref_name }}-macos-arm-sha256sum.txt
- name: Release Universal
run: |
rustup target add aarch64-apple-darwin
rustup target add x86_64-apple-darwin
cargo zigbuild --release --target universal2-apple-darwin
yes | cp -rf target/universal2-apple-darwin/release/grim macos/Grim.app/Contents/MacOS
- name: Archive Universal
run: |
cd macos
zip -r grim-${{ github.ref_name }}-macos-universal.zip Grim.app
mv grim-${{ github.ref_name }}-macos-universal.zip ../target/universal2-apple-darwin/release
cd ..
- name: Checksum Release Universal
working-directory: target/universal2-apple-darwin/release
shell: pwsh
run: get-filehash -algorithm sha256 grim-${{ github.ref_name }}-macos-universal.zip | Format-List | Out-String | ForEach-Object { $_.Trim() } > grim-${{ github.ref_name }}-macos-universal-sha256sum.txt
- name: Release
uses: softprops/action-gh-release@v1
with:
files: |
target/x86_64-apple-darwin/release/grim-${{ github.ref_name }}-macos-x86_64.zip
target/x86_64-apple-darwin/release/grim-${{ github.ref_name }}-macos-x86_64-sha256sum.txt
target/aarch64-apple-darwin/release/grim-${{ github.ref_name }}-macos-arm.zip
target/aarch64-apple-darwin/release/grim-${{ github.ref_name }}-macos-arm-sha256sum.txt
target/universal2-apple-darwin/release/grim-${{ github.ref_name }}-macos-universal.zip
target/universal2-apple-darwin/release/grim-${{ github.ref_name }}-macos-universal-sha256sum.txt

170
.github/workflows/release.yml.bak vendored Normal file
View file

@ -0,0 +1,170 @@
name: Release
on:
push:
tags:
- "v*.*.*"
jobs:
linux_release:
name: Linux Release
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Download appimagetools
run: |
wget https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage
chmod +x appimagetool-x86_64.AppImage
sudo apt install libfuse2
- name: Zig Setup
uses: goto-bus-stop/setup-zig@v2
with:
version: 0.12.1
- name: Install cargo-zigbuild
run: cargo install cargo-zigbuild
- name: Release x86
run: cargo zigbuild --release --target x86_64-unknown-linux-gnu
- name: Release ARM
run: |
rustup target add aarch64-unknown-linux-gnu
cargo zigbuild --release --target aarch64-unknown-linux-gnu
- name: AppImage x86
run: |
cp target/x86_64-unknown-linux-gnu/release/grim linux/Grim.AppDir/AppRun
./appimagetool-x86_64.AppImage linux/Grim.AppDir target/x86_64-unknown-linux-gnu/release/grim-${{ github.ref_name }}-linux-x86_64.AppImage
- name: Checksum AppImage x86
working-directory: target/x86_64-unknown-linux-gnu/release
shell: bash
run: sha256sum grim-${{ github.ref_name }}-linux-x86_64.AppImage > grim-${{ github.ref_name }}-linux-x86_64-appimage-sha256sum.txt
- name: AppImage ARM
run: |
cp target/aarch64-unknown-linux-gnu/release/grim linux/Grim.AppDir/AppRun
./appimagetool-x86_64.AppImage linux/Grim.AppDir target/aarch64-unknown-linux-gnu/release/grim-${{ github.ref_name }}-linux-arm.AppImage
- name: Checksum AppImage ARM
working-directory: target/aarch64-unknown-linux-gnu/release
shell: bash
run: sha256sum grim-${{ github.ref_name }}-linux-arm.AppImage > grim-${{ github.ref_name }}-linux-arm-appimage-sha256sum.txt
- name: Release
uses: softprops/action-gh-release@v1
with:
files: |
target/x86_64-unknown-linux-gnu/release/grim-${{ github.ref_name }}-linux-x86_64.AppImage
target/x86_64-unknown-linux-gnu/release/grim-${{ github.ref_name }}-linux-x86_64-appimage-sha256sum.txt
target/aarch64-unknown-linux-gnu/release/grim-${{ github.ref_name }}-linux-arm.AppImage
target/aarch64-unknown-linux-gnu/release/grim-${{ github.ref_name }}-linux-arm-appimage-sha256sum.txt
windows_release:
name: Windows Release
runs-on: windows-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Build release
run: cargo build --release
- name: Archive release
uses: vimtor/action-zip@v1
with:
files: target/release/grim.exe
dest: target/release/grim-${{ github.ref_name }}-win-x86_64.zip
- name: Checksum release
working-directory: target/release
shell: bash
run: sha256sum grim-${{ github.ref_name }}-win-x86_64.zip > grim-${{ github.ref_name }}-win-x86_64-sha256sum.txt
- name: Install cargo-wix
run: cargo install cargo-wix
- name: Run cargo-wix
run: cargo wix -p grim -o ./target/wix/grim-${{ github.ref_name }}-win-x86_64.msi --nocapture
- name: Checksum msi
working-directory: target/wix
shell: bash
run: sha256sum grim-${{ github.ref_name }}-win-x86_64.msi > grim-${{ github.ref_name }}-win-x86_64-msi-sha256sum.txt
- name: Release
uses: softprops/action-gh-release@v1
with:
files: |
target/release/grim-${{ github.ref_name }}-win-x86_64.zip
target/release/grim-${{ github.ref_name }}-win-x86_64-sha256sum.txt
target/wix/grim-${{ github.ref_name }}-win-x86_64.msi
target/wix/grim-${{ github.ref_name }}-win-x86_64-msi-sha256sum.txt
macos_release:
name: MacOS Release
runs-on: macos-12
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install coreutils
run: brew install coreutils
- name: Zig Setup
uses: goto-bus-stop/setup-zig@v2
with:
version: 0.12.1
- name: Install cargo-zigbuild
run: cargo install cargo-zigbuild
- name: Download SDK
run: wget https://github.com/phracker/MacOSX-SDKs/releases/download/11.3/MacOSX11.0.sdk.tar.xz
- name: Setup SDK env
run: tar xf ${{ github.workspace }}/MacOSX11.0.sdk.tar.xz && echo "SDKROOT=${{ github.workspace }}/MacOSX11.0.sdk" >> $GITHUB_ENV
- name: Setup platform env
run: echo "MACOSX_DEPLOYMENT_TARGET=11.0" >> $GITHUB_ENV
- name: Release x86
run: |
rustup target add x86_64-apple-darwin
cargo zigbuild --release --target x86_64-apple-darwin
yes | cp -rf target/x86_64-apple-darwin/release/grim macos/Grim.app/Contents/MacOS
- name: Archive x86
run: |
cd macos
zip -r grim-${{ github.ref_name }}-macos-x86_64.zip Grim.app
mv grim-${{ github.ref_name }}-macos-x86_64.zip ../target/x86_64-apple-darwin/release
cd ..
- name: Checksum Release x86
working-directory: target/x86_64-apple-darwin/release
shell: bash
run: sha256sum grim-${{ github.ref_name }}-macos-x86_64.zip > grim-${{ github.ref_name }}-macos-x86_64-sha256sum.txt
- name: Release ARM
run: |
rustup target add aarch64-apple-darwin
cargo zigbuild --release --target aarch64-apple-darwin
yes | cp -rf target/aarch64-apple-darwin/release/grim macos/Grim.app/Contents/MacOS
- name: Archive ARM
run: |
cd macos
zip -r grim-${{ github.ref_name }}-macos-arm.zip Grim.app
mv grim-${{ github.ref_name }}-macos-arm.zip ../target/aarch64-apple-darwin/release
cd ..
- name: Checksum Release ARM
working-directory: target/aarch64-apple-darwin/release
shell: bash
run: sha256sum grim-${{ github.ref_name }}-macos-arm.zip > grim-${{ github.ref_name }}-macos-arm-sha256sum.txt
- name: Release Universal
run: |
cargo zigbuild --release --target universal2-apple-darwin
yes | cp -rf target/universal2-apple-darwin/release/grim macos/Grim.app/Contents/MacOS
- name: Archive Universal
run: |
cd macos
zip -r grim-${{ github.ref_name }}-macos-universal.zip Grim.app
mv grim-${{ github.ref_name }}-macos-universal.zip ../target/universal2-apple-darwin/release
cd ..
- name: Checksum Release Universal
working-directory: target/universal2-apple-darwin/release
shell: pwsh
run: sha256sum grim-${{ github.ref_name }}-macos-universal.zip > grim-${{ github.ref_name }}-macos-universal-sha256sum.txt
- name: Release
uses: softprops/action-gh-release@v1
with:
files: |
target/x86_64-apple-darwin/release/grim-${{ github.ref_name }}-macos-x86_64.zip
target/x86_64-apple-darwin/release/grim-${{ github.ref_name }}-macos-x86_64-sha256sum.txt
target/aarch64-apple-darwin/release/grim-${{ github.ref_name }}-macos-arm.zip
target/aarch64-apple-darwin/release/grim-${{ github.ref_name }}-macos-arm-sha256sum.txt
target/universal2-apple-darwin/release/grim-${{ github.ref_name }}-macos-universal.zip
target/universal2-apple-darwin/release/grim-${{ github.ref_name }}-macos-universal-sha256sum.txt

7
.gitignore vendored
View file

@ -1,9 +1,12 @@
*.iml
android/.idea
android/.gradle
android/local.properties
android/keystore
android/keystore.asc
android/keystore.properties
android/*.apk
android/*sha256sum.txt
/.idea
.DS_Store
/captures
@ -13,7 +16,7 @@ android/keystore.properties
target
.cargo/
app/src/main/jniLibs
macos/Grim.app/Contents/MacOS/grim
macos/cert.pem
linux/Grim.AppDir/AppRun
.intentionally-empty-file.o
.intentionally-empty-file.o
Cargo.toml-e

5177
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,10 +1,10 @@
[package]
name = "grim"
version = "0.1.0"
authors = ["Ardocrat <ardocrat@proton.me>"]
version = "0.2.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://github.com/ardocrat/grim"
repository = "https://gri.mw/code/GUI/grim"
keywords = [ "crypto", "grin", "mimblewimble" ]
edition = "2021"
@ -16,9 +16,6 @@ path = "src/main.rs"
name="grim"
crate-type = ["rlib"]
[profile.release]
debug = 1
[profile.release-apk]
inherits = "release"
strip = true
@ -28,108 +25,108 @@ codegen-units = 1
panic = "abort"
[dependencies]
log = "0.4"
log = "0.4.27"
## node
openssl-sys = { version = "0.9.82", features = ["vendored"] }
grin_api = "5.3.1"
grin_chain = "5.3.1"
grin_config = "5.3.1"
grin_core = "5.3.1"
grin_p2p = "5.3.1"
grin_servers = "5.3.1"
grin_keychain = "5.3.1"
grin_util = "5.3.1"
grin_api = "5.3.3"
grin_chain = "5.3.3"
grin_config = "5.3.3"
grin_core = "5.3.3"
grin_p2p = "5.3.3"
grin_servers = "5.3.3"
grin_keychain = "5.3.3"
grin_util = "5.3.3"
## wallet
grin_wallet_impls = "5.3.1"
grin_wallet_api = "5.3.1"
grin_wallet_libwallet = "5.3.1"
grin_wallet_util = "5.3.1"
grin_wallet_controller = "5.3.1"
grin_wallet_impls = "5.3.3"
grin_wallet_api = "5.3.3"
grin_wallet_libwallet = "5.3.3"
grin_wallet_util = "5.3.3"
grin_wallet_controller = "5.3.3"
## ui
egui = { version = "0.28.1", default-features = false }
egui_extras = { version = "0.28.1", features = ["image", "svg"] }
egui = { version = "0.29.1", default-features = false }
egui_extras = { version = "0.29.1", features = ["image", "svg"] }
rust-i18n = "2.3.1"
## other
backtrace = "0.3"
panic-message = "0.3.0"
thiserror = "1.0.58"
futures = "0.3"
dirs = "5.0.1"
sys-locale = "0.3.0"
chrono = "0.4.31"
parking_lot = "0.12.1"
lazy_static = "1.4.0"
toml = "0.8.2"
serde = "1.0.170"
local-ip-address = "0.6.1"
url = "2.4.0"
rand = "0.8.5"
serde_derive = "1.0.197"
serde_json = "1.0.115"
tokio = { version = "1.37.0", features = ["full"] }
image = "0.25.1"
rqrr = "0.7.1"
anyhow = "1.0.97"
pin-project = "1.1.10"
backtrace = "0.3.74"
thiserror = "1.0.64"
futures = "0.3.31"
dirs = "6.0.0"
sys-locale = "0.3.1"
chrono = "0.4.38"
parking_lot = "0.12.3"
lazy_static = "1.5.0"
toml = "0.8.19"
serde = "1.0.210"
local-ip-address = "0.6.3"
url = "2.5.2"
rand = "0.9.0"
serde_derive = "1.0.219"
serde_json = "1.0.140"
tokio = { version = "1.44.1", features = ["full"] }
image = "0.25.6"
rqrr = "0.8.0"
qrcodegen = "1.8.0"
qrcode = "0.14.0"
qrcode = "0.14.1"
ur = "0.4.1"
gif = "0.13.1"
rkv = { version = "0.19.0", features = ["lmdb"] }
## tor
arti-client = { version = "0.19.0", features = ["pt-client", "static", "onion-service-service", "onion-service-client"] }
tor-rtcompat = { version = "0.19.0", features = ["static"] }
tor-config = "0.19.0"
fs-mistrust = "0.7.9"
tor-hsservice = "0.19.0"
tor-hsrproxy = "0.19.0"
tor-keymgr = "0.19.0"
tor-llcrypto = "0.19.0"
tor-hscrypto = "0.19.0"
arti-hyper = "0.19.0"
sha2 = "0.10.0"
arti-client = { version = "0.29.0", features = ["pt-client", "static", "onion-service-service", "onion-service-client"] }
tor-rtcompat = { version = "0.29.0", features = ["static"] }
tor-config = "0.29.0"
fs-mistrust = "0.9.1"
tor-hsservice = "0.29.0"
tor-hsrproxy = "0.29.0"
tor-keymgr = "0.29.0"
tor-llcrypto = "0.29.0"
tor-hscrypto = "0.29.0"
tor-error = "0.29.0"
sha2 = "0.10.8"
ed25519-dalek = "2.1.1"
curve25519-dalek = "4.1.2"
hyper = { version = "0.14.28", features = ["full"] }
curve25519-dalek = "4.1.3"
hyper = { version = "0.14.31", features = ["full"] }
hyper-tls = "0.5.0"
tls-api = "0.9.0"
tls-api-native-tls = "0.9.0"
tls-api = "0.12.0"
tls-api-native-tls = "0.12.1"
## stratum server
tokio-old = {version = "0.2", features = ["full"], package = "tokio" }
tokio-util-old = { version = "0.2", features = ["codec"], package = "tokio-util" }
[target.'cfg(all(not(target_os = "windows"), not(target_os = "android")))'.dependencies]
eye = { version = "0.5.0", default-features = false }
[target.'cfg(target_os = "linux")'.dependencies]
nokhwa = { version = "0.10.5", default-features = false, features = ["input-v4l"] }
[target.'cfg(target_os = "windows")'.dependencies]
nokhwa = { version = "0.10.4", default-features = false, features = ["input-msmf"] }
nokhwa = { version = "0.10.5", default-features = false, features = ["input-msmf"] }
[target.'cfg(target_os = "macos")'.dependencies]
tls-api-openssl = "0.9.0"
openpnp_capture_sys = "0.4.0"
nokhwa-mac = { git = "https://github.com/l1npengtul/nokhwa", rev = "612c861ef153cf0ee575d8dd1413b960e4e19dd6", features = ["input-avfoundation", "output-threaded"], package = "nokhwa" }
[target.'cfg(not(target_os = "android"))'.dependencies]
env_logger = "0.11.3"
winit = { version = "0.29.15" }
eframe = { version = "0.28.1", features = ["wgpu", "glow"] }
winit = { version = "0.30.5" }
eframe = { version = "0.29.1", features = ["wgpu", "glow"] }
arboard = "3.2.0"
rfd = "0.14.1"
dark-light = "1.1.1"
rfd = "0.15.0"
interprocess = { version = "2.2.1", features = ["tokio"] }
[target.'cfg(target_os = "android")'.dependencies]
android_logger = "0.13.1"
android_logger = "0.14.1"
jni = "0.21.1"
wgpu = "22.1.0"
android-activity = { version = "0.6.0", features = ["game-activity"] }
wgpu = "0.20.1"
winit = { version = "0.29.15", features = ["android-game-activity"] }
eframe = { version = "0.28.1", features = ["wgpu", "android-game-activity"] }
winit = { version = "0.30.5", features = ["android-game-activity"] }
eframe = { version = "0.29.1", features = ["wgpu", "android-game-activity"] }
[patch.crates-io]
egui_extras = { git = "https://github.com/emilk/egui", rev = "5b846b4554fe47269affb43efef2cad8710a8a47" }
egui = { git = "https://github.com/emilk/egui", rev = "5b846b4554fe47269affb43efef2cad8710a8a47" }
eframe = { git = "https://github.com/emilk/egui", rev = "5b846b4554fe47269affb43efef2cad8710a8a47" }
### patch grin store
#grin_store = { path = "../grin-store" }
### fix cross-compilation support for macos
openpnp_capture_sys = { git = "https://github.com/ardocrat/openpnp-capture-rs", branch = "cross_compilation_support" }

View file

@ -1,11 +1,11 @@
# <img height="22" src="https://github.com/ardocrat/grim/blob/master/android/app/src/main/ic_launcher-playstore.png?raw=true"> Grim <img height="20" src="https://github.com/mimblewimble/site/blob/master/assets/images/grin-logo.png?raw=true"> <img height="20" src="https://github.com/ardocrat/grim/blob/master/img/logo.png?raw=true">
# Grim <img height="20" src="https://gri.mw/code/GUI/grim/raw/branch/master/img/grin-logo.png"/> <img height="20" src="https://gri.mw/code/GUI/grim/raw/branch/master/img/logo.png"/>
Cross-platform GUI for [GRiN ツ](https://grin.mw) in [Rust](https://www.rust-lang.org/)
for maximum compatibility with original [Mimblewimble](https://github.com/mimblewimble/grin) implementation.
Initially supported platforms are Linux, Mac, Windows, limited Android and possible web support with help of [egui](https://github.com/emilk/egui) - immediate mode GUI library in pure Rust.
Named by the character [Grim](http://harrypotter.wikia.com/wiki/Grim) - the shape of a large, black, menacing, spectral giant dog.
![image](https://github.com/user-attachments/assets/a925b1c8-02c9-4b08-b888-0315d11138b6)
![image](https://gri.mw/code/GUI/grim/raw/branch/master/img/cover.png)
## Build instructions

View file

@ -2,10 +2,6 @@ plugins {
id 'com.android.application'
}
def keystorePropertiesFile = rootProject.file("keystore.properties")
def keystoreProperties = new Properties()
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
android {
compileSdk 33
ndkVersion '26.0.10792818'
@ -14,24 +10,36 @@ android {
applicationId "mw.gri.android"
minSdk 24
targetSdk 33
versionCode 1
versionName "0.1.0"
versionCode 3
versionName "0.2.4"
}
signingConfigs {
release {
keyAlias keystoreProperties['keyAlias']
keyPassword keystoreProperties['keyPassword']
storeFile file(keystoreProperties['storeFile'])
storePassword keystoreProperties['storePassword']
def keystorePropertiesFile = rootProject.file("keystore.properties")
def keystoreProperties = new Properties()
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
signingConfigs {
release {
keyAlias keystoreProperties['keyAlias']
keyPassword keystoreProperties['keyPassword']
storeFile file(keystoreProperties['storeFile'])
storePassword keystoreProperties['storePassword']
}
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.release
}
if (keystorePropertiesFile.exists()) {
signedRelease {
initWith release
signingConfig signingConfigs.release
}
}
debug {
minifyEnabled false

View file

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

View file

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

View file

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

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<paths>
<external-cache-path name="images" path="images/" />
<external-cache-path name="share" path="share/" />
</paths>

View file

@ -1,10 +1,6 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
id 'com.android.application' version '8.1.1' apply false
id 'com.android.library' version '8.1.1' apply false
}
task clean(type: Delete) {
delete rootProject.buildDir
id 'com.android.application' version '8.6.1' apply false
id 'com.android.library' version '8.6.1' apply false
}

View file

@ -19,5 +19,4 @@ android.useAndroidX=true
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true
android.defaults.buildfeatures.buildconfig=true
android.nonFinalResIds=false

View file

@ -1,6 +1,6 @@
#Mon May 02 15:39:12 BST 2022
distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
distributionPath=wrapper/dists
zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME

BIN
img/cover.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 KiB

BIN
img/grin-logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

View file

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

View file

@ -4,7 +4,7 @@ case $2 in
x86_64|arm)
;;
*)
echo "Usage: release_linux.sh [version] [platform]\n - platform: 'x86_64', 'arm'" >&2
echo "Usage: release_linux.sh [platform] [version]\n - platform: 'x86_64', 'arm'" >&2
exit 1
esac
@ -17,11 +17,11 @@ cd ..
[[ $2 == "x86_64" ]] && arch+=(x86_64-unknown-linux-gnu)
[[ $2 == "arm" ]] && arch+=(aarch64-unknown-linux-gnu)
# Start release build with zig linker for cross-compilation
rustup target add ${arch}
cargo install cargo-zigbuild
cargo zigbuild --release --target ${arch}
# Create AppImage with https://github.com/AppImage/appimagetool
cp target/${arch}/release/grim linux/Grim.AppDir/AppRun
rm target/${arch}/release/*.AppImage
appimagetool linux/Grim.AppDir target/${arch}/release/grim-v$1-linux-$2.AppImage
appimagetool linux/Grim.AppDir target/${arch}/release/grim-v$2-linux-$1.AppImage

View file

@ -28,6 +28,7 @@ light: Hell
choose_file: Datei auswählen
crash_report: Absturzbericht
crash_report_warning: Anwendung wurde beim letzten Mal unerwartet geschlossen, Sie können den Absturzbericht mit Entwicklern teilen.
confirmation: Bestätigung
wallets:
await_conf_amount: Erwarte Bestätigung
await_fin_amount: Warten auf die Fertigstellung
@ -287,7 +288,6 @@ network_settings:
modal:
cancel: Abbrechen
save: Speichern
confirmation: Bestätigung
add: Hinzufügen
modal_exit:
description: Sind Sie sicher, dass Sie die Anwendung beenden wollen?

View file

@ -28,6 +28,7 @@ light: Light
choose_file: Choose file
crash_report: Crash report
crash_report_warning: Application closed unexpectedly last time, you can share crash report with developers.
confirmation: Confirmation
wallets:
await_conf_amount: Awaiting confirmation
await_fin_amount: Awaiting finalization
@ -287,7 +288,6 @@ network_settings:
modal:
cancel: Cancel
save: Save
confirmation: Confirmation
add: Add
modal_exit:
description: Are you sure you want to quit the application?

View file

@ -28,6 +28,7 @@ light: Clair
choose_file: Choisir un fichier
crash_report: Rapport d'échec
crash_report_warning: L'application s'est fermée de manière inattendue la dernière fois, vous pouvez partager un rapport d'incident avec les développeurs.
confirmation: Confirmation
wallets:
await_conf_amount: En attente de confirmation
await_fin_amount: En attente de finalisation
@ -287,7 +288,6 @@ network_settings:
modal:
cancel: Annuler
save: Sauvegarder
confirmation: Confirmation
add: Ajouter
modal_exit:
description: "Êtes-vous sûr de vouloir quitter l'application ?"

View file

@ -28,6 +28,7 @@ light: Светлая
choose_file: Выбрать файл
crash_report: Отчёт о сбое
crash_report_warning: В прошлый раз приложение неожиданно закрылось, вы можете поделиться отчетом о сбое с разработчиками.
confirmation: Подтверждение
wallets:
await_conf_amount: Ожидает подтверждения
await_fin_amount: Ожидает завершения
@ -287,7 +288,6 @@ network_settings:
modal:
cancel: Отмена
save: Сохранить
confirmation: Подтверждение
add: Добавить
modal_exit:
description: Вы уверены, что хотите выйти из приложения?

View file

@ -28,6 +28,7 @@ light: Isik
choose_file: Dosya seçin
crash_report: Ariza Raporu
crash_report_warning: Uygulama beklenmedik bir sekilde kapandi son kez, kilitlenme raporunu gelistiricilerle paylasabilirsiniz.
confirmation: Onay
wallets:
await_conf_amount: Onay bekleniyor
await_fin_amount: Tamamlanma bekleniyor
@ -287,7 +288,6 @@ network_settings:
modal:
cancel: Iptal
save: Kaydet
confirmation: Onay
add: Ekle
modal_exit:
description: Uygulamadan cikmak için exit, emin misiniz?

294
locales/zh-CN.yml Normal file
View file

@ -0,0 +1,294 @@
lang_name: 英语
copy: 复制
paste: 粘贴
continue: 继续
complete: 完成
error: 错误
retry: 重试
close: 关闭
change: 更改
show: 显示
delete: 删除
clear: 清楚
create: 创建
id: 标识
kernel: 核心
settings: 设置
language: 语言
scan: 扫描
qr_code: 二维码
scan_qr: 扫描二维码
repeat: 重复
scan_result: 扫描结果
back: 返回
share: 分享
theme: '主题:'
dark: 深色
light: 淡色
choose_file: 选择文件
crash_report: 崩溃报告
crash_report_warning: 上次应用程序意外关闭,您可以报告开发人员崩溃事件.
confirmation: 确认
wallets:
await_conf_amount: 等待确认中
await_fin_amount: 等待确定中
locked_amount: 锁定帐户
txs_empty: '手动接收资金或通过传输接收资金 %{message} or %{transport} 更改钱包设置, 请按屏幕底部的按钮 %{settings} 按钮.'
title: 钱包
create_desc: 创建或种子单词导入已有钱包.
add: 添加钱包
name: '用户名:'
pass: '密码:'
pass_empty: 输入钱包的密码
current_pass: '目前密码:'
new_pass: '新密码:'
min_tx_conf_count: '确认交易的最低数量:'
recover: 恢复
recovery_phrase: 助记词
words_count: '字数:'
enter_word: '输入单词 #%{number}:'
not_valid_word: 输入的单词无效
not_valid_phrase: 输入的助记词无效
create_phrase_desc: 已安全地写下并保存助记词.
restore_phrase_desc: 从已保存的助记词中输入.
setup_conn_desc: 选择钱包连接到网络的方式.
conn_method: 连接方式
ext_conn: '外部连接:'
add_node: 添加节点
node_url: '节点网址:'
node_secret: 'API 密钥 (可选):'
invalid_url: 输入的网址无效
open: 打开钱包
wrong_pass: 输入的密码错误
locked: 已锁定
unlocked: 解锁
enable_node: '通过选择屏幕底部的按钮 %{settings} 启用集成节点以使用钱包或更改连接设置.'
node_loading: '集成节点同步后钱包会加载,你可选择屏幕底部的按钮 %{settings} 更改连接.'
loading: 正在加载
closing: 正在关闭
checking: 检查中
default_wallet: 默认钱包
new_account_desc: '输入新帐户的名称:'
wallet_loading: 加载钱包
wallet_closing: 关闭钱包
wallet_checking: 检查钱包
tx_loading: 加载事务
default_account: 默认账户
accounts: 账户
tx_sent: 已发送
tx_received: 已接收
tx_sending: 发送中
tx_receiving: 接收中
tx_confirming: 等待确认
tx_canceled: 已取消
tx_cancelling: 取消
tx_finalizing: 完成
tx_confirmed: 已确认
txs: 所有交易
tx: 交易
messages: 消息
transport: 传输
input_slatepack_desc: '输入收到的 Slatepack 消息创建响应或完成的请求:'
parse_slatepack_err: '读取消息时出错,请检查输入:'
pay_balance_error: '账户余额不足以支付 %{amount} ツ 和网络费用.'
parse_i1_slatepack_desc: '要支付 %{amount} ツ 请将此消息发送给接收者:'
parse_i2_slatepack_desc: '完成交易以接收 %{amount} ツ:'
parse_i3_slatepack_desc: '发布交易以完成 %{amount} ツ的接收 ツ:'
parse_s1_slatepack_desc: '要接收 %{amount} ツ 请将此消息发送给发件人:'
parse_s2_slatepack_desc: '完成交易以发送 %{amount} ツ:'
parse_s3_slatepack_desc: '发布交易以完成 %{amount} ツ的发送:'
resp_slatepack_err: '创建响应时出错,请检查输入数据或重试:'
resp_exists_err: 此交易已存在.
resp_canceled_err: 此交易已被取消.
create_request_desc: '创建发送或接收资金的请求:'
send_request_desc: '您已创建发送请求 %{amount} ツ. 将此消息发送给接收者:'
send_slatepack_err: 创建发送资金请求时出错,请检查输入数据或重试.
invoice_desc: '您已创建接收请求 %{amount} ツ. 将此消息发送给发送者:'
invoice_slatepack_err: 发票开具时出错,请检查输入数据或重试.
finalize_slatepack_err: '完结时出错,请检查输入数据或重试:'
finalize: 完成
use_dandelion: 使用蒲公英
enter_amount_send: '你有 %{amount} ツ. 输入要发送的金额:'
enter_amount_receive: '输入要接收的金额:'
recovery: 恢复
repair_wallet: 修复钱包
repair_desc: 检查钱包,必要时修复和恢复丢失的输出. 此操作需要时间.
repair_unavailable: 您需要与节点建立有效连接并完成钱包同步.
delete: 删除钱包
delete_conf: 您确定要删除钱包吗?
delete_desc: 确保您已保存恢复助记语,以便日后使用资金。.
wallet_loading_err: '同步钱包时出错,你可以通过选择屏幕底部的按钮 %{settings} 来重试或更改连接设置.'
wallet: 钱包
send: 发送
receive: 接收
settings: 钱包设置
tx_send_cancel_conf: '您确定要取消 %{amount} ツ的发送吗?'
tx_receive_cancel_conf: '您确定要取消 %{amount} ツ的接收吗?'
rec_phrase_not_found: 找不到恢复助记词.
restore_wallet_desc: 如果常规修复没有帮助,通过删除所有文件来恢复钱包.您将需要重新打开您的钱包.
transport:
desc: '使用传输同步接收或发送消息:'
tor_network: Tor 网络
connected: 已连接
connecting: 正在连接
disconnecting: 断开连接
conn_error: 连接错误
disconnected: 已断开连接
receiver_address: '接收者的地址:'
incorrect_addr_err: '输入的地址不正确:'
tor_send_error: 通过 Tor 发送时出错,请确保接收方在线, 交易已取消.
tor_autorun_desc: 是否在开钱包时启动 Tor 服务以同步接收交易.
tor_sending: '通过 Tor 发送%{amount} ツ'
tor_settings: Tor 设置
bridges: 桥梁
bridges_desc: 如果常规连接不正常,设置网桥,可以绕过 Tor 网络审查.
bin_file: '二进制文件:'
conn_line: '连接线:'
bridges_disabled: 网桥已禁用
bridge_name: '网桥%{b}'
network:
self: 网络
type: '网络类型:'
mainnet: 主网
testnet: 测试网
connections: 连接
node: 集成节点
metrics: 指标
mining: 挖矿
settings: 节点设置
enable_node: 启用节点
autorun: 自动运行
disabled_server: '按屏幕左上角的按钮 %{dots}启用集成节点或添加其他连接方法.'
no_ips: T您的系统上没有可用的 IP 地址,服务器无法启动,请检查您的网络连接.
available: 可用
not_available: 不可用
availability_check: 检查是否可用
android_warning: Android 用户注意 .要成功同步集成节点,您必须在手机的系统设置中允许访问通知并取消 Grim 应用程序的电池使用限制.这是在后台正确运行应用程序的必要操作.
sync_status:
node_restarting: 节点正在重新启动
node_down: 节点已关闭
initial: 节点正在启动
no_sync: 节点正在运行
awaiting_peers: 等待网络对点
header_sync: 正下载标题
header_sync_percent: '正在下载标题: %{percent}%'
tx_hashset_pibd: 下载状态 (PIBD)
tx_hashset_pibd_percent: '下载状态 (PIBD): %{percent}%'
tx_hashset_download: 正在下载状态
tx_hashset_download_percent: '下载状态: %{percent}%'
tx_hashset_setup_history: '正在准备状态(历史记录): %{percent}%'
tx_hashset_setup_position: '正在准备状态(位置): %{percent}%'
tx_hashset_setup: 正在准备状态
tx_hashset_range_proofs_validation: '验证状态(范围证明): %{percent}%'
tx_hashset_kernels_validation: '正在验证状态(核心): %{percent}%'
tx_hashset_save: 最终确定链状态
body_sync: 下载区块
body_sync_percent: '下载区块中: %{percent}%'
shutdown: 节点正在关闭
network_node:
header: 标题
block: 区块
hash: 哈希值
height: 高度
difficulty: 难度
time: 时间
main_pool: 主池
stem_pool: stem池
data: 数据
size: 大小 (GB)
peers: 网络对点
error_clean: 点数据已损坏,需要重新同步.
resync: 重新同步
error_p2p_api: '%{p2p_api} 服务器初始化时出错,请选择屏幕底部的按钮 %{p2p_api} 来检查 %{settings}设置.'
error_config: '配置初始化时出错,请选择屏幕底部的按钮 %{settings} 检查设置.'
error_unknown: '初始化时出错,请选择屏幕底部的按钮 %{settings} 来检查集成节点设置,或者重新同步.'
network_metrics:
loading: 指标在同步后将可用
emission: 发射
inflation: 通货膨胀
supply: 供应
block_time: Block time
reward: 奖励
difficulty_window: '难度窗口 %{size}'
network_mining:
loading: 同步后即可挖矿
info: '挖矿服务器已启用,您可以通过选择屏幕底部的按钮 %{settings} 来更改其设置。连接设备后,数据会更新.'
restart_server_required: 需要重启服务器才能应用更改.
rewards_wallet: 奖励钱包
server: 阶层服务器
address: 地址
miners: 矿工
devices: 设备
blocks_found: 找到的区块
hashrate: '哈希率 (C%{bits})'
connected: 已连接
disconnected: 已断开连接
network_settings:
change_value: 更改值
stratum_ip: '层 IP 地址:'
stratum_port: '层端口:'
port_unavailable: 指定的端口不可用
restart_node_required: 需要重启节点才能应用更改.
choose_wallet: 选择钱包
stratum_wallet_warning: 必须打开钱包才能获得奖励.
enable: 启用
disable: 禁用
restart: 重新启动
server: 服务器
api_ip: 'API IP 地址:'
api_port: 'API 端口:'
api_secret: '其它API 和 V2 所有者 API 令牌:'
foreign_api_secret: '外部 API 令牌:'
disabled: 已禁用
enabled: 已启用
ftl: '未来时间限制 (FTL):'
ftl_description: 限制未来多长时间, 相对于节点的本地时间,以秒为单位, 新区块的时间戳可以被接受.
not_valid_value: 输入的值无效
full_validation: 完全验证
full_validation_description: 在处理每个区块时是否运行全链验证(同步期间除外).
archive_mode: 存档模式
archive_mode_desc: 以全部存档模式运行全节点(同步需要更多的磁盘空间和时间).
attempt_time: '尝试挖矿时间 (秒):'
attempt_time_desc: 在停止并从池中重新收集交易之前尝试对特定标题进行挖矿的时间
min_share_diff: '可接受的最低份额难度:'
reset_settings_desc: 将节点设置重置为默认值
reset_settings: 重置设置
reset: 重置
tx_pool: 交易池
pool_fee: '接受到矿池的基本费用:'
reorg_period: '重组缓存保留期(以分钟为单位):'
max_tx_pool: '池中的最大交易数:'
max_tx_stempool: 'stem池中的最大交易数:'
max_tx_weight: '可以选择构建区块交易的最大总权重:'
epoch_duration: '纪元持续时间(以秒为单位):'
embargo_timer: '禁止计时器(以秒为单位):'
aggregation_period: '聚合周期(以秒为单位):'
stem_probability: 'stem助记词概率:'
stem_txs: stem交易
p2p_server: P2P 服务器
p2p_port: 'P2P 端口:'
add_seed: 添加 DNS 种子
seed_address: 'DNS 种子地址:'
add_peer: 添加网络对点
peer_address: '网络对点地址:'
peer_address_error: '以正确的格式输入 IP 地址或 DNS 名称确保指定的主机可用例如192.168.0.11234 或 example.com:5678'
default: 默认
allow_list: 允许列表
allow_list_desc: 仅连接到此列表中的网络对点.
deny_list: 拒绝列表
deny_list_desc: 切勿连接到此列表中的网络对点.
favourites: 收藏夹
favourites_desc: 要连接的首选网络对点列表.
ban_window: '被封禁的网络对点应该保持被封禁多长时间(以秒为单位):'
ban_window_desc: 禁止的决定是由节点 根据从网络对点收到的数据的正确性做出的.
max_inbound_count: '入站网络对点连接的最大数量:'
max_outbound_count: '最大出站网络对点连接数:'
reset_peers_desc: 重置网络对点数据。仅当查找网络对点出现问题时,才请谨慎使用它.
reset_peers: 重置网络对点
modal:
cancel: 取消
save: 保存
add: 添加
modal_exit:
description: 您确定要退出应用程序吗?
exit: 退出

View file

@ -1,49 +1,65 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleDisplayName</key>
<string>Grim</string>
<key>CFBundleExecutable</key>
<string>grim</string>
<key>CFBundleIconFile</key>
<string>AppIcon</string>
<key>CFBundleIconName</key>
<string>AppIcon</string>
<key>CFBundleIdentifier</key>
<string>mw.gri.macos</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>Grim</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>0.1.0</string>
<key>CFBundleSupportedPlatforms</key>
<array>
<string>MacOSX</string>
</array>
<key>CFBundleVersion</key>
<string>1</string>
<key>LSApplicationCategoryType</key>
<string>public.app-category.finance</string>
<key>NSHumanReadableCopyright</key>
<string>2024</string>
</dict>
</plist>
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleDisplayName</key>
<string>Grim</string>
<key>CFBundleExecutable</key>
<string>grim</string>
<key>CFBundleIconFile</key>
<string>AppIcon</string>
<key>CFBundleIconName</key>
<string>AppIcon</string>
<key>CFBundleIdentifier</key>
<string>mw.gri.macos</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>Grim</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>0.2.3</string>
<key>CFBundleSupportedPlatforms</key>
<array>
<string>MacOSX</string>
</array>
<key>CFBundleVersion</key>
<string>1</string>
<key>NSCameraUsageDescription</key>
<string>Grim needs an access to your camera to scan QR code.</string>
<key>CFBundleDocumentTypes</key>
<array>
<dict>
<key>CFBundleTypeName</key>
<string>Apple SimpleText document</string>
<key>CFBundleTypeRole</key>
<string>Viewer</string>
<key>LSItemContentTypes</key>
<array>
<string>com.apple.traditional-mac-plain-text</string>
</array>
<key>NSDocumentClass</key>
<string>Document</string>
</dict>
<dict>
<key>CFBundleTypeName</key>
<string>Unknown document</string>
<key>CFBundleTypeRole</key>
<string>Viewer</string>
<key>LSItemContentTypes</key>
<array>
<string>public.data</string>
</array>
<key>NSDocumentClass</key>
<string>Document</string>
</dict>
</array>
<key>LSApplicationCategoryType</key>
<string>public.app-category.finance</string>
<key>NSHumanReadableCopyright</key>
<string>2024</string>
</dict>
</plist>

View file

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

View file

@ -1,22 +1,21 @@
#!/bin/bash
set -e
case $2 in
case $1 in
x86_64|arm|universal)
;;
*)
echo "Usage: release_macos.sh [version] [platform]\n - platform: 'x86_64', 'arm', 'universal'" >&2
echo "Usage: release_macos.sh [platform] [version]\n - platform: 'x86_64', 'arm', 'universal'" >&2
exit 1
esac
if [[ ! -v SDKROOT ]]; then
if [[ "$OSTYPE" != "darwin"* ]]; then
if [ -z ${SDKROOT+x} ]; then
echo "MacOS SDKROOT is not set"
exit 1
elif [[ -z "SDKROOT" ]]; then
echo "MacOS SDKROOT is set to the empty string"
exit 1
else
else
echo "Use MacOS SDK: ${SDKROOT}"
fi
fi
# Setup build directory
@ -25,31 +24,25 @@ cd ${BASEDIR}
cd ..
# Setup platform
[[ $1 == "x86_64" ]] && arch+=(x86_64-apple-darwin)
[[ $1 == "arm" ]] && arch+=(aarch64-apple-darwin)
rustup target add x86_64-apple-darwin
rustup target add aarch64-apple-darwin
rm -rf target/x86_64-apple-darwin
rm -rf target/aarch64-apple-darwin
[[ $2 == "x86_64" ]] && arch+=(x86_64-apple-darwin)
[[ $2 == "arm" ]] && arch+=(aarch64-apple-darwin)
[[ $2 == "universal" ]] && arch+=(universal2-apple-darwin)
# Start release build with zig linker for cross-compilation
# zig 0.12+ required
[[ $1 == "universal" ]]; arch+=(universal2-apple-darwin)
cargo install cargo-zigbuild
cargo zigbuild --release --target ${arch}
rm -rf .intentionally-empty-file.o
mkdir macos/Grim.app/Contents/MacOS
rm -f .intentionally-empty-file.o
yes | cp -rf target/${arch}/release/grim macos/Grim.app/Contents/MacOS
### Sign .app resources on change:
# Sign .app resources on change:
#rcodesign generate-self-signed-certificate
#rcodesign sign --pem-file cert.pem macos/Grim.app
# Create release package
FILE_NAME=grim-v$1-macos-$2.zip
rm -rf target/${arch}/release/${FILE_NAME}
FILE_NAME=grim-v$2-macos-$1.zip
cd macos
zip -r ${FILE_NAME} Grim.app
mv ${FILE_NAME} ../target/${arch}/release

View file

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

View file

@ -1,25 +1,27 @@
#!/bin/bash
case $1 in
debug|release)
debug|build)
;;
*)
echo "Usage: build_run.sh [type] where is type is 'debug' or 'release'" >&2
echo "Usage: build_run.sh [type] where is type is 'debug' or 'build'" >&2
exit 1
esac
# Setup build directory
BASEDIR=$(cd $(dirname $0) && pwd)
cd ${BASEDIR}
BASEDIR=$(cd "$(dirname $0)" && pwd)
cd "${BASEDIR}" || return
cd ..
# Build application
type=$1
[[ ${type} == "release" ]] && release_param+=(--release)
cargo build ${release_param[@]}
[[ ${type} == "build" ]] && release_param+=(--release)
cargo --config profile.release.incremental=true build "${release_param[@]}"
# Start application
if [ $? -eq 0 ]
then
./target/${type}/grim
fi
path=${type}
[[ ${type} == "build" ]] && path="release"
./target/"${path}"/grim
fi

99
scripts/version.sh Executable file
View file

@ -0,0 +1,99 @@
#!/bin/bash
# Usage to bump version
# ./version.sh patch
# ./version.sh minor
# ./version.sh major
# Setup base directory
BASEDIR=$(cd $(dirname $0) && pwd)
cd ${BASEDIR}
cd ..
# Exit script if command fails or uninitialized variables used
set -euo pipefail
# ==================================
# Verify repo is clean
# ==================================
# List uncommitted changes and
# check if the output is not empty
if [ -n "$(git status --porcelain)" ]; then
# Print error message
printf "\nError: repo has uncommitted changes\n\n"
# Exit with error code
exit 1
fi
# ==================================
# Get latest version from git tags
# ==================================
# List git tags sorted lexicographically
# so version numbers sorted correctly
GIT_TAGS=$(git tag --sort=version:refname)
# Get last line of output which returns the
# last tag (most recent version)
GIT_TAG_LATEST=$(echo "$GIT_TAGS" | tail -n 1)
# If no tag found, default to v0.1.0
if [ -z "$GIT_TAG_LATEST" ]; then
GIT_TAG_LATEST="v0.1.0"
fi
# Strip prefix 'v' from the tag to easily increment
GIT_TAG_LATEST=$(echo "$GIT_TAG_LATEST" | sed 's/^v//')
# ==================================
# Increment version number
# ==================================
# Get version type from first argument passed to script
VERSION_TYPE="${1-}"
VERSION_NEXT=""
if [ "$VERSION_TYPE" = "patch" ]; then
# Increment patch version
VERSION_NEXT="$(echo "$GIT_TAG_LATEST" | awk -F. '{$NF++; print $1"."$2"."$NF}')"
elif [ "$VERSION_TYPE" = "minor" ]; then
# Increment minor version
VERSION_NEXT="$(echo "$GIT_TAG_LATEST" | awk -F. '{$2++; $3=0; print $1"."$2"."$3}')"
elif [ "$VERSION_TYPE" = "major" ]; then
# Increment major version
VERSION_NEXT="$(echo "$GIT_TAG_LATEST" | awk -F. '{$1++; $2=0; $3=0; print $1"."$2"."$3}')"
else
# Print error for unknown versioning type
printf "\nError: invalid VERSION_TYPE arg passed, must be 'patch', 'minor' or 'major'\n\n"
# Exit with error code
exit 1
fi
# 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
# Update Android version in build.gradle
sed -i'.bak' -e 's/versionName [0-9a-zA-Z -_]*/versionName "'"$VERSION_NEXT"'"/' android/app/build.gradle
rm -f android/app/build.gradle.bak
# Update version in Cargo.toml
sed -i'.bak' -e "s/^version = .*/version = \"$VERSION_NEXT\"/" Cargo.toml
rm -f Cargo.toml.bak
# Update Cargo.lock as this changes when
# updating the version in your manifest
cargo update -p grim
# Commit the changes
git add .
git commit -m "release: v$VERSION_NEXT"
# ==================================
# Create git tag for new version
# ==================================
# Create a tag and push to master branch
git tag "v$VERSION_NEXT" master
#git push origin master --follow-tags

314
src/gui/app.rs Normal file → Executable file
View file

@ -14,15 +14,15 @@
use std::sync::atomic::{AtomicBool, Ordering};
use lazy_static::lazy_static;
use egui::{Align, Context, CursorIcon, Layout, Modifiers, Rect, ResizeDirection, Rounding, Stroke, ViewportCommand};
use egui::{Align, Context, CursorIcon, Layout, Modifiers, ResizeDirection, Rounding, Stroke, UiBuilder, ViewportCommand};
use egui::epaint::{RectShape};
use egui::os::OperatingSystem;
use crate::AppConfig;
use crate::gui::Colors;
use crate::gui::icons::{ARROWS_IN, ARROWS_OUT, CARET_DOWN, MOON, SUN, X};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Content, TitlePanel, View};
use crate::gui::views::{Content, Modal, TitlePanel, View};
use crate::wallet::ExternalConnection;
lazy_static! {
/// State to check if platform Back button was pressed.
@ -31,27 +31,49 @@ lazy_static! {
/// Implements ui entry point and contains platform-specific callbacks.
pub struct App<Platform> {
/// Platform specific callbacks handler.
pub(crate) platform: Platform,
/// Main ui content.
/// Handles platform-specific functionality.
pub platform: Platform,
/// Main content.
content: Content,
/// Last window resize direction.
resize_direction: Option<ResizeDirection>
resize_direction: Option<ResizeDirection>,
/// Flag to check if it's first draw.
first_draw: bool
}
impl<Platform: PlatformCallbacks> App<Platform> {
pub fn new(platform: Platform) -> Self {
Self { platform, content: Content::default(), resize_direction: None }
Self {
platform,
content: Content::default(),
resize_direction: None,
first_draw: true
}
}
/// Called of first content draw.
fn on_first_draw(&mut self, ctx: &Context) {
// Set platform context.
if View::is_desktop() {
self.platform.set_context(ctx);
}
// Check connections availability.
ExternalConnection::check(None, ctx);
// Setup visuals.
crate::setup_visuals(ctx);
}
/// Draw application content.
pub fn ui(&mut self, ctx: &Context) {
if self.first_draw {
self.on_first_draw(ctx);
self.first_draw = false;
}
// Handle Esc keyboard key event and platform Back button key event.
let back_pressed = BACK_BUTTON_PRESSED.load(Ordering::Relaxed);
if ctx.input_mut(|i| i.consume_key(Modifiers::NONE, egui::Key::Escape)) || back_pressed {
self.content.on_back();
if back_pressed || ctx.input_mut(|i| i.consume_key(Modifiers::NONE, egui::Key::Escape)) {
self.content.on_back(&self.platform);
if back_pressed {
BACK_BUTTON_PRESSED.store(false, Ordering::Relaxed);
}
@ -59,16 +81,15 @@ impl<Platform: PlatformCallbacks> App<Platform> {
ctx.request_repaint();
}
// Handle Close event (on desktop).
if ctx.input(|i| i.viewport().close_requested()) {
// Handle Close event on desktop.
if View::is_desktop() && ctx.input(|i| i.viewport().close_requested()) {
if !self.content.exit_allowed {
ctx.send_viewport_cmd(ViewportCommand::CancelClose);
Content::show_exit_modal();
} else {
let (w, h) = View::window_size(ctx);
AppConfig::save_window_size(w, h);
ctx.input(|i| {
if let Some(rect) = i.viewport().inner_rect {
AppConfig::save_window_size(rect.width(), rect.height());
}
if let Some(rect) = i.viewport().outer_rect {
AppConfig::save_window_pos(rect.left(), rect.top());
}
@ -76,95 +97,92 @@ impl<Platform: PlatformCallbacks> App<Platform> {
}
}
// Show main content with custom frame on desktop.
// Show main content.
egui::CentralPanel::default()
.frame(egui::Frame {
..Default::default()
})
.show(ctx, |ui| {
let is_mac_os = OperatingSystem::from_target_os() == OperatingSystem::Mac;
if View::is_desktop() && !is_mac_os {
self.desktop_window_ui(ui);
} else {
if is_mac_os {
self.window_title_ui(ui);
ui.add_space(-1.0);
if View::is_desktop() {
let is_fullscreen = ui.ctx().input(|i| {
i.viewport().fullscreen.unwrap_or(false)
});
let os = egui::os::OperatingSystem::from_target_os();
match os {
egui::os::OperatingSystem::Mac => {
self.window_title_ui(ui, is_fullscreen);
ui.add_space(-1.0);
Self::title_panel_bg(ui, true);
self.content.ui(ui, &self.platform);
}
egui::os::OperatingSystem::Windows => {
Self::title_panel_bg(ui, false);
self.content.ui(ui, &self.platform);
}
_ => {
self.custom_frame_ui(ui, is_fullscreen);
}
}
} else {
Self::title_panel_bg(ui, false);
self.content.ui(ui, &self.platform);
}
// Provide incoming data to wallets.
if let Some(data) = crate::consume_incoming_data() {
if !data.is_empty() {
self.content.wallets.on_data(ui, Some(data), &self.platform);
}
}
});
// Check if desktop window was focused after requested attention.
if self.platform.user_attention_required() &&
ctx.input(|i| i.viewport().focused.unwrap_or(true)) {
self.platform.clear_user_attention();
}
}
/// Draw custom resizeable window content.
fn desktop_window_ui(&mut self, ui: &mut egui::Ui) {
let is_fullscreen = ui.ctx().input(|i| {
i.viewport().fullscreen.unwrap_or(false)
});
let title_stroke_rect = {
let mut rect = ui.max_rect();
/// Draw custom desktop window frame content.
fn custom_frame_ui(&mut self, ui: &mut egui::Ui, is_fullscreen: bool) {
let content_bg_rect = {
let mut r = ui.max_rect();
if !is_fullscreen {
rect = rect.shrink(Content::WINDOW_FRAME_MARGIN);
r = r.shrink(Content::WINDOW_FRAME_MARGIN);
}
rect.max.y = if !is_fullscreen {
Content::WINDOW_FRAME_MARGIN
} else {
0.0
} + Content::WINDOW_TITLE_HEIGHT + TitlePanel::DEFAULT_HEIGHT + 0.5;
rect
r.min.y += Content::WINDOW_TITLE_HEIGHT + TitlePanel::HEIGHT;
r
};
let title_stroke = RectShape {
rect: title_stroke_rect,
rounding: Rounding {
nw: 8.0,
ne: 8.0,
sw: 0.0,
se: 0.0,
},
fill: Colors::yellow(),
stroke: Stroke {
width: 1.0,
color: egui::Color32::from_gray(200)
},
blur_width: 0.0,
fill_texture_id: Default::default(),
uv: Rect::ZERO
};
// Draw title stroke.
ui.painter().add(title_stroke);
let content_bg = RectShape::new(content_bg_rect,
Rounding::ZERO,
Colors::fill_lite(),
View::default_stroke());
// Draw content background.
ui.painter().add(content_bg);
let content_stroke_rect = {
let mut rect = ui.max_rect();
if !is_fullscreen {
rect = rect.shrink(Content::WINDOW_FRAME_MARGIN);
}
let top = Content::WINDOW_TITLE_HEIGHT + TitlePanel::DEFAULT_HEIGHT + 0.5;
rect.min += egui::vec2(0.0, top);
rect
};
let content_stroke = RectShape {
rect: content_stroke_rect,
rounding: Rounding::ZERO,
fill: Colors::fill(),
stroke: Stroke {
width: 1.0,
color: Colors::stroke()
},
blur_width: 0.0,
fill_texture_id: Default::default(),
uv: Rect::ZERO
};
// Draw content stroke.
ui.painter().add(content_stroke);
// Draw window content.
let mut content_rect = ui.max_rect();
if !is_fullscreen {
content_rect = content_rect.shrink(Content::WINDOW_FRAME_MARGIN);
}
ui.allocate_ui_at_rect(content_rect, |ui| {
self.window_title_ui(ui);
self.window_content(ui);
// Draw window content.
ui.allocate_new_ui(UiBuilder::new().max_rect(content_rect), |ui| {
// Draw window title.
self.window_title_ui(ui, is_fullscreen);
ui.add_space(-1.0);
// Draw title panel background.
Self::title_panel_bg(ui, true);
let content_rect = {
let mut rect = ui.max_rect();
rect.min.y += Content::WINDOW_TITLE_HEIGHT;
rect
};
let mut content_ui = ui.new_child(UiBuilder::new()
.max_rect(content_rect)
.layout(*ui.layout()));
// Draw main content.
self.content.ui(&mut content_ui, &self.platform);
});
// Setup resize areas.
@ -180,57 +198,53 @@ impl<Platform: PlatformCallbacks> App<Platform> {
}
}
/// Draw window content for desktop.
fn window_content(&mut self, ui: &mut egui::Ui) {
let content_rect = {
/// Draw title panel background.
fn title_panel_bg(ui: &mut egui::Ui, window_title: bool) {
let title_rect = {
let mut rect = ui.max_rect();
rect.min.y += Content::WINDOW_TITLE_HEIGHT;
if window_title {
rect.min.y += Content::WINDOW_TITLE_HEIGHT - 0.5;
}
rect.max.y = rect.min.y + View::get_top_inset() + TitlePanel::HEIGHT;
rect
};
// Draw main content.
let mut content_ui = ui.child_ui(content_rect, *ui.layout(), None);
self.content.ui(&mut content_ui, &self.platform);
let title_bg = RectShape::filled(title_rect, Rounding::ZERO, Colors::yellow());
ui.painter().add(title_bg);
}
/// Draw custom window title content.
fn window_title_ui(&self, ui: &mut egui::Ui) {
let content_rect = ui.max_rect();
fn window_title_ui(&self, ui: &mut egui::Ui, is_fullscreen: bool) {
let title_rect = {
let mut rect = content_rect;
let mut rect = ui.max_rect();
rect.max.y = rect.min.y + Content::WINDOW_TITLE_HEIGHT;
rect
};
let is_fullscreen = ui.ctx().input(|i| {
i.viewport().fullscreen.unwrap_or(false)
});
let window_title_bg = RectShape {
rect: title_rect,
rounding: if is_fullscreen {
Rounding::ZERO
} else {
Rounding {
nw: 8.0,
ne: 8.0,
sw: 0.0,
se: 0.0,
}
},
fill: Colors::yellow_dark(),
stroke: Stroke::NONE,
blur_width: 0.0,
fill_texture_id: Default::default(),
uv: Rect::ZERO
let title_bg_rect = {
let mut r = title_rect.clone();
r.max.y += TitlePanel::HEIGHT - 1.0;
r
};
let is_mac = egui::os::OperatingSystem::from_target_os() == egui::os::OperatingSystem::Mac;
let window_title_bg = RectShape::new(title_bg_rect, if is_fullscreen || is_mac {
Rounding::ZERO
} else {
Rounding {
nw: 8.0,
ne: 8.0,
sw: 0.0,
se: 0.0,
}
}, Colors::yellow_dark(), Stroke::new(1.0, Colors::STROKE));
// Draw title background.
ui.painter().add(window_title_bg);
let painter = ui.painter();
let interact_rect = {
let mut rect = title_rect;
let mut rect = title_rect.clone();
rect.max.x -= 128.0;
rect.min.x += 85.0;
if !is_fullscreen {
rect.min.y += Content::WINDOW_FRAME_MARGIN;
}
@ -239,25 +253,29 @@ impl<Platform: PlatformCallbacks> App<Platform> {
let title_resp = ui.interact(
interact_rect,
egui::Id::new("window_title"),
egui::Sense::click_and_drag(),
egui::Sense::drag(),
);
// Interact with the window title (drag to move window):
if !is_fullscreen && title_resp.drag_started_by(egui::PointerButton::Primary) {
ui.ctx().send_viewport_cmd(ViewportCommand::StartDrag);
}
// Paint the title.
let dual_wallets_panel =
ui.available_width() >= (Content::SIDE_PANEL_WIDTH * 3.0) + View::get_right_inset();
let wallet_panel_opened = self.content.wallets.wallet_panel_opened();
let hide_app_name = if dual_wallets_panel {
!wallet_panel_opened || (AppConfig::show_wallets_at_dual_panel() &&
self.content.wallets.showing_wallet() && !self.content.wallets.creating_wallet())
} else if Content::is_dual_panel_mode(ui) {
!wallet_panel_opened
let dual_wallets_panel = ui.available_width() >= (Content::SIDE_PANEL_WIDTH * 3.0) +
View::get_right_inset() + View::get_left_inset();
let wallet_panel_opened = self.content.wallets.showing_wallet();
let show_app_name = if dual_wallets_panel {
wallet_panel_opened && !AppConfig::show_wallets_at_dual_panel()
} else if Content::is_dual_panel_mode(ui.ctx()) {
wallet_panel_opened
} else {
!Content::is_network_panel_open() && !wallet_panel_opened
Content::is_network_panel_open() || wallet_panel_opened
};
let title_text = if hide_app_name {
"".to_string()
} else {
let creating_wallet = self.content.wallets.creating_wallet();
let title_text = if creating_wallet || show_app_name {
format!("Grim {}", crate::VERSION)
} else {
"".to_string()
};
painter.text(
title_rect.center(),
@ -267,20 +285,13 @@ impl<Platform: PlatformCallbacks> App<Platform> {
Colors::title(true),
);
// Interact with the window title (drag to move window):
if !is_fullscreen && title_resp.double_clicked() {
ui.ctx().send_viewport_cmd(ViewportCommand::Fullscreen(!is_fullscreen));
}
if !is_fullscreen && title_resp.drag_started_by(egui::PointerButton::Primary) {
ui.ctx().send_viewport_cmd(ViewportCommand::StartDrag);
}
ui.allocate_ui_at_rect(title_rect, |ui| {
ui.allocate_new_ui(UiBuilder::new().max_rect(title_rect), |ui| {
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
// Draw button to close window.
View::title_button_small(ui, X, |_| {
Content::show_exit_modal();
if Modal::opened().is_none() || Modal::opened_closeable() {
Content::show_exit_modal();
}
});
// Draw fullscreen button.
@ -392,16 +403,13 @@ impl<Platform: PlatformCallbacks> eframe::App for App<Platform> {
}
fn clear_color(&self, _visuals: &egui::Visuals) -> [f32; 4] {
if View::is_desktop() {
let is_mac_os = OperatingSystem::from_target_os() == OperatingSystem::Mac;
if is_mac_os {
Colors::fill().to_normalized_gamma_f32()
} else {
egui::Rgba::TRANSPARENT.to_array()
}
} else {
Colors::fill().to_normalized_gamma_f32()
let os = egui::os::OperatingSystem::from_target_os();
let is_win = os == egui::os::OperatingSystem::Windows;
let is_mac = os == egui::os::OperatingSystem::Mac;
if !View::is_desktop() || is_win || is_mac {
return Colors::fill_lite().to_normalized_gamma_f32();
}
Colors::TRANSPARENT.to_normalized_gamma_f32()
}
}

View file

@ -31,10 +31,14 @@ const YELLOW: Color32 = Color32::from_rgb(254, 241, 2);
const YELLOW_DARK: Color32 = Color32::from_rgb(239, 229, 3);
const GREEN: Color32 = Color32::from_rgb(0, 0x64, 0);
const GREEN_DARK: Color32 = Color32::from_rgb(0, (0x64 as f32 * 1.3 + 0.5) as u8, 0);
const RED: Color32 = Color32::from_rgb(0x8B, 0, 0);
const RED_DARK: Color32 = Color32::from_rgb((0x8B as f32 * 1.3 + 0.5) as u8, 0, 0);
const BLUE: Color32 = Color32::from_rgb(0, 0x66, 0xE4);
const BLUE_DARK: Color32 =
Color32::from_rgb(0, (0x66 as f32 * 1.3 + 0.5) as u8, (0xE4 as f32 * 1.3 + 0.5) as u8);
const FILL: Color32 = Color32::from_gray(244);
const FILL_DARK: Color32 = Color32::from_gray(24);
@ -42,6 +46,9 @@ const FILL_DARK: Color32 = Color32::from_gray(24);
const FILL_DEEP: Color32 = Color32::from_gray(238);
const FILL_DEEP_DARK: Color32 = Color32::from_gray(18);
const FILL_LITE: Color32 = Color32::from_gray(249);
const FILL_LITE_DARK: Color32 = Color32::from_gray(16);
const TEXT: Color32 = Color32::from_gray(80);
const TEXT_DARK: Color32 = Color32::from_gray(185);
@ -54,13 +61,9 @@ const TEXT_BUTTON_DARK: Color32 = Color32::from_gray(195);
const TITLE: Color32 = Color32::from_gray(60);
const TITLE_DARK: Color32 = Color32::from_gray(205);
const BUTTON: Color32 = Color32::from_gray(249);
const BUTTON_DARK: Color32 = Color32::from_gray(16);
const GRAY: Color32 = Color32::from_gray(120);
const GRAY_DARK: Color32 = Color32::from_gray(145);
const STROKE: Color32 = Color32::from_gray(200);
const STROKE_DARK: Color32 = Color32::from_gray(50);
const INACTIVE_TEXT: Color32 = Color32::from_gray(150);
@ -82,6 +85,7 @@ fn use_dark() -> bool {
impl Colors {
pub const TRANSPARENT: Color32 = Color32::from_rgba_premultiplied(0, 0, 0, 0);
pub const STROKE: Color32 = Color32::from_gray(200);
pub fn white_or_black(black_in_white: bool) -> Color32 {
if use_dark() {
@ -125,7 +129,7 @@ impl Colors {
pub fn green() -> Color32 {
if use_dark() {
GREEN.gamma_multiply(1.3)
GREEN_DARK
} else {
GREEN
}
@ -133,7 +137,7 @@ impl Colors {
pub fn red() -> Color32 {
if use_dark() {
RED.gamma_multiply(1.3)
RED_DARK
} else {
RED
}
@ -141,7 +145,7 @@ impl Colors {
pub fn blue() -> Color32 {
if use_dark() {
BLUE.gamma_multiply(1.3)
BLUE_DARK
} else {
BLUE
}
@ -163,6 +167,14 @@ impl Colors {
}
}
pub fn fill_lite() -> Color32 {
if use_dark() {
FILL_LITE_DARK
} else {
FILL_LITE
}
}
pub fn checkbox() -> Color32 {
if use_dark() {
CHECKBOX_DARK
@ -195,14 +207,6 @@ impl Colors {
}
}
pub fn button() -> Color32 {
if use_dark() {
BUTTON_DARK
} else {
BUTTON
}
}
pub fn gray() -> Color32 {
if use_dark() {
GRAY_DARK
@ -215,7 +219,7 @@ impl Colors {
if use_dark() {
STROKE_DARK
} else {
STROKE
Self::STROKE
}
}

View file

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

View file

@ -13,12 +13,13 @@
// limitations under the License.
use std::fs::File;
use std::io:: Write;
use lazy_static::lazy_static;
use parking_lot::RwLock;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::io::Write;
use std::thread;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::Arc;
use parking_lot::RwLock;
use lazy_static::lazy_static;
use egui::{UserAttentionType, ViewportCommand, WindowLevel};
use rfd::FileDialog;
use crate::gui::platform::PlatformCallbacks;
@ -26,19 +27,159 @@ use crate::gui::platform::PlatformCallbacks;
/// Desktop platform related actions.
#[derive(Clone)]
pub struct Desktop {
/// Context to repaint content and handle viewport commands.
ctx: Arc<RwLock<Option<egui::Context>>>,
/// Cameras amount.
cameras_amount: Arc<AtomicUsize>,
/// Camera index.
camera_index: Arc<AtomicUsize>,
/// Flag to check if camera stop is needed.
stop_camera: Arc<AtomicBool>,
/// Flag to check if attention required after window focusing.
attention_required: Arc<AtomicBool>,
}
impl Default for Desktop {
fn default() -> Self {
impl Desktop {
pub fn new() -> Self {
Self {
ctx: Arc::new(RwLock::new(None)),
cameras_amount: Arc::new(AtomicUsize::new(0)),
camera_index: Arc::new(AtomicUsize::new(0)),
stop_camera: Arc::new(AtomicBool::new(false)),
attention_required: Arc::new(AtomicBool::new(false)),
}
}
// #[allow(dead_code)]
#[cfg(not(target_os = "macos"))]
fn start_camera_capture(cameras_amount: Arc<AtomicUsize>,
camera_index: Arc<AtomicUsize>,
stop_camera: Arc<AtomicBool>) {
use nokhwa::Camera;
use nokhwa::pixel_format::RgbFormat;
use nokhwa::utils::{CameraIndex, RequestedFormat, RequestedFormatType};
use nokhwa::utils::ApiBackend;
let devices = nokhwa::query(ApiBackend::Auto).unwrap();
cameras_amount.store(devices.len(), Ordering::Relaxed);
let index = camera_index.load(Ordering::Relaxed);
if devices.is_empty() || index >= devices.len() {
return;
}
thread::spawn(move || {
let index = CameraIndex::Index(camera_index.load(Ordering::Relaxed) as u32);
let requested = RequestedFormat::new::<RgbFormat>(
RequestedFormatType::AbsoluteHighestFrameRate
);
// Create and open camera.
if let Ok(mut camera) = Camera::new(index, requested) {
if let Ok(_) = camera.open_stream() {
loop {
// Stop if camera was stopped.
if stop_camera.load(Ordering::Relaxed) {
stop_camera.store(false, Ordering::Relaxed);
// Clear image.
let mut w_image = LAST_CAMERA_IMAGE.write();
*w_image = None;
break;
}
// Get a frame.
if let Ok(frame) = camera.frame() {
// Save image.
let mut w_image = LAST_CAMERA_IMAGE.write();
*w_image = Some((frame.buffer().to_vec(), 0));
} else {
// Clear image.
let mut w_image = LAST_CAMERA_IMAGE.write();
*w_image = None;
break;
}
}
camera.stop_stream().unwrap();
};
}
});
}
#[allow(dead_code)]
#[cfg(target_os = "macos")]
fn start_camera_capture(cameras_amount: Arc<AtomicUsize>,
camera_index: Arc<AtomicUsize>,
stop_camera: Arc<AtomicBool>) {
use nokhwa_mac::nokhwa_initialize;
use nokhwa_mac::pixel_format::RgbFormat;
use nokhwa_mac::utils::{CameraIndex, RequestedFormat, RequestedFormatType};
use nokhwa_mac::utils::ApiBackend;
use nokhwa_mac::query;
use nokhwa_mac::CallbackCamera;
// Ask permission to open camera.
nokhwa_initialize(|_| {});
thread::spawn(move || {
let cameras = query(ApiBackend::Auto).unwrap();
cameras_amount.store(cameras.len(), Ordering::Relaxed);
let index = camera_index.load(Ordering::Relaxed);
if cameras.is_empty() || index >= cameras.len() {
return;
}
// Start camera.
let camera_index = CameraIndex::Index(camera_index.load(Ordering::Relaxed) as u32);
let camera_callback = CallbackCamera::new(
camera_index,
RequestedFormat::new::<RgbFormat>(RequestedFormatType::AbsoluteHighestFrameRate),
|_| {}
);
if let Ok(mut cb) = camera_callback {
if cb.open_stream().is_ok() {
loop {
// Stop if camera was stopped.
if stop_camera.load(Ordering::Relaxed) {
stop_camera.store(false, Ordering::Relaxed);
// Clear image.
let mut w_image = LAST_CAMERA_IMAGE.write();
*w_image = None;
break;
}
// Get image from camera.
if let Ok(frame) = cb.poll_frame() {
let image = frame.decode_image::<RgbFormat>().unwrap();
let mut bytes: Vec<u8> = Vec::new();
let format = image::ImageFormat::Jpeg;
// Convert image to Jpeg format.
image.write_to(&mut std::io::Cursor::new(&mut bytes), format).unwrap();
let mut w_image = LAST_CAMERA_IMAGE.write();
*w_image = Some((bytes, 0));
} else {
// Clear image.
let mut w_image = LAST_CAMERA_IMAGE.write();
*w_image = None;
break;
}
}
}
}
});
}
}
impl PlatformCallbacks for Desktop {
fn set_context(&mut self, ctx: &egui::Context) {
let mut w_ctx = self.ctx.write();
*w_ctx = Some(ctx.clone());
}
fn exit(&self) {
let r_ctx = self.ctx.read();
if r_ctx.is_some() {
let ctx = r_ctx.as_ref().unwrap();
ctx.send_viewport_cmd(ViewportCommand::Close);
}
}
fn show_keyboard(&self) {}
fn hide_keyboard(&self) {}
@ -59,15 +200,13 @@ impl PlatformCallbacks for Desktop {
let mut w_image = LAST_CAMERA_IMAGE.write();
*w_image = None;
}
// Setup stop camera flag.
let stop_camera = self.stop_camera.clone();
stop_camera.store(false, Ordering::Relaxed);
// Capture images at separate thread.
thread::spawn(move || {
Self::start_camera_capture(stop_camera);
});
Self::start_camera_capture(self.cameras_amount.clone(),
self.camera_index.clone(),
stop_camera);
}
fn stop_camera(&self) {
@ -84,11 +223,20 @@ impl PlatformCallbacks for Desktop {
}
fn can_switch_camera(&self) -> bool {
false
let amount = self.cameras_amount.load(Ordering::Relaxed);
amount > 1
}
fn switch_camera(&self) {
return;
self.stop_camera();
let index = self.camera_index.load(Ordering::Relaxed);
let amount = self.cameras_amount.load(Ordering::Relaxed);
if index == amount - 1 {
self.camera_index.store(0, Ordering::Relaxed);
} else {
self.camera_index.store(index + 1, Ordering::Relaxed);
}
self.start_camera();
}
fn share_data(&self, name: String, data: Vec<u8>) -> Result<(), std::io::Error> {
@ -119,86 +267,43 @@ impl PlatformCallbacks for Desktop {
fn picked_file(&self) -> Option<String> {
None
}
}
impl Desktop {
#[allow(dead_code)]
#[cfg(target_os = "windows")]
fn start_camera_capture(stop_camera: Arc<AtomicBool>) {
use nokhwa::Camera;
use nokhwa::pixel_format::RgbFormat;
use nokhwa::utils::{CameraIndex, RequestedFormat, RequestedFormatType};
let index = CameraIndex::Index(0);
let requested = RequestedFormat::new::<RgbFormat>(
RequestedFormatType::AbsoluteHighestFrameRate
);
// Create and open camera.
let mut camera = Camera::new(index, requested).unwrap();
if let Ok(_) = camera.open_stream() {
loop {
// Stop if camera was stopped.
if stop_camera.load(Ordering::Relaxed) {
stop_camera.store(false, Ordering::Relaxed);
// Clear image.
let mut w_image = LAST_CAMERA_IMAGE.write();
*w_image = None;
break;
}
// Get a frame.
if let Ok(frame) = camera.frame() {
// Save image.
let mut w_image = LAST_CAMERA_IMAGE.write();
*w_image = Some((frame.buffer().to_vec(), 0));
} else {
// Clear image.
let mut w_image = LAST_CAMERA_IMAGE.write();
*w_image = None;
break;
}
fn request_user_attention(&self) {
let r_ctx = self.ctx.read();
if r_ctx.is_some() {
let ctx = r_ctx.as_ref().unwrap();
// Request attention on taskbar.
ctx.send_viewport_cmd(
ViewportCommand::RequestUserAttention(UserAttentionType::Informational)
);
// Un-minimize window.
if ctx.input(|i| i.viewport().minimized.unwrap_or(false)) {
ctx.send_viewport_cmd(ViewportCommand::Minimized(false));
}
camera.stop_stream().unwrap();
};
// Focus to window.
if !ctx.input(|i| i.viewport().focused.unwrap_or(false)) {
ctx.send_viewport_cmd(ViewportCommand::WindowLevel(WindowLevel::AlwaysOnTop));
ctx.send_viewport_cmd(ViewportCommand::Focus);
}
ctx.request_repaint();
}
self.attention_required.store(true, Ordering::Relaxed);
}
#[allow(dead_code)]
#[cfg(not(target_os = "windows"))]
fn start_camera_capture(stop_camera: Arc<AtomicBool>) {
use eye::hal::{traits::{Context, Device, Stream}, PlatformContext};
use image::ImageEncoder;
fn user_attention_required(&self) -> bool {
self.attention_required.load(Ordering::Relaxed)
}
let ctx = PlatformContext::default();
let devices = ctx.devices().unwrap();
let dev = ctx.open_device(&devices[0].uri).unwrap();
let streams = dev.streams().unwrap();
let stream_desc = streams[0].clone();
let w = stream_desc.width;
let h = stream_desc.height;
let mut stream = dev.start_stream(&stream_desc).unwrap();
loop {
// Stop if camera was stopped.
if stop_camera.load(Ordering::Relaxed) {
stop_camera.store(false, Ordering::Relaxed);
// Clear image.
let mut w_image = LAST_CAMERA_IMAGE.write();
*w_image = None;
break;
}
// Get a frame.
let frame = stream.next().expect("Stream is dead").expect("Failed to capture a frame");
let mut out = vec![];
if let Some(buf) = image::ImageBuffer::<image::Rgb<u8>, &[u8]>::from_raw(w, h, &frame) {
image::codecs::jpeg::JpegEncoder::new(&mut out)
.write_image(buf.as_raw(), w, h, image::ExtendedColorType::Rgb8).unwrap();
} else {
out = frame.to_vec();
}
// Save image.
let mut w_image = LAST_CAMERA_IMAGE.write();
*w_image = Some((out, 0));
fn clear_user_attention(&self) {
let r_ctx = self.ctx.read();
if r_ctx.is_some() {
let ctx = r_ctx.as_ref().unwrap();
ctx.send_viewport_cmd(
ViewportCommand::RequestUserAttention(UserAttentionType::Reset)
);
ctx.send_viewport_cmd(ViewportCommand::WindowLevel(WindowLevel::Normal));
}
self.attention_required.store(false, Ordering::Relaxed);
}
}

View file

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

View file

@ -15,11 +15,9 @@
use std::sync::Arc;
use parking_lot::RwLock;
use std::thread;
use eframe::emath::Align;
use egui::load::SizedTexture;
use egui::{Layout, Pos2, Rect, RichText, TextureOptions, Widget};
use image::{DynamicImage, EncodableLayout, ImageFormat};
use egui::{Pos2, Rect, RichText, TextureOptions, UiBuilder, Widget};
use image::{DynamicImage, EncodableLayout};
use grin_util::ZeroingString;
use grin_wallet_libwallet::SlatepackAddress;
use grin_keychain::mnemonic::WORDS;
@ -36,16 +34,15 @@ use crate::wallet::WalletUtils;
pub struct CameraContent {
/// QR code scanning progress and result.
qr_scan_state: Arc<RwLock<QrScanState>>,
/// Uniform Resources URIs collected from QR code scanning.
ur_data: Arc<RwLock<Option<(Vec<String>, usize)>>>,
ur_data: Arc<RwLock<Option<(Vec<String>, usize)>>>
}
impl Default for CameraContent {
fn default() -> Self {
Self {
qr_scan_state: Arc::new(RwLock::new(QrScanState::default())),
ur_data: Arc::new(RwLock::new(None)),
ur_data: Arc::new(RwLock::new(None))
}
}
}
@ -53,102 +50,112 @@ impl Default for CameraContent {
impl CameraContent {
/// Draw camera content.
pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
// Draw last image from camera or loader.
if let Some(img_data) = cb.camera_image() {
// Load image to draw.
if let Ok(mut img) =
image::load_from_memory_with_format(&*img_data.0, ImageFormat::Jpeg) {
ui.ctx().request_repaint();
let rect = if let Some(img_data) = cb.camera_image() {
if let Ok(img) =
image::load_from_memory(&*img_data.0) {
// Process image to find QR code.
self.scan_qr(&img);
// Setup image rotation.
img = match img_data.1 {
90 => img.rotate90(),
180 => img.rotate180(),
270 => img.rotate270(),
_ => img
};
// Convert to ColorImage to add at content.
let color_img = match &img {
DynamicImage::ImageRgb8(image) => {
egui::ColorImage::from_rgb(
[image.width() as usize, image.height() as usize],
image.as_bytes(),
)
},
other => {
let image = other.to_rgba8();
egui::ColorImage::from_rgba_unmultiplied(
[image.width() as usize, image.height() as usize],
image.as_bytes(),
)
},
};
// Create image texture.
let texture = ui.ctx().load_texture("camera_image",
color_img.clone(),
TextureOptions::default());
let img_size = egui::emath::vec2(color_img.width() as f32,
color_img.height() as f32);
let sized_img = SizedTexture::new(texture.id(), img_size);
// Add image to content.
ui.vertical_centered(|ui| {
egui::Image::from_texture(sized_img)
// Setup to crop image at square.
.uv(Rect::from([
Pos2::new(1.0 - (img_size.y / img_size.x), 0.0),
Pos2::new(1.0, 1.0)
]))
.max_height(ui.available_width())
.maintain_aspect_ratio(false)
.shrink_to_fit()
.ui(ui);
});
// Draw image.
let img_rect = self.image_ui(ui, img, img_data.1);
// Show UR scan progress.
let show_ur_progress = {
self.ur_data.clone().read().is_some()
};
let ur_progress = self.ur_progress();
if show_ur_progress && ur_progress != 0 {
ui.add_space(-52.0);
ui.vertical_centered(|ui| {
ui.label(RichText::new(format!("{}%", ur_progress))
.size(16.0)
.color(Colors::yellow()));
});
}
// Show button to switch cameras.
if cb.can_switch_camera() {
ui.add_space(-52.0);
let mut size = ui.available_size();
size.y = 48.0;
ui.allocate_ui_with_layout(size, Layout::right_to_left(Align::Max), |ui| {
ui.add_space(4.0);
View::button(ui, CAMERA_ROTATE.to_string(), Colors::white_or_black(false), || {
cb.switch_camera();
});
});
}
self.ur_progress_ui(ui);
img_rect
} else {
self.loading_content_ui(ui);
self.loading_ui(ui)
}
} else {
self.loading_content_ui(ui);
}
self.loading_ui(ui)
};
// Request redraw.
ui.ctx().request_repaint();
// Show button to switch cameras.
if cb.can_switch_camera() {
let r = {
let mut r = rect.clone();
r.min.y = r.max.y - 52.0;
r.min.x = r.max.x - 52.0;
r
};
ui.allocate_new_ui(UiBuilder::new().max_rect(r), |ui| {
let rotate_img = CAMERA_ROTATE.to_string();
View::button(ui, rotate_img, Colors::white_or_black(false), || {
cb.switch_camera();
});
});
}
}
/// Draw camera image.
fn image_ui(&mut self, ui: &mut egui::Ui, mut img: DynamicImage, rotation: u32) -> Rect {
// Setup image rotation.
img = match rotation {
90 => img.rotate90(),
180 => img.rotate180(),
270 => img.rotate270(),
_ => img
};
if View::is_desktop() {
img = img.fliph();
}
// Convert to ColorImage.
let color_img = match &img {
DynamicImage::ImageRgb8(image) => {
egui::ColorImage::from_rgb(
[image.width() as usize, image.height() as usize],
image.as_bytes(),
)
},
other => {
let image = other.to_rgba8();
egui::ColorImage::from_rgba_unmultiplied(
[image.width() as usize, image.height() as usize],
image.as_bytes(),
)
},
};
// Create image texture.
let texture = ui.ctx().load_texture("camera_image",
color_img.clone(),
TextureOptions::default());
let img_size = egui::emath::vec2(color_img.width() as f32,
color_img.height() as f32);
let sized_img = SizedTexture::new(texture.id(), img_size);
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),
Pos2::new(1.0, 1.0)
]))
.max_height(ui.available_width())
.maintain_aspect_ratio(false)
.shrink_to_fit()
.ui(ui).rect
}
/// Draw animated QR code scanning progress.
fn ur_progress_ui(&self, ui: &mut egui::Ui) {
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()));
});
}
}
/// Draw camera loading progress content.
fn loading_content_ui(&self, ui: &mut egui::Ui) {
fn loading_ui(&self, ui: &mut egui::Ui) -> Rect {
let space = (ui.available_width() - View::BIG_SPINNER_SIZE) / 2.0;
ui.vertical_centered(|ui| {
ui.add_space(space);
View::big_loading_spinner(ui);
ui.add_space(space);
});
}).response.rect
}
/// Check if image is processing to find QR code.
@ -283,7 +290,7 @@ impl CameraContent {
// Launch scanner at separate thread.
thread::spawn(move || {
tokio::runtime::Builder::new_multi_thread()
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap()
@ -430,14 +437,4 @@ impl CameraContent {
}
None
}
/// Reset camera content state to default.
pub fn clear_state(&mut self) {
// Clear QR code scanning state.
let mut w_scan = self.qr_scan_state.write();
*w_scan = QrScanState::default();
// Clear UR data.
let mut w_data = self.ur_data.write();
*w_data = None;
}
}

View file

@ -25,7 +25,7 @@ use crate::gui::views::types::{ModalContainer, ModalPosition};
use crate::node::Node;
use crate::{AppConfig, Settings};
use crate::gui::icons::{CHECK, CHECK_FAT, FILE_X};
use crate::gui::views::network::{NetworkContent, NodeSetup};
use crate::gui::views::network::NetworkContent;
use crate::gui::views::wallets::WalletsContent;
lazy_static! {
@ -40,8 +40,8 @@ pub struct Content {
/// Central panel [`WalletsContent`] content.
pub wallets: WalletsContent,
/// Check if app exit is allowed on close event of [`eframe::App`] implementation.
pub(crate) exit_allowed: bool,
/// Check if app exit is allowed on Desktop close event.
pub exit_allowed: bool,
/// Flag to show exit progress at [`Modal`].
show_exit_progress: bool,
@ -52,6 +52,11 @@ pub struct Content {
allowed_modal_ids: Vec<&'static str>
}
/// Identifier for integrated node warning [`Modal`] on Android.
const ANDROID_INTEGRATED_NODE_WARNING_MODAL: &'static str = "android_node_warning_modal";
/// Identifier for crash report [`Modal`].
const CRASH_REPORT_MODAL: &'static str = "crash_report_modal";
impl Default for Content {
fn default() -> Self {
// Exit from eframe only for non-mobile platforms.
@ -66,8 +71,8 @@ impl Default for Content {
allowed_modal_ids: vec![
Self::EXIT_CONFIRMATION_MODAL,
Self::SETTINGS_MODAL,
Self::ANDROID_INTEGRATED_NODE_WARNING_MODAL,
Self::CRASH_REPORT_MODAL
ANDROID_INTEGRATED_NODE_WARNING_MODAL,
CRASH_REPORT_MODAL
],
}
}
@ -83,10 +88,10 @@ impl ModalContainer for Content {
modal: &Modal,
cb: &dyn PlatformCallbacks) {
match modal.id {
Self::EXIT_CONFIRMATION_MODAL => self.exit_modal_content(ui, modal),
Self::EXIT_CONFIRMATION_MODAL => self.exit_modal_content(ui, modal, cb),
Self::SETTINGS_MODAL => self.settings_modal_ui(ui, modal),
Self::ANDROID_INTEGRATED_NODE_WARNING_MODAL => self.android_warning_modal_ui(ui, modal),
Self::CRASH_REPORT_MODAL => self.crash_report_modal_ui(ui, modal, cb),
ANDROID_INTEGRATED_NODE_WARNING_MODAL => self.android_warning_modal_ui(ui, modal),
CRASH_REPORT_MODAL => self.crash_report_modal_ui(ui, modal, cb),
_ => {}
}
}
@ -97,10 +102,6 @@ impl Content {
pub const EXIT_CONFIRMATION_MODAL: &'static str = "exit_confirmation_modal";
/// Identifier for wallet opening [`Modal`].
pub const SETTINGS_MODAL: &'static str = "settings_modal";
/// Identifier for integrated node warning [`Modal`] on Android.
const ANDROID_INTEGRATED_NODE_WARNING_MODAL: &'static str = "android_node_warning_modal";
/// Identifier for crash report [`Modal`].
const CRASH_REPORT_MODAL: &'static str = "crash_report_modal";
/// Default width of side panel at application UI.
pub const SIDE_PANEL_WIDTH: f32 = 400.0;
@ -110,11 +111,10 @@ impl Content {
pub const WINDOW_FRAME_MARGIN: f32 = 6.0;
pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
// Draw modal content for current ui container.
self.current_modal_ui(ui, cb);
let dual_panel = Self::is_dual_panel_mode(ui);
let (is_panel_open, panel_width) = Self::network_panel_state_width(ui, dual_panel);
let dual_panel = Self::is_dual_panel_mode(ui.ctx());
let (is_panel_open, panel_width) = network_panel_state_width(ui.ctx(), dual_panel);
// Show network content.
egui::SidePanel::left("network_panel")
@ -137,48 +137,26 @@ impl Content {
});
if self.first_draw {
// Show crash report if needed.
if AppConfig::show_crash() {
Modal::new(Self::CRASH_REPORT_MODAL)
// Show crash report or integrated node Android warning.
if Settings::crash_report_path().exists() {
Modal::new(CRASH_REPORT_MODAL)
.closeable(false)
.position(ModalPosition::Center)
.title(t!("crash_report"))
.show();
} else {
// Show integrated node warning on Android if needed.
if OperatingSystem::from_target_os() == OperatingSystem::Android &&
} else if OperatingSystem::from_target_os() == OperatingSystem::Android &&
AppConfig::android_integrated_node_warning_needed() {
Modal::new(Self::ANDROID_INTEGRATED_NODE_WARNING_MODAL)
Modal::new(ANDROID_INTEGRATED_NODE_WARNING_MODAL)
.title(t!("network.node"))
.show();
}
}
self.first_draw = false;
}
}
/// Get [`NetworkContent`] panel state and width.
fn network_panel_state_width(ui: &mut egui::Ui, dual_panel: bool) -> (bool, f32) {
let is_panel_open = dual_panel || Self::is_network_panel_open();
let panel_width = if dual_panel {
Self::SIDE_PANEL_WIDTH + View::get_left_inset()
} else {
let is_fullscreen = ui.ctx().input(|i| {
i.viewport().fullscreen.unwrap_or(false)
});
View::window_size(ui).0 - if View::is_desktop() && !is_fullscreen &&
OperatingSystem::from_target_os() != OperatingSystem::Mac {
Self::WINDOW_FRAME_MARGIN * 2.0
} else {
0.0
}
};
(is_panel_open, panel_width)
}
/// Check if ui can show [`NetworkContent`] and [`WalletsContent`] at same time.
pub fn is_dual_panel_mode(ui: &egui::Ui) -> bool {
let (w, h) = View::window_size(ui);
pub fn is_dual_panel_mode(ctx: &egui::Context) -> bool {
let (w, h) = View::window_size(ctx);
// Screen is wide if width is greater than height or just 20% smaller.
let is_wide_screen = w > h || w + (w * 0.2) >= h;
// Dual panel mode is available when window is wide and its width is at least 2 times
@ -201,16 +179,16 @@ impl Content {
/// Show exit confirmation [`Modal`].
pub fn show_exit_modal() {
Modal::new(Self::EXIT_CONFIRMATION_MODAL)
.title(t!("modal.confirmation"))
.title(t!("confirmation"))
.show();
}
/// Draw exit confirmation modal content.
fn exit_modal_content(&mut self, ui: &mut egui::Ui, modal: &Modal) {
fn exit_modal_content(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
if self.show_exit_progress {
if !Node::is_running() {
self.exit_allowed = true;
ui.ctx().send_viewport_cmd(egui::ViewportCommand::Close);
cb.exit();
modal.close();
}
ui.add_space(16.0);
@ -241,10 +219,10 @@ impl Content {
});
});
columns[1].vertical_centered_justified(|ui| {
View::button_ui(ui, t!("modal_exit.exit"), Colors::white_or_black(false), |ui| {
View::button_ui(ui, t!("modal_exit.exit"), Colors::white_or_black(false), |_| {
if !Node::is_running() {
self.exit_allowed = true;
ui.ctx().send_viewport_cmd(egui::ViewportCommand::Close);
cb.exit();
modal.close();
} else {
Node::stop(true);
@ -260,9 +238,9 @@ impl Content {
}
/// Handle Back key event.
pub fn on_back(&mut self) {
pub fn on_back(&mut self, cb: &dyn PlatformCallbacks) {
if Modal::on_back() {
if self.wallets.on_back() {
if self.wallets.on_back(cb) {
Self::show_exit_modal()
}
}
@ -272,14 +250,7 @@ impl Content {
pub fn settings_modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal) {
ui.add_space(6.0);
// Draw chain type selection.
NodeSetup::chain_type_ui(ui);
ui.add_space(8.0);
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(8.0);
// Draw theme selection.
// Show theme selection.
Self::theme_selection_ui(ui);
ui.add_space(8.0);
@ -348,42 +319,40 @@ impl Content {
let item_rounding = View::item_rounding(index, len, false);
ui.painter().rect(bg_rect, item_rounding, Colors::fill(), View::item_stroke());
ui.vertical(|ui| {
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
// Draw button to select language.
let is_current = if let Some(lang) = AppConfig::locale() {
lang == locale
} else {
rust_i18n::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);
modal.close();
});
} else {
ui.add_space(14.0);
ui.label(RichText::new(CHECK_FAT).size(20.0).color(Colors::green()));
ui.add_space(14.0);
}
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
// Draw button to select language.
let is_current = if let Some(lang) = AppConfig::locale() {
lang == locale
} else {
rust_i18n::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);
modal.close();
});
} else {
ui.add_space(14.0);
ui.label(RichText::new(CHECK_FAT).size(20.0).color(Colors::green()));
ui.add_space(14.0);
}
let layout_size = ui.available_size();
ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |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| {
// Draw language name.
ui.add_space(12.0);
ui.vertical(|ui| {
// Draw language name.
ui.add_space(12.0);
let color = if is_current {
Colors::title(false)
} else {
Colors::gray()
};
ui.label(RichText::new(t!("lang_name", locale = locale))
.size(17.0)
.color(color));
ui.add_space(3.0);
});
let color = if is_current {
Colors::title(false)
} else {
Colors::gray()
};
ui.label(RichText::new(t!("lang_name", locale = locale))
.size(17.0)
.color(color));
ui.add_space(3.0);
});
});
});
@ -422,10 +391,10 @@ impl Content {
let text = format!("{} {}", FILE_X, t!("share"));
View::colored_text_button(ui, text, Colors::blue(), Colors::white_or_black(false), || {
if let Ok(data) = fs::read_to_string(Settings::crash_report_path()) {
cb.share_data(Settings::CRASH_REPORT_FILE_NAME.to_string(),
data.as_bytes().to_vec()).unwrap_or_default()
let name = Settings::CRASH_REPORT_FILE_NAME.to_string();
let _ = cb.share_data(name, data.as_bytes().to_vec());
}
AppConfig::set_show_crash(false);
Settings::delete_crash_report();
modal.close();
});
});
@ -434,10 +403,29 @@ impl Content {
ui.add_space(8.0);
ui.vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
AppConfig::set_show_crash(false);
Settings::delete_crash_report();
modal.close();
});
});
ui.add_space(6.0);
}
}
/// Get [`NetworkContent`] panel state and width.
fn network_panel_state_width(ctx: &egui::Context, dual_panel: bool) -> (bool, f32) {
let is_panel_open = dual_panel || Content::is_network_panel_open();
let panel_width = if dual_panel {
Content::SIDE_PANEL_WIDTH + View::get_left_inset()
} else {
let is_fullscreen = ctx.input(|i| {
i.viewport().fullscreen.unwrap_or(false)
});
View::window_size(ctx).0 - if View::is_desktop() && !is_fullscreen &&
OperatingSystem::from_target_os() != OperatingSystem::Mac {
Content::WINDOW_FRAME_MARGIN * 2.0
} else {
0.0
}
};
(is_panel_open, panel_width)
}

View file

@ -78,8 +78,8 @@ impl FilePickButton {
}
} else {
// Draw button to pick file.
let file_text = format!("{} {}", ARCHIVE_BOX, t!("choose_file"));
View::colored_text_button(ui, file_text, Colors::blue(), Colors::button(), || {
let text = format!("{} {}", ARCHIVE_BOX, t!("choose_file"));
View::colored_text_button(ui, text, Colors::blue(), Colors::white_or_black(false), || {
if let Some(path) = cb.pick_file() {
self.on_file_pick(path);
}

View file

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

202
src/gui/views/modal.rs Normal file → Executable file
View file

@ -16,7 +16,7 @@ use lazy_static::lazy_static;
use std::sync::Arc;
use parking_lot::RwLock;
use std::sync::atomic::{AtomicBool, Ordering};
use egui::{Align2, Rect, RichText, Rounding, Stroke, Vec2};
use egui::{Align2, RichText, Rounding, Stroke, UiBuilder, Vec2};
use egui::epaint::{RectShape, Shadow};
use egui::os::OperatingSystem;
@ -29,17 +29,17 @@ lazy_static! {
static ref MODAL_STATE: Arc<RwLock<ModalState>> = Arc::new(RwLock::new(ModalState::default()));
}
/// Stores data to draw modal [`egui::Window`] at ui.
/// Modal [`egui::Window`] container.
#[derive(Clone)]
pub struct Modal {
/// Identifier for modal.
pub(crate) id: &'static str,
/// Position on the screen.
position: ModalPosition,
/// To check if it can be closed.
pub position: ModalPosition,
/// Flag to check if modal can be closed by keys.
closeable: Arc<AtomicBool>,
/// Title text
title: Option<String>
/// Title text.
title: Option<String>,
}
impl Modal {
@ -54,7 +54,7 @@ impl Modal {
id,
position: ModalPosition::Center,
closeable: Arc::new(AtomicBool::new(true)),
title: None
title: None,
}
}
@ -64,6 +64,12 @@ impl Modal {
self
}
/// Change [`Modal`] position on the screen.
pub fn change_position(position: ModalPosition) {
let mut w_state = MODAL_STATE.write();
w_state.modal.as_mut().unwrap().position = position;
}
/// Mark [`Modal`] closed.
pub fn close(&self) {
let mut w_nav = MODAL_STATE.write();
@ -104,7 +110,7 @@ impl Modal {
}
/// Remove [`Modal`] from [`ModalState`] if it's showing and can be closed.
/// Return `false` if Modal existed in [`ModalState`] before call.
/// Return `false` if modal existed in state before call.
pub fn on_back() -> bool {
let mut w_state = MODAL_STATE.write();
@ -119,7 +125,7 @@ impl Modal {
true
}
/// Return id of opened [`Modal`].
/// Return identifier of opened [`Modal`].
pub fn opened() -> Option<&'static str> {
// Check if modal is showing.
{
@ -134,6 +140,19 @@ impl Modal {
Some(modal.id)
}
/// Check if [`Modal`] is opened and can be closed.
pub fn opened_closeable() -> bool {
// Check if modal is showing.
{
if MODAL_STATE.read().modal.is_none() {
return false;
}
}
let r_state = MODAL_STATE.read();
let modal = r_state.modal.as_ref().unwrap();
modal.closeable.load(Ordering::Relaxed)
}
/// Set title text for current opened [`Modal`].
pub fn set_title(title: String) {
// Save state.
@ -164,40 +183,43 @@ impl Modal {
let is_fullscreen = ctx.input(|i| {
i.viewport().fullscreen.unwrap_or(false)
});
let is_mac_os = OperatingSystem::from_target_os() == OperatingSystem::Mac;
let mut rect = ctx.screen_rect();
if View::is_desktop() && !is_mac_os {
let margin = if !is_fullscreen {
Content::WINDOW_FRAME_MARGIN
} else {
0.0
};
rect = rect.shrink(margin - 0.5);
rect.min += egui::vec2(0.0, Content::WINDOW_TITLE_HEIGHT + 0.5);
rect.max.x += 0.5;
}
// Setup background rect.
let is_win = OperatingSystem::Windows == OperatingSystem::from_target_os();
let bg_rect = if View::is_desktop() && !is_win {
let mut r = ctx.screen_rect();
let is_mac = OperatingSystem::Mac == OperatingSystem::from_target_os();
if !is_mac && !is_fullscreen {
r = r.shrink(Content::WINDOW_FRAME_MARGIN - 1.0);
}
r.min.y += Content::WINDOW_TITLE_HEIGHT;
r
} else {
ctx.screen_rect()
};
// Draw modal background.
egui::Window::new("modal_bg_window")
.title_bar(false)
.resizable(false)
.collapsible(false)
.fixed_rect(rect)
.fixed_rect(bg_rect)
.frame(egui::Frame {
fill: Colors::semi_transparent(),
..Default::default()
})
.show(ctx, |ui| {
ui.set_min_size(rect.size());
ui.set_min_size(bg_rect.size());
});
// Setup width of modal content.
let side_insets = View::get_left_inset() + View::get_right_inset();
let available_width = rect.width() - (side_insets + Self::DEFAULT_MARGIN);
let available_width = ctx.screen_rect().width() - (side_insets + Self::DEFAULT_MARGIN);
let width = f32::min(available_width, Self::DEFAULT_WIDTH);
// Show main content Window at given position.
let (content_align, content_offset) = self.modal_position(is_fullscreen);
let layer_id = egui::Window::new(format!("modal_window_{}", self.id))
// Show main content window at given position.
let (content_align, content_offset) = self.modal_position();
let layer_id = egui::Window::new("modal_window")
.title_bar(false)
.resizable(false)
.collapsible(false)
@ -212,33 +234,31 @@ impl Modal {
color: egui::Color32::from_black_alpha(32),
},
rounding: Rounding::same(8.0),
fill: Colors::fill(),
..Default::default()
})
.show(ctx, |ui| {
if self.title.is_some() {
self.title_ui(ui);
if let Some(title) = &self.title {
title_ui(title, ui);
}
self.content_ui(ui, add_content);
}).unwrap().response.layer_id;
// Always show main content Window above background Window.
// Always show main content window above background window.
ctx.move_to_top(layer_id);
}
/// Get [`egui::Window`] position based on [`ModalPosition`].
fn modal_position(&self, is_fullscreen: bool) -> (Align2, Vec2) {
fn modal_position(&self) -> (Align2, Vec2) {
let align = match self.position {
ModalPosition::CenterTop => Align2::CENTER_TOP,
ModalPosition::Center => Align2::CENTER_CENTER
};
let x_align = View::get_left_inset() - View::get_right_inset();
let is_mac_os = OperatingSystem::from_target_os() == OperatingSystem::Mac;
let extra_y = if View::is_desktop() && !is_mac_os {
Content::WINDOW_TITLE_HEIGHT + if !is_fullscreen {
let is_mac = OperatingSystem::Mac == OperatingSystem::from_target_os();
let is_win = OperatingSystem::Windows == OperatingSystem::from_target_os();
let extra_y = if View::is_desktop() && !is_win {
Content::WINDOW_TITLE_HEIGHT + if !is_mac {
Content::WINDOW_FRAME_MARGIN
} else {
0.0
@ -246,7 +266,7 @@ impl Modal {
} else {
0.0
};
let y_align = View::get_top_inset() + Self::DEFAULT_MARGIN + extra_y;
let y_align = View::get_top_inset() + Self::DEFAULT_MARGIN / 2.0 + extra_y;
let offset = match self.position {
ModalPosition::CenterTop => Vec2::new(x_align, y_align),
@ -258,80 +278,64 @@ impl Modal {
/// Draw provided content.
fn content_ui(&self, ui: &mut egui::Ui, add_content: impl FnOnce(&mut egui::Ui, &Modal)) {
let mut rect = ui.available_rect_before_wrap();
rect.min += egui::emath::vec2(6.0, 0.0);
rect.max -= egui::emath::vec2(6.0, 0.0);
// Create background shape.
let rounding = if self.title.is_some() {
let mut bg_shape = RectShape::new(rect, if self.title.is_none() {
Rounding::same(8.0)
} else {
Rounding {
nw: 0.0,
ne: 0.0,
sw: 8.0,
se: 8.0,
}
} else {
Rounding::same(8.0)
};
let mut bg_shape = RectShape {
rect,
rounding,
fill: Colors::fill(),
stroke: Stroke::NONE,
blur_width: 0.0,
fill_texture_id: Default::default(),
uv: Rect::ZERO
};
}, Colors::fill(), Stroke::NONE);
let bg_idx = ui.painter().add(bg_shape);
// Draw main content.
let mut content_rect = ui.allocate_ui_at_rect(rect, |ui| {
rect.min += egui::emath::vec2(6.0, 0.0);
rect.max -= egui::emath::vec2(6.0, 0.0);
let resp = ui.allocate_new_ui(UiBuilder::new().max_rect(rect), |ui| {
(add_content)(ui, self);
}).response.rect;
// Setup background shape to be painted behind main content.
content_rect.min -= egui::emath::vec2(6.0, 0.0);
content_rect.max += egui::emath::vec2(6.0, 0.0);
bg_shape.rect = content_rect;
ui.painter().set(bg_idx, bg_shape);
}
/// Draw title content.
fn title_ui(&self, ui: &mut egui::Ui) {
let rect = ui.available_rect_before_wrap();
// Create background shape.
let mut bg_shape = RectShape {
rect,
rounding: Rounding {
nw: 8.0,
ne: 8.0,
sw: 0.0,
se: 0.0,
},
fill: Colors::yellow(),
stroke: Stroke::NONE,
blur_width: 0.0,
fill_texture_id: Default::default(),
uv: Rect::ZERO
};
let bg_idx = ui.painter().add(bg_shape);
// Draw title content.
let title_resp = ui.allocate_ui_at_rect(rect, |ui| {
ui.vertical_centered_justified(|ui| {
ui.add_space(Self::DEFAULT_MARGIN + 1.0);
ui.label(RichText::new(self.title.as_ref().unwrap())
.size(19.0)
.color(Colors::title(true))
);
ui.add_space(Self::DEFAULT_MARGIN);
// Draw line below title.
View::horizontal_line(ui, Colors::item_stroke());
});
}).response;
// Setup background shape to be painted behind title content.
bg_shape.rect = title_resp.rect;
// Setup background size.
let bg_rect = {
let mut r = resp.rect.clone();
r.min -= egui::emath::vec2(6.0, 0.0);
r.max += egui::emath::vec2(6.0, 0.0);
r
};
bg_shape.rect = bg_rect;
ui.painter().set(bg_idx, bg_shape);
}
}
/// Draw title content.
fn title_ui(title: &String, ui: &mut egui::Ui) {
let rect = ui.available_rect_before_wrap();
// Create background shape.
let mut bg_shape = RectShape::new(rect, Rounding {
nw: 8.0,
ne: 8.0,
sw: 0.0,
se: 0.0,
}, Colors::yellow(), Stroke::NONE);
let bg_idx = ui.painter().add(bg_shape);
// Draw title content.
let resp = ui.vertical_centered(|ui| {
ui.add_space(Modal::DEFAULT_MARGIN + 2.0);
ui.label(RichText::new(title)
.size(19.0)
.color(Colors::title(true))
);
ui.add_space(Modal::DEFAULT_MARGIN + 1.0);
// Draw line below title.
View::horizontal_line(ui, Colors::item_stroke());
}).response;
// Setup background size.
bg_shape.rect = resp.rect;
ui.painter().set(bg_idx, bg_shape);
}

View file

@ -16,7 +16,7 @@ use egui::{Align, Layout, RichText, Rounding};
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, X_CIRCLE};
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::platform::PlatformCallbacks;
use crate::gui::views::{Modal, View};
use crate::gui::views::network::modals::ExternalConnectionModal;
@ -36,7 +36,6 @@ pub struct ConnectionsContent {
impl Default for ConnectionsContent {
fn default() -> Self {
ExternalConnection::check_ext_conn_availability(None);
Self {
ext_conn_modal: ExternalConnectionModal::new(None),
modal_ids: vec![
@ -78,7 +77,7 @@ impl ConnectionsContent {
// Check connections availability.
if saved_chain_type != AppConfig::chain_type() {
ExternalConnection::check_ext_conn_availability(None);
ExternalConnection::check(None, ui.ctx());
}
// Show integrated node info content.
@ -103,23 +102,20 @@ impl ConnectionsContent {
ui.add_space(4.0);
let ext_conn_list = ConnectionsConfig::ext_conn_list();
if !ext_conn_list.is_empty() {
let ext_conn_size = ext_conn_list.len();
if ext_conn_size != 0 {
ui.add_space(8.0);
for (index, conn) in ext_conn_list.iter().enumerate() {
ui.horizontal_wrapped(|ui| {
// Draw connection list item.
let len = ext_conn_list.len();
Self::ext_conn_item_ui(ui, conn, index, len, |ui| {
// Draw buttons for non-default connections.
if conn.url != ExternalConnection::DEFAULT_MAIN_URL {
let button_rounding = View::item_rounding(index, len, true);
View::item_button(ui, button_rounding, TRASH, None, || {
ConnectionsConfig::remove_ext_conn(conn.id);
});
View::item_button(ui, Rounding::default(), PENCIL, None, || {
self.show_add_ext_conn_modal(Some(conn.clone()), cb);
});
}
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);
});
View::item_button(ui, Rounding::default(), PENCIL, None, || {
self.show_add_ext_conn_modal(Some(conn.clone()), cb);
});
});
});
}
@ -138,16 +134,17 @@ impl ConnectionsContent {
// Draw custom button.
custom_button(ui);
if !Node::is_running() {
// Draw button to start integrated node.
View::item_button(ui, Rounding::default(), POWER, Some(Colors::green()), || {
Node::start();
});
} else if !Node::is_starting() && !Node::is_stopping() && !Node::is_restarting() {
// Draw button to stop integrated node.
View::item_button(ui, Rounding::default(), POWER, Some(Colors::red()), || {
Node::stop(false);
});
// Draw buttons to start/stop node.
if Node::get_error().is_none() {
if !Node::is_running() {
View::item_button(ui, Rounding::default(), POWER, Some(Colors::green()), || {
Node::start();
});
} else if !Node::is_starting() && !Node::is_stopping() && !Node::is_restarting() {
View::item_button(ui, Rounding::default(), POWER, Some(Colors::red()), || {
Node::stop(false);
});
}
}
let layout_size = ui.available_size();
@ -163,15 +160,22 @@ impl ConnectionsContent {
});
// Setup node status text.
let status_icon = if !Node::is_running() {
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, Node::get_sync_status_text());
ui.label(RichText::new(status_text).size(15.0).color(Colors::text(false)));
let status_text = format!("{} {}", status_icon, if has_error {
t!("error")
} 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.
@ -198,34 +202,32 @@ impl ConnectionsContent {
let item_rounding = View::item_rounding(index, len, false);
ui.painter().rect(bg_rect, item_rounding, Colors::fill(), View::item_stroke());
ui.vertical(|ui| {
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
// Draw provided buttons.
buttons_ui(ui);
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"))
} else {
format!("{} {}", X_CIRCLE, t!("network.not_available"))
}
// Setup connection status text.
let status_text = if let Some(available) = conn.available {
if available {
format!("{} {}", CHECK_CIRCLE, t!("network.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!("{} {}", 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);
});
});
});

View file

@ -12,7 +12,6 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use std::time::Duration;
use egui::{Id, Margin, RichText, ScrollArea};
use egui::scroll_area::ScrollBarVisibility;
@ -22,15 +21,15 @@ use crate::gui::icons::{ARROWS_COUNTER_CLOCKWISE, BRIEFCASE, DATABASE, DOTS_THRE
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Content, TitlePanel, View};
use crate::gui::views::network::{ConnectionsContent, NetworkMetrics, NetworkMining, NetworkNode, NetworkSettings};
use crate::gui::views::network::types::{NetworkTab, NetworkTabType};
use crate::gui::views::types::{TitleContentType, TitleType};
use crate::node::{Node, NodeError};
use crate::gui::views::network::types::{NodeTab, NodeTabType};
use crate::gui::views::types::{LinePosition, TitleContentType, TitleType};
use crate::node::{Node, NodeConfig, NodeError};
use crate::wallet::ExternalConnection;
/// Network content.
pub struct NetworkContent {
/// Current integrated node tab content.
node_tab_content: Box<dyn NetworkTab>,
node_tab_content: Box<dyn NodeTab>,
/// Connections content.
connections: ConnectionsContent,
}
@ -47,14 +46,14 @@ impl Default for NetworkContent {
impl NetworkContent {
pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
let show_connections = AppConfig::show_connections_network_panel();
let dual_panel = Content::is_dual_panel_mode(ui);
let dual_panel = Content::is_dual_panel_mode(ui.ctx());
// Show title panel.
self.title_ui(ui, show_connections);
self.title_ui(ui, dual_panel, show_connections);
// Show integrated node tabs content.
if !show_connections {
egui::TopBottomPanel::bottom("node_tabs_content")
egui::TopBottomPanel::bottom("node_tabs")
.min_height(0.5)
.resizable(false)
.frame(egui::Frame {
@ -68,15 +67,23 @@ impl NetworkContent {
..Default::default()
})
.show_inside(ui, |ui| {
ui.vertical_centered(|ui| {
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
self.tabs_ui(ui);
});
let rect = ui.available_rect_before_wrap();
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
self.tabs_ui(ui);
});
// Draw content divider line.
let r = {
let mut r = rect.clone();
r.min.x -= View::get_left_inset() + View::TAB_ITEMS_PADDING;
r.min.y -= View::TAB_ITEMS_PADDING;
r.max.x += View::far_right_inset_margin(ui) + View::TAB_ITEMS_PADDING;
r
};
View::line(ui, LinePosition::TOP, &r, Colors::stroke());
});
}
// Show current node tab content.
// Show integrated node tab content.
egui::SidePanel::right("node_tab_content")
.resizable(false)
.exact_width(ui.available_width())
@ -86,8 +93,6 @@ impl NetworkContent {
.show_animated_inside(ui, !show_connections, |ui| {
egui::CentralPanel::default()
.frame(egui::Frame {
fill: Colors::white_or_black(false),
stroke: View::item_stroke(),
inner_margin: Margin {
left: View::get_left_inset() + 4.0,
right: View::far_right_inset_margin(ui) + 4.0,
@ -97,14 +102,42 @@ impl NetworkContent {
..Default::default()
})
.show_inside(ui, |ui| {
self.node_tab_content.ui(ui, cb);
let rect = ui.available_rect_before_wrap();
if self.node_tab_content.get_type() != NodeTabType::Settings {
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
let node_err = Node::get_error();
if let Some(err) = node_err {
node_error_ui(ui, err);
} else if !Node::is_running() {
disabled_node_ui(ui);
} else if Node::get_stats().is_none() || Node::is_restarting() ||
Node::is_stopping() {
NetworkContent::loading_ui(ui, None);
} else {
self.node_tab_content.ui(ui, cb);
}
});
} else {
self.node_tab_content.ui(ui, cb);
}
// Draw content divider line.
let r = {
let mut r = rect.clone();
r.min.y -= 3.0;
r.max.x += 4.0;
r.max.y += 4.0;
r
};
if dual_panel {
View::line(ui, LinePosition::RIGHT, &r, Colors::item_stroke());
}
});
});
// Show connections content.
egui::CentralPanel::default()
.frame(egui::Frame {
stroke: View::item_stroke(),
inner_margin: Margin {
left: if show_connections {
View::get_left_inset() + 4.0
@ -117,18 +150,14 @@ impl NetworkContent {
0.0
},
top: 3.0,
bottom: if View::is_desktop() && show_connections {
6.0
} else {
4.0
},
bottom: 4.0 + View::get_bottom_inset(),
},
fill: Colors::button(),
..Default::default()
})
.show_inside(ui, |ui| {
let rect = ui.available_rect_before_wrap();
ScrollArea::vertical()
.id_source("connections_content")
.id_salt("connections_scroll")
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.auto_shrink([false; 2])
.show(ui, |ui| {
@ -144,17 +173,26 @@ impl NetworkContent {
});
});
});
// Draw content divider line.
let r = {
let mut r = rect.clone();
r.min.y -= 3.0;
r.max.x += 4.0;
r.max.y += 4.0 + View::get_bottom_inset();
r
};
if show_connections && dual_panel {
View::line(ui, LinePosition::RIGHT, &r, Colors::item_stroke());
}
});
// Redraw after delay.
if Node::is_running() {
// Redraw after delay if node is running at non-dual-panel mode.
if ((!dual_panel && Content::is_network_panel_open()) || dual_panel) && Node::is_running() {
ui.ctx().request_repaint_after(Node::STATS_UPDATE_DELAY);
} else if show_connections {
ui.ctx().request_repaint_after(Duration::from_millis(1000));
}
}
/// Draw tab buttons in the bottom of the screen.
/// Draw tab buttons at bottom of the screen.
fn tabs_ui(&mut self, ui: &mut egui::Ui) {
ui.vertical_centered(|ui| {
// Setup spacing between tabs.
@ -166,22 +204,22 @@ impl NetworkContent {
let current_type = self.node_tab_content.get_type();
ui.columns(4, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::tab_button(ui, DATABASE, current_type == NetworkTabType::Node, || {
View::tab_button(ui, DATABASE, current_type == NodeTabType::Info, |_| {
self.node_tab_content = Box::new(NetworkNode::default());
});
});
columns[1].vertical_centered_justified(|ui| {
View::tab_button(ui, GAUGE, current_type == NetworkTabType::Metrics, || {
View::tab_button(ui, GAUGE, current_type == NodeTabType::Metrics, |_| {
self.node_tab_content = Box::new(NetworkMetrics::default());
});
});
columns[2].vertical_centered_justified(|ui| {
View::tab_button(ui, FACTORY, current_type == NetworkTabType::Mining, || {
View::tab_button(ui, FACTORY, current_type == NodeTabType::Mining, |_| {
self.node_tab_content = Box::new(NetworkMining::default());
});
});
columns[3].vertical_centered_justified(|ui| {
View::tab_button(ui, FADERS, current_type == NetworkTabType::Settings, || {
View::tab_button(ui, FADERS, current_type == NodeTabType::Settings, |_| {
self.node_tab_content = Box::new(NetworkSettings::default());
});
});
@ -190,29 +228,29 @@ impl NetworkContent {
}
/// Draw title content.
fn title_ui(&mut self, ui: &mut egui::Ui, show_connections: bool) {
fn title_ui(&mut self, ui: &mut egui::Ui, dual_panel: bool, show_connections: bool) {
// Setup values for title panel.
let title_text = self.node_tab_content.get_type().title().to_uppercase();
let title_text = self.node_tab_content.get_type().title();
let subtitle_text = Node::get_sync_status_text();
let not_syncing = Node::not_syncing();
let title_content = if !show_connections {
TitleContentType::WithSubTitle(title_text, subtitle_text, !not_syncing)
} else {
TitleContentType::Title(t!("network.connections").to_uppercase())
TitleContentType::Title(t!("network.connections"))
};
// Draw title panel.
TitlePanel::new(Id::from("network_title_panel")).ui(TitleType::Single(title_content), |ui| {
if !show_connections {
View::title_button_big(ui, DOTS_THREE_OUTLINE_VERTICAL, |_| {
View::title_button_big(ui, DOTS_THREE_OUTLINE_VERTICAL, |ui| {
AppConfig::toggle_show_connections_network_panel();
if AppConfig::show_connections_network_panel() {
ExternalConnection::check_ext_conn_availability(None);
ExternalConnection::check(None, ui.ctx());
}
});
}
}, |ui| {
if !Content::is_dual_panel_mode(ui) {
if !dual_panel {
View::title_button_big(ui, BRIEFCASE, |_| {
Content::toggle_network_panel();
});
@ -220,23 +258,6 @@ impl NetworkContent {
}, ui);
}
/// Content to draw when node is disabled.
pub fn disabled_node_ui(ui: &mut egui::Ui) {
View::center_content(ui, 156.0, |ui| {
let text = t!("network.disabled_server", "dots" => DOTS_THREE_OUTLINE_VERTICAL);
ui.label(RichText::new(text)
.size(16.0)
.color(Colors::inactive_text())
);
ui.add_space(8.0);
View::action_button(ui, format!("{} {}", POWER, t!("network.enable_node")), || {
Node::start();
});
ui.add_space(2.0);
Self::autorun_node_ui(ui);
});
}
/// Content to draw on loading.
pub fn loading_ui(ui: &mut egui::Ui, text: Option<String>) {
match text {
@ -265,73 +286,98 @@ impl NetworkContent {
AppConfig::toggle_node_autostart();
});
}
}
/// Draw integrated node error content.
pub fn node_error_ui(ui: &mut egui::Ui, e: NodeError) {
match e {
NodeError::Storage => {
View::center_content(ui, 156.0, |ui| {
ui.label(RichText::new(t!("network_node.error_clean"))
.size(16.0)
.color(Colors::red())
);
ui.add_space(8.0);
let btn_txt = format!("{} {}",
ARROWS_COUNTER_CLOCKWISE,
t!("network_node.resync"));
View::action_button(ui, btn_txt, || {
Node::clean_up_data();
Node::start();
});
ui.add_space(2.0);
/// Content to draw when node is disabled.
fn disabled_node_ui(ui: &mut egui::Ui) {
View::center_content(ui, 156.0, |ui| {
let text = t!("network.disabled_server", "dots" => DOTS_THREE_OUTLINE_VERTICAL);
ui.label(RichText::new(text)
.size(16.0)
.color(Colors::inactive_text())
);
ui.add_space(8.0);
View::action_button(ui, format!("{} {}", POWER, t!("network.enable_node")), || {
Node::start();
});
ui.add_space(2.0);
NetworkContent::autorun_node_ui(ui);
});
}
/// Draw integrated node error content.
pub fn node_error_ui(ui: &mut egui::Ui, e: NodeError) {
match e {
NodeError::Storage => {
View::center_content(ui, 156.0, |ui| {
ui.label(RichText::new(t!("network_node.error_clean"))
.size(16.0)
.color(Colors::red())
);
ui.add_space(8.0);
let btn_txt = format!("{} {}",
ARROWS_COUNTER_CLOCKWISE,
t!("network_node.resync"));
View::action_button(ui, btn_txt, || {
Node::clean_up_data();
Node::start();
});
return;
}
NodeError::P2P | NodeError::API => {
let msg_type = match e {
NodeError::API => "API",
_ => "P2P"
};
View::center_content(ui, 106.0, |ui| {
let text = t!(
ui.add_space(2.0);
});
return;
}
NodeError::P2P | NodeError::API => {
let msg_type = match e {
NodeError::API => "API",
_ => "P2P"
};
View::center_content(ui, 106.0, |ui| {
let text = t!(
"network_node.error_p2p_api",
"p2p_api" => msg_type,
"settings" => FADERS
);
ui.label(RichText::new(text)
.size(16.0)
.color(Colors::red())
);
ui.add_space(2.0);
ui.label(RichText::new(text)
.size(16.0)
.color(Colors::red())
);
ui.add_space(2.0);
});
return;
}
NodeError::Configuration => {
View::center_content(ui, 106.0, |ui| {
ui.label(RichText::new(t!("network_node.error_config", "settings" => FADERS))
.size(16.0)
.color(Colors::red())
);
ui.add_space(8.0);
let btn_txt = format!("{} {}",
ARROWS_COUNTER_CLOCKWISE,
t!("network_settings.reset"));
View::action_button(ui, btn_txt, || {
NodeConfig::reset_to_default();
Node::start();
});
return;
}
NodeError::Configuration => {
View::center_content(ui, 106.0, |ui| {
ui.label(RichText::new(t!("network_node.error_config", "settings" => FADERS))
.size(16.0)
.color(Colors::red())
);
ui.add_space(2.0);
ui.add_space(2.0);
});
}
NodeError::Unknown => {
View::center_content(ui, 156.0, |ui| {
ui.label(RichText::new(t!("network_node.error_unknown", "settings" => FADERS))
.size(16.0)
.color(Colors::red())
);
ui.add_space(8.0);
let btn_txt = format!("{} {}",
ARROWS_COUNTER_CLOCKWISE,
t!("network_node.resync"));
View::action_button(ui, btn_txt, || {
Node::clean_up_data();
Node::start();
});
}
NodeError::Unknown => {
View::center_content(ui, 156.0, |ui| {
ui.label(RichText::new(t!("network_node.error_unknown", "settings" => FADERS))
.size(16.0)
.color(Colors::red())
);
ui.add_space(8.0);
let btn_txt = format!("{} {}",
ARROWS_COUNTER_CLOCKWISE,
t!("network_node.resync"));
View::action_button(ui, btn_txt, || {
Node::clean_up_data();
Node::start();
});
ui.add_space(2.0);
});
}
ui.add_space(2.0);
});
}
}
}

View file

@ -14,6 +14,7 @@
use egui::{RichText, Rounding, ScrollArea, vec2};
use egui::scroll_area::ScrollBarVisibility;
use grin_core::consensus::{DAY_HEIGHT, GRIN_BASE, HOUR_SEC, REWARD};
use grin_servers::{DiffBlock, ServerStats};
use crate::gui::Colors;
@ -21,90 +22,64 @@ use crate::gui::icons::{AT, COINS, CUBE_TRANSPARENT, HOURGLASS_LOW, HOURGLASS_ME
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Content, View};
use crate::gui::views::network::NetworkContent;
use crate::gui::views::network::types::{NetworkTab, NetworkTabType};
use crate::gui::views::network::types::{NodeTab, NodeTabType};
use crate::node::Node;
/// Chain metrics tab content.
#[derive(Default)]
pub struct NetworkMetrics;
const BLOCK_REWARD: f64 = 60.0;
// 1 year is calculated as 365 days and 6 hours (31557600).
const YEARLY_SUPPLY: f64 = ((60 * 60 * 24 * 365) + 6 * 60 * 60) as f64;
const BLOCK_REWARD: u64 = REWARD / GRIN_BASE;
// 1 year as 365 days and 6 hours (31557600).
const YEARLY_SUPPLY: u64 = (BLOCK_REWARD * DAY_HEIGHT * 365) + 6 * HOUR_SEC;
impl NetworkTab for NetworkMetrics {
fn get_type(&self) -> NetworkTabType {
NetworkTabType::Metrics
impl NodeTab for NetworkMetrics {
fn get_type(&self) -> NodeTabType {
NodeTabType::Metrics
}
fn ui(&mut self, ui: &mut egui::Ui, _: &dyn PlatformCallbacks) {
// Show an error content when available.
let node_err = Node::get_error();
if node_err.is_some() {
NetworkContent::node_error_ui(ui, node_err.unwrap());
return;
}
// Show message to enable node when it's not running.
if !Node::is_running() {
NetworkContent::disabled_node_ui(ui);
return;
}
// Show loading spinner when node is stopping.
if Node::is_stopping() {
NetworkContent::loading_ui(ui, None);
return;
}
// Show message when metrics are not available.
let server_stats = Node::get_stats();
if server_stats.is_none() || Node::is_restarting()
|| server_stats.as_ref().unwrap().diff_stats.height == 0 {
let stats = server_stats.as_ref().unwrap();
if stats.diff_stats.height == 0 {
NetworkContent::loading_ui(ui, Some(t!("network_metrics.loading")));
return;
}
ui.add_space(1.0);
ui.vertical_centered(|ui| {
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
let stats = server_stats.as_ref().unwrap();
// Show emission and difficulty info.
info_ui(ui, stats);
// Show difficulty adjustment window blocks.
blocks_ui(ui, stats);
});
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
// Show emission and difficulty info.
info_ui(ui, stats);
// Show difficulty adjustment window blocks.
blocks_ui(ui, stats);
});
}
}
const BLOCK_ITEM_HEIGHT: f32 = 78.0;
/// Draw emission and difficulty info.
fn info_ui(ui: &mut egui::Ui, stats: &ServerStats) {
// Show emission info.
View::sub_title(ui, format!("{} {}", COINS, t!("network_metrics.emission")));
ui.columns(3, |columns| {
let supply = stats.header_stats.height as f64 * BLOCK_REWARD;
let rate = (YEARLY_SUPPLY * 100.0) / supply;
let supply = stats.header_stats.height * BLOCK_REWARD;
let rate = (YEARLY_SUPPLY * 100) / supply;
columns[0].vertical_centered(|ui| {
View::rounded_box(ui,
format!("{}", BLOCK_REWARD),
t!("network_metrics.reward"),
[true, false, true, false]);
View::label_box(ui,
format!("{}", BLOCK_REWARD),
t!("network_metrics.reward"),
[true, false, true, false]);
});
columns[1].vertical_centered(|ui| {
View::rounded_box(ui,
format!("{:.2}%", rate),
t!("network_metrics.inflation"),
[false, false, false, false]);
View::label_box(ui,
format!("{:.2}%", rate),
t!("network_metrics.inflation"),
[false, false, false, false]);
});
columns[2].vertical_centered(|ui| {
View::rounded_box(ui,
supply.to_string(),
t!("network_metrics.supply"),
[false, true, false, true]);
View::label_box(ui,
supply.to_string(),
t!("network_metrics.supply"),
[false, true, false, true]);
});
});
ui.add_space(5.0);
@ -117,32 +92,34 @@ fn info_ui(ui: &mut egui::Ui, stats: &ServerStats) {
View::sub_title(ui, format!("{} {}", HOURGLASS_MEDIUM, difficulty_title));
ui.columns(3, |columns| {
columns[0].vertical_centered(|ui| {
View::rounded_box(ui,
stats.diff_stats.height.to_string(),
t!("network_node.height"),
[true, false, true, false]);
View::label_box(ui,
stats.diff_stats.height.to_string(),
t!("network_node.height"),
[true, false, true, false]);
});
columns[1].vertical_centered(|ui| {
View::rounded_box(ui,
format!("{}s", stats.diff_stats.average_block_time),
t!("network_metrics.block_time"),
[false, false, false, false]);
View::label_box(ui,
format!("{}s", stats.diff_stats.average_block_time),
t!("network_metrics.block_time"),
[false, false, false, false]);
});
columns[2].vertical_centered(|ui| {
View::rounded_box(ui,
stats.diff_stats.average_difficulty.to_string(),
t!("network_node.difficulty"),
[false, true, false, true]);
View::label_box(ui,
stats.diff_stats.average_difficulty.to_string(),
t!("network_node.difficulty"),
[false, true, false, true]);
});
});
}
const BLOCK_ITEM_HEIGHT: f32 = 77.0;
/// Draw difficulty adjustment window blocks content.
fn blocks_ui(ui: &mut egui::Ui, stats: &ServerStats) {
let blocks_size = stats.diff_stats.last_blocks.len();
ui.add_space(4.0);
ScrollArea::vertical()
.id_source("difficulty_scroll")
.id_salt("mining_difficulty_scroll")
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.auto_shrink([false; 2])
.stick_to_bottom(true)
@ -151,11 +128,8 @@ fn blocks_ui(ui: &mut egui::Ui, stats: &ServerStats) {
BLOCK_ITEM_HEIGHT,
blocks_size,
|ui, row_range| {
ui.add_space(4.0);
for index in row_range {
// Add space before the first item.
if index == 0 {
ui.add_space(4.0);
}
let db = stats.diff_stats.last_blocks.get(index).unwrap();
block_item_ui(ui, db, View::item_rounding(index, blocks_size, false));
}
@ -167,11 +141,11 @@ fn blocks_ui(ui: &mut egui::Ui, stats: &ServerStats) {
fn block_item_ui(ui: &mut egui::Ui, db: &DiffBlock, rounding: Rounding) {
let mut rect = ui.available_rect_before_wrap();
rect.set_height(BLOCK_ITEM_HEIGHT);
ui.allocate_ui_at_rect(rect, |ui| {
ui.allocate_ui(rect.size(), |ui| {
ui.horizontal(|ui| {
ui.add_space(6.0);
ui.vertical(|ui| {
ui.add_space(3.0);
ui.add_space(4.0);
// Draw round background.
rect.min += vec2(8.0, 0.0);
@ -180,24 +154,26 @@ fn block_item_ui(ui: &mut egui::Ui, db: &DiffBlock, rounding: Rounding) {
// Draw block hash.
ui.horizontal(|ui| {
ui.add_space(7.0);
ui.add_space(8.0);
ui.label(RichText::new(db.block_hash.to_string())
.color(Colors::white_or_black(true))
.size(17.0));
});
// Draw block difficulty and height.
ui.horizontal(|ui| {
ui.add_space(6.0);
ui.add_space(7.0);
let diff_text = format!("{} {} {} {}",
CUBE_TRANSPARENT,
db.difficulty,
AT,
db.block_height);
ui.label(RichText::new(diff_text).color(Colors::title(false)).size(16.0));
ui.label(RichText::new(diff_text)
.color(Colors::title(false))
.size(15.0));
});
// Draw block date.
ui.horizontal(|ui| {
ui.add_space(6.0);
ui.add_space(7.0);
let block_time = View::format_time(db.time as i64);
ui.label(RichText::new(format!("{} {}s {} {}",
TIMER,
@ -205,7 +181,7 @@ fn block_item_ui(ui: &mut egui::Ui, db: &DiffBlock, rounding: Rounding) {
HOURGLASS_LOW,
block_time))
.color(Colors::gray())
.size(16.0));
.size(15.0));
});
ui.add_space(3.0);
});

View file

@ -23,62 +23,30 @@ use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Content, View};
use crate::gui::views::network::NetworkContent;
use crate::gui::views::network::setup::StratumSetup;
use crate::gui::views::network::types::{NetworkTab, NetworkTabType};
use crate::gui::views::network::types::{NodeTab, NodeTabType};
use crate::node::{Node, NodeConfig};
use crate::wallet::WalletConfig;
/// Mining tab content.
pub struct NetworkMining {
/// Stratum server setup content.
stratum_server_setup: StratumSetup,
/// Wallet name for rewards.
wallet_name: String,
}
impl Default for NetworkMining {
fn default() -> Self {
let wallet_name = if let Some(id) = NodeConfig::get_stratum_wallet_id() {
WalletConfig::name_by_id(id).unwrap_or("-".to_string())
} else {
"-".to_string()
};
Self {
stratum_server_setup: StratumSetup::default(),
wallet_name,
}
}
}
impl NetworkTab for NetworkMining {
fn get_type(&self) -> NetworkTabType {
NetworkTabType::Mining
impl NodeTab for NetworkMining {
fn get_type(&self) -> NodeTabType {
NodeTabType::Mining
}
fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
// Show an error content when available.
let node_err = Node::get_error();
if node_err.is_some() {
NetworkContent::node_error_ui(ui, node_err.unwrap());
return;
}
// Show message to enable node when it's not running.
if !Node::is_running() {
NetworkContent::disabled_node_ui(ui);
return;
}
// Show loading spinner when node is stopping or stratum server is starting.
if Node::is_stopping() || Node::is_stratum_starting() {
NetworkContent::loading_ui(ui, None);
return;
}
// Show message when mining is not available.
let server_stats = Node::get_stats();
if server_stats.is_none() || Node::is_restarting()
|| Node::get_sync_status().unwrap() != SyncStatus::NoSync {
if Node::is_stratum_starting() || Node::get_sync_status().unwrap() != SyncStatus::NoSync {
NetworkContent::loading_ui(ui, Some(t!("network_mining.loading")));
return;
}
@ -87,15 +55,13 @@ impl NetworkTab for NetworkMining {
let stratum_stats = Node::get_stratum_stats();
if !stratum_stats.is_running {
ScrollArea::vertical()
.id_source("stratum_setup_scroll")
.id_salt("stratum_setup_scroll")
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.auto_shrink([false; 2])
.show(ui, |ui| {
ui.add_space(1.0);
ui.vertical_centered(|ui| {
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
self.stratum_server_setup.ui(ui, cb);
});
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
self.stratum_server_setup.ui(ui, cb);
});
});
return;
@ -108,16 +74,19 @@ impl NetworkTab for NetworkMining {
ui.columns(2, |columns| {
columns[0].vertical_centered(|ui| {
let (stratum_addr, stratum_port) = NodeConfig::get_stratum_address();
View::rounded_box(ui,
format!("{}:{}", stratum_addr, stratum_port),
t!("network_mining.address"),
[true, false, true, false]);
View::label_box(ui,
format!("{}:{}", stratum_addr, stratum_port),
t!("network_mining.address"),
[true, false, true, false]);
});
columns[1].vertical_centered(|ui| {
View::rounded_box(ui,
self.wallet_name.clone(),
t!("network_mining.rewards_wallet"),
[false, true, false, true]);
View::label_box(ui,
self.stratum_server_setup
.wallet_name
.clone()
.unwrap_or("-".to_string()),
t!("network_mining.rewards_wallet"),
[false, true, false, true]);
});
});
ui.add_space(4.0);
@ -131,10 +100,10 @@ impl NetworkTab for NetworkMining {
} else {
"-".into()
};
View::rounded_box(ui,
difficulty,
t!("network_node.difficulty"),
[true, false, true, false]);
View::label_box(ui,
difficulty,
t!("network_node.difficulty"),
[true, false, true, false]);
});
columns[1].vertical_centered(|ui| {
let block_height = if stratum_stats.block_height > 0 {
@ -142,10 +111,10 @@ impl NetworkTab for NetworkMining {
} else {
"-".into()
};
View::rounded_box(ui,
block_height,
t!("network_node.header"),
[false, false, false, false]);
View::label_box(ui,
block_height,
t!("network_node.header"),
[false, false, false, false]);
});
columns[2].vertical_centered(|ui| {
let hashrate = if stratum_stats.network_hashrate > 0.0 {
@ -153,10 +122,10 @@ impl NetworkTab for NetworkMining {
} else {
"-".into()
};
View::rounded_box(ui,
hashrate,
t!("network_mining.hashrate", "bits" => stratum_stats.edge_bits),
[false, true, false, true]);
View::label_box(ui,
hashrate,
t!("network_mining.hashrate", "bits" => stratum_stats.edge_bits),
[false, true, false, true]);
});
});
ui.add_space(4.0);
@ -165,17 +134,17 @@ impl NetworkTab for NetworkMining {
View::sub_title(ui, format!("{} {}", CPU, t!("network_mining.miners")));
ui.columns(2, |columns| {
columns[0].vertical_centered(|ui| {
View::rounded_box(ui,
stratum_stats.num_workers.to_string(),
t!("network_mining.devices"),
[true, false, true, false]);
View::label_box(ui,
stratum_stats.num_workers.to_string(),
t!("network_mining.devices"),
[true, false, true, false]);
});
columns[1].vertical_centered(|ui| {
View::rounded_box(ui,
stratum_stats.blocks_found.to_string(),
t!("network_mining.blocks_found"),
[false, true, false, true]);
View::label_box(ui,
stratum_stats.blocks_found.to_string(),
t!("network_mining.blocks_found"),
[false, true, false, true]);
});
});
ui.add_space(4.0);
@ -187,7 +156,7 @@ impl NetworkTab for NetworkMining {
View::horizontal_line(ui, Colors::item_stroke());
ui.add_space(4.0);
ScrollArea::vertical()
.id_source("stratum_workers_scroll")
.id_salt("stratum_workers_scroll")
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.auto_shrink([false; 2])
.show_rows(

View file

@ -119,7 +119,7 @@ impl ExternalConnectionModal {
});
columns[1].vertical_centered_justified(|ui| {
// Add connection button callback.
let mut on_add = || {
let mut on_add = |ui: &mut egui::Ui| {
if !self.ext_node_url_edit.starts_with("http") {
self.ext_node_url_edit = format!("http://{}", self.ext_node_url_edit)
}
@ -139,7 +139,7 @@ impl ExternalConnectionModal {
ext_conn.id = id;
}
ConnectionsConfig::add_ext_conn(ext_conn.clone());
ExternalConnection::check_ext_conn_availability(Some(ext_conn.id));
ExternalConnection::check(Some(ext_conn.id), ui.ctx());
on_save(ext_conn);
// Close modal.
@ -150,10 +150,17 @@ impl ExternalConnectionModal {
modal.close();
}
};
// Handle Enter key press.
let mut enter = false;
View::on_enter_key(ui, || {
(on_add)();
enter = true;
});
View::button(ui, if self.ext_conn_id.is_some() {
if enter {
(on_add)(ui);
}
View::button_ui(ui, if self.ext_conn_id.is_some() {
t!("modal.save")
} else {
t!("modal.add")

View file

@ -20,51 +20,28 @@ use crate::gui::Colors;
use crate::gui::icons::{AT, CUBE, DEVICES, FLOW_ARROW, HANDSHAKE, PACKAGE, SHARE_NETWORK};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Content, View};
use crate::gui::views::network::NetworkContent;
use crate::gui::views::network::types::{NetworkTab, NetworkTabType};
use crate::gui::views::network::types::{NodeTab, NodeTabType};
use crate::node::{Node, NodeConfig};
/// Integrated node tab content.
#[derive(Default)]
pub struct NetworkNode;
impl NetworkTab for NetworkNode {
fn get_type(&self) -> NetworkTabType {
NetworkTabType::Node
impl NodeTab for NetworkNode {
fn get_type(&self) -> NodeTabType {
NodeTabType::Info
}
fn ui(&mut self, ui: &mut egui::Ui, _: &dyn PlatformCallbacks) {
// Show an error content when available.
let node_err = Node::get_error();
if node_err.is_some() {
NetworkContent::node_error_ui(ui, node_err.unwrap());
return;
}
// Show message to enable node when it's not running.
if !Node::is_running() {
NetworkContent::disabled_node_ui(ui);
return;
}
// Show loading spinner when stats are not available.
let server_stats = Node::get_stats();
if server_stats.is_none() || Node::is_restarting() || Node::is_stopping() {
NetworkContent::loading_ui(ui, None);
return;
}
ScrollArea::vertical()
.id_source("integrated_node")
.id_salt("integrated_node_info_scroll")
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.auto_shrink([false; 2])
.show(ui, |ui| {
ui.add_space(2.0);
ui.vertical_centered(|ui| {
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
// Show node stats content.
node_stats_ui(ui);
});
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
// Show node stats content.
node_stats_ui(ui);
});
});
}
@ -79,32 +56,32 @@ fn node_stats_ui(ui: &mut egui::Ui) {
View::sub_title(ui, format!("{} {}", FLOW_ARROW, t!("network_node.header")));
ui.columns(2, |columns| {
columns[0].vertical_centered(|ui| {
View::rounded_box(ui,
stats.header_stats.last_block_h.to_string(),
t!("network_node.hash"),
[true, false, false, false]);
View::label_box(ui,
stats.header_stats.last_block_h.to_string(),
t!("network_node.hash"),
[true, false, false, false]);
});
columns[1].vertical_centered(|ui| {
View::rounded_box(ui,
stats.header_stats.height.to_string(),
t!("network_node.height"),
[false, true, false, false]);
View::label_box(ui,
stats.header_stats.height.to_string(),
t!("network_node.height"),
[false, true, false, false]);
});
});
ui.columns(2, |columns| {
columns[0].vertical_centered(|ui| {
View::rounded_box(ui,
stats.header_stats.total_difficulty.to_string(),
t!("network_node.difficulty"),
[false, false, true, false]);
View::label_box(ui,
stats.header_stats.total_difficulty.to_string(),
t!("network_node.difficulty"),
[false, false, true, false]);
});
columns[1].vertical_centered(|ui| {
let h_ts = stats.header_stats.latest_timestamp.timestamp();
let h_time = View::format_time(h_ts);
View::rounded_box(ui,
h_time,
t!("network_node.time"),
[false, false, false, true]);
View::label_box(ui,
h_time,
t!("network_node.time"),
[false, false, false, true]);
});
});
ui.add_space(5.0);
@ -113,32 +90,32 @@ fn node_stats_ui(ui: &mut egui::Ui) {
View::sub_title(ui, format!("{} {}", CUBE, t!("network_node.block")));
ui.columns(2, |columns| {
columns[0].vertical_centered(|ui| {
View::rounded_box(ui,
stats.chain_stats.last_block_h.to_string(),
t!("network_node.hash"),
[true, false, false, false]);
View::label_box(ui,
stats.chain_stats.last_block_h.to_string(),
t!("network_node.hash"),
[true, false, false, false]);
});
columns[1].vertical_centered(|ui| {
View::rounded_box(ui,
stats.chain_stats.height.to_string(),
t!("network_node.height"),
[false, true, false, false]);
View::label_box(ui,
stats.chain_stats.height.to_string(),
t!("network_node.height"),
[false, true, false, false]);
});
});
ui.columns(2, |columns| {
columns[0].vertical_centered(|ui| {
View::rounded_box(ui,
stats.chain_stats.total_difficulty.to_string(),
t!("network_node.difficulty"),
[false, false, true, false]);
View::label_box(ui,
stats.chain_stats.total_difficulty.to_string(),
t!("network_node.difficulty"),
[false, false, true, false]);
});
columns[1].vertical_centered(|ui| {
let b_ts = stats.chain_stats.latest_timestamp.timestamp();
let b_time = View::format_time(b_ts);
View::rounded_box(ui,
b_time,
t!("network_node.time"),
[false, false, false, true]);
View::label_box(ui,
b_time,
t!("network_node.time"),
[false, false, false, true]);
});
});
ui.add_space(5.0);
@ -151,10 +128,10 @@ fn node_stats_ui(ui: &mut egui::Ui) {
None => "0 (0)".to_string(),
Some(tx) => format!("{} ({})", tx.tx_pool_size, tx.tx_pool_kernels)
};
View::rounded_box(ui,
tx_stat,
t!("network_node.main_pool"),
[true, false, false, false]);
View::label_box(ui,
tx_stat,
t!("network_node.main_pool"),
[true, false, false, false]);
});
columns[1].vertical_centered(|ui| {
let stem_tx_stat = match &stats.tx_stats {
@ -163,24 +140,24 @@ fn node_stats_ui(ui: &mut egui::Ui) {
stx.stem_pool_size,
stx.stem_pool_kernels)
};
View::rounded_box(ui,
stem_tx_stat,
t!("network_node.stem_pool"),
[false, true, false, false]);
View::label_box(ui,
stem_tx_stat,
t!("network_node.stem_pool"),
[false, true, false, false]);
});
});
ui.columns(2, |columns| {
columns[0].vertical_centered(|ui| {
View::rounded_box(ui,
stats.disk_usage_gb.to_string(),
t!("network_node.size"),
[false, false, true, false]);
View::label_box(ui,
stats.disk_usage_gb.to_string(),
t!("network_node.size"),
[false, false, true, false]);
});
columns[1].vertical_centered(|ui| {
let peers_txt = format!("{} ({})",
stats.peer_count,
NodeConfig::get_max_outbound_peers());
View::rounded_box(ui, peers_txt, t!("network_node.peers"), [false, false, false, true]);
View::label_box(ui, peers_txt, t!("network_node.peers"), [false, false, false, true]);
});
});
ui.add_space(5.0);
@ -196,23 +173,27 @@ fn node_stats_ui(ui: &mut egui::Ui) {
}
}
const PEER_ITEM_HEIGHT: f32 = 77.0;
/// Draw connected peer info item.
fn peer_item_ui(ui: &mut egui::Ui, peer: &PeerStats, rounding: Rounding) {
let mut rect = ui.available_rect_before_wrap();
rect.set_height(79.0);
ui.allocate_ui_at_rect(rect, |ui| {
rect.set_height(PEER_ITEM_HEIGHT);
ui.allocate_ui(rect.size(), |ui| {
ui.vertical(|ui| {
ui.add_space(4.0);
// Draw round background.
ui.painter().rect(rect, rounding, Colors::white_or_black(false), View::item_stroke());
ui.painter().rect(rect, rounding, Colors::fill_lite(), View::item_stroke());
// Draw peer address
// Draw IP address.
ui.horizontal(|ui| {
ui.add_space(7.0);
ui.label(RichText::new(&peer.addr).color(Colors::white_or_black(true)).size(17.0));
ui.label(RichText::new(&peer.addr)
.color(Colors::white_or_black(true))
.size(17.0));
});
// Draw peer difficulty and height
// Draw difficulty and height.
ui.horizontal(|ui| {
ui.add_space(6.0);
let diff_text = format!("{} {} {} {}",
@ -220,13 +201,17 @@ fn peer_item_ui(ui: &mut egui::Ui, peer: &PeerStats, rounding: Rounding) {
peer.total_difficulty,
AT,
peer.height);
ui.label(RichText::new(diff_text).color(Colors::title(false)).size(16.0));
ui.label(RichText::new(diff_text)
.color(Colors::title(false))
.size(15.0));
});
// Draw peer user-agent
// Draw user-agent.
ui.horizontal(|ui| {
ui.add_space(6.0);
let agent_text = format!("{} {}", DEVICES, &peer.user_agent);
ui.label(RichText::new(agent_text).color(Colors::gray()).size(16.0));
ui.label(RichText::new(agent_text)
.color(Colors::gray())
.size(15.0));
});
ui.add_space(3.0);

View file

@ -20,7 +20,7 @@ use crate::gui::icons::ARROW_COUNTER_CLOCKWISE;
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Modal, Content, View};
use crate::gui::views::network::setup::{DandelionSetup, NodeSetup, P2PSetup, PoolSetup, StratumSetup};
use crate::gui::views::network::types::{NetworkTab, NetworkTabType};
use crate::gui::views::network::types::{NodeTab, NodeTabType};
use crate::gui::views::types::{ModalContainer, ModalPosition};
use crate::node::{Node, NodeConfig};
@ -42,7 +42,7 @@ pub struct NetworkSettings {
}
/// Identifier for settings reset confirmation [`Modal`].
pub const RESET_SETTINGS_MODAL: &'static str = "reset_settings";
pub const RESET_SETTINGS_CONFIRMATION_MODAL: &'static str = "reset_settings_confirmation";
impl Default for NetworkSettings {
fn default() -> Self {
@ -53,7 +53,7 @@ impl Default for NetworkSettings {
pool: PoolSetup::default(),
dandelion: DandelionSetup::default(),
modal_ids: vec![
RESET_SETTINGS_MODAL
RESET_SETTINGS_CONFIRMATION_MODAL
]
}
}
@ -69,15 +69,15 @@ impl ModalContainer for NetworkSettings {
modal: &Modal,
_: &dyn PlatformCallbacks) {
match modal.id {
RESET_SETTINGS_MODAL => reset_settings_confirmation_modal(ui, modal),
RESET_SETTINGS_CONFIRMATION_MODAL => reset_settings_confirmation_modal(ui, modal),
_ => {}
}
}
}
impl NetworkTab for NetworkSettings {
fn get_type(&self) -> NetworkTabType {
NetworkTabType::Settings
impl NodeTab for NetworkSettings {
fn get_type(&self) -> NodeTabType {
NodeTabType::Settings
}
fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
@ -85,7 +85,7 @@ impl NetworkTab for NetworkSettings {
self.current_modal_ui(ui, cb);
ScrollArea::vertical()
.id_source("network_settings")
.id_salt("node_settings_scroll")
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.auto_shrink([false; 2])
.show(ui, |ui| {
@ -210,9 +210,9 @@ fn reset_settings_ui(ui: &mut egui::Ui) {
t!("network_settings.reset_settings"));
View::action_button(ui, button_text, || {
// Show modal to confirm settings reset.
Modal::new(RESET_SETTINGS_MODAL)
Modal::new(RESET_SETTINGS_CONFIRMATION_MODAL)
.position(ModalPosition::Center)
.title(t!("modal.confirmation"))
.title(t!("confirmation"))
.show();
});

View file

@ -141,7 +141,7 @@ impl DandelionSetup {
ui.add_space(6.0);
let epoch = NodeConfig::get_dandelion_epoch();
View::button(ui, format!("{} {}", WATCH, epoch.clone()), Colors::button(), || {
View::button(ui, format!("{} {}", WATCH, &epoch), Colors::white_or_black(false), || {
// Setup values for modal.
self.epoch_edit = epoch;
// Show epoch setup modal.
@ -193,6 +193,11 @@ impl DandelionSetup {
}
};
// Continue on Enter key press.
View::on_enter_key(ui, || {
on_save();
});
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
@ -218,8 +223,7 @@ impl DandelionSetup {
ui.add_space(6.0);
let embargo = NodeConfig::get_dandelion_embargo();
View::button(ui, format!("{} {}", TIMER, embargo.clone()), Colors::button(), || {
// Setup values for modal.
View::button(ui, format!("{} {}", TIMER, &embargo), Colors::white_or_black(false), || {
self.embargo_edit = embargo;
// Show embargo setup modal.
Modal::new(EMBARGO_MODAL)
@ -270,6 +274,11 @@ impl DandelionSetup {
}
};
// Continue on Enter key press.
View::on_enter_key(ui, || {
on_save();
});
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
@ -294,10 +303,10 @@ impl DandelionSetup {
);
ui.add_space(6.0);
let agg = NodeConfig::get_dandelion_aggregation();
View::button(ui, format!("{} {}", CLOCK_COUNTDOWN, agg.clone()), Colors::button(), || {
let ag = NodeConfig::get_dandelion_aggregation();
View::button(ui, format!("{} {}", CLOCK_COUNTDOWN, &ag), Colors::white_or_black(false), || {
// Setup values for modal.
self.aggregation_edit = agg;
self.aggregation_edit = ag;
// Show aggregation setup modal.
Modal::new(AGGREGATION_MODAL)
.position(ModalPosition::CenterTop)
@ -347,6 +356,11 @@ impl DandelionSetup {
}
};
// Continue on Enter key press.
View::on_enter_key(ui, || {
on_save();
});
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
@ -372,7 +386,7 @@ impl DandelionSetup {
ui.add_space(6.0);
let stem_prob = NodeConfig::get_stem_probability();
View::button(ui, format!("{}%", stem_prob.clone()), Colors::button(), || {
View::button(ui, format!("{}%", &stem_prob), Colors::white_or_black(false), || {
// Setup values for modal.
self.stem_prob_edit = stem_prob;
// Show stem probability setup modal.
@ -424,6 +438,11 @@ impl DandelionSetup {
}
};
// Continue on Enter key press.
View::on_enter_key(ui, || {
on_save();
});
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {

View file

@ -255,7 +255,7 @@ impl NodeSetup {
ui.add_space(6.0);
let (_, port) = NodeConfig::get_api_ip_port();
View::button(ui, format!("{} {}", PLUG, port.clone()), Colors::button(), || {
View::button(ui, format!("{} {}", PLUG, &port), Colors::white_or_black(false), || {
// Setup values for modal.
self.api_port_edit = port;
self.api_port_available_edit = self.is_api_port_available;
@ -283,7 +283,9 @@ impl NodeSetup {
fn api_port_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("network_settings.api_port")).size(17.0).color(Colors::gray()));
ui.label(RichText::new(t!("network_settings.api_port"))
.size(17.0)
.color(Colors::gray()));
ui.add_space(6.0);
// Draw API port text edit.
@ -308,7 +310,7 @@ impl NodeSetup {
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
// Save button callback.
let on_save = || {
let mut on_save = || {
// Check if port is available.
let (api_ip, _) = NodeConfig::get_api_ip_port();
let available = NodeConfig::is_api_port_available(&api_ip, &self.api_port_edit);
@ -328,6 +330,11 @@ impl NodeSetup {
}
};
// Continue on Enter key press.
View::on_enter_key(ui, || {
on_save();
});
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
@ -356,8 +363,8 @@ impl NodeSetup {
ui.add_space(6.0);
let secret_value = match modal_id {
API_SECRET_MODAL => NodeConfig::get_api_secret(),
_ => NodeConfig::get_foreign_api_secret()
API_SECRET_MODAL => NodeConfig::get_api_secret(false),
_ => NodeConfig::get_api_secret(true)
};
let secret_text = if secret_value.is_some() {
@ -366,7 +373,7 @@ impl NodeSetup {
format!("{} {}", SHIELD_SLASH, t!("network_settings.disabled"))
};
View::button(ui, secret_text, Colors::button(), || {
View::button(ui, secret_text, Colors::white_or_black(false), || {
// Setup values for modal.
self.secret_edit = secret_value.unwrap_or("".to_string());
// Show secret edit modal.
@ -425,6 +432,11 @@ impl NodeSetup {
modal.close();
};
// Continue on Enter key press.
View::on_enter_key(ui, || {
on_save();
});
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
@ -449,7 +461,9 @@ impl NodeSetup {
ui.add_space(6.0);
let ftl = NodeConfig::get_ftl();
View::button(ui, format!("{} {}", CLOCK_CLOCKWISE, ftl.clone()), Colors::button(), || {
View::button(ui,
format!("{} {}", CLOCK_CLOCKWISE, &ftl),
Colors::white_or_black(false), || {
// Setup values for modal.
self.ftl_edit = ftl;
// Show ftl value setup modal.
@ -505,6 +519,11 @@ impl NodeSetup {
}
};
// Continue on Enter key press.
View::on_enter_key(ui, || {
on_save();
});
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {

View file

@ -91,7 +91,7 @@ impl Default for P2PSetup {
fn default() -> Self {
let port = NodeConfig::get_p2p_port();
let is_port_available = NodeConfig::is_p2p_port_available(&port);
let default_main_seeds = grin_servers::MAINNET_DNS_SEEDS
let default_main_seeds = Node::MAINNET_DNS_SEEDS
.iter()
.map(|s| s.to_string())
.collect();
@ -246,17 +246,19 @@ impl P2PSetup {
ui.add_space(6.0);
let port = NodeConfig::get_p2p_port();
View::button(ui, format!("{} {}", PLUG, port.clone()), Colors::button(), || {
// Setup values for modal.
self.port_edit = port;
self.port_available_edit = self.is_port_available;
// Show p2p port modal.
Modal::new(PORT_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("network_settings.change_value"))
.show();
cb.show_keyboard();
});
View::button(ui,
format!("{} {}", PLUG, &port),
Colors::white_or_black(false), || {
// Setup values for modal.
self.port_edit = port;
self.port_available_edit = self.is_port_available;
// Show p2p port modal.
Modal::new(PORT_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("network_settings.change_value"))
.show();
cb.show_keyboard();
});
ui.add_space(6.0);
// Show error when p2p port is unavailable.
@ -298,7 +300,7 @@ impl P2PSetup {
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
// Save button callback.
let on_save = || {
let mut on_save = || {
// Check if port is available.
let available = NodeConfig::is_p2p_port_available(&self.port_edit);
self.port_available_edit = available;
@ -306,17 +308,20 @@ impl P2PSetup {
// Save port at config if it's available.
if available {
NodeConfig::save_p2p_port(self.port_edit.parse::<u16>().unwrap());
if Node::is_running() {
Node::restart();
}
self.is_port_available = true;
cb.hide_keyboard();
modal.close();
}
};
// Continue on Enter key press.
View::on_enter_key(ui, || {
on_save();
});
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
@ -371,6 +376,8 @@ impl P2PSetup {
.size(16.0)
.color(Colors::inactive_text()));
ui.add_space(12.0);
} else if !peers.is_empty() {
ui.add_space(12.0);
}
let add_text = if peer_type == &PeerType::CustomSeed {
@ -379,7 +386,7 @@ impl P2PSetup {
format!("{} {}", PLUS_CIRCLE, t!("network_settings.add_peer"))
};
View::button(ui, add_text, Colors::button(), || {
View::button(ui, add_text, Colors::white_or_black(false), || {
// Setup values for modal.
self.is_correct_address_edit = true;
self.peer_edit = "".to_string();
@ -438,7 +445,7 @@ impl P2PSetup {
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
// Save button callback.
let on_save = || {
let mut on_save = || {
// Check if peer is correct and/or available.
let peer = self.peer_edit.clone();
let is_correct_address = PeersConfig::peer_to_addr(peer.clone()).is_some();
@ -460,6 +467,11 @@ impl P2PSetup {
}
};
// Continue on Enter key press.
View::on_enter_key(ui, || {
on_save();
});
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
@ -506,16 +518,18 @@ impl P2PSetup {
ui.add_space(6.0);
let ban_window = NodeConfig::get_p2p_ban_window();
View::button(ui, format!("{} {}", PROHIBIT_INSET, ban_window.clone()), Colors::button(), || {
// Setup values for modal.
self.ban_window_edit = ban_window;
// Show ban window period setup modal.
Modal::new(BAN_WINDOW_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("network_settings.change_value"))
.show();
cb.show_keyboard();
});
View::button(ui,
format!("{} {}", PROHIBIT_INSET, &ban_window),
Colors::white_or_black(false), || {
// Setup values for modal.
self.ban_window_edit = ban_window;
// Show ban window period setup modal.
Modal::new(BAN_WINDOW_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("network_settings.change_value"))
.show();
cb.show_keyboard();
});
ui.add_space(6.0);
ui.label(RichText::new(t!("network_settings.ban_window_desc"))
.size(16.0)
@ -563,6 +577,11 @@ impl P2PSetup {
}
};
// Continue on Enter key press.
View::on_enter_key(ui, || {
on_save();
});
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
@ -588,17 +607,18 @@ impl P2PSetup {
ui.add_space(6.0);
let max_inbound = NodeConfig::get_max_inbound_peers();
let button_text = format!("{} {}", ARROW_FAT_LINES_DOWN, max_inbound.clone());
View::button(ui, button_text, Colors::button(), || {
// Setup values for modal.
self.max_inbound_count = max_inbound;
// Show maximum number of inbound peers setup modal.
Modal::new(MAX_INBOUND_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("network_settings.change_value"))
.show();
cb.show_keyboard();
});
View::button(ui,
format!("{} {}", ARROW_FAT_LINES_DOWN, &max_inbound),
Colors::white_or_black(false), || {
// Setup values for modal.
self.max_inbound_count = max_inbound;
// Show maximum number of inbound peers setup modal.
Modal::new(MAX_INBOUND_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("network_settings.change_value"))
.show();
cb.show_keyboard();
});
ui.add_space(6.0);
}
@ -641,6 +661,11 @@ impl P2PSetup {
}
};
// Continue on Enter key press.
View::on_enter_key(ui, || {
on_save();
});
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
@ -664,19 +689,19 @@ impl P2PSetup {
.color(Colors::gray())
);
ui.add_space(6.0);
let max_outbound = NodeConfig::get_max_outbound_peers();
let button_text = format!("{} {}", ARROW_FAT_LINES_UP, max_outbound.clone());
View::button(ui, button_text, Colors::button(), || {
// Setup values for modal.
self.max_outbound_count = max_outbound;
// Show maximum number of outbound peers setup modal.
Modal::new(MAX_OUTBOUND_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("network_settings.change_value"))
.show();
cb.show_keyboard();
});
View::button(ui,
format!("{} {}", ARROW_FAT_LINES_UP, &max_outbound),
Colors::white_or_black(false), || {
// Setup values for modal.
self.max_outbound_count = max_outbound;
// Show maximum number of outbound peers setup modal.
Modal::new(MAX_OUTBOUND_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("network_settings.change_value"))
.show();
cb.show_keyboard();
});
ui.add_space(6.0);
}
@ -719,6 +744,11 @@ impl P2PSetup {
}
};
// Continue on Enter key press.
View::on_enter_key(ui, || {
on_save();
});
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
@ -738,12 +768,13 @@ impl P2PSetup {
/// Draw content to reset peers data.
fn reset_peers_ui(&mut self, ui: &mut egui::Ui) {
ui.add_space(4.0);
let button_text = format!("{} {}", TRASH, t!("network_settings.reset_peers"));
View::colored_text_button(ui, button_text, Colors::red(), Colors::button(), || {
Node::reset_peers(false);
self.peers_reset = true;
});
View::colored_text_button(ui,
format!("{} {}", TRASH, t!("network_settings.reset_peers")),
Colors::red(),
Colors::white_or_black(false), || {
Node::reset_peers(false);
self.peers_reset = true;
});
ui.add_space(6.0);
ui.label(RichText::new(t!("network_settings.reset_peers_desc"))
.size(16.0)

View file

@ -145,7 +145,7 @@ impl PoolSetup {
ui.add_space(6.0);
let fee = NodeConfig::get_base_fee();
View::button(ui, format!("{} {}", HAND_COINS, fee.clone()), Colors::button(),