mirror of
https://code.gri.mw/GUI/grim.git
synced 2026-07-13 10:18:54 +00:00
Compare commits
273 Commits
v0.2.3
...
v0.3.6-dev
| Author | SHA1 | Date | |
|---|---|---|---|
| 84a47ee59a | |||
| 8165379cf2 | |||
| a5a491499f | |||
| f891c8f516 | |||
| 38352bc75c | |||
| f80ca03e5d | |||
| 2bc3f191a7 | |||
| ee56061d68 | |||
| ee8841590a | |||
| 20db758bc2 | |||
| c03293aba2 | |||
| 3981ebe3ed | |||
| 161faba9a0 | |||
| 53bc6d3e3e | |||
| 8524084c47 | |||
| a91d9016a8 | |||
| 726a51bd0e | |||
| 60d8dc7555 | |||
| b51a46b943 | |||
| 3d1a721f29 | |||
| 176df6f93e | |||
| f7287bd9ad | |||
| 4c4b6cd5dc | |||
| 4aeda9c9dc | |||
| a4eadebef2 | |||
| 15d1aa1a21 | |||
| 242a3b9434 | |||
| edc1a09b2c | |||
| f31953f455 | |||
| d573ddedca | |||
| 3be6925ff8 | |||
| c7abd9cbfa | |||
| 512d216fee | |||
| eaefc58c5a | |||
| 2519e68dd5 | |||
| 73c0884f95 | |||
| f7b2150228 | |||
| 03924b5300 | |||
| a479189135 | |||
| e691a7b02d | |||
| f2b79cd70d | |||
| f20d1ee2c2 | |||
| 534c4cc86a | |||
| 558ac034b2 | |||
| 13bf8e830c | |||
| 57f319edfc | |||
| 748aebffb6 | |||
| b32085a423 | |||
| a9c65546e3 | |||
| b94241b82a | |||
| 8a1a69b739 | |||
| 01d17e25ee | |||
| cab38097fa | |||
| 0026fc3717 | |||
| 0fd04f14a4 | |||
| 3338f51de5 | |||
| 0fa8963bd2 | |||
| 70bba5d7ce | |||
| 0bb43e1e5d | |||
| fd52757549 | |||
| 6835bb1909 | |||
| 31bc74529c | |||
| 8d6943975b | |||
| 4c5d8abe7b | |||
| 4dc42bce4a | |||
| e2d5d92f18 | |||
| b001eb4712 | |||
| f14bd902ea | |||
| 33ab11933a | |||
| 6b05a2177e | |||
| 7bbe637414 | |||
| 9b6252de3a | |||
| 26debcf51c | |||
| 497b967fd0 | |||
| 05e18cf6c4 | |||
| 6e50b2b38a | |||
| 9bc96de398 | |||
| 5a525c50e1 | |||
| ba0af0968d | |||
| a0947aa47c | |||
| 06c6b8b4f5 | |||
| b19335d0bc | |||
| 40eb30fb75 | |||
| 8223e52570 | |||
| 875bd11bdb | |||
| 19e4cb664d | |||
| 18bc327a99 | |||
| 88e2fb0715 | |||
| feb38dc7cf | |||
| 28ecb5b1f4 | |||
| 024a9d0098 | |||
| 59cf46e1cb | |||
| 22255e0f2a | |||
| 7fdb8d272b | |||
| d043562058 | |||
| 096788c899 | |||
| ba914903eb | |||
| 162c5f88eb | |||
| ae0ff12935 | |||
| af203b8f9b | |||
| 1bd57cd88d | |||
| 8eea776111 | |||
| f5f6141881 | |||
| beb1a80c6a | |||
| 2a41689231 | |||
| dda3be7f86 | |||
| 65e9546f81 | |||
| 8f1175ff1a | |||
| 0ca2c7f372 | |||
| ee4752a95f | |||
| 366bbaeac6 | |||
| 4e1ada3188 | |||
| a499c91619 | |||
| 9f2ad32031 | |||
| 431cda358f | |||
| 149555cc0a | |||
| 72de1d5c05 | |||
| b4c64dae6b | |||
| e334386fe2 | |||
| a03758d383 | |||
| 7d75fc2ae0 | |||
| a8df3a20ba | |||
| 67514b8609 | |||
| 3a23438e17 | |||
| 86d4fde77d | |||
| b751aa256e | |||
| b2ef91e67d | |||
| b54fd3251f | |||
| 9bb5f1d66a | |||
| e56058ff33 | |||
| 45473ded7e | |||
| 6eec01bad6 | |||
| 35dbc3eca9 | |||
| 94bae256af | |||
| dae59744b3 | |||
| 7d28a31e18 | |||
| 6d5445f72f | |||
| 04417f1f53 | |||
| 97239ba0f5 | |||
| 51898404db | |||
| 2ca7c03999 | |||
| 86187e4e59 | |||
| 7ebfaaf477 | |||
| 00f8eb7d18 | |||
| 0713ba0213 | |||
| ec81ba2cee | |||
| cc5831358a | |||
| 961e65be4c | |||
| 12b6626624 | |||
| 03fbb0914e | |||
| ed2dc880aa | |||
| 646a7c5e04 | |||
| 11a5a73775 | |||
| 48ec553e0a | |||
| a567243716 | |||
| 4773fdb8d5 | |||
| 7f65471ba1 | |||
| fabd0a90df | |||
| 6093d2bddb | |||
| 42bcda621a | |||
| 215b5d3f27 | |||
| 97d8b86d39 | |||
| 8ba11daf31 | |||
| fe2f79ecad | |||
| 606072ca3a | |||
| 2eef58e23a | |||
| cf4f0789a3 | |||
| 1b78118f51 | |||
| a89a9bcaed | |||
| 8528c33be5 | |||
| d1502e26b1 | |||
| 2f56defffa | |||
| 01af084568 | |||
| cd0e3485c5 | |||
| b540fcbf19 | |||
| 7d29b2af6d | |||
| ad030fe811 | |||
| fae1364f10 | |||
| 93297b5401 | |||
| 511611f994 | |||
| e9e2a0a8e7 | |||
| 1222399926 | |||
| 845c1dc0ea | |||
| 3a21e60e19 | |||
| 9622429180 | |||
| d04b7a4e6a | |||
| 8b369b6049 | |||
| b54a573f61 | |||
| 184326bfde | |||
| b1f3c7d42b | |||
| 53a96e567d | |||
| 20daa7b465 | |||
| 0fa2ef4283 | |||
| e067a0a900 | |||
| 31d8e2f012 | |||
| 84d385ef1a | |||
| fabef9492e | |||
| 92f8386264 | |||
| 1ef62a806b | |||
| f8da3d0754 | |||
| 8165fab326 | |||
| 918c5b4355 | |||
| f930cd4ade | |||
| 3f3940e752 | |||
| 4ef5dd839d | |||
| fd14700eae | |||
| e5548eb6f1 | |||
| a364daf52e | |||
| 7089e6e1b2 | |||
| 0621154902 | |||
| acfb5fec1a | |||
| 1a3df4619e | |||
| 8994775be2 | |||
| 81365dbe6a | |||
| 7ae63b2b66 | |||
| b8dd5911d4 | |||
| 3fc4ffa179 | |||
| b84f6480e7 | |||
| 5dd8de7950 | |||
| 78baaca4a3 | |||
| e597ac7e4b | |||
| 4d5cc93a38 | |||
| ed50132d5e | |||
| fbb084f636 | |||
| d42ef102b2 | |||
| 9673c7d719 | |||
| 9b4623c558 | |||
| b7563e63c1 | |||
| 4d4b5eb007 | |||
| 6c04eec026 | |||
| 1ff2b27edc | |||
| 6bce9ec071 | |||
| 98619cc362 | |||
| 1987d0553c | |||
| 3f78095fe3 | |||
| 245766e1b5 | |||
| 2591653f66 | |||
| d11e90226b | |||
| fb159c17a0 | |||
| f7eb6580cc | |||
| 43720b34ba | |||
| f1f0f002ce | |||
| 86afa21a60 | |||
| 0169acba81 | |||
| 073d950d41 | |||
| 4eaaebd739 | |||
| a9e2106fda | |||
| 8b427989c5 | |||
| f16ce3c69b | |||
| a1b3330e5e | |||
| 3da8f5420b | |||
| 109e896506 | |||
| 8ad38f381e | |||
| 1e32315346 | |||
| ef8c645a6a | |||
| 15ecdf1e57 | |||
| 587b00c93a | |||
| aba2bead27 | |||
| 85ce58f69c | |||
| bb7e00b0eb | |||
| d60b35ebef | |||
| eb60c52224 | |||
| 61828ea2db | |||
| 7e819e14d1 | |||
| 1d9b7d9698 | |||
| 82c05588bc | |||
| 1cddd05bc0 | |||
| 8ad0d1c461 | |||
| a22a75913c | |||
| e797da0ed8 | |||
| 6936c14ed2 | |||
| c626ed5a48 | |||
| d79d05ef5a |
@@ -0,0 +1,71 @@
|
|||||||
|
name: Test build
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags-ignore:
|
||||||
|
- "*"
|
||||||
|
branches-ignore:
|
||||||
|
- master
|
||||||
|
- ci
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
- name: Checkout submodules
|
||||||
|
run: |
|
||||||
|
sed -i 's#https://code\.gri\.mw#${{ secrets.REPO_HOST }}#g' .gitmodules
|
||||||
|
git submodule update --init --recursive --remote
|
||||||
|
- name: Check commit
|
||||||
|
id: check
|
||||||
|
run: |
|
||||||
|
git fetch && git checkout master
|
||||||
|
sha=$(git rev-parse HEAD)
|
||||||
|
[[ "${{ github.sha }}" == "${sha}" ]] && test=false || test=true
|
||||||
|
echo "test=${test}" >> "$FORGEJO_OUTPUT"
|
||||||
|
- name: Restore cargo cache
|
||||||
|
id: cache-cargo-restore
|
||||||
|
uses: actions/cache/restore@v5
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cargo/bin/
|
||||||
|
~/.cargo/registry/index/
|
||||||
|
~/.cargo/registry/cache/
|
||||||
|
~/.cargo/git/db/
|
||||||
|
target/
|
||||||
|
key: ${{ runner.os }}-test-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||||
|
- name: Setup registry
|
||||||
|
run: |
|
||||||
|
echo -e '[registries.nexus]\nindex = "sparse+${{ secrets.MAVEN_HOST }}/repository/cargo/"\n[registry]\ndefault = "nexus"\n[source.crates-io]\nreplace-with = "nexus"\n[source.nexus]\nregistry = "sparse+${{ secrets.MAVEN_HOST }}/repository/cargo/"' > ~/.cargo/config.toml
|
||||||
|
- name: Build
|
||||||
|
if: ${{ steps.check.outputs.test == 'true' }}
|
||||||
|
run: cargo build --release
|
||||||
|
- name: Save cargo cache
|
||||||
|
uses: actions/cache/save@v5
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cargo/bin/
|
||||||
|
~/.cargo/registry/index/
|
||||||
|
~/.cargo/registry/cache/
|
||||||
|
~/.cargo/git/db/
|
||||||
|
target/
|
||||||
|
key: ${{ steps.cache-cargo-restore.outputs.cache-primary-key }}
|
||||||
|
- name: Telegram Notify Channel
|
||||||
|
if: ${{ steps.check.outputs.test == 'true' && (success() || failure()) }}
|
||||||
|
uses: actions/telegram-notifier@main
|
||||||
|
with:
|
||||||
|
api_url: ${{ secrets.TELEGRAM_API_URL }}
|
||||||
|
token: ${{ secrets.TELEGRAM_BOT_TOKEN }}
|
||||||
|
chat_id: ${{ secrets.TELEGRAM_CHANNEL_ID }}
|
||||||
|
status: ${{ job.status }}
|
||||||
|
notify_fields: "actor,repository,workflow,branch,commit"
|
||||||
|
- name: Telegram Notify Group
|
||||||
|
if: ${{ steps.check.outputs.test == 'true' && (success() || failure()) }}
|
||||||
|
uses: actions/telegram-notifier@main
|
||||||
|
with:
|
||||||
|
api_url: ${{ secrets.TELEGRAM_API_URL }}
|
||||||
|
token: ${{ secrets.TELEGRAM_BOT_TOKEN }}
|
||||||
|
chat_id: ${{ secrets.TELEGRAM_GROUP_ID }}
|
||||||
|
status: ${{ job.status }}
|
||||||
|
notify_fields: "actor,repository,workflow,branch,commit"
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
name: Pull Request
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types:
|
||||||
|
- closed
|
||||||
|
- opened
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
notify:
|
||||||
|
runs-on: debian-release
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Telegram Notify Channel
|
||||||
|
uses: actions/telegram-notifier@main
|
||||||
|
with:
|
||||||
|
api_url: ${{ secrets.TELEGRAM_API_URL }}
|
||||||
|
token: ${{ secrets.TELEGRAM_BOT_TOKEN }}
|
||||||
|
chat_id: ${{ secrets.TELEGRAM_CHANNEL_ID }}
|
||||||
|
status: ${{ job.status }}
|
||||||
|
notify_fields: "actor,repository"
|
||||||
|
- name: Telegram Notify Group
|
||||||
|
uses: actions/telegram-notifier@main
|
||||||
|
with:
|
||||||
|
api_url: ${{ secrets.TELEGRAM_API_URL }}
|
||||||
|
token: ${{ secrets.TELEGRAM_BOT_TOKEN }}
|
||||||
|
chat_id: ${{ secrets.TELEGRAM_GROUP_ID }}
|
||||||
|
status: ${{ job.status }}
|
||||||
|
notify_fields: "actor,repository"
|
||||||
@@ -0,0 +1,474 @@
|
|||||||
|
name: Release build
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
- ci
|
||||||
|
tags-ignore:
|
||||||
|
- "*-dev*"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
version:
|
||||||
|
runs-on: debian-release
|
||||||
|
outputs:
|
||||||
|
v: ${{ steps.version.outputs.v }}
|
||||||
|
pre: ${{ steps.version.outputs.pre }}
|
||||||
|
exists: ${{ steps.check.outputs.exists }}
|
||||||
|
last_tag: ${{ steps.check_prev.outputs.last_tag }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
- name: Checkout submodules
|
||||||
|
run: |
|
||||||
|
sed -i 's#https://code\.gri\.mw#${{ secrets.REPO_HOST }}#g' .gitmodules
|
||||||
|
git submodule update --init --recursive --remote
|
||||||
|
- name: Get version
|
||||||
|
id: version
|
||||||
|
run: |
|
||||||
|
ver="$(grep -m 1 -Po 'version = "\K[^"]*' Cargo.toml)"
|
||||||
|
[[ $ver == *"-dev"* ]] && ver=${ver} || ver=${ver}-dev
|
||||||
|
[[ ${{ forgejo.ref_type }} == 'tag' ]] && app_ver=${{ forgejo.ref_name }} || app_ver=v${ver}
|
||||||
|
echo "v=${app_ver}" >> "$FORGEJO_OUTPUT"
|
||||||
|
echo $app_ver
|
||||||
|
[[ ${{ forgejo.ref_type }} == 'tag' ]] && pre='false' || pre='true'
|
||||||
|
echo "pre=${pre}" >> "$FORGEJO_OUTPUT"
|
||||||
|
echo "pre-release: ${pre}"
|
||||||
|
- name: Check existing release
|
||||||
|
if: ${{ forgejo.ref_type == 'tag' }}
|
||||||
|
id: check
|
||||||
|
run: |
|
||||||
|
git fetch --tags
|
||||||
|
dev_sha=$(git rev-parse refs/tags/${{ forgejo.ref_name }}-dev) || :
|
||||||
|
[[ "$(git show-ref)" == *"${dev_sha}"* ]] && exists='true' || exists='false'
|
||||||
|
echo "exists=${exists}" >> "$FORGEJO_OUTPUT"
|
||||||
|
echo ${exists}
|
||||||
|
mkdir release
|
||||||
|
- uses: actions/forgejo-release@v2.11.3
|
||||||
|
if: ${{ forgejo.ref_type == 'tag' && steps.check.outputs.exists == 'true' }}
|
||||||
|
with:
|
||||||
|
direction: download
|
||||||
|
token: ${{ secrets.RELEASE_TOKEN }}
|
||||||
|
tag: "${{ forgejo.ref_name }}-dev"
|
||||||
|
release-dir: ./release
|
||||||
|
- name: Rename files
|
||||||
|
if: ${{ forgejo.ref_type == 'tag' && steps.check.outputs.exists == 'true' }}
|
||||||
|
working-directory: release
|
||||||
|
run: for f in *; do mv "$f" "$(echo "$f" | sed s/-dev-/-/)"; done
|
||||||
|
- name: Delete dev release
|
||||||
|
if: ${{ forgejo.ref_type == 'tag' && steps.check.outputs.exists == 'true' }}
|
||||||
|
uses: actions/delete-release@v1
|
||||||
|
with:
|
||||||
|
release_name: "${{ forgejo.ref_name }}-dev"
|
||||||
|
- name: Check previous release
|
||||||
|
id: check_prev
|
||||||
|
run: |
|
||||||
|
git fetch --tags
|
||||||
|
[[ ${{ forgejo.ref_type }} == 'tag' ]] && skip=0 || skip=1
|
||||||
|
last_tag=$(git describe --abbrev=0 --tags $(git rev-list --tags --skip=${skip} --max-count=1)) || true
|
||||||
|
echo "last_tag=${last_tag}" >> "$FORGEJO_OUTPUT"
|
||||||
|
- uses: actions/forgejo-release@v2.11.3
|
||||||
|
if: ${{ forgejo.ref_type == 'tag' && steps.check.outputs.exists == 'true' }}
|
||||||
|
with:
|
||||||
|
direction: upload
|
||||||
|
token: ${{ secrets.RELEASE_TOKEN }}
|
||||||
|
tag: ${{ forgejo.ref_name }}
|
||||||
|
override: false
|
||||||
|
prerelease: false
|
||||||
|
release-dir: ./release
|
||||||
|
release-notes: "Full Changelog: [${{ steps.check_prev.outputs.last_tag }}...${{ steps.version.outputs.v }}](https://code.gri.mw/${{ forgejo.repository }}/compare/${{ steps.check_prev.outputs.last_tag }}...${{ steps.version.outputs.v }})"
|
||||||
|
|
||||||
|
android:
|
||||||
|
if: ${{ forgejo.ref_type == 'branch' || needs.version.outputs.exists == 'false' }}
|
||||||
|
runs-on: macos
|
||||||
|
needs: version
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
- name: Checkout submodules
|
||||||
|
run: |
|
||||||
|
sed -i -- 's#https://code\.gri\.mw#${{ secrets.REPO_HOST }}#g' .gitmodules
|
||||||
|
git submodule update --init --recursive --remote
|
||||||
|
- name: Restore cargo cache
|
||||||
|
id: cache-cargo-restore
|
||||||
|
uses: actions/cache/restore@v5
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cargo/bin/
|
||||||
|
~/.cargo/registry/index/
|
||||||
|
~/.cargo/registry/cache/
|
||||||
|
~/.cargo/git/db/
|
||||||
|
target/
|
||||||
|
key: grim-android-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||||
|
- name: Build libs
|
||||||
|
run: |
|
||||||
|
rustup -q update
|
||||||
|
chmod +x scripts/android.sh && ./scripts/android.sh lib ${{ needs.version.outputs.v }}
|
||||||
|
- name: Save cargo cache
|
||||||
|
uses: actions/cache/save@v5
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cargo/bin/
|
||||||
|
~/.cargo/registry/index/
|
||||||
|
~/.cargo/registry/cache/
|
||||||
|
~/.cargo/git/db/
|
||||||
|
target/
|
||||||
|
key: ${{ steps.cache-cargo-restore.outputs.cache-primary-key }}
|
||||||
|
- name: Restore gradle cache
|
||||||
|
id: cache-gradle-restore
|
||||||
|
uses: actions/cache/restore@v5
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.gradle/caches
|
||||||
|
~/.gradle/wrapper
|
||||||
|
key: grim-android-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-gradle-
|
||||||
|
- name: Setup build
|
||||||
|
run: |
|
||||||
|
chmod +x android/gradlew
|
||||||
|
echo "${{ secrets.ANDROID_KEYSTORE }}" > release.keystore.txt
|
||||||
|
base64 -d < release.keystore.txt -o android/keystore
|
||||||
|
echo "${{ secrets.ANDROID_KEYSTORE_PROPS }}" > release.keystore.props.txt
|
||||||
|
base64 -d < release.keystore.props.txt -o android/keystore.properties
|
||||||
|
mkdir ~/.gradle && touch ~/.gradle/gradle.properties
|
||||||
|
printf "mavenHost=${{ secrets.MAVEN_LOCAL_HOST }}\n" >> ~/.gradle/gradle.properties
|
||||||
|
printf "mavenUser=${{ secrets.MAVEN_USER }}\n" >> ~/.gradle/gradle.properties
|
||||||
|
printf "mavenPassword=${{ secrets.MAVEN_PASSWORD }}" >> ~/.gradle/gradle.properties
|
||||||
|
- name: Release ARMv7+v8 APK
|
||||||
|
working-directory: android
|
||||||
|
run: |
|
||||||
|
jni_path=app/src/main/jniLibs
|
||||||
|
mv ${jni_path}/x86_64 x86_64
|
||||||
|
./gradlew assembleCiSignedRelease
|
||||||
|
apk_path=app/build/outputs/apk/ci/signedRelease/app-ci-signedRelease.apk
|
||||||
|
name=grim-${{ needs.version.outputs.v }}-android.apk
|
||||||
|
mv ${apk_path} "${name}"
|
||||||
|
- name: Checksum ARM APK
|
||||||
|
working-directory: android
|
||||||
|
run: |
|
||||||
|
name=grim-${{ needs.version.outputs.v }}-android.apk
|
||||||
|
checksum=grim-${{ needs.version.outputs.v }}-android-sha256sum.txt
|
||||||
|
sha256sum "${name}" > "${checksum}"
|
||||||
|
- name: Release x86_64 APK
|
||||||
|
working-directory: android
|
||||||
|
run: |
|
||||||
|
./gradlew clean
|
||||||
|
jni_path=app/src/main/jniLibs
|
||||||
|
rm -rf ${jni_path}/*
|
||||||
|
mv x86_64 ${jni_path}
|
||||||
|
./gradlew assembleCiSignedRelease
|
||||||
|
apk_path=app/build/outputs/apk/ci/signedRelease/app-ci-signedRelease.apk
|
||||||
|
name=grim-${{ needs.version.outputs.v }}-android-x86_64.apk
|
||||||
|
mv ${apk_path} "${name}"
|
||||||
|
- name: Save gradle cache
|
||||||
|
uses: actions/cache/save@v5
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.gradle/caches
|
||||||
|
~/.gradle/wrapper
|
||||||
|
key: ${{ steps.cache-gradle-restore.outputs.cache-primary-key }}
|
||||||
|
- name: Checksum x86_64 APK
|
||||||
|
working-directory: android
|
||||||
|
run: |
|
||||||
|
name=grim-${{ needs.version.outputs.v }}-android.apk
|
||||||
|
checksum=grim-${{ needs.version.outputs.v }}-android-x86_64-sha256sum.txt
|
||||||
|
sha256sum "${name}" > "${checksum}"
|
||||||
|
- name: Upload artifacts
|
||||||
|
run: |
|
||||||
|
mkdir release
|
||||||
|
mv android/grim* release
|
||||||
|
tar -czf android.tar.gz release
|
||||||
|
curl -s -u ${{ secrets.MAVEN_USER }}:${{ secrets.MAVEN_PASSWORD }} --upload-file android.tar.gz ${{ secrets.MAVEN_LOCAL_HOST }}/repository/grim-ci/${{ forgejo.repository }}/android.tar.gz
|
||||||
|
|
||||||
|
linux_arm:
|
||||||
|
if: ${{ forgejo.ref_type == 'branch' || needs.version.outputs.exists == 'false' }}
|
||||||
|
runs-on: debian-rust-arm
|
||||||
|
needs: [version]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
- name: Checkout submodules
|
||||||
|
run: |
|
||||||
|
sed -i 's#https://code\.gri\.mw#${{ secrets.REPO_HOST }}#g' .gitmodules
|
||||||
|
git submodule update --init --recursive --remote
|
||||||
|
- name: Restore cargo cache
|
||||||
|
id: cache-cargo-restore
|
||||||
|
uses: actions/cache/restore@v5
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cargo/bin/
|
||||||
|
~/.cargo/registry/index/
|
||||||
|
~/.cargo/registry/cache/
|
||||||
|
~/.cargo/git/db/
|
||||||
|
target/
|
||||||
|
key: grim-linux-arm-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||||
|
- name: Setup registry
|
||||||
|
run: |
|
||||||
|
echo -e '[registries.nexus]\nindex = "sparse+${{ secrets.MAVEN_LOCAL_HOST }}/repository/cargo/"\n[registry]\ndefault = "nexus"\n[source.crates-io]\nreplace-with = "nexus"\n[source.nexus]\nregistry = "sparse+${{ secrets.MAVEN_LOCAL_HOST }}/repository/cargo/"' > ~/.cargo/config.toml
|
||||||
|
- name: Release Linux ARM
|
||||||
|
run: |
|
||||||
|
rustup -q update
|
||||||
|
cargo zigbuild --release --target aarch64-unknown-linux-gnu
|
||||||
|
- name: Save cargo cache
|
||||||
|
uses: actions/cache/save@v5
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cargo/bin/
|
||||||
|
~/.cargo/registry/index/
|
||||||
|
~/.cargo/registry/cache/
|
||||||
|
~/.cargo/git/db/
|
||||||
|
target/
|
||||||
|
key: ${{ steps.cache-cargo-restore.outputs.cache-primary-key }}
|
||||||
|
- name: Upload artifacts
|
||||||
|
run: |
|
||||||
|
curl -s -u ${{ secrets.MAVEN_USER }}:${{ secrets.MAVEN_PASSWORD }} --upload-file target/aarch64-unknown-linux-gnu/release/grim ${{ secrets.MAVEN_LOCAL_HOST }}/repository/grim-ci/${{ forgejo.repository }}/grim-linux-arm
|
||||||
|
|
||||||
|
linux_arm_appimage:
|
||||||
|
if: ${{ forgejo.ref_type == 'branch' || needs.version.outputs.exists == 'false' }}
|
||||||
|
runs-on: debian-arm
|
||||||
|
needs: [version, linux_arm]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
- name: Download Artifact
|
||||||
|
run: |
|
||||||
|
wget ${{ secrets.MAVEN_LOCAL_HOST }}/repository/grim-ci/${{ forgejo.repository }}/grim-linux-arm
|
||||||
|
- name: AppImage
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
mkdir release
|
||||||
|
chmod +x grim-linux-arm
|
||||||
|
mv grim-linux-arm linux/Grim.AppDir/AppRun
|
||||||
|
ARCH=aarch64 appimagetool linux/Grim.AppDir grim-${{ needs.version.outputs.v }}-linux-arm.AppImage
|
||||||
|
mv grim-${{ needs.version.outputs.v }}-linux-arm.AppImage release/
|
||||||
|
- name: Checksum AppImage ARM
|
||||||
|
working-directory: release
|
||||||
|
run: sha256sum grim-${{ needs.version.outputs.v }}-linux-arm.AppImage > grim-${{ needs.version.outputs.v }}-linux-arm-appimage-sha256sum.txt
|
||||||
|
- name: Upload artifacts
|
||||||
|
run: |
|
||||||
|
tar -czf linux-arm.appimage.tar.gz release
|
||||||
|
curl -s -u ${{ secrets.MAVEN_USER }}:${{ secrets.MAVEN_PASSWORD }} --upload-file linux-arm.appimage.tar.gz ${{ secrets.MAVEN_LOCAL_HOST }}/repository/grim-ci/${{ forgejo.repository }}/linux-arm.appimage.tar.gz
|
||||||
|
|
||||||
|
linux_x86:
|
||||||
|
if: ${{ forgejo.ref_type == 'branch' || needs.version.outputs.exists == 'false' }}
|
||||||
|
runs-on: debian-rust-x86_64
|
||||||
|
needs: [version]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
- name: Checkout submodules
|
||||||
|
run: |
|
||||||
|
sed -i 's#https://code\.gri\.mw#${{ secrets.REPO_HOST }}#g' .gitmodules
|
||||||
|
git submodule update --init --recursive --remote
|
||||||
|
- name: Restore cargo cache
|
||||||
|
id: cache-cargo-restore
|
||||||
|
uses: actions/cache/restore@v5
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cargo/bin/
|
||||||
|
~/.cargo/registry/index/
|
||||||
|
~/.cargo/registry/cache/
|
||||||
|
~/.cargo/git/db/
|
||||||
|
target/
|
||||||
|
key: grim-linux-arm-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||||
|
- name: Setup registry
|
||||||
|
run: |
|
||||||
|
echo -e '[registries.nexus]\nindex = "sparse+${{ secrets.MAVEN_LOCAL_HOST }}/repository/cargo/"\n[registry]\ndefault = "nexus"\n[source.crates-io]\nreplace-with = "nexus"\n[source.nexus]\nregistry = "sparse+${{ secrets.MAVEN_LOCAL_HOST }}/repository/cargo/"' > ~/.cargo/config.toml
|
||||||
|
- name: Release Linux x86
|
||||||
|
run: |
|
||||||
|
rustup -q update
|
||||||
|
cargo zigbuild --release --target x86_64-unknown-linux-gnu
|
||||||
|
- name: Save cargo cache
|
||||||
|
uses: actions/cache/save@v5
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cargo/bin/
|
||||||
|
~/.cargo/registry/index/
|
||||||
|
~/.cargo/registry/cache/
|
||||||
|
~/.cargo/git/db/
|
||||||
|
target/
|
||||||
|
key: ${{ steps.cache-cargo-restore.outputs.cache-primary-key }}
|
||||||
|
- name: AppImage x86
|
||||||
|
run: |
|
||||||
|
mkdir release
|
||||||
|
mv target/x86_64-unknown-linux-gnu/release/grim linux/Grim.AppDir/AppRun
|
||||||
|
appimagetool linux/Grim.AppDir grim-${{ needs.version.outputs.v }}-linux-x86_64.AppImage
|
||||||
|
mv grim-${{ needs.version.outputs.v }}-linux-x86_64.AppImage release/
|
||||||
|
- name: Checksum AppImage x86
|
||||||
|
working-directory: release
|
||||||
|
run: sha256sum grim-${{ needs.version.outputs.v }}-linux-x86_64.AppImage > grim-${{ needs.version.outputs.v }}-linux-x86_64-appimage-sha256sum.txt
|
||||||
|
- name: Upload artifacts
|
||||||
|
run: |
|
||||||
|
tar -czf linux-x86_64.appimage.tar.gz release
|
||||||
|
curl -s -u ${{ secrets.MAVEN_USER }}:${{ secrets.MAVEN_PASSWORD }} --upload-file linux-x86_64.appimage.tar.gz ${{ secrets.MAVEN_LOCAL_HOST }}/repository/grim-ci/${{ forgejo.repository }}/linux-x86_64.appimage.tar.gz
|
||||||
|
|
||||||
|
macos:
|
||||||
|
if: ${{ forgejo.ref_type == 'branch' || needs.version.outputs.exists == 'false' }}
|
||||||
|
runs-on: macos
|
||||||
|
needs: [version, android]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
- name: Checkout submodules
|
||||||
|
run: |
|
||||||
|
sed -i -- 's#https://code\.gri\.mw#${{ secrets.REPO_HOST }}#g' .gitmodules
|
||||||
|
git submodule update --init --recursive --remote
|
||||||
|
- run: mkdir release
|
||||||
|
- name: Restore cargo cache
|
||||||
|
id: cache-cargo-restore
|
||||||
|
uses: actions/cache/restore@v5
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cargo/bin/
|
||||||
|
~/.cargo/registry/index/
|
||||||
|
~/.cargo/registry/cache/
|
||||||
|
~/.cargo/git/db/
|
||||||
|
target/
|
||||||
|
key: grim-macos-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||||
|
- name: Release MacOS Universal
|
||||||
|
run: |
|
||||||
|
rustup -q update
|
||||||
|
export MACOSX_DEPLOYMENT_TARGET=11.0
|
||||||
|
cargo build --release --target x86_64-apple-darwin
|
||||||
|
cargo build --release --target aarch64-apple-darwin
|
||||||
|
lipo -create -output grim "target/x86_64-apple-darwin/release/grim" "target/aarch64-apple-darwin/release/grim"
|
||||||
|
mv grim macos/Grim.app/Contents/MacOS
|
||||||
|
- name: Save cargo cache
|
||||||
|
uses: actions/cache/save@v5
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cargo/bin/
|
||||||
|
~/.cargo/registry/index/
|
||||||
|
~/.cargo/registry/cache/
|
||||||
|
~/.cargo/git/db/
|
||||||
|
target/
|
||||||
|
key: ${{ steps.cache-cargo-restore.outputs.cache-primary-key }}
|
||||||
|
- name: Archive Universal
|
||||||
|
working-directory: macos
|
||||||
|
run: |
|
||||||
|
zip -r grim-${{ needs.version.outputs.v }}-macos-universal.zip Grim.app
|
||||||
|
mv grim-${{ needs.version.outputs.v }}-macos-universal.zip ../release
|
||||||
|
- name: Checksum Release Universal
|
||||||
|
working-directory: release
|
||||||
|
run: sha256sum grim-${{ needs.version.outputs.v }}-macos-universal.zip > grim-${{ needs.version.outputs.v }}-macos-universal-sha256sum.txt
|
||||||
|
- name: Upload artifacts
|
||||||
|
run: |
|
||||||
|
tar -czf macos.tar.gz release
|
||||||
|
curl -s -u ${{ secrets.MAVEN_USER }}:${{ secrets.MAVEN_PASSWORD }} --upload-file macos.tar.gz ${{ secrets.MAVEN_LOCAL_HOST }}/repository/grim-ci/${{ forgejo.repository }}/macos.tar.gz
|
||||||
|
|
||||||
|
windows:
|
||||||
|
if: ${{ forgejo.ref_type == 'branch' || needs.version.outputs.exists == 'false' }}
|
||||||
|
runs-on: windows
|
||||||
|
needs: [version]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
- name: Checkout submodules
|
||||||
|
run: |
|
||||||
|
(Get-content .gitmodules) | Foreach-Object {$_ -replace "https://code.gri.mw", "${{ secrets.REPO_HOST }}"} | Set-Content .gitmodules
|
||||||
|
git submodule update --init --recursive --remote
|
||||||
|
- name: Update UpgradeCode
|
||||||
|
shell: powershell
|
||||||
|
run: |
|
||||||
|
$guid = [guid]::NewGuid().ToString()
|
||||||
|
$wix = [xml](Get-Content wix/main.wxs)
|
||||||
|
$wix.Wix.Product.UpgradeCode = $guid
|
||||||
|
$wix.Save("wix/main.wxs")
|
||||||
|
Get-Content wix/main.wxs
|
||||||
|
- run: mkdir release
|
||||||
|
- name: Release Windows x86
|
||||||
|
run: |
|
||||||
|
rustup -q update
|
||||||
|
cargo wix -p grim -o grim-${{ needs.version.outputs.v }}-win-x86_64.msi --nocapture
|
||||||
|
mv grim-${{ needs.version.outputs.v }}-win-x86_64.msi release\
|
||||||
|
Compress-Archive -Path target\release\grim.exe -DestinationPath grim-${{ needs.version.outputs.v }}-win-x86_64.zip
|
||||||
|
mv grim-${{ needs.version.outputs.v }}-win-x86_64.zip release\
|
||||||
|
- name: Checksum Archive x86
|
||||||
|
working-directory: release
|
||||||
|
run: |
|
||||||
|
certutil -hashfile grim-${{ needs.version.outputs.v }}-win-x86_64.msi SHA256 > grim-${{ needs.version.outputs.v }}-win-x86_64-msi-sha256sum.txt
|
||||||
|
certutil -hashfile grim-${{ needs.version.outputs.v }}-win-x86_64.zip SHA256 > grim-${{ needs.version.outputs.v }}-win-x86_64-sha256sum.txt
|
||||||
|
- name: Upload artifacts
|
||||||
|
run: |
|
||||||
|
tar -czf windows.tar.gz release
|
||||||
|
Remove-Item alias:curl
|
||||||
|
curl -s -u ${{ secrets.MAVEN_USER }}:${{ secrets.MAVEN_PASSWORD }} --upload-file windows.tar.gz ${{ secrets.MAVEN_LOCAL_HOST }}/repository/grim-ci/${{ forgejo.repository }}/windows.tar.gz
|
||||||
|
|
||||||
|
release:
|
||||||
|
if: ${{ forgejo.ref_type == 'branch' || needs.version.outputs.exists == 'false' }}
|
||||||
|
runs-on: debian-release
|
||||||
|
needs: [version, android, linux_x86, linux_arm_appimage, macos, windows]
|
||||||
|
steps:
|
||||||
|
- name: Download All Artifacts
|
||||||
|
run: |
|
||||||
|
curl -s -o android.tar.gz -u ${{ secrets.MAVEN_USER }}:${{ secrets.MAVEN_PASSWORD }} ${{ secrets.MAVEN_HOST }}/repository/grim-ci/${{ forgejo.repository }}/android.tar.gz
|
||||||
|
tar -xzf android.tar.gz
|
||||||
|
rm android.tar.gz
|
||||||
|
curl -s -o linux-x86_64.appimage.tar.gz -u ${{ secrets.MAVEN_USER }}:${{ secrets.MAVEN_PASSWORD }} ${{ secrets.MAVEN_HOST }}/repository/grim-ci/${{ forgejo.repository }}/linux-x86_64.appimage.tar.gz
|
||||||
|
tar -xzf linux-x86_64.appimage.tar.gz
|
||||||
|
rm linux-x86_64.appimage.tar.gz
|
||||||
|
curl -s -o linux-arm.appimage.tar.gz -u ${{ secrets.MAVEN_USER }}:${{ secrets.MAVEN_PASSWORD }} ${{ secrets.MAVEN_HOST }}/repository/grim-ci/${{ forgejo.repository }}/linux-arm.appimage.tar.gz
|
||||||
|
tar -xzf linux-arm.appimage.tar.gz
|
||||||
|
rm linux-arm.appimage.tar.gz
|
||||||
|
curl -s -o macos.tar.gz -u ${{ secrets.MAVEN_USER }}:${{ secrets.MAVEN_PASSWORD }} ${{ secrets.MAVEN_HOST }}/repository/grim-ci/${{ forgejo.repository }}/macos.tar.gz
|
||||||
|
tar -xzf macos.tar.gz
|
||||||
|
rm macos.tar.gz
|
||||||
|
curl -s -o windows.tar.gz -u ${{ secrets.MAVEN_USER }}:${{ secrets.MAVEN_PASSWORD }} ${{ secrets.MAVEN_HOST }}/repository/grim-ci/${{ forgejo.repository }}/windows.tar.gz
|
||||||
|
tar -xzf windows.tar.gz
|
||||||
|
rm windows.tar.gz
|
||||||
|
- name: Upload release to Forgejo
|
||||||
|
uses: actions/forgejo-release@v2.11.3
|
||||||
|
with:
|
||||||
|
direction: upload
|
||||||
|
token: ${{ secrets.RELEASE_TOKEN }}
|
||||||
|
tag: ${{ needs.version.outputs.v }}
|
||||||
|
override: true
|
||||||
|
prerelease: ${{ needs.version.outputs.pre == 'true' }}
|
||||||
|
release-dir: release
|
||||||
|
release-notes: "Full Changelog: [${{ needs.version.outputs.last_tag }}...${{ needs.version.outputs.v }}](https://code.gri.mw/${{ forgejo.repository }}/compare/${{ needs.version.outputs.last_tag }}...${{ needs.version.outputs.v }})"
|
||||||
|
- name: Telegram Notify Channel
|
||||||
|
if: always()
|
||||||
|
uses: actions/telegram-notifier@main
|
||||||
|
with:
|
||||||
|
api_url: ${{ secrets.TELEGRAM_API_URL }}
|
||||||
|
token: ${{ secrets.TELEGRAM_BOT_TOKEN }}
|
||||||
|
chat_id: ${{ secrets.TELEGRAM_CHANNEL_ID }}
|
||||||
|
status: ${{ job.status }}
|
||||||
|
notify_fields: "actor,repository,workflow,branch,commit"
|
||||||
|
- name: Telegram Notify Group
|
||||||
|
if: always()
|
||||||
|
uses: actions/telegram-notifier@main
|
||||||
|
with:
|
||||||
|
api_url: ${{ secrets.TELEGRAM_API_URL }}
|
||||||
|
token: ${{ secrets.TELEGRAM_BOT_TOKEN }}
|
||||||
|
chat_id: ${{ secrets.TELEGRAM_GROUP_ID }}
|
||||||
|
status: ${{ job.status }}
|
||||||
|
notify_fields: "actor,repository,workflow,branch,commit"
|
||||||
|
|
||||||
|
release-telegram:
|
||||||
|
runs-on: debian-release
|
||||||
|
needs: [version, release]
|
||||||
|
steps:
|
||||||
|
- name: Download All Artifacts
|
||||||
|
run: |
|
||||||
|
mkdir release
|
||||||
|
cd release
|
||||||
|
wget ${{ secrets.REPO_HOST }}/${{ forgejo.repository }}/releases/download/${{ needs.version.outputs.v }}/grim-${{ needs.version.outputs.v }}-android.apk
|
||||||
|
wget ${{ secrets.REPO_HOST }}/${{ forgejo.repository }}/releases/download/${{ needs.version.outputs.v }}/grim-${{ needs.version.outputs.v }}-android-x86_64.apk
|
||||||
|
wget ${{ secrets.REPO_HOST }}/${{ forgejo.repository }}/releases/download/${{ needs.version.outputs.v }}/grim-${{ needs.version.outputs.v }}-linux-arm.AppImage
|
||||||
|
wget ${{ secrets.REPO_HOST }}/${{ forgejo.repository }}/releases/download/${{ needs.version.outputs.v }}/grim-${{ needs.version.outputs.v }}-linux-x86_64.AppImage
|
||||||
|
wget ${{ secrets.REPO_HOST }}/${{ forgejo.repository }}/releases/download/${{ needs.version.outputs.v }}/grim-${{ needs.version.outputs.v }}-macos-universal.zip
|
||||||
|
wget ${{ secrets.REPO_HOST }}/${{ forgejo.repository }}/releases/download/${{ needs.version.outputs.v }}/grim-${{ needs.version.outputs.v }}-win-x86_64.msi
|
||||||
|
wget ${{ secrets.REPO_HOST }}/${{ forgejo.repository }}/releases/download/${{ needs.version.outputs.v }}/grim-${{ needs.version.outputs.v }}-win-x86_64.zip
|
||||||
|
- name: Upload files to Telegram
|
||||||
|
uses: actions/telegram-send-file@main
|
||||||
|
with:
|
||||||
|
api_url: ${{ secrets.TELEGRAM_API_URL }}
|
||||||
|
chat_ids: |
|
||||||
|
${{ secrets.TELEGRAM_CHANNEL_ID }}
|
||||||
|
${{ secrets.TELEGRAM_GROUP_ID }}
|
||||||
|
body: '🎁 Release <a href="https://code.gri.mw/${{ forgejo.repository }}/releases">${{ needs.version.outputs.v }}</a> is ready!'
|
||||||
|
token: ${{ secrets.TELEGRAM_BOT_TOKEN }}
|
||||||
|
pin: true
|
||||||
|
files: |
|
||||||
|
release/grim-${{ needs.version.outputs.v }}-android.apk
|
||||||
|
release/grim-${{ needs.version.outputs.v }}-android-x86_64.apk
|
||||||
|
release/grim-${{ needs.version.outputs.v }}-linux-arm.AppImage
|
||||||
|
release/grim-${{ needs.version.outputs.v }}-linux-x86_64.AppImage
|
||||||
|
release/grim-${{ needs.version.outputs.v }}-macos-universal.zip
|
||||||
|
release/grim-${{ needs.version.outputs.v }}-win-x86_64.msi
|
||||||
|
release/grim-${{ needs.version.outputs.v }}-win-x86_64.zip
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
HOST=https://code.gri.mw
|
||||||
|
REPO_NAME=$1
|
||||||
|
TAG=$2
|
||||||
|
DOWNLOAD_URL=${HOST}/${REPO_NAME}/releases/download/${TAG}
|
||||||
|
|
||||||
|
FILES=( "grim-${TAG}-android.apk" "grim-${TAG}-android-x86_64.apk" "grim-${TAG}-linux-arm.AppImage" "grim-${TAG}-linux-x86_64.AppImage" "grim-${TAG}-macos-universal.zip" "grim-${TAG}-win-x86_64.msi" "grim-${TAG}-win-x86_64.zip" )
|
||||||
|
|
||||||
|
# Download release files
|
||||||
|
for f in "${FILES[@]}"; do
|
||||||
|
wget -q ${DOWNLOAD_URL}/${f}
|
||||||
|
echo Downloading ${f}...
|
||||||
|
while [ ! -f ${f} ]; do
|
||||||
|
sleep 5
|
||||||
|
echo Retry ${f}...
|
||||||
|
wget -q ${DOWNLOAD_URL}/${f}
|
||||||
|
done
|
||||||
|
done
|
||||||
|
|
||||||
|
# Save release notes
|
||||||
|
INFO_URL=${HOST}/api/v1/repos/${REPO_NAME}/releases/tags/${TAG}
|
||||||
|
curl -s "${INFO_URL}" | jq -r '.body' > release_notes.txt
|
||||||
@@ -6,7 +6,9 @@ jobs:
|
|||||||
name: Linux Build
|
name: Linux Build
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
- name: Release build
|
- name: Release build
|
||||||
run: cargo build --release
|
run: cargo build --release
|
||||||
|
|
||||||
@@ -14,7 +16,9 @@ jobs:
|
|||||||
name: Windows Build
|
name: Windows Build
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
- name: Release build
|
- name: Release build
|
||||||
run: cargo build --release
|
run: cargo build --release
|
||||||
|
|
||||||
@@ -22,6 +26,8 @@ jobs:
|
|||||||
name: MacOS Build
|
name: MacOS Build
|
||||||
runs-on: macos-latest
|
runs-on: macos-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
- name: Release build
|
- name: Release build
|
||||||
run: cargo build --release
|
run: cargo build --release
|
||||||
|
|||||||
+16
-155
@@ -6,165 +6,26 @@ on:
|
|||||||
- "v*.*.*"
|
- "v*.*.*"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
linux_release:
|
create_release:
|
||||||
name: Linux Release
|
name: Create Release
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
- name: Download release
|
||||||
fetch-depth: 0
|
run: chmod +x .github/download_release.sh && .github/download_release.sh GUI/grim ${{ github.ref_name }}
|
||||||
- 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
|
- name: Release
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
|
body_path: release_notes.txt
|
||||||
|
overwrite_files: true
|
||||||
files: |
|
files: |
|
||||||
target/x86_64-unknown-linux-gnu/release/grim-${{ github.ref_name }}-linux-x86_64.AppImage
|
grim-${{ github.ref_name }}-android.apk
|
||||||
target/x86_64-unknown-linux-gnu/release/grim-${{ github.ref_name }}-linux-x86_64-appimage-sha256sum.txt
|
grim-${{ github.ref_name }}-android-x86_64.apk
|
||||||
target/aarch64-unknown-linux-gnu/release/grim-${{ github.ref_name }}-linux-arm.AppImage
|
grim-${{ github.ref_name }}-linux-arm.AppImage
|
||||||
target/aarch64-unknown-linux-gnu/release/grim-${{ github.ref_name }}-linux-arm-appimage-sha256sum.txt
|
grim-${{ github.ref_name }}-linux-x86_64.AppImage
|
||||||
|
grim-${{ github.ref_name }}-macos-universal.zip
|
||||||
windows_release:
|
grim-${{ github.ref_name }}-win-x86_64.msi
|
||||||
name: Windows Release
|
grim-${{ github.ref_name }}-win-x86_64.zip
|
||||||
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
|
|
||||||
@@ -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
|
||||||
+3
-1
@@ -1,4 +1,5 @@
|
|||||||
*.iml
|
*.iml
|
||||||
|
android/build
|
||||||
android/.idea
|
android/.idea
|
||||||
android/.gradle
|
android/.gradle
|
||||||
android/local.properties
|
android/local.properties
|
||||||
@@ -18,4 +19,5 @@ target
|
|||||||
app/src/main/jniLibs
|
app/src/main/jniLibs
|
||||||
macos/cert.pem
|
macos/cert.pem
|
||||||
linux/Grim.AppDir/AppRun
|
linux/Grim.AppDir/AppRun
|
||||||
.intentionally-empty-file.o
|
.intentionally-empty-file.o
|
||||||
|
Cargo.toml-e
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
[submodule "wallet"]
|
||||||
|
path = wallet
|
||||||
|
url = https://code.gri.mw/ardocrat/wallet
|
||||||
|
branch = grim-staging
|
||||||
|
[submodule "tor/webtunnel"]
|
||||||
|
path = tor/webtunnel
|
||||||
|
url = https://code.gri.mw/WEB/webtunnel
|
||||||
|
branch = grim
|
||||||
Executable
+50
@@ -0,0 +1,50 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# Copyright 2026 The Grim Developers
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
rustfmt --version &>/dev/null
|
||||||
|
if [ $? != 0 ]; then
|
||||||
|
printf "[pre_commit] \033[0;31merror\033[0m: \"rustfmt\" not available. \n"
|
||||||
|
printf "[pre_commit] \033[0;31merror\033[0m: rustfmt can be installed via - \n"
|
||||||
|
printf "[pre_commit] $ rustup component add rustfmt-preview \n"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
problem_files=()
|
||||||
|
|
||||||
|
# first collect all the files that need reformatting
|
||||||
|
for file in $(git diff --name-only --cached); do
|
||||||
|
if [ ${file: -3} == ".rs" ]; then
|
||||||
|
rustfmt --check $file &>/dev/null
|
||||||
|
if [ $? != 0 ]; then
|
||||||
|
problem_files+=($file)
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ ${#problem_files[@]} == 0 ]; then
|
||||||
|
# nothing to do
|
||||||
|
printf "[pre_commit] rustfmt \033[0;32mok\033[0m \n"
|
||||||
|
else
|
||||||
|
# reformat the files that need it and re-stage them.
|
||||||
|
printf "[pre_commit] the following files were rustfmt'd before commit: \n"
|
||||||
|
for file in ${problem_files[@]}; do
|
||||||
|
rustfmt $file
|
||||||
|
git add $file
|
||||||
|
printf "\033[0;32m $file\033[0m \n"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit 0
|
||||||
Generated
+4952
-3181
File diff suppressed because it is too large
Load Diff
+77
-76
@@ -1,12 +1,13 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "grim"
|
name = "grim"
|
||||||
version = "0.2.3"
|
version = "0.3.6"
|
||||||
authors = ["Ardocrat <ardocrat@proton.me>"]
|
authors = ["Ardocrat <ardocrat@gri.mw>"]
|
||||||
description = "Cross-platform GUI for Grin with focus on usability and availability to be used by anyone, anywhere."
|
description = "Cross-platform GUI for Grin with focus on usability and availability to be used by anyone, anywhere."
|
||||||
license = "Apache-2.0"
|
license = "Apache-2.0"
|
||||||
repository = "https://github.com/ardocrat/grim"
|
repository = "https://code.gri.mw/GUI/grim"
|
||||||
keywords = [ "crypto", "grin", "mimblewimble" ]
|
keywords = [ "crypto", "grin", "mimblewimble" ]
|
||||||
edition = "2021"
|
edition = "2024"
|
||||||
|
build = "build.rs"
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "grim"
|
name = "grim"
|
||||||
@@ -25,111 +26,111 @@ codegen-units = 1
|
|||||||
panic = "abort"
|
panic = "abort"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
log = "0.4.22"
|
log = "0.4.27"
|
||||||
|
|
||||||
## node
|
# node
|
||||||
openssl-sys = { version = "0.9.103", features = ["vendored"] }
|
grin_api = { path = "wallet/grin/api" }
|
||||||
grin_api = "5.3.3"
|
grin_chain = { path = "wallet/grin/chain" }
|
||||||
grin_chain = "5.3.3"
|
grin_config = { path = "wallet/grin/config" }
|
||||||
grin_config = "5.3.3"
|
grin_core = { path = "wallet/grin/core" }
|
||||||
grin_core = "5.3.3"
|
grin_p2p = { path = "wallet/grin/p2p" }
|
||||||
grin_p2p = "5.3.3"
|
grin_servers = { path = "wallet/grin/servers" }
|
||||||
grin_servers = "5.3.3"
|
grin_keychain = { path = "wallet/grin/keychain" }
|
||||||
grin_keychain = "5.3.3"
|
grin_util = { path = "wallet/grin/util" }
|
||||||
grin_util = "5.3.3"
|
|
||||||
|
|
||||||
## wallet
|
# wallet
|
||||||
grin_wallet_impls = "5.3.3"
|
grin_wallet_impls = { path = "wallet/impls" }
|
||||||
grin_wallet_api = "5.3.3"
|
grin_wallet_api = { path = "wallet/api"}
|
||||||
grin_wallet_libwallet = "5.3.3"
|
grin_wallet_libwallet = { path = "wallet/libwallet" }
|
||||||
grin_wallet_util = "5.3.3"
|
grin_wallet_util = { path = "wallet/util" }
|
||||||
grin_wallet_controller = "5.3.3"
|
grin_wallet_controller = { path = "wallet/controller" }
|
||||||
|
|
||||||
## ui
|
## ui
|
||||||
egui = { version = "0.29.1", default-features = false }
|
egui = { version = "0.33.3", default-features = false }
|
||||||
egui_extras = { version = "0.29.1", features = ["image", "svg"] }
|
egui_extras = { version = "0.33.3", features = ["image", "svg"] }
|
||||||
rust-i18n = "2.3.1"
|
egui-async = "0.3.4"
|
||||||
|
rust-i18n = "3.1.5"
|
||||||
|
|
||||||
## other
|
## other
|
||||||
anyhow = "1.0.89"
|
log4rs = "1.4.0"
|
||||||
pin-project = "1.1.6"
|
backtrace = "0.3.76"
|
||||||
backtrace = "0.3.74"
|
|
||||||
thiserror = "1.0.64"
|
|
||||||
futures = "0.3.31"
|
futures = "0.3.31"
|
||||||
dirs = "5.0.1"
|
dirs = "6.0.0"
|
||||||
sys-locale = "0.3.1"
|
sys-locale = "0.3.2"
|
||||||
chrono = "0.4.38"
|
chrono = "0.4.43"
|
||||||
parking_lot = "0.12.3"
|
parking_lot = "0.12.3"
|
||||||
lazy_static = "1.5.0"
|
lazy_static = "1.5.0"
|
||||||
toml = "0.8.19"
|
toml = "0.9.11+spec-1.1.0"
|
||||||
serde = "1.0.210"
|
serde = "1.0.228"
|
||||||
local-ip-address = "0.6.3"
|
local-ip-address = "0.6.9"
|
||||||
url = "2.5.2"
|
url = "2.5.8"
|
||||||
rand = "0.8.5"
|
rand = "0.9.2"
|
||||||
serde_derive = "1.0.210"
|
serde_derive = "1.0.228"
|
||||||
serde_json = "1.0.128"
|
serde_json = "1.0.149"
|
||||||
tokio = { version = "1.40.0", features = ["full"] }
|
tokio = { version = "1.49.0", features = ["full"] }
|
||||||
image = "0.25.2"
|
image = "0.25.9"
|
||||||
rqrr = "0.8.0"
|
rqrr = "0.10.1"
|
||||||
qrcodegen = "1.8.0"
|
qrcodegen = "1.8.0"
|
||||||
qrcode = "0.14.1"
|
qrcode = "0.14.1"
|
||||||
ur = "0.4.1"
|
ur = "0.4.1"
|
||||||
gif = "0.13.1"
|
gif = "0.14.1"
|
||||||
rkv = { version = "0.19.0", features = ["lmdb"] }
|
rkv = "0.20.0"
|
||||||
|
usvg = "0.45.1"
|
||||||
|
ring = "0.16.20"
|
||||||
|
hyper = { version = "1.6.0", features = ["full"], package = "hyper" }
|
||||||
|
hyper-util = { version = "0.1.19", features = ["http1", "client", "client-legacy"] }
|
||||||
|
http-body-util = "0.1.3"
|
||||||
|
bytes = "1.11.0"
|
||||||
|
hyper-socks2 = "0.9.1"
|
||||||
|
hyper-proxy2 = "0.1.0"
|
||||||
|
hyper-tls = "0.6.0"
|
||||||
|
async-std = "1.13.2"
|
||||||
|
uuid = { version = "0.8.2", features = ["v4"] }
|
||||||
|
num-bigint = "0.4.6"
|
||||||
|
|
||||||
## tor
|
## tor
|
||||||
arti-client = { version = "0.23.0", features = ["pt-client", "static", "onion-service-service", "onion-service-client"] }
|
arti-client = { version = "0.43.0", features = ["static", "pt-client", "onion-service-service", "onion-service-client"] }
|
||||||
tor-rtcompat = { version = "0.23.0", features = ["static"] }
|
tor-rtcompat = { version = "0.43.0", features = ["static"] }
|
||||||
tor-config = "0.23.0"
|
tor-config = "0.43.0"
|
||||||
fs-mistrust = "0.8.0"
|
fs-mistrust = "0.14.2"
|
||||||
tor-hsservice = "0.23.0"
|
tor-hsservice = "0.43.0"
|
||||||
tor-hsrproxy = "0.23.0"
|
tor-hsrproxy = "0.43.0"
|
||||||
tor-keymgr = "0.23.0"
|
tor-keymgr = "0.43.0"
|
||||||
tor-llcrypto = "0.23.0"
|
tor-llcrypto = "0.43.0"
|
||||||
tor-hscrypto = "0.23.0"
|
tor-hscrypto = "0.43.0"
|
||||||
tor-error = "0.23.0"
|
|
||||||
sha2 = "0.10.8"
|
sha2 = "0.10.8"
|
||||||
ed25519-dalek = "2.1.1"
|
ed25519-dalek = "2.1.1"
|
||||||
curve25519-dalek = "4.1.3"
|
curve25519-dalek = "4.1.3"
|
||||||
hyper = { version = "0.14.30", features = ["full"] }
|
safelog = "0.8.1"
|
||||||
hyper-tls = "0.5.0"
|
|
||||||
tls-api = "0.9.0"
|
|
||||||
tls-api-native-tls = "0.9.0"
|
|
||||||
|
|
||||||
## stratum server
|
## stratum server
|
||||||
tokio-old = {version = "0.2", features = ["full"], package = "tokio" }
|
tokio-old = { version = "0.2", features = ["full"], package = "tokio" }
|
||||||
tokio-util-old = { version = "0.2", features = ["codec"], package = "tokio-util" }
|
tokio-util-old = { version = "0.2", features = ["codec"], package = "tokio-util" }
|
||||||
|
|
||||||
[target.'cfg(target_os = "linux")'.dependencies]
|
[target.'cfg(target_os = "linux")'.dependencies]
|
||||||
nokhwa = { version = "0.10.5", default-features = false, features = ["input-v4l"] }
|
nokhwa = { version = "0.10.10", default-features = false, features = ["input-v4l"] }
|
||||||
|
|
||||||
[target.'cfg(target_os = "windows")'.dependencies]
|
[target.'cfg(target_os = "windows")'.dependencies]
|
||||||
nokhwa = { version = "0.10.5", default-features = false, features = ["input-msmf"] }
|
nokhwa = { version = "0.10.10", default-features = false, features = ["input-msmf"] }
|
||||||
|
|
||||||
[target.'cfg(target_os = "macos")'.dependencies]
|
[target.'cfg(target_os = "macos")'.dependencies]
|
||||||
eye = { git = "https://github.com/raymanfx/eye-rs", rev = "5b7e3f7a1e79966091692896c568aab042e449ef", default-features = false }
|
nokhwa = { version = "0.10.10", default-features = false, features = ["input-avfoundation", "output-threaded"] }
|
||||||
tls-api-openssl = "0.9.0"
|
|
||||||
|
|
||||||
[target.'cfg(not(target_os = "android"))'.dependencies]
|
[target.'cfg(not(target_os = "android"))'.dependencies]
|
||||||
env_logger = "0.11.3"
|
env_logger = "0.11.3"
|
||||||
winit = { version = "0.30.5" }
|
winit = { version = "0.30.12" }
|
||||||
eframe = { version = "0.29.1", features = ["wgpu", "glow"] }
|
wgpu = { version = "27.0.1" }
|
||||||
|
eframe = { version = "0.33.2", features = ["wgpu"] }
|
||||||
arboard = "3.2.0"
|
arboard = "3.2.0"
|
||||||
rfd = "0.15.0"
|
rfd = "0.17.2"
|
||||||
interprocess = { version = "2.2.1", features = ["tokio"] }
|
interprocess = { version = "2.2.1", features = ["tokio"] }
|
||||||
|
|
||||||
[target.'cfg(target_os = "android")'.dependencies]
|
[target.'cfg(target_os = "android")'.dependencies]
|
||||||
android_logger = "0.14.1"
|
android_logger = "0.15.0"
|
||||||
jni = "0.21.1"
|
jni = "0.21.1"
|
||||||
wgpu = "22.1.0"
|
|
||||||
android-activity = { version = "0.6.0", features = ["game-activity"] }
|
android-activity = { version = "0.6.0", features = ["game-activity"] }
|
||||||
winit = { version = "0.30.5", features = ["android-game-activity"] }
|
winit = { version = "0.30.12", features = ["android-game-activity"] }
|
||||||
eframe = { version = "0.29.1", features = ["wgpu", "android-game-activity"] }
|
eframe = { version = "0.33.2", default-features = false, features = ["glow", "android-game-activity"] }
|
||||||
|
|
||||||
[patch.crates-io]
|
[build-dependencies]
|
||||||
openpnp_capture = { git = "https://github.com/ardocrat/openpnp-capture-rs", rev = "f9b06f627c5e5d42c672d117650af700846ca6cf" }
|
built = "0.8.0"
|
||||||
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" }
|
|
||||||
|
|||||||
@@ -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="img/grin-logo.png"/> <img height="20" src="img/logo.png"/>
|
||||||
Cross-platform GUI for [GRiN ツ](https://grin.mw) in [Rust](https://www.rust-lang.org/)
|
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.
|
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.
|
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.
|
Named by the character [Grim](http://harrypotter.wikia.com/wiki/Grim) - the shape of a large, black, menacing, spectral giant dog.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|
||||||
## Build instructions
|
## Build instructions
|
||||||
@@ -20,6 +20,7 @@ Follow instructions on [Windows](https://forge.rust-lang.org/infra/other-install
|
|||||||
To build and run application go to project directory and run:
|
To build and run application go to project directory and run:
|
||||||
|
|
||||||
```
|
```
|
||||||
|
git submodule update --init --recursive
|
||||||
cargo build --release
|
cargo build --release
|
||||||
./target/release/grim
|
./target/release/grim
|
||||||
```
|
```
|
||||||
@@ -31,7 +32,7 @@ Install Android SDK / NDK / Platform Tools for your OS according to this [FAQ](h
|
|||||||
|
|
||||||
#### Build the project
|
#### Build the project
|
||||||
Run Android emulator or connect a real device. Command `adb devices` should show at least one device.
|
Run Android emulator or connect a real device. Command `adb devices` should show at least one device.
|
||||||
In the root of the repo run `./scripts/build_run_android.sh debug|release v7|v8`, where is `v7`, `v8` - device CPU architecture.
|
In the root of the repo run `./scripts/android.sh build|release v7|v8|x86`, where is `v7`, `v8`, `x86` - device CPU architecture for `build` type, for `release` specify version number in format `major.minor.patch`.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
+58
-13
@@ -3,15 +3,20 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdk 33
|
compileSdk = 36
|
||||||
ndkVersion '26.0.10792818'
|
ndkVersion '29.0.14206865'
|
||||||
|
buildToolsVersion = '36.1.0'
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "mw.gri.android"
|
applicationId "mw.gri.android"
|
||||||
minSdk 24
|
minSdk 24
|
||||||
targetSdk 33
|
targetSdk 36
|
||||||
versionCode 3
|
versionCode 5
|
||||||
versionName "0.2.3"
|
versionName "0.3.6"
|
||||||
|
}
|
||||||
|
|
||||||
|
lint {
|
||||||
|
checkReleaseBuilds false
|
||||||
}
|
}
|
||||||
|
|
||||||
def keystorePropertiesFile = rootProject.file("keystore.properties")
|
def keystorePropertiesFile = rootProject.file("keystore.properties")
|
||||||
@@ -47,21 +52,61 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
sourceCompatibility JavaVersion.VERSION_17
|
||||||
targetCompatibility JavaVersion.VERSION_1_8
|
targetCompatibility JavaVersion.VERSION_17
|
||||||
}
|
}
|
||||||
namespace 'mw.gri.android'
|
namespace 'mw.gri.android'
|
||||||
|
|
||||||
|
flavorDimensions "mode"
|
||||||
|
|
||||||
|
productFlavors {
|
||||||
|
ci {
|
||||||
|
dimension "mode"
|
||||||
|
}
|
||||||
|
local {
|
||||||
|
dimension "mode"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
applicationVariants.all { variant ->
|
||||||
|
def flavor = variant.productFlavors[0].name
|
||||||
|
|
||||||
|
if (flavor == "ci") {
|
||||||
|
repositories {
|
||||||
|
maven {
|
||||||
|
credentials {
|
||||||
|
username "$mavenUser"
|
||||||
|
password "$mavenPassword"
|
||||||
|
}
|
||||||
|
url "$mavenHost/repository/maven-central/"
|
||||||
|
allowInsecureProtocol = true
|
||||||
|
}
|
||||||
|
maven {
|
||||||
|
credentials {
|
||||||
|
username "$mavenUser"
|
||||||
|
password "$mavenPassword"
|
||||||
|
}
|
||||||
|
url "$mavenHost/repository/google-android-maven/"
|
||||||
|
allowInsecureProtocol = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (flavor == "local") {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
implementation 'androidx.appcompat:appcompat:1.7.0'
|
||||||
|
|
||||||
// To use the Games Activity library
|
// To use the Games Activity library
|
||||||
implementation "androidx.games:games-activity:2.0.2"
|
implementation "androidx.games:games-activity:2.0.2"
|
||||||
|
|
||||||
// Android Camera
|
// Android Camera
|
||||||
implementation 'androidx.camera:camera-core:1.2.3'
|
implementation 'androidx.camera:camera-core:1.5.1'
|
||||||
implementation 'androidx.camera:camera-camera2:1.2.3'
|
implementation 'androidx.camera:camera-camera2:1.5.1'
|
||||||
implementation 'androidx.camera:camera-lifecycle:1.2.3'
|
implementation 'androidx.camera:camera-lifecycle:1.5.1'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:tools="http://schemas.android.com/tools" 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-feature android:name="android.hardware.camera" android:required="false"/>
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.EXPAND_STATUS_BAR" />
|
<uses-permission android:name="android.permission.EXPAND_STATUS_BAR" />
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||||
<uses-permission android:name="android.permission.INTERNET"/>
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
<uses-permission android:name="android.permission.CAMERA"/>
|
<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.READ_EXTERNAL_STORAGE" tools:ignore="ScopedStorage"/>
|
||||||
@@ -14,31 +14,34 @@
|
|||||||
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" tools:ignore="ScopedStorage"/>
|
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" tools:ignore="ScopedStorage"/>
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:hardwareAccelerated="true"
|
android:hardwareAccelerated="true"
|
||||||
android:largeHeap="true"
|
android:largeHeap="true"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="Grim"
|
android:label="Grim"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:theme="@style/Theme.Main">
|
android:theme="@style/Theme.Main"
|
||||||
|
android:enableOnBackInvokedCallback="false"
|
||||||
|
android:extractNativeLibs="true"
|
||||||
|
tools:ignore="UnusedAttribute">
|
||||||
|
|
||||||
<receiver android:name=".NotificationActionsReceiver"/>
|
<receiver android:name=".NotificationActionsReceiver"/>
|
||||||
|
|
||||||
<provider
|
<provider
|
||||||
android:name=".FileProvider"
|
android:name=".FileProvider"
|
||||||
android:authorities="mw.gri.android.fileprovider"
|
android:authorities="mw.gri.android.fileprovider"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:grantUriPermissions="true">
|
android:grantUriPermissions="true">
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
android:resource="@xml/paths" />
|
android:resource="@xml/paths" />
|
||||||
</provider>
|
</provider>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:launchMode="singleTask"
|
android:launchMode="singleTask"
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:configChanges="orientation|screenSize"
|
android:configChanges="orientation|screenSize"
|
||||||
android:exported="true">
|
android:exported="true">
|
||||||
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
@@ -51,19 +54,17 @@
|
|||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
<data android:mimeType="text/*" />
|
<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/*" />
|
<data android:mimeType="application/*" />
|
||||||
|
<data android:pathPattern=".*\\.slatepack" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
<meta-data android:name="android.app.lib_name" android:value="grim" />
|
<meta-data android:name="android.app.lib_name" android:value="grim" />
|
||||||
</activity>
|
</activity>
|
||||||
<service android:name=".BackgroundService" android:stopWithTask="true" />
|
|
||||||
|
<service
|
||||||
|
android:name=".BackgroundService"
|
||||||
|
android:stopWithTask="true"
|
||||||
|
android:foregroundServiceType="dataSync" />
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
@@ -2,13 +2,13 @@ package mw.gri.android;
|
|||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.app.*;
|
import android.app.*;
|
||||||
import android.content.BroadcastReceiver;
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.IntentFilter;
|
|
||||||
import android.os.*;
|
import android.os.*;
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.core.app.NotificationCompat;
|
import androidx.core.app.NotificationCompat;
|
||||||
|
import androidx.core.content.ContextCompat;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@ import static android.app.Notification.EXTRA_NOTIFICATION_ID;
|
|||||||
|
|
||||||
public class BackgroundService extends Service {
|
public class BackgroundService extends Service {
|
||||||
private static final String TAG = BackgroundService.class.getSimpleName();
|
private static final String TAG = BackgroundService.class.getSimpleName();
|
||||||
|
|
||||||
private PowerManager.WakeLock mWakeLock;
|
private PowerManager.WakeLock mWakeLock;
|
||||||
|
|
||||||
private final Handler mHandler = new Handler(Looper.getMainLooper());
|
private final Handler mHandler = new Handler(Looper.getMainLooper());
|
||||||
@@ -31,26 +31,6 @@ public class BackgroundService extends Service {
|
|||||||
|
|
||||||
public static final String ACTION_START_NODE = "start_node";
|
public static final String ACTION_START_NODE = "start_node";
|
||||||
public static final String ACTION_STOP_NODE = "stop_node";
|
public static final String ACTION_STOP_NODE = "stop_node";
|
||||||
public static final String ACTION_EXIT = "exit";
|
|
||||||
public static final String ACTION_REFRESH = "refresh";
|
|
||||||
public static final String ACTION_STOP = "stop";
|
|
||||||
|
|
||||||
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
|
|
||||||
@SuppressLint("RestrictedApi")
|
|
||||||
@Override
|
|
||||||
public void onReceive(Context context, Intent intent) {
|
|
||||||
if (intent.getAction().equals(ACTION_STOP)) {
|
|
||||||
mStopped = true;
|
|
||||||
// Remove actions buttons.
|
|
||||||
mNotificationBuilder.mActions.clear();
|
|
||||||
NotificationManager manager = getSystemService(NotificationManager.class);
|
|
||||||
manager.notify(NOTIFICATION_ID, mNotificationBuilder.build());
|
|
||||||
} else {
|
|
||||||
mHandler.removeCallbacks(mUpdateSyncStatus);
|
|
||||||
mHandler.post(mUpdateSyncStatus);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private final Runnable mUpdateSyncStatus = new Runnable() {
|
private final Runnable mUpdateSyncStatus = new Runnable() {
|
||||||
@SuppressLint("RestrictedApi")
|
@SuppressLint("RestrictedApi")
|
||||||
@@ -101,18 +81,6 @@ public class BackgroundService extends Service {
|
|||||||
.getBroadcast(BackgroundService.this, 1, startStopIntent, PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_ONE_SHOT);
|
.getBroadcast(BackgroundService.this, 1, startStopIntent, PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_ONE_SHOT);
|
||||||
mNotificationBuilder.addAction(R.drawable.ic_stop, getStopText(), i);
|
mNotificationBuilder.addAction(R.drawable.ic_stop, getStopText(), i);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set up a button to exit from the app.
|
|
||||||
if (canStart || canStop) {
|
|
||||||
Intent exitIntent = new Intent(BackgroundService.this, NotificationActionsReceiver.class);
|
|
||||||
if (Build.VERSION.SDK_INT > 25) {
|
|
||||||
exitIntent.putExtra(EXTRA_NOTIFICATION_ID, NOTIFICATION_ID);
|
|
||||||
}
|
|
||||||
exitIntent.setAction(ACTION_EXIT);
|
|
||||||
PendingIntent i = PendingIntent
|
|
||||||
.getBroadcast(BackgroundService.this, 1, exitIntent, PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_ONE_SHOT);
|
|
||||||
mNotificationBuilder.addAction(R.drawable.ic_close, getExitText(), i);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update notification.
|
// Update notification.
|
||||||
@@ -170,9 +138,6 @@ public class BackgroundService extends Service {
|
|||||||
|
|
||||||
// Update sync status at notification.
|
// Update sync status at notification.
|
||||||
mHandler.post(mUpdateSyncStatus);
|
mHandler.post(mUpdateSyncStatus);
|
||||||
|
|
||||||
// Register receiver to refresh notifications by intent.
|
|
||||||
registerReceiver(mReceiver, new IntentFilter(ACTION_REFRESH));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -203,7 +168,6 @@ public class BackgroundService extends Service {
|
|||||||
|
|
||||||
// Stop updating the notification.
|
// Stop updating the notification.
|
||||||
mHandler.removeCallbacks(mUpdateSyncStatus);
|
mHandler.removeCallbacks(mUpdateSyncStatus);
|
||||||
unregisterReceiver(mReceiver);
|
|
||||||
clearNotification();
|
clearNotification();
|
||||||
|
|
||||||
// Remove service from foreground state.
|
// Remove service from foreground state.
|
||||||
@@ -226,12 +190,12 @@ public class BackgroundService extends Service {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Start the service.
|
// Start the service.
|
||||||
public static void start(Context context) {
|
public static void start(Context c) {
|
||||||
if (!isServiceRunning(context)) {
|
if (!isServiceRunning(c)) {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
context.startForegroundService(new Intent(context, BackgroundService.class));
|
ContextCompat.startForegroundService(c, new Intent(c, BackgroundService.class));
|
||||||
} else {
|
} else {
|
||||||
context.startService(new Intent(context, BackgroundService.class));
|
c.startService(new Intent(c, BackgroundService.class));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -270,9 +234,6 @@ public class BackgroundService extends Service {
|
|||||||
// Check if stop node is possible.
|
// Check if stop node is possible.
|
||||||
private native boolean canStopNode();
|
private native boolean canStopNode();
|
||||||
|
|
||||||
// Get exit text for notification.
|
|
||||||
private native String getExitText();
|
|
||||||
|
|
||||||
// Check if app from the app is needed after node stop.
|
// Check if app from the app is needed after node stop.
|
||||||
private native boolean exitAppAfterNodeStop();
|
private native boolean exitAppAfterNodeStop();
|
||||||
}
|
}
|
||||||
@@ -12,10 +12,10 @@ import android.os.Process;
|
|||||||
import android.provider.Settings;
|
import android.provider.Settings;
|
||||||
import android.system.ErrnoException;
|
import android.system.ErrnoException;
|
||||||
import android.system.Os;
|
import android.system.Os;
|
||||||
import android.util.Size;
|
import android.util.Log;
|
||||||
import android.view.KeyEvent;
|
import android.view.KeyEvent;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.inputmethod.InputMethodManager;
|
|
||||||
import androidx.activity.result.ActivityResultLauncher;
|
import androidx.activity.result.ActivityResultLauncher;
|
||||||
import androidx.activity.result.contract.ActivityResultContracts;
|
import androidx.activity.result.contract.ActivityResultContracts;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
@@ -28,9 +28,11 @@ import androidx.core.view.DisplayCutoutCompat;
|
|||||||
import androidx.core.view.ViewCompat;
|
import androidx.core.view.ViewCompat;
|
||||||
import androidx.core.view.WindowInsetsCompat;
|
import androidx.core.view.WindowInsetsCompat;
|
||||||
import com.google.androidgamesdk.GameActivity;
|
import com.google.androidgamesdk.GameActivity;
|
||||||
|
import com.google.androidgamesdk.gametextinput.State;
|
||||||
import com.google.common.util.concurrent.ListenableFuture;
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
|
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
|
|
||||||
@@ -50,14 +52,13 @@ public class MainActivity extends GameActivity {
|
|||||||
private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
|
private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
|
||||||
@Override
|
@Override
|
||||||
public void onReceive(Context ctx, Intent i) {
|
public void onReceive(Context ctx, Intent i) {
|
||||||
if (i.getAction().equals(STOP_APP_ACTION)) {
|
if (Objects.equals(i.getAction(), STOP_APP_ACTION)) {
|
||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private final ImageAnalysis mImageAnalysis = new ImageAnalysis.Builder()
|
private final ImageAnalysis mImageAnalysis = new ImageAnalysis.Builder()
|
||||||
.setTargetResolution(new Size(640, 480))
|
|
||||||
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
|
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
@@ -80,15 +81,16 @@ public class MainActivity extends GameActivity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Clear cache on start.
|
// Clear cache on start.
|
||||||
if (savedInstanceState == null) {
|
if (savedInstanceState == null && getExternalCacheDir() != null) {
|
||||||
Utils.deleteDirectoryContent(new File(getExternalCacheDir().getPath()), false);
|
Utils.deleteDirectoryContent(new File(getExternalCacheDir().getPath()), false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setup environment variables for native code.
|
// Setup environment variables for native code.
|
||||||
try {
|
try {
|
||||||
Os.setenv("HOME", getExternalFilesDir("").getPath(), true);
|
Os.setenv("HOME", Objects.requireNonNull(getExternalFilesDir("")).getPath(), true);
|
||||||
Os.setenv("XDG_CACHE_HOME", getExternalCacheDir().getPath(), true);
|
Os.setenv("XDG_CACHE_HOME", Objects.requireNonNull(getExternalCacheDir()).getPath(), true);
|
||||||
Os.setenv("ARTI_FS_DISABLE_PERMISSION_CHECKS", "true", true);
|
Os.setenv("ARTI_FS_DISABLE_PERMISSION_CHECKS", "true", true);
|
||||||
|
Os.setenv("NATIVE_LIBS_DIR", getApplicationInfo().nativeLibraryDir, true);
|
||||||
} catch (ErrnoException e) {
|
} catch (ErrnoException e) {
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
@@ -96,7 +98,7 @@ public class MainActivity extends GameActivity {
|
|||||||
super.onCreate(null);
|
super.onCreate(null);
|
||||||
|
|
||||||
// Register receiver to finish activity from the BackgroundService.
|
// Register receiver to finish activity from the BackgroundService.
|
||||||
registerReceiver(mBroadcastReceiver, new IntentFilter(STOP_APP_ACTION));
|
ContextCompat.registerReceiver(this, mBroadcastReceiver, new IntentFilter(STOP_APP_ACTION), ContextCompat.RECEIVER_NOT_EXPORTED);
|
||||||
|
|
||||||
// Register associated file opening result.
|
// Register associated file opening result.
|
||||||
mOpenFilePermissionsResult = registerForActivityResult(
|
mOpenFilePermissionsResult = registerForActivityResult(
|
||||||
@@ -119,19 +121,21 @@ public class MainActivity extends GameActivity {
|
|||||||
Intent data = result.getData();
|
Intent data = result.getData();
|
||||||
if (resultCode == Activity.RESULT_OK) {
|
if (resultCode == Activity.RESULT_OK) {
|
||||||
String path = "";
|
String path = "";
|
||||||
if (data != null) {
|
if (data != null && data.getData() != null) {
|
||||||
Uri uri = data.getData();
|
Uri uri = data.getData();
|
||||||
String name = "pick" + Utils.getFileExtension(uri, this);
|
String name = "pick" + Utils.getFileExtension(uri, this);
|
||||||
File file = new File(getExternalCacheDir(), name);
|
File file = new File(getExternalCacheDir(), name);
|
||||||
try (InputStream is = getContentResolver().openInputStream(uri);
|
try (InputStream is = getContentResolver().openInputStream(uri);
|
||||||
OutputStream os = new FileOutputStream(file)) {
|
OutputStream os = new FileOutputStream(file)) {
|
||||||
byte[] buffer = new byte[1024];
|
byte[] buffer = new byte[1024];
|
||||||
int length;
|
int length;
|
||||||
while ((length = is.read(buffer)) > 0) {
|
while (true) {
|
||||||
os.write(buffer, 0, length);
|
assert is != null;
|
||||||
}
|
if (!((length = is.read(buffer)) > 0)) break;
|
||||||
|
os.write(buffer, 0, length);
|
||||||
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
Log.e("grim", e.toString());
|
||||||
}
|
}
|
||||||
path = file.getPath();
|
path = file.getPath();
|
||||||
}
|
}
|
||||||
@@ -193,6 +197,9 @@ public class MainActivity extends GameActivity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pass display insets into native code.
|
||||||
|
public native void onDisplayInsets(int[] cutouts);
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onNewIntent(Intent intent) {
|
protected void onNewIntent(Intent intent) {
|
||||||
super.onNewIntent(intent);
|
super.onNewIntent(intent);
|
||||||
@@ -221,6 +228,7 @@ public class MainActivity extends GameActivity {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
ParcelFileDescriptor parcelFile = getContentResolver().openFileDescriptor(data, "r");
|
ParcelFileDescriptor parcelFile = getContentResolver().openFileDescriptor(data, "r");
|
||||||
|
assert parcelFile != null;
|
||||||
FileReader fileReader = new FileReader(parcelFile.getFileDescriptor());
|
FileReader fileReader = new FileReader(parcelFile.getFileDescriptor());
|
||||||
BufferedReader reader = new BufferedReader(fileReader);
|
BufferedReader reader = new BufferedReader(fileReader);
|
||||||
String line;
|
String line;
|
||||||
@@ -234,7 +242,7 @@ public class MainActivity extends GameActivity {
|
|||||||
// Provide file content into native code.
|
// Provide file content into native code.
|
||||||
onData(buff.toString());
|
onData(buff.toString());
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
Log.e("grim", e.toString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -257,54 +265,99 @@ public class MainActivity extends GameActivity {
|
|||||||
if (results.length != 0 && results[0] == PackageManager.PERMISSION_GRANTED) {
|
if (results.length != 0 && results[0] == PackageManager.PERMISSION_GRANTED) {
|
||||||
switch (requestCode) {
|
switch (requestCode) {
|
||||||
case NOTIFICATIONS_PERMISSION_CODE: {
|
case NOTIFICATIONS_PERMISSION_CODE: {
|
||||||
// Start notification service.
|
|
||||||
BackgroundService.start(this);
|
BackgroundService.start(this);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
case CAMERA_PERMISSION_CODE: {
|
case CAMERA_PERMISSION_CODE: {
|
||||||
// Start camera.
|
|
||||||
startCamera();
|
startCamera();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onTextInputEventNative(long l, State state) {
|
||||||
|
super.onTextInputEventNative(l, state);
|
||||||
|
if (state.selectionEnd > state.composingRegionStart && state.composingRegionStart >= 0) {
|
||||||
|
String input = String.valueOf(state.text.charAt(state.composingRegionStart));
|
||||||
|
if (input.contains("\n")) {
|
||||||
|
onEnterInput();
|
||||||
|
} else {
|
||||||
|
onTextInput(input);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean dispatchKeyEvent(KeyEvent event) {
|
public boolean dispatchKeyEvent(KeyEvent event) {
|
||||||
// To support non-english input.
|
if (event.getAction() == KeyEvent.ACTION_DOWN) {
|
||||||
if (event.getAction() == KeyEvent.ACTION_MULTIPLE && event.getKeyCode() == KeyEvent.KEYCODE_UNKNOWN) {
|
if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
|
||||||
if (!event.getCharacters().isEmpty()) {
|
onBack();
|
||||||
onInput(event.getCharacters());
|
return false;
|
||||||
|
} else if (event.getKeyCode() == KeyEvent.KEYCODE_DEL) {
|
||||||
|
onClearInput();
|
||||||
|
return false;
|
||||||
|
} else if (event.getKeyCode() == KeyEvent.KEYCODE_ENTER) {
|
||||||
|
onEnterInput();
|
||||||
|
return false;
|
||||||
|
} else if (event.getKeyCode() == KeyEvent.KEYCODE_0) {
|
||||||
|
onTextInput("0");
|
||||||
|
return false;
|
||||||
|
} else if (event.getKeyCode() == KeyEvent.KEYCODE_1) {
|
||||||
|
onTextInput("1");
|
||||||
|
return false;
|
||||||
|
} else if (event.getKeyCode() == KeyEvent.KEYCODE_2) {
|
||||||
|
onTextInput("2");
|
||||||
|
return false;
|
||||||
|
} else if (event.getKeyCode() == KeyEvent.KEYCODE_3) {
|
||||||
|
onTextInput("3");
|
||||||
|
return false;
|
||||||
|
} else if (event.getKeyCode() == KeyEvent.KEYCODE_4) {
|
||||||
|
onTextInput("4");
|
||||||
|
return false;
|
||||||
|
} else if (event.getKeyCode() == KeyEvent.KEYCODE_5) {
|
||||||
|
onTextInput("5");
|
||||||
|
return false;
|
||||||
|
} else if (event.getKeyCode() == KeyEvent.KEYCODE_6) {
|
||||||
|
onTextInput("6");
|
||||||
|
return false;
|
||||||
|
} else if (event.getKeyCode() == KeyEvent.KEYCODE_7) {
|
||||||
|
onTextInput("7");
|
||||||
|
return false;
|
||||||
|
} else if (event.getKeyCode() == KeyEvent.KEYCODE_8) {
|
||||||
|
onTextInput("8");
|
||||||
|
return false;
|
||||||
|
} else if (event.getKeyCode() == KeyEvent.KEYCODE_9) {
|
||||||
|
onTextInput("9");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// Pass any other input values into native code.
|
} else if (event.getAction() == KeyEvent.ACTION_MULTIPLE && event.getKeyCode() == KeyEvent.KEYCODE_UNKNOWN) {
|
||||||
|
if (!event.getCharacters().isEmpty()) {
|
||||||
|
onTextInput(event.getCharacters());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Pass any other input values into native code.
|
||||||
} else if (event.getAction() == KeyEvent.ACTION_UP &&
|
} else if (event.getAction() == KeyEvent.ACTION_UP &&
|
||||||
event.getKeyCode() != KeyEvent.KEYCODE_ENTER &&
|
event.getKeyCode() != KeyEvent.KEYCODE_ENTER &&
|
||||||
event.getKeyCode() != KeyEvent.KEYCODE_BACK) {
|
event.getKeyCode() != KeyEvent.KEYCODE_BACK) {
|
||||||
onInput(String.valueOf((char)event.getUnicodeChar()));
|
onTextInput(String.valueOf((char)event.getUnicodeChar()));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return super.dispatchKeyEvent(event);
|
return super.dispatchKeyEvent(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Provide last entered character from soft keyboard into native code.
|
// Pass back navigation event into native code.
|
||||||
public native void onInput(String character);
|
|
||||||
|
|
||||||
// Implemented into native code to handle display insets change.
|
|
||||||
native void onDisplayInsets(int[] cutouts);
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onKeyDown(int keyCode, KeyEvent event) {
|
|
||||||
if (keyCode == KeyEvent.KEYCODE_BACK) {
|
|
||||||
onBack();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return super.onKeyDown(keyCode, event);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Implemented into native code to handle key code BACK event.
|
|
||||||
public native void onBack();
|
public native void onBack();
|
||||||
|
|
||||||
|
// Pass clear key event into native code.
|
||||||
|
public native void onClearInput();
|
||||||
|
|
||||||
|
// Pass enter key event into native code.
|
||||||
|
public native void onEnterInput();
|
||||||
|
|
||||||
|
// Pass last entered character from soft keyboard into native code.
|
||||||
|
public native void onTextInput(String character);
|
||||||
|
|
||||||
// Called from native code to exit app.
|
// Called from native code to exit app.
|
||||||
public void exit() {
|
public void exit() {
|
||||||
finishAndRemoveTask();
|
finishAndRemoveTask();
|
||||||
@@ -342,31 +395,21 @@ public class MainActivity extends GameActivity {
|
|||||||
// Called from native code to get text from clipboard.
|
// Called from native code to get text from clipboard.
|
||||||
public String pasteText() {
|
public String pasteText() {
|
||||||
ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
|
ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
|
||||||
String text;
|
ClipDescription desc = clipboard.getPrimaryClipDescription();
|
||||||
|
ClipData data = clipboard.getPrimaryClip();
|
||||||
|
String text = "";
|
||||||
if (!(clipboard.hasPrimaryClip())) {
|
if (!(clipboard.hasPrimaryClip())) {
|
||||||
text = "";
|
text = "";
|
||||||
} else if (!(clipboard.getPrimaryClipDescription().hasMimeType(MIMETYPE_TEXT_PLAIN))
|
} else if (desc != null && (!(desc.hasMimeType(MIMETYPE_TEXT_PLAIN))
|
||||||
&& !(clipboard.getPrimaryClipDescription().hasMimeType(MIMETYPE_TEXT_HTML))) {
|
&& !(desc.hasMimeType(MIMETYPE_TEXT_HTML)))) {
|
||||||
text = "";
|
text = "";
|
||||||
} else {
|
} else if (data != null) {
|
||||||
ClipData.Item item = clipboard.getPrimaryClip().getItemAt(0);
|
ClipData.Item item = data.getItemAt(0);
|
||||||
text = item.getText().toString();
|
text = item.getText().toString();
|
||||||
}
|
}
|
||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Called from native code to show keyboard.
|
|
||||||
public void showKeyboard() {
|
|
||||||
InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
|
|
||||||
imm.showSoftInput(getWindow().getDecorView(), InputMethodManager.SHOW_IMPLICIT);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Called from native code to hide keyboard.
|
|
||||||
public void hideKeyboard() {
|
|
||||||
InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
|
|
||||||
imm.hideSoftInputFromWindow(getWindow().getDecorView().getWindowToken(), 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Called from native code to start camera.
|
// Called from native code to start camera.
|
||||||
public void startCamera() {
|
public void startCamera() {
|
||||||
String notificationsPermission = Manifest.permission.CAMERA;
|
String notificationsPermission = Manifest.permission.CAMERA;
|
||||||
@@ -456,7 +499,7 @@ public class MainActivity extends GameActivity {
|
|||||||
Uri uri = FileProvider.getUriForFile(this, "mw.gri.android.fileprovider", file);
|
Uri uri = FileProvider.getUriForFile(this, "mw.gri.android.fileprovider", file);
|
||||||
Intent intent = new Intent(Intent.ACTION_SEND);
|
Intent intent = new Intent(Intent.ACTION_SEND);
|
||||||
intent.putExtra(Intent.EXTRA_STREAM, uri);
|
intent.putExtra(Intent.EXTRA_STREAM, uri);
|
||||||
intent.setType("*/*");
|
intent.setType("text/*");
|
||||||
startActivity(Intent.createChooser(intent, "Share data"));
|
startActivity(Intent.createChooser(intent, "Share data"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -469,7 +512,7 @@ public class MainActivity extends GameActivity {
|
|||||||
// Called from native code to pick the file.
|
// Called from native code to pick the file.
|
||||||
public void pickFile() {
|
public void pickFile() {
|
||||||
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
|
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
|
||||||
intent.setType("*/*");
|
intent.setType("text/*");
|
||||||
try {
|
try {
|
||||||
mFilePickResult.launch(Intent.createChooser(intent, "Pick file"));
|
mFilePickResult.launch(Intent.createChooser(intent, "Pick file"));
|
||||||
} catch (android.content.ActivityNotFoundException ex) {
|
} catch (android.content.ActivityNotFoundException ex) {
|
||||||
|
|||||||
@@ -4,23 +4,18 @@ import android.content.BroadcastReceiver;
|
|||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
public class NotificationActionsReceiver extends BroadcastReceiver {
|
public class NotificationActionsReceiver extends BroadcastReceiver {
|
||||||
@Override
|
@Override
|
||||||
public void onReceive(Context context, Intent i) {
|
public void onReceive(Context context, Intent i) {
|
||||||
String a = i.getAction();
|
String a = i.getAction();
|
||||||
if (a.equals(BackgroundService.ACTION_START_NODE)) {
|
if (Objects.equals(a, BackgroundService.ACTION_START_NODE)) {
|
||||||
startNode();
|
startNode();
|
||||||
context.sendBroadcast(new Intent(BackgroundService.ACTION_REFRESH));
|
} else if (Objects.equals(a, BackgroundService.ACTION_STOP_NODE)) {
|
||||||
} else if (a.equals(BackgroundService.ACTION_STOP_NODE)) {
|
|
||||||
stopNode();
|
stopNode();
|
||||||
context.sendBroadcast(new Intent(BackgroundService.ACTION_REFRESH));
|
|
||||||
} else {
|
} else {
|
||||||
if (isNodeRunning()) {
|
stopNodeToExit();
|
||||||
stopNodeToExit();
|
|
||||||
context.sendBroadcast(new Intent(BackgroundService.ACTION_REFRESH));
|
|
||||||
} else {
|
|
||||||
context.sendBroadcast(new Intent(MainActivity.STOP_APP_ACTION));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,6 +25,4 @@ public class NotificationActionsReceiver extends BroadcastReceiver {
|
|||||||
native void stopNode();
|
native void stopNode();
|
||||||
// Stop node and exit from the app.
|
// Stop node and exit from the app.
|
||||||
native void stopNodeToExit();
|
native void stopNodeToExit();
|
||||||
// Check if node is running.
|
|
||||||
native boolean isNodeRunning();
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -153,4 +153,4 @@ public class Utils {
|
|||||||
String fileType = context.getContentResolver().getType(uri);
|
String fileType = context.getContentResolver().getType(uri);
|
||||||
return MimeTypeMap.getSingleton().getExtensionFromMimeType(fileType);
|
return MimeTypeMap.getSingleton().getExtensionFromMimeType(fileType);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
<item name="android:statusBarColor">@color/yellow</item>
|
<item name="android:statusBarColor">@color/yellow</item>
|
||||||
<item name="android:windowLightStatusBar">true</item>
|
<item name="android:windowLightStatusBar">true</item>
|
||||||
<item name="android:navigationBarColor">@color/black</item>
|
<item name="android:navigationBarColor">@color/black</item>
|
||||||
|
<item name="android:windowLightNavigationBar" tools:targetApi="27">false</item>
|
||||||
<item name="android:windowLayoutInDisplayCutoutMode" tools:targetApi="o_mr1">shortEdges</item>
|
<item name="android:windowLayoutInDisplayCutoutMode" tools:targetApi="o_mr1">shortEdges</item>
|
||||||
</style>
|
</style>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||||
plugins {
|
plugins {
|
||||||
id 'com.android.application' version '8.6.1' apply false
|
id 'com.android.application' version '8.10.0' apply false
|
||||||
id 'com.android.library' version '8.6.1' apply false
|
id 'com.android.library' version '8.10.0' apply false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||||
# Specifies the JVM arguments used for the daemon process.
|
# Specifies the JVM arguments used for the daemon process.
|
||||||
# The setting is particularly useful for tweaking memory settings.
|
# The setting is particularly useful for tweaking memory settings.
|
||||||
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
org.gradle.jvmargs=-Xmx4096m -Dfile.encoding=UTF-8
|
||||||
# When configured, Gradle will run in incubating parallel mode.
|
# When configured, Gradle will run in incubating parallel mode.
|
||||||
# This option should only be used with decoupled projects. More details, visit
|
# This option should only be used with decoupled projects. More details, visit
|
||||||
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
#Mon May 02 15:39:12 BST 2022
|
#Mon May 02 15:39:12 BST 2022
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
|
distributionUrl=https\://code.gri.mw/DEV/gradle/releases/download/v8.11.1/gradle-8.11.1-bin.zip
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
|||||||
+25
-12
@@ -1,16 +1,29 @@
|
|||||||
pluginManagement {
|
pluginManagement {
|
||||||
repositories {
|
repositories {
|
||||||
gradlePluginPortal()
|
maven {
|
||||||
google()
|
credentials {
|
||||||
mavenCentral()
|
username "$mavenUser"
|
||||||
|
password "$mavenPassword"
|
||||||
|
}
|
||||||
|
url "$mavenHost/repository/gradle-plugin-portal/"
|
||||||
|
allowInsecureProtocol = true
|
||||||
|
}
|
||||||
|
maven {
|
||||||
|
credentials {
|
||||||
|
username "$mavenUser"
|
||||||
|
password "$mavenPassword"
|
||||||
|
}
|
||||||
|
url "$mavenHost/repository/google-maven/"
|
||||||
|
allowInsecureProtocol = true
|
||||||
|
}
|
||||||
|
maven {
|
||||||
|
credentials {
|
||||||
|
username "$mavenUser"
|
||||||
|
password "$mavenPassword"
|
||||||
|
}
|
||||||
|
url "$mavenHost/repository/maven-central/"
|
||||||
|
allowInsecureProtocol = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
dependencyResolutionManagement {
|
include ':app'
|
||||||
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
|
||||||
repositories {
|
|
||||||
google()
|
|
||||||
mavenCentral()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//rootProject.name = "Rust Template"
|
|
||||||
include ':app'
|
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
use std::path::PathBuf;
|
||||||
|
use std::process::Command;
|
||||||
|
use std::{env, fs};
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
built::write_built_file().expect("Failed to acquire build-time information");
|
||||||
|
|
||||||
|
// Setting up git hooks in the project: rustfmt and so on.
|
||||||
|
let git_hooks = format!(
|
||||||
|
"git config core.hooksPath {}",
|
||||||
|
PathBuf::from("./.hooks").to_str().unwrap()
|
||||||
|
);
|
||||||
|
|
||||||
|
if cfg!(target_os = "windows") {
|
||||||
|
Command::new("cmd")
|
||||||
|
.args(&["/C", &git_hooks])
|
||||||
|
.output()
|
||||||
|
.expect("failed to execute git config for hooks");
|
||||||
|
} else {
|
||||||
|
Command::new("sh")
|
||||||
|
.args(&["-c", &git_hooks])
|
||||||
|
.output()
|
||||||
|
.expect("failed to execute git config for hooks");
|
||||||
|
}
|
||||||
|
|
||||||
|
let out_dir = env::var("OUT_DIR").unwrap();
|
||||||
|
let tor_out_dir = format!("{}/tor", out_dir);
|
||||||
|
let mut webtunnel_file = format!("{}/webtunnel", tor_out_dir);
|
||||||
|
let exists = fs::exists(&webtunnel_file).unwrap();
|
||||||
|
if !exists {
|
||||||
|
// Create empty webtunnel file to allow build with include_bytes! macro.
|
||||||
|
fs::create_dir(&tor_out_dir).unwrap_or_default();
|
||||||
|
fs::File::create(&webtunnel_file).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
let target = env::var("TARGET").unwrap();
|
||||||
|
let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap();
|
||||||
|
if target_os == "ios" {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let is_android = target_os == "android";
|
||||||
|
if is_android {
|
||||||
|
// Set a path to Android Webtunnel binary.
|
||||||
|
let arch = if target.contains("aarch64") {
|
||||||
|
"arm64-v8a"
|
||||||
|
} else if target.contains("arm") {
|
||||||
|
"armeabi-v7a"
|
||||||
|
} else {
|
||||||
|
"x86_64"
|
||||||
|
};
|
||||||
|
let root = env::var("CARGO_MANIFEST_DIR").unwrap();
|
||||||
|
webtunnel_file = format!(
|
||||||
|
"{}/android/app/src/main/jniLibs/{}/libwebtunnel.so",
|
||||||
|
root, arch
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build if Webtunnel binary is empty or not exists.
|
||||||
|
let empty = match fs::File::open(&webtunnel_file) {
|
||||||
|
Ok(file) => file.metadata().unwrap().len() == 0,
|
||||||
|
Err(_) => true,
|
||||||
|
};
|
||||||
|
let build = !exists || empty;
|
||||||
|
if build {
|
||||||
|
// Setup GOOS env variable.
|
||||||
|
let go_os = if target_os == "macos" {
|
||||||
|
"darwin"
|
||||||
|
} else {
|
||||||
|
target_os.as_str()
|
||||||
|
};
|
||||||
|
// Setup GOARCH env variable.
|
||||||
|
let go_arch = if target.contains("aarch64") {
|
||||||
|
"arm64"
|
||||||
|
} else if target.contains("arm") {
|
||||||
|
"arm"
|
||||||
|
} else {
|
||||||
|
"amd64"
|
||||||
|
};
|
||||||
|
// Run Webtunnel Go build.
|
||||||
|
let output = if env::consts::OS == "windows" {
|
||||||
|
Command::new("./scripts/webtunnel.bat")
|
||||||
|
.arg(go_os)
|
||||||
|
.arg(go_arch)
|
||||||
|
.arg(webtunnel_file)
|
||||||
|
.output()
|
||||||
|
} else {
|
||||||
|
Command::new("bash")
|
||||||
|
.arg("./scripts/webtunnel.sh")
|
||||||
|
.arg(go_os)
|
||||||
|
.arg(go_arch)
|
||||||
|
.arg(webtunnel_file)
|
||||||
|
.output()
|
||||||
|
};
|
||||||
|
if let Ok(out) = output {
|
||||||
|
if out.status.code().is_none() || out.status.code().unwrap() != 0 {
|
||||||
|
panic!("webtunnel go build failed:\n{:?}", out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 191 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 37 KiB |
@@ -4,7 +4,7 @@ case $2 in
|
|||||||
x86_64|arm)
|
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
|
exit 1
|
||||||
esac
|
esac
|
||||||
|
|
||||||
@@ -17,9 +17,11 @@ cd ..
|
|||||||
[[ $2 == "x86_64" ]] && arch+=(x86_64-unknown-linux-gnu)
|
[[ $2 == "x86_64" ]] && arch+=(x86_64-unknown-linux-gnu)
|
||||||
[[ $2 == "arm" ]] && arch+=(aarch64-unknown-linux-gnu)
|
[[ $2 == "arm" ]] && arch+=(aarch64-unknown-linux-gnu)
|
||||||
|
|
||||||
cargo build --release --target ${arch}
|
rustup target add ${arch}
|
||||||
|
cargo install cargo-zigbuild
|
||||||
|
cargo zigbuild --release --target ${arch}
|
||||||
|
|
||||||
# Create AppImage with https://github.com/AppImage/appimagetool
|
# Create AppImage with https://github.com/AppImage/appimagetool
|
||||||
cp target/${arch}/release/grim linux/Grim.AppDir/AppRun
|
cp target/${arch}/release/grim linux/Grim.AppDir/AppRun
|
||||||
rm target/${arch}/release/*.AppImage
|
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
|
||||||
+70
-4
@@ -25,10 +25,20 @@ share: teilen
|
|||||||
theme: 'Theme:'
|
theme: 'Theme:'
|
||||||
dark: Dunkel
|
dark: Dunkel
|
||||||
light: Hell
|
light: Hell
|
||||||
|
file: Datei
|
||||||
choose_file: Datei auswählen
|
choose_file: Datei auswählen
|
||||||
|
choose_folder: Ordner auswählen
|
||||||
crash_report: Absturzbericht
|
crash_report: Absturzbericht
|
||||||
crash_report_warning: Anwendung wurde beim letzten Mal unerwartet geschlossen, Sie können den Absturzbericht mit Entwicklern teilen.
|
crash_report_warning: Anwendung wurde beim letzten Mal unerwartet geschlossen, Sie können den Absturzbericht mit Entwicklern teilen.
|
||||||
confirmation: Bestätigung
|
confirmation: Bestätigung
|
||||||
|
enter_url: URL eingeben
|
||||||
|
max_short: MAX
|
||||||
|
files_location: Dateistandort
|
||||||
|
moving_files: Dateien verschieben
|
||||||
|
wrong_path_error: Falscher Weg angegeben
|
||||||
|
check_updates: Suchen Sie beim Start nach Updates
|
||||||
|
update_available: Update ist verfügbar!
|
||||||
|
changelog: 'Wechselbuch:'
|
||||||
wallets:
|
wallets:
|
||||||
await_conf_amount: Erwarte Bestätigung
|
await_conf_amount: Erwarte Bestätigung
|
||||||
await_fin_amount: Warten auf die Fertigstellung
|
await_fin_amount: Warten auf die Fertigstellung
|
||||||
@@ -83,6 +93,7 @@ wallets:
|
|||||||
tx_canceled: Abgebrochen
|
tx_canceled: Abgebrochen
|
||||||
tx_cancelling: Abbrechen
|
tx_cancelling: Abbrechen
|
||||||
tx_finalizing: Finalisierung
|
tx_finalizing: Finalisierung
|
||||||
|
tx_posting: Buchungsvorgang
|
||||||
tx_confirmed: Bestätigt
|
tx_confirmed: Bestätigt
|
||||||
txs: Transaktionen
|
txs: Transaktionen
|
||||||
tx: Transaktion
|
tx: Transaktion
|
||||||
@@ -126,6 +137,12 @@ wallets:
|
|||||||
tx_receive_cancel_conf: 'Sind Sie sicher, dass Sie das Empfangen von %{amount} ツ abbrechen wollen?'
|
tx_receive_cancel_conf: 'Sind Sie sicher, dass Sie das Empfangen von %{amount} ツ abbrechen wollen?'
|
||||||
rec_phrase_not_found: Wiederhestellungsphrase nicht gefunden.
|
rec_phrase_not_found: Wiederhestellungsphrase nicht gefunden.
|
||||||
restore_wallet_desc: Stellen Sie das Wallet wieder her, indem Sie alle Dateien löschen. Wenn die normale Reparatur nicht geholfen hat, müssen Sie Ihr Wallet erneut öffnen.
|
restore_wallet_desc: Stellen Sie das Wallet wieder her, indem Sie alle Dateien löschen. Wenn die normale Reparatur nicht geholfen hat, müssen Sie Ihr Wallet erneut öffnen.
|
||||||
|
fee_base_desc: 'Gebühr (basiswert%{value}):'
|
||||||
|
payment_proof: Zahlungsnachweis
|
||||||
|
payment_proof_desc: 'Geben Sie den erhaltenen Zahlungsnachweis ein, um die Transaktion zu verifizieren:'
|
||||||
|
payment_proof_valid: 'Der eingegebene Zahlungsnachweis ist gültig:'
|
||||||
|
payment_proof_error: 'Der eingetragene Zahlungsnachweis ist nicht gültig:'
|
||||||
|
tx_delete_confirmation: Bist du sicher, dass du die Transaktion aus dem Verlauf löschen möchtest?
|
||||||
transport:
|
transport:
|
||||||
desc: 'Transport verwenden, um Nachrichten synchron zu empfangen oder zu senden:'
|
desc: 'Transport verwenden, um Nachrichten synchron zu empfangen oder zu senden:'
|
||||||
tor_network: Tor Netzwek
|
tor_network: Tor Netzwek
|
||||||
@@ -135,10 +152,11 @@ transport:
|
|||||||
conn_error: Verbindungsproblem
|
conn_error: Verbindungsproblem
|
||||||
disconnected: Verbindung getrennt
|
disconnected: Verbindung getrennt
|
||||||
receiver_address: 'Empfängeraddresse:'
|
receiver_address: 'Empfängeraddresse:'
|
||||||
|
sender_address: 'Absenderadresse:'
|
||||||
incorrect_addr_err: 'Eingegebene Addresse ist inkorrekt:'
|
incorrect_addr_err: 'Eingegebene Addresse ist inkorrekt:'
|
||||||
tor_send_error: Beim Senden über Tor ist ein Fehler aufgetreten. Stellen Sie sicher, dass der Empfänger online ist. Die Transaktion wurde abgebrochen.
|
tor_send_error: Beim Senden über Tor ist ein Fehler aufgetreten. Stellen Sie sicher, dass der Empfänger online ist. Die Transaktion wurde abgebrochen.
|
||||||
tor_autorun_desc: Gibt an, ob beim Öffnen des Wallets der Tor-Dienst gestartet werden soll, um Transaktionen synchron zu empfangen.
|
tor_autorun_desc: Gibt an, ob beim Öffnen des Wallets der Tor-Dienst gestartet werden soll, um Transaktionen synchron zu empfangen.
|
||||||
tor_sending: 'Sende %{amount} ツ über Tor'
|
tor_sending: Sende über Tor
|
||||||
tor_settings: Tor Einstellungen
|
tor_settings: Tor Einstellungen
|
||||||
bridges: Brücken
|
bridges: Brücken
|
||||||
bridges_desc: Richten Sie Brücken ein, um die Zensur des Tor-Netzwerks zu umgehen, wenn die normale Verbindung nicht funktioniert.
|
bridges_desc: Richten Sie Brücken ein, um die Zensur des Tor-Netzwerks zu umgehen, wenn die normale Verbindung nicht funktioniert.
|
||||||
@@ -283,12 +301,60 @@ network_settings:
|
|||||||
ban_window_desc: Die Entscheidung über das Verbot trifft der Knoten auf der Grundlage der Korrektheit der von der Gegenstelle erhaltenen Daten.
|
ban_window_desc: Die Entscheidung über das Verbot trifft der Knoten auf der Grundlage der Korrektheit der von der Gegenstelle erhaltenen Daten.
|
||||||
max_inbound_count: 'Maximale Anzahl der eingehenden Peer-Verbindungen:'
|
max_inbound_count: 'Maximale Anzahl der eingehenden Peer-Verbindungen:'
|
||||||
max_outbound_count: 'Maximale Anzahl von ausgehenden Peer-Verbindungen:'
|
max_outbound_count: 'Maximale Anzahl von ausgehenden Peer-Verbindungen:'
|
||||||
reset_peers_desc: Peer-Daten zurücksetzen. Verwenden Sie diese Funktion nur, wenn es Probleme beim finden von Peers gibt.
|
reset_data_desc: Reset-Knotendaten. Verwenden Sie diese Funktion nur, wenn es Probleme mit der Synchronisation gibt.
|
||||||
reset_peers: Peers zurücksetzten
|
reset_data: Daten zurücksetzten
|
||||||
|
ip_listen_all: Hören Sie auf allen Schnittstellen
|
||||||
modal:
|
modal:
|
||||||
cancel: Abbrechen
|
cancel: Abbrechen
|
||||||
save: Speichern
|
save: Speichern
|
||||||
add: Hinzufügen
|
add: Hinzufügen
|
||||||
modal_exit:
|
modal_exit:
|
||||||
description: Sind Sie sicher, dass Sie die Anwendung beenden wollen?
|
description: Sind Sie sicher, dass Sie die Anwendung beenden wollen?
|
||||||
exit: Schließen
|
exit: Schließen
|
||||||
|
app_settings:
|
||||||
|
proxy: Proxy
|
||||||
|
proxy_desc: Lohnt es sich, einen Proxy für Netzwerkanfragen von der Anwendung zu verwenden.
|
||||||
|
keyboard:
|
||||||
|
1: 1
|
||||||
|
2: 2
|
||||||
|
3: 3
|
||||||
|
4: 4
|
||||||
|
5: 5
|
||||||
|
6: 6
|
||||||
|
7: 7
|
||||||
|
8: 8
|
||||||
|
9: 9
|
||||||
|
0: 0
|
||||||
|
01: ß
|
||||||
|
q: q
|
||||||
|
w: w
|
||||||
|
e: e
|
||||||
|
r: r
|
||||||
|
t: t
|
||||||
|
y: z
|
||||||
|
u: u
|
||||||
|
i: i
|
||||||
|
o: o
|
||||||
|
p: p
|
||||||
|
p1: ü
|
||||||
|
a: a
|
||||||
|
s: s
|
||||||
|
d: d
|
||||||
|
f: f
|
||||||
|
g: g
|
||||||
|
h: h
|
||||||
|
j: j
|
||||||
|
k: k
|
||||||
|
l: l
|
||||||
|
l1: ö
|
||||||
|
l2: ä
|
||||||
|
z: y
|
||||||
|
x: x
|
||||||
|
c: c
|
||||||
|
v: v
|
||||||
|
b: b
|
||||||
|
n: n
|
||||||
|
m: m
|
||||||
|
m1: ','
|
||||||
|
m2: .
|
||||||
|
m3: '/'
|
||||||
+70
-4
@@ -25,10 +25,20 @@ share: Share
|
|||||||
theme: 'Theme:'
|
theme: 'Theme:'
|
||||||
dark: Dark
|
dark: Dark
|
||||||
light: Light
|
light: Light
|
||||||
|
file: File
|
||||||
choose_file: Choose file
|
choose_file: Choose file
|
||||||
|
choose_folder: Choose folder
|
||||||
crash_report: Crash report
|
crash_report: Crash report
|
||||||
crash_report_warning: Application closed unexpectedly last time, you can share crash report with developers.
|
crash_report_warning: Application closed unexpectedly last time, you can share crash report with developers.
|
||||||
confirmation: Confirmation
|
confirmation: Confirmation
|
||||||
|
enter_url: Enter URL
|
||||||
|
max_short: MAX
|
||||||
|
files_location: Files location
|
||||||
|
moving_files: Moving files
|
||||||
|
wrong_path_error: Wrong path specified
|
||||||
|
check_updates: Check for updates at startup
|
||||||
|
update_available: Update is available!
|
||||||
|
changelog: 'Changelog:'
|
||||||
wallets:
|
wallets:
|
||||||
await_conf_amount: Awaiting confirmation
|
await_conf_amount: Awaiting confirmation
|
||||||
await_fin_amount: Awaiting finalization
|
await_fin_amount: Awaiting finalization
|
||||||
@@ -83,6 +93,7 @@ wallets:
|
|||||||
tx_canceled: Canceled
|
tx_canceled: Canceled
|
||||||
tx_cancelling: Cancelling
|
tx_cancelling: Cancelling
|
||||||
tx_finalizing: Finalizing
|
tx_finalizing: Finalizing
|
||||||
|
tx_posting: Posting
|
||||||
tx_confirmed: Confirmed
|
tx_confirmed: Confirmed
|
||||||
txs: Transactions
|
txs: Transactions
|
||||||
tx: Transaction
|
tx: Transaction
|
||||||
@@ -126,6 +137,12 @@ wallets:
|
|||||||
tx_receive_cancel_conf: 'Are you sure you want to cancel receiving of %{amount} ツ?'
|
tx_receive_cancel_conf: 'Are you sure you want to cancel receiving of %{amount} ツ?'
|
||||||
rec_phrase_not_found: Recovery phrase not found.
|
rec_phrase_not_found: Recovery phrase not found.
|
||||||
restore_wallet_desc: Restore wallet by deleting all files if usual repair not helped, you will need to re-open your wallet.
|
restore_wallet_desc: Restore wallet by deleting all files if usual repair not helped, you will need to re-open your wallet.
|
||||||
|
fee_base_desc: 'Fee (base value%{value}):'
|
||||||
|
payment_proof: Payment proof
|
||||||
|
payment_proof_desc: 'Enter received payment proof to verify transaction:'
|
||||||
|
payment_proof_valid: 'Entered payment proof is valid:'
|
||||||
|
payment_proof_error: 'Entered payment proof is not valid:'
|
||||||
|
tx_delete_confirmation: Are you sure you want to delete the transaction from history?
|
||||||
transport:
|
transport:
|
||||||
desc: 'Use transport to receive or send messages synchronously:'
|
desc: 'Use transport to receive or send messages synchronously:'
|
||||||
tor_network: Tor network
|
tor_network: Tor network
|
||||||
@@ -135,10 +152,11 @@ transport:
|
|||||||
conn_error: Connection error
|
conn_error: Connection error
|
||||||
disconnected: Disconnected
|
disconnected: Disconnected
|
||||||
receiver_address: 'Address of the receiver:'
|
receiver_address: 'Address of the receiver:'
|
||||||
|
sender_address: 'Address of the sender:'
|
||||||
incorrect_addr_err: 'Entered address is incorrect:'
|
incorrect_addr_err: 'Entered address is incorrect:'
|
||||||
tor_send_error: An error occurred during sending over Tor, make sure receiver is online, transaction was canceled.
|
tor_send_error: An error occurred during sending over Tor, make sure receiver is online, transaction was canceled.
|
||||||
tor_autorun_desc: Whether to launch Tor service on wallet opening to receive transactions synchronously.
|
tor_autorun_desc: Whether to launch Tor service on wallet opening to receive transactions synchronously.
|
||||||
tor_sending: 'Sending %{amount} ツ over Tor'
|
tor_sending: Sending over Tor
|
||||||
tor_settings: Tor Settings
|
tor_settings: Tor Settings
|
||||||
bridges: Bridges
|
bridges: Bridges
|
||||||
bridges_desc: Setup bridges to bypass Tor network censorship if usual connection is not working.
|
bridges_desc: Setup bridges to bypass Tor network censorship if usual connection is not working.
|
||||||
@@ -283,12 +301,60 @@ network_settings:
|
|||||||
ban_window_desc: The decision to ban is made by node, based on the correctness of the data received from the peer.
|
ban_window_desc: The decision to ban is made by node, based on the correctness of the data received from the peer.
|
||||||
max_inbound_count: 'Maximum number of inbound peer connections:'
|
max_inbound_count: 'Maximum number of inbound peer connections:'
|
||||||
max_outbound_count: 'Maximum number of outbound peer connections:'
|
max_outbound_count: 'Maximum number of outbound peer connections:'
|
||||||
reset_peers_desc: Reset peers data. Use it with a caution only if there are problems with finding peers.
|
reset_data_desc: Reset the node data. Use it with a caution only if there are problems with synchronization.
|
||||||
reset_peers: Reset peers
|
reset_data: Reset data
|
||||||
|
ip_listen_all: Listen on all interfaces
|
||||||
modal:
|
modal:
|
||||||
cancel: Cancel
|
cancel: Cancel
|
||||||
save: Save
|
save: Save
|
||||||
add: Add
|
add: Add
|
||||||
modal_exit:
|
modal_exit:
|
||||||
description: Are you sure you want to quit the application?
|
description: Are you sure you want to quit the application?
|
||||||
exit: Exit
|
exit: Exit
|
||||||
|
app_settings:
|
||||||
|
proxy: Proxy
|
||||||
|
proxy_desc: Whether to use proxy for network requests from the application.
|
||||||
|
keyboard:
|
||||||
|
1: 1
|
||||||
|
2: 2
|
||||||
|
3: 3
|
||||||
|
4: 4
|
||||||
|
5: 5
|
||||||
|
6: 6
|
||||||
|
7: 7
|
||||||
|
8: 8
|
||||||
|
9: 9
|
||||||
|
0: 0
|
||||||
|
01: '-'
|
||||||
|
q: q
|
||||||
|
w: w
|
||||||
|
e: e
|
||||||
|
r: r
|
||||||
|
t: t
|
||||||
|
y: y
|
||||||
|
u: u
|
||||||
|
i: i
|
||||||
|
o: o
|
||||||
|
p: p
|
||||||
|
p1: '"'
|
||||||
|
a: a
|
||||||
|
s: s
|
||||||
|
d: d
|
||||||
|
f: f
|
||||||
|
g: g
|
||||||
|
h: h
|
||||||
|
j: j
|
||||||
|
k: k
|
||||||
|
l: l
|
||||||
|
l1: \
|
||||||
|
l2: ':'
|
||||||
|
z: z
|
||||||
|
x: x
|
||||||
|
c: c
|
||||||
|
v: v
|
||||||
|
b: b
|
||||||
|
n: n
|
||||||
|
m: m
|
||||||
|
m1: ','
|
||||||
|
m2: .
|
||||||
|
m3: /
|
||||||
+70
-4
@@ -25,10 +25,20 @@ share: Partager
|
|||||||
theme: 'Thème:'
|
theme: 'Thème:'
|
||||||
dark: Sombre
|
dark: Sombre
|
||||||
light: Clair
|
light: Clair
|
||||||
|
file: Fichier
|
||||||
choose_file: Choisir un fichier
|
choose_file: Choisir un fichier
|
||||||
|
choose_folder: Choisir un dossier
|
||||||
crash_report: Rapport d'échec
|
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.
|
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
|
confirmation: Confirmation
|
||||||
|
enter_url: Entrez l'URL
|
||||||
|
max_short: MAX
|
||||||
|
files_location: Emplacement du fichier
|
||||||
|
moving_files: Déplacer des fichiers
|
||||||
|
wrong_path_error: Chemin incorrect spécifié
|
||||||
|
check_updates: Vérifiez les mises à jour au démarrage
|
||||||
|
update_available: Mise à jour disponible!
|
||||||
|
changelog: 'Journal des modifications:'
|
||||||
wallets:
|
wallets:
|
||||||
await_conf_amount: En attente de confirmation
|
await_conf_amount: En attente de confirmation
|
||||||
await_fin_amount: En attente de finalisation
|
await_fin_amount: En attente de finalisation
|
||||||
@@ -83,6 +93,7 @@ wallets:
|
|||||||
tx_canceled: Annulé
|
tx_canceled: Annulé
|
||||||
tx_cancelling: Annulation
|
tx_cancelling: Annulation
|
||||||
tx_finalizing: Finalisation
|
tx_finalizing: Finalisation
|
||||||
|
tx_posting: Publication
|
||||||
tx_confirmed: Confirmé
|
tx_confirmed: Confirmé
|
||||||
txs: Transactions
|
txs: Transactions
|
||||||
tx: Transaction
|
tx: Transaction
|
||||||
@@ -126,6 +137,12 @@ wallets:
|
|||||||
tx_receive_cancel_conf: 'Êtes-vous sûr de vouloir annuler la réception de %{amount} ツ?'
|
tx_receive_cancel_conf: 'Êtes-vous sûr de vouloir annuler la réception de %{amount} ツ?'
|
||||||
rec_phrase_not_found: Phrase de récupération non trouvée.
|
rec_phrase_not_found: Phrase de récupération non trouvée.
|
||||||
restore_wallet_desc: "Restaurer le portefeuille en supprimant tous les fichiers si la réparation habituelle n'a pas aidé. Vous devrez rouvrir votre portefeuille."
|
restore_wallet_desc: "Restaurer le portefeuille en supprimant tous les fichiers si la réparation habituelle n'a pas aidé. Vous devrez rouvrir votre portefeuille."
|
||||||
|
fee_base_desc: 'Frais (valeur de base%{value}):'
|
||||||
|
payment_proof: Preuve de paiement
|
||||||
|
payment_proof_desc: 'Saisissez la preuve de paiement reçue pour vérifier la transaction:'
|
||||||
|
payment_proof_valid: 'La preuve de paiement saisie est valide:'
|
||||||
|
payment_proof_error: "La preuve de paiement saisie n'est pas valide:"
|
||||||
|
tx_delete_confirmation: Êtes-vous sûr de vouloir supprimer la transaction de l'historique?
|
||||||
transport:
|
transport:
|
||||||
desc: 'Utilisez le transport pour recevoir ou envoyer des messages de manière synchronisée:'
|
desc: 'Utilisez le transport pour recevoir ou envoyer des messages de manière synchronisée:'
|
||||||
tor_network: Réseau Tor
|
tor_network: Réseau Tor
|
||||||
@@ -135,10 +152,11 @@ transport:
|
|||||||
conn_error: Erreur de connexion
|
conn_error: Erreur de connexion
|
||||||
disconnected: Déconnecté
|
disconnected: Déconnecté
|
||||||
receiver_address: 'Adresse du destinataire:'
|
receiver_address: 'Adresse du destinataire:'
|
||||||
|
sender_address: "Adresse de l'expéditeur:"
|
||||||
incorrect_addr_err: 'Adresse entrée incorrecte:'
|
incorrect_addr_err: 'Adresse entrée incorrecte:'
|
||||||
tor_send_error: "Une erreur s'est produite lors de l'envoi via Tor. Assurez-vous que le destinataire est en ligne, la transaction a été annulée."
|
tor_send_error: "Une erreur s'est produite lors de l'envoi via Tor. Assurez-vous que le destinataire est en ligne, la transaction a été annulée."
|
||||||
tor_autorun_desc: "Lancer automatiquement le service Tor à l'ouverture du portefeuille pour recevoir les transactions de manière synchronisée."
|
tor_autorun_desc: "Lancer automatiquement le service Tor à l'ouverture du portefeuille pour recevoir les transactions de manière synchronisée."
|
||||||
tor_sending: 'Envoi de %{amount} ツ via Tor'
|
tor_sending: Envoi via Tor
|
||||||
tor_settings: Paramètres Tor
|
tor_settings: Paramètres Tor
|
||||||
bridges: Passerelles
|
bridges: Passerelles
|
||||||
bridges_desc: Configurez des passerelles pour contourner la censure du réseau Tor si la connexion habituelle ne fonctionne pas.
|
bridges_desc: Configurez des passerelles pour contourner la censure du réseau Tor si la connexion habituelle ne fonctionne pas.
|
||||||
@@ -283,12 +301,60 @@ network_settings:
|
|||||||
ban_window_desc: La décision de bannir est prise par le noeud, en fonction de la validité des données reçues du pair.
|
ban_window_desc: La décision de bannir est prise par le noeud, en fonction de la validité des données reçues du pair.
|
||||||
max_inbound_count: 'Nombre maximum de connexions de pairs entrants :'
|
max_inbound_count: 'Nombre maximum de connexions de pairs entrants :'
|
||||||
max_outbound_count: 'Nombre maximum de connexions de pairs sortants :'
|
max_outbound_count: 'Nombre maximum de connexions de pairs sortants :'
|
||||||
reset_peers_desc: Réinitialiser les données des pairs. Utilisez-le avec précaution uniquement en cas de problèmes pour trouver des pairs.
|
reset_data_desc: Réinitialisez les données du noeud. Utilisez-le avec prudence uniquement en cas de problème de synchronisation.
|
||||||
reset_peers: Réinitialiser les pairs
|
reset_data: Réinitialisation des données
|
||||||
|
ip_listen_all: Écoutez sur toutes les interfaces
|
||||||
modal:
|
modal:
|
||||||
cancel: Annuler
|
cancel: Annuler
|
||||||
save: Sauvegarder
|
save: Sauvegarder
|
||||||
add: Ajouter
|
add: Ajouter
|
||||||
modal_exit:
|
modal_exit:
|
||||||
description: "Êtes-vous sûr de vouloir quitter l'application ?"
|
description: "Êtes-vous sûr de vouloir quitter l'application ?"
|
||||||
exit: Quitter
|
exit: Quitter
|
||||||
|
app_settings:
|
||||||
|
proxy: Proxy
|
||||||
|
proxy_desc: Vaut-il la peine d'utiliser un proxy pour les requêtes réseau de l'application.
|
||||||
|
keyboard:
|
||||||
|
1: 1
|
||||||
|
2: 2
|
||||||
|
3: 3
|
||||||
|
4: 4
|
||||||
|
5: 5
|
||||||
|
6: 6
|
||||||
|
7: 7
|
||||||
|
8: 8
|
||||||
|
9: 9
|
||||||
|
0: 0
|
||||||
|
01: '`'
|
||||||
|
q: a
|
||||||
|
w: z
|
||||||
|
e: e
|
||||||
|
r: r
|
||||||
|
t: t
|
||||||
|
y: y
|
||||||
|
u: u
|
||||||
|
i: i
|
||||||
|
o: o
|
||||||
|
p: p
|
||||||
|
p1: ç
|
||||||
|
a: q
|
||||||
|
s: s
|
||||||
|
d: d
|
||||||
|
f: f
|
||||||
|
g: g
|
||||||
|
h: h
|
||||||
|
j: j
|
||||||
|
k: k
|
||||||
|
l: l
|
||||||
|
l1: m
|
||||||
|
l2: ù
|
||||||
|
z: w
|
||||||
|
x: x
|
||||||
|
c: c
|
||||||
|
v: v
|
||||||
|
b: b
|
||||||
|
n: n
|
||||||
|
m: ','
|
||||||
|
m1: .
|
||||||
|
m2: ':'
|
||||||
|
m3: /
|
||||||
+70
-4
@@ -25,10 +25,20 @@ share: Поделиться
|
|||||||
theme: 'Тема:'
|
theme: 'Тема:'
|
||||||
dark: Тёмная
|
dark: Тёмная
|
||||||
light: Светлая
|
light: Светлая
|
||||||
|
file: Файл
|
||||||
choose_file: Выбрать файл
|
choose_file: Выбрать файл
|
||||||
|
choose_folder: Выбрать папку
|
||||||
crash_report: Отчёт о сбое
|
crash_report: Отчёт о сбое
|
||||||
crash_report_warning: В прошлый раз приложение неожиданно закрылось, вы можете поделиться отчетом о сбое с разработчиками.
|
crash_report_warning: В прошлый раз приложение неожиданно закрылось, вы можете поделиться отчетом о сбое с разработчиками.
|
||||||
confirmation: Подтверждение
|
confirmation: Подтверждение
|
||||||
|
enter_url: Введите URL-адрес
|
||||||
|
max_short: МАКС
|
||||||
|
files_location: Расположение файлов
|
||||||
|
moving_files: Перемещение файлов
|
||||||
|
wrong_path_error: Указан неправильный путь
|
||||||
|
check_updates: Проверять обновления при запуске
|
||||||
|
update_available: Доступно обновление!
|
||||||
|
changelog: 'Журнал изменений:'
|
||||||
wallets:
|
wallets:
|
||||||
await_conf_amount: Ожидает подтверждения
|
await_conf_amount: Ожидает подтверждения
|
||||||
await_fin_amount: Ожидает завершения
|
await_fin_amount: Ожидает завершения
|
||||||
@@ -83,6 +93,7 @@ wallets:
|
|||||||
tx_canceled: Отменено
|
tx_canceled: Отменено
|
||||||
tx_cancelling: Отмена
|
tx_cancelling: Отмена
|
||||||
tx_finalizing: Завершение
|
tx_finalizing: Завершение
|
||||||
|
tx_posting: Публикация
|
||||||
tx_confirmed: Подтверждено
|
tx_confirmed: Подтверждено
|
||||||
txs: Транзакции
|
txs: Транзакции
|
||||||
tx: Транзакция
|
tx: Транзакция
|
||||||
@@ -126,6 +137,12 @@ wallets:
|
|||||||
tx_receive_cancel_conf: 'Вы действительно хотите отменить получение %{amount} ツ?'
|
tx_receive_cancel_conf: 'Вы действительно хотите отменить получение %{amount} ツ?'
|
||||||
rec_phrase_not_found: Фраза восстановления не найдена.
|
rec_phrase_not_found: Фраза восстановления не найдена.
|
||||||
restore_wallet_desc: Восстановить кошелёк, удалив все файлы, если обычное исправление не помогло. Необходимо переоткрыть кошелёк.
|
restore_wallet_desc: Восстановить кошелёк, удалив все файлы, если обычное исправление не помогло. Необходимо переоткрыть кошелёк.
|
||||||
|
fee_base_desc: 'Комиссия (базовое значение%{value}):'
|
||||||
|
payment_proof: Подтверждение оплаты
|
||||||
|
payment_proof_desc: 'Введите полученное подтверждение оплаты для проверки транзакции:'
|
||||||
|
payment_proof_valid: 'Введённое подтверждение оплаты действительно:'
|
||||||
|
payment_proof_error: 'Введённое подтверждение оплаты недействительно:'
|
||||||
|
tx_delete_confirmation: Вы уверены, что хотите удалить транзакцию из истории?
|
||||||
transport:
|
transport:
|
||||||
desc: 'Используйте транспорт для синхронных получения или отправки сообщений:'
|
desc: 'Используйте транспорт для синхронных получения или отправки сообщений:'
|
||||||
tor_network: Сеть Tor
|
tor_network: Сеть Tor
|
||||||
@@ -135,10 +152,11 @@ transport:
|
|||||||
conn_error: Ошибка подключения
|
conn_error: Ошибка подключения
|
||||||
disconnected: Отключено
|
disconnected: Отключено
|
||||||
receiver_address: 'Адрес получателя:'
|
receiver_address: 'Адрес получателя:'
|
||||||
|
sender_address: 'Адрес отправителя:'
|
||||||
incorrect_addr_err: 'Введённый адрес неверен:'
|
incorrect_addr_err: 'Введённый адрес неверен:'
|
||||||
tor_send_error: Во время отправки через Tor произошла ошибка, убедитесь, что получатель находится онлайн, транзакция была отменена.
|
tor_send_error: Во время отправки через Tor произошла ошибка, убедитесь, что получатель находится онлайн, транзакция была отменена.
|
||||||
tor_autorun_desc: Запускать ли Tor сервис при открытии кошелька для синхронного получения транзакций.
|
tor_autorun_desc: Запускать ли Tor сервис при открытии кошелька для синхронного получения транзакций.
|
||||||
tor_sending: 'Отправка %{amount} ツ через Tor'
|
tor_sending: Отправка через Tor
|
||||||
tor_settings: Настройки Tor
|
tor_settings: Настройки Tor
|
||||||
bridges: Мосты
|
bridges: Мосты
|
||||||
bridges_desc: Настройте мосты для обхода цензуры сети Tor, если обычное соединение не работает.
|
bridges_desc: Настройте мосты для обхода цензуры сети Tor, если обычное соединение не работает.
|
||||||
@@ -283,12 +301,60 @@ network_settings:
|
|||||||
ban_window_desc: Решение о запрете принимается узлом, основываясь на корректности данных полученных от пира.
|
ban_window_desc: Решение о запрете принимается узлом, основываясь на корректности данных полученных от пира.
|
||||||
max_inbound_count: 'Максимальное количество входящих подключений пиров:'
|
max_inbound_count: 'Максимальное количество входящих подключений пиров:'
|
||||||
max_outbound_count: 'Максимальное количество исходящих подключений к пирам:'
|
max_outbound_count: 'Максимальное количество исходящих подключений к пирам:'
|
||||||
reset_peers_desc: Сбросить данные пиров. Используйте с осторожностью, только при наличии проблем с поиском пиров.
|
reset_data_desc: Сбросить данные узла. Используйте с осторожностью, только при наличии проблем с синхронизацией.
|
||||||
reset_peers: Сбросить пиры
|
reset_data: Сброс данных
|
||||||
|
ip_listen_all: Слушать на всех интерфейсах
|
||||||
modal:
|
modal:
|
||||||
cancel: Отмена
|
cancel: Отмена
|
||||||
save: Сохранить
|
save: Сохранить
|
||||||
add: Добавить
|
add: Добавить
|
||||||
modal_exit:
|
modal_exit:
|
||||||
description: Вы уверены, что хотите выйти из приложения?
|
description: Вы уверены, что хотите выйти из приложения?
|
||||||
exit: Выход
|
exit: Выход
|
||||||
|
app_settings:
|
||||||
|
proxy: Прокси
|
||||||
|
proxy_desc: Стоит ли использовать прокси для сетевых запросов из приложения.
|
||||||
|
keyboard:
|
||||||
|
1: 1
|
||||||
|
2: 2
|
||||||
|
3: 3
|
||||||
|
4: 4
|
||||||
|
5: 5
|
||||||
|
6: 6
|
||||||
|
7: 7
|
||||||
|
8: 8
|
||||||
|
9: 9
|
||||||
|
0: 0
|
||||||
|
01: ъ
|
||||||
|
q: й
|
||||||
|
w: ц
|
||||||
|
e: у
|
||||||
|
r: к
|
||||||
|
t: е
|
||||||
|
y: н
|
||||||
|
u: г
|
||||||
|
i: ш
|
||||||
|
o: щ
|
||||||
|
p: з
|
||||||
|
p1: х
|
||||||
|
a: ф
|
||||||
|
s: ы
|
||||||
|
d: в
|
||||||
|
f: а
|
||||||
|
g: п
|
||||||
|
h: р
|
||||||
|
j: о
|
||||||
|
k: л
|
||||||
|
l: д
|
||||||
|
l1: ж
|
||||||
|
l2: э
|
||||||
|
z: я
|
||||||
|
x: ч
|
||||||
|
c: с
|
||||||
|
v: м
|
||||||
|
b: и
|
||||||
|
n: т
|
||||||
|
m: ь
|
||||||
|
m1: б
|
||||||
|
m2: ю
|
||||||
|
m3: ё
|
||||||
+70
-4
@@ -25,10 +25,20 @@ share: Paylasmak
|
|||||||
theme: 'Tema:'
|
theme: 'Tema:'
|
||||||
dark: Karanlik
|
dark: Karanlik
|
||||||
light: Isik
|
light: Isik
|
||||||
|
file: Dosya
|
||||||
choose_file: Dosya seçin
|
choose_file: Dosya seçin
|
||||||
|
choose_folder: Klasör seç
|
||||||
crash_report: Ariza Raporu
|
crash_report: Ariza Raporu
|
||||||
crash_report_warning: Uygulama beklenmedik bir sekilde kapandi son kez, kilitlenme raporunu gelistiricilerle paylasabilirsiniz.
|
crash_report_warning: Uygulama beklenmedik bir sekilde kapandi son kez, kilitlenme raporunu gelistiricilerle paylasabilirsiniz.
|
||||||
confirmation: Onay
|
confirmation: Onay
|
||||||
|
enter_url: URL'yi girin
|
||||||
|
max_short: MAKS
|
||||||
|
files_location: Dosya konumu
|
||||||
|
moving_files: Dosyalari Tasima
|
||||||
|
wrong_path_error: Yanlis yol belirtildi
|
||||||
|
check_updates: Başlangiçta güncellemeleri kontrol edin
|
||||||
|
update_available: Güncelleme mevcut!
|
||||||
|
changelog: 'Değişiklik Günlüğü:'
|
||||||
wallets:
|
wallets:
|
||||||
await_conf_amount: Onay bekleniyor
|
await_conf_amount: Onay bekleniyor
|
||||||
await_fin_amount: Tamamlanma bekleniyor
|
await_fin_amount: Tamamlanma bekleniyor
|
||||||
@@ -83,6 +93,7 @@ wallets:
|
|||||||
tx_canceled: Iptal edildi
|
tx_canceled: Iptal edildi
|
||||||
tx_cancelling: Iptal ediliyor
|
tx_cancelling: Iptal ediliyor
|
||||||
tx_finalizing: Islem tamamlaniyor
|
tx_finalizing: Islem tamamlaniyor
|
||||||
|
tx_posting: Islem kaydetme
|
||||||
tx_confirmed: Onaylandi
|
tx_confirmed: Onaylandi
|
||||||
txs: Islemler
|
txs: Islemler
|
||||||
tx: Islem
|
tx: Islem
|
||||||
@@ -126,6 +137,12 @@ wallets:
|
|||||||
tx_receive_cancel_conf: Gelen tx iptal
|
tx_receive_cancel_conf: Gelen tx iptal
|
||||||
rec_phrase_not_found: Sifre kelime bulunmuyor
|
rec_phrase_not_found: Sifre kelime bulunmuyor
|
||||||
restore_wallet_desc: Cuzdani restore et
|
restore_wallet_desc: Cuzdani restore et
|
||||||
|
fee_base_desc: 'Ücret (taban değeri%{value}):'
|
||||||
|
payment_proof: Ödeme kaniti
|
||||||
|
payment_proof_desc: 'Islemi doğrulamak için alinan ödeme kanitini girin:'
|
||||||
|
payment_proof_valid: 'Girilen ödeme kaniti geçerlidir:'
|
||||||
|
payment_proof_error: 'Girilen ödeme kaniti geçerli değildir:'
|
||||||
|
tx_delete_confirmation: Islemi geçmişten silmek istediğinizden emin misiniz?
|
||||||
transport:
|
transport:
|
||||||
desc: 'Adresten senkronize GONDER veya AL:'
|
desc: 'Adresten senkronize GONDER veya AL:'
|
||||||
tor_network: Tor network
|
tor_network: Tor network
|
||||||
@@ -135,10 +152,11 @@ transport:
|
|||||||
conn_error: Bagalanti hatasi
|
conn_error: Bagalanti hatasi
|
||||||
disconnected: Baglanti yok
|
disconnected: Baglanti yok
|
||||||
receiver_address: 'Alicinin adresi:'
|
receiver_address: 'Alicinin adresi:'
|
||||||
|
sender_address: 'Gönderici adresi:'
|
||||||
incorrect_addr_err: 'Girilen adres hatali:'
|
incorrect_addr_err: 'Girilen adres hatali:'
|
||||||
tor_send_error: Tor adresi uzerinden gonderimde aksaklik olustu, alici online olmasi gerek, islem iptal edildi.
|
tor_send_error: Tor adresi uzerinden gonderimde aksaklik olustu, alici online olmasi gerek, islem iptal edildi.
|
||||||
tor_autorun_desc: Islemleri Tor adresi olarak AL,bunun için cuzdan acilisinda Tor hizmetinin baslatilip baslatilmayacagi.
|
tor_autorun_desc: Islemleri Tor adresi olarak AL,bunun için cuzdan acilisinda Tor hizmetinin baslatilip baslatilmayacagi.
|
||||||
tor_sending: 'Tor adrese %{amount} ツ gonderiliyor.'
|
tor_sending: Tor adrese gonderiliyor
|
||||||
tor_settings: Tor Ayarlar
|
tor_settings: Tor Ayarlar
|
||||||
bridges: Bridges
|
bridges: Bridges
|
||||||
bridges_desc: Setup bridges to bypass Tor network censorship if usual connection is not working.
|
bridges_desc: Setup bridges to bypass Tor network censorship if usual connection is not working.
|
||||||
@@ -283,12 +301,60 @@ network_settings:
|
|||||||
ban_window_desc: Banlama karari, peerden alinan verilerin dogruluguna bagli olarak Node tarafindan verilir.
|
ban_window_desc: Banlama karari, peerden alinan verilerin dogruluguna bagli olarak Node tarafindan verilir.
|
||||||
max_inbound_count: 'Maksimum gelen Peer baglanti sayisi:'
|
max_inbound_count: 'Maksimum gelen Peer baglanti sayisi:'
|
||||||
max_outbound_count: 'Maksimum giden Peer baglanti sayisi:'
|
max_outbound_count: 'Maksimum giden Peer baglanti sayisi:'
|
||||||
reset_peers_desc: Peers verilerini sifirlayin. Yalnizca Peers bulma konusunda sorun yasiyorsaniz dikkatli kullanin.
|
reset_data_desc: Node verisini sifirlama. Sadece senkronizasyonda sorun varsa dikkatli kullanin.
|
||||||
reset_peers: Peers Resetle
|
reset_data: Verileri sifirlama
|
||||||
|
ip_listen_all: Tüm arayüzlerde dinle
|
||||||
modal:
|
modal:
|
||||||
cancel: Iptal
|
cancel: Iptal
|
||||||
save: Kaydet
|
save: Kaydet
|
||||||
add: Ekle
|
add: Ekle
|
||||||
modal_exit:
|
modal_exit:
|
||||||
description: Uygulamadan cikmak için exit, emin misiniz?
|
description: Uygulamadan cikmak için exit, emin misiniz?
|
||||||
exit: Exit
|
exit: Exit
|
||||||
|
app_settings:
|
||||||
|
proxy: Proxy
|
||||||
|
proxy_desc: Uygulamadan gelen ağ istekleri için bir proxy kullanmaya değer mi.
|
||||||
|
keyboard:
|
||||||
|
1: 1
|
||||||
|
2: 2
|
||||||
|
3: 3
|
||||||
|
4: 4
|
||||||
|
5: 5
|
||||||
|
6: 6
|
||||||
|
7: 7
|
||||||
|
8: 8
|
||||||
|
9: 9
|
||||||
|
0: 0
|
||||||
|
01: '-'
|
||||||
|
q: q
|
||||||
|
w: w
|
||||||
|
e: e
|
||||||
|
r: r
|
||||||
|
t: t
|
||||||
|
y: y
|
||||||
|
u: u
|
||||||
|
i: i
|
||||||
|
o: o
|
||||||
|
p: p
|
||||||
|
p1: ü
|
||||||
|
a: a
|
||||||
|
s: s
|
||||||
|
d: d
|
||||||
|
f: f
|
||||||
|
g: g
|
||||||
|
h: h
|
||||||
|
j: j
|
||||||
|
k: k
|
||||||
|
l: l
|
||||||
|
l1: ö
|
||||||
|
l2: ':'
|
||||||
|
z: z
|
||||||
|
x: x
|
||||||
|
c: c
|
||||||
|
v: v
|
||||||
|
b: b
|
||||||
|
n: n
|
||||||
|
m: m
|
||||||
|
m1: ','
|
||||||
|
m2: .
|
||||||
|
m3: /
|
||||||
@@ -0,0 +1,360 @@
|
|||||||
|
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: 浅色
|
||||||
|
file: 文件
|
||||||
|
choose_file: 选择文件
|
||||||
|
choose_folder: 选择文件夹
|
||||||
|
crash_report: 崩溃报告
|
||||||
|
crash_report_warning: 应用上次意外关闭,你可以将崩溃报告分享给开发者。
|
||||||
|
confirmation: 确认
|
||||||
|
enter_url: 输入网址
|
||||||
|
max_short: 最大
|
||||||
|
files_location: 文件位置
|
||||||
|
moving_files: 正在移动文件
|
||||||
|
wrong_path_error: 指定的路径错误
|
||||||
|
check_updates: 启动时检查更新
|
||||||
|
update_available: 有可用更新!
|
||||||
|
changelog: '更新日志:'
|
||||||
|
wallets:
|
||||||
|
await_conf_amount: 等待确认
|
||||||
|
await_fin_amount: 等待最终确认
|
||||||
|
locked_amount: 已锁定
|
||||||
|
txs_empty: '要手动或通过传输方式接收资金,请使用屏幕底部的 %{message} 或 %{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_posting: 提交中
|
||||||
|
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: 使用蒲公英(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: 如果常规修复无效,可通过删除所有文件来恢复钱包,你需要重新打开钱包。
|
||||||
|
fee_base_desc: '手续费(基础值%{value}):'
|
||||||
|
payment_proof: 支付证明
|
||||||
|
payment_proof_desc: '输入收到的支付证明以验证交易:'
|
||||||
|
payment_proof_valid: '输入的支付证明有效:'
|
||||||
|
payment_proof_error: '输入的支付证明无效:'
|
||||||
|
tx_delete_confirmation: 确定要从历史记录中删除该交易吗?
|
||||||
|
transport:
|
||||||
|
desc: '使用传输方式同步收发消息:'
|
||||||
|
tor_network: Tor 网络
|
||||||
|
connected: 已连接
|
||||||
|
connecting: 连接中
|
||||||
|
disconnecting: 断开中
|
||||||
|
conn_error: 连接错误
|
||||||
|
disconnected: 已断开
|
||||||
|
receiver_address: '接收方地址:'
|
||||||
|
sender_address: '发送方地址:'
|
||||||
|
incorrect_addr_err: '输入的地址不正确:'
|
||||||
|
tor_send_error: 通过 Tor 发送时发生错误,请确认接收方在线,交易已取消。
|
||||||
|
tor_autorun_desc: 是否在打开钱包时启动 Tor 服务以同步接收交易。
|
||||||
|
tor_sending: 正在通过 Tor 发送
|
||||||
|
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: 你的系统上没有可用的 IP 地址,无法启动服务器,请检查网络连接。
|
||||||
|
available: 可用
|
||||||
|
not_available: 不可用
|
||||||
|
availability_check: 可用性检查
|
||||||
|
android_warning: 安卓用户请注意。要成功同步集成节点,你必须在手机系统设置中允许 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: 茎池
|
||||||
|
data: 数据
|
||||||
|
size: 大小(GB)
|
||||||
|
peers: 对等节点
|
||||||
|
error_clean: 节点数据已损坏,需要重新同步。
|
||||||
|
resync: 重新同步
|
||||||
|
error_p2p_api: '%{p2p_api} 服务器初始化时发生错误,请通过选择屏幕底部的 %{settings} 检查 %{p2p_api} 设置。'
|
||||||
|
error_config: '配置初始化时发生错误,请通过选择屏幕底部的 %{settings} 检查设置。'
|
||||||
|
error_unknown: '初始化时发生未知错误,请通过选择屏幕底部的 %{settings} 检查集成节点设置,或重新同步。'
|
||||||
|
network_metrics:
|
||||||
|
loading: 指标将在同步完成后可用
|
||||||
|
emission: 发行量
|
||||||
|
inflation: 通胀率
|
||||||
|
supply: 供应量
|
||||||
|
block_time: 出块时间
|
||||||
|
reward: 奖励
|
||||||
|
difficulty_window: '难度窗口 %{size}'
|
||||||
|
network_mining:
|
||||||
|
loading: 挖矿将在同步完成后可用
|
||||||
|
info: '挖矿服务器已启用,你可以通过选择屏幕底部的 %{settings} 更改其设置。数据在设备连接时更新。'
|
||||||
|
restart_server_required: 需要重启服务器以应用更改。
|
||||||
|
rewards_wallet: 奖励接收钱包
|
||||||
|
server: Stratum 服务器
|
||||||
|
address: 地址
|
||||||
|
miners: 矿工
|
||||||
|
devices: 设备
|
||||||
|
blocks_found: 已找到区块
|
||||||
|
hashrate: '算力(C%{bits})'
|
||||||
|
connected: 已连接
|
||||||
|
disconnected: 已断开
|
||||||
|
network_settings:
|
||||||
|
change_value: 更改值
|
||||||
|
stratum_ip: 'Stratum IP 地址:'
|
||||||
|
stratum_port: 'Stratum 端口:'
|
||||||
|
port_unavailable: 指定的端口不可用
|
||||||
|
restart_node_required: 需要重启节点以应用更改。
|
||||||
|
choose_wallet: 选择钱包
|
||||||
|
stratum_wallet_warning: 必须打开钱包才能接收奖励。
|
||||||
|
enable: 启用
|
||||||
|
disable: 禁用
|
||||||
|
restart: 重启
|
||||||
|
server: 服务器
|
||||||
|
api_ip: 'API IP 地址:'
|
||||||
|
api_port: 'API 端口:'
|
||||||
|
api_secret: 'Rest 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: '茎池中最大交易数:'
|
||||||
|
max_tx_weight: '可被选中构建区块的最大交易总权重:'
|
||||||
|
epoch_duration: '纪元持续时间(秒):'
|
||||||
|
embargo_timer: ' embargo 计时器(秒):'
|
||||||
|
aggregation_period: '聚合周期(秒):'
|
||||||
|
stem_probability: '茎阶段概率:'
|
||||||
|
stem_txs: 茎阶段交易
|
||||||
|
p2p_server: P2P 服务器
|
||||||
|
p2p_port: 'P2P 端口:'
|
||||||
|
add_seed: 添加 DNS 种子节点
|
||||||
|
seed_address: 'DNS 种子节点地址:'
|
||||||
|
add_peer: 添加对等节点
|
||||||
|
peer_address: '对等节点地址:'
|
||||||
|
peer_address_error: '请输入正确格式的 IP 地址或 DNS 名称(确保指定主机可用),例如:192.168.0.1:1234 或 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_data_desc: 重置节点数据。请谨慎使用,仅在同步出现问题时使用。
|
||||||
|
reset_data: 重置数据
|
||||||
|
ip_listen_all: 监听所有接口
|
||||||
|
modal:
|
||||||
|
cancel: 取消
|
||||||
|
save: 保存
|
||||||
|
add: 添加
|
||||||
|
modal_exit:
|
||||||
|
description: 确定要退出应用吗?
|
||||||
|
exit: 退出
|
||||||
|
app_settings:
|
||||||
|
proxy: 代理
|
||||||
|
proxy_desc: 是否为应用的网络请求使用代理。
|
||||||
|
keyboard:
|
||||||
|
1: 1
|
||||||
|
2: 2
|
||||||
|
3: 3
|
||||||
|
4: 4
|
||||||
|
5: 5
|
||||||
|
6: 6
|
||||||
|
7: 7
|
||||||
|
8: 8
|
||||||
|
9: 9
|
||||||
|
0: 0
|
||||||
|
01: '-'
|
||||||
|
q: 手
|
||||||
|
w: 田
|
||||||
|
e: 水
|
||||||
|
r: 口
|
||||||
|
t: 廿
|
||||||
|
y: 卜
|
||||||
|
u: 山
|
||||||
|
i: 戈
|
||||||
|
o: 人
|
||||||
|
p: 心
|
||||||
|
p1: '"'
|
||||||
|
a: 日
|
||||||
|
s: 尸
|
||||||
|
d: 木
|
||||||
|
f: 火
|
||||||
|
g: 土
|
||||||
|
h: 竹
|
||||||
|
j: 十
|
||||||
|
k: 大
|
||||||
|
l: 中
|
||||||
|
l1: \
|
||||||
|
l2: ':'
|
||||||
|
z: 重
|
||||||
|
x: 難
|
||||||
|
c: 金
|
||||||
|
v: 女
|
||||||
|
b: 月
|
||||||
|
n: 弓
|
||||||
|
m: 一
|
||||||
|
m1: ','
|
||||||
|
m2: .
|
||||||
|
m3: /
|
||||||
@@ -0,0 +1,360 @@
|
|||||||
|
lang_name: 繁體中文
|
||||||
|
copy: 複製
|
||||||
|
paste: 貼上
|
||||||
|
continue: 繼續
|
||||||
|
complete: 完成
|
||||||
|
error: 錯誤
|
||||||
|
retry: 重試
|
||||||
|
close: 關閉
|
||||||
|
change: 變更
|
||||||
|
show: 顯示
|
||||||
|
delete: 刪除
|
||||||
|
clear: 清除
|
||||||
|
create: 建立
|
||||||
|
id: 識別碼
|
||||||
|
kernel: 核心
|
||||||
|
settings: 設定
|
||||||
|
language: 語言
|
||||||
|
scan: 掃描
|
||||||
|
qr_code: QR 條碼
|
||||||
|
scan_qr: 掃描 QR 條碼
|
||||||
|
repeat: 重複
|
||||||
|
scan_result: 掃描結果
|
||||||
|
back: 返回
|
||||||
|
share: 分享
|
||||||
|
theme: '主題:'
|
||||||
|
dark: 深色
|
||||||
|
light: 淺色
|
||||||
|
file: 檔案
|
||||||
|
choose_file: 選擇檔案
|
||||||
|
choose_folder: 選擇資料夾
|
||||||
|
crash_report: 當機報告
|
||||||
|
crash_report_warning: 應用程式上次意外關閉,你可以將當機報告分享給開發者。
|
||||||
|
confirmation: 確認
|
||||||
|
enter_url: 輸入網址
|
||||||
|
max_short: 最大
|
||||||
|
files_location: 檔案位置
|
||||||
|
moving_files: 正在移動檔案
|
||||||
|
wrong_path_error: 指定的路徑錯誤
|
||||||
|
check_updates: 啟動時檢查更新
|
||||||
|
update_available: 有可用更新!
|
||||||
|
changelog: '更新日誌:'
|
||||||
|
wallets:
|
||||||
|
await_conf_amount: 等待確認
|
||||||
|
await_fin_amount: 等待最終確認
|
||||||
|
locked_amount: 已鎖定
|
||||||
|
txs_empty: '要手動或透過傳輸方式接收資金,請使用畫面底部的 %{message} 或 %{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_posting: 提交中
|
||||||
|
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: 使用蒲公英(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: 如果一般修復無效,可透過刪除所有檔案來還原錢包,你需要重新開啟錢包。
|
||||||
|
fee_base_desc: '手續費(基礎值%{value}):'
|
||||||
|
payment_proof: 支付證明
|
||||||
|
payment_proof_desc: '輸入收到的支付證明以驗證交易:'
|
||||||
|
payment_proof_valid: '輸入的支付證明有效:'
|
||||||
|
payment_proof_error: '輸入的支付證明無效:'
|
||||||
|
tx_delete_confirmation: 確定要從歷史記錄中刪除該交易嗎?
|
||||||
|
transport:
|
||||||
|
desc: '使用傳輸方式同步收發訊息:'
|
||||||
|
tor_network: Tor 網路
|
||||||
|
connected: 已連線
|
||||||
|
connecting: 連線中
|
||||||
|
disconnecting: 中斷中
|
||||||
|
conn_error: 連線錯誤
|
||||||
|
disconnected: 已中斷
|
||||||
|
receiver_address: '接收方位址:'
|
||||||
|
sender_address: '傳送方位址:'
|
||||||
|
incorrect_addr_err: '輸入的位址不正確:'
|
||||||
|
tor_send_error: 透過 Tor 傳送時發生錯誤,請確認接收方在線上,交易已取消。
|
||||||
|
tor_autorun_desc: 是否在開啟錢包時啟動 Tor 服務以同步接收交易。
|
||||||
|
tor_sending: 正在透過 Tor 傳送
|
||||||
|
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: 你的系統上沒有可用的 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: 莖池
|
||||||
|
data: 資料
|
||||||
|
size: 大小(GB)
|
||||||
|
peers: 對等節點
|
||||||
|
error_clean: 節點資料已損毀,需要重新同步。
|
||||||
|
resync: 重新同步
|
||||||
|
error_p2p_api: '%{p2p_api} 伺服器初始化時發生錯誤,請透過選擇畫面底部的 %{settings} 檢查 %{p2p_api} 設定。'
|
||||||
|
error_config: '設定初始化時發生錯誤,請透過選擇畫面底部的 %{settings} 檢查設定。'
|
||||||
|
error_unknown: '初始化時發生未知錯誤,請透過選擇畫面底部的 %{settings} 檢查整合節點設定,或重新同步。'
|
||||||
|
network_metrics:
|
||||||
|
loading: 指標將在同步完成後可用
|
||||||
|
emission: 發行量
|
||||||
|
inflation: 通膨率
|
||||||
|
supply: 供應量
|
||||||
|
block_time: 出塊時間
|
||||||
|
reward: 獎勵
|
||||||
|
difficulty_window: '難度視窗 %{size}'
|
||||||
|
network_mining:
|
||||||
|
loading: 挖礦將在同步完成後可用
|
||||||
|
info: '挖礦伺服器已啟用,你可以透過選擇畫面底部的 %{settings} 變更其設定。資料在裝置連線時更新。'
|
||||||
|
restart_server_required: 需要重新啟動伺服器以套用變更。
|
||||||
|
rewards_wallet: 獎勵接收錢包
|
||||||
|
server: Stratum 伺服器
|
||||||
|
address: 位址
|
||||||
|
miners: 礦工
|
||||||
|
devices: 裝置
|
||||||
|
blocks_found: 已找到區塊
|
||||||
|
hashrate: '算力(C%{bits})'
|
||||||
|
connected: 已連線
|
||||||
|
disconnected: 已中斷
|
||||||
|
network_settings:
|
||||||
|
change_value: 變更值
|
||||||
|
stratum_ip: 'Stratum IP 位址:'
|
||||||
|
stratum_port: 'Stratum 連接埠:'
|
||||||
|
port_unavailable: 指定的連接埠無法使用
|
||||||
|
restart_node_required: 需要重新啟動節點以套用變更。
|
||||||
|
choose_wallet: 選擇錢包
|
||||||
|
stratum_wallet_warning: 必須開啟錢包才能接收獎勵。
|
||||||
|
enable: 啟用
|
||||||
|
disable: 停用
|
||||||
|
restart: 重新啟動
|
||||||
|
server: 伺服器
|
||||||
|
api_ip: 'API IP 位址:'
|
||||||
|
api_port: 'API 連接埠:'
|
||||||
|
api_secret: 'Rest 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: '莖池中最大交易數:'
|
||||||
|
max_tx_weight: '可被選中建構區塊的最大交易總權重:'
|
||||||
|
epoch_duration: '紀元持續時間(秒):'
|
||||||
|
embargo_timer: 'Embargo 計時器(秒):'
|
||||||
|
aggregation_period: '彙總週期(秒):'
|
||||||
|
stem_probability: '莖階段機率:'
|
||||||
|
stem_txs: 莖階段交易
|
||||||
|
p2p_server: P2P 伺服器
|
||||||
|
p2p_port: 'P2P 連接埠:'
|
||||||
|
add_seed: 新增 DNS 種子節點
|
||||||
|
seed_address: 'DNS 種子節點位址:'
|
||||||
|
add_peer: 新增對等節點
|
||||||
|
peer_address: '對等節點位址:'
|
||||||
|
peer_address_error: '請輸入正確格式的 IP 位址或 DNS 名稱(確保指定主機可用),例如:192.168.0.1:1234 或 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_data_desc: 重設節點資料。請謹慎使用,僅在同步發生問題時使用。
|
||||||
|
reset_data: 重設資料
|
||||||
|
ip_listen_all: 監聽所有介面
|
||||||
|
modal:
|
||||||
|
cancel: 取消
|
||||||
|
save: 儲存
|
||||||
|
add: 新增
|
||||||
|
modal_exit:
|
||||||
|
description: 確定要離開應用程式嗎?
|
||||||
|
exit: 離開
|
||||||
|
app_settings:
|
||||||
|
proxy: 代理
|
||||||
|
proxy_desc: 是否為應用程式的網路請求使用代理。
|
||||||
|
keyboard:
|
||||||
|
1: 1
|
||||||
|
2: 2
|
||||||
|
3: 3
|
||||||
|
4: 4
|
||||||
|
5: 5
|
||||||
|
6: 6
|
||||||
|
7: 7
|
||||||
|
8: 8
|
||||||
|
9: 9
|
||||||
|
0: 0
|
||||||
|
01: '-'
|
||||||
|
q: 手
|
||||||
|
w: 田
|
||||||
|
e: 水
|
||||||
|
r: 口
|
||||||
|
t: 廿
|
||||||
|
y: 卜
|
||||||
|
u: 山
|
||||||
|
i: 戈
|
||||||
|
o: 人
|
||||||
|
p: 心
|
||||||
|
p1: '"'
|
||||||
|
a: 日
|
||||||
|
s: 尸
|
||||||
|
d: 木
|
||||||
|
f: 火
|
||||||
|
g: 土
|
||||||
|
h: 竹
|
||||||
|
j: 十
|
||||||
|
k: 大
|
||||||
|
l: 中
|
||||||
|
l1: \
|
||||||
|
l2: ':'
|
||||||
|
z: 重
|
||||||
|
x: 難
|
||||||
|
c: 金
|
||||||
|
v: 女
|
||||||
|
b: 月
|
||||||
|
n: 弓
|
||||||
|
m: 一
|
||||||
|
m1: ','
|
||||||
|
m2: .
|
||||||
|
m3: /
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>0.2.3</string>
|
<string>0.3.6</string>
|
||||||
<key>CFBundleSupportedPlatforms</key>
|
<key>CFBundleSupportedPlatforms</key>
|
||||||
<array>
|
<array>
|
||||||
<string>MacOSX</string>
|
<string>MacOSX</string>
|
||||||
|
|||||||
+5
-17
@@ -27,23 +27,11 @@ cd ..
|
|||||||
[[ $1 == "x86_64" ]] && arch+=(x86_64-apple-darwin)
|
[[ $1 == "x86_64" ]] && arch+=(x86_64-apple-darwin)
|
||||||
[[ $1 == "arm" ]] && arch+=(aarch64-apple-darwin)
|
[[ $1 == "arm" ]] && arch+=(aarch64-apple-darwin)
|
||||||
|
|
||||||
if [[ "$OSTYPE" != "darwin"* ]]; then
|
rustup target add x86_64-apple-darwin
|
||||||
# Start release build on non-MacOS with zig linker, requires zig 0.12.1
|
rustup target add aarch64-apple-darwin
|
||||||
rustup target add x86_64-apple-darwin
|
[[ $1 == "universal" ]]; arch+=(universal2-apple-darwin)
|
||||||
rustup target add aarch64-apple-darwin
|
cargo install cargo-zigbuild
|
||||||
[[ $1 == "universal" ]]; arch+=(universal2-apple-darwin)
|
cargo zigbuild --release --target ${arch}
|
||||||
cargo install cargo-zigbuild
|
|
||||||
cargo zigbuild --release --target ${arch}
|
|
||||||
else
|
|
||||||
rustup target add ${arch}
|
|
||||||
if [[ $1 == "universal" ]]; then
|
|
||||||
cargo build --release --target x86_64-apple-darwin
|
|
||||||
cargo build --release --target aarch64-apple-darwin
|
|
||||||
lipo -create -output target/grim target/aarch64-apple-darwin/release/grim target/x86_64-apple-darwin/release/grim
|
|
||||||
else
|
|
||||||
cargo build --release --target ${arch}
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
rm -f .intentionally-empty-file.o
|
rm -f .intentionally-empty-file.o
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
hard_tabs = true
|
||||||
|
edition = "2024"
|
||||||
+38
-31
@@ -1,8 +1,8 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
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'"
|
usage="Usage: android.sh [type] [platform|version] [flavor]\n - type: 'build' to run locally, 'lib' - .so for all platforms, 'release' - .apk for all platforms\n - platform, for 'build' type: 'v7', 'v8', 'x86'\n - version for 'lib' and 'release', example: '0.2.2'\n - optional flavor, for non-'lib' type: 'ci' for local maven, default - 'local' for external"
|
||||||
case $1 in
|
case $1 in
|
||||||
build|release)
|
build|lib|release)
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
printf "$usage"
|
printf "$usage"
|
||||||
@@ -38,39 +38,40 @@ function build_lib() {
|
|||||||
[[ $1 == "v8" ]] && arch=arm64-v8a
|
[[ $1 == "v8" ]] && arch=arm64-v8a
|
||||||
[[ $1 == "x86" ]] && arch=x86_64
|
[[ $1 == "x86" ]] && arch=x86_64
|
||||||
|
|
||||||
sed -i -e 's/"rlib"/"cdylib","rlib"/g' Cargo.toml
|
sed -i -e 's/"cdylib","rlib"]/"rlib"]/g' Cargo.toml
|
||||||
|
sed -i -e 's/"rlib"]/"cdylib","rlib"]/g' Cargo.toml
|
||||||
|
|
||||||
# Fix for https://stackoverflow.com/questions/57193895/error-use-of-undeclared-identifier-pthread-mutex-robust-cargo-build-liblmdb-s
|
|
||||||
# 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
|
cargo ndk -t "${arch}" -o android/app/src/main/jniLibs build --profile release-apk
|
||||||
if [ $? -eq 0 ]
|
if [ $? -ne 0 ]; then
|
||||||
then
|
|
||||||
success=1
|
|
||||||
else
|
|
||||||
success=0
|
success=0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
sed -i -e 's/"cdylib","rlib"/"rlib"/g' Cargo.toml
|
sed -i -e 's/"cdylib","rlib"]/"rlib"]/g' Cargo.toml
|
||||||
rm -f Cargo.toml-e
|
rm -f Cargo.toml-e
|
||||||
}
|
}
|
||||||
|
|
||||||
### Build application
|
### Build application
|
||||||
function build_apk() {
|
function build_apk() {
|
||||||
|
flavor=$3
|
||||||
|
[[ flavor == "" ]] && flavor="local"
|
||||||
cd android || exit 1
|
cd android || exit 1
|
||||||
./gradlew clean
|
./gradlew clean
|
||||||
# Build signed apk if keystore exists
|
# Build signed apk if keystore exists
|
||||||
if [ ! -f keystore.properties ]; then
|
if [ ! -f keystore.properties ]; then
|
||||||
./gradlew assembleRelease
|
./gradlew assemble${flavor}Debug
|
||||||
apk_path=app/build/outputs/apk/release/app-release.apk
|
if [ $? -ne 0 ]; then
|
||||||
|
success=0
|
||||||
|
fi
|
||||||
|
apk_path=app/build/outputs/apk/${flavor}/debug/app-${flavor}-debug.apk
|
||||||
else
|
else
|
||||||
./gradlew assembleSignedRelease
|
./gradlew assemble${flavor}SignedRelease
|
||||||
apk_path=app/build/outputs/apk/signedRelease/app-signedRelease.apk
|
if [ $? -ne 0 ]; then
|
||||||
|
success=0
|
||||||
|
fi
|
||||||
|
apk_path=app/build/outputs/apk/${flavor}/signedRelease/app-${flavor}-signedRelease.apk
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ $1 == "" ]]; then
|
if [[ $1 == "" ]] && [ $success -eq 1 ]; then
|
||||||
# Launch application at all connected devices.
|
# Launch application at all connected devices.
|
||||||
for SERIAL in $(adb devices | grep -v List | cut -f 1);
|
for SERIAL in $(adb devices | grep -v List | cut -f 1);
|
||||||
do
|
do
|
||||||
@@ -78,18 +79,17 @@ function build_apk() {
|
|||||||
sleep 1s
|
sleep 1s
|
||||||
adb -s "$SERIAL" shell am start -n mw.gri.android/.MainActivity;
|
adb -s "$SERIAL" shell am start -n mw.gri.android/.MainActivity;
|
||||||
done
|
done
|
||||||
else
|
elif [ $success -eq 1 ]; then
|
||||||
if [[ "$OSTYPE" != "darwin"* ]]; then
|
# Get version
|
||||||
version=$(grep -m 1 -Po 'version = "\K[^"]*' Cargo.toml)
|
version=$2
|
||||||
else
|
if [[ -z "$version" ]]; then
|
||||||
version=v$2
|
version=v$(grep -m 1 -Po 'version = "\K[^"]*' ../Cargo.toml)
|
||||||
fi
|
fi
|
||||||
# Setup release file name
|
# Setup release file name
|
||||||
name=grim-${version}-android-$1.apk
|
name=grim-${version}-android-$1.apk
|
||||||
[[ $1 == "arm" ]] && name=grim-${version}-android.apk
|
[[ $1 == "arm" ]] && name=grim-${version}-android.apk
|
||||||
rm -f "${name}"
|
rm -f "${name}"
|
||||||
mv ${apk_path} "${name}"
|
mv ${apk_path} "${name}"
|
||||||
|
|
||||||
# Calculate checksum
|
# Calculate checksum
|
||||||
checksum=grim-${version}-android-$1-sha256sum.txt
|
checksum=grim-${version}-android-$1-sha256sum.txt
|
||||||
[[ $1 == "arm" ]] && checksum=grim-${version}-android-sha256sum.txt
|
[[ $1 == "arm" ]] && checksum=grim-${version}-android-sha256sum.txt
|
||||||
@@ -101,20 +101,27 @@ function build_apk() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
rm -rf android/app/src/main/jniLibs/*
|
rm -rf android/app/src/main/jniLibs/*
|
||||||
|
if [[ $1 == "lib" ]]; then
|
||||||
if [[ $1 == "build" ]]; then
|
build_lib "v7"
|
||||||
|
[ $success -eq 1 ] && build_lib "v8"
|
||||||
|
[ $success -eq 1 ] && build_lib "x86"
|
||||||
|
[ $success -eq 1 ] && exit 0
|
||||||
|
elif [[ $1 == "build" ]]; then
|
||||||
build_lib "$2"
|
build_lib "$2"
|
||||||
[ $success -eq 1 ] && build_apk
|
[ $success -eq 1 ] && build_apk "" "" "$3"
|
||||||
|
[ $success -eq 1 ] && exit 0
|
||||||
else
|
else
|
||||||
rm -rf target/release-apk
|
rm -rf target/release-apk
|
||||||
rm -rf target/aarch64-linux-android
|
rm -rf target/aarch64-linux-android
|
||||||
rm -rf target/x86_64-linux-android
|
rm -rf target/x86_64-linux-android
|
||||||
rm -rf target/armv7-linux-androideabi
|
rm -rf target/armv7-linux-androideabi
|
||||||
|
|
||||||
build_lib "v7"
|
build_lib "v7"
|
||||||
[ $success -eq 1 ] && build_lib "v8"
|
[ $success -eq 1 ] && build_lib "v8"
|
||||||
[ $success -eq 1 ] && build_apk "arm" "$2"
|
[ $success -eq 1 ] && build_apk "arm" "$2" "$3"
|
||||||
rm -rf android/app/src/main/jniLibs/*
|
rm -rf android/app/src/main/jniLibs/*
|
||||||
[ $success -eq 1 ] && build_lib "x86"
|
[ $success -eq 1 ] && build_lib "x86"
|
||||||
[ $success -eq 1 ] && build_apk "x86_64" "$2"
|
[ $success -eq 1 ] && build_apk "x86_64" "$2" "$3"
|
||||||
fi
|
[ $success -eq 1 ] && exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit 1
|
||||||
+8
-7
@@ -70,13 +70,14 @@ else
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ==================================
|
# Update MacOS version.
|
||||||
# Update Android build.gradle file
|
sed -i '' -e 's/'"$GIT_TAG_LATEST"'/'"$VERSION_NEXT"'/' macos/Grim.app/Contents/Info.plist
|
||||||
# and package version at Cargo.toml
|
|
||||||
# ==================================
|
|
||||||
|
|
||||||
# Update version in build.gradle
|
# 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
|
sed -i'.bak' -e 's/versionName [0-9a-zA-Z -_]*/versionName "'"$VERSION_NEXT"'"/' android/app/build.gradle
|
||||||
rm -f android/app/build.gradle.bak
|
rm -f android/app/build.gradle.bak
|
||||||
|
|
||||||
@@ -90,12 +91,12 @@ cargo update -p grim
|
|||||||
|
|
||||||
# Commit the changes
|
# Commit the changes
|
||||||
git add .
|
git add .
|
||||||
git commit -m "release: v$VERSION_NEXT"
|
git commit -m "build: version $VERSION_NEXT"
|
||||||
|
|
||||||
# ==================================
|
# ==================================
|
||||||
# Create git tag for new version
|
# Create git tag for new version
|
||||||
# ==================================
|
# ==================================
|
||||||
|
|
||||||
# Create a tag and push to master branch
|
# Create a tag and push to master branch
|
||||||
git tag "v$VERSION_NEXT" master
|
#git tag "v$VERSION_NEXT" master
|
||||||
#git push origin master --follow-tags
|
#git push origin master --follow-tags
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
@echo off
|
||||||
|
setlocal enabledelayedexpansion
|
||||||
|
|
||||||
|
:: Change directory to the script's location
|
||||||
|
cd /d "%~dp0"
|
||||||
|
|
||||||
|
:: Skip if Go not found.
|
||||||
|
where go >nul 2>nul
|
||||||
|
if %ERRORLEVEL% neq 0 (
|
||||||
|
echo Go could not be found
|
||||||
|
exit /b 0
|
||||||
|
)
|
||||||
|
|
||||||
|
set "go_os=%~1"
|
||||||
|
set "go_arch=%~2"
|
||||||
|
set "output_path=%~3"
|
||||||
|
|
||||||
|
echo Go build for os: %go_os%, arch: %go_arch%
|
||||||
|
|
||||||
|
:: Setup vars for Android.
|
||||||
|
if "%go_os%"=="android" (
|
||||||
|
|
||||||
|
:: Setup NDK root path env.
|
||||||
|
if "%ANDROID_NDK_HOME%"=="" (
|
||||||
|
:: Extract ndkVersion from build.gradle
|
||||||
|
:: Equivalent to: cat ../android/app/build.gradle | grep 'ndkVersion' | cut -d ' -f 2
|
||||||
|
for /f "tokens=2 delims='" %%a in ('findstr "ndkVersion" ..\android\app\build.gradle') do (
|
||||||
|
set "NDK_VERSION=%%a"
|
||||||
|
)
|
||||||
|
set "ANDROID_NDK_HOME=%ANDROID_HOME%\ndk\!NDK_VERSION!"
|
||||||
|
)
|
||||||
|
|
||||||
|
:: Setup NDK host path.
|
||||||
|
:: Since this is a Batch script, the host is Windows.
|
||||||
|
set "arch_host=windows-x86_64"
|
||||||
|
|
||||||
|
:: Setup NDK target arch.
|
||||||
|
if "%go_arch%"=="arm64" (
|
||||||
|
set "arch_bin_prefix=aarch64-linux-android"
|
||||||
|
) else if "%go_arch%"=="arm" (
|
||||||
|
set "arch_bin_prefix=armv7a-linux-androideabi"
|
||||||
|
) else (
|
||||||
|
set "arch_bin_prefix=x86_64-linux-android"
|
||||||
|
)
|
||||||
|
|
||||||
|
:: Build for current target.
|
||||||
|
set "CGO_ENABLED=1"
|
||||||
|
set "GOOS=%go_os%"
|
||||||
|
set "GOARCH=%go_arch%"
|
||||||
|
|
||||||
|
:: Define CC and CXX paths
|
||||||
|
set "CC=!ANDROID_NDK_HOME!\toolchains\llvm\prebuilt\!arch_host!\bin\!arch_bin_prefix!35-clang"
|
||||||
|
set "CXX=!ANDROID_NDK_HOME!\toolchains\llvm\prebuilt\!arch_host!\bin\!arch_bin_prefix!35-clang++"
|
||||||
|
|
||||||
|
go build -C "../tor/webtunnel" -ldflags="-s -w" -o "%output_path%" code.gri.mw/WEB/webtunnel/main/client
|
||||||
|
|
||||||
|
) else (
|
||||||
|
set "extra_flag="
|
||||||
|
if "%go_os%"=="windows" (
|
||||||
|
set "extra_flag=-H=windowsgui"
|
||||||
|
)
|
||||||
|
|
||||||
|
set "GOOS=%go_os%"
|
||||||
|
set "GOARCH=%go_arch%"
|
||||||
|
|
||||||
|
:: Build for non-android targets
|
||||||
|
go build -C "../tor/webtunnel" -ldflags="-s -w !extra_flag!" -o "%output_path%" code.gri.mw/WEB/webtunnel/main/client
|
||||||
|
)
|
||||||
|
|
||||||
|
endlocal
|
||||||
Executable
+51
@@ -0,0 +1,51 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
|
# Skip if Go not found.
|
||||||
|
if ! command -v go >/dev/null 2>&1
|
||||||
|
then
|
||||||
|
echo "Go could not be found"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
go_os=$1
|
||||||
|
go_arch=$2
|
||||||
|
|
||||||
|
echo "Go build for os: $go_os, arch: $go_arch"
|
||||||
|
|
||||||
|
# Setup vars for Android.
|
||||||
|
if [[ "$go_os" == "android" ]]; then
|
||||||
|
# Setup NDK root path env.
|
||||||
|
if [[ -z "$ANDROID_NDK_HOME" ]]; then
|
||||||
|
NDK_VERSION=$(cat ../android/app/build.gradle | grep 'ndkVersion' | cut -d \' -f 2)
|
||||||
|
ANDROID_NDK_HOME=$ANDROID_HOME/ndk/$NDK_VERSION
|
||||||
|
fi
|
||||||
|
# Setup NDK host path.
|
||||||
|
if [[ "$(uname)" == "Darwin" ]]; then
|
||||||
|
arch_host=darwin-x86_64
|
||||||
|
else
|
||||||
|
if [[ "$(uname -m)" == "aarch64" ]]; then
|
||||||
|
arch_host=linux-arm64
|
||||||
|
else
|
||||||
|
arch_host=linux-x86_64
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
# Setup NDK target arch.
|
||||||
|
if [[ "$go_arch" == "arm64" ]]; then
|
||||||
|
arch_bin_prefix=aarch64-linux-android
|
||||||
|
elif [[ "$go_arch" == "arm" ]]; then
|
||||||
|
arch_bin_prefix=armv7a-linux-androideabi
|
||||||
|
else
|
||||||
|
arch_bin_prefix=x86_64-linux-android
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Build for current target.
|
||||||
|
CGO_ENABLED=1 GOOS=$1 GOARCH=$2 CC="${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/${arch_host}/bin/${arch_bin_prefix}35-clang" CXX="${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/${arch_path}/bin/${arch_bin_prefix}35-clang++" go build -C "../tor/webtunnel" -ldflags="-s -w" -o "$3" code.gri.mw/WEB/webtunnel/main/client
|
||||||
|
else
|
||||||
|
if [[ "$go_os" == "windows" ]]; then
|
||||||
|
extra_flag="-H=windowsgui"
|
||||||
|
fi
|
||||||
|
GOOS=$1 GOARCH=$2 go build -C "../tor/webtunnel" -ldflags="-s -w ${extra_flag}" -o "$3" code.gri.mw/WEB/webtunnel/main/client
|
||||||
|
fi
|
||||||
|
|
||||||
Regular → Executable
+374
-356
@@ -12,412 +12,430 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
use egui::epaint::RectShape;
|
||||||
|
use egui::{
|
||||||
|
Align, Context, CornerRadius, CursorIcon, LayerId, Layout, Modifiers, Order, ResizeDirection,
|
||||||
|
Stroke, StrokeKind, UiBuilder, ViewportCommand,
|
||||||
|
};
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use egui::{Align, Context, CursorIcon, Layout, Modifiers, ResizeDirection, Rounding, Stroke, UiBuilder, ViewportCommand};
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
use egui::epaint::{RectShape};
|
|
||||||
use egui::os::OperatingSystem;
|
|
||||||
|
|
||||||
use crate::AppConfig;
|
use crate::AppConfig;
|
||||||
use crate::gui::Colors;
|
use crate::gui::Colors;
|
||||||
use crate::gui::icons::{ARROWS_IN, ARROWS_OUT, CARET_DOWN, MOON, SUN, X};
|
use crate::gui::icons::{ARROWS_IN, ARROWS_OUT, CARET_DOWN, MOON, SUN, X};
|
||||||
use crate::gui::platform::PlatformCallbacks;
|
use crate::gui::platform::PlatformCallbacks;
|
||||||
use crate::gui::views::{Content, Modal, TitlePanel, View};
|
use crate::gui::views::types::ContentContainer;
|
||||||
use crate::wallet::ExternalConnection;
|
use crate::gui::views::{Content, KeyboardContent, Modal, TitlePanel, View};
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
/// State to check if platform Back button was pressed.
|
/// State to check if platform Back button was pressed.
|
||||||
static ref BACK_BUTTON_PRESSED: AtomicBool = AtomicBool::new(false);
|
static ref BACK_BUTTON_PRESSED: AtomicBool = AtomicBool::new(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Implements ui entry point and contains platform-specific callbacks.
|
/// Implements ui entry point and contains platform-specific callbacks.
|
||||||
pub struct App<Platform> {
|
pub struct App<Platform> {
|
||||||
/// Handles platform-specific functionality.
|
/// Handles platform-specific functionality.
|
||||||
pub platform: Platform,
|
pub platform: Platform,
|
||||||
/// Main content.
|
|
||||||
content: Content,
|
/// Main content.
|
||||||
/// Last window resize direction.
|
content: Content,
|
||||||
resize_direction: Option<ResizeDirection>,
|
|
||||||
/// Flag to check if it's first draw.
|
/// Last window resize direction.
|
||||||
first_draw: bool
|
resize_direction: Option<ResizeDirection>,
|
||||||
|
/// Flag to check if it's first draw.
|
||||||
|
first_draw: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<Platform: PlatformCallbacks> App<Platform> {
|
impl<Platform: PlatformCallbacks> App<Platform> {
|
||||||
pub fn new(platform: Platform) -> Self {
|
pub fn new(platform: Platform) -> Self {
|
||||||
Self {
|
Self {
|
||||||
platform,
|
platform,
|
||||||
content: Content::default(),
|
content: Content::default(),
|
||||||
resize_direction: None,
|
resize_direction: None,
|
||||||
first_draw: true
|
first_draw: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Called of first content draw.
|
/// Called of first content draw.
|
||||||
fn on_first_draw(&mut self, ctx: &Context) {
|
fn on_first_draw(&mut self, ctx: &Context) {
|
||||||
// Set platform context.
|
// Set platform context.
|
||||||
if View::is_desktop() {
|
if View::is_desktop() {
|
||||||
self.platform.set_context(ctx);
|
self.platform.set_context(ctx);
|
||||||
}
|
}
|
||||||
// Check connections availability.
|
// Setup visuals.
|
||||||
ExternalConnection::check(None, ctx);
|
crate::setup_fonts(ctx);
|
||||||
// Setup visuals.
|
crate::setup_visuals(ctx);
|
||||||
crate::setup_visuals(ctx);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/// Draw application content.
|
/// Draw application content.
|
||||||
pub fn ui(&mut self, ctx: &Context) {
|
pub fn ui(&mut self, ctx: &Context) {
|
||||||
if self.first_draw {
|
if self.first_draw {
|
||||||
self.on_first_draw(ctx);
|
self.on_first_draw(ctx);
|
||||||
self.first_draw = false;
|
self.first_draw = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle Esc keyboard key event and platform Back button key event.
|
// Handle Esc keyboard key event and platform Back button key event.
|
||||||
let back_pressed = BACK_BUTTON_PRESSED.load(Ordering::Relaxed);
|
let back_pressed = BACK_BUTTON_PRESSED.load(Ordering::Relaxed);
|
||||||
if back_pressed || ctx.input_mut(|i| i.consume_key(Modifiers::NONE, egui::Key::Escape)) {
|
if back_pressed
|
||||||
self.content.on_back(&self.platform);
|
|| ctx.input_mut(|i| {
|
||||||
if back_pressed {
|
i.consume_key(Modifiers::NONE, egui::Key::Escape)
|
||||||
BACK_BUTTON_PRESSED.store(false, Ordering::Relaxed);
|
|| i.consume_key(Modifiers::NONE, egui::Key::BrowserBack)
|
||||||
}
|
}) {
|
||||||
// Request repaint to update previous content.
|
// Pass event to content.
|
||||||
ctx.request_repaint();
|
self.content.on_back(ctx, &self.platform);
|
||||||
}
|
if back_pressed {
|
||||||
|
BACK_BUTTON_PRESSED.store(false, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
// Request repaint to update previous content.
|
||||||
|
ctx.request_repaint();
|
||||||
|
}
|
||||||
|
|
||||||
// Handle Close event on desktop.
|
// Handle Close event on desktop.
|
||||||
if View::is_desktop() && ctx.input(|i| i.viewport().close_requested()) {
|
if View::is_desktop() && ctx.input(|i| i.viewport().close_requested()) {
|
||||||
if !self.content.exit_allowed {
|
if !self.content.exit_allowed {
|
||||||
ctx.send_viewport_cmd(ViewportCommand::CancelClose);
|
ctx.send_viewport_cmd(ViewportCommand::CancelClose);
|
||||||
Content::show_exit_modal();
|
Content::show_exit_modal();
|
||||||
} else {
|
} else {
|
||||||
let (w, h) = View::window_size(ctx);
|
let (w, h) = View::window_size(ctx);
|
||||||
AppConfig::save_window_size(w, h);
|
AppConfig::save_window_size(w, h);
|
||||||
ctx.input(|i| {
|
ctx.input(|i| {
|
||||||
if let Some(rect) = i.viewport().outer_rect {
|
if let Some(rect) = i.viewport().outer_rect {
|
||||||
AppConfig::save_window_pos(rect.left(), rect.top());
|
AppConfig::save_window_pos(rect.left(), rect.top());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show main content.
|
// Show main content.
|
||||||
egui::CentralPanel::default()
|
egui::CentralPanel::default()
|
||||||
.frame(egui::Frame {
|
.frame(egui::Frame {
|
||||||
..Default::default()
|
..Default::default()
|
||||||
})
|
})
|
||||||
.show(ctx, |ui| {
|
.show(ctx, |ui| {
|
||||||
if View::is_desktop() {
|
if View::is_desktop() {
|
||||||
let is_fullscreen = ui.ctx().input(|i| {
|
let is_fullscreen =
|
||||||
i.viewport().fullscreen.unwrap_or(false)
|
ui.ctx().input(|i| i.viewport().fullscreen.unwrap_or(false));
|
||||||
});
|
let os = egui::os::OperatingSystem::from_target_os();
|
||||||
if OperatingSystem::from_target_os() != OperatingSystem::Mac {
|
match os {
|
||||||
self.desktop_window_ui(ui, is_fullscreen);
|
egui::os::OperatingSystem::Mac => {
|
||||||
} else {
|
self.window_title_ui(ui, is_fullscreen);
|
||||||
self.window_title_ui(ui, is_fullscreen);
|
ui.add_space(-1.0);
|
||||||
ui.add_space(-1.0);
|
Self::title_panel_bg(ui, true);
|
||||||
Self::title_panel_bg(ui);
|
self.content.ui(ui, &self.platform);
|
||||||
self.content.ui(ui, &self.platform);
|
}
|
||||||
}
|
egui::os::OperatingSystem::Windows => {
|
||||||
} else {
|
Self::title_panel_bg(ui, false);
|
||||||
self.mobile_window_ui(ui);
|
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.
|
// Check if desktop window was focused after requested attention.
|
||||||
if let Some(data) = crate::consume_incoming_data() {
|
if self.platform.user_attention_required()
|
||||||
if !data.is_empty() {
|
&& ctx.input(|i| i.viewport().focused.unwrap_or(true))
|
||||||
self.content.wallets.on_data(ui, Some(data), &self.platform);
|
{
|
||||||
}
|
self.platform.clear_user_attention();
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
// Check if desktop window was focused after requested attention.
|
// Show modal or keyboard window above opened Modal.
|
||||||
if self.platform.user_attention_required() &&
|
if Modal::opened().is_some() {
|
||||||
ctx.input(|i| i.viewport().focused.unwrap_or(true)) {
|
ctx.move_to_top(LayerId::new(Order::Middle, egui::Id::new(Modal::WINDOW_ID)));
|
||||||
self.platform.clear_user_attention();
|
let keyboard_showing = if let Some(l) = ctx.top_layer_id() {
|
||||||
}
|
l.id == egui::Id::new(KeyboardContent::WINDOW_ID)
|
||||||
}
|
} else {
|
||||||
|
false
|
||||||
|
};
|
||||||
|
if keyboard_showing {
|
||||||
|
ctx.move_to_top(LayerId::new(
|
||||||
|
Order::Middle,
|
||||||
|
egui::Id::new(KeyboardContent::WINDOW_ID),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Reset keyboard state for newly opened modal.
|
||||||
|
if Modal::first_draw() {
|
||||||
|
KeyboardContent::reset_window_state();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Draw mobile platform window content.
|
/// Draw custom desktop window frame content.
|
||||||
fn mobile_window_ui(&mut self, ui: &mut egui::Ui) {
|
fn custom_frame_ui(&mut self, ui: &mut egui::Ui, is_fullscreen: bool) {
|
||||||
Self::title_panel_bg(ui);
|
let content_bg_rect = {
|
||||||
self.content.ui(ui, &self.platform);
|
let mut r = ui.max_rect();
|
||||||
}
|
if !is_fullscreen {
|
||||||
|
r = r.shrink(Content::WINDOW_FRAME_MARGIN);
|
||||||
|
}
|
||||||
|
r.min.y += Content::WINDOW_TITLE_HEIGHT + TitlePanel::HEIGHT;
|
||||||
|
r
|
||||||
|
};
|
||||||
|
let content_bg = RectShape::new(
|
||||||
|
content_bg_rect,
|
||||||
|
CornerRadius::ZERO,
|
||||||
|
Colors::fill_lite(),
|
||||||
|
View::default_stroke(),
|
||||||
|
StrokeKind::Outside,
|
||||||
|
);
|
||||||
|
// Draw content background.
|
||||||
|
ui.painter().add(content_bg);
|
||||||
|
|
||||||
/// Draw desktop platform window content.
|
let mut content_rect = ui.max_rect();
|
||||||
fn desktop_window_ui(&mut self, ui: &mut egui::Ui, is_fullscreen: bool) {
|
if !is_fullscreen {
|
||||||
let content_bg_rect = {
|
content_rect = content_rect.shrink(Content::WINDOW_FRAME_MARGIN);
|
||||||
let mut r = ui.max_rect();
|
}
|
||||||
if !is_fullscreen {
|
// Draw window content.
|
||||||
r = r.shrink(Content::WINDOW_FRAME_MARGIN);
|
ui.scope_builder(UiBuilder::new().max_rect(content_rect), |ui| {
|
||||||
}
|
// Draw window title.
|
||||||
r.min.y += Content::WINDOW_TITLE_HEIGHT + TitlePanel::HEIGHT;
|
self.window_title_ui(ui, is_fullscreen);
|
||||||
r
|
ui.add_space(-1.0);
|
||||||
};
|
|
||||||
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 mut content_rect = ui.max_rect();
|
// Draw title panel background.
|
||||||
if !is_fullscreen {
|
Self::title_panel_bg(ui, true);
|
||||||
content_rect = content_rect.shrink(Content::WINDOW_FRAME_MARGIN);
|
|
||||||
}
|
|
||||||
// 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.
|
let content_rect = {
|
||||||
Self::title_panel_bg(ui);
|
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);
|
||||||
|
});
|
||||||
|
|
||||||
let content_rect = {
|
// Setup resize areas.
|
||||||
let mut rect = ui.max_rect();
|
if !is_fullscreen {
|
||||||
rect.min.y += Content::WINDOW_TITLE_HEIGHT;
|
self.resize_area_ui(ui, ResizeDirection::North);
|
||||||
rect
|
self.resize_area_ui(ui, ResizeDirection::East);
|
||||||
};
|
self.resize_area_ui(ui, ResizeDirection::South);
|
||||||
let mut content_ui = ui.new_child(UiBuilder::new()
|
self.resize_area_ui(ui, ResizeDirection::West);
|
||||||
.max_rect(content_rect)
|
self.resize_area_ui(ui, ResizeDirection::NorthWest);
|
||||||
.layout(*ui.layout()));
|
self.resize_area_ui(ui, ResizeDirection::NorthEast);
|
||||||
// Draw main content.
|
self.resize_area_ui(ui, ResizeDirection::SouthEast);
|
||||||
self.content.ui(&mut content_ui, &self.platform);
|
self.resize_area_ui(ui, ResizeDirection::SouthWest);
|
||||||
});
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Setup resize areas.
|
/// Draw title panel background.
|
||||||
if !is_fullscreen {
|
fn title_panel_bg(ui: &mut egui::Ui, window_title: bool) {
|
||||||
self.resize_area_ui(ui, ResizeDirection::North);
|
let title_rect = {
|
||||||
self.resize_area_ui(ui, ResizeDirection::East);
|
let mut rect = ui.max_rect();
|
||||||
self.resize_area_ui(ui, ResizeDirection::South);
|
if window_title {
|
||||||
self.resize_area_ui(ui, ResizeDirection::West);
|
rect.min.y += Content::WINDOW_TITLE_HEIGHT - 0.5;
|
||||||
self.resize_area_ui(ui, ResizeDirection::NorthWest);
|
}
|
||||||
self.resize_area_ui(ui, ResizeDirection::NorthEast);
|
rect.max.y = rect.min.y + View::get_top_inset() + TitlePanel::HEIGHT;
|
||||||
self.resize_area_ui(ui, ResizeDirection::SouthEast);
|
rect
|
||||||
self.resize_area_ui(ui, ResizeDirection::SouthWest);
|
};
|
||||||
}
|
let title_bg = RectShape::filled(title_rect, CornerRadius::ZERO, Colors::yellow());
|
||||||
}
|
ui.painter().add(title_bg);
|
||||||
|
}
|
||||||
|
|
||||||
/// Draw title panel background.
|
/// Draw custom window title content.
|
||||||
fn title_panel_bg(ui: &mut egui::Ui) {
|
fn window_title_ui(&self, ui: &mut egui::Ui, is_fullscreen: bool) {
|
||||||
let title_rect = {
|
let title_rect = {
|
||||||
let mut rect = ui.max_rect();
|
let mut rect = ui.max_rect();
|
||||||
if View::is_desktop() {
|
rect.max.y = rect.min.y + Content::WINDOW_TITLE_HEIGHT;
|
||||||
rect.min.y += Content::WINDOW_TITLE_HEIGHT - 0.5;
|
rect
|
||||||
}
|
};
|
||||||
rect.max.y = rect.min.y + View::get_top_inset() + TitlePanel::HEIGHT;
|
|
||||||
rect
|
|
||||||
};
|
|
||||||
let title_bg = RectShape::filled(title_rect, Rounding::ZERO, Colors::yellow());
|
|
||||||
ui.painter().add(title_bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Draw custom window title content.
|
let title_bg_rect = {
|
||||||
fn window_title_ui(&self, ui: &mut egui::Ui, is_fullscreen: bool) {
|
let mut r = title_rect.clone();
|
||||||
let title_rect = {
|
r.max.y += TitlePanel::HEIGHT - 1.0;
|
||||||
let mut rect = ui.max_rect();
|
r
|
||||||
rect.max.y = rect.min.y + Content::WINDOW_TITLE_HEIGHT;
|
};
|
||||||
rect
|
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 {
|
||||||
|
CornerRadius::ZERO
|
||||||
|
} else {
|
||||||
|
CornerRadius {
|
||||||
|
nw: 8.0 as u8,
|
||||||
|
ne: 8.0 as u8,
|
||||||
|
sw: 0.0 as u8,
|
||||||
|
se: 0.0 as u8,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Colors::yellow_dark(),
|
||||||
|
Stroke::new(1.0, Colors::STROKE),
|
||||||
|
StrokeKind::Outside,
|
||||||
|
);
|
||||||
|
// Draw title background.
|
||||||
|
ui.painter().add(window_title_bg);
|
||||||
|
|
||||||
let title_bg_rect = {
|
let painter = ui.painter();
|
||||||
let mut r = title_rect.clone();
|
|
||||||
r.max.y += TitlePanel::HEIGHT - 1.0;
|
|
||||||
r
|
|
||||||
};
|
|
||||||
let is_mac = OperatingSystem::from_target_os() == OperatingSystem::Mac;
|
|
||||||
let window_title_bg = RectShape::new(title_bg_rect, if is_fullscreen || is_mac {
|
|
||||||
Rounding::ZERO
|
|
||||||
} else {
|
|
||||||
Rounding {
|
|
||||||
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.clone();
|
||||||
|
rect.max.x -= 128.0;
|
||||||
|
rect.min.x += 85.0;
|
||||||
|
if !is_fullscreen {
|
||||||
|
rect.min.y += Content::WINDOW_FRAME_MARGIN;
|
||||||
|
}
|
||||||
|
rect
|
||||||
|
};
|
||||||
|
let title_resp = ui.interact(
|
||||||
|
interact_rect,
|
||||||
|
egui::Id::new("window_title"),
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
let interact_rect = {
|
// Paint the title.
|
||||||
let mut rect = title_rect.clone();
|
let title_text = format!("Grim {} ツ", crate::VERSION);
|
||||||
rect.max.x -= 128.0;
|
painter.text(
|
||||||
rect.min.x += 85.0;
|
title_rect.center(),
|
||||||
if !is_fullscreen {
|
egui::Align2::CENTER_CENTER,
|
||||||
rect.min.y += Content::WINDOW_FRAME_MARGIN;
|
title_text,
|
||||||
}
|
egui::FontId::proportional(15.0),
|
||||||
rect
|
Colors::title(true),
|
||||||
};
|
);
|
||||||
let title_resp = ui.interact(
|
|
||||||
interact_rect,
|
|
||||||
egui::Id::new("window_title"),
|
|
||||||
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.
|
ui.scope_builder(UiBuilder::new().max_rect(title_rect), |ui| {
|
||||||
let dual_wallets_panel = ui.available_width() >= (Content::SIDE_PANEL_WIDTH * 3.0) +
|
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
|
||||||
View::get_right_inset() + View::get_left_inset();
|
// Draw button to close window.
|
||||||
let wallet_panel_opened = self.content.wallets.showing_wallet();
|
View::title_button_small(ui, X, |_| {
|
||||||
let show_app_name = if dual_wallets_panel {
|
if Modal::opened().is_none() || Modal::opened_closeable() {
|
||||||
wallet_panel_opened && !AppConfig::show_wallets_at_dual_panel()
|
Content::show_exit_modal();
|
||||||
} else if Content::is_dual_panel_mode(ui.ctx()) {
|
}
|
||||||
wallet_panel_opened
|
});
|
||||||
} else {
|
|
||||||
Content::is_network_panel_open() || wallet_panel_opened
|
|
||||||
};
|
|
||||||
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(),
|
|
||||||
egui::Align2::CENTER_CENTER,
|
|
||||||
title_text,
|
|
||||||
egui::FontId::proportional(15.0),
|
|
||||||
Colors::title(true),
|
|
||||||
);
|
|
||||||
|
|
||||||
ui.allocate_new_ui(UiBuilder::new().max_rect(title_rect), |ui| {
|
// Draw fullscreen button.
|
||||||
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
|
let fullscreen_icon = if is_fullscreen { ARROWS_IN } else { ARROWS_OUT };
|
||||||
// Draw button to close window.
|
View::title_button_small(ui, fullscreen_icon, |ui| {
|
||||||
View::title_button_small(ui, X, |_| {
|
ui.ctx()
|
||||||
if Modal::opened().is_none() || Modal::opened_closeable() {
|
.send_viewport_cmd(ViewportCommand::Fullscreen(!is_fullscreen));
|
||||||
Content::show_exit_modal();
|
});
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Draw fullscreen button.
|
// Draw button to minimize window.
|
||||||
let fullscreen_icon = if is_fullscreen {
|
View::title_button_small(ui, CARET_DOWN, |ui| {
|
||||||
ARROWS_IN
|
ui.ctx().send_viewport_cmd(ViewportCommand::Minimized(true));
|
||||||
} else {
|
});
|
||||||
ARROWS_OUT
|
|
||||||
};
|
|
||||||
View::title_button_small(ui, fullscreen_icon, |ui| {
|
|
||||||
ui.ctx().send_viewport_cmd(ViewportCommand::Fullscreen(!is_fullscreen));
|
|
||||||
});
|
|
||||||
|
|
||||||
// Draw button to minimize window.
|
// Draw application icon.
|
||||||
View::title_button_small(ui, CARET_DOWN, |ui| {
|
let layout_size = ui.available_size();
|
||||||
ui.ctx().send_viewport_cmd(ViewportCommand::Minimized(true));
|
ui.allocate_ui_with_layout(
|
||||||
});
|
layout_size,
|
||||||
|
Layout::left_to_right(Align::Center),
|
||||||
|
|ui| {
|
||||||
|
// Draw button to minimize window.
|
||||||
|
let use_dark = AppConfig::dark_theme().unwrap_or(false);
|
||||||
|
let theme_icon = if use_dark { SUN } else { MOON };
|
||||||
|
View::title_button_small(ui, theme_icon, |ui| {
|
||||||
|
AppConfig::set_dark_theme(!use_dark);
|
||||||
|
crate::setup_visuals(ui.ctx());
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Draw application icon.
|
/// Setup window resize area.
|
||||||
let layout_size = ui.available_size();
|
fn resize_area_ui(&mut self, ui: &egui::Ui, direction: ResizeDirection) {
|
||||||
ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| {
|
let mut rect = ui.max_rect();
|
||||||
// Draw button to minimize window.
|
|
||||||
let use_dark = AppConfig::dark_theme().unwrap_or(false);
|
|
||||||
let theme_icon = if use_dark {
|
|
||||||
SUN
|
|
||||||
} else {
|
|
||||||
MOON
|
|
||||||
};
|
|
||||||
View::title_button_small(ui, theme_icon, |ui| {
|
|
||||||
AppConfig::set_dark_theme(!use_dark);
|
|
||||||
crate::setup_visuals(ui.ctx());
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Setup window resize area.
|
// Setup area id, cursor and area rect based on direction.
|
||||||
fn resize_area_ui(&mut self, ui: &egui::Ui, direction: ResizeDirection) {
|
let (id, cursor, rect) = match direction {
|
||||||
let mut rect = ui.max_rect();
|
ResizeDirection::North => ("n", CursorIcon::ResizeNorth, {
|
||||||
|
rect.min.x += Content::WINDOW_FRAME_MARGIN * 2.0;
|
||||||
|
rect.max.y = rect.min.y + Content::WINDOW_FRAME_MARGIN;
|
||||||
|
rect.max.x -= Content::WINDOW_FRAME_MARGIN * 2.0;
|
||||||
|
rect
|
||||||
|
}),
|
||||||
|
ResizeDirection::East => ("e", CursorIcon::ResizeEast, {
|
||||||
|
rect.min.y += Content::WINDOW_FRAME_MARGIN * 2.0;
|
||||||
|
rect.min.x = rect.max.x - Content::WINDOW_FRAME_MARGIN;
|
||||||
|
rect.max.y -= Content::WINDOW_FRAME_MARGIN * 2.0;
|
||||||
|
rect
|
||||||
|
}),
|
||||||
|
ResizeDirection::South => ("s", CursorIcon::ResizeSouth, {
|
||||||
|
rect.min.x += Content::WINDOW_FRAME_MARGIN * 2.0;
|
||||||
|
rect.min.y = rect.max.y - Content::WINDOW_FRAME_MARGIN;
|
||||||
|
rect.max.x -= Content::WINDOW_FRAME_MARGIN * 2.0;
|
||||||
|
rect
|
||||||
|
}),
|
||||||
|
ResizeDirection::West => ("w", CursorIcon::ResizeWest, {
|
||||||
|
rect.min.y += Content::WINDOW_FRAME_MARGIN * 2.0;
|
||||||
|
rect.max.x = rect.min.x + Content::WINDOW_FRAME_MARGIN;
|
||||||
|
rect.max.y -= Content::WINDOW_FRAME_MARGIN * 2.0;
|
||||||
|
rect
|
||||||
|
}),
|
||||||
|
ResizeDirection::NorthWest => ("nw", CursorIcon::ResizeNorthWest, {
|
||||||
|
rect.max.y = rect.min.y + Content::WINDOW_FRAME_MARGIN * 2.0;
|
||||||
|
rect.max.x = rect.max.y + Content::WINDOW_FRAME_MARGIN * 2.0;
|
||||||
|
rect
|
||||||
|
}),
|
||||||
|
ResizeDirection::NorthEast => ("ne", CursorIcon::ResizeNorthEast, {
|
||||||
|
rect.min.x = rect.max.x - Content::WINDOW_FRAME_MARGIN * 2.0;
|
||||||
|
rect.max.y = Content::WINDOW_FRAME_MARGIN * 2.0;
|
||||||
|
rect
|
||||||
|
}),
|
||||||
|
ResizeDirection::SouthEast => ("se", CursorIcon::ResizeSouthEast, {
|
||||||
|
rect.min.y = rect.max.y - Content::WINDOW_FRAME_MARGIN * 2.0;
|
||||||
|
rect.min.x = rect.max.x - Content::WINDOW_FRAME_MARGIN * 2.0;
|
||||||
|
rect
|
||||||
|
}),
|
||||||
|
ResizeDirection::SouthWest => ("sw", CursorIcon::ResizeSouthWest, {
|
||||||
|
rect.min.y = rect.max.y - Content::WINDOW_FRAME_MARGIN * 2.0;
|
||||||
|
rect.max.x = rect.min.x + Content::WINDOW_FRAME_MARGIN * 2.0;
|
||||||
|
rect
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
// Setup area id, cursor and area rect based on direction.
|
// Setup resize area.
|
||||||
let (id, cursor, rect) = match direction {
|
let id = egui::Id::new("window_resize").with(id);
|
||||||
ResizeDirection::North => ("n", CursorIcon::ResizeNorth, {
|
let sense = egui::Sense::drag();
|
||||||
rect.min.x += Content::WINDOW_FRAME_MARGIN * 2.0;
|
let area_resp = ui.interact(rect, id, sense).on_hover_cursor(cursor);
|
||||||
rect.max.y = rect.min.y + Content::WINDOW_FRAME_MARGIN;
|
if area_resp.dragged() {
|
||||||
rect.max.x -= Content::WINDOW_FRAME_MARGIN * 2.0;
|
if self.resize_direction.is_none() {
|
||||||
rect
|
self.resize_direction = Some(direction.clone());
|
||||||
}),
|
ui.ctx()
|
||||||
ResizeDirection::East => ("e", CursorIcon::ResizeEast, {
|
.send_viewport_cmd(ViewportCommand::BeginResize(direction));
|
||||||
rect.min.y += Content::WINDOW_FRAME_MARGIN * 2.0;
|
}
|
||||||
rect.min.x = rect.max.x - Content::WINDOW_FRAME_MARGIN;
|
}
|
||||||
rect.max.y -= Content::WINDOW_FRAME_MARGIN * 2.0;
|
if area_resp.drag_stopped() {
|
||||||
rect
|
self.resize_direction = None;
|
||||||
}),
|
}
|
||||||
ResizeDirection::South => ("s", CursorIcon::ResizeSouth, {
|
}
|
||||||
rect.min.x += Content::WINDOW_FRAME_MARGIN * 2.0;
|
|
||||||
rect.min.y = rect.max.y - Content::WINDOW_FRAME_MARGIN;
|
|
||||||
rect.max.x -= Content::WINDOW_FRAME_MARGIN * 2.0;
|
|
||||||
rect
|
|
||||||
}),
|
|
||||||
ResizeDirection::West => ("w", CursorIcon::ResizeWest, {
|
|
||||||
rect.min.y += Content::WINDOW_FRAME_MARGIN * 2.0;
|
|
||||||
rect.max.x = rect.min.x + Content::WINDOW_FRAME_MARGIN;
|
|
||||||
rect.max.y -= Content::WINDOW_FRAME_MARGIN * 2.0;
|
|
||||||
rect
|
|
||||||
}),
|
|
||||||
ResizeDirection::NorthWest => ("nw", CursorIcon::ResizeNorthWest, {
|
|
||||||
rect.max.y = rect.min.y + Content::WINDOW_FRAME_MARGIN * 2.0;
|
|
||||||
rect.max.x = rect.max.y + Content::WINDOW_FRAME_MARGIN * 2.0;
|
|
||||||
rect
|
|
||||||
}),
|
|
||||||
ResizeDirection::NorthEast => ("ne", CursorIcon::ResizeNorthEast, {
|
|
||||||
rect.min.x = rect.max.x - Content::WINDOW_FRAME_MARGIN * 2.0;
|
|
||||||
rect.max.y = Content::WINDOW_FRAME_MARGIN * 2.0;
|
|
||||||
rect
|
|
||||||
}),
|
|
||||||
ResizeDirection::SouthEast => ("se", CursorIcon::ResizeSouthEast, {
|
|
||||||
rect.min.y = rect.max.y - Content::WINDOW_FRAME_MARGIN * 2.0;
|
|
||||||
rect.min.x = rect.max.x - Content::WINDOW_FRAME_MARGIN * 2.0;
|
|
||||||
rect
|
|
||||||
}),
|
|
||||||
ResizeDirection::SouthWest => ("sw", CursorIcon::ResizeSouthWest, {
|
|
||||||
rect.min.y = rect.max.y - Content::WINDOW_FRAME_MARGIN * 2.0;
|
|
||||||
rect.max.x = rect.min.x + Content::WINDOW_FRAME_MARGIN * 2.0;
|
|
||||||
rect
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Setup resize area.
|
|
||||||
let id = egui::Id::new("window_resize").with(id);
|
|
||||||
let sense = egui::Sense::drag();
|
|
||||||
let area_resp = ui.interact(rect, id, sense).on_hover_cursor(cursor);
|
|
||||||
if area_resp.dragged() {
|
|
||||||
if self.resize_direction.is_none() {
|
|
||||||
self.resize_direction = Some(direction.clone());
|
|
||||||
ui.ctx().send_viewport_cmd(ViewportCommand::BeginResize(direction));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if area_resp.drag_stopped() {
|
|
||||||
self.resize_direction = None;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// To draw with egui`s eframe (for wgpu, glow backends and wasm target).
|
/// To draw with egui`s eframe (for wgpu, glow backends and wasm target).
|
||||||
impl<Platform: PlatformCallbacks> eframe::App for App<Platform> {
|
impl<Platform: PlatformCallbacks> eframe::App for App<Platform> {
|
||||||
fn update(&mut self, ctx: &Context, _: &mut eframe::Frame) {
|
fn update(&mut self, ctx: &Context, _: &mut eframe::Frame) {
|
||||||
self.ui(ctx);
|
ctx.plugin_or_default::<egui_async::EguiAsyncPlugin>();
|
||||||
}
|
self.ui(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
fn clear_color(&self, _visuals: &egui::Visuals) -> [f32; 4] {
|
fn clear_color(&self, _visuals: &egui::Visuals) -> [f32; 4] {
|
||||||
let is_mac = OperatingSystem::from_target_os() == OperatingSystem::Mac;
|
let os = egui::os::OperatingSystem::from_target_os();
|
||||||
if !View::is_desktop() || is_mac {
|
let is_win = os == egui::os::OperatingSystem::Windows;
|
||||||
return Colors::fill_lite().to_normalized_gamma_f32();
|
let is_mac = os == egui::os::OperatingSystem::Mac;
|
||||||
}
|
if !View::is_desktop() || is_win || is_mac {
|
||||||
Colors::TRANSPARENT.to_normalized_gamma_f32()
|
return Colors::fill_lite().to_normalized_gamma_f32();
|
||||||
}
|
}
|
||||||
|
Colors::TRANSPARENT.to_normalized_gamma_f32()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
#[cfg(target_os = "android")]
|
#[cfg(target_os = "android")]
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
#[no_mangle]
|
#[unsafe(no_mangle)]
|
||||||
/// Handle Back key code event from Android.
|
/// Handle Back key code event from Android.
|
||||||
pub extern "C" fn Java_mw_gri_android_MainActivity_onBack(
|
pub extern "C" fn Java_mw_gri_android_MainActivity_onBack(
|
||||||
_env: jni::JNIEnv,
|
_env: jni::JNIEnv,
|
||||||
_class: jni::objects::JObject,
|
_class: jni::objects::JObject,
|
||||||
_activity: jni::objects::JObject,
|
_activity: jni::objects::JObject,
|
||||||
) {
|
) {
|
||||||
BACK_BUTTON_PRESSED.store(true, Ordering::Relaxed);
|
BACK_BUTTON_PRESSED.store(true, Ordering::Relaxed);
|
||||||
}
|
}
|
||||||
|
|||||||
+138
-158
@@ -26,6 +26,7 @@ const SEMI_TRANSPARENT: Color32 = Color32::from_black_alpha(100);
|
|||||||
const DARK_SEMI_TRANSPARENT: Color32 = Color32::from_black_alpha(170);
|
const DARK_SEMI_TRANSPARENT: Color32 = Color32::from_black_alpha(170);
|
||||||
|
|
||||||
const GOLD: Color32 = Color32::from_rgb(255, 215, 0);
|
const GOLD: Color32 = Color32::from_rgb(255, 215, 0);
|
||||||
|
const GOLD_DARK: Color32 = Color32::from_rgb(240, 203, 1);
|
||||||
|
|
||||||
const YELLOW: Color32 = Color32::from_rgb(254, 241, 2);
|
const YELLOW: Color32 = Color32::from_rgb(254, 241, 2);
|
||||||
const YELLOW_DARK: Color32 = Color32::from_rgb(239, 229, 3);
|
const YELLOW_DARK: Color32 = Color32::from_rgb(239, 229, 3);
|
||||||
@@ -34,20 +35,22 @@ 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 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: 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 RED_DARK: Color32 = Color32::from_rgb((0x8B as f32 * 1.3 + 0.5) as u8, 50, 30);
|
||||||
|
|
||||||
const BLUE: Color32 = Color32::from_rgb(0, 0x66, 0xE4);
|
const BLUE: Color32 = Color32::from_rgb(0, 0x66, 0xE4);
|
||||||
const BLUE_DARK: Color32 =
|
const BLUE_DARK: Color32 = Color32::from_rgb(
|
||||||
Color32::from_rgb(0, (0x66 as f32 * 1.3 + 0.5) as u8, (0xE4 as f32 * 1.3 + 0.5) as u8);
|
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: Color32 = Color32::from_gray(244);
|
||||||
const FILL_DARK: Color32 = Color32::from_gray(24);
|
const FILL_DARK: Color32 = Color32::from_gray(26);
|
||||||
|
|
||||||
const FILL_DEEP: Color32 = Color32::from_gray(238);
|
const FILL_DEEP_DARK: Color32 = Color32::from_gray(32);
|
||||||
const FILL_DEEP_DARK: Color32 = Color32::from_gray(18);
|
|
||||||
|
|
||||||
const FILL_LITE: Color32 = Color32::from_gray(249);
|
const FILL_LITE: Color32 = Color32::from_gray(249);
|
||||||
const FILL_LITE_DARK: Color32 = Color32::from_gray(16);
|
const FILL_LITE_DARK: Color32 = Color32::from_gray(21);
|
||||||
|
|
||||||
const TEXT: Color32 = Color32::from_gray(80);
|
const TEXT: Color32 = Color32::from_gray(80);
|
||||||
const TEXT_DARK: Color32 = Color32::from_gray(185);
|
const TEXT_DARK: Color32 = Color32::from_gray(185);
|
||||||
@@ -80,178 +83,155 @@ const ITEM_HOVER_DARK: Color32 = Color32::from_gray(48);
|
|||||||
|
|
||||||
/// Check if dark theme should be used.
|
/// Check if dark theme should be used.
|
||||||
fn use_dark() -> bool {
|
fn use_dark() -> bool {
|
||||||
AppConfig::dark_theme().unwrap_or(false)
|
AppConfig::dark_theme().unwrap_or(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Colors {
|
impl Colors {
|
||||||
pub const TRANSPARENT: Color32 = Color32::from_rgba_premultiplied(0, 0, 0, 0);
|
pub const FILL_DEEP: Color32 = Color32::from_gray(238);
|
||||||
pub const STROKE: Color32 = Color32::from_gray(200);
|
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 {
|
pub fn white_or_black(black_in_white: bool) -> Color32 {
|
||||||
if use_dark() {
|
if use_dark() {
|
||||||
if black_in_white {
|
if black_in_white { WHITE } else { BLACK }
|
||||||
WHITE
|
} else {
|
||||||
} else {
|
if black_in_white { BLACK } else { WHITE }
|
||||||
BLACK
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
if black_in_white {
|
|
||||||
BLACK
|
|
||||||
} else {
|
|
||||||
WHITE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn semi_transparent() -> Color32 {
|
pub fn semi_transparent() -> Color32 {
|
||||||
if use_dark() {
|
if use_dark() {
|
||||||
DARK_SEMI_TRANSPARENT
|
DARK_SEMI_TRANSPARENT
|
||||||
} else {
|
} else {
|
||||||
SEMI_TRANSPARENT
|
SEMI_TRANSPARENT
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn gold() -> Color32 {
|
pub fn gold() -> Color32 {
|
||||||
if use_dark() {
|
if use_dark() {
|
||||||
GOLD.gamma_multiply(0.9)
|
GOLD.gamma_multiply(0.9)
|
||||||
} else {
|
} else {
|
||||||
GOLD
|
GOLD
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn yellow() -> Color32 {
|
pub fn gold_dark() -> Color32 {
|
||||||
YELLOW
|
if use_dark() {
|
||||||
}
|
GOLD_DARK.gamma_multiply(0.9)
|
||||||
|
} else {
|
||||||
|
GOLD_DARK
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn yellow_dark() -> Color32 {
|
pub fn yellow() -> Color32 {
|
||||||
YELLOW_DARK
|
YELLOW
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn green() -> Color32 {
|
pub fn yellow_dark() -> Color32 {
|
||||||
if use_dark() {
|
YELLOW_DARK
|
||||||
GREEN_DARK
|
}
|
||||||
} else {
|
|
||||||
GREEN
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn red() -> Color32 {
|
pub fn green() -> Color32 {
|
||||||
if use_dark() {
|
if use_dark() { GREEN_DARK } else { GREEN }
|
||||||
RED_DARK
|
}
|
||||||
} else {
|
|
||||||
RED
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn blue() -> Color32 {
|
pub fn red() -> Color32 {
|
||||||
if use_dark() {
|
if use_dark() { RED_DARK } else { RED }
|
||||||
BLUE_DARK
|
}
|
||||||
} else {
|
|
||||||
BLUE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn fill() -> Color32 {
|
pub fn blue() -> Color32 {
|
||||||
if use_dark() {
|
if use_dark() { BLUE_DARK } else { BLUE }
|
||||||
FILL_DARK
|
}
|
||||||
} else {
|
|
||||||
FILL
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn fill_deep() -> Color32 {
|
pub fn fill() -> Color32 {
|
||||||
if use_dark() {
|
if use_dark() { FILL_DARK } else { FILL }
|
||||||
FILL_DEEP_DARK
|
}
|
||||||
} else {
|
|
||||||
FILL_DEEP
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn fill_lite() -> Color32 {
|
pub fn fill_deep() -> Color32 {
|
||||||
if use_dark() {
|
if use_dark() {
|
||||||
FILL_LITE_DARK
|
FILL_DEEP_DARK
|
||||||
} else {
|
} else {
|
||||||
FILL_LITE
|
Self::FILL_DEEP
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn checkbox() -> Color32 {
|
pub fn fill_lite() -> Color32 {
|
||||||
if use_dark() {
|
if use_dark() {
|
||||||
CHECKBOX_DARK
|
FILL_LITE_DARK
|
||||||
} else {
|
} else {
|
||||||
CHECKBOX
|
FILL_LITE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn text(always_light: bool) -> Color32 {
|
pub fn checkbox() -> Color32 {
|
||||||
if use_dark() && !always_light {
|
if use_dark() { CHECKBOX_DARK } else { CHECKBOX }
|
||||||
TEXT_DARK
|
}
|
||||||
} else {
|
|
||||||
TEXT
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn text_button() -> Color32 {
|
pub fn text(always_light: bool) -> Color32 {
|
||||||
if use_dark() {
|
if use_dark() && !always_light {
|
||||||
TEXT_BUTTON_DARK
|
TEXT_DARK
|
||||||
} else {
|
} else {
|
||||||
TEXT_BUTTON
|
TEXT
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn title(always_light: bool) -> Color32 {
|
pub fn text_button() -> Color32 {
|
||||||
if use_dark() && !always_light {
|
if use_dark() {
|
||||||
TITLE_DARK
|
TEXT_BUTTON_DARK
|
||||||
} else {
|
} else {
|
||||||
TITLE
|
TEXT_BUTTON
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn gray() -> Color32 {
|
pub fn title(always_light: bool) -> Color32 {
|
||||||
if use_dark() {
|
if use_dark() && !always_light {
|
||||||
GRAY_DARK
|
TITLE_DARK
|
||||||
} else {
|
} else {
|
||||||
GRAY
|
TITLE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn stroke() -> Color32 {
|
pub fn gray() -> Color32 {
|
||||||
if use_dark() {
|
if use_dark() { GRAY_DARK } else { GRAY }
|
||||||
STROKE_DARK
|
}
|
||||||
} else {
|
|
||||||
Self::STROKE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn inactive_text() -> Color32 {
|
pub fn stroke() -> Color32 {
|
||||||
if use_dark() {
|
if use_dark() {
|
||||||
INACTIVE_TEXT_DARK
|
STROKE_DARK
|
||||||
} else {
|
} else {
|
||||||
INACTIVE_TEXT
|
Self::STROKE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn item_button() -> Color32 {
|
pub fn inactive_text() -> Color32 {
|
||||||
if use_dark() {
|
if use_dark() {
|
||||||
ITEM_BUTTON_DARK
|
INACTIVE_TEXT_DARK
|
||||||
} else {
|
} else {
|
||||||
ITEM_BUTTON
|
INACTIVE_TEXT
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn item_stroke() -> Color32 {
|
pub fn item_button_text() -> Color32 {
|
||||||
if use_dark() {
|
if use_dark() {
|
||||||
ITEM_STROKE_DARK
|
ITEM_BUTTON_DARK
|
||||||
} else {
|
} else {
|
||||||
ITEM_STROKE
|
ITEM_BUTTON
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn item_hover() -> Color32 {
|
pub fn item_stroke() -> Color32 {
|
||||||
if use_dark() {
|
if use_dark() {
|
||||||
ITEM_HOVER_DARK
|
ITEM_STROKE_DARK
|
||||||
} else {
|
} else {
|
||||||
ITEM_HOVER
|
ITEM_STROKE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
pub fn item_hover() -> Color32 {
|
||||||
|
if use_dark() {
|
||||||
|
ITEM_HOVER_DARK
|
||||||
|
} else {
|
||||||
|
ITEM_HOVER
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+1
-2
@@ -12,13 +12,12 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
|
|
||||||
mod app;
|
mod app;
|
||||||
pub use app::App;
|
pub use app::App;
|
||||||
|
|
||||||
mod colors;
|
mod colors;
|
||||||
pub use colors::Colors;
|
pub use colors::Colors;
|
||||||
|
|
||||||
|
pub mod icons;
|
||||||
pub mod platform;
|
pub mod platform;
|
||||||
pub mod views;
|
pub mod views;
|
||||||
pub mod icons;
|
|
||||||
+178
-175
@@ -12,14 +12,14 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
use parking_lot::RwLock;
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::ffi::OsString;
|
use std::ffi::OsString;
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use lazy_static::lazy_static;
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use parking_lot::RwLock;
|
|
||||||
|
|
||||||
use jni::JNIEnv;
|
use jni::JNIEnv;
|
||||||
use jni::objects::{JByteArray, JObject, JString, JValue};
|
use jni::objects::{JByteArray, JObject, JString, JValue};
|
||||||
@@ -30,214 +30,217 @@ use crate::gui::platform::PlatformCallbacks;
|
|||||||
/// Android platform implementation.
|
/// Android platform implementation.
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Android {
|
pub struct Android {
|
||||||
/// Android related state.
|
/// Android related state.
|
||||||
android_app: AndroidApp,
|
android_app: AndroidApp,
|
||||||
|
|
||||||
/// Context to repaint content and handle viewport commands.
|
/// Context to repaint content and handle viewport commands.
|
||||||
ctx: Arc<RwLock<Option<egui::Context>>>,
|
ctx: Arc<RwLock<Option<egui::Context>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Android {
|
impl Android {
|
||||||
/// Create new Android platform instance from provided [`AndroidApp`].
|
/// Create new Android platform instance from provided [`AndroidApp`].
|
||||||
pub fn new(app: AndroidApp) -> Self {
|
pub fn new(app: AndroidApp) -> Self {
|
||||||
Self {
|
Self {
|
||||||
android_app: app,
|
android_app: app,
|
||||||
ctx: Arc::new(RwLock::new(None)),
|
ctx: Arc::new(RwLock::new(None)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Call Android Activity method with JNI.
|
/// Call Android Activity method with JNI.
|
||||||
pub fn call_java_method(&self, name: &str, s: &str, a: &[JValue]) -> Option<jni::sys::jvalue> {
|
pub fn call_java_method(&self, name: &str, s: &str, a: &[JValue]) -> Option<jni::sys::jvalue> {
|
||||||
let vm = unsafe { jni::JavaVM::from_raw(self.android_app.vm_as_ptr() as _) }.unwrap();
|
let vm = unsafe { jni::JavaVM::from_raw(self.android_app.vm_as_ptr() as _) }.unwrap();
|
||||||
let mut env = vm.attach_current_thread().unwrap();
|
let mut env = vm.attach_current_thread().unwrap();
|
||||||
let activity = unsafe {
|
let activity =
|
||||||
JObject::from_raw(self.android_app.activity_as_ptr() as jni::sys::jobject)
|
unsafe { JObject::from_raw(self.android_app.activity_as_ptr() as jni::sys::jobject) };
|
||||||
};
|
if let Ok(result) = env.call_method(activity, name, s, a) {
|
||||||
if let Ok(result) = env.call_method(activity, name, s, a) {
|
return Some(result.as_jni().clone());
|
||||||
return Some(result.as_jni().clone());
|
}
|
||||||
}
|
None
|
||||||
None
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PlatformCallbacks for Android {
|
impl PlatformCallbacks for Android {
|
||||||
fn set_context(&mut self, ctx: &egui::Context) {
|
fn set_context(&mut self, ctx: &egui::Context) {
|
||||||
let mut w_ctx = self.ctx.write();
|
let mut w_ctx = self.ctx.write();
|
||||||
*w_ctx = Some(ctx.clone());
|
*w_ctx = Some(ctx.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
fn exit(&self) {
|
fn exit(&self) {
|
||||||
let _ = self.call_java_method("exit", "()V", &[]);
|
let _ = self.call_java_method("exit", "()V", &[]);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn show_keyboard(&self) {
|
fn copy_string_to_buffer(&self, data: String) {
|
||||||
// Disable NDK soft input show call before fix for egui.
|
let vm = unsafe { jni::JavaVM::from_raw(self.android_app.vm_as_ptr() as _) }.unwrap();
|
||||||
// self.android_app.show_soft_input(false);
|
let env = vm.attach_current_thread().unwrap();
|
||||||
|
let arg_value = env.new_string(data).unwrap();
|
||||||
|
let _ = self.call_java_method(
|
||||||
|
"copyText",
|
||||||
|
"(Ljava/lang/String;)V",
|
||||||
|
&[JValue::Object(&JObject::from(arg_value))],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
let _ = self.call_java_method("showKeyboard", "()V", &[]);
|
fn get_string_from_buffer(&self) -> String {
|
||||||
}
|
let result = self
|
||||||
|
.call_java_method("pasteText", "()Ljava/lang/String;", &[])
|
||||||
|
.unwrap();
|
||||||
|
let vm = unsafe { jni::JavaVM::from_raw(self.android_app.vm_as_ptr() as _) }.unwrap();
|
||||||
|
let mut env = vm.attach_current_thread().unwrap();
|
||||||
|
let j_object: jni::sys::jobject = unsafe { result.l };
|
||||||
|
let paste_data: String = unsafe {
|
||||||
|
env.get_string(JString::from(JObject::from_raw(j_object)).as_ref())
|
||||||
|
.unwrap()
|
||||||
|
.into()
|
||||||
|
};
|
||||||
|
paste_data
|
||||||
|
}
|
||||||
|
|
||||||
fn hide_keyboard(&self) {
|
fn start_camera(&self) {
|
||||||
// Disable NDK soft input hide call before fix for egui.
|
// Clear image.
|
||||||
// self.android_app.hide_soft_input(false);
|
let mut w_image = LAST_CAMERA_IMAGE.write();
|
||||||
|
*w_image = None;
|
||||||
|
// Start camera.
|
||||||
|
let _ = self.call_java_method("startCamera", "()V", &[]);
|
||||||
|
}
|
||||||
|
|
||||||
let _ = self.call_java_method("hideKeyboard", "()V", &[]);
|
fn stop_camera(&self) {
|
||||||
}
|
// Stop camera.
|
||||||
|
let _ = self.call_java_method("stopCamera", "()V", &[]);
|
||||||
|
// Clear image.
|
||||||
|
let mut w_image = LAST_CAMERA_IMAGE.write();
|
||||||
|
*w_image = None;
|
||||||
|
}
|
||||||
|
|
||||||
fn copy_string_to_buffer(&self, data: String) {
|
fn camera_image(&self) -> Option<(Vec<u8>, u32)> {
|
||||||
let vm = unsafe { jni::JavaVM::from_raw(self.android_app.vm_as_ptr() as _) }.unwrap();
|
let r_image = LAST_CAMERA_IMAGE.read();
|
||||||
let env = vm.attach_current_thread().unwrap();
|
if r_image.is_some() {
|
||||||
let arg_value = env.new_string(data).unwrap();
|
return Some(r_image.clone().unwrap());
|
||||||
let _ = self.call_java_method("copyText",
|
}
|
||||||
"(Ljava/lang/String;)V",
|
None
|
||||||
&[JValue::Object(&JObject::from(arg_value))]);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fn get_string_from_buffer(&self) -> String {
|
fn can_switch_camera(&self) -> bool {
|
||||||
let result = self.call_java_method("pasteText", "()Ljava/lang/String;", &[]).unwrap();
|
if let Some(res) = self.call_java_method("camerasAmount", "()I", &[]) {
|
||||||
let vm = unsafe { jni::JavaVM::from_raw(self.android_app.vm_as_ptr() as _) }.unwrap();
|
let amount = unsafe { res.i };
|
||||||
let mut env = vm.attach_current_thread().unwrap();
|
return amount > 1;
|
||||||
let j_object: jni::sys::jobject = unsafe { result.l };
|
}
|
||||||
let paste_data: String = unsafe {
|
false
|
||||||
env.get_string(JString::from(JObject::from_raw(j_object)).as_ref()).unwrap().into()
|
}
|
||||||
};
|
|
||||||
paste_data
|
|
||||||
}
|
|
||||||
|
|
||||||
fn start_camera(&self) {
|
fn switch_camera(&self) {
|
||||||
// Clear image.
|
let _ = self.call_java_method("switchCamera", "()V", &[]);
|
||||||
let mut w_image = LAST_CAMERA_IMAGE.write();
|
}
|
||||||
*w_image = None;
|
|
||||||
// Start camera.
|
|
||||||
let _ = self.call_java_method("startCamera", "()V", &[]);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn stop_camera(&self) {
|
fn share_data(&self, name: String, data: Vec<u8>) -> Result<(), std::io::Error> {
|
||||||
// Stop camera.
|
let default_cache = OsString::from(dirs::cache_dir().unwrap());
|
||||||
let _ = self.call_java_method("stopCamera", "()V", &[]);
|
let mut file = PathBuf::from(env::var_os("XDG_CACHE_HOME").unwrap_or(default_cache));
|
||||||
// Clear image.
|
// File path for Android provider.
|
||||||
let mut w_image = LAST_CAMERA_IMAGE.write();
|
file.push("share");
|
||||||
*w_image = None;
|
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(file.to_str().unwrap()).unwrap();
|
||||||
|
let _ = self.call_java_method(
|
||||||
|
"shareData",
|
||||||
|
"(Ljava/lang/String;)V",
|
||||||
|
&[JValue::Object(&JObject::from(arg_value))],
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn camera_image(&self) -> Option<(Vec<u8>, u32)> {
|
fn pick_file(&self) -> Option<String> {
|
||||||
let r_image = LAST_CAMERA_IMAGE.read();
|
// Clear previous result.
|
||||||
if r_image.is_some() {
|
let mut w_path = PICKED_FILE_PATH.write();
|
||||||
return Some(r_image.clone().unwrap());
|
*w_path = None;
|
||||||
}
|
// Launch file picker.
|
||||||
None
|
let _ = self.call_java_method("pickFile", "()V", &[]);
|
||||||
}
|
// Return empty string to identify async pick.
|
||||||
|
Some("".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
fn can_switch_camera(&self) -> bool {
|
fn pick_folder(&self) -> Option<String> {
|
||||||
if let Some(res) = self.call_java_method("camerasAmount", "()I", &[]) {
|
// Clear previous result.
|
||||||
let amount = unsafe { res.i };
|
let mut w_path = PICKED_FILE_PATH.write();
|
||||||
return amount > 1;
|
*w_path = None;
|
||||||
}
|
// Launch file picker.
|
||||||
false
|
let _ = self.call_java_method("pickFolder", "()V", &[]);
|
||||||
}
|
// Return empty string to identify async pick.
|
||||||
|
Some("".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
fn switch_camera(&self) {
|
fn picked_file(&self) -> Option<String> {
|
||||||
let _ = self.call_java_method("switchCamera", "()V", &[]);
|
let has_file = {
|
||||||
}
|
let r_path = PICKED_FILE_PATH.read();
|
||||||
|
r_path.is_some()
|
||||||
|
};
|
||||||
|
if has_file {
|
||||||
|
let mut w_path = PICKED_FILE_PATH.write();
|
||||||
|
let path = Some(w_path.clone().unwrap());
|
||||||
|
*w_path = None;
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
fn share_data(&self, name: String, data: Vec<u8>) -> Result<(), std::io::Error> {
|
fn request_user_attention(&self) {}
|
||||||
let default_cache = OsString::from(dirs::cache_dir().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(file.to_str().unwrap()).unwrap();
|
|
||||||
let _ = self.call_java_method("shareData",
|
|
||||||
"(Ljava/lang/String;)V",
|
|
||||||
&[JValue::Object(&JObject::from(arg_value))]);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn pick_file(&self) -> Option<String> {
|
fn user_attention_required(&self) -> bool {
|
||||||
// Clear previous result.
|
false
|
||||||
let mut w_path = PICKED_FILE_PATH.write();
|
}
|
||||||
*w_path = None;
|
|
||||||
// Launch file picker.
|
|
||||||
let _ = self.call_java_method("pickFile", "()V", &[]);
|
|
||||||
// Return empty string to identify async pick.
|
|
||||||
Some("".to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn picked_file(&self) -> Option<String> {
|
fn clear_user_attention(&self) {}
|
||||||
let has_file = {
|
|
||||||
let r_path = PICKED_FILE_PATH.read();
|
|
||||||
r_path.is_some()
|
|
||||||
};
|
|
||||||
if has_file {
|
|
||||||
let mut w_path = PICKED_FILE_PATH.write();
|
|
||||||
let path = Some(w_path.clone().unwrap());
|
|
||||||
*w_path = None;
|
|
||||||
return path
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn request_user_attention(&self) {}
|
|
||||||
|
|
||||||
fn user_attention_required(&self) -> bool {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
fn clear_user_attention(&self) {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
/// Last image data from camera.
|
/// Last image data from camera.
|
||||||
static ref LAST_CAMERA_IMAGE: Arc<RwLock<Option<(Vec<u8>, u32)>>> = Arc::new(RwLock::new(None));
|
static ref LAST_CAMERA_IMAGE: Arc<RwLock<Option<(Vec<u8>, u32)>>> = Arc::new(RwLock::new(None));
|
||||||
/// Picked file path.
|
/// Picked file path.
|
||||||
static ref PICKED_FILE_PATH: Arc<RwLock<Option<String>>> = Arc::new(RwLock::new(None));
|
static ref PICKED_FILE_PATH: Arc<RwLock<Option<String>>> = Arc::new(RwLock::new(None));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Callback from Java code with last entered character from soft keyboard.
|
/// Callback from Java code with last entered character from soft keyboard.
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
#[no_mangle]
|
#[unsafe(no_mangle)]
|
||||||
pub extern "C" fn Java_mw_gri_android_MainActivity_onCameraImage(
|
pub extern "C" fn Java_mw_gri_android_MainActivity_onCameraImage(
|
||||||
env: JNIEnv,
|
env: JNIEnv,
|
||||||
_class: JObject,
|
_class: JObject,
|
||||||
buff: jni::sys::jbyteArray,
|
buff: jni::sys::jbyteArray,
|
||||||
rotation: jni::sys::jint,
|
rotation: jni::sys::jint,
|
||||||
) {
|
) {
|
||||||
let arr = unsafe { JByteArray::from_raw(buff) };
|
let arr = unsafe { JByteArray::from_raw(buff) };
|
||||||
let image : Vec<u8> = env.convert_byte_array(arr).unwrap();
|
let image: Vec<u8> = env.convert_byte_array(arr).unwrap();
|
||||||
let mut w_image = LAST_CAMERA_IMAGE.write();
|
let mut w_image = LAST_CAMERA_IMAGE.write();
|
||||||
*w_image = Some((image, rotation as u32));
|
*w_image = Some((image, rotation as u32));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Callback from Java code with picked file path.
|
/// Callback from Java code with picked file path.
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
#[no_mangle]
|
#[unsafe(no_mangle)]
|
||||||
pub extern "C" fn Java_mw_gri_android_MainActivity_onFilePick(
|
pub extern "C" fn Java_mw_gri_android_MainActivity_onFilePick(
|
||||||
_env: JNIEnv,
|
_env: JNIEnv,
|
||||||
_class: JObject,
|
_class: JObject,
|
||||||
char: jni::sys::jstring
|
char: jni::sys::jstring,
|
||||||
) {
|
) {
|
||||||
use std::ops::Add;
|
use std::ops::Add;
|
||||||
unsafe {
|
unsafe {
|
||||||
let j_obj = JString::from_raw(char);
|
let j_obj = JString::from_raw(char);
|
||||||
let j_str = _env.get_string_unchecked(j_obj.as_ref()).unwrap();
|
let j_str = _env.get_string_unchecked(j_obj.as_ref()).unwrap();
|
||||||
match j_str.to_str() {
|
match j_str.to_str() {
|
||||||
Ok(str) => {
|
Ok(str) => {
|
||||||
let mut w_path = PICKED_FILE_PATH.write();
|
let mut w_path = PICKED_FILE_PATH.write();
|
||||||
*w_path = Some(w_path.clone().unwrap_or("".to_string()).add(str));
|
*w_path = Some(w_path.clone().unwrap_or("".to_string()).add(str));
|
||||||
}
|
}
|
||||||
Err(_) => {}
|
Err(_) => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+270
-252
@@ -12,298 +12,316 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
|
use egui::{UserAttentionType, ViewportCommand, WindowLevel};
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
use parking_lot::RwLock;
|
||||||
|
use rfd::FileDialog;
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use std::thread;
|
|
||||||
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use parking_lot::RwLock;
|
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
||||||
use lazy_static::lazy_static;
|
use std::thread;
|
||||||
use egui::{UserAttentionType, ViewportCommand, WindowLevel};
|
|
||||||
use rfd::FileDialog;
|
|
||||||
|
|
||||||
use crate::gui::platform::PlatformCallbacks;
|
use crate::gui::platform::PlatformCallbacks;
|
||||||
|
|
||||||
/// Desktop platform related actions.
|
/// Desktop platform related actions.
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Desktop {
|
pub struct Desktop {
|
||||||
/// Context to repaint content and handle viewport commands.
|
/// Context to repaint content and handle viewport commands.
|
||||||
ctx: Arc<RwLock<Option<egui::Context>>>,
|
ctx: Arc<RwLock<Option<egui::Context>>>,
|
||||||
|
|
||||||
/// Cameras amount.
|
/// Cameras amount.
|
||||||
cameras_amount: Arc<AtomicUsize>,
|
cameras_amount: Arc<AtomicUsize>,
|
||||||
/// Camera index.
|
/// Camera index.
|
||||||
camera_index: Arc<AtomicUsize>,
|
camera_index: Arc<AtomicUsize>,
|
||||||
/// Flag to check if camera stop is needed.
|
/// Flag to check if camera stop is needed.
|
||||||
stop_camera: Arc<AtomicBool>,
|
stop_camera: Arc<AtomicBool>,
|
||||||
|
|
||||||
/// Flag to check if attention required after window focusing.
|
/// Flag to check if attention required after window focusing.
|
||||||
attention_required: Arc<AtomicBool>,
|
attention_required: Arc<AtomicBool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Desktop {
|
impl Desktop {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
ctx: Arc::new(RwLock::new(None)),
|
ctx: Arc::new(RwLock::new(None)),
|
||||||
cameras_amount: Arc::new(AtomicUsize::new(0)),
|
cameras_amount: Arc::new(AtomicUsize::new(0)),
|
||||||
camera_index: Arc::new(AtomicUsize::new(0)),
|
camera_index: Arc::new(AtomicUsize::new(0)),
|
||||||
stop_camera: Arc::new(AtomicBool::new(false)),
|
stop_camera: Arc::new(AtomicBool::new(false)),
|
||||||
attention_required: Arc::new(AtomicBool::new(false)),
|
attention_required: Arc::new(AtomicBool::new(false)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
// #[allow(dead_code)]
|
||||||
#[cfg(not(target_os = "macos"))]
|
#[cfg(not(target_os = "macos"))]
|
||||||
fn start_camera_capture(cameras_amount: Arc<AtomicUsize>,
|
fn start_camera_capture(
|
||||||
camera_index: Arc<AtomicUsize>,
|
cameras_amount: Arc<AtomicUsize>,
|
||||||
stop_camera: Arc<AtomicBool>) {
|
camera_index: Arc<AtomicUsize>,
|
||||||
use nokhwa::Camera;
|
stop_camera: Arc<AtomicBool>,
|
||||||
use nokhwa::pixel_format::RgbFormat;
|
) {
|
||||||
use nokhwa::utils::{CameraIndex, RequestedFormat, RequestedFormatType};
|
use nokhwa::Camera;
|
||||||
use nokhwa::utils::ApiBackend;
|
use nokhwa::pixel_format::RgbFormat;
|
||||||
|
use nokhwa::utils::ApiBackend;
|
||||||
|
use nokhwa::utils::{CameraIndex, RequestedFormat, RequestedFormatType};
|
||||||
|
|
||||||
let devices = nokhwa::query(ApiBackend::Auto).unwrap();
|
let devices = nokhwa::query(ApiBackend::Auto).unwrap();
|
||||||
cameras_amount.store(devices.len(), Ordering::Relaxed);
|
cameras_amount.store(devices.len(), Ordering::Relaxed);
|
||||||
let index = camera_index.load(Ordering::Relaxed);
|
let index = camera_index.load(Ordering::Relaxed);
|
||||||
if devices.is_empty() || index >= devices.len() {
|
if devices.is_empty() || index >= devices.len() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
thread::spawn(move || {
|
thread::spawn(move || {
|
||||||
let index = CameraIndex::Index(camera_index.load(Ordering::Relaxed) as u32);
|
let index = CameraIndex::Index(camera_index.load(Ordering::Relaxed) as u32);
|
||||||
let requested = RequestedFormat::new::<RgbFormat>(
|
let requested =
|
||||||
RequestedFormatType::AbsoluteHighestFrameRate
|
RequestedFormat::new::<RgbFormat>(RequestedFormatType::AbsoluteHighestFrameRate);
|
||||||
);
|
// Create and open camera.
|
||||||
// Create and open camera.
|
if let Ok(mut camera) = Camera::new(index, requested) {
|
||||||
if let Ok(mut camera) = Camera::new(index, requested) {
|
if let Ok(_) = camera.open_stream() {
|
||||||
if let Ok(_) = camera.open_stream() {
|
loop {
|
||||||
loop {
|
// Stop if camera was stopped.
|
||||||
// Stop if camera was stopped.
|
if stop_camera.load(Ordering::Relaxed) {
|
||||||
if stop_camera.load(Ordering::Relaxed) {
|
stop_camera.store(false, Ordering::Relaxed);
|
||||||
stop_camera.store(false, Ordering::Relaxed);
|
// Clear image.
|
||||||
// Clear image.
|
let mut w_image = LAST_CAMERA_IMAGE.write();
|
||||||
let mut w_image = LAST_CAMERA_IMAGE.write();
|
*w_image = None;
|
||||||
*w_image = None;
|
break;
|
||||||
break;
|
}
|
||||||
}
|
// Get a frame.
|
||||||
// Get a frame.
|
if let Ok(frame) = camera.frame() {
|
||||||
if let Ok(frame) = camera.frame() {
|
// Save image.
|
||||||
// Save image.
|
let mut w_image = LAST_CAMERA_IMAGE.write();
|
||||||
let mut w_image = LAST_CAMERA_IMAGE.write();
|
*w_image = Some((frame.buffer().to_vec(), 0));
|
||||||
*w_image = Some((frame.buffer().to_vec(), 0));
|
} else {
|
||||||
} else {
|
// Clear image.
|
||||||
// Clear image.
|
let mut w_image = LAST_CAMERA_IMAGE.write();
|
||||||
let mut w_image = LAST_CAMERA_IMAGE.write();
|
*w_image = None;
|
||||||
*w_image = None;
|
break;
|
||||||
break;
|
}
|
||||||
}
|
}
|
||||||
}
|
camera.stop_stream().unwrap();
|
||||||
camera.stop_stream().unwrap();
|
};
|
||||||
};
|
}
|
||||||
}
|
});
|
||||||
});
|
}
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
fn start_camera_capture(cameras_amount: Arc<AtomicUsize>,
|
fn start_camera_capture(
|
||||||
camera_index: Arc<AtomicUsize>,
|
cameras_amount: Arc<AtomicUsize>,
|
||||||
stop_camera: Arc<AtomicBool>) {
|
camera_index: Arc<AtomicUsize>,
|
||||||
use image::{ExtendedColorType, ImageBuffer, ImageEncoder, Rgb};
|
stop_camera: Arc<AtomicBool>,
|
||||||
use eye::hal::{traits::{Context, Device, Stream}, PlatformContext};
|
) {
|
||||||
use image::codecs::jpeg::JpegEncoder;
|
use nokhwa::CallbackCamera;
|
||||||
|
use nokhwa::nokhwa_initialize;
|
||||||
|
use nokhwa::pixel_format::RgbFormat;
|
||||||
|
use nokhwa::query;
|
||||||
|
use nokhwa::utils::ApiBackend;
|
||||||
|
use nokhwa::utils::{CameraIndex, RequestedFormat, RequestedFormatType};
|
||||||
|
|
||||||
let index = camera_index.load(Ordering::Relaxed);
|
// Ask permission to open camera.
|
||||||
let devices = PlatformContext::default().devices().unwrap_or(vec![]);
|
nokhwa_initialize(|_| {});
|
||||||
cameras_amount.store(devices.len(), Ordering::Relaxed);
|
|
||||||
if devices.is_empty() || index >= devices.len() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Capture images at separate thread.
|
thread::spawn(move || {
|
||||||
let uri = devices[camera_index.load(Ordering::Relaxed)].uri.clone();
|
let cameras = query(ApiBackend::Auto).unwrap();
|
||||||
thread::spawn(move || {
|
cameras_amount.store(cameras.len(), Ordering::Relaxed);
|
||||||
if let Ok(dev) = PlatformContext::default().open_device(&uri) {
|
let index = camera_index.load(Ordering::Relaxed);
|
||||||
let streams = dev.streams().unwrap_or(vec![]);
|
if cameras.is_empty() || index >= cameras.len() {
|
||||||
if streams.is_empty() {
|
return;
|
||||||
return;
|
}
|
||||||
}
|
// Start camera.
|
||||||
let stream_desc = streams[0].clone();
|
let camera_index = CameraIndex::Index(camera_index.load(Ordering::Relaxed) as u32);
|
||||||
let w = stream_desc.width;
|
let camera_callback = CallbackCamera::new(
|
||||||
let h = stream_desc.height;
|
camera_index,
|
||||||
if let Ok(mut stream) = dev.start_stream(&stream_desc) {
|
RequestedFormat::new::<RgbFormat>(RequestedFormatType::AbsoluteHighestFrameRate),
|
||||||
loop {
|
|_| {},
|
||||||
// Stop if camera was stopped.
|
);
|
||||||
if stop_camera.load(Ordering::Relaxed) {
|
if let Ok(mut cb) = camera_callback {
|
||||||
stop_camera.store(false, Ordering::Relaxed);
|
if cb.open_stream().is_ok() {
|
||||||
let mut w_image = LAST_CAMERA_IMAGE.write();
|
loop {
|
||||||
*w_image = None;
|
// Stop if camera was stopped.
|
||||||
break;
|
if stop_camera.load(Ordering::Relaxed) {
|
||||||
}
|
stop_camera.store(false, Ordering::Relaxed);
|
||||||
// Get a frame.
|
// Clear image.
|
||||||
let frame = stream.next()
|
let mut w_image = LAST_CAMERA_IMAGE.write();
|
||||||
.expect("Stream is dead")
|
*w_image = None;
|
||||||
.expect("Failed to capture a frame");
|
break;
|
||||||
let mut out = vec![];
|
}
|
||||||
if let Some(buf) = ImageBuffer::<Rgb<u8>, &[u8]>::from_raw(w, h, &frame) {
|
// Get image from camera.
|
||||||
JpegEncoder::new(&mut out)
|
if let Ok(frame) = cb.poll_frame() {
|
||||||
.write_image(buf.as_raw(), w, h, ExtendedColorType::Rgb8)
|
let image = frame.decode_image::<RgbFormat>().unwrap();
|
||||||
.unwrap_or_default();
|
let mut bytes: Vec<u8> = Vec::new();
|
||||||
} else {
|
let format = image::ImageFormat::Jpeg;
|
||||||
out = frame.to_vec();
|
// Convert image to Jpeg format.
|
||||||
}
|
image
|
||||||
// Save image.
|
.write_to(&mut std::io::Cursor::new(&mut bytes), format)
|
||||||
let mut w_image = LAST_CAMERA_IMAGE.write();
|
.unwrap();
|
||||||
*w_image = Some((out, 0));
|
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 {
|
impl PlatformCallbacks for Desktop {
|
||||||
fn set_context(&mut self, ctx: &egui::Context) {
|
fn set_context(&mut self, ctx: &egui::Context) {
|
||||||
let mut w_ctx = self.ctx.write();
|
let mut w_ctx = self.ctx.write();
|
||||||
*w_ctx = Some(ctx.clone());
|
*w_ctx = Some(ctx.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
fn exit(&self) {
|
fn exit(&self) {
|
||||||
let r_ctx = self.ctx.read();
|
let r_ctx = self.ctx.read();
|
||||||
if r_ctx.is_some() {
|
if r_ctx.is_some() {
|
||||||
let ctx = r_ctx.as_ref().unwrap();
|
let ctx = r_ctx.as_ref().unwrap();
|
||||||
ctx.send_viewport_cmd(ViewportCommand::Close);
|
ctx.send_viewport_cmd(ViewportCommand::Close);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn show_keyboard(&self) {}
|
fn copy_string_to_buffer(&self, data: String) {
|
||||||
|
let mut clipboard = arboard::Clipboard::new().unwrap();
|
||||||
|
clipboard.set_text(data).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
fn hide_keyboard(&self) {}
|
fn get_string_from_buffer(&self) -> String {
|
||||||
|
let mut clipboard = arboard::Clipboard::new().unwrap();
|
||||||
|
clipboard.get_text().unwrap_or("".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
fn copy_string_to_buffer(&self, data: String) {
|
fn start_camera(&self) {
|
||||||
let mut clipboard = arboard::Clipboard::new().unwrap();
|
// Clear image.
|
||||||
clipboard.set_text(data).unwrap();
|
{
|
||||||
}
|
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);
|
||||||
|
|
||||||
fn get_string_from_buffer(&self) -> String {
|
Self::start_camera_capture(
|
||||||
let mut clipboard = arboard::Clipboard::new().unwrap();
|
self.cameras_amount.clone(),
|
||||||
clipboard.get_text().unwrap_or("".to_string())
|
self.camera_index.clone(),
|
||||||
}
|
stop_camera,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
fn start_camera(&self) {
|
fn stop_camera(&self) {
|
||||||
// Clear image.
|
// Stop camera.
|
||||||
{
|
self.stop_camera.store(true, Ordering::Relaxed);
|
||||||
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);
|
|
||||||
|
|
||||||
Self::start_camera_capture(self.cameras_amount.clone(),
|
fn camera_image(&self) -> Option<(Vec<u8>, u32)> {
|
||||||
self.camera_index.clone(),
|
let r_image = LAST_CAMERA_IMAGE.read();
|
||||||
stop_camera);
|
if r_image.is_some() {
|
||||||
}
|
return r_image.clone();
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
fn stop_camera(&self) {
|
fn can_switch_camera(&self) -> bool {
|
||||||
// Stop camera.
|
let amount = self.cameras_amount.load(Ordering::Relaxed);
|
||||||
self.stop_camera.store(true, Ordering::Relaxed);
|
amount > 1
|
||||||
}
|
}
|
||||||
|
|
||||||
fn camera_image(&self) -> Option<(Vec<u8>, u32)> {
|
fn switch_camera(&self) {
|
||||||
let r_image = LAST_CAMERA_IMAGE.read();
|
self.stop_camera();
|
||||||
if r_image.is_some() {
|
let index = self.camera_index.load(Ordering::Relaxed);
|
||||||
return r_image.clone();
|
let amount = self.cameras_amount.load(Ordering::Relaxed);
|
||||||
}
|
if index == amount - 1 {
|
||||||
None
|
self.camera_index.store(0, Ordering::Relaxed);
|
||||||
}
|
} else {
|
||||||
|
self.camera_index.store(index + 1, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
self.start_camera();
|
||||||
|
}
|
||||||
|
|
||||||
fn can_switch_camera(&self) -> bool {
|
fn share_data(&self, name: String, data: Vec<u8>) -> Result<(), std::io::Error> {
|
||||||
let amount = self.cameras_amount.load(Ordering::Relaxed);
|
let folder = FileDialog::new()
|
||||||
amount > 1
|
.set_title(t!("share"))
|
||||||
}
|
.set_directory(dirs::home_dir().unwrap())
|
||||||
|
.set_file_name(name.clone())
|
||||||
|
.save_file();
|
||||||
|
if let Some(folder) = folder {
|
||||||
|
let mut image = File::create(folder)?;
|
||||||
|
image.write_all(data.as_slice())?;
|
||||||
|
image.sync_all()?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn switch_camera(&self) {
|
fn pick_file(&self) -> Option<String> {
|
||||||
self.stop_camera();
|
let file = FileDialog::new()
|
||||||
let index = self.camera_index.load(Ordering::Relaxed);
|
.set_title(t!("choose_file"))
|
||||||
let amount = self.cameras_amount.load(Ordering::Relaxed);
|
.set_directory(dirs::home_dir().unwrap())
|
||||||
if index == amount - 1 {
|
.pick_file();
|
||||||
self.camera_index.store(0, Ordering::Relaxed);
|
if let Some(file) = file {
|
||||||
} else {
|
return Some(file.to_str().unwrap_or_default().to_string());
|
||||||
self.camera_index.store(index + 1, Ordering::Relaxed);
|
}
|
||||||
}
|
None
|
||||||
self.start_camera();
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fn share_data(&self, name: String, data: Vec<u8>) -> Result<(), std::io::Error> {
|
fn pick_folder(&self) -> Option<String> {
|
||||||
let folder = FileDialog::new()
|
let file = FileDialog::new()
|
||||||
.set_title(t!("share"))
|
.set_title(t!("choose_folder"))
|
||||||
.set_directory(dirs::home_dir().unwrap())
|
.set_directory(dirs::home_dir().unwrap())
|
||||||
.set_file_name(name.clone())
|
.pick_folder();
|
||||||
.save_file();
|
if let Some(file) = file {
|
||||||
if let Some(folder) = folder {
|
return Some(file.to_str().unwrap_or_default().to_string());
|
||||||
let mut image = File::create(folder)?;
|
}
|
||||||
image.write_all(data.as_slice())?;
|
None
|
||||||
image.sync_all()?;
|
}
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn pick_file(&self) -> Option<String> {
|
fn picked_file(&self) -> Option<String> {
|
||||||
let file = FileDialog::new()
|
None
|
||||||
.set_title(t!("choose_file"))
|
}
|
||||||
.set_directory(dirs::home_dir().unwrap())
|
|
||||||
.pick_file();
|
|
||||||
if let Some(file) = file {
|
|
||||||
return Some(file.to_str().unwrap_or_default().to_string());
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn picked_file(&self) -> Option<String> {
|
fn request_user_attention(&self) {
|
||||||
None
|
let r_ctx = self.ctx.read();
|
||||||
}
|
if r_ctx.is_some() {
|
||||||
|
let ctx = r_ctx.as_ref().unwrap();
|
||||||
|
// Request attention on taskbar.
|
||||||
|
ctx.send_viewport_cmd(ViewportCommand::RequestUserAttention(
|
||||||
|
UserAttentionType::Informational,
|
||||||
|
));
|
||||||
|
// Un-minimize window.
|
||||||
|
if ctx.input(|i| i.viewport().minimized.unwrap_or(false)) {
|
||||||
|
ctx.send_viewport_cmd(ViewportCommand::Minimized(false));
|
||||||
|
}
|
||||||
|
// Focus to window.
|
||||||
|
if !ctx.input(|i| i.viewport().focused.unwrap_or(false)) {
|
||||||
|
ctx.send_viewport_cmd(ViewportCommand::WindowLevel(WindowLevel::AlwaysOnTop));
|
||||||
|
ctx.send_viewport_cmd(ViewportCommand::Focus);
|
||||||
|
}
|
||||||
|
ctx.request_repaint();
|
||||||
|
}
|
||||||
|
self.attention_required.store(true, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
fn request_user_attention(&self) {
|
fn user_attention_required(&self) -> bool {
|
||||||
let r_ctx = self.ctx.read();
|
self.attention_required.load(Ordering::Relaxed)
|
||||||
if r_ctx.is_some() {
|
}
|
||||||
let ctx = r_ctx.as_ref().unwrap();
|
|
||||||
// Request attention on taskbar.
|
|
||||||
ctx.send_viewport_cmd(
|
|
||||||
ViewportCommand::RequestUserAttention(UserAttentionType::Informational)
|
|
||||||
);
|
|
||||||
// Un-minimize window.
|
|
||||||
if ctx.input(|i| i.viewport().minimized.unwrap_or(false)) {
|
|
||||||
ctx.send_viewport_cmd(ViewportCommand::Minimized(false));
|
|
||||||
}
|
|
||||||
// Focus to window.
|
|
||||||
if !ctx.input(|i| i.viewport().focused.unwrap_or(false)) {
|
|
||||||
ctx.send_viewport_cmd(ViewportCommand::WindowLevel(WindowLevel::AlwaysOnTop));
|
|
||||||
ctx.send_viewport_cmd(ViewportCommand::Focus);
|
|
||||||
}
|
|
||||||
ctx.request_repaint();
|
|
||||||
}
|
|
||||||
self.attention_required.store(true, Ordering::Relaxed);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn user_attention_required(&self) -> bool {
|
fn clear_user_attention(&self) {
|
||||||
self.attention_required.load(Ordering::Relaxed)
|
let r_ctx = self.ctx.read();
|
||||||
}
|
if r_ctx.is_some() {
|
||||||
|
let ctx = r_ctx.as_ref().unwrap();
|
||||||
fn clear_user_attention(&self) {
|
ctx.send_viewport_cmd(ViewportCommand::RequestUserAttention(
|
||||||
let r_ctx = self.ctx.read();
|
UserAttentionType::Reset,
|
||||||
if r_ctx.is_some() {
|
));
|
||||||
let ctx = r_ctx.as_ref().unwrap();
|
ctx.send_viewport_cmd(ViewportCommand::WindowLevel(WindowLevel::Normal));
|
||||||
ctx.send_viewport_cmd(
|
}
|
||||||
ViewportCommand::RequestUserAttention(UserAttentionType::Reset)
|
self.attention_required.store(false, Ordering::Relaxed);
|
||||||
);
|
}
|
||||||
ctx.send_viewport_cmd(ViewportCommand::WindowLevel(WindowLevel::Normal));
|
|
||||||
}
|
|
||||||
self.attention_required.store(false, Ordering::Relaxed);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
/// Last captured image from started camera.
|
/// Last captured image from started camera.
|
||||||
static ref LAST_CAMERA_IMAGE: Arc<RwLock<Option<(Vec<u8>, u32)>>> = Arc::new(RwLock::new(None));
|
static ref LAST_CAMERA_IMAGE: Arc<RwLock<Option<(Vec<u8>, u32)>>> = Arc::new(RwLock::new(None));
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-18
@@ -22,21 +22,20 @@ pub mod platform;
|
|||||||
pub mod platform;
|
pub mod platform;
|
||||||
|
|
||||||
pub trait PlatformCallbacks {
|
pub trait PlatformCallbacks {
|
||||||
fn set_context(&mut self, ctx: &egui::Context);
|
fn set_context(&mut self, ctx: &egui::Context);
|
||||||
fn exit(&self);
|
fn exit(&self);
|
||||||
fn show_keyboard(&self);
|
fn copy_string_to_buffer(&self, data: String);
|
||||||
fn hide_keyboard(&self);
|
fn get_string_from_buffer(&self) -> String;
|
||||||
fn copy_string_to_buffer(&self, data: String);
|
fn start_camera(&self);
|
||||||
fn get_string_from_buffer(&self) -> String;
|
fn stop_camera(&self);
|
||||||
fn start_camera(&self);
|
fn camera_image(&self) -> Option<(Vec<u8>, u32)>;
|
||||||
fn stop_camera(&self);
|
fn can_switch_camera(&self) -> bool;
|
||||||
fn camera_image(&self) -> Option<(Vec<u8>, u32)>;
|
fn switch_camera(&self);
|
||||||
fn can_switch_camera(&self) -> bool;
|
fn share_data(&self, name: String, data: Vec<u8>) -> Result<(), std::io::Error>;
|
||||||
fn switch_camera(&self);
|
fn pick_file(&self) -> Option<String>;
|
||||||
fn share_data(&self, name: String, data: Vec<u8>) -> Result<(), std::io::Error>;
|
fn pick_folder(&self) -> Option<String>;
|
||||||
fn pick_file(&self) -> Option<String>;
|
fn picked_file(&self) -> Option<String>;
|
||||||
fn picked_file(&self) -> Option<String>;
|
fn request_user_attention(&self);
|
||||||
fn request_user_attention(&self);
|
fn user_attention_required(&self) -> bool;
|
||||||
fn user_attention_required(&self) -> bool;
|
fn clear_user_attention(&self);
|
||||||
fn clear_user_attention(&self);
|
}
|
||||||
}
|
|
||||||
|
|||||||
+434
-388
@@ -12,429 +12,475 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
use std::sync::Arc;
|
|
||||||
use parking_lot::RwLock;
|
|
||||||
use std::thread;
|
|
||||||
use egui::load::SizedTexture;
|
use egui::load::SizedTexture;
|
||||||
use egui::{Pos2, Rect, RichText, TextureOptions, UiBuilder, Widget};
|
use egui::{Pos2, Rect, RichText, TextureOptions, UiBuilder, Widget};
|
||||||
use image::{DynamicImage, EncodableLayout};
|
use grin_keychain::mnemonic::WORDS;
|
||||||
use grin_util::ZeroingString;
|
use grin_util::ZeroingString;
|
||||||
use grin_wallet_libwallet::SlatepackAddress;
|
use grin_wallet_libwallet::SlatepackAddress;
|
||||||
use grin_keychain::mnemonic::WORDS;
|
use image::{DynamicImage, EncodableLayout};
|
||||||
|
use parking_lot::RwLock;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::thread;
|
||||||
|
|
||||||
use crate::gui::Colors;
|
use crate::gui::Colors;
|
||||||
use crate::gui::icons::CAMERA_ROTATE;
|
use crate::gui::icons::CAMERA_ROTATE;
|
||||||
use crate::gui::platform::PlatformCallbacks;
|
use crate::gui::platform::PlatformCallbacks;
|
||||||
use crate::gui::views::types::{QrScanResult, QrScanState};
|
use crate::gui::views::types::{QrScanResult, QrScanState};
|
||||||
use crate::gui::views::View;
|
use crate::gui::views::{Modal, View};
|
||||||
use crate::wallet::types::PhraseSize;
|
|
||||||
use crate::wallet::WalletUtils;
|
use crate::wallet::WalletUtils;
|
||||||
|
use crate::wallet::types::PhraseSize;
|
||||||
|
|
||||||
/// Camera QR code scanner.
|
/// Camera QR code scanner.
|
||||||
pub struct CameraContent {
|
pub struct CameraContent {
|
||||||
/// QR code scanning progress and result.
|
/// QR code scanning progress and result.
|
||||||
qr_scan_state: Arc<RwLock<QrScanState>>,
|
qr_scan_state: Arc<RwLock<QrScanState>>,
|
||||||
/// Uniform Resources URIs collected from QR code scanning.
|
/// 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 {
|
impl Default for CameraContent {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
qr_scan_state: Arc::new(RwLock::new(QrScanState::default())),
|
qr_scan_state: Arc::new(RwLock::new(QrScanState::default())),
|
||||||
ur_data: Arc::new(RwLock::new(None))
|
ur_data: Arc::new(RwLock::new(None)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CameraContent {
|
impl CameraContent {
|
||||||
/// Draw camera content.
|
/// Draw camera content.
|
||||||
pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||||
ui.ctx().request_repaint();
|
let rect = if let Some(img_data) = cb.camera_image() {
|
||||||
let rect = if let Some(img_data) = cb.camera_image() {
|
if let Ok(img) = image::load_from_memory(&*img_data.0) {
|
||||||
if let Ok(img) =
|
// Process image to find QR code.
|
||||||
image::load_from_memory(&*img_data.0) {
|
self.scan_qr(&img);
|
||||||
// Process image to find QR code.
|
|
||||||
self.scan_qr(&img);
|
|
||||||
|
|
||||||
// Draw image.
|
// Draw image.
|
||||||
let img_rect = self.image_ui(ui, img, img_data.1);
|
let img_rect = self.image_ui(ui, img, img_data.1);
|
||||||
|
|
||||||
// Show UR scan progress.
|
img_rect
|
||||||
self.ur_progress_ui(ui);
|
} else {
|
||||||
img_rect
|
self.loading_ui(ui)
|
||||||
} else {
|
}
|
||||||
self.loading_ui(ui)
|
} else {
|
||||||
}
|
self.loading_ui(ui)
|
||||||
} else {
|
};
|
||||||
self.loading_ui(ui)
|
|
||||||
};
|
|
||||||
|
|
||||||
// Show button to switch cameras.
|
// Show UR scan progress.
|
||||||
if cb.can_switch_camera() {
|
self.ur_progress_ui(ui, &rect);
|
||||||
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.
|
// Show button to switch cameras.
|
||||||
fn image_ui(&mut self, ui: &mut egui::Ui, mut img: DynamicImage, rotation: u32) -> Rect {
|
if cb.can_switch_camera() {
|
||||||
// Setup image rotation.
|
let r = {
|
||||||
img = match rotation {
|
let mut r = rect.clone();
|
||||||
90 => img.rotate90(),
|
r.min.y = r.max.y - 52.0;
|
||||||
180 => img.rotate180(),
|
r.min.x = r.max.x - 52.0;
|
||||||
270 => img.rotate270(),
|
r
|
||||||
_ => img
|
};
|
||||||
};
|
ui.scope_builder(UiBuilder::new().max_rect(r), |ui| {
|
||||||
if View::is_desktop() {
|
let rotate_img = CAMERA_ROTATE.to_string();
|
||||||
img = img.fliph();
|
View::button(ui, rotate_img, Colors::white_or_black(false), || {
|
||||||
}
|
cb.switch_camera();
|
||||||
// Convert to ColorImage.
|
});
|
||||||
let color_img = match &img {
|
});
|
||||||
DynamicImage::ImageRgb8(image) => {
|
}
|
||||||
egui::ColorImage::from_rgb(
|
ui.add_space(6.0);
|
||||||
[image.width() as usize, image.height() as usize],
|
ui.ctx().request_repaint();
|
||||||
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.
|
/// Draw modal camera content.
|
||||||
fn ur_progress_ui(&self, ui: &mut egui::Ui) {
|
pub fn modal_ui(
|
||||||
let show_ur_progress = {
|
&mut self,
|
||||||
self.ur_data.as_ref().read().is_some()
|
ui: &mut egui::Ui,
|
||||||
};
|
cb: &dyn PlatformCallbacks,
|
||||||
if show_ur_progress {
|
mut on_result: impl FnMut(Option<QrScanResult>),
|
||||||
ui.centered_and_justified(|ui| {
|
) {
|
||||||
ui.label(RichText::new(format!("{}%", self.ur_progress()))
|
if let Some(result) = self.qr_scan_result() {
|
||||||
.size(17.0)
|
on_result(Some(result));
|
||||||
.color(Colors::green()));
|
} else {
|
||||||
});
|
ui.add_space(6.0);
|
||||||
}
|
self.ui(ui, cb);
|
||||||
}
|
ui.add_space(6.0);
|
||||||
|
|
||||||
/// Draw camera loading progress content.
|
// Setup spacing between buttons.
|
||||||
fn loading_ui(&self, ui: &mut egui::Ui) -> Rect {
|
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
|
||||||
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.
|
// Show buttons to close modal or come back to sending input.
|
||||||
fn image_processing(&self) -> bool {
|
ui.columns(2, |cols| {
|
||||||
let r_scan = self.qr_scan_state.read();
|
cols[0].vertical_centered_justified(|ui| {
|
||||||
r_scan.image_processing
|
View::button(ui, t!("close"), Colors::white_or_black(false), || {
|
||||||
}
|
cb.stop_camera();
|
||||||
|
on_result(None);
|
||||||
|
Modal::close();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
cols[1].vertical_centered_justified(|ui| {
|
||||||
|
View::button(ui, t!("back"), Colors::white_or_black(false), || {
|
||||||
|
on_result(None);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
ui.add_space(6.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Get UR scanning progress in percents.
|
/// Draw camera image.
|
||||||
fn ur_progress(&self) -> i32 {
|
fn image_ui(&mut self, ui: &mut egui::Ui, mut img: DynamicImage, rotation: u32) -> Rect {
|
||||||
// Setup data.
|
// Setup image rotation.
|
||||||
let r_data = self.ur_data.read();
|
img = match rotation {
|
||||||
let (data, total) = r_data.clone().unwrap_or((vec![], 0));
|
90 => img.rotate90(),
|
||||||
if data.is_empty() {
|
180 => img.rotate180(),
|
||||||
return 0;
|
270 => img.rotate270(),
|
||||||
}
|
_ => img,
|
||||||
// Calculate progress.
|
};
|
||||||
let mut complete = 0;
|
if View::is_desktop() {
|
||||||
for i in &data {
|
img = img.fliph();
|
||||||
if !i.is_empty() {
|
}
|
||||||
complete += 1;
|
// Convert to ColorImage.
|
||||||
}
|
let color_img = match &img {
|
||||||
}
|
DynamicImage::ImageRgb8(image) => egui::ColorImage::from_rgb(
|
||||||
(100 * complete / total) as i32
|
[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([
|
||||||
|
if img_size.y > img_size.x {
|
||||||
|
Pos2::new(0.0, 1.0 - (img_size.x / img_size.y))
|
||||||
|
} else {
|
||||||
|
Pos2::new(1.0 - (img_size.y / img_size.x), 0.0)
|
||||||
|
},
|
||||||
|
Pos2::new(1.0, 1.0),
|
||||||
|
]))
|
||||||
|
.max_height(ui.available_width())
|
||||||
|
.maintain_aspect_ratio(false)
|
||||||
|
.shrink_to_fit()
|
||||||
|
.ui(ui)
|
||||||
|
.rect
|
||||||
|
}
|
||||||
|
|
||||||
/// Parse QR code from provided image data.
|
/// Draw animated QR code scanning progress.
|
||||||
fn scan_qr(&self, image_data: &DynamicImage) {
|
fn ur_progress_ui(&self, ui: &mut egui::Ui, rect: &Rect) {
|
||||||
// Do not scan when another image is processing.
|
let show_ur_progress = { self.ur_data.as_ref().read().is_some() };
|
||||||
if self.image_processing() {
|
if show_ur_progress {
|
||||||
return;
|
ui.scope_builder(UiBuilder::new().max_rect(rect.clone()), |ui| {
|
||||||
}
|
ui.centered_and_justified(|ui| {
|
||||||
// Setup scanning flag.
|
ui.label(
|
||||||
{
|
RichText::new(format!("{}%", self.ur_progress()))
|
||||||
let mut w_scan = self.qr_scan_state.write();
|
.size(32.0)
|
||||||
w_scan.image_processing = true;
|
.color(Colors::gold_dark()),
|
||||||
}
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let image_data = image_data.clone();
|
/// Draw camera loading progress content.
|
||||||
let qr_scan_state = self.qr_scan_state.clone();
|
fn loading_ui(&self, ui: &mut egui::Ui) -> Rect {
|
||||||
let ur_data = self.ur_data.clone();
|
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
|
||||||
|
}
|
||||||
|
|
||||||
let on_scan = async move {
|
/// Check if image is processing to find QR code.
|
||||||
// Prepare image data.
|
fn image_processing(&self) -> bool {
|
||||||
let img = image_data.to_luma8();
|
let r_scan = self.qr_scan_state.read();
|
||||||
let mut img: rqrr::PreparedImage<image::GrayImage>
|
r_scan.image_processing
|
||||||
= rqrr::PreparedImage::prepare(img);
|
}
|
||||||
// Scan and save results.
|
|
||||||
let grids = img.detect_grids();
|
|
||||||
if let Some(g) = grids.get(0) {
|
|
||||||
let mut qr_data = vec![];
|
|
||||||
if let Ok(_) = g.decode_to(&mut qr_data) {
|
|
||||||
// Setup scanned data into text.
|
|
||||||
let text = String::from_utf8(qr_data.clone()).unwrap_or("".to_string());
|
|
||||||
// Setup current text.
|
|
||||||
let cur_text = {
|
|
||||||
let r_scan = qr_scan_state.read();
|
|
||||||
let text = if let Some(res) = r_scan.qr_scan_result.clone() {
|
|
||||||
res.text()
|
|
||||||
} else {
|
|
||||||
"".to_string()
|
|
||||||
};
|
|
||||||
text
|
|
||||||
};
|
|
||||||
// Parse non-empty data if parsed text is different from saved.
|
|
||||||
if !qr_data.is_empty() && (cur_text.is_empty() || text != cur_text) {
|
|
||||||
let res = Self::parse_qr_code(qr_data);
|
|
||||||
match res {
|
|
||||||
QrScanResult::URPart(uri, index, total) => {
|
|
||||||
// Setup current UR data.
|
|
||||||
let mut cur_data = {
|
|
||||||
let r_data = ur_data.read();
|
|
||||||
let mut cur_data = vec!["".to_string(); total];
|
|
||||||
if let Some((d, _)) = r_data.clone() {
|
|
||||||
cur_data = d;
|
|
||||||
}
|
|
||||||
cur_data
|
|
||||||
};
|
|
||||||
if !cur_data.contains(&uri) {
|
|
||||||
// Save part of UR data.
|
|
||||||
{
|
|
||||||
cur_data.insert(index, uri);
|
|
||||||
let mut w_data = ur_data.write();
|
|
||||||
*w_data = Some((cur_data.clone(), total));
|
|
||||||
}
|
|
||||||
// Setup UR decoder.
|
|
||||||
let mut decoder = ur::Decoder::default();
|
|
||||||
for m in cur_data {
|
|
||||||
if !m.is_empty() {
|
|
||||||
if let Ok(_) = decoder.receive(m.as_str()) {
|
|
||||||
continue;
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Check if UR data is complete.
|
|
||||||
if decoder.complete() {
|
|
||||||
if let Ok(data) = decoder.message() {
|
|
||||||
// Parse complete data.
|
|
||||||
let res = Self::parse_qr_code(data.unwrap_or(vec![]));
|
|
||||||
// Clean UR data.
|
|
||||||
let mut w_data = ur_data.write();
|
|
||||||
*w_data = None;
|
|
||||||
// Save scan result.
|
|
||||||
let mut w_scan = qr_scan_state.write();
|
|
||||||
w_scan.qr_scan_result = Some(res);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
// Clean UR data.
|
|
||||||
let mut w_data = ur_data.write();
|
|
||||||
*w_data = None;
|
|
||||||
// Save scan result.
|
|
||||||
let mut w_scan = qr_scan_state.write();
|
|
||||||
w_scan.qr_scan_result = Some(res);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Reset scanning flag to process again.
|
|
||||||
{
|
|
||||||
let mut w_scan = qr_scan_state.write();
|
|
||||||
w_scan.image_processing = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Launch scanner at separate thread.
|
/// Get UR scanning progress in percents.
|
||||||
thread::spawn(move || {
|
fn ur_progress(&self) -> i32 {
|
||||||
tokio::runtime::Builder::new_multi_thread()
|
// Setup data.
|
||||||
.enable_all()
|
let r_data = self.ur_data.read();
|
||||||
.build()
|
let (data, total) = r_data.clone().unwrap_or((vec![], 0));
|
||||||
.unwrap()
|
if data.is_empty() {
|
||||||
.block_on(on_scan);
|
return 0;
|
||||||
});
|
}
|
||||||
}
|
// Calculate progress.
|
||||||
|
let mut complete = 0;
|
||||||
|
for i in &data {
|
||||||
|
if !i.is_empty() {
|
||||||
|
complete += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(100 * complete / total) as i32
|
||||||
|
}
|
||||||
|
|
||||||
/// Parse QR code scan result.
|
/// Parse QR code from provided image data.
|
||||||
fn parse_qr_code(data: Vec<u8>) -> QrScanResult {
|
fn scan_qr(&self, image_data: &DynamicImage) {
|
||||||
// Check if string starts with Grin address prefix.
|
// Do not scan when another image is processing.
|
||||||
let text_string = String::from_utf8(data.clone()).unwrap_or("".to_string());
|
if self.image_processing() {
|
||||||
let text = text_string.trim();
|
return;
|
||||||
if text.starts_with("tgrin") || text.starts_with("grin") {
|
}
|
||||||
if SlatepackAddress::try_from(text).is_ok() {
|
// Setup scanning flag.
|
||||||
return QrScanResult::Address(ZeroingString::from(text));
|
{
|
||||||
}
|
let mut w_scan = self.qr_scan_state.write();
|
||||||
}
|
w_scan.image_processing = true;
|
||||||
|
}
|
||||||
|
|
||||||
// Check if string contains Slatepack message prefix and postfix.
|
let image_data = image_data.clone();
|
||||||
if text.starts_with("BEGINSLATEPACK.") && text.ends_with("ENDSLATEPACK.") {
|
let qr_scan_state = self.qr_scan_state.clone();
|
||||||
return QrScanResult::Slatepack(ZeroingString::from(text));
|
let ur_data = self.ur_data.clone();
|
||||||
}
|
|
||||||
|
|
||||||
// Check Uniform Resource data.
|
let on_scan = async move {
|
||||||
// https://github.com/BlockchainCommons/Research/blob/master/papers/bcr-2020-005-ur.md
|
// Prepare image data.
|
||||||
if text.starts_with("ur:bytes/") {
|
let img = image_data.to_luma8();
|
||||||
let split = text.split("/").collect::<Vec<_>>();
|
let mut img: rqrr::PreparedImage<image::GrayImage> = rqrr::PreparedImage::prepare(img);
|
||||||
if let Some(index_total) = split.get(1) {
|
// Scan and save results.
|
||||||
if let Some((index, total)) = index_total.split_once("-") {
|
let grids = img.detect_grids();
|
||||||
let index = index.parse::<usize>();
|
if let Some(g) = grids.get(0) {
|
||||||
let total = total.parse::<usize>();
|
let mut qr_data = vec![];
|
||||||
if index.is_ok() && total.is_ok() {
|
if let Ok(_) = g.decode_to(&mut qr_data) {
|
||||||
let index = index.unwrap() - 1;
|
// Setup scanned data into text.
|
||||||
let total = total.unwrap();
|
let text = String::from_utf8(qr_data.clone()).unwrap_or("".to_string());
|
||||||
return QrScanResult::URPart(text_string, index, total);
|
// Setup current text.
|
||||||
}
|
let cur_text = {
|
||||||
}
|
let r_scan = qr_scan_state.read();
|
||||||
}
|
let text = if let Some(res) = r_scan.qr_scan_result.clone() {
|
||||||
}
|
res.text()
|
||||||
|
} else {
|
||||||
|
"".to_string()
|
||||||
|
};
|
||||||
|
text
|
||||||
|
};
|
||||||
|
// Parse non-empty data if parsed text is different from saved.
|
||||||
|
if !qr_data.is_empty() && (cur_text.is_empty() || text != cur_text) {
|
||||||
|
let res = Self::parse_qr_code(qr_data);
|
||||||
|
match res {
|
||||||
|
QrScanResult::URPart(uri, index, total) => {
|
||||||
|
// Setup current UR data.
|
||||||
|
let mut cur_data = {
|
||||||
|
let r_data = ur_data.read();
|
||||||
|
let mut cur_data = vec!["".to_string(); total];
|
||||||
|
if let Some((d, _)) = r_data.clone() {
|
||||||
|
cur_data = d;
|
||||||
|
}
|
||||||
|
cur_data
|
||||||
|
};
|
||||||
|
if !cur_data.contains(&uri) {
|
||||||
|
// Save part of UR data.
|
||||||
|
{
|
||||||
|
cur_data.insert(index, uri);
|
||||||
|
let mut w_data = ur_data.write();
|
||||||
|
*w_data = Some((cur_data.clone(), total));
|
||||||
|
}
|
||||||
|
// Setup UR decoder.
|
||||||
|
let mut decoder = ur::Decoder::default();
|
||||||
|
for m in cur_data {
|
||||||
|
if !m.is_empty() {
|
||||||
|
if let Ok(_) = decoder.receive(m.as_str()) {
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Check if UR data is complete.
|
||||||
|
if decoder.complete() {
|
||||||
|
if let Ok(data) = decoder.message() {
|
||||||
|
// Parse complete data.
|
||||||
|
let res = Self::parse_qr_code(data.unwrap_or(vec![]));
|
||||||
|
// Clean UR data.
|
||||||
|
let mut w_data = ur_data.write();
|
||||||
|
*w_data = None;
|
||||||
|
// Save scan result.
|
||||||
|
let mut w_scan = qr_scan_state.write();
|
||||||
|
w_scan.qr_scan_result = Some(res);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
// Clean UR data.
|
||||||
|
let mut w_data = ur_data.write();
|
||||||
|
*w_data = None;
|
||||||
|
// Save scan result.
|
||||||
|
let mut w_scan = qr_scan_state.write();
|
||||||
|
w_scan.qr_scan_result = Some(res);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Reset scanning flag to process again.
|
||||||
|
{
|
||||||
|
let mut w_scan = qr_scan_state.write();
|
||||||
|
w_scan.image_processing = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Check Compact SeedQR format.
|
// Launch scanner at separate thread.
|
||||||
// https://github.com/SeedSigner/seedsigner/blob/dev/docs/seed_qr/README.md#compactseedqr-specification
|
thread::spawn(move || {
|
||||||
if data.len() <= 32 && 16 <= data.len() && data.len() % 4 == 0 {
|
tokio::runtime::Builder::new_current_thread()
|
||||||
// Setup words amount.
|
.enable_all()
|
||||||
let total_bits = data.len() * 8;
|
.build()
|
||||||
let checksum_bits = total_bits / 32;
|
.unwrap()
|
||||||
let total_words = (total_bits + checksum_bits) / 11;
|
.block_on(on_scan);
|
||||||
// Setup entropy.
|
});
|
||||||
let mut entropy = data.clone();
|
}
|
||||||
WalletUtils::setup_checksum(&mut entropy);
|
|
||||||
// Setup bits.
|
|
||||||
let mut bits = vec![false; entropy.len() * 8];
|
|
||||||
for i in 0..entropy.len() {
|
|
||||||
for j in 0..8 {
|
|
||||||
bits[(i * 8) + j] = (entropy[i] & (1 << (7 - j))) != 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Extract word index.
|
|
||||||
let extract_index = |i: usize| -> usize {
|
|
||||||
let mut index = 0;
|
|
||||||
for j in 0..11 {
|
|
||||||
index = index << 1;
|
|
||||||
if bits[(i * 11) + j] {
|
|
||||||
index += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return index;
|
|
||||||
};
|
|
||||||
// Setup words.
|
|
||||||
let mut words = "".to_string();
|
|
||||||
for n in 0..total_words {
|
|
||||||
// Setup word index.
|
|
||||||
let index = extract_index(n);
|
|
||||||
// Setup word.
|
|
||||||
let empty_word = "".to_string();
|
|
||||||
let word = WORDS.get(index).clone().unwrap_or(&empty_word).clone();
|
|
||||||
if word.is_empty() {
|
|
||||||
words = empty_word;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
words = if words.is_empty() {
|
|
||||||
format!("{}", word)
|
|
||||||
} else {
|
|
||||||
format!("{} {}", words, word)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if !words.is_empty() {
|
|
||||||
return QrScanResult::SeedQR(ZeroingString::from(words));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check Standard SeedQR format.
|
/// Parse QR code scan result.
|
||||||
// https://github.com/SeedSigner/seedsigner/blob/dev/docs/seed_qr/README.md#standard-seedqr-specification
|
fn parse_qr_code(data: Vec<u8>) -> QrScanResult {
|
||||||
let only_numbers = || {
|
// Check if string starts with Grin address prefix.
|
||||||
for c in text.chars() {
|
let text_string = String::from_utf8(data.clone()).unwrap_or("".to_string());
|
||||||
if !c.is_numeric() {
|
let text = text_string.trim();
|
||||||
return false;
|
if text.starts_with("tgrin") || text.starts_with("grin") {
|
||||||
}
|
if SlatepackAddress::try_from(text).is_ok() {
|
||||||
}
|
return QrScanResult::Address(ZeroingString::from(text));
|
||||||
true
|
}
|
||||||
};
|
}
|
||||||
if !text.is_empty() && data.len() <= 96 && data.len() % 4 == 0 && only_numbers() {
|
|
||||||
if let Some(_) = PhraseSize::type_for_value(text.len() / 4) {
|
|
||||||
let chars: Vec<char> = text.trim().chars().collect();
|
|
||||||
let split = &chars.chunks(4)
|
|
||||||
.map(|chunk| chunk.iter().collect::<String>()
|
|
||||||
.trim()
|
|
||||||
.trim_start_matches("0")
|
|
||||||
.to_string()
|
|
||||||
)
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
let mut words = "".to_string();
|
|
||||||
for i in split {
|
|
||||||
let index = if i.is_empty() {
|
|
||||||
0usize
|
|
||||||
} else {
|
|
||||||
i.parse::<usize>().unwrap_or(WORDS.len())
|
|
||||||
};
|
|
||||||
let empty_word = "".to_string();
|
|
||||||
let word = WORDS.get(index).clone().unwrap_or(&empty_word).clone();
|
|
||||||
// Return text result when BIP39 word was not found.
|
|
||||||
if word.is_empty() {
|
|
||||||
return QrScanResult::Text(ZeroingString::from(text));
|
|
||||||
}
|
|
||||||
words = if words.is_empty() {
|
|
||||||
format!("{}", word)
|
|
||||||
} else {
|
|
||||||
format!("{} {}", words, word)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return QrScanResult::SeedQR(ZeroingString::from(words));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return default text result.
|
// Check if string contains Slatepack message prefix and postfix.
|
||||||
QrScanResult::Text(ZeroingString::from(text))
|
if text.starts_with("BEGINSLATEPACK.") && text.ends_with("ENDSLATEPACK.") {
|
||||||
}
|
return QrScanResult::Slatepack(text.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
/// Get QR code scan result.
|
// Check Uniform Resource data.
|
||||||
pub fn qr_scan_result(&self) -> Option<QrScanResult> {
|
// https://github.com/BlockchainCommons/Research/blob/master/papers/bcr-2020-005-ur.md
|
||||||
let r_scan = self.qr_scan_state.read();
|
if text.starts_with("ur:bytes/") {
|
||||||
if r_scan.qr_scan_result.is_some() {
|
let split = text.split("/").collect::<Vec<_>>();
|
||||||
return Some(r_scan.qr_scan_result.clone().unwrap());
|
if let Some(index_total) = split.get(1) {
|
||||||
}
|
if let Some((index, total)) = index_total.split_once("-") {
|
||||||
None
|
let index = index.parse::<usize>();
|
||||||
}
|
let total = total.parse::<usize>();
|
||||||
}
|
if index.is_ok() && total.is_ok() {
|
||||||
|
let index = index.unwrap() - 1;
|
||||||
|
let total = total.unwrap();
|
||||||
|
return QrScanResult::URPart(text_string, index, total);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check Compact SeedQR format.
|
||||||
|
// https://github.com/SeedSigner/seedsigner/blob/dev/docs/seed_qr/README.md#compactseedqr-specification
|
||||||
|
if data.len() <= 32 && 16 <= data.len() && data.len() % 4 == 0 {
|
||||||
|
// Setup words amount.
|
||||||
|
let total_bits = data.len() * 8;
|
||||||
|
let checksum_bits = total_bits / 32;
|
||||||
|
let total_words = (total_bits + checksum_bits) / 11;
|
||||||
|
// Setup entropy.
|
||||||
|
let mut entropy = data.clone();
|
||||||
|
WalletUtils::setup_checksum(&mut entropy);
|
||||||
|
// Setup bits.
|
||||||
|
let mut bits = vec![false; entropy.len() * 8];
|
||||||
|
for i in 0..entropy.len() {
|
||||||
|
for j in 0..8 {
|
||||||
|
bits[(i * 8) + j] = (entropy[i] & (1 << (7 - j))) != 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Extract word index.
|
||||||
|
let extract_index = |i: usize| -> usize {
|
||||||
|
let mut index = 0;
|
||||||
|
for j in 0..11 {
|
||||||
|
index = index << 1;
|
||||||
|
if bits[(i * 11) + j] {
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return index;
|
||||||
|
};
|
||||||
|
// Setup words.
|
||||||
|
let mut words = "".to_string();
|
||||||
|
for n in 0..total_words {
|
||||||
|
// Setup word index.
|
||||||
|
let index = extract_index(n);
|
||||||
|
// Setup word.
|
||||||
|
let empty_word = "".to_string();
|
||||||
|
let word = WORDS.get(index).clone().unwrap_or(&empty_word).clone();
|
||||||
|
if word.is_empty() {
|
||||||
|
words = empty_word;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
words = if words.is_empty() {
|
||||||
|
format!("{}", word)
|
||||||
|
} else {
|
||||||
|
format!("{} {}", words, word)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if !words.is_empty() {
|
||||||
|
return QrScanResult::SeedQR(ZeroingString::from(words));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check Standard SeedQR format.
|
||||||
|
// https://github.com/SeedSigner/seedsigner/blob/dev/docs/seed_qr/README.md#standard-seedqr-specification
|
||||||
|
let only_numbers = || {
|
||||||
|
for c in text.chars() {
|
||||||
|
if !c.is_numeric() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
true
|
||||||
|
};
|
||||||
|
if !text.is_empty() && data.len() <= 96 && data.len() % 4 == 0 && only_numbers() {
|
||||||
|
if let Some(_) = PhraseSize::type_for_value(text.len() / 4) {
|
||||||
|
let chars: Vec<char> = text.trim().chars().collect();
|
||||||
|
let split = &chars
|
||||||
|
.chunks(4)
|
||||||
|
.map(|chunk| {
|
||||||
|
chunk
|
||||||
|
.iter()
|
||||||
|
.collect::<String>()
|
||||||
|
.trim()
|
||||||
|
.trim_start_matches("0")
|
||||||
|
.to_string()
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let mut words = "".to_string();
|
||||||
|
for i in split {
|
||||||
|
let index = if i.is_empty() {
|
||||||
|
0usize
|
||||||
|
} else {
|
||||||
|
i.parse::<usize>().unwrap_or(WORDS.len())
|
||||||
|
};
|
||||||
|
let empty_word = "".to_string();
|
||||||
|
let word = WORDS.get(index).clone().unwrap_or(&empty_word).clone();
|
||||||
|
// Return text result when BIP39 word was not found.
|
||||||
|
if word.is_empty() {
|
||||||
|
return QrScanResult::Text(ZeroingString::from(text));
|
||||||
|
}
|
||||||
|
words = if words.is_empty() {
|
||||||
|
format!("{}", word)
|
||||||
|
} else {
|
||||||
|
format!("{} {}", words, word)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return QrScanResult::SeedQR(ZeroingString::from(words));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return default text result.
|
||||||
|
QrScanResult::Text(ZeroingString::from(text))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get QR code scan result.
|
||||||
|
pub fn qr_scan_result(&self) -> Option<QrScanResult> {
|
||||||
|
let r_scan = self.qr_scan_state.read();
|
||||||
|
if r_scan.qr_scan_result.is_some() {
|
||||||
|
return Some(r_scan.qr_scan_result.clone().unwrap());
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+293
-373
@@ -12,44 +12,57 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
|
use egui::RichText;
|
||||||
|
use egui::os::OperatingSystem;
|
||||||
|
use lazy_static::lazy_static;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
use egui::os::OperatingSystem;
|
|
||||||
use egui::{Align, Layout, RichText};
|
|
||||||
use lazy_static::lazy_static;
|
|
||||||
|
|
||||||
use crate::gui::Colors;
|
use crate::gui::Colors;
|
||||||
|
use crate::gui::icons::FILE_X;
|
||||||
use crate::gui::platform::PlatformCallbacks;
|
use crate::gui::platform::PlatformCallbacks;
|
||||||
|
use crate::gui::views::network::NetworkContent;
|
||||||
|
use crate::gui::views::types::{ContentContainer, ModalPosition};
|
||||||
|
use crate::gui::views::wallets::WalletsContent;
|
||||||
use crate::gui::views::{Modal, View};
|
use crate::gui::views::{Modal, View};
|
||||||
use crate::gui::views::types::{ModalContainer, ModalPosition};
|
|
||||||
use crate::node::Node;
|
use crate::node::Node;
|
||||||
use crate::{AppConfig, Settings};
|
use crate::{AppConfig, Settings};
|
||||||
use crate::gui::icons::{CHECK, CHECK_FAT, FILE_X};
|
|
||||||
use crate::gui::views::network::NetworkContent;
|
|
||||||
use crate::gui::views::wallets::WalletsContent;
|
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
/// Global state to check if [`NetworkContent`] panel is open.
|
/// Global state to check if [`NetworkContent`] panel is open.
|
||||||
static ref NETWORK_PANEL_OPEN: AtomicBool = AtomicBool::new(false);
|
static ref NETWORK_PANEL_OPEN: AtomicBool = AtomicBool::new(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Contains main ui content, handles side panel state.
|
/// Contains main ui content, handles side panel state.
|
||||||
pub struct Content {
|
pub struct Content {
|
||||||
/// Side panel [`NetworkContent`] content.
|
/// Side panel [`NetworkContent`] content.
|
||||||
network: NetworkContent,
|
network: NetworkContent,
|
||||||
/// Central panel [`WalletsContent`] content.
|
|
||||||
pub wallets: WalletsContent,
|
|
||||||
|
|
||||||
/// Check if app exit is allowed on Desktop close event.
|
/// Central panel [`WalletsContent`] content.
|
||||||
pub exit_allowed: bool,
|
wallets: WalletsContent,
|
||||||
/// Flag to show exit progress at [`Modal`].
|
|
||||||
show_exit_progress: bool,
|
|
||||||
|
|
||||||
/// Flag to check it's first draw of content.
|
/// Check if app exit is allowed on Desktop close event.
|
||||||
first_draw: bool,
|
pub exit_allowed: bool,
|
||||||
|
/// Flag to show exit progress at [`Modal`].
|
||||||
|
show_exit_progress: bool,
|
||||||
|
|
||||||
/// List of allowed [`Modal`] ids for this [`ModalContainer`].
|
/// Flag to check it's first draw of content.
|
||||||
allowed_modal_ids: Vec<&'static str>
|
first_draw: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Content {
|
||||||
|
fn default() -> Self {
|
||||||
|
// Exit from eframe only for non-mobile platforms.
|
||||||
|
let os = OperatingSystem::from_target_os();
|
||||||
|
let exit_allowed = os == OperatingSystem::Android || os == OperatingSystem::IOS;
|
||||||
|
Self {
|
||||||
|
network: NetworkContent::default(),
|
||||||
|
wallets: WalletsContent::default(),
|
||||||
|
exit_allowed,
|
||||||
|
show_exit_progress: false,
|
||||||
|
first_draw: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Identifier for integrated node warning [`Modal`] on Android.
|
/// Identifier for integrated node warning [`Modal`] on Android.
|
||||||
@@ -57,375 +70,282 @@ const ANDROID_INTEGRATED_NODE_WARNING_MODAL: &'static str = "android_node_warnin
|
|||||||
/// Identifier for crash report [`Modal`].
|
/// Identifier for crash report [`Modal`].
|
||||||
const CRASH_REPORT_MODAL: &'static str = "crash_report_modal";
|
const CRASH_REPORT_MODAL: &'static str = "crash_report_modal";
|
||||||
|
|
||||||
impl Default for Content {
|
impl ContentContainer for Content {
|
||||||
fn default() -> Self {
|
fn modal_ids(&self) -> Vec<&'static str> {
|
||||||
// Exit from eframe only for non-mobile platforms.
|
vec![
|
||||||
let os = OperatingSystem::from_target_os();
|
Self::EXIT_CONFIRMATION_MODAL,
|
||||||
let exit_allowed = os == OperatingSystem::Android || os == OperatingSystem::IOS;
|
ANDROID_INTEGRATED_NODE_WARNING_MODAL,
|
||||||
Self {
|
CRASH_REPORT_MODAL,
|
||||||
network: NetworkContent::default(),
|
]
|
||||||
wallets: WalletsContent::default(),
|
}
|
||||||
exit_allowed,
|
|
||||||
show_exit_progress: false,
|
|
||||||
first_draw: true,
|
|
||||||
allowed_modal_ids: vec![
|
|
||||||
Self::EXIT_CONFIRMATION_MODAL,
|
|
||||||
Self::SETTINGS_MODAL,
|
|
||||||
ANDROID_INTEGRATED_NODE_WARNING_MODAL,
|
|
||||||
CRASH_REPORT_MODAL
|
|
||||||
],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ModalContainer for Content {
|
fn modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
|
||||||
fn modal_ids(&self) -> &Vec<&'static str> {
|
match modal.id {
|
||||||
&self.allowed_modal_ids
|
Self::EXIT_CONFIRMATION_MODAL => self.exit_modal_content(ui, modal, cb),
|
||||||
}
|
ANDROID_INTEGRATED_NODE_WARNING_MODAL => self.android_warning_modal_ui(ui),
|
||||||
|
CRASH_REPORT_MODAL => self.crash_report_modal_ui(ui, cb),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn modal_ui(&mut self,
|
fn container_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||||
ui: &mut egui::Ui,
|
let dual_panel = Self::is_dual_panel_mode(ui.ctx());
|
||||||
modal: &Modal,
|
let (is_panel_open, mut panel_width) = network_panel_state_width(ui.ctx(), dual_panel);
|
||||||
cb: &dyn PlatformCallbacks) {
|
if self.network.showing_settings() {
|
||||||
match modal.id {
|
panel_width = ui.available_width();
|
||||||
Self::EXIT_CONFIRMATION_MODAL => self.exit_modal_content(ui, modal, cb),
|
}
|
||||||
Self::SETTINGS_MODAL => self.settings_modal_ui(ui, modal),
|
|
||||||
ANDROID_INTEGRATED_NODE_WARNING_MODAL => self.android_warning_modal_ui(ui, modal),
|
// Show network content.
|
||||||
CRASH_REPORT_MODAL => self.crash_report_modal_ui(ui, modal, cb),
|
egui::SidePanel::left("network_panel")
|
||||||
_ => {}
|
.resizable(false)
|
||||||
}
|
.exact_width(panel_width)
|
||||||
}
|
.frame(egui::Frame {
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.show_animated_inside(ui, is_panel_open, |ui| {
|
||||||
|
self.network.ui(ui, cb);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Show wallets content.
|
||||||
|
egui::CentralPanel::default()
|
||||||
|
.frame(egui::Frame {
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.show_inside(ui, |ui| {
|
||||||
|
self.wallets.ui(ui, cb);
|
||||||
|
});
|
||||||
|
|
||||||
|
if self.first_draw {
|
||||||
|
// Show crash report or integrated node Android warning.
|
||||||
|
if Settings::crash_check_path().exists() {
|
||||||
|
Modal::new(CRASH_REPORT_MODAL)
|
||||||
|
.closeable(false)
|
||||||
|
.position(ModalPosition::Center)
|
||||||
|
.title(t!("crash_report"))
|
||||||
|
.show();
|
||||||
|
} else if OperatingSystem::from_target_os() == OperatingSystem::Android
|
||||||
|
&& AppConfig::android_integrated_node_warning_needed()
|
||||||
|
{
|
||||||
|
Modal::new(ANDROID_INTEGRATED_NODE_WARNING_MODAL)
|
||||||
|
.title(t!("network.node"))
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
self.first_draw = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Content {
|
impl Content {
|
||||||
/// Identifier for exit confirmation [`Modal`].
|
/// Default width of side panel at application UI.
|
||||||
pub const EXIT_CONFIRMATION_MODAL: &'static str = "exit_confirmation_modal";
|
pub const SIDE_PANEL_WIDTH: f32 = 400.0;
|
||||||
/// Identifier for wallet opening [`Modal`].
|
/// Desktop window title height.
|
||||||
pub const SETTINGS_MODAL: &'static str = "settings_modal";
|
pub const WINDOW_TITLE_HEIGHT: f32 = 38.0;
|
||||||
|
/// Margin of window frame at desktop.
|
||||||
|
pub const WINDOW_FRAME_MARGIN: f32 = 6.0;
|
||||||
|
|
||||||
/// Default width of side panel at application UI.
|
/// Identifier for exit confirmation [`Modal`].
|
||||||
pub const SIDE_PANEL_WIDTH: f32 = 400.0;
|
pub const EXIT_CONFIRMATION_MODAL: &'static str = "exit_confirmation_modal";
|
||||||
/// Desktop window title height.
|
|
||||||
pub const WINDOW_TITLE_HEIGHT: f32 = 38.0;
|
|
||||||
/// Margin of window frame at desktop.
|
|
||||||
pub const WINDOW_FRAME_MARGIN: f32 = 6.0;
|
|
||||||
|
|
||||||
pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
/// Called to navigate back, return `true` if action was not consumed.
|
||||||
self.current_modal_ui(ui, cb);
|
pub fn on_back(&mut self, ctx: &egui::Context, cb: &dyn PlatformCallbacks) -> bool {
|
||||||
|
if Modal::on_back() {
|
||||||
|
let dual_panel = Self::is_dual_panel_mode(ctx);
|
||||||
|
if !dual_panel && Self::is_network_panel_open() {
|
||||||
|
if self.network.on_back() {
|
||||||
|
Self::toggle_network_panel();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else if self.wallets.on_back(cb) {
|
||||||
|
Self::show_exit_modal();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
let dual_panel = Self::is_dual_panel_mode(ui.ctx());
|
/// Check if ui can show [`NetworkContent`] and [`WalletsContent`] at same time.
|
||||||
let (is_panel_open, panel_width) = network_panel_state_width(ui.ctx(), dual_panel);
|
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
|
||||||
|
// greater than minimal width of the side panel plus display insets from both sides.
|
||||||
|
let side_insets = View::get_left_inset() + View::get_right_inset();
|
||||||
|
is_wide_screen && w >= (Self::SIDE_PANEL_WIDTH * 2.0) + side_insets
|
||||||
|
}
|
||||||
|
|
||||||
// Show network content.
|
/// Toggle [`NetworkContent`] panel state.
|
||||||
egui::SidePanel::left("network_panel")
|
pub fn toggle_network_panel() {
|
||||||
.resizable(false)
|
let is_open = NETWORK_PANEL_OPEN.load(Ordering::Relaxed);
|
||||||
.exact_width(panel_width)
|
NETWORK_PANEL_OPEN.store(!is_open, Ordering::Relaxed);
|
||||||
.frame(egui::Frame {
|
}
|
||||||
..Default::default()
|
|
||||||
})
|
|
||||||
.show_animated_inside(ui, is_panel_open, |ui| {
|
|
||||||
self.network.ui(ui, cb);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Show wallets content.
|
/// Check if [`NetworkContent`] panel is open.
|
||||||
egui::CentralPanel::default()
|
pub fn is_network_panel_open() -> bool {
|
||||||
.frame(egui::Frame {
|
NETWORK_PANEL_OPEN.load(Ordering::Relaxed)
|
||||||
..Default::default()
|
}
|
||||||
})
|
|
||||||
.show_inside(ui, |ui| {
|
|
||||||
self.wallets.ui(ui, cb);
|
|
||||||
});
|
|
||||||
|
|
||||||
if self.first_draw {
|
/// Show exit confirmation [`Modal`].
|
||||||
// Show crash report or integrated node Android warning.
|
pub fn show_exit_modal() {
|
||||||
if Settings::crash_report_path().exists() {
|
Modal::new(Self::EXIT_CONFIRMATION_MODAL)
|
||||||
Modal::new(CRASH_REPORT_MODAL)
|
.title(t!("confirmation"))
|
||||||
.closeable(false)
|
.show();
|
||||||
.position(ModalPosition::Center)
|
}
|
||||||
.title(t!("crash_report"))
|
|
||||||
.show();
|
|
||||||
} else if OperatingSystem::from_target_os() == OperatingSystem::Android &&
|
|
||||||
AppConfig::android_integrated_node_warning_needed() {
|
|
||||||
Modal::new(ANDROID_INTEGRATED_NODE_WARNING_MODAL)
|
|
||||||
.title(t!("network.node"))
|
|
||||||
.show();
|
|
||||||
}
|
|
||||||
self.first_draw = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if ui can show [`NetworkContent`] and [`WalletsContent`] at same time.
|
/// Draw exit confirmation modal content.
|
||||||
pub fn is_dual_panel_mode(ctx: &egui::Context) -> bool {
|
fn exit_modal_content(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
|
||||||
let (w, h) = View::window_size(ctx);
|
if self.show_exit_progress {
|
||||||
// Screen is wide if width is greater than height or just 20% smaller.
|
if !Node::is_running() && !Node::data_dir_changing() {
|
||||||
let is_wide_screen = w > h || w + (w * 0.2) >= h;
|
self.exit_allowed = true;
|
||||||
// Dual panel mode is available when window is wide and its width is at least 2 times
|
cb.exit();
|
||||||
// greater than minimal width of the side panel plus display insets from both sides.
|
Modal::close();
|
||||||
let side_insets = View::get_left_inset() + View::get_right_inset();
|
}
|
||||||
is_wide_screen && w >= (Self::SIDE_PANEL_WIDTH * 2.0) + side_insets
|
ui.add_space(16.0);
|
||||||
}
|
ui.vertical_centered(|ui| {
|
||||||
|
View::small_loading_spinner(ui);
|
||||||
|
ui.add_space(12.0);
|
||||||
|
let exit_status_text = if Node::data_dir_changing() {
|
||||||
|
t!("moving_files")
|
||||||
|
} else {
|
||||||
|
t!("sync_status.shutdown")
|
||||||
|
};
|
||||||
|
ui.label(
|
||||||
|
RichText::new(exit_status_text)
|
||||||
|
.size(17.0)
|
||||||
|
.color(Colors::text(false)),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
ui.add_space(10.0);
|
||||||
|
} else {
|
||||||
|
ui.add_space(8.0);
|
||||||
|
ui.vertical_centered(|ui| {
|
||||||
|
ui.label(
|
||||||
|
RichText::new(t!("modal_exit.description"))
|
||||||
|
.size(17.0)
|
||||||
|
.color(Colors::text(false)),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
ui.add_space(10.0);
|
||||||
|
|
||||||
/// Toggle [`NetworkContent`] panel state.
|
// Setup spacing between buttons.
|
||||||
pub fn toggle_network_panel() {
|
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
|
||||||
let is_open = NETWORK_PANEL_OPEN.load(Ordering::Relaxed);
|
|
||||||
NETWORK_PANEL_OPEN.store(!is_open, Ordering::Relaxed);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if [`NetworkContent`] panel is open.
|
ui.columns(2, |columns| {
|
||||||
pub fn is_network_panel_open() -> bool {
|
columns[0].vertical_centered_justified(|ui| {
|
||||||
NETWORK_PANEL_OPEN.load(Ordering::Relaxed)
|
View::button(
|
||||||
}
|
ui,
|
||||||
|
t!("modal.cancel"),
|
||||||
|
Colors::white_or_black(false),
|
||||||
|
|| {
|
||||||
|
Modal::close();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
columns[1].vertical_centered_justified(|ui| {
|
||||||
|
View::button_ui(
|
||||||
|
ui,
|
||||||
|
t!("modal_exit.exit"),
|
||||||
|
Colors::white_or_black(false),
|
||||||
|
|_| {
|
||||||
|
if !Node::is_running() && !Node::data_dir_changing() {
|
||||||
|
self.exit_allowed = true;
|
||||||
|
cb.exit();
|
||||||
|
Modal::close();
|
||||||
|
} else {
|
||||||
|
Node::stop(true);
|
||||||
|
modal.disable_closing();
|
||||||
|
Modal::set_title(t!("modal_exit.exit"));
|
||||||
|
self.show_exit_progress = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
ui.add_space(6.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Show exit confirmation [`Modal`].
|
/// Draw content for integrated node warning [`Modal`] on Android.
|
||||||
pub fn show_exit_modal() {
|
fn android_warning_modal_ui(&mut self, ui: &mut egui::Ui) {
|
||||||
Modal::new(Self::EXIT_CONFIRMATION_MODAL)
|
ui.add_space(6.0);
|
||||||
.title(t!("confirmation"))
|
ui.vertical_centered(|ui| {
|
||||||
.show();
|
ui.label(
|
||||||
}
|
RichText::new(t!("network.android_warning"))
|
||||||
|
.size(16.0)
|
||||||
|
.color(Colors::text(false)),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
ui.add_space(8.0);
|
||||||
|
ui.vertical_centered_justified(|ui| {
|
||||||
|
View::button(ui, t!("continue"), Colors::white_or_black(false), || {
|
||||||
|
AppConfig::show_android_integrated_node_warning();
|
||||||
|
Modal::close();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
ui.add_space(6.0);
|
||||||
|
}
|
||||||
|
|
||||||
/// Draw exit confirmation modal content.
|
/// Draw content for integrated node warning [`Modal`] on Android.
|
||||||
fn exit_modal_content(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
|
fn crash_report_modal_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||||
if self.show_exit_progress {
|
ui.add_space(6.0);
|
||||||
if !Node::is_running() {
|
ui.vertical_centered(|ui| {
|
||||||
self.exit_allowed = true;
|
ui.label(
|
||||||
cb.exit();
|
RichText::new(t!("crash_report_warning"))
|
||||||
modal.close();
|
.size(16.0)
|
||||||
}
|
.color(Colors::text(false)),
|
||||||
ui.add_space(16.0);
|
);
|
||||||
ui.vertical_centered(|ui| {
|
ui.add_space(6.0);
|
||||||
View::small_loading_spinner(ui);
|
// Draw button to share log file.
|
||||||
ui.add_space(12.0);
|
let text = format!("{} {}", FILE_X, t!("share"));
|
||||||
ui.label(RichText::new(t!("sync_status.shutdown"))
|
View::colored_text_button(
|
||||||
.size(17.0)
|
ui,
|
||||||
.color(Colors::text(false)));
|
text,
|
||||||
});
|
Colors::blue(),
|
||||||
ui.add_space(10.0);
|
Colors::white_or_black(false),
|
||||||
} else {
|
|| {
|
||||||
ui.add_space(8.0);
|
if let Ok(data) = fs::read_to_string(Settings::log_path()) {
|
||||||
ui.vertical_centered(|ui| {
|
let name = Settings::LOG_FILE_NAME.to_string();
|
||||||
ui.label(RichText::new(t!("modal_exit.description"))
|
let _ = cb.share_data(name, data.as_bytes().to_vec());
|
||||||
.size(17.0)
|
}
|
||||||
.color(Colors::text(false)));
|
Settings::delete_crash_check();
|
||||||
});
|
Modal::close();
|
||||||
ui.add_space(10.0);
|
},
|
||||||
|
);
|
||||||
// Setup spacing between buttons.
|
});
|
||||||
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
|
ui.add_space(8.0);
|
||||||
|
View::horizontal_line(ui, Colors::item_stroke());
|
||||||
ui.columns(2, |columns| {
|
ui.add_space(8.0);
|
||||||
columns[0].vertical_centered_justified(|ui| {
|
ui.vertical_centered_justified(|ui| {
|
||||||
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
|
View::button(
|
||||||
modal.close();
|
ui,
|
||||||
});
|
t!("modal.cancel"),
|
||||||
});
|
Colors::white_or_black(false),
|
||||||
columns[1].vertical_centered_justified(|ui| {
|
|| {
|
||||||
View::button_ui(ui, t!("modal_exit.exit"), Colors::white_or_black(false), |_| {
|
Settings::delete_crash_check();
|
||||||
if !Node::is_running() {
|
Modal::close();
|
||||||
self.exit_allowed = true;
|
},
|
||||||
cb.exit();
|
);
|
||||||
modal.close();
|
});
|
||||||
} else {
|
ui.add_space(6.0);
|
||||||
Node::stop(true);
|
}
|
||||||
modal.disable_closing();
|
|
||||||
Modal::set_title(t!("modal_exit.exit"));
|
|
||||||
self.show_exit_progress = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
ui.add_space(6.0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Handle Back key event.
|
|
||||||
pub fn on_back(&mut self, cb: &dyn PlatformCallbacks) {
|
|
||||||
if Modal::on_back() {
|
|
||||||
if self.wallets.on_back(cb) {
|
|
||||||
Self::show_exit_modal()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Draw creating wallet name/password input [`Modal`] content.
|
|
||||||
pub fn settings_modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal) {
|
|
||||||
ui.add_space(6.0);
|
|
||||||
|
|
||||||
// Show theme selection.
|
|
||||||
Self::theme_selection_ui(ui);
|
|
||||||
|
|
||||||
ui.add_space(8.0);
|
|
||||||
View::horizontal_line(ui, Colors::item_stroke());
|
|
||||||
ui.add_space(6.0);
|
|
||||||
|
|
||||||
ui.vertical_centered(|ui| {
|
|
||||||
ui.label(RichText::new(format!("{}:", t!("language")))
|
|
||||||
.size(16.0)
|
|
||||||
.color(Colors::gray())
|
|
||||||
);
|
|
||||||
});
|
|
||||||
ui.add_space(8.0);
|
|
||||||
|
|
||||||
// Draw available list of languages to select.
|
|
||||||
let locales = rust_i18n::available_locales!();
|
|
||||||
for (index, locale) in locales.iter().enumerate() {
|
|
||||||
Self::language_item_ui(locale, ui, index, locales.len(), modal);
|
|
||||||
}
|
|
||||||
|
|
||||||
ui.add_space(8.0);
|
|
||||||
|
|
||||||
// Show button to close modal.
|
|
||||||
ui.vertical_centered_justified(|ui| {
|
|
||||||
View::button(ui, t!("close"), Colors::white_or_black(false), || {
|
|
||||||
modal.close();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
ui.add_space(6.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Draw theme selection content.
|
|
||||||
fn theme_selection_ui(ui: &mut egui::Ui) {
|
|
||||||
ui.vertical_centered(|ui| {
|
|
||||||
ui.label(RichText::new(t!("theme")).size(16.0).color(Colors::gray()));
|
|
||||||
});
|
|
||||||
|
|
||||||
let saved_use_dark = AppConfig::dark_theme().unwrap_or(false);
|
|
||||||
let mut selected_use_dark = saved_use_dark;
|
|
||||||
|
|
||||||
ui.add_space(8.0);
|
|
||||||
ui.columns(2, |columns| {
|
|
||||||
columns[0].vertical_centered(|ui| {
|
|
||||||
View::radio_value(ui, &mut selected_use_dark, false, t!("light"));
|
|
||||||
});
|
|
||||||
columns[1].vertical_centered(|ui| {
|
|
||||||
View::radio_value(ui, &mut selected_use_dark, true, t!("dark"));
|
|
||||||
})
|
|
||||||
});
|
|
||||||
ui.add_space(8.0);
|
|
||||||
|
|
||||||
if saved_use_dark != selected_use_dark {
|
|
||||||
AppConfig::set_dark_theme(selected_use_dark);
|
|
||||||
crate::setup_visuals(ui.ctx());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Draw language selection item content.
|
|
||||||
fn language_item_ui(locale: &str, ui: &mut egui::Ui, index: usize, len: usize, modal: &Modal) {
|
|
||||||
// Setup layout size.
|
|
||||||
let mut rect = ui.available_rect_before_wrap();
|
|
||||||
rect.set_height(50.0);
|
|
||||||
|
|
||||||
// Draw round background.
|
|
||||||
let bg_rect = rect.clone();
|
|
||||||
let item_rounding = View::item_rounding(index, len, false);
|
|
||||||
ui.painter().rect(bg_rect, item_rounding, Colors::fill(), View::item_stroke());
|
|
||||||
|
|
||||||
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| {
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Draw content for integrated node warning [`Modal`] on Android.
|
|
||||||
fn android_warning_modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal) {
|
|
||||||
ui.add_space(6.0);
|
|
||||||
ui.vertical_centered(|ui| {
|
|
||||||
ui.label(RichText::new(t!("network.android_warning"))
|
|
||||||
.size(16.0)
|
|
||||||
.color(Colors::text(false)));
|
|
||||||
});
|
|
||||||
ui.add_space(8.0);
|
|
||||||
ui.vertical_centered_justified(|ui| {
|
|
||||||
View::button(ui, t!("continue"), Colors::white_or_black(false), || {
|
|
||||||
AppConfig::show_android_integrated_node_warning();
|
|
||||||
modal.close();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
ui.add_space(6.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Draw content for integrated node warning [`Modal`] on Android.
|
|
||||||
fn crash_report_modal_ui(&mut self,
|
|
||||||
ui: &mut egui::Ui,
|
|
||||||
modal: &Modal,
|
|
||||||
cb: &dyn PlatformCallbacks) {
|
|
||||||
ui.add_space(6.0);
|
|
||||||
ui.vertical_centered(|ui| {
|
|
||||||
ui.label(RichText::new(t!("crash_report_warning"))
|
|
||||||
.size(16.0)
|
|
||||||
.color(Colors::text(false)));
|
|
||||||
ui.add_space(6.0);
|
|
||||||
// Draw button to share crash report.
|
|
||||||
let text = format!("{} {}", FILE_X, t!("share"));
|
|
||||||
View::colored_text_button(ui, text, Colors::blue(), Colors::white_or_black(false), || {
|
|
||||||
if let Ok(data) = fs::read_to_string(Settings::crash_report_path()) {
|
|
||||||
let name = Settings::CRASH_REPORT_FILE_NAME.to_string();
|
|
||||||
let _ = cb.share_data(name, data.as_bytes().to_vec());
|
|
||||||
}
|
|
||||||
Settings::delete_crash_report();
|
|
||||||
modal.close();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
ui.add_space(8.0);
|
|
||||||
View::horizontal_line(ui, Colors::item_stroke());
|
|
||||||
ui.add_space(8.0);
|
|
||||||
ui.vertical_centered_justified(|ui| {
|
|
||||||
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
|
|
||||||
Settings::delete_crash_report();
|
|
||||||
modal.close();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
ui.add_space(6.0);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get [`NetworkContent`] panel state and width.
|
/// Get [`NetworkContent`] panel state and width.
|
||||||
fn network_panel_state_width(ctx: &egui::Context, dual_panel: bool) -> (bool, f32) {
|
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 is_panel_open = dual_panel || Content::is_network_panel_open();
|
||||||
let panel_width = if dual_panel {
|
let panel_width = if dual_panel {
|
||||||
Content::SIDE_PANEL_WIDTH + View::get_left_inset()
|
Content::SIDE_PANEL_WIDTH + View::get_left_inset()
|
||||||
} else {
|
} else {
|
||||||
let is_fullscreen = ctx.input(|i| {
|
let is_fullscreen = ctx.input(|i| i.viewport().fullscreen.unwrap_or(false));
|
||||||
i.viewport().fullscreen.unwrap_or(false)
|
View::window_size(ctx).0
|
||||||
});
|
- if View::is_desktop()
|
||||||
View::window_size(ctx).0 - if View::is_desktop() && !is_fullscreen &&
|
&& !is_fullscreen
|
||||||
OperatingSystem::from_target_os() != OperatingSystem::Mac {
|
&& OperatingSystem::from_target_os() != OperatingSystem::Mac
|
||||||
Content::WINDOW_FRAME_MARGIN * 2.0
|
{
|
||||||
} else {
|
Content::WINDOW_FRAME_MARGIN * 2.0
|
||||||
0.0
|
} else {
|
||||||
}
|
0.0
|
||||||
};
|
}
|
||||||
(is_panel_open, panel_width)
|
};
|
||||||
}
|
(is_panel_open, panel_width)
|
||||||
|
}
|
||||||
|
|||||||
+168
-89
@@ -12,105 +12,184 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
|
use egui::CornerRadius;
|
||||||
|
use parking_lot::RwLock;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
use std::{fs, thread};
|
use std::{fs, thread};
|
||||||
use parking_lot::RwLock;
|
|
||||||
|
|
||||||
use crate::gui::Colors;
|
use crate::gui::Colors;
|
||||||
use crate::gui::icons::ARCHIVE_BOX;
|
use crate::gui::icons::ARCHIVE_BOX;
|
||||||
use crate::gui::platform::PlatformCallbacks;
|
use crate::gui::platform::PlatformCallbacks;
|
||||||
use crate::gui::views::View;
|
use crate::gui::views::View;
|
||||||
|
|
||||||
|
/// Type of button.
|
||||||
|
pub enum FilePickContentType {
|
||||||
|
Button(String),
|
||||||
|
ItemButton(CornerRadius),
|
||||||
|
Tab,
|
||||||
|
}
|
||||||
|
|
||||||
/// Button to pick file and parse its data into text.
|
/// Button to pick file and parse its data into text.
|
||||||
pub struct FilePickButton {
|
pub struct FilePickContent {
|
||||||
/// Flag to check if file is picking.
|
/// Content type.
|
||||||
pub file_picking: Arc<AtomicBool>,
|
content_type: FilePickContentType,
|
||||||
/// Flag to check if file is parsing.
|
|
||||||
pub file_parsing: Arc<AtomicBool>,
|
/// Flag to check if button is active.
|
||||||
/// File parsing result.
|
active: bool,
|
||||||
pub file_parsing_result: Arc<RwLock<Option<String>>>
|
|
||||||
|
/// Flag to check if file is picking.
|
||||||
|
file_picking: Arc<AtomicBool>,
|
||||||
|
|
||||||
|
/// Flag to check if folder should be picked.
|
||||||
|
pick_folder: bool,
|
||||||
|
/// Flag to parse file content after pick.
|
||||||
|
parse_file: bool,
|
||||||
|
/// Flag to check if file is parsing.
|
||||||
|
file_parsing: Arc<AtomicBool>,
|
||||||
|
/// File parsing result.
|
||||||
|
file_parsing_result: Arc<RwLock<Option<String>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for FilePickButton {
|
impl FilePickContent {
|
||||||
fn default() -> Self {
|
/// Create new content from provided type.
|
||||||
Self {
|
pub fn new(content_type: FilePickContentType) -> Self {
|
||||||
file_picking: Arc::new(AtomicBool::new(false)),
|
Self {
|
||||||
file_parsing: Arc::new(AtomicBool::new(false)),
|
content_type,
|
||||||
file_parsing_result: Arc::new(RwLock::new(None))
|
active: false,
|
||||||
}
|
file_picking: Arc::new(AtomicBool::new(false)),
|
||||||
}
|
pick_folder: false,
|
||||||
|
parse_file: true,
|
||||||
|
file_parsing: Arc::new(AtomicBool::new(false)),
|
||||||
|
file_parsing_result: Arc::new(RwLock::new(None)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pick folder.
|
||||||
|
pub fn pick_folder(mut self) -> Self {
|
||||||
|
self.pick_folder = true;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Do not parse file content.
|
||||||
|
pub fn no_parse(mut self) -> Self {
|
||||||
|
self.parse_file = false;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enable or disable the button.
|
||||||
|
pub fn set_active(&mut self, active: bool) {
|
||||||
|
self.active = active;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw content with provided callback to return path of the file.
|
||||||
|
pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks, pick: impl FnOnce(String)) {
|
||||||
|
if self.file_picking.load(Ordering::Relaxed) {
|
||||||
|
View::small_loading_spinner(ui);
|
||||||
|
// Check file pick result.
|
||||||
|
if let Some(path) = cb.picked_file() {
|
||||||
|
self.file_picking.store(false, Ordering::Relaxed);
|
||||||
|
if !path.is_empty() {
|
||||||
|
if self.parse_file {
|
||||||
|
self.parse_file(path);
|
||||||
|
} else {
|
||||||
|
pick(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if self.file_parsing.load(Ordering::Relaxed) {
|
||||||
|
View::small_loading_spinner(ui);
|
||||||
|
// Check file parsing result.
|
||||||
|
let has_result = {
|
||||||
|
let r_res = self.file_parsing_result.read();
|
||||||
|
r_res.is_some()
|
||||||
|
};
|
||||||
|
if has_result {
|
||||||
|
let text = {
|
||||||
|
let r_res = self.file_parsing_result.read();
|
||||||
|
r_res.clone().unwrap()
|
||||||
|
};
|
||||||
|
// Callback on result.
|
||||||
|
pick(text);
|
||||||
|
// Clear result.
|
||||||
|
let mut w_res = self.file_parsing_result.write();
|
||||||
|
*w_res = None;
|
||||||
|
self.file_parsing.store(false, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Draw button to pick file.
|
||||||
|
match &self.content_type {
|
||||||
|
FilePickContentType::Button(text) => {
|
||||||
|
let text = format!("{} {}", ARCHIVE_BOX, text);
|
||||||
|
let text_color = Colors::blue();
|
||||||
|
let fill = Colors::white_or_black(false);
|
||||||
|
View::colored_text_button(ui, text, text_color, fill, || {
|
||||||
|
self.on_file_pick(pick, cb);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
FilePickContentType::ItemButton(r) => {
|
||||||
|
View::item_button(ui, r.clone(), ARCHIVE_BOX, Some(Colors::blue()), || {
|
||||||
|
self.on_file_pick(pick, cb);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
FilePickContentType::Tab => {
|
||||||
|
let active = match self.active {
|
||||||
|
true => Some(
|
||||||
|
self.file_parsing.load(Ordering::Relaxed)
|
||||||
|
|| self.file_picking.load(Ordering::Relaxed),
|
||||||
|
),
|
||||||
|
false => None,
|
||||||
|
};
|
||||||
|
View::tab_button(ui, ARCHIVE_BOX, Some(Colors::blue()), active, |_| {
|
||||||
|
self.on_file_pick(pick, cb);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle pick file request.
|
||||||
|
fn on_file_pick(&self, on_pick: impl FnOnce(String), cb: &dyn PlatformCallbacks) {
|
||||||
|
let path = if self.pick_folder {
|
||||||
|
cb.pick_folder()
|
||||||
|
} else {
|
||||||
|
cb.pick_file()
|
||||||
|
};
|
||||||
|
if path.is_none() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let path = path.unwrap();
|
||||||
|
// Wait for asynchronous file pick result if path is empty.
|
||||||
|
if path.is_empty() {
|
||||||
|
self.file_picking.store(true, Ordering::Relaxed);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Parse result if needed.
|
||||||
|
if self.parse_file {
|
||||||
|
self.parse_file(path);
|
||||||
|
} else {
|
||||||
|
on_pick(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle picked file path.
|
||||||
|
fn parse_file(&self, path: String) {
|
||||||
|
self.file_parsing.store(true, Ordering::Relaxed);
|
||||||
|
let result = self.file_parsing_result.clone();
|
||||||
|
thread::spawn(move || {
|
||||||
|
if path.ends_with(".gif") {
|
||||||
|
//TODO: Detect QR codes on GIF file.
|
||||||
|
} else if path.ends_with(".jpeg") || path.ends_with(".jpg") || path.ends_with(".png") {
|
||||||
|
//TODO: Detect QR codes on image files.
|
||||||
|
} else {
|
||||||
|
// Parse file as plain text.
|
||||||
|
let mut w_res = result.write();
|
||||||
|
if let Ok(text) = fs::read_to_string(path) {
|
||||||
|
*w_res = Some(text);
|
||||||
|
} else {
|
||||||
|
*w_res = Some("".to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FilePickButton {
|
|
||||||
/// Draw button content.
|
|
||||||
pub fn ui(&mut self,
|
|
||||||
ui: &mut egui::Ui,
|
|
||||||
cb: &dyn PlatformCallbacks,
|
|
||||||
on_result: impl FnOnce(String)) {
|
|
||||||
if self.file_picking.load(Ordering::Relaxed) {
|
|
||||||
View::small_loading_spinner(ui);
|
|
||||||
// Check file pick result.
|
|
||||||
if let Some(path) = cb.picked_file() {
|
|
||||||
self.file_picking.store(false, Ordering::Relaxed);
|
|
||||||
if !path.is_empty() {
|
|
||||||
self.on_file_pick(path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if self.file_parsing.load(Ordering::Relaxed) {
|
|
||||||
View::small_loading_spinner(ui);
|
|
||||||
// Check file parsing result.
|
|
||||||
let has_result = {
|
|
||||||
let r_res = self.file_parsing_result.read();
|
|
||||||
r_res.is_some()
|
|
||||||
};
|
|
||||||
if has_result {
|
|
||||||
let text = {
|
|
||||||
let r_res = self.file_parsing_result.read();
|
|
||||||
r_res.clone().unwrap()
|
|
||||||
};
|
|
||||||
// Callback on result.
|
|
||||||
on_result(text);
|
|
||||||
// Clear result.
|
|
||||||
let mut w_res = self.file_parsing_result.write();
|
|
||||||
*w_res = None;
|
|
||||||
self.file_parsing.store(false, Ordering::Relaxed);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Draw button to pick file.
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Handle picked file path.
|
|
||||||
fn on_file_pick(&self, path: String) {
|
|
||||||
// Wait for asynchronous file pick result if path is empty.
|
|
||||||
if path.is_empty() {
|
|
||||||
self.file_picking.store(true, Ordering::Relaxed);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
self.file_parsing.store(true, Ordering::Relaxed);
|
|
||||||
let result = self.file_parsing_result.clone();
|
|
||||||
thread::spawn(move || {
|
|
||||||
if path.ends_with(".gif") {
|
|
||||||
//TODO: Detect QR codes on GIF file.
|
|
||||||
} else if path.ends_with(".jpeg") || path.ends_with(".jpg") ||
|
|
||||||
path.ends_with(".png") {
|
|
||||||
//TODO: Detect QR codes on image files.
|
|
||||||
} else {
|
|
||||||
// Parse file as plain text.
|
|
||||||
let mut w_res = result.write();
|
|
||||||
if let Ok(text) = fs::read_to_string(path) {
|
|
||||||
*w_res = Some(text);
|
|
||||||
} else {
|
|
||||||
*w_res = Some("".to_string());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,475 @@
|
|||||||
|
// Copyright 2025 The Grim Developers
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
use egui::text_edit::TextEditState;
|
||||||
|
use egui::{Align, Layout, TextBuffer, TextStyle, ViewportCommand, Widget};
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
use parking_lot::RwLock;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::gui::Colors;
|
||||||
|
use crate::gui::icons::{CLIPBOARD_TEXT, COPY, EYE, EYE_SLASH, SCAN};
|
||||||
|
use crate::gui::platform::PlatformCallbacks;
|
||||||
|
use crate::gui::views::input::keyboard::KeyboardContent;
|
||||||
|
use crate::gui::views::{KeyboardEvent, View};
|
||||||
|
|
||||||
|
/// Text input content.
|
||||||
|
pub struct TextEdit {
|
||||||
|
/// View identifier.
|
||||||
|
id: egui::Id,
|
||||||
|
/// Check if input is enabled or disabled.
|
||||||
|
enabled: bool,
|
||||||
|
/// Horizontal text centering is needed.
|
||||||
|
h_center: bool,
|
||||||
|
/// Focus is needed.
|
||||||
|
focus: bool,
|
||||||
|
/// Focus request was passed.
|
||||||
|
focus_request: bool,
|
||||||
|
/// Hide letters and draw button to show/hide letters.
|
||||||
|
password: bool,
|
||||||
|
/// Show copy button.
|
||||||
|
copy: bool,
|
||||||
|
/// Show paste button.
|
||||||
|
paste: bool,
|
||||||
|
/// Show button to scan QR code into text.
|
||||||
|
scan_qr: bool,
|
||||||
|
/// Scan button was pressed.
|
||||||
|
pub scan_pressed: bool,
|
||||||
|
/// Tab or Enter keys were pressed to focus on next line.
|
||||||
|
pub enter_pressed: bool,
|
||||||
|
/// Flag to enter only numbers.
|
||||||
|
numeric: bool,
|
||||||
|
/// Flag to not show soft keyboard.
|
||||||
|
no_soft_keyboard: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TextEdit {
|
||||||
|
/// Default height of [`egui::TextEdit`] view.
|
||||||
|
const TEXT_EDIT_HEIGHT: f32 = 42.0;
|
||||||
|
|
||||||
|
pub fn new(id: egui::Id) -> Self {
|
||||||
|
Self {
|
||||||
|
id,
|
||||||
|
enabled: true,
|
||||||
|
h_center: false,
|
||||||
|
focus: true,
|
||||||
|
focus_request: false,
|
||||||
|
password: false,
|
||||||
|
copy: false,
|
||||||
|
paste: false,
|
||||||
|
scan_qr: false,
|
||||||
|
scan_pressed: false,
|
||||||
|
enter_pressed: false,
|
||||||
|
numeric: false,
|
||||||
|
no_soft_keyboard: is_android(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw text input.
|
||||||
|
pub fn ui(&mut self, ui: &mut egui::Ui, input: &mut String, cb: &dyn PlatformCallbacks) {
|
||||||
|
self.input_ui(ui, input, |_| {}, cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw text input with additional buttons (right to left order).
|
||||||
|
pub fn custom_buttons_ui(
|
||||||
|
&mut self,
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
input: &mut String,
|
||||||
|
cb: &dyn PlatformCallbacks,
|
||||||
|
buttons_content: impl FnOnce(&mut egui::Ui),
|
||||||
|
) {
|
||||||
|
self.input_ui(ui, input, buttons_content, cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw text input content.
|
||||||
|
fn input_ui(
|
||||||
|
&mut self,
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
input: &mut String,
|
||||||
|
buttons_content: impl FnOnce(&mut egui::Ui),
|
||||||
|
cb: &dyn PlatformCallbacks,
|
||||||
|
) {
|
||||||
|
let mut layout_rect = ui.available_rect_before_wrap();
|
||||||
|
layout_rect.set_height(Self::TEXT_EDIT_HEIGHT);
|
||||||
|
ui.allocate_ui_with_layout(
|
||||||
|
layout_rect.size(),
|
||||||
|
Layout::right_to_left(Align::Max),
|
||||||
|
|ui| {
|
||||||
|
let mut hide_input = false;
|
||||||
|
if self.password {
|
||||||
|
let show_pass_id = egui::Id::new(self.id).with("_show_pass");
|
||||||
|
hide_input = ui.data(|data| data.get_temp(show_pass_id)).unwrap_or(true);
|
||||||
|
// Draw button to show/hide current password.
|
||||||
|
let eye_icon = if hide_input { EYE } else { EYE_SLASH };
|
||||||
|
View::button_ui(
|
||||||
|
ui,
|
||||||
|
eye_icon.to_string(),
|
||||||
|
Colors::white_or_black(false),
|
||||||
|
|ui| {
|
||||||
|
hide_input = !hide_input;
|
||||||
|
ui.data_mut(|data| {
|
||||||
|
data.insert_temp(show_pass_id, hide_input);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
ui.add_space(8.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extra buttons content.
|
||||||
|
(buttons_content)(ui);
|
||||||
|
|
||||||
|
// Setup copy button.
|
||||||
|
if self.copy {
|
||||||
|
let copy_icon = COPY.to_string();
|
||||||
|
View::button(ui, copy_icon, Colors::white_or_black(false), || {
|
||||||
|
cb.copy_string_to_buffer(input.clone());
|
||||||
|
});
|
||||||
|
ui.add_space(8.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup paste button.
|
||||||
|
if self.paste {
|
||||||
|
let paste_icon = CLIPBOARD_TEXT.to_string();
|
||||||
|
View::button(ui, paste_icon, Colors::white_or_black(false), || {
|
||||||
|
*input = cb.get_string_from_buffer();
|
||||||
|
});
|
||||||
|
ui.add_space(8.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup scan QR code button.
|
||||||
|
if self.scan_qr {
|
||||||
|
let scan_icon = SCAN.to_string();
|
||||||
|
View::button(ui, scan_icon, Colors::white_or_black(false), || {
|
||||||
|
cb.start_camera();
|
||||||
|
self.scan_pressed = true;
|
||||||
|
});
|
||||||
|
ui.add_space(8.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
let layout_size = ui.available_size();
|
||||||
|
ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Min), |ui| {
|
||||||
|
// Setup text edit size.
|
||||||
|
let mut edit_rect = ui.available_rect_before_wrap();
|
||||||
|
edit_rect.set_height(Self::TEXT_EDIT_HEIGHT);
|
||||||
|
|
||||||
|
// Setup focused input value to avoid dismiss when click on keyboard.
|
||||||
|
let focused_input_id = egui::Id::new("focused_input_id");
|
||||||
|
let focused = ui
|
||||||
|
.data(|data| data.get_temp(focused_input_id))
|
||||||
|
.unwrap_or(egui::Id::new(""))
|
||||||
|
== self.id;
|
||||||
|
|
||||||
|
// Show text edit.
|
||||||
|
let text_edit_resp = egui::TextEdit::singleline(input)
|
||||||
|
.text_color(if self.enabled {
|
||||||
|
Colors::text(false)
|
||||||
|
} else {
|
||||||
|
Colors::inactive_text()
|
||||||
|
})
|
||||||
|
.interactive(self.enabled)
|
||||||
|
.id(self.id)
|
||||||
|
.font(TextStyle::Heading)
|
||||||
|
.min_size(edit_rect.size())
|
||||||
|
.margin(if View::is_desktop() {
|
||||||
|
egui::Margin::symmetric(4, 2)
|
||||||
|
} else {
|
||||||
|
egui::Margin::symmetric(8, 8)
|
||||||
|
})
|
||||||
|
.horizontal_align(if self.h_center {
|
||||||
|
Align::Center
|
||||||
|
} else {
|
||||||
|
Align::Min
|
||||||
|
})
|
||||||
|
.vertical_align(Align::Center)
|
||||||
|
.password(hide_input)
|
||||||
|
.cursor_at_end(true)
|
||||||
|
.ui(ui);
|
||||||
|
|
||||||
|
// Setup focus state.
|
||||||
|
let clicked = text_edit_resp.clicked();
|
||||||
|
if !text_edit_resp.has_focus()
|
||||||
|
&& (self.focus || self.focus_request || clicked || focused)
|
||||||
|
{
|
||||||
|
text_edit_resp.request_focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset keyboard state for newly focused.
|
||||||
|
if clicked || self.focus_request {
|
||||||
|
ui.ctx()
|
||||||
|
.send_viewport_cmd(ViewportCommand::IMEAllowed(true));
|
||||||
|
KeyboardContent::reset_window_state();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply text from software input.
|
||||||
|
if text_edit_resp.has_focus() {
|
||||||
|
ui.data_mut(|data| {
|
||||||
|
data.insert_temp(focused_input_id, self.id);
|
||||||
|
});
|
||||||
|
self.enter_pressed = self.on_soft_input(ui, self.id, false, input);
|
||||||
|
// Check Enter or Tab keys press.
|
||||||
|
if !self.focus_request {
|
||||||
|
if ui.ctx().input(|i| {
|
||||||
|
i.key_pressed(egui::Key::Enter) || i.key_pressed(egui::Key::Tab)
|
||||||
|
}) {
|
||||||
|
self.enter_pressed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if self.enter_pressed {
|
||||||
|
KeyboardContent::unshift();
|
||||||
|
}
|
||||||
|
if !self.no_soft_keyboard {
|
||||||
|
KeyboardContent::default().window_ui(self.numeric, ui.ctx());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
// Immediate repaint when input is open.
|
||||||
|
ui.ctx().request_repaint();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Apply soft keyboard input data to provided String, returns `true` if Enter was pressed.
|
||||||
|
fn on_soft_input(
|
||||||
|
&self,
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
id: egui::Id,
|
||||||
|
multiline: bool,
|
||||||
|
value: &mut String,
|
||||||
|
) -> bool {
|
||||||
|
let event: Option<KeyboardEvent> = if is_android() {
|
||||||
|
let mut w_input = LAST_SOFT_KEYBOARD_EVENT.write();
|
||||||
|
w_input.take()
|
||||||
|
} else {
|
||||||
|
KeyboardContent::consume_event()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle keyboard input event.
|
||||||
|
if let Some(e) = event {
|
||||||
|
let mut enter_pressed = false;
|
||||||
|
let mut state = TextEditState::load(ui.ctx(), id).unwrap();
|
||||||
|
match state.cursor.char_range() {
|
||||||
|
None => {}
|
||||||
|
Some(range) => {
|
||||||
|
let mut r = range.clone();
|
||||||
|
let mut index = r.primary.index;
|
||||||
|
|
||||||
|
let selected = r.primary.index != r.secondary.index;
|
||||||
|
let start_select =
|
||||||
|
f32::min(r.primary.index as f32, r.secondary.index as f32) as usize;
|
||||||
|
let end_select =
|
||||||
|
f32::max(r.primary.index as f32, r.secondary.index as f32) as usize;
|
||||||
|
match e {
|
||||||
|
KeyboardEvent::TEXT(text) => {
|
||||||
|
if selected {
|
||||||
|
*value = {
|
||||||
|
let part1: String =
|
||||||
|
value.chars().skip(0).take(start_select).collect();
|
||||||
|
let part2: String = value
|
||||||
|
.chars()
|
||||||
|
.skip(end_select)
|
||||||
|
.take(value.len() - end_select)
|
||||||
|
.collect();
|
||||||
|
format!("{}{}{}", part1, text, part2)
|
||||||
|
};
|
||||||
|
index = start_select + 1;
|
||||||
|
} else {
|
||||||
|
value.insert_text(text.as_str(), index);
|
||||||
|
index = index + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KeyboardEvent::CLEAR => {
|
||||||
|
if selected {
|
||||||
|
*value = {
|
||||||
|
let part1: String =
|
||||||
|
value.chars().skip(0).take(start_select).collect();
|
||||||
|
let part2: String = value
|
||||||
|
.chars()
|
||||||
|
.skip(end_select)
|
||||||
|
.take(value.len() - end_select)
|
||||||
|
.collect();
|
||||||
|
format!("{}{}", part1, part2)
|
||||||
|
};
|
||||||
|
index = start_select;
|
||||||
|
} else if index != 0 {
|
||||||
|
*value = {
|
||||||
|
let part1: String =
|
||||||
|
value.chars().skip(0).take(index - 1).collect();
|
||||||
|
let part2: String = value
|
||||||
|
.chars()
|
||||||
|
.skip(index)
|
||||||
|
.take(value.len() - index)
|
||||||
|
.collect();
|
||||||
|
format!("{}{}", part1, part2)
|
||||||
|
};
|
||||||
|
index = index - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KeyboardEvent::ENTER => {
|
||||||
|
if multiline {
|
||||||
|
value.insert_text("\n", index);
|
||||||
|
index = index + 1;
|
||||||
|
} else {
|
||||||
|
enter_pressed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Setup cursor index.
|
||||||
|
r.primary.index = index;
|
||||||
|
r.secondary.index = r.primary.index;
|
||||||
|
|
||||||
|
state.cursor.set_char_range(Some(r));
|
||||||
|
TextEditState::store(state, ui.ctx(), id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return enter_pressed;
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set cursor to the end of text.
|
||||||
|
pub fn cursor_to_end(&self, text_len: usize, ui: &mut egui::Ui) {
|
||||||
|
let mut state = TextEditState::load(ui.ctx(), self.id).unwrap();
|
||||||
|
match state.cursor.char_range() {
|
||||||
|
None => {}
|
||||||
|
Some(range) => {
|
||||||
|
let mut r = range.clone();
|
||||||
|
r.primary.index = text_len;
|
||||||
|
r.secondary.index = text_len;
|
||||||
|
state.cursor.set_char_range(Some(r));
|
||||||
|
TextEditState::store(state, ui.ctx(), self.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Disable input.
|
||||||
|
pub fn disable(mut self) -> Self {
|
||||||
|
self.enabled = false;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Center text horizontally.
|
||||||
|
pub fn h_center(mut self) -> Self {
|
||||||
|
self.h_center = true;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enable or disable constant focus.
|
||||||
|
pub fn focus(mut self, focus: bool) -> Self {
|
||||||
|
self.focus = focus;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Focus on field.
|
||||||
|
pub fn focus_request(&mut self) {
|
||||||
|
self.focus_request = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Allow input of numbers only.
|
||||||
|
pub fn numeric(mut self) -> Self {
|
||||||
|
self.numeric = true;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Hide letters and draw button to show/hide letters.
|
||||||
|
pub fn password(mut self) -> Self {
|
||||||
|
self.password = true;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Show button to copy text.
|
||||||
|
pub fn copy(mut self) -> Self {
|
||||||
|
self.copy = true;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Show button to paste text.
|
||||||
|
pub fn paste(mut self) -> Self {
|
||||||
|
self.paste = true;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Show button to scan QR code to text.
|
||||||
|
pub fn scan_qr(mut self) -> Self {
|
||||||
|
self.scan_qr = true;
|
||||||
|
self.scan_pressed = false;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Do not show soft keyboard for input.
|
||||||
|
pub fn no_soft_keyboard(mut self) -> Self {
|
||||||
|
self.no_soft_keyboard = true;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if current system is Android.
|
||||||
|
fn is_android() -> bool {
|
||||||
|
egui::os::OperatingSystem::from_target_os() == egui::os::OperatingSystem::Android
|
||||||
|
}
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
static ref LAST_SOFT_KEYBOARD_EVENT: Arc<RwLock<Option<KeyboardEvent>>> =
|
||||||
|
Arc::new(RwLock::new(None));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
#[unsafe(no_mangle)]
|
||||||
|
/// Callback from Java code with last entered character from soft keyboard.
|
||||||
|
pub extern "C" fn Java_mw_gri_android_MainActivity_onTextInput(
|
||||||
|
_env: jni::JNIEnv,
|
||||||
|
_class: jni::objects::JObject,
|
||||||
|
char: jni::sys::jstring,
|
||||||
|
) {
|
||||||
|
use jni::objects::JString;
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
let j_obj = JString::from_raw(char);
|
||||||
|
let j_str = _env.get_string_unchecked(j_obj.as_ref()).unwrap();
|
||||||
|
match j_str.to_str() {
|
||||||
|
Ok(str) => {
|
||||||
|
let mut w_input = LAST_SOFT_KEYBOARD_EVENT.write();
|
||||||
|
*w_input = Some(KeyboardEvent::TEXT(str.to_string()));
|
||||||
|
}
|
||||||
|
Err(_) => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
#[unsafe(no_mangle)]
|
||||||
|
/// Callback from Java code when Clear key was pressed at soft keyboard.
|
||||||
|
pub extern "C" fn Java_mw_gri_android_MainActivity_onClearInput(
|
||||||
|
_env: jni::JNIEnv,
|
||||||
|
_class: jni::objects::JObject,
|
||||||
|
) {
|
||||||
|
let mut w_input = LAST_SOFT_KEYBOARD_EVENT.write();
|
||||||
|
*w_input = Some(KeyboardEvent::CLEAR);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
#[unsafe(no_mangle)]
|
||||||
|
/// Callback from Java code when Enter key was pressed at soft keyboard.
|
||||||
|
pub extern "C" fn Java_mw_gri_android_MainActivity_onEnterInput(
|
||||||
|
_env: jni::JNIEnv,
|
||||||
|
_class: jni::objects::JObject,
|
||||||
|
) {
|
||||||
|
let mut w_input = LAST_SOFT_KEYBOARD_EVENT.write();
|
||||||
|
*w_input = Some(KeyboardEvent::ENTER);
|
||||||
|
}
|
||||||
@@ -0,0 +1,549 @@
|
|||||||
|
// Copyright 2025 The Grim Developers
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
use egui::{
|
||||||
|
Align, Align2, Button, Color32, CursorIcon, Layout, Margin, Rect, Response, RichText, Sense,
|
||||||
|
Shadow, Vec2, Widget,
|
||||||
|
};
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
use parking_lot::RwLock;
|
||||||
|
use std::string::ToString;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::sync::atomic::Ordering;
|
||||||
|
|
||||||
|
use crate::AppConfig;
|
||||||
|
use crate::gui::Colors;
|
||||||
|
use crate::gui::icons::{ARROW_FAT_UP, BACKSPACE, GLOBE_SIMPLE, KEY_RETURN};
|
||||||
|
use crate::gui::views::{KeyboardEvent, KeyboardLayout, KeyboardState, View};
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
/// Keyboard window state.
|
||||||
|
static ref WINDOW_STATE: Arc<RwLock<KeyboardState >> = Arc::new(
|
||||||
|
RwLock::new(KeyboardState::default())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Software keyboard content.
|
||||||
|
pub struct KeyboardContent {
|
||||||
|
/// Keyboard content state.
|
||||||
|
state: KeyboardState,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for KeyboardContent {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
state: KeyboardState::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl KeyboardContent {
|
||||||
|
/// Maximum keyboard content width.
|
||||||
|
const MAX_WIDTH: f32 = 600.0;
|
||||||
|
/// Maximum numbers layout width.
|
||||||
|
const MAX_WIDTH_NUMBERS: f32 = 400.0;
|
||||||
|
|
||||||
|
/// Keyboard window id.
|
||||||
|
pub const WINDOW_ID: &'static str = "soft_keyboard_window";
|
||||||
|
|
||||||
|
/// Draw keyboard content as separate [`Window`].
|
||||||
|
pub fn window_ui(&mut self, numeric: bool, ctx: &egui::Context) {
|
||||||
|
let width = ctx.content_rect().width();
|
||||||
|
let layer_id = egui::Window::new(Self::WINDOW_ID)
|
||||||
|
.title_bar(false)
|
||||||
|
.resizable(false)
|
||||||
|
.collapsible(false)
|
||||||
|
.min_width(width)
|
||||||
|
.default_width(width)
|
||||||
|
.anchor(Align2::CENTER_BOTTOM, Vec2::new(0.0, 0.0))
|
||||||
|
.frame(egui::Frame {
|
||||||
|
shadow: Shadow {
|
||||||
|
offset: Default::default(),
|
||||||
|
blur: 30.0 as u8,
|
||||||
|
spread: 3.0 as u8,
|
||||||
|
color: Color32::from_black_alpha(32),
|
||||||
|
},
|
||||||
|
inner_margin: Margin {
|
||||||
|
left: View::get_left_inset() as i8,
|
||||||
|
right: View::get_right_inset() as i8,
|
||||||
|
top: 1.0 as i8,
|
||||||
|
bottom: View::get_bottom_inset() as i8,
|
||||||
|
},
|
||||||
|
fill: Colors::fill(),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.show(ctx, |ui| {
|
||||||
|
ui.set_min_width(width);
|
||||||
|
// Setup state.
|
||||||
|
{
|
||||||
|
let r_state = WINDOW_STATE.read();
|
||||||
|
self.state = (*r_state).clone();
|
||||||
|
}
|
||||||
|
// Calculate content width.
|
||||||
|
let side_insets = View::get_left_inset() + View::get_right_inset();
|
||||||
|
let available_width = width - side_insets;
|
||||||
|
let w = f32::min(
|
||||||
|
available_width,
|
||||||
|
if numeric {
|
||||||
|
Self::MAX_WIDTH_NUMBERS
|
||||||
|
} else {
|
||||||
|
Self::MAX_WIDTH
|
||||||
|
},
|
||||||
|
);
|
||||||
|
// Draw content.
|
||||||
|
View::max_width_ui(ui, w, |ui| {
|
||||||
|
self.ui(numeric, ui);
|
||||||
|
});
|
||||||
|
// Save state.
|
||||||
|
let mut w_state = WINDOW_STATE.write();
|
||||||
|
*w_state = self.state.clone();
|
||||||
|
})
|
||||||
|
.unwrap()
|
||||||
|
.response
|
||||||
|
.layer_id;
|
||||||
|
|
||||||
|
// Always show keyboard above others windows.
|
||||||
|
ctx.move_to_top(layer_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw keyboard content.
|
||||||
|
pub fn ui(&mut self, numeric: bool, ui: &mut egui::Ui) {
|
||||||
|
// Setup layout.
|
||||||
|
if numeric {
|
||||||
|
self.state.layout = Arc::new(KeyboardLayout::NUMBERS);
|
||||||
|
} else if *self.state.layout == KeyboardLayout::NUMBERS {
|
||||||
|
self.state.layout = Arc::new(KeyboardLayout::TEXT);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup spacing between buttons.
|
||||||
|
ui.style_mut().spacing.item_spacing = egui::vec2(0.0, 0.0);
|
||||||
|
// Setup vertical padding inside buttons.
|
||||||
|
ui.style_mut().spacing.button_padding = egui::vec2(0.0, if numeric { 12.0 } else { 10.0 });
|
||||||
|
|
||||||
|
// Draw input buttons.
|
||||||
|
let button_rect = match *self.state.layout {
|
||||||
|
KeyboardLayout::TEXT => self.text_ui(ui),
|
||||||
|
KeyboardLayout::SYMBOLS => self.symbols_ui(ui),
|
||||||
|
KeyboardLayout::NUMBERS => self.numbers_ui(ui),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Draw bottom keyboard buttons.
|
||||||
|
let bottom_size = {
|
||||||
|
let mut r = button_rect.clone();
|
||||||
|
r.set_width(ui.available_width());
|
||||||
|
r.size()
|
||||||
|
};
|
||||||
|
let button_width = ui.available_width()
|
||||||
|
/ match *self.state.layout {
|
||||||
|
KeyboardLayout::TEXT => 11.0,
|
||||||
|
KeyboardLayout::SYMBOLS => 10.0,
|
||||||
|
KeyboardLayout::NUMBERS => 4.0,
|
||||||
|
};
|
||||||
|
ui.allocate_ui_with_layout(bottom_size, Layout::right_to_left(Align::Center), |ui| {
|
||||||
|
match *self.state.layout {
|
||||||
|
KeyboardLayout::TEXT => {
|
||||||
|
// Enter key input.
|
||||||
|
ui.horizontal_centered(|ui| {
|
||||||
|
ui.set_max_width(button_width * 2.0);
|
||||||
|
self.custom_button_ui(
|
||||||
|
KEY_RETURN.to_string(),
|
||||||
|
Colors::white_or_black(false),
|
||||||
|
Some(Colors::green()),
|
||||||
|
ui,
|
||||||
|
|_, c| {
|
||||||
|
c.state.last_event = Arc::new(Some(KeyboardEvent::ENTER));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
// Custom input key.
|
||||||
|
ui.horizontal_centered(|ui| {
|
||||||
|
ui.set_max_width(button_width);
|
||||||
|
self.input_button_ui("m3", true, ui);
|
||||||
|
});
|
||||||
|
// Space key input.
|
||||||
|
ui.horizontal_centered(|ui| {
|
||||||
|
ui.set_max_width(button_width * 5.0);
|
||||||
|
self.custom_button_ui(
|
||||||
|
" ".to_string(),
|
||||||
|
Colors::inactive_text(),
|
||||||
|
None,
|
||||||
|
ui,
|
||||||
|
|l, c| {
|
||||||
|
c.state.last_event = Arc::new(Some(KeyboardEvent::TEXT(l)));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
// Switch to english and back.
|
||||||
|
ui.horizontal_centered(|ui| {
|
||||||
|
ui.set_max_width(button_width);
|
||||||
|
self.custom_button_ui(
|
||||||
|
GLOBE_SIMPLE.to_string(),
|
||||||
|
Colors::text_button(),
|
||||||
|
Some(Colors::fill_lite()),
|
||||||
|
ui,
|
||||||
|
|_, _| AppConfig::toggle_english_keyboard(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
// Switch to symbols layout.
|
||||||
|
self.custom_button_ui(
|
||||||
|
"!@ツ".to_string(),
|
||||||
|
Colors::text_button(),
|
||||||
|
Some(Colors::fill_lite()),
|
||||||
|
ui,
|
||||||
|
|_, c| {
|
||||||
|
c.state.layout = Arc::new(KeyboardLayout::SYMBOLS);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
KeyboardLayout::SYMBOLS => {
|
||||||
|
// Enter key input.
|
||||||
|
ui.horizontal_centered(|ui| {
|
||||||
|
ui.set_max_width(button_width * 2.0);
|
||||||
|
self.custom_button_ui(
|
||||||
|
KEY_RETURN.to_string(),
|
||||||
|
Colors::white_or_black(false),
|
||||||
|
Some(Colors::green()),
|
||||||
|
ui,
|
||||||
|
|_, c| {
|
||||||
|
c.state.last_event = Arc::new(Some(KeyboardEvent::ENTER));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
// Custom input key.
|
||||||
|
ui.horizontal_centered(|ui| {
|
||||||
|
ui.set_max_width(button_width);
|
||||||
|
self.input_button_ui("ツ", false, ui);
|
||||||
|
});
|
||||||
|
// Space key input.
|
||||||
|
ui.horizontal_centered(|ui| {
|
||||||
|
ui.set_max_width(button_width * 4.0);
|
||||||
|
self.custom_button_ui(
|
||||||
|
" ".to_string(),
|
||||||
|
Colors::inactive_text(),
|
||||||
|
None,
|
||||||
|
ui,
|
||||||
|
|l, c| {
|
||||||
|
c.state.last_event = Arc::new(Some(KeyboardEvent::TEXT(l)));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
// Switch to text layout.
|
||||||
|
let label = {
|
||||||
|
let q = t!("keyboard.q", locale = Self::input_locale().as_str());
|
||||||
|
let w = t!("keyboard.w", locale = Self::input_locale().as_str());
|
||||||
|
let e = t!("keyboard.e", locale = Self::input_locale().as_str());
|
||||||
|
format!("{}{}{}", q, w, e).to_uppercase()
|
||||||
|
};
|
||||||
|
self.custom_button_ui(
|
||||||
|
label,
|
||||||
|
Colors::text_button(),
|
||||||
|
Some(Colors::fill_lite()),
|
||||||
|
ui,
|
||||||
|
|_, c| {
|
||||||
|
c.state.layout = Arc::new(KeyboardLayout::TEXT);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
KeyboardLayout::NUMBERS => {
|
||||||
|
ui.horizontal_centered(|ui| {
|
||||||
|
ui.set_max_width(button_width * 2.0);
|
||||||
|
self.custom_button_ui(
|
||||||
|
KEY_RETURN.to_string(),
|
||||||
|
Colors::white_or_black(false),
|
||||||
|
Some(Colors::green()),
|
||||||
|
ui,
|
||||||
|
|_, c| {
|
||||||
|
c.state.last_event = Arc::new(Some(KeyboardEvent::ENTER));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
ui.horizontal_centered(|ui| {
|
||||||
|
ui.set_max_width(button_width);
|
||||||
|
self.input_button_ui("0", true, ui);
|
||||||
|
});
|
||||||
|
ui.horizontal_centered(|ui| {
|
||||||
|
ui.set_max_width(button_width);
|
||||||
|
self.input_button_ui(".", false, ui);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw numbers content returning button [`Rect`].
|
||||||
|
fn numbers_ui(&mut self, ui: &mut egui::Ui) -> Rect {
|
||||||
|
let mut button_rect = ui.available_rect_before_wrap();
|
||||||
|
let tl_0: Vec<&str> = vec!["1", "2", "3", "+"];
|
||||||
|
ui.columns(tl_0.len(), |columns| {
|
||||||
|
for (index, s) in tl_0.iter().enumerate() {
|
||||||
|
let last = index == tl_0.len() - 1;
|
||||||
|
button_rect = self.input_button_ui(s, !last, &mut columns[index]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let tl_1: Vec<&str> = vec!["4", "5", "6", ","];
|
||||||
|
ui.columns(tl_1.len(), |columns| {
|
||||||
|
for (index, s) in tl_1.iter().enumerate() {
|
||||||
|
let last = index == tl_1.len() - 1;
|
||||||
|
self.input_button_ui(s, !last, &mut columns[index]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let tl_2: Vec<&str> = vec!["7", "8", "9", BACKSPACE];
|
||||||
|
ui.columns(tl_2.len(), |columns| {
|
||||||
|
for (index, s) in tl_2.iter().enumerate() {
|
||||||
|
if index == tl_2.len() - 1 {
|
||||||
|
self.custom_button_ui(
|
||||||
|
BACKSPACE.to_string(),
|
||||||
|
Colors::red(),
|
||||||
|
Some(Colors::fill_lite()),
|
||||||
|
&mut columns[index],
|
||||||
|
|_, c| {
|
||||||
|
c.state.last_event = Arc::new(Some(KeyboardEvent::CLEAR));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
self.input_button_ui(s, true, &mut columns[index]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
button_rect
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw text content returning button [`Rect`].
|
||||||
|
fn text_ui(&mut self, ui: &mut egui::Ui) -> Rect {
|
||||||
|
let mut button_rect = ui.available_rect_before_wrap();
|
||||||
|
let tl_0: Vec<&str> = vec!["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "01"];
|
||||||
|
ui.columns(tl_0.len(), |columns| {
|
||||||
|
for (index, s) in tl_0.iter().enumerate() {
|
||||||
|
button_rect = self.input_button_ui(s, true, &mut columns[index]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let tl_1: Vec<&str> = vec!["q", "w", "e", "r", "t", "y", "u", "i", "o", "p", "p1"];
|
||||||
|
ui.columns(tl_1.len(), |columns| {
|
||||||
|
for (index, s) in tl_1.iter().enumerate() {
|
||||||
|
self.input_button_ui(s, true, &mut columns[index]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let tl_2: Vec<&str> = vec!["a", "s", "d", "f", "g", "h", "j", "k", "l", "l1", "l2"];
|
||||||
|
ui.columns(tl_2.len(), |columns| {
|
||||||
|
for (index, s) in tl_2.iter().enumerate() {
|
||||||
|
self.input_button_ui(s, true, &mut columns[index]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let tl_3: Vec<&str> = vec![
|
||||||
|
ARROW_FAT_UP,
|
||||||
|
"z",
|
||||||
|
"x",
|
||||||
|
"c",
|
||||||
|
"v",
|
||||||
|
"b",
|
||||||
|
"n",
|
||||||
|
"m",
|
||||||
|
"m1",
|
||||||
|
"m2",
|
||||||
|
BACKSPACE,
|
||||||
|
];
|
||||||
|
ui.columns(tl_3.len(), |columns| {
|
||||||
|
for (index, s) in tl_3.iter().enumerate() {
|
||||||
|
if index == 0 {
|
||||||
|
let shift = self.state.shift.load(Ordering::Relaxed);
|
||||||
|
let color = if shift {
|
||||||
|
Colors::yellow_dark()
|
||||||
|
} else {
|
||||||
|
Colors::inactive_text()
|
||||||
|
};
|
||||||
|
self.custom_button_ui(
|
||||||
|
ARROW_FAT_UP.to_string(),
|
||||||
|
color,
|
||||||
|
Some(Colors::fill_lite()),
|
||||||
|
&mut columns[index],
|
||||||
|
|_, c| {
|
||||||
|
c.state.shift.store(!shift, Ordering::Relaxed);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else if index == tl_3.len() - 1 {
|
||||||
|
self.custom_button_ui(
|
||||||
|
BACKSPACE.to_string(),
|
||||||
|
Colors::red(),
|
||||||
|
Some(Colors::fill_lite()),
|
||||||
|
&mut columns[index],
|
||||||
|
|_, c| {
|
||||||
|
c.state.last_event = Arc::new(Some(KeyboardEvent::CLEAR));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
self.input_button_ui(s, true, &mut columns[index]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
button_rect
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw symbols content returning button [`Rect`].
|
||||||
|
fn symbols_ui(&mut self, ui: &mut egui::Ui) -> Rect {
|
||||||
|
let mut button_rect = ui.available_rect_before_wrap();
|
||||||
|
let tl_0: Vec<&str> = vec!["[", "]", "{", "}", "#", "%", "^", "*", "+", "="];
|
||||||
|
ui.columns(tl_0.len(), |columns| {
|
||||||
|
for (index, s) in tl_0.iter().enumerate() {
|
||||||
|
button_rect = self.input_button_ui(s, false, &mut columns[index]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let tl_1: Vec<&str> = vec!["_", "\\", "|", "~", "<", ">", "№", "√", "π", "•"];
|
||||||
|
ui.columns(tl_1.len(), |columns| {
|
||||||
|
for (index, s) in tl_1.iter().enumerate() {
|
||||||
|
self.input_button_ui(s, false, &mut columns[index]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let tl_2: Vec<&str> = vec!["-", "/", ":", ";", "(", ")", "`", "&", "@", "\""];
|
||||||
|
ui.columns(tl_2.len(), |columns| {
|
||||||
|
for (index, s) in tl_2.iter().enumerate() {
|
||||||
|
self.input_button_ui(s, false, &mut columns[index]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let tl_3: Vec<&str> = vec![".", ",", "?", "!", "€", "£", "¥", "$", "¢", BACKSPACE];
|
||||||
|
ui.columns(tl_3.len(), |columns| {
|
||||||
|
for (index, s) in tl_3.iter().enumerate() {
|
||||||
|
if index == tl_3.len() - 1 {
|
||||||
|
self.custom_button_ui(
|
||||||
|
BACKSPACE.to_string(),
|
||||||
|
Colors::red(),
|
||||||
|
Some(Colors::fill_lite()),
|
||||||
|
&mut columns[index],
|
||||||
|
|_, c| {
|
||||||
|
c.state.last_event = Arc::new(Some(KeyboardEvent::CLEAR));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
self.input_button_ui(s, false, &mut columns[index]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
button_rect
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw custom keyboard button.
|
||||||
|
fn custom_button_ui(
|
||||||
|
&mut self,
|
||||||
|
s: String,
|
||||||
|
color: Color32,
|
||||||
|
bg: Option<Color32>,
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
cb: impl FnOnce(String, &mut KeyboardContent),
|
||||||
|
) -> Response {
|
||||||
|
ui.vertical_centered_justified(|ui| {
|
||||||
|
// Disable expansion on click/hover.
|
||||||
|
ui.style_mut().visuals.widgets.hovered.expansion = 0.0;
|
||||||
|
ui.style_mut().visuals.widgets.active.expansion = 0.0;
|
||||||
|
// Setup fill colors.
|
||||||
|
ui.visuals_mut().widgets.inactive.weak_bg_fill = Colors::white_or_black(false);
|
||||||
|
ui.visuals_mut().widgets.hovered.weak_bg_fill = Colors::fill_lite();
|
||||||
|
ui.visuals_mut().widgets.active.weak_bg_fill = Colors::fill();
|
||||||
|
// Setup stroke colors.
|
||||||
|
ui.visuals_mut().widgets.inactive.bg_stroke = View::item_stroke();
|
||||||
|
ui.visuals_mut().widgets.hovered.bg_stroke = View::item_stroke();
|
||||||
|
ui.visuals_mut().widgets.active.bg_stroke = View::hover_stroke();
|
||||||
|
|
||||||
|
let shift = self.state.shift.load(Ordering::Relaxed);
|
||||||
|
let label = if shift {
|
||||||
|
s.to_uppercase()
|
||||||
|
} else {
|
||||||
|
s.to_string()
|
||||||
|
};
|
||||||
|
let mut button = Button::new(RichText::new(label.clone()).size(18.0).color(color))
|
||||||
|
.corner_radius(egui::CornerRadius::ZERO);
|
||||||
|
if let Some(bg) = bg {
|
||||||
|
button = button.fill(bg);
|
||||||
|
}
|
||||||
|
// Setup long press/touch.
|
||||||
|
let long_press = s == BACKSPACE;
|
||||||
|
if long_press {
|
||||||
|
button = button.sense(Sense::click_and_drag());
|
||||||
|
}
|
||||||
|
// Draw button.
|
||||||
|
let resp = button.ui(ui).on_hover_cursor(CursorIcon::PointingHand);
|
||||||
|
if resp.clicked() || resp.long_touched() || resp.dragged() {
|
||||||
|
cb(label, self);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.response
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw input button.
|
||||||
|
fn input_button_ui(&mut self, s: &str, translate: bool, ui: &mut egui::Ui) -> Rect {
|
||||||
|
let value = if translate {
|
||||||
|
t!(
|
||||||
|
format!("keyboard.{}", s),
|
||||||
|
locale = Self::input_locale().as_str()
|
||||||
|
)
|
||||||
|
.into()
|
||||||
|
} else {
|
||||||
|
s.to_string()
|
||||||
|
};
|
||||||
|
let rect = self
|
||||||
|
.custom_button_ui(value, Colors::text_button(), None, ui, |l, c| {
|
||||||
|
c.state.last_event = Arc::new(Some(KeyboardEvent::TEXT(l)));
|
||||||
|
c.state.shift.store(false, Ordering::Relaxed);
|
||||||
|
})
|
||||||
|
.rect;
|
||||||
|
rect
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get input locale.
|
||||||
|
fn input_locale() -> String {
|
||||||
|
let english = AppConfig::english_keyboard();
|
||||||
|
if english {
|
||||||
|
"en".to_string()
|
||||||
|
} else {
|
||||||
|
AppConfig::locale().unwrap_or("en".to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check last keyboard input event.
|
||||||
|
pub fn consume_event() -> Option<KeyboardEvent> {
|
||||||
|
let empty = {
|
||||||
|
let r_state = WINDOW_STATE.read();
|
||||||
|
r_state.last_event.is_none()
|
||||||
|
};
|
||||||
|
if !empty {
|
||||||
|
let mut w_state = WINDOW_STATE.write();
|
||||||
|
let event = w_state.last_event.as_ref().clone().unwrap();
|
||||||
|
w_state.last_event = Arc::new(None);
|
||||||
|
return Some(event);
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Emulate stop of Shift key press.
|
||||||
|
pub fn unshift() {
|
||||||
|
let r_state = WINDOW_STATE.read();
|
||||||
|
r_state.shift.store(false, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reset keyboard window state.
|
||||||
|
pub fn reset_window_state() {
|
||||||
|
let mut w_state = WINDOW_STATE.write();
|
||||||
|
w_state.layout = Arc::new(KeyboardLayout::TEXT);
|
||||||
|
// *w_state = KeyboardState::default();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
// Copyright 2025 The Grim Developers
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
mod types;
|
||||||
|
pub use types::*;
|
||||||
|
|
||||||
|
mod edit;
|
||||||
|
pub use edit::*;
|
||||||
|
|
||||||
|
mod keyboard;
|
||||||
|
pub use keyboard::*;
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
// Copyright 2025 The Grim Developers
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::sync::atomic::AtomicBool;
|
||||||
|
|
||||||
|
/// Software keyboard input type.
|
||||||
|
#[derive(Clone, PartialOrd, PartialEq)]
|
||||||
|
pub enum KeyboardLayout {
|
||||||
|
TEXT,
|
||||||
|
SYMBOLS,
|
||||||
|
NUMBERS,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Software keyboard input event.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub enum KeyboardEvent {
|
||||||
|
TEXT(String),
|
||||||
|
CLEAR,
|
||||||
|
ENTER,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Software keyboard Window State.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct KeyboardState {
|
||||||
|
/// Last input event.
|
||||||
|
pub last_event: Arc<Option<KeyboardEvent>>,
|
||||||
|
/// Current layout.
|
||||||
|
pub layout: Arc<KeyboardLayout>,
|
||||||
|
/// Flag to enter uppercase symbol first.
|
||||||
|
pub shift: Arc<AtomicBool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for KeyboardState {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
last_event: Arc::new(None),
|
||||||
|
layout: Arc::new(KeyboardLayout::TEXT),
|
||||||
|
shift: Arc::new(AtomicBool::new(false)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -27,7 +27,7 @@ mod content;
|
|||||||
pub use content::*;
|
pub use content::*;
|
||||||
|
|
||||||
pub mod network;
|
pub mod network;
|
||||||
|
pub mod settings;
|
||||||
pub mod wallets;
|
pub mod wallets;
|
||||||
|
|
||||||
mod camera;
|
mod camera;
|
||||||
@@ -43,4 +43,7 @@ mod pull_to_refresh;
|
|||||||
pub use pull_to_refresh::*;
|
pub use pull_to_refresh::*;
|
||||||
|
|
||||||
mod scan;
|
mod scan;
|
||||||
pub use scan::*;
|
pub use scan::*;
|
||||||
|
|
||||||
|
mod input;
|
||||||
|
pub use input::*;
|
||||||
|
|||||||
Regular → Executable
+323
-273
@@ -12,328 +12,378 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
use lazy_static::lazy_static;
|
|
||||||
use std::sync::Arc;
|
|
||||||
use parking_lot::RwLock;
|
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
|
||||||
use egui::{Align2, RichText, Rounding, Stroke, UiBuilder, Vec2};
|
|
||||||
use egui::epaint::{RectShape, Shadow};
|
use egui::epaint::{RectShape, Shadow};
|
||||||
use egui::os::OperatingSystem;
|
use egui::os::OperatingSystem;
|
||||||
|
use egui::{Align2, Color32, CornerRadius, RichText, Stroke, StrokeKind, UiBuilder, Vec2};
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
use parking_lot::RwLock;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
|
|
||||||
use crate::gui::Colors;
|
use crate::gui::Colors;
|
||||||
use crate::gui::views::{Content, View};
|
use crate::gui::platform::PlatformCallbacks;
|
||||||
use crate::gui::views::types::{ModalPosition, ModalState};
|
use crate::gui::views::types::{ModalPosition, ModalState};
|
||||||
|
use crate::gui::views::{Content, View};
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
/// Showing [`Modal`] state to be accessible from different ui parts.
|
/// Showing [`Modal`] state to be accessible from different ui parts.
|
||||||
static ref MODAL_STATE: Arc<RwLock<ModalState>> = Arc::new(RwLock::new(ModalState::default()));
|
static ref MODAL_STATE: Arc<RwLock<ModalState>> = Arc::new(RwLock::new(ModalState::default()));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Modal [`egui::Window`] container.
|
/// Modal [`egui::Window`] container.
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Modal {
|
pub struct Modal {
|
||||||
/// Identifier for modal.
|
/// Identifier for modal.
|
||||||
pub(crate) id: &'static str,
|
pub(crate) id: &'static str,
|
||||||
/// Position on the screen.
|
/// Position on the screen.
|
||||||
pub position: ModalPosition,
|
pub position: ModalPosition,
|
||||||
/// Flag to check if modal can be closed by keys.
|
/// Flag to check if modal can be closed by keys.
|
||||||
closeable: Arc<AtomicBool>,
|
closeable: Arc<AtomicBool>,
|
||||||
/// Title text.
|
/// Title text.
|
||||||
title: Option<String>,
|
title: Option<String>,
|
||||||
|
/// Flag to check first content render.
|
||||||
|
first_draw: Arc<AtomicBool>,
|
||||||
|
/// Background color.
|
||||||
|
fill: Option<Color32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Modal {
|
impl Modal {
|
||||||
/// Margin from [`Modal`] window at top/left/right.
|
/// Margin from [`Modal`] window at top/left/right.
|
||||||
const DEFAULT_MARGIN: f32 = 8.0;
|
const DEFAULT_MARGIN: f32 = 8.0;
|
||||||
/// Maximum width of the content.
|
/// Maximum width of the content.
|
||||||
const DEFAULT_WIDTH: f32 = Content::SIDE_PANEL_WIDTH - (2.0 * Self::DEFAULT_MARGIN);
|
const DEFAULT_WIDTH: f32 = Content::SIDE_PANEL_WIDTH - (2.0 * Self::DEFAULT_MARGIN);
|
||||||
|
/// Modal content [`egui::Window`] id.
|
||||||
|
pub const WINDOW_ID: &'static str = "modal_window";
|
||||||
|
|
||||||
/// Create closeable [`Modal`] with center position.
|
/// Create closeable [`Modal`] with center position.
|
||||||
pub fn new(id: &'static str) -> Self {
|
pub fn new(id: &'static str) -> Self {
|
||||||
Self {
|
Self {
|
||||||
id,
|
id,
|
||||||
position: ModalPosition::Center,
|
position: ModalPosition::Center,
|
||||||
closeable: Arc::new(AtomicBool::new(true)),
|
closeable: Arc::new(AtomicBool::new(true)),
|
||||||
title: None,
|
title: None,
|
||||||
}
|
first_draw: Arc::new(AtomicBool::new(true)),
|
||||||
}
|
fill: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Setup position of [`Modal`] on the screen.
|
/// Setup position of [`Modal`] on the screen.
|
||||||
pub fn position(mut self, position: ModalPosition) -> Self {
|
pub fn position(mut self, position: ModalPosition) -> Self {
|
||||||
self.position = position;
|
self.position = position;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Change [`Modal`] position on the screen.
|
/// Change [`Modal`] position on the screen.
|
||||||
pub fn change_position(position: ModalPosition) {
|
pub fn change_position(position: ModalPosition) {
|
||||||
let mut w_state = MODAL_STATE.write();
|
let mut w_state = MODAL_STATE.write();
|
||||||
w_state.modal.as_mut().unwrap().position = position;
|
w_state.modal.as_mut().unwrap().position = position;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Mark [`Modal`] closed.
|
/// Close [`Modal`] by clearing its state.
|
||||||
pub fn close(&self) {
|
pub fn close() {
|
||||||
let mut w_nav = MODAL_STATE.write();
|
let mut w_nav = MODAL_STATE.write();
|
||||||
w_nav.modal = None;
|
w_nav.modal = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Setup possibility to close [`Modal`].
|
/// Setup possibility to close [`Modal`].
|
||||||
pub fn closeable(self, closeable: bool) -> Self {
|
pub fn closeable(self, closeable: bool) -> Self {
|
||||||
self.closeable.store(closeable, Ordering::Relaxed);
|
self.closeable.store(closeable, Ordering::Relaxed);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Disable possibility to close [`Modal`].
|
/// Disable possibility to close [`Modal`].
|
||||||
pub fn disable_closing(&self) {
|
pub fn disable_closing(&self) {
|
||||||
self.closeable.store(false, Ordering::Relaxed);
|
self.closeable.store(false, Ordering::Relaxed);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Enable possibility to close [`Modal`].
|
/// Enable possibility to close [`Modal`].
|
||||||
pub fn enable_closing(&self) {
|
pub fn enable_closing(&self) {
|
||||||
self.closeable.store(true, Ordering::Relaxed);
|
self.closeable.store(true, Ordering::Relaxed);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if [`Modal`] is closeable.
|
/// Check if [`Modal`] is closeable.
|
||||||
pub fn is_closeable(&self) -> bool {
|
pub fn is_closeable(&self) -> bool {
|
||||||
self.closeable.load(Ordering::Relaxed)
|
self.closeable.load(Ordering::Relaxed)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set title text on [`Modal`] creation.
|
/// Set title text on [`Modal`] creation.
|
||||||
pub fn title(mut self, title: String) -> Self {
|
pub fn title(mut self, title: impl Into<String>) -> Self {
|
||||||
self.title = Some(title.to_uppercase());
|
self.title = Some(title.into().to_uppercase());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set [`Modal`] instance into state to show at ui.
|
/// Set [`Modal`] instance into state to show at ui.
|
||||||
pub fn show(self) {
|
pub fn show(self) {
|
||||||
let mut w_nav = MODAL_STATE.write();
|
let mut w_nav = MODAL_STATE.write();
|
||||||
w_nav.modal = Some(self);
|
self.first_draw.store(true, Ordering::Relaxed);
|
||||||
}
|
w_nav.modal = Some(self);
|
||||||
|
}
|
||||||
|
|
||||||
/// Remove [`Modal`] from [`ModalState`] if it's showing and can be closed.
|
/// Remove [`Modal`] from [`ModalState`] if it's showing and can be closed.
|
||||||
/// Return `false` if modal existed in state before call.
|
/// Return `false` if modal existed in state before call.
|
||||||
pub fn on_back() -> bool {
|
pub fn on_back() -> bool {
|
||||||
let mut w_state = MODAL_STATE.write();
|
if Self::opened().is_some() {
|
||||||
|
if Self::opened_closeable() {
|
||||||
|
Self::close();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
// If Modal is showing and closeable, remove it from state.
|
/// Return identifier of opened [`Modal`].
|
||||||
if w_state.modal.is_some() {
|
pub fn opened() -> Option<&'static str> {
|
||||||
let modal = w_state.modal.as_ref().unwrap();
|
// Check if modal is showing.
|
||||||
if modal.is_closeable() {
|
{
|
||||||
w_state.modal = None;
|
if MODAL_STATE.read().modal.is_none() {
|
||||||
}
|
return None;
|
||||||
return false;
|
}
|
||||||
}
|
}
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return identifier of opened [`Modal`].
|
// Get identifier of opened modal.
|
||||||
pub fn opened() -> Option<&'static str> {
|
let r_state = MODAL_STATE.read();
|
||||||
// Check if modal is showing.
|
let modal = r_state.modal.as_ref().unwrap();
|
||||||
{
|
Some(modal.id)
|
||||||
if MODAL_STATE.read().modal.is_none() {
|
}
|
||||||
return None;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get identifier of opened modal.
|
/// Check if [`Modal`] is opened and can be closed.
|
||||||
let r_state = MODAL_STATE.read();
|
pub fn opened_closeable() -> bool {
|
||||||
let modal = r_state.modal.as_ref().unwrap();
|
// Check if modal is showing.
|
||||||
Some(modal.id)
|
{
|
||||||
}
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
/// Check if [`Modal`] is opened and can be closed.
|
/// Set title text for current opened [`Modal`].
|
||||||
pub fn opened_closeable() -> bool {
|
pub fn set_title(title: impl Into<String>) {
|
||||||
// Check if modal is showing.
|
let mut w_state = MODAL_STATE.write();
|
||||||
{
|
if w_state.modal.is_some() {
|
||||||
if MODAL_STATE.read().modal.is_none() {
|
let mut modal = w_state.modal.clone().unwrap();
|
||||||
return false;
|
modal.title = Some(title.into().to_uppercase());
|
||||||
}
|
w_state.modal = Some(modal);
|
||||||
}
|
}
|
||||||
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`].
|
/// Check for first [`Modal`] content rendering.
|
||||||
pub fn set_title(title: String) {
|
pub fn first_draw() -> bool {
|
||||||
// Save state.
|
if Self::opened().is_none() {
|
||||||
let mut w_state = MODAL_STATE.write();
|
return false;
|
||||||
if w_state.modal.is_some() {
|
}
|
||||||
let mut modal = w_state.modal.clone().unwrap();
|
let r_state = MODAL_STATE.read();
|
||||||
modal.title = Some(title.to_uppercase());
|
let modal = r_state.modal.as_ref().unwrap();
|
||||||
w_state.modal = Some(modal);
|
modal.first_draw.load(Ordering::Relaxed)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/// Draw opened [`Modal`] content.
|
pub fn ui(
|
||||||
pub fn ui(ctx: &egui::Context, add_content: impl FnOnce(&mut egui::Ui, &Modal)) {
|
ctx: &egui::Context,
|
||||||
let has_modal = {
|
cb: &dyn PlatformCallbacks,
|
||||||
MODAL_STATE.read().modal.is_some()
|
add_content: impl FnOnce(&mut egui::Ui, &Modal, &dyn PlatformCallbacks),
|
||||||
};
|
) {
|
||||||
if has_modal {
|
let has_modal = { MODAL_STATE.read().modal.is_some() };
|
||||||
let modal = {
|
if has_modal {
|
||||||
let r_state = MODAL_STATE.read();
|
let modal = {
|
||||||
r_state.modal.clone().unwrap()
|
let r_state = MODAL_STATE.read();
|
||||||
};
|
r_state.modal.clone().unwrap()
|
||||||
modal.window_ui(ctx, add_content);
|
};
|
||||||
}
|
modal.window_ui(ctx, cb, add_content);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Draw [`egui::Window`] with provided content.
|
/// Draw [`egui::Window`] with provided content.
|
||||||
fn window_ui(&self, ctx: &egui::Context, add_content: impl FnOnce(&mut egui::Ui, &Modal)) {
|
fn window_ui(
|
||||||
let is_fullscreen = ctx.input(|i| {
|
&self,
|
||||||
i.viewport().fullscreen.unwrap_or(false)
|
ctx: &egui::Context,
|
||||||
});
|
cb: &dyn PlatformCallbacks,
|
||||||
|
add_content: impl FnOnce(&mut egui::Ui, &Modal, &dyn PlatformCallbacks),
|
||||||
|
) {
|
||||||
|
let is_fullscreen = ctx.input(|i| i.viewport().fullscreen.unwrap_or(false));
|
||||||
|
|
||||||
// Setup background rect.
|
// Setup background rect.
|
||||||
let bg_rect = if View::is_desktop() {
|
let is_win = OperatingSystem::Windows == OperatingSystem::from_target_os();
|
||||||
let mut r = ctx.screen_rect();
|
let bg_rect = if View::is_desktop() && !is_win {
|
||||||
let is_mac = OperatingSystem::Mac == OperatingSystem::from_target_os();
|
let mut r = ctx.content_rect();
|
||||||
if !is_mac && !is_fullscreen {
|
let is_mac = OperatingSystem::Mac == OperatingSystem::from_target_os();
|
||||||
r = r.shrink(Content::WINDOW_FRAME_MARGIN - 1.0);
|
if !is_mac && !is_fullscreen {
|
||||||
}
|
r = r.shrink(Content::WINDOW_FRAME_MARGIN - 1.0);
|
||||||
r.min.y += Content::WINDOW_TITLE_HEIGHT;
|
}
|
||||||
r
|
r.min.y += Content::WINDOW_TITLE_HEIGHT;
|
||||||
} else {
|
r
|
||||||
ctx.screen_rect()
|
} else {
|
||||||
};
|
ctx.content_rect()
|
||||||
|
};
|
||||||
|
|
||||||
// Draw modal background.
|
// Draw modal background.
|
||||||
egui::Window::new("modal_bg_window")
|
egui::Window::new("modal_bg_window")
|
||||||
.title_bar(false)
|
.title_bar(false)
|
||||||
.resizable(false)
|
.resizable(false)
|
||||||
.collapsible(false)
|
.collapsible(false)
|
||||||
.fixed_rect(bg_rect)
|
.fixed_rect(bg_rect)
|
||||||
.frame(egui::Frame {
|
.frame(egui::Frame {
|
||||||
fill: Colors::semi_transparent(),
|
fill: Colors::semi_transparent(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
})
|
})
|
||||||
.show(ctx, |ui| {
|
.show(ctx, |ui| {
|
||||||
ui.set_min_size(bg_rect.size());
|
ui.set_min_size(bg_rect.size());
|
||||||
});
|
});
|
||||||
|
|
||||||
// Setup width of modal content.
|
// Setup width of modal content.
|
||||||
let side_insets = View::get_left_inset() + View::get_right_inset();
|
let side_insets = View::get_left_inset() + View::get_right_inset();
|
||||||
let available_width = ctx.screen_rect().width() - (side_insets + Self::DEFAULT_MARGIN);
|
let available_width = ctx.content_rect().width() - (side_insets + Self::DEFAULT_MARGIN);
|
||||||
let width = f32::min(available_width, Self::DEFAULT_WIDTH);
|
let width = f32::min(available_width, Self::DEFAULT_WIDTH);
|
||||||
|
|
||||||
// Show main content window at given position.
|
// Show main content window at given position.
|
||||||
let (content_align, content_offset) = self.modal_position();
|
let (content_align, content_offset) = self.modal_position();
|
||||||
let layer_id = egui::Window::new("modal_window")
|
egui::Window::new(Self::WINDOW_ID)
|
||||||
.title_bar(false)
|
.title_bar(false)
|
||||||
.resizable(false)
|
.resizable(false)
|
||||||
.collapsible(false)
|
.collapsible(false)
|
||||||
.min_width(width)
|
.min_width(width)
|
||||||
.default_width(width)
|
.default_width(width)
|
||||||
.anchor(content_align, content_offset)
|
.anchor(content_align, content_offset)
|
||||||
.frame(egui::Frame {
|
.frame(egui::Frame {
|
||||||
shadow: Shadow {
|
shadow: Shadow {
|
||||||
offset: Default::default(),
|
offset: Default::default(),
|
||||||
blur: 30.0,
|
blur: 30.0 as u8,
|
||||||
spread: 3.0,
|
spread: 3.0 as u8,
|
||||||
color: egui::Color32::from_black_alpha(32),
|
color: egui::Color32::from_black_alpha(32),
|
||||||
},
|
},
|
||||||
rounding: Rounding::same(8.0),
|
corner_radius: CornerRadius::same(8.0 as u8),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
})
|
})
|
||||||
.show(ctx, |ui| {
|
.show(ctx, |ui| {
|
||||||
if let Some(title) = &self.title {
|
if let Some(title) = &self.title {
|
||||||
title_ui(title, ui);
|
title_ui(title, ui);
|
||||||
}
|
}
|
||||||
self.content_ui(ui, add_content);
|
self.content_ui(ui, cb, add_content);
|
||||||
}).unwrap().response.layer_id;
|
});
|
||||||
|
|
||||||
// Always show main content window above background window.
|
// Setup first draw flag.
|
||||||
ctx.move_to_top(layer_id);
|
if Self::first_draw() {
|
||||||
}
|
let r_state = MODAL_STATE.read();
|
||||||
|
let modal = r_state.modal.as_ref().unwrap();
|
||||||
|
modal.first_draw.store(false, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Get [`egui::Window`] position based on [`ModalPosition`].
|
/// Get [`egui::Window`] position based on [`ModalPosition`].
|
||||||
fn modal_position(&self) -> (Align2, Vec2) {
|
fn modal_position(&self) -> (Align2, Vec2) {
|
||||||
let align = match self.position {
|
let align = match self.position {
|
||||||
ModalPosition::CenterTop => Align2::CENTER_TOP,
|
ModalPosition::CenterTop => Align2::CENTER_TOP,
|
||||||
ModalPosition::Center => Align2::CENTER_CENTER
|
ModalPosition::Center => Align2::CENTER_CENTER,
|
||||||
};
|
};
|
||||||
|
|
||||||
let x_align = View::get_left_inset() - View::get_right_inset();
|
let x_align = View::get_left_inset() - View::get_right_inset();
|
||||||
let is_mac = OperatingSystem::Mac == OperatingSystem::from_target_os();
|
let is_mac = OperatingSystem::Mac == OperatingSystem::from_target_os();
|
||||||
let extra_y = if View::is_desktop() {
|
let is_win = OperatingSystem::Windows == OperatingSystem::from_target_os();
|
||||||
Content::WINDOW_TITLE_HEIGHT + if !is_mac {
|
let extra_y = if View::is_desktop() && !is_win {
|
||||||
Content::WINDOW_FRAME_MARGIN
|
Content::WINDOW_TITLE_HEIGHT
|
||||||
} else {
|
+ if !is_mac {
|
||||||
0.0
|
Content::WINDOW_FRAME_MARGIN
|
||||||
}
|
} else {
|
||||||
} else {
|
0.0
|
||||||
0.0
|
}
|
||||||
};
|
} else {
|
||||||
let y_align = View::get_top_inset() + Self::DEFAULT_MARGIN / 2.0 + extra_y;
|
0.0
|
||||||
|
};
|
||||||
|
let y_align = View::get_top_inset() + Self::DEFAULT_MARGIN / 2.0 + extra_y;
|
||||||
|
|
||||||
let offset = match self.position {
|
let offset = match self.position {
|
||||||
ModalPosition::CenterTop => Vec2::new(x_align, y_align),
|
ModalPosition::CenterTop => Vec2::new(x_align, y_align),
|
||||||
ModalPosition::Center => Vec2::new(x_align, 0.0)
|
ModalPosition::Center => Vec2::new(x_align, 0.0),
|
||||||
};
|
};
|
||||||
(align, offset)
|
(align, offset)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Draw provided content.
|
/// Set custom background color.
|
||||||
fn content_ui(&self, ui: &mut egui::Ui, add_content: impl FnOnce(&mut egui::Ui, &Modal)) {
|
pub fn set_background_color(&self, color: Color32) {
|
||||||
let mut rect = ui.available_rect_before_wrap();
|
let mut w_state = MODAL_STATE.write();
|
||||||
|
w_state.modal.as_mut().unwrap().fill = Some(color);
|
||||||
|
}
|
||||||
|
|
||||||
// Create background shape.
|
/// Draw provided content.
|
||||||
let mut bg_shape = RectShape::new(rect, if self.title.is_none() {
|
fn content_ui(
|
||||||
Rounding::same(8.0)
|
&self,
|
||||||
} else {
|
ui: &mut egui::Ui,
|
||||||
Rounding {
|
cb: &dyn PlatformCallbacks,
|
||||||
nw: 0.0,
|
add_content: impl FnOnce(&mut egui::Ui, &Modal, &dyn PlatformCallbacks),
|
||||||
ne: 0.0,
|
) {
|
||||||
sw: 8.0,
|
let mut rect = ui.available_rect_before_wrap();
|
||||||
se: 8.0,
|
|
||||||
}
|
|
||||||
}, Colors::fill(), Stroke::NONE);
|
|
||||||
let bg_idx = ui.painter().add(bg_shape);
|
|
||||||
|
|
||||||
rect.min += egui::emath::vec2(6.0, 0.0);
|
// Create background shape.
|
||||||
rect.max -= egui::emath::vec2(6.0, 0.0);
|
let mut bg_shape = RectShape::new(
|
||||||
let resp = ui.allocate_new_ui(UiBuilder::new().max_rect(rect), |ui| {
|
rect,
|
||||||
(add_content)(ui, self);
|
if self.title.is_none() {
|
||||||
}).response;
|
CornerRadius::same(8.0 as u8)
|
||||||
|
} else {
|
||||||
|
CornerRadius {
|
||||||
|
nw: 0.0 as u8,
|
||||||
|
ne: 0.0 as u8,
|
||||||
|
sw: 8.0 as u8,
|
||||||
|
se: 8.0 as u8,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
self.fill.unwrap_or(Colors::fill_lite()),
|
||||||
|
Stroke::NONE,
|
||||||
|
StrokeKind::Outside,
|
||||||
|
);
|
||||||
|
let bg_idx = ui.painter().add(bg_shape.clone());
|
||||||
|
|
||||||
// Setup background size.
|
rect.min += egui::emath::vec2(6.0, 0.0);
|
||||||
let bg_rect = {
|
rect.max -= egui::emath::vec2(6.0, 0.0);
|
||||||
let mut r = resp.rect.clone();
|
let resp = ui
|
||||||
r.min -= egui::emath::vec2(6.0, 0.0);
|
.scope_builder(UiBuilder::new().max_rect(rect), |ui| {
|
||||||
r.max += egui::emath::vec2(6.0, 0.0);
|
(add_content)(ui, self, cb);
|
||||||
r
|
})
|
||||||
};
|
.response;
|
||||||
bg_shape.rect = bg_rect;
|
|
||||||
ui.painter().set(bg_idx, bg_shape);
|
// 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.
|
/// Draw title content.
|
||||||
fn title_ui(title: &String, ui: &mut egui::Ui) {
|
fn title_ui(title: &String, ui: &mut egui::Ui) {
|
||||||
let rect = ui.available_rect_before_wrap();
|
let rect = ui.available_rect_before_wrap();
|
||||||
|
|
||||||
// Create background shape.
|
// Create background shape.
|
||||||
let mut bg_shape = RectShape::new(rect, Rounding {
|
let mut bg_shape = RectShape::new(
|
||||||
nw: 8.0,
|
rect,
|
||||||
ne: 8.0,
|
CornerRadius {
|
||||||
sw: 0.0,
|
nw: 8.0 as u8,
|
||||||
se: 0.0,
|
ne: 8.0 as u8,
|
||||||
}, Colors::yellow(), Stroke::NONE);
|
sw: 0.0 as u8,
|
||||||
let bg_idx = ui.painter().add(bg_shape);
|
se: 0.0 as u8,
|
||||||
|
},
|
||||||
|
Colors::yellow(),
|
||||||
|
Stroke::NONE,
|
||||||
|
StrokeKind::Outside,
|
||||||
|
);
|
||||||
|
let bg_idx = ui.painter().add(bg_shape.clone());
|
||||||
|
|
||||||
// Draw title content.
|
// Draw title content.
|
||||||
let resp = ui.vertical_centered(|ui| {
|
let resp = ui
|
||||||
ui.add_space(Modal::DEFAULT_MARGIN + 2.0);
|
.vertical_centered(|ui| {
|
||||||
ui.label(RichText::new(title)
|
ui.add_space(Modal::DEFAULT_MARGIN + 2.0);
|
||||||
.size(19.0)
|
ui.label(RichText::new(title).size(19.0).color(Colors::title(true)));
|
||||||
.color(Colors::title(true))
|
ui.add_space(Modal::DEFAULT_MARGIN + 1.0);
|
||||||
);
|
// Draw line below title.
|
||||||
ui.add_space(Modal::DEFAULT_MARGIN + 1.0);
|
View::horizontal_line(ui, Colors::item_stroke());
|
||||||
// Draw line below title.
|
})
|
||||||
View::horizontal_line(ui, Colors::item_stroke());
|
.response;
|
||||||
}).response;
|
|
||||||
|
|
||||||
// Setup background size.
|
// Setup background size.
|
||||||
bg_shape.rect = resp.rect;
|
bg_shape.rect = resp.rect;
|
||||||
ui.painter().set(bg_idx, bg_shape);
|
ui.painter().set(bg_idx, bg_shape);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,237 +12,385 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
use egui::{Align, Layout, RichText, Rounding};
|
use eframe::epaint::RectShape;
|
||||||
|
use egui::{
|
||||||
|
Align, Color32, CornerRadius, CursorIcon, Layout, RichText, Sense, StrokeKind, UiBuilder,
|
||||||
|
};
|
||||||
|
|
||||||
use crate::AppConfig;
|
use crate::AppConfig;
|
||||||
use crate::gui::Colors;
|
use crate::gui::Colors;
|
||||||
use crate::gui::icons::{CARET_RIGHT, CHECK_CIRCLE, COMPUTER_TOWER, DOTS_THREE_CIRCLE, GLOBE_SIMPLE, PENCIL, PLUS_CIRCLE, POWER, TRASH, WARNING_CIRCLE, X_CIRCLE};
|
use crate::gui::icons::{
|
||||||
|
CHECK_CIRCLE, COMPUTER_TOWER, DOTS_THREE_CIRCLE, GLOBE_SIMPLE, PLUS_CIRCLE, POWER, QR_CODE,
|
||||||
|
TRASH, WARNING_CIRCLE, X_CIRCLE,
|
||||||
|
};
|
||||||
use crate::gui::platform::PlatformCallbacks;
|
use crate::gui::platform::PlatformCallbacks;
|
||||||
use crate::gui::views::{Modal, View};
|
|
||||||
use crate::gui::views::network::modals::ExternalConnectionModal;
|
|
||||||
use crate::gui::views::network::NodeSetup;
|
use crate::gui::views::network::NodeSetup;
|
||||||
use crate::gui::views::types::{ModalContainer, ModalPosition};
|
use crate::gui::views::network::modals::{ExternalConnectionModal, ShareConnectionContent};
|
||||||
|
use crate::gui::views::network::types::ShareConnection;
|
||||||
|
use crate::gui::views::types::{ContentContainer, ModalPosition};
|
||||||
|
use crate::gui::views::{Modal, View};
|
||||||
use crate::node::{Node, NodeConfig};
|
use crate::node::{Node, NodeConfig};
|
||||||
use crate::wallet::{ConnectionsConfig, ExternalConnection};
|
use crate::wallet::{ConnectionsConfig, ExternalConnection};
|
||||||
|
|
||||||
/// Network connections content.
|
/// Network connections content.
|
||||||
pub struct ConnectionsContent {
|
pub struct ConnectionsContent {
|
||||||
/// External connection [`Modal`] content.
|
/// Flag to check connections state on first draw.
|
||||||
ext_conn_modal: ExternalConnectionModal,
|
first_draw: bool,
|
||||||
|
|
||||||
/// [`Modal`] identifiers allowed at this ui container.
|
/// External connection [`Modal`] content.
|
||||||
modal_ids: Vec<&'static str>
|
ext_conn_modal_content: ExternalConnectionModal,
|
||||||
|
|
||||||
|
/// [`Modal`] content to share connection with QR code.
|
||||||
|
share_conn_modal_content: Option<ShareConnectionContent>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for ConnectionsContent {
|
impl Default for ConnectionsContent {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
ext_conn_modal: ExternalConnectionModal::new(None),
|
first_draw: true,
|
||||||
modal_ids: vec![
|
ext_conn_modal_content: ExternalConnectionModal::new(None),
|
||||||
ExternalConnectionModal::NETWORK_ID
|
share_conn_modal_content: None,
|
||||||
],
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ModalContainer for ConnectionsContent {
|
/// Identifier for [`Modal`] to share connection.
|
||||||
fn modal_ids(&self) -> &Vec<&'static str> {
|
const SHARE_CONN_QR_MODAL: &'static str = "share_conn_qr_modal";
|
||||||
&self.modal_ids
|
|
||||||
}
|
|
||||||
|
|
||||||
fn modal_ui(&mut self,
|
impl ContentContainer for ConnectionsContent {
|
||||||
ui: &mut egui::Ui,
|
fn modal_ids(&self) -> Vec<&'static str> {
|
||||||
modal: &Modal,
|
vec![ExternalConnectionModal::NETWORK_ID, SHARE_CONN_QR_MODAL]
|
||||||
cb: &dyn PlatformCallbacks) {
|
}
|
||||||
match modal.id {
|
|
||||||
ExternalConnectionModal::NETWORK_ID => {
|
fn modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
|
||||||
self.ext_conn_modal.ui(ui, cb, modal, |_| {});
|
match modal.id {
|
||||||
},
|
ExternalConnectionModal::NETWORK_ID => {
|
||||||
_ => {}
|
self.ext_conn_modal_content.ui(ui, cb, modal, |_| {});
|
||||||
}
|
}
|
||||||
}
|
SHARE_CONN_QR_MODAL => {
|
||||||
|
if let Some(c) = self.share_conn_modal_content.as_mut() {
|
||||||
|
c.ui(ui, modal, cb);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn container_ui(&mut self, ui: &mut egui::Ui, _: &dyn PlatformCallbacks) {
|
||||||
|
// Check connections state on first draw.
|
||||||
|
if self.first_draw {
|
||||||
|
ExternalConnection::check(None, ui.ctx());
|
||||||
|
self.first_draw = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.add_space(2.0);
|
||||||
|
|
||||||
|
// Show network type selection.
|
||||||
|
let saved_chain_type = AppConfig::chain_type();
|
||||||
|
NodeSetup::chain_type_ui(ui);
|
||||||
|
ui.add_space(6.0);
|
||||||
|
|
||||||
|
// Check connections availability.
|
||||||
|
if saved_chain_type != AppConfig::chain_type() {
|
||||||
|
ExternalConnection::check(None, ui.ctx());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show integrated node info content.
|
||||||
|
Self::integrated_node_item_ui(
|
||||||
|
ui,
|
||||||
|
Colors::fill_lite(),
|
||||||
|
(true, || {
|
||||||
|
AppConfig::toggle_show_connections_network_panel();
|
||||||
|
}),
|
||||||
|
|ui| {
|
||||||
|
let r = View::item_rounding(0, 1, true);
|
||||||
|
View::item_button(ui, r, QR_CODE, None, || {
|
||||||
|
let (api_address, api_port) = NodeConfig::get_api_address();
|
||||||
|
if let Ok(c) = ShareConnectionContent::new(ShareConnection {
|
||||||
|
url: format!("http://{}:{}", api_address, api_port),
|
||||||
|
username: "grin".to_string(),
|
||||||
|
secret: NodeConfig::get_api_secret(true).unwrap_or("".to_string()),
|
||||||
|
}) {
|
||||||
|
self.share_conn_modal_content = Some(c);
|
||||||
|
// Show QR code to share integrated node connection.
|
||||||
|
Modal::new(SHARE_CONN_QR_MODAL)
|
||||||
|
.position(ModalPosition::Center)
|
||||||
|
.title(t!("network.node"))
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
true
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Show external connections.
|
||||||
|
ui.add_space(8.0);
|
||||||
|
ui.label(
|
||||||
|
RichText::new(t!("wallets.ext_conn"))
|
||||||
|
.size(16.0)
|
||||||
|
.color(Colors::gray()),
|
||||||
|
);
|
||||||
|
ui.add_space(6.0);
|
||||||
|
|
||||||
|
// Show button to add new external node connection.
|
||||||
|
let add_node_text = format!("{} {}", PLUS_CIRCLE, t!("wallets.add_node"));
|
||||||
|
View::button(ui, add_node_text, Colors::white_or_black(false), || {
|
||||||
|
self.show_add_ext_conn_modal(None);
|
||||||
|
});
|
||||||
|
|
||||||
|
ui.add_space(4.0);
|
||||||
|
|
||||||
|
let ext_conn_list = ConnectionsConfig::ext_conn_list();
|
||||||
|
let len = ext_conn_list.len();
|
||||||
|
if len != 0 {
|
||||||
|
ui.add_space(8.0);
|
||||||
|
for (i, c) in ext_conn_list.iter().enumerate() {
|
||||||
|
ui.horizontal_wrapped(|ui| {
|
||||||
|
let mut show_qr_content: Option<ShareConnectionContent> = None;
|
||||||
|
// Draw external connection list item.
|
||||||
|
let bg = Colors::fill_lite();
|
||||||
|
Self::ext_conn_item_ui(
|
||||||
|
ui,
|
||||||
|
bg,
|
||||||
|
c,
|
||||||
|
i,
|
||||||
|
len,
|
||||||
|
(true, || {
|
||||||
|
self.show_add_ext_conn_modal(Some(c.clone()));
|
||||||
|
}),
|
||||||
|
|ui| {
|
||||||
|
// Draw button to delete connection.
|
||||||
|
let r = View::item_rounding(i, len, true);
|
||||||
|
View::item_button(ui, r, TRASH, Some(Colors::inactive_text()), || {
|
||||||
|
ConnectionsConfig::remove_ext_conn(c.id);
|
||||||
|
});
|
||||||
|
// Draw button to share connection
|
||||||
|
let r = CornerRadius::default();
|
||||||
|
View::item_button(ui, r, QR_CODE, None, || {
|
||||||
|
if let Ok(c) = ShareConnectionContent::new(ShareConnection {
|
||||||
|
url: c.url.clone(),
|
||||||
|
username: "grin".to_string(),
|
||||||
|
secret: c.secret.clone().unwrap_or("".to_string()),
|
||||||
|
}) {
|
||||||
|
show_qr_content = Some(c);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if let Some(c) = show_qr_content {
|
||||||
|
self.share_conn_modal_content = Some(c);
|
||||||
|
// Show QR code to share external connection.
|
||||||
|
Modal::new(SHARE_CONN_QR_MODAL)
|
||||||
|
.position(ModalPosition::Center)
|
||||||
|
.title(t!("wallets.ext_conn").replace(":", ""))
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ConnectionsContent {
|
impl ConnectionsContent {
|
||||||
/// Draw connections content.
|
/// Draw integrated node connection item content.
|
||||||
pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
pub fn integrated_node_item_ui(
|
||||||
self.current_modal_ui(ui, cb);
|
ui: &mut egui::Ui,
|
||||||
|
bg: Color32,
|
||||||
|
on_click: (bool, impl FnOnce()),
|
||||||
|
custom_button: impl FnOnce(&mut egui::Ui) -> bool,
|
||||||
|
) {
|
||||||
|
// Draw round background.
|
||||||
|
let mut rect = ui.available_rect_before_wrap();
|
||||||
|
rect.set_height(78.0);
|
||||||
|
let r = View::item_rounding(0, 1, false);
|
||||||
|
let mut bg_shape = RectShape::new(rect, r, bg, View::item_stroke(), StrokeKind::Outside);
|
||||||
|
let bg_idx = ui.painter().add(bg_shape.clone());
|
||||||
|
|
||||||
ui.add_space(2.0);
|
let res = ui
|
||||||
|
.scope_builder(
|
||||||
|
UiBuilder::new()
|
||||||
|
.sense(Sense::click())
|
||||||
|
.layout(Layout::right_to_left(Align::Center))
|
||||||
|
.max_rect(rect),
|
||||||
|
|ui| {
|
||||||
|
// Draw custom button.
|
||||||
|
let extra_button = custom_button(ui);
|
||||||
|
|
||||||
// Show network type selection.
|
// Draw buttons to start/stop node.
|
||||||
let saved_chain_type = AppConfig::chain_type();
|
if Node::get_error().is_none() {
|
||||||
NodeSetup::chain_type_ui(ui);
|
let rounding = if extra_button {
|
||||||
ui.add_space(6.0);
|
CornerRadius::default()
|
||||||
|
} else {
|
||||||
|
View::item_rounding(0, 1, true)
|
||||||
|
};
|
||||||
|
if !Node::is_running() {
|
||||||
|
View::item_button(ui, rounding, POWER, Some(Colors::green()), || {
|
||||||
|
Node::start();
|
||||||
|
});
|
||||||
|
} else if !Node::is_starting()
|
||||||
|
&& !Node::is_stopping()
|
||||||
|
&& !Node::is_restarting()
|
||||||
|
{
|
||||||
|
View::item_button(ui, rounding, POWER, Some(Colors::red()), || {
|
||||||
|
Node::stop(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Check connections availability.
|
let layout_size = ui.available_size();
|
||||||
if saved_chain_type != AppConfig::chain_type() {
|
ui.allocate_ui_with_layout(
|
||||||
ExternalConnection::check(None, ui.ctx());
|
layout_size,
|
||||||
}
|
Layout::left_to_right(Align::Center),
|
||||||
|
|ui| {
|
||||||
|
ui.add_space(6.0);
|
||||||
|
ui.vertical(|ui| {
|
||||||
|
ui.add_space(3.0);
|
||||||
|
ui.with_layout(Layout::left_to_right(Align::Min), |ui| {
|
||||||
|
ui.add_space(1.0);
|
||||||
|
ui.label(
|
||||||
|
RichText::new(t!("network.node"))
|
||||||
|
.size(18.0)
|
||||||
|
.color(Colors::title(false)),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
// Show integrated node info content.
|
// Setup node status text.
|
||||||
Self::integrated_node_item_ui(ui, |ui| {
|
let has_error = Node::get_error().is_some();
|
||||||
// Draw button to show integrated node info.
|
let status_icon = if has_error {
|
||||||
View::item_button(ui, View::item_rounding(0, 1, true), CARET_RIGHT, None, || {
|
WARNING_CIRCLE
|
||||||
AppConfig::toggle_show_connections_network_panel();
|
} else if !Node::is_running() {
|
||||||
});
|
X_CIRCLE
|
||||||
});
|
} else if Node::not_syncing() {
|
||||||
|
CHECK_CIRCLE
|
||||||
|
} else {
|
||||||
|
DOTS_THREE_CIRCLE
|
||||||
|
};
|
||||||
|
let status_text = format!(
|
||||||
|
"{} {}",
|
||||||
|
status_icon,
|
||||||
|
if has_error {
|
||||||
|
t!("error").into()
|
||||||
|
} else {
|
||||||
|
Node::get_sync_status_text()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
View::ellipsize_text(ui, status_text, 15.0, Colors::text(false));
|
||||||
|
ui.add_space(1.0);
|
||||||
|
|
||||||
// Show external connections.
|
// Setup node API address text.
|
||||||
ui.add_space(8.0);
|
let (api_address, api_port) = NodeConfig::get_api_address();
|
||||||
ui.label(RichText::new(t!("wallets.ext_conn")).size(16.0).color(Colors::gray()));
|
let address_text = format!(
|
||||||
ui.add_space(6.0);
|
"{} http://{}:{}",
|
||||||
|
COMPUTER_TOWER, api_address, api_port
|
||||||
|
);
|
||||||
|
ui.label(
|
||||||
|
RichText::new(address_text).size(15.0).color(Colors::gray()),
|
||||||
|
);
|
||||||
|
})
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.response;
|
||||||
|
let (clickable, on_click) = on_click;
|
||||||
|
let clicked = res.clicked() || res.long_touched();
|
||||||
|
// Setup background and cursor.
|
||||||
|
if clickable && res.hovered() {
|
||||||
|
res.on_hover_cursor(CursorIcon::PointingHand);
|
||||||
|
bg_shape.fill = Colors::fill();
|
||||||
|
}
|
||||||
|
ui.painter().set(bg_idx, bg_shape);
|
||||||
|
// Handle clicks on layout.
|
||||||
|
if clicked && clickable {
|
||||||
|
on_click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Show button to add new external node connection.
|
/// Draw external connection item content.
|
||||||
let add_node_text = format!("{} {}", PLUS_CIRCLE, t!("wallets.add_node"));
|
pub fn ext_conn_item_ui(
|
||||||
View::button(ui, add_node_text, Colors::white_or_black(false), || {
|
ui: &mut egui::Ui,
|
||||||
self.show_add_ext_conn_modal(None, cb);
|
bg: Color32,
|
||||||
});
|
conn: &ExternalConnection,
|
||||||
|
index: usize,
|
||||||
|
len: usize,
|
||||||
|
on_click: (bool, impl FnOnce()),
|
||||||
|
custom_button: impl FnOnce(&mut egui::Ui),
|
||||||
|
) {
|
||||||
|
// Draw round background.
|
||||||
|
let mut rect = ui.available_rect_before_wrap();
|
||||||
|
rect.set_height(52.0);
|
||||||
|
let r = View::item_rounding(index, len, false);
|
||||||
|
let mut bg_shape = RectShape::new(rect, r, bg, View::item_stroke(), StrokeKind::Outside);
|
||||||
|
let bg_idx = ui.painter().add(bg_shape.clone());
|
||||||
|
|
||||||
ui.add_space(4.0);
|
let res = ui
|
||||||
|
.scope_builder(
|
||||||
|
UiBuilder::new()
|
||||||
|
.sense(Sense::click())
|
||||||
|
.layout(Layout::right_to_left(Align::Center))
|
||||||
|
.max_rect(rect),
|
||||||
|
|ui| {
|
||||||
|
// Draw custom button.
|
||||||
|
custom_button(ui);
|
||||||
|
|
||||||
let ext_conn_list = ConnectionsConfig::ext_conn_list();
|
let layout_size = ui.available_size();
|
||||||
let ext_conn_size = ext_conn_list.len();
|
ui.allocate_ui_with_layout(
|
||||||
if ext_conn_size != 0 {
|
layout_size,
|
||||||
ui.add_space(8.0);
|
Layout::left_to_right(Align::Center),
|
||||||
for (index, conn) in ext_conn_list.iter().filter(|c| !c.deleted).enumerate() {
|
|ui| {
|
||||||
ui.horizontal_wrapped(|ui| {
|
ui.add_space(6.0);
|
||||||
// Draw connection list item.
|
ui.vertical(|ui| {
|
||||||
Self::ext_conn_item_ui(ui, conn, index, ext_conn_size, |ui| {
|
// Draw connections URL.
|
||||||
let button_rounding = View::item_rounding(index, ext_conn_size, true);
|
ui.add_space(4.0);
|
||||||
View::item_button(ui, button_rounding, TRASH, None, || {
|
let conn_text = format!("{} {}", GLOBE_SIMPLE, conn.url);
|
||||||
ConnectionsConfig::remove_ext_conn(conn.id);
|
View::ellipsize_text(ui, conn_text, 15.0, Colors::title(false));
|
||||||
});
|
ui.add_space(1.0);
|
||||||
View::item_button(ui, Rounding::default(), PENCIL, None, || {
|
|
||||||
self.show_add_ext_conn_modal(Some(conn.clone()), cb);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Draw integrated node connection item content.
|
// Setup connection status text.
|
||||||
pub fn integrated_node_item_ui(ui: &mut egui::Ui, custom_button: impl FnOnce(&mut egui::Ui)) {
|
let status_text = if let Some(available) = conn.available {
|
||||||
// Draw round background.
|
if available {
|
||||||
let mut rect = ui.available_rect_before_wrap();
|
format!("{} {}", CHECK_CIRCLE, t!("network.available"))
|
||||||
rect.set_height(78.0);
|
} else {
|
||||||
let rounding = View::item_rounding(0, 1, false);
|
format!("{} {}", X_CIRCLE, t!("network.not_available"))
|
||||||
ui.painter().rect(rect, rounding, Colors::fill(), View::item_stroke());
|
}
|
||||||
|
} 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);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.response;
|
||||||
|
let (clickable, on_click) = on_click;
|
||||||
|
let clicked = res.clicked() || res.long_touched();
|
||||||
|
// Setup background and cursor.
|
||||||
|
if clickable && res.hovered() {
|
||||||
|
res.on_hover_cursor(CursorIcon::PointingHand);
|
||||||
|
bg_shape.fill = Colors::fill();
|
||||||
|
}
|
||||||
|
ui.painter().set(bg_idx, bg_shape);
|
||||||
|
// Handle clicks on layout.
|
||||||
|
if clicked && clickable {
|
||||||
|
on_click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
|
/// Show [`Modal`] to add external connection.
|
||||||
// Draw custom button.
|
pub fn show_add_ext_conn_modal(&mut self, conn: Option<ExternalConnection>) {
|
||||||
custom_button(ui);
|
self.ext_conn_modal_content = ExternalConnectionModal::new(conn);
|
||||||
|
// Show modal.
|
||||||
// Draw buttons to start/stop node.
|
Modal::new(ExternalConnectionModal::NETWORK_ID)
|
||||||
if Node::get_error().is_none() {
|
.position(ModalPosition::CenterTop)
|
||||||
if !Node::is_running() {
|
.title(t!("wallets.add_node"))
|
||||||
View::item_button(ui, Rounding::default(), POWER, Some(Colors::green()), || {
|
.show();
|
||||||
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();
|
|
||||||
ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| {
|
|
||||||
ui.add_space(6.0);
|
|
||||||
ui.vertical(|ui| {
|
|
||||||
ui.add_space(3.0);
|
|
||||||
ui.with_layout(Layout::left_to_right(Align::Min), |ui| {
|
|
||||||
ui.add_space(1.0);
|
|
||||||
ui.label(RichText::new(t!("network.node"))
|
|
||||||
.size(18.0)
|
|
||||||
.color(Colors::title(false)));
|
|
||||||
});
|
|
||||||
|
|
||||||
// Setup node status text.
|
|
||||||
let has_error = Node::get_error().is_some();
|
|
||||||
let status_icon = if has_error {
|
|
||||||
WARNING_CIRCLE
|
|
||||||
} else if !Node::is_running() {
|
|
||||||
X_CIRCLE
|
|
||||||
} else if Node::not_syncing() {
|
|
||||||
CHECK_CIRCLE
|
|
||||||
} else {
|
|
||||||
DOTS_THREE_CIRCLE
|
|
||||||
};
|
|
||||||
let status_text = format!("{} {}", status_icon, if has_error {
|
|
||||||
t!("error")
|
|
||||||
} else {
|
|
||||||
Node::get_sync_status_text()
|
|
||||||
});
|
|
||||||
View::ellipsize_text(ui, status_text, 15.0, Colors::text(false));
|
|
||||||
ui.add_space(1.0);
|
|
||||||
|
|
||||||
// Setup node API address text.
|
|
||||||
let api_address = NodeConfig::get_api_address();
|
|
||||||
let address_text = format!("{} http://{}", COMPUTER_TOWER, api_address);
|
|
||||||
ui.label(RichText::new(address_text).size(15.0).color(Colors::gray()));
|
|
||||||
})
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Draw external connection item content.
|
|
||||||
pub fn ext_conn_item_ui(ui: &mut egui::Ui,
|
|
||||||
conn: &ExternalConnection,
|
|
||||||
index: usize,
|
|
||||||
len: usize,
|
|
||||||
buttons_ui: impl FnOnce(&mut egui::Ui)) {
|
|
||||||
// Setup layout size.
|
|
||||||
let mut rect = ui.available_rect_before_wrap();
|
|
||||||
rect.set_height(52.0);
|
|
||||||
|
|
||||||
// Draw round background.
|
|
||||||
let bg_rect = rect.clone();
|
|
||||||
let item_rounding = View::item_rounding(index, len, false);
|
|
||||||
ui.painter().rect(bg_rect, item_rounding, Colors::fill(), View::item_stroke());
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
// Setup connection status text.
|
|
||||||
let status_text = if let Some(available) = conn.available {
|
|
||||||
if available {
|
|
||||||
format!("{} {}", CHECK_CIRCLE, t!("network.available"))
|
|
||||||
} else {
|
|
||||||
format!("{} {}", X_CIRCLE, t!("network.not_available"))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
format!("{} {}", DOTS_THREE_CIRCLE, t!("network.availability_check"))
|
|
||||||
};
|
|
||||||
ui.label(RichText::new(status_text).size(15.0).color(Colors::gray()));
|
|
||||||
ui.add_space(3.0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Show [`Modal`] to add external connection.
|
|
||||||
pub fn show_add_ext_conn_modal(&mut self,
|
|
||||||
conn: Option<ExternalConnection>,
|
|
||||||
cb: &dyn PlatformCallbacks) {
|
|
||||||
self.ext_conn_modal = ExternalConnectionModal::new(conn);
|
|
||||||
// Show modal.
|
|
||||||
Modal::new(ExternalConnectionModal::NETWORK_ID)
|
|
||||||
.position(ModalPosition::CenterTop)
|
|
||||||
.title(t!("wallets.add_node"))
|
|
||||||
.show();
|
|
||||||
cb.show_keyboard();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
+396
-333
@@ -12,372 +12,435 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
use egui::{Id, Margin, RichText, ScrollArea};
|
|
||||||
use egui::scroll_area::ScrollBarVisibility;
|
use egui::scroll_area::ScrollBarVisibility;
|
||||||
|
use egui::{Id, Margin, RichText, ScrollArea};
|
||||||
|
|
||||||
use crate::AppConfig;
|
use crate::AppConfig;
|
||||||
use crate::gui::Colors;
|
use crate::gui::Colors;
|
||||||
use crate::gui::icons::{ARROWS_COUNTER_CLOCKWISE, BRIEFCASE, DATABASE, DOTS_THREE_OUTLINE_VERTICAL, FACTORY, FADERS, GAUGE, POWER};
|
use crate::gui::icons::{
|
||||||
|
ARROW_LEFT, ARROWS_COUNTER_CLOCKWISE, BRIEFCASE, DATABASE, DOTS_THREE_OUTLINE_VERTICAL,
|
||||||
|
FACTORY, FADERS, GAUGE, GEAR, GLOBE, POWER,
|
||||||
|
};
|
||||||
use crate::gui::platform::PlatformCallbacks;
|
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::{NodeTab, NodeTabType};
|
use crate::gui::views::network::types::{NodeTab, NodeTabType};
|
||||||
use crate::gui::views::types::{LinePosition, TitleContentType, TitleType};
|
use crate::gui::views::network::{
|
||||||
|
ConnectionsContent, NetworkMetrics, NetworkMining, NetworkNode, NetworkSettings,
|
||||||
|
};
|
||||||
|
use crate::gui::views::settings::SettingsContent;
|
||||||
|
use crate::gui::views::types::{ContentContainer, LinePosition, TitleContentType, TitleType};
|
||||||
|
use crate::gui::views::{Content, TitlePanel, View};
|
||||||
use crate::node::{Node, NodeConfig, NodeError};
|
use crate::node::{Node, NodeConfig, NodeError};
|
||||||
use crate::wallet::ExternalConnection;
|
|
||||||
|
|
||||||
/// Network content.
|
/// Network content.
|
||||||
pub struct NetworkContent {
|
pub struct NetworkContent {
|
||||||
/// Current integrated node tab content.
|
/// Current integrated node tab content.
|
||||||
node_tab_content: Box<dyn NodeTab>,
|
node_tab_content: Box<dyn NodeTab>,
|
||||||
/// Connections content.
|
/// Connections content.
|
||||||
connections: ConnectionsContent,
|
connections: ConnectionsContent,
|
||||||
|
|
||||||
|
/// Application settings content.
|
||||||
|
settings_content: Option<SettingsContent>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for NetworkContent {
|
impl Default for NetworkContent {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
node_tab_content: Box::new(NetworkNode::default()),
|
node_tab_content: Box::new(NetworkNode::default()),
|
||||||
connections: ConnectionsContent::default(),
|
connections: ConnectionsContent::default(),
|
||||||
}
|
settings_content: None,
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NetworkContent {
|
impl NetworkContent {
|
||||||
pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||||
let show_connections = AppConfig::show_connections_network_panel();
|
let show_settings = self.showing_settings();
|
||||||
let dual_panel = Content::is_dual_panel_mode(ui.ctx());
|
let show_connections = AppConfig::show_connections_network_panel();
|
||||||
|
let dual_panel = Content::is_dual_panel_mode(ui.ctx());
|
||||||
|
|
||||||
// Show title panel.
|
// Show title panel.
|
||||||
self.title_ui(ui, dual_panel, show_connections);
|
self.title_ui(ui, dual_panel, show_connections);
|
||||||
|
|
||||||
// Show integrated node tabs content.
|
// Show integrated node tabs content.
|
||||||
if !show_connections {
|
if !show_connections && !show_settings {
|
||||||
egui::TopBottomPanel::bottom("node_tabs")
|
let side_padding = View::TAB_ITEMS_PADDING + if View::is_desktop() { 0.0 } else { 4.0 };
|
||||||
.min_height(0.5)
|
let tabs_margin = Margin {
|
||||||
.resizable(false)
|
left: (View::get_left_inset() + side_padding) as i8,
|
||||||
.frame(egui::Frame {
|
right: (View::far_right_inset_margin(ui) + side_padding) as i8,
|
||||||
inner_margin: Margin {
|
top: View::TAB_ITEMS_PADDING as i8,
|
||||||
left: View::get_left_inset() + View::TAB_ITEMS_PADDING,
|
bottom: (View::get_bottom_inset() + View::TAB_ITEMS_PADDING) as i8,
|
||||||
right: View::far_right_inset_margin(ui) + View::TAB_ITEMS_PADDING,
|
};
|
||||||
top: View::TAB_ITEMS_PADDING,
|
egui::TopBottomPanel::bottom("network_tabs_content")
|
||||||
bottom: View::get_bottom_inset() + View::TAB_ITEMS_PADDING,
|
.min_height(0.5)
|
||||||
},
|
.resizable(false)
|
||||||
fill: Colors::fill(),
|
.frame(egui::Frame {
|
||||||
..Default::default()
|
inner_margin: tabs_margin,
|
||||||
})
|
fill: Colors::fill(),
|
||||||
.show_inside(ui, |ui| {
|
..Default::default()
|
||||||
let rect = ui.available_rect_before_wrap();
|
})
|
||||||
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
|
.show_inside(ui, |ui| {
|
||||||
self.tabs_ui(ui);
|
let rect = ui.available_rect_before_wrap();
|
||||||
});
|
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
|
||||||
// Draw content divider line.
|
self.tabs_ui(ui);
|
||||||
let r = {
|
});
|
||||||
let mut r = rect.clone();
|
// Draw content divider line.
|
||||||
r.min.x -= View::get_left_inset() + View::TAB_ITEMS_PADDING;
|
let r = {
|
||||||
r.min.y -= View::TAB_ITEMS_PADDING;
|
let mut r = rect.clone();
|
||||||
r.max.x += View::far_right_inset_margin(ui) + View::TAB_ITEMS_PADDING;
|
r.min.x -= tabs_margin.left as f32;
|
||||||
r
|
r.min.y -= tabs_margin.top as f32;
|
||||||
};
|
r.max.x += tabs_margin.right as f32;
|
||||||
View::line(ui, LinePosition::TOP, &r, Colors::stroke());
|
r
|
||||||
});
|
};
|
||||||
}
|
View::line(ui, LinePosition::TOP, &r, Colors::stroke());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Show integrated node tab content.
|
// Show settings or integrated node content.
|
||||||
egui::SidePanel::right("node_tab_content")
|
egui::SidePanel::right("network_side_content")
|
||||||
.resizable(false)
|
.resizable(false)
|
||||||
.exact_width(ui.available_width())
|
.exact_width(ui.available_width())
|
||||||
.frame(egui::Frame {
|
.frame(egui::Frame {
|
||||||
..Default::default()
|
..Default::default()
|
||||||
})
|
})
|
||||||
.show_animated_inside(ui, !show_connections, |ui| {
|
.show_animated_inside(ui, show_settings || !show_connections, |ui| {
|
||||||
egui::CentralPanel::default()
|
egui::CentralPanel::default()
|
||||||
.frame(egui::Frame {
|
.frame(egui::Frame {
|
||||||
inner_margin: Margin {
|
inner_margin: Margin {
|
||||||
left: View::get_left_inset() + 4.0,
|
left: (View::get_left_inset() + View::content_padding()) as i8,
|
||||||
right: View::far_right_inset_margin(ui) + 4.0,
|
right: (View::far_right_inset_margin(ui) + View::content_padding())
|
||||||
top: 3.0,
|
as i8,
|
||||||
bottom: 4.0,
|
top: 3.0 as i8,
|
||||||
},
|
bottom: 4.0 as i8,
|
||||||
..Default::default()
|
},
|
||||||
})
|
..Default::default()
|
||||||
.show_inside(ui, |ui| {
|
})
|
||||||
let rect = ui.available_rect_before_wrap();
|
.show_inside(ui, |ui| {
|
||||||
if self.node_tab_content.get_type() != NodeTabType::Settings {
|
let rect = ui.available_rect_before_wrap();
|
||||||
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
|
if let Some(c) = &mut self.settings_content {
|
||||||
let node_err = Node::get_error();
|
ScrollArea::vertical()
|
||||||
if let Some(err) = node_err {
|
.id_salt("app_settings_network")
|
||||||
node_error_ui(ui, err);
|
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
|
||||||
} else if !Node::is_running() {
|
.auto_shrink([false; 2])
|
||||||
disabled_node_ui(ui);
|
.show(ui, |ui| {
|
||||||
} else if Node::get_stats().is_none() || Node::is_restarting() ||
|
ui.add_space(1.0);
|
||||||
Node::is_stopping() {
|
ui.vertical_centered(|ui| {
|
||||||
NetworkContent::loading_ui(ui, None);
|
// Show application settings content.
|
||||||
} else {
|
View::max_width_ui(
|
||||||
self.node_tab_content.ui(ui, cb);
|
ui,
|
||||||
}
|
Content::SIDE_PANEL_WIDTH * 1.3,
|
||||||
});
|
|ui| {
|
||||||
} else {
|
c.ui(ui, cb);
|
||||||
self.node_tab_content.ui(ui, cb);
|
},
|
||||||
}
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else 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::<String>);
|
||||||
|
} else {
|
||||||
|
self.node_tab_content.tab_ui(ui, cb);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
self.node_tab_content.tab_ui(ui, cb);
|
||||||
|
}
|
||||||
|
// Draw content divider line.
|
||||||
|
let r = {
|
||||||
|
let mut r = rect.clone();
|
||||||
|
r.min.y -= 3.0;
|
||||||
|
r.max.x += View::content_padding();
|
||||||
|
r.max.y += View::content_padding();
|
||||||
|
r
|
||||||
|
};
|
||||||
|
if dual_panel {
|
||||||
|
View::line(ui, LinePosition::RIGHT, &r, Colors::item_stroke());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// Draw content divider line.
|
// Show connections content.
|
||||||
let r = {
|
egui::CentralPanel::default()
|
||||||
let mut r = rect.clone();
|
.frame(egui::Frame {
|
||||||
r.min.y -= 3.0;
|
inner_margin: Margin {
|
||||||
r.max.x += 4.0;
|
left: if show_connections {
|
||||||
r.max.y += 4.0;
|
View::get_left_inset() + View::content_padding()
|
||||||
r
|
} else {
|
||||||
};
|
0.0
|
||||||
if dual_panel {
|
} as i8,
|
||||||
View::line(ui, LinePosition::RIGHT, &r, Colors::item_stroke());
|
right: if show_connections {
|
||||||
}
|
View::far_right_inset_margin(ui) + View::content_padding()
|
||||||
});
|
} else {
|
||||||
});
|
0.0
|
||||||
|
} as i8,
|
||||||
|
top: 3.0 as i8,
|
||||||
|
bottom: 0.0 as i8,
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.show_inside(ui, |ui| {
|
||||||
|
let rect = ui.available_rect_before_wrap();
|
||||||
|
ScrollArea::vertical()
|
||||||
|
.id_salt("connections_scroll")
|
||||||
|
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
|
||||||
|
.auto_shrink([false; 2])
|
||||||
|
.show(ui, |ui| {
|
||||||
|
ui.add_space(1.0);
|
||||||
|
ui.vertical_centered(|ui| {
|
||||||
|
let max_width = if !dual_panel {
|
||||||
|
Content::SIDE_PANEL_WIDTH * 1.3
|
||||||
|
} else {
|
||||||
|
ui.available_width()
|
||||||
|
};
|
||||||
|
View::max_width_ui(ui, max_width, |ui| {
|
||||||
|
self.connections.ui(ui, cb);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
ui.add_space(32.0);
|
||||||
|
});
|
||||||
|
// Draw content divider line.
|
||||||
|
let r = {
|
||||||
|
let mut r = rect.clone();
|
||||||
|
r.min.y -= 3.0;
|
||||||
|
r.max.x += View::content_padding();
|
||||||
|
r.max.y += View::content_padding() + View::get_bottom_inset();
|
||||||
|
r
|
||||||
|
};
|
||||||
|
if show_connections && dual_panel {
|
||||||
|
View::line(ui, LinePosition::RIGHT, &r, Colors::item_stroke());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Show connections content.
|
// Redraw after delay if node is running at non-dual-panel mode.
|
||||||
egui::CentralPanel::default()
|
if ((!dual_panel && Content::is_network_panel_open()) || dual_panel) && Node::is_running() {
|
||||||
.frame(egui::Frame {
|
ui.ctx().request_repaint_after(Node::STATS_UPDATE_DELAY);
|
||||||
inner_margin: Margin {
|
}
|
||||||
left: if show_connections {
|
}
|
||||||
View::get_left_inset() + 4.0
|
|
||||||
} else {
|
|
||||||
0.0
|
|
||||||
},
|
|
||||||
right: if show_connections {
|
|
||||||
View::far_right_inset_margin(ui) + 4.0
|
|
||||||
} else {
|
|
||||||
0.0
|
|
||||||
},
|
|
||||||
top: 3.0,
|
|
||||||
bottom: 4.0 + View::get_bottom_inset(),
|
|
||||||
},
|
|
||||||
..Default::default()
|
|
||||||
})
|
|
||||||
.show_inside(ui, |ui| {
|
|
||||||
let rect = ui.available_rect_before_wrap();
|
|
||||||
ScrollArea::vertical()
|
|
||||||
.id_salt("connections_scroll")
|
|
||||||
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
|
|
||||||
.auto_shrink([false; 2])
|
|
||||||
.show(ui, |ui| {
|
|
||||||
ui.add_space(1.0);
|
|
||||||
ui.vertical_centered(|ui| {
|
|
||||||
let max_width = if !dual_panel {
|
|
||||||
Content::SIDE_PANEL_WIDTH * 1.3
|
|
||||||
} else {
|
|
||||||
ui.available_width()
|
|
||||||
};
|
|
||||||
View::max_width_ui(ui, max_width, |ui| {
|
|
||||||
self.connections.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 + 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 at non-dual-panel mode.
|
/// Navigate back, return `true` if action was not consumed.
|
||||||
if !dual_panel && Content::is_network_panel_open() && Node::is_running() {
|
pub fn on_back(&mut self) -> bool {
|
||||||
ui.ctx().request_repaint_after(Node::STATS_UPDATE_DELAY);
|
if self.showing_settings() {
|
||||||
}
|
// Close settings.
|
||||||
}
|
self.settings_content = None;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
/// Draw tab buttons at bottom of the screen.
|
/// Check if application settings content is showing.
|
||||||
fn tabs_ui(&mut self, ui: &mut egui::Ui) {
|
pub fn showing_settings(&self) -> bool {
|
||||||
ui.vertical_centered(|ui| {
|
self.settings_content.is_some()
|
||||||
// Setup spacing between tabs.
|
}
|
||||||
ui.style_mut().spacing.item_spacing = egui::vec2(View::TAB_ITEMS_PADDING, 0.0);
|
|
||||||
// Setup vertical padding inside tab button.
|
|
||||||
ui.style_mut().spacing.button_padding = egui::vec2(0.0, 4.0);
|
|
||||||
|
|
||||||
// Draw tab buttons.
|
/// Draw tab buttons at bottom of the screen.
|
||||||
let current_type = self.node_tab_content.get_type();
|
fn tabs_ui(&mut self, ui: &mut egui::Ui) {
|
||||||
ui.columns(4, |columns| {
|
ui.vertical_centered(|ui| {
|
||||||
columns[0].vertical_centered_justified(|ui| {
|
// Setup spacing between tabs.
|
||||||
View::tab_button(ui, DATABASE, current_type == NodeTabType::Info, |_| {
|
ui.style_mut().spacing.item_spacing = egui::vec2(View::TAB_ITEMS_PADDING, 0.0);
|
||||||
self.node_tab_content = Box::new(NetworkNode::default());
|
|
||||||
});
|
|
||||||
});
|
|
||||||
columns[1].vertical_centered_justified(|ui| {
|
|
||||||
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 == NodeTabType::Mining, |_| {
|
|
||||||
self.node_tab_content = Box::new(NetworkMining::default());
|
|
||||||
});
|
|
||||||
});
|
|
||||||
columns[3].vertical_centered_justified(|ui| {
|
|
||||||
View::tab_button(ui, FADERS, current_type == NodeTabType::Settings, |_| {
|
|
||||||
self.node_tab_content = Box::new(NetworkSettings::default());
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Draw title content.
|
// Draw tab buttons.
|
||||||
fn title_ui(&mut self, ui: &mut egui::Ui, dual_panel: bool, show_connections: bool) {
|
let current_type = self.node_tab_content.get_type();
|
||||||
// Setup values for title panel.
|
ui.columns(4, |columns| {
|
||||||
let title_text = self.node_tab_content.get_type().title();
|
columns[0].vertical_centered_justified(|ui| {
|
||||||
let subtitle_text = Node::get_sync_status_text();
|
let active = Some(current_type == NodeTabType::Info);
|
||||||
let not_syncing = Node::not_syncing();
|
View::tab_button(ui, DATABASE, None, active, |_| {
|
||||||
let title_content = if !show_connections {
|
self.node_tab_content = Box::new(NetworkNode::default());
|
||||||
TitleContentType::WithSubTitle(title_text, subtitle_text, !not_syncing)
|
});
|
||||||
} else {
|
});
|
||||||
TitleContentType::Title(t!("network.connections"))
|
columns[1].vertical_centered_justified(|ui| {
|
||||||
};
|
let active = Some(current_type == NodeTabType::Metrics);
|
||||||
|
View::tab_button(ui, GAUGE, None, active, |_| {
|
||||||
|
self.node_tab_content = Box::new(NetworkMetrics::default());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
columns[2].vertical_centered_justified(|ui| {
|
||||||
|
let active = Some(current_type == NodeTabType::Mining);
|
||||||
|
View::tab_button(ui, FACTORY, None, active, |_| {
|
||||||
|
self.node_tab_content = Box::new(NetworkMining::default());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
columns[3].vertical_centered_justified(|ui| {
|
||||||
|
let active = Some(current_type == NodeTabType::Settings);
|
||||||
|
View::tab_button(ui, FADERS, None, active, |_| {
|
||||||
|
self.node_tab_content = Box::new(NetworkSettings::default());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Draw title panel.
|
/// Draw title content.
|
||||||
TitlePanel::new(Id::from("network_title_panel")).ui(TitleType::Single(title_content), |ui| {
|
fn title_ui(&mut self, ui: &mut egui::Ui, dual_panel: bool, show_connections: bool) {
|
||||||
if !show_connections {
|
let show_settings = self.showing_settings();
|
||||||
View::title_button_big(ui, DOTS_THREE_OUTLINE_VERTICAL, |ui| {
|
|
||||||
AppConfig::toggle_show_connections_network_panel();
|
|
||||||
if AppConfig::show_connections_network_panel() {
|
|
||||||
ExternalConnection::check(None, ui.ctx());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, |ui| {
|
|
||||||
if !dual_panel {
|
|
||||||
View::title_button_big(ui, BRIEFCASE, |_| {
|
|
||||||
Content::toggle_network_panel();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, ui);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Content to draw on loading.
|
// Setup values for title panel.
|
||||||
pub fn loading_ui(ui: &mut egui::Ui, text: Option<String>) {
|
let title_text = self.node_tab_content.get_type().title();
|
||||||
match text {
|
let subtitle_text = Node::get_sync_status_text().into();
|
||||||
None => {
|
let not_syncing = Node::not_syncing() && !Node::data_dir_changing();
|
||||||
ui.centered_and_justified(|ui| {
|
let title_content = if show_settings {
|
||||||
View::big_loading_spinner(ui);
|
TitleContentType::Title(t!("settings").into())
|
||||||
});
|
} else if !show_connections {
|
||||||
}
|
TitleContentType::WithSubTitle(title_text, subtitle_text, !not_syncing)
|
||||||
Some(t) => {
|
} else {
|
||||||
View::center_content(ui, 162.0, |ui| {
|
TitleContentType::Title(t!("network.connections").into())
|
||||||
View::big_loading_spinner(ui);
|
};
|
||||||
ui.add_space(18.0);
|
|
||||||
ui.label(RichText::new(t)
|
|
||||||
.size(16.0)
|
|
||||||
.color(Colors::inactive_text())
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Draw checkbox to run integrated node on application launch.
|
// Draw title panel.
|
||||||
pub fn autorun_node_ui(ui: &mut egui::Ui) {
|
TitlePanel::new(Id::from("network_title_panel")).ui(
|
||||||
let autostart = AppConfig::autostart_node();
|
TitleType::Single(title_content),
|
||||||
View::checkbox(ui, autostart, t!("network.autorun"), || {
|
|ui| {
|
||||||
AppConfig::toggle_node_autostart();
|
if show_settings {
|
||||||
});
|
View::title_button_big(ui, ARROW_LEFT, |_| {
|
||||||
}
|
self.settings_content = None;
|
||||||
|
});
|
||||||
|
} else if !show_connections {
|
||||||
|
View::title_button_big(ui, GLOBE, |_| {
|
||||||
|
AppConfig::toggle_show_connections_network_panel();
|
||||||
|
});
|
||||||
|
} else if !dual_panel {
|
||||||
|
View::title_button_big(ui, GEAR, |_| {
|
||||||
|
self.settings_content = Some(SettingsContent::default());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|ui| {
|
||||||
|
if !dual_panel && !show_settings {
|
||||||
|
View::title_button_big(ui, BRIEFCASE, |_| {
|
||||||
|
Content::toggle_network_panel();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ui,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Content to draw on loading.
|
||||||
|
pub fn loading_ui(ui: &mut egui::Ui, text: Option<impl Into<String>>) {
|
||||||
|
match text {
|
||||||
|
None => {
|
||||||
|
ui.centered_and_justified(|ui| {
|
||||||
|
View::big_loading_spinner(ui);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Some(t) => {
|
||||||
|
View::center_content(ui, 162.0, |ui| {
|
||||||
|
View::big_loading_spinner(ui);
|
||||||
|
ui.add_space(18.0);
|
||||||
|
ui.label(RichText::new(t).size(16.0).color(Colors::inactive_text()));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw checkbox to run integrated node on application launch.
|
||||||
|
pub fn autorun_node_ui(ui: &mut egui::Ui) {
|
||||||
|
let autostart = AppConfig::autostart_node();
|
||||||
|
View::checkbox(ui, autostart, t!("network.autorun"), || {
|
||||||
|
AppConfig::toggle_node_autostart();
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Content to draw when node is disabled.
|
/// Content to draw when node is disabled.
|
||||||
fn disabled_node_ui(ui: &mut egui::Ui) {
|
fn disabled_node_ui(ui: &mut egui::Ui) {
|
||||||
View::center_content(ui, 156.0, |ui| {
|
View::center_content(ui, 156.0, |ui| {
|
||||||
let text = t!("network.disabled_server", "dots" => DOTS_THREE_OUTLINE_VERTICAL);
|
let text = t!("network.disabled_server", "dots" => DOTS_THREE_OUTLINE_VERTICAL);
|
||||||
ui.label(RichText::new(text)
|
ui.label(
|
||||||
.size(16.0)
|
RichText::new(text)
|
||||||
.color(Colors::inactive_text())
|
.size(16.0)
|
||||||
);
|
.color(Colors::inactive_text()),
|
||||||
ui.add_space(8.0);
|
);
|
||||||
View::action_button(ui, format!("{} {}", POWER, t!("network.enable_node")), || {
|
ui.add_space(8.0);
|
||||||
Node::start();
|
View::action_button(
|
||||||
});
|
ui,
|
||||||
ui.add_space(2.0);
|
format!("{} {}", POWER, t!("network.enable_node")),
|
||||||
NetworkContent::autorun_node_ui(ui);
|
|| {
|
||||||
});
|
Node::start();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
ui.add_space(2.0);
|
||||||
|
NetworkContent::autorun_node_ui(ui);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Draw integrated node error content.
|
/// Draw integrated node error content.
|
||||||
pub fn node_error_ui(ui: &mut egui::Ui, e: NodeError) {
|
pub fn node_error_ui(ui: &mut egui::Ui, e: NodeError) {
|
||||||
match e {
|
match e {
|
||||||
NodeError::Storage => {
|
NodeError::Storage => {
|
||||||
View::center_content(ui, 156.0, |ui| {
|
View::center_content(ui, 156.0, |ui| {
|
||||||
ui.label(RichText::new(t!("network_node.error_clean"))
|
ui.label(
|
||||||
.size(16.0)
|
RichText::new(t!("network_node.error_clean"))
|
||||||
.color(Colors::red())
|
.size(16.0)
|
||||||
);
|
.color(Colors::red()),
|
||||||
ui.add_space(8.0);
|
);
|
||||||
let btn_txt = format!("{} {}",
|
ui.add_space(8.0);
|
||||||
ARROWS_COUNTER_CLOCKWISE,
|
let btn_txt = format!("{} {}", ARROWS_COUNTER_CLOCKWISE, t!("network_node.resync"));
|
||||||
t!("network_node.resync"));
|
View::action_button(ui, btn_txt, || {
|
||||||
View::action_button(ui, btn_txt, || {
|
Node::clean_up_data();
|
||||||
Node::clean_up_data();
|
Node::start();
|
||||||
Node::start();
|
});
|
||||||
});
|
ui.add_space(2.0);
|
||||||
ui.add_space(2.0);
|
});
|
||||||
});
|
return;
|
||||||
return;
|
}
|
||||||
}
|
NodeError::P2P | NodeError::API => {
|
||||||
NodeError::P2P | NodeError::API => {
|
let msg_type = match e {
|
||||||
let msg_type = match e {
|
NodeError::API => "API",
|
||||||
NodeError::API => "API",
|
_ => "P2P",
|
||||||
_ => "P2P"
|
};
|
||||||
};
|
View::center_content(ui, 106.0, |ui| {
|
||||||
View::center_content(ui, 106.0, |ui| {
|
let text = t!(
|
||||||
let text = t!(
|
"network_node.error_p2p_api",
|
||||||
"network_node.error_p2p_api",
|
"p2p_api" => msg_type,
|
||||||
"p2p_api" => msg_type,
|
"settings" => FADERS
|
||||||
"settings" => FADERS
|
);
|
||||||
);
|
ui.label(RichText::new(text).size(16.0).color(Colors::red()));
|
||||||
ui.label(RichText::new(text)
|
ui.add_space(2.0);
|
||||||
.size(16.0)
|
});
|
||||||
.color(Colors::red())
|
return;
|
||||||
);
|
}
|
||||||
ui.add_space(2.0);
|
NodeError::Configuration => {
|
||||||
});
|
View::center_content(ui, 106.0, |ui| {
|
||||||
return;
|
ui.label(
|
||||||
}
|
RichText::new(t!("network_node.error_config", "settings" => FADERS))
|
||||||
NodeError::Configuration => {
|
.size(16.0)
|
||||||
View::center_content(ui, 106.0, |ui| {
|
.color(Colors::red()),
|
||||||
ui.label(RichText::new(t!("network_node.error_config", "settings" => FADERS))
|
);
|
||||||
.size(16.0)
|
ui.add_space(8.0);
|
||||||
.color(Colors::red())
|
let btn_txt = format!(
|
||||||
);
|
"{} {}",
|
||||||
ui.add_space(8.0);
|
ARROWS_COUNTER_CLOCKWISE,
|
||||||
let btn_txt = format!("{} {}",
|
t!("network_settings.reset")
|
||||||
ARROWS_COUNTER_CLOCKWISE,
|
);
|
||||||
t!("network_settings.reset"));
|
View::action_button(ui, btn_txt, || {
|
||||||
View::action_button(ui, btn_txt, || {
|
NodeConfig::reset_to_default();
|
||||||
NodeConfig::reset_to_default();
|
Node::start();
|
||||||
Node::start();
|
});
|
||||||
});
|
ui.add_space(2.0);
|
||||||
ui.add_space(2.0);
|
});
|
||||||
});
|
}
|
||||||
}
|
NodeError::Unknown => {
|
||||||
NodeError::Unknown => {
|
View::center_content(ui, 156.0, |ui| {
|
||||||
View::center_content(ui, 156.0, |ui| {
|
ui.label(
|
||||||
ui.label(RichText::new(t!("network_node.error_unknown", "settings" => FADERS))
|
RichText::new(t!("network_node.error_unknown", "settings" => FADERS))
|
||||||
.size(16.0)
|
.size(16.0)
|
||||||
.color(Colors::red())
|
.color(Colors::red()),
|
||||||
);
|
);
|
||||||
ui.add_space(8.0);
|
ui.add_space(8.0);
|
||||||
let btn_txt = format!("{} {}",
|
let btn_txt = format!("{} {}", ARROWS_COUNTER_CLOCKWISE, t!("network_node.resync"));
|
||||||
ARROWS_COUNTER_CLOCKWISE,
|
View::action_button(ui, btn_txt, || {
|
||||||
t!("network_node.resync"));
|
Node::clean_up_data();
|
||||||
View::action_button(ui, btn_txt, || {
|
Node::start();
|
||||||
Node::clean_up_data();
|
});
|
||||||
Node::start();
|
ui.add_space(2.0);
|
||||||
});
|
});
|
||||||
ui.add_space(2.0);
|
}
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
+156
-139
@@ -12,17 +12,17 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
use egui::{RichText, Rounding, ScrollArea, vec2};
|
|
||||||
use egui::scroll_area::ScrollBarVisibility;
|
use egui::scroll_area::ScrollBarVisibility;
|
||||||
|
use egui::{CornerRadius, RichText, ScrollArea, StrokeKind, vec2};
|
||||||
use grin_core::consensus::{DAY_HEIGHT, GRIN_BASE, HOUR_SEC, REWARD};
|
use grin_core::consensus::{DAY_HEIGHT, GRIN_BASE, HOUR_SEC, REWARD};
|
||||||
use grin_servers::{DiffBlock, ServerStats};
|
use grin_servers::{DiffBlock, ServerStats};
|
||||||
|
|
||||||
use crate::gui::Colors;
|
use crate::gui::Colors;
|
||||||
use crate::gui::icons::{AT, COINS, CUBE_TRANSPARENT, HOURGLASS_LOW, HOURGLASS_MEDIUM, TIMER};
|
use crate::gui::icons::{AT, COINS, CUBE_TRANSPARENT, HOURGLASS_LOW, HOURGLASS_MEDIUM, TIMER};
|
||||||
use crate::gui::platform::PlatformCallbacks;
|
use crate::gui::platform::PlatformCallbacks;
|
||||||
use crate::gui::views::{Content, View};
|
|
||||||
use crate::gui::views::network::NetworkContent;
|
use crate::gui::views::network::NetworkContent;
|
||||||
use crate::gui::views::network::types::{NodeTab, NodeTabType};
|
use crate::gui::views::network::types::{NodeTab, NodeTabType};
|
||||||
|
use crate::gui::views::{Content, View};
|
||||||
use crate::node::Node;
|
use crate::node::Node;
|
||||||
|
|
||||||
/// Chain metrics tab content.
|
/// Chain metrics tab content.
|
||||||
@@ -34,158 +34,175 @@ const BLOCK_REWARD: u64 = REWARD / GRIN_BASE;
|
|||||||
const YEARLY_SUPPLY: u64 = (BLOCK_REWARD * DAY_HEIGHT * 365) + 6 * HOUR_SEC;
|
const YEARLY_SUPPLY: u64 = (BLOCK_REWARD * DAY_HEIGHT * 365) + 6 * HOUR_SEC;
|
||||||
|
|
||||||
impl NodeTab for NetworkMetrics {
|
impl NodeTab for NetworkMetrics {
|
||||||
fn get_type(&self) -> NodeTabType {
|
fn get_type(&self) -> NodeTabType {
|
||||||
NodeTabType::Metrics
|
NodeTabType::Metrics
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ui(&mut self, ui: &mut egui::Ui, _: &dyn PlatformCallbacks) {
|
fn tab_ui(&mut self, ui: &mut egui::Ui, _: &dyn PlatformCallbacks) {
|
||||||
let server_stats = Node::get_stats();
|
let server_stats = Node::get_stats();
|
||||||
let stats = server_stats.as_ref().unwrap();
|
let stats = server_stats.as_ref().unwrap();
|
||||||
if stats.diff_stats.height == 0 {
|
if stats.diff_stats.height == 0 {
|
||||||
NetworkContent::loading_ui(ui, Some(t!("network_metrics.loading")));
|
NetworkContent::loading_ui(ui, Some(t!("network_metrics.loading")));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ui.add_space(1.0);
|
ui.add_space(1.0);
|
||||||
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
|
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
|
||||||
// Show emission and difficulty info.
|
// Show emission and difficulty info.
|
||||||
info_ui(ui, stats);
|
info_ui(ui, stats);
|
||||||
// Show difficulty adjustment window blocks.
|
// Show difficulty adjustment window blocks.
|
||||||
blocks_ui(ui, stats);
|
blocks_ui(ui, stats);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Draw emission and difficulty info.
|
/// Draw emission and difficulty info.
|
||||||
fn info_ui(ui: &mut egui::Ui, stats: &ServerStats) {
|
fn info_ui(ui: &mut egui::Ui, stats: &ServerStats) {
|
||||||
// Show emission info.
|
// Show emission info.
|
||||||
View::sub_title(ui, format!("{} {}", COINS, t!("network_metrics.emission")));
|
View::sub_title(ui, format!("{} {}", COINS, t!("network_metrics.emission")));
|
||||||
ui.columns(3, |columns| {
|
ui.columns(3, |columns| {
|
||||||
let supply = stats.header_stats.height * BLOCK_REWARD;
|
let supply = stats.header_stats.height * BLOCK_REWARD;
|
||||||
let rate = (YEARLY_SUPPLY * 100) / supply;
|
let rate = (YEARLY_SUPPLY * 100) / supply;
|
||||||
|
|
||||||
columns[0].vertical_centered(|ui| {
|
columns[0].vertical_centered(|ui| {
|
||||||
View::label_box(ui,
|
View::label_box(
|
||||||
format!("{}ツ", BLOCK_REWARD),
|
ui,
|
||||||
t!("network_metrics.reward"),
|
format!("{}ツ", BLOCK_REWARD),
|
||||||
[true, false, true, false]);
|
t!("network_metrics.reward"),
|
||||||
});
|
[true, false, true, false],
|
||||||
columns[1].vertical_centered(|ui| {
|
);
|
||||||
View::label_box(ui,
|
});
|
||||||
format!("{:.2}%", rate),
|
columns[1].vertical_centered(|ui| {
|
||||||
t!("network_metrics.inflation"),
|
View::label_box(
|
||||||
[false, false, false, false]);
|
ui,
|
||||||
});
|
format!("{:.2}%", rate),
|
||||||
columns[2].vertical_centered(|ui| {
|
t!("network_metrics.inflation"),
|
||||||
View::label_box(ui,
|
[false, false, false, false],
|
||||||
supply.to_string(),
|
);
|
||||||
t!("network_metrics.supply"),
|
});
|
||||||
[false, true, false, true]);
|
columns[2].vertical_centered(|ui| {
|
||||||
});
|
View::label_box(
|
||||||
});
|
ui,
|
||||||
ui.add_space(5.0);
|
supply.to_string(),
|
||||||
|
t!("network_metrics.supply"),
|
||||||
|
[false, true, false, true],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
ui.add_space(5.0);
|
||||||
|
|
||||||
// Show difficulty adjustment window info.
|
// Show difficulty adjustment window info.
|
||||||
let difficulty_title = t!(
|
let difficulty_title = t!(
|
||||||
"network_metrics.difficulty_window",
|
"network_metrics.difficulty_window",
|
||||||
"size" => stats.diff_stats.window_size
|
"size" => stats.diff_stats.window_size
|
||||||
);
|
);
|
||||||
View::sub_title(ui, format!("{} {}", HOURGLASS_MEDIUM, difficulty_title));
|
View::sub_title(ui, format!("{} {}", HOURGLASS_MEDIUM, difficulty_title));
|
||||||
ui.columns(3, |columns| {
|
ui.columns(3, |columns| {
|
||||||
columns[0].vertical_centered(|ui| {
|
columns[0].vertical_centered(|ui| {
|
||||||
View::label_box(ui,
|
View::label_box(
|
||||||
stats.diff_stats.height.to_string(),
|
ui,
|
||||||
t!("network_node.height"),
|
stats.diff_stats.height.to_string(),
|
||||||
[true, false, true, false]);
|
t!("network_node.height"),
|
||||||
});
|
[true, false, true, false],
|
||||||
columns[1].vertical_centered(|ui| {
|
);
|
||||||
View::label_box(ui,
|
});
|
||||||
format!("{}s", stats.diff_stats.average_block_time),
|
columns[1].vertical_centered(|ui| {
|
||||||
t!("network_metrics.block_time"),
|
View::label_box(
|
||||||
[false, false, false, false]);
|
ui,
|
||||||
});
|
format!("{}s", stats.diff_stats.average_block_time),
|
||||||
columns[2].vertical_centered(|ui| {
|
t!("network_metrics.block_time"),
|
||||||
View::label_box(ui,
|
[false, false, false, false],
|
||||||
stats.diff_stats.average_difficulty.to_string(),
|
);
|
||||||
t!("network_node.difficulty"),
|
});
|
||||||
[false, true, false, true]);
|
columns[2].vertical_centered(|ui| {
|
||||||
});
|
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;
|
const BLOCK_ITEM_HEIGHT: f32 = 77.0;
|
||||||
|
|
||||||
/// Draw difficulty adjustment window blocks content.
|
/// Draw difficulty adjustment window blocks content.
|
||||||
fn blocks_ui(ui: &mut egui::Ui, stats: &ServerStats) {
|
fn blocks_ui(ui: &mut egui::Ui, stats: &ServerStats) {
|
||||||
let blocks_size = stats.diff_stats.last_blocks.len();
|
let blocks_size = stats.diff_stats.last_blocks.len();
|
||||||
ui.add_space(4.0);
|
ui.add_space(4.0);
|
||||||
ScrollArea::vertical()
|
ScrollArea::vertical()
|
||||||
.id_salt("mining_difficulty_scroll")
|
.id_salt("mining_difficulty_scroll")
|
||||||
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
|
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
|
||||||
.auto_shrink([false; 2])
|
.auto_shrink([false; 2])
|
||||||
.stick_to_bottom(true)
|
.stick_to_bottom(true)
|
||||||
.show_rows(
|
.show_rows(ui, BLOCK_ITEM_HEIGHT, blocks_size, |ui, row_range| {
|
||||||
ui,
|
ui.add_space(4.0);
|
||||||
BLOCK_ITEM_HEIGHT,
|
for index in row_range {
|
||||||
blocks_size,
|
let db = stats.diff_stats.last_blocks.get(index).unwrap();
|
||||||
|ui, row_range| {
|
block_item_ui(ui, db, View::item_rounding(index, blocks_size, false));
|
||||||
ui.add_space(4.0);
|
}
|
||||||
for index in row_range {
|
});
|
||||||
let db = stats.diff_stats.last_blocks.get(index).unwrap();
|
|
||||||
block_item_ui(ui, db, View::item_rounding(index, blocks_size, false));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Draw block difficulty item.
|
/// Draw block difficulty item.
|
||||||
fn block_item_ui(ui: &mut egui::Ui, db: &DiffBlock, rounding: Rounding) {
|
fn block_item_ui(ui: &mut egui::Ui, db: &DiffBlock, rounding: CornerRadius) {
|
||||||
let mut rect = ui.available_rect_before_wrap();
|
let mut rect = ui.available_rect_before_wrap();
|
||||||
rect.set_height(BLOCK_ITEM_HEIGHT);
|
rect.set_height(BLOCK_ITEM_HEIGHT);
|
||||||
ui.allocate_ui(rect.size(), |ui| {
|
ui.allocate_ui(rect.size(), |ui| {
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.add_space(6.0);
|
ui.add_space(6.0);
|
||||||
ui.vertical(|ui| {
|
ui.vertical(|ui| {
|
||||||
ui.add_space(4.0);
|
ui.add_space(4.0);
|
||||||
|
|
||||||
// Draw round background.
|
// Draw round background.
|
||||||
rect.min += vec2(8.0, 0.0);
|
rect.min += vec2(8.0, 0.0);
|
||||||
rect.max -= vec2(8.0, 0.0);
|
rect.max -= vec2(8.0, 0.0);
|
||||||
ui.painter().rect(rect, rounding, Colors::white_or_black(false), View::item_stroke());
|
ui.painter().rect(
|
||||||
|
rect,
|
||||||
|
rounding,
|
||||||
|
Colors::fill(),
|
||||||
|
View::item_stroke(),
|
||||||
|
StrokeKind::Outside,
|
||||||
|
);
|
||||||
|
|
||||||
// Draw block hash.
|
// Draw block hash.
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.add_space(8.0);
|
ui.add_space(8.0);
|
||||||
ui.label(RichText::new(db.block_hash.to_string())
|
ui.label(
|
||||||
.color(Colors::white_or_black(true))
|
RichText::new(db.block_hash.to_string())
|
||||||
.size(17.0));
|
.color(Colors::white_or_black(true))
|
||||||
});
|
.size(17.0),
|
||||||
// Draw block difficulty and height.
|
);
|
||||||
ui.horizontal(|ui| {
|
});
|
||||||
ui.add_space(7.0);
|
// Draw block difficulty and height.
|
||||||
let diff_text = format!("{} {} {} {}",
|
ui.horizontal(|ui| {
|
||||||
CUBE_TRANSPARENT,
|
ui.add_space(7.0);
|
||||||
db.difficulty,
|
let diff_text = format!(
|
||||||
AT,
|
"{} {} {} {}",
|
||||||
db.block_height);
|
CUBE_TRANSPARENT, db.difficulty, AT, db.block_height
|
||||||
ui.label(RichText::new(diff_text)
|
);
|
||||||
.color(Colors::title(false))
|
ui.label(
|
||||||
.size(15.0));
|
RichText::new(diff_text)
|
||||||
});
|
.color(Colors::title(false))
|
||||||
// Draw block date.
|
.size(15.0),
|
||||||
ui.horizontal(|ui| {
|
);
|
||||||
ui.add_space(7.0);
|
});
|
||||||
let block_time = View::format_time(db.time as i64);
|
// Draw block date.
|
||||||
ui.label(RichText::new(format!("{} {}s {} {}",
|
ui.horizontal(|ui| {
|
||||||
TIMER,
|
ui.add_space(7.0);
|
||||||
db.duration,
|
let block_time = View::format_time(db.time as i64);
|
||||||
HOURGLASS_LOW,
|
ui.label(
|
||||||
block_time))
|
RichText::new(format!(
|
||||||
.color(Colors::gray())
|
"{} {}s {} {}",
|
||||||
.size(15.0));
|
TIMER, db.duration, HOURGLASS_LOW, block_time
|
||||||
});
|
))
|
||||||
ui.add_space(3.0);
|
.color(Colors::gray())
|
||||||
});
|
.size(15.0),
|
||||||
ui.add_space(6.0);
|
);
|
||||||
});
|
});
|
||||||
});
|
ui.add_space(3.0);
|
||||||
}
|
});
|
||||||
|
ui.add_space(6.0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
+248
-215
@@ -12,259 +12,292 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
use egui::{RichText, Rounding, ScrollArea};
|
|
||||||
use egui::scroll_area::ScrollBarVisibility;
|
use egui::scroll_area::ScrollBarVisibility;
|
||||||
|
use egui::{CornerRadius, RichText, ScrollArea, StrokeKind};
|
||||||
use grin_chain::SyncStatus;
|
use grin_chain::SyncStatus;
|
||||||
use grin_servers::WorkerStats;
|
use grin_servers::WorkerStats;
|
||||||
|
|
||||||
use crate::gui::Colors;
|
use crate::gui::Colors;
|
||||||
use crate::gui::icons::{BARBELL, CLOCK_AFTERNOON, CPU, CUBE, FADERS, FOLDER_DASHED, FOLDER_SIMPLE_MINUS, FOLDER_SIMPLE_PLUS, HARD_DRIVES, PLUGS, PLUGS_CONNECTED, POLYGON};
|
use crate::gui::icons::{
|
||||||
|
BARBELL, CLOCK_AFTERNOON, CPU, CUBE, FADERS, FOLDER_DASHED, FOLDER_SIMPLE_MINUS,
|
||||||
|
FOLDER_SIMPLE_PLUS, HARD_DRIVES, PLUGS, PLUGS_CONNECTED, POLYGON,
|
||||||
|
};
|
||||||
use crate::gui::platform::PlatformCallbacks;
|
use crate::gui::platform::PlatformCallbacks;
|
||||||
use crate::gui::views::{Content, View};
|
|
||||||
use crate::gui::views::network::NetworkContent;
|
use crate::gui::views::network::NetworkContent;
|
||||||
use crate::gui::views::network::setup::StratumSetup;
|
use crate::gui::views::network::setup::StratumSetup;
|
||||||
use crate::gui::views::network::types::{NodeTab, NodeTabType};
|
use crate::gui::views::network::types::{NodeTab, NodeTabType};
|
||||||
|
use crate::gui::views::types::ContentContainer;
|
||||||
|
use crate::gui::views::{Content, View};
|
||||||
use crate::node::{Node, NodeConfig};
|
use crate::node::{Node, NodeConfig};
|
||||||
|
|
||||||
/// Mining tab content.
|
/// Mining tab content.
|
||||||
pub struct NetworkMining {
|
pub struct NetworkMining {
|
||||||
/// Stratum server setup content.
|
/// Stratum server setup content.
|
||||||
stratum_server_setup: StratumSetup,
|
stratum_server_setup: StratumSetup,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for NetworkMining {
|
impl Default for NetworkMining {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
stratum_server_setup: StratumSetup::default(),
|
stratum_server_setup: StratumSetup::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NodeTab for NetworkMining {
|
impl NodeTab for NetworkMining {
|
||||||
fn get_type(&self) -> NodeTabType {
|
fn get_type(&self) -> NodeTabType {
|
||||||
NodeTabType::Mining
|
NodeTabType::Mining
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
fn tab_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||||
if Node::is_stratum_starting() || 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")));
|
NetworkContent::loading_ui(ui, Some(t!("network_mining.loading")));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show stratum server setup when mining server is not running.
|
// Show stratum server setup when mining server is not running.
|
||||||
let stratum_stats = Node::get_stratum_stats();
|
let stratum_stats = Node::get_stratum_stats();
|
||||||
if !stratum_stats.is_running {
|
if !stratum_stats.is_running {
|
||||||
ScrollArea::vertical()
|
ScrollArea::vertical()
|
||||||
.id_salt("stratum_setup_scroll")
|
.id_salt("stratum_setup_scroll")
|
||||||
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
|
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
|
||||||
.auto_shrink([false; 2])
|
.auto_shrink([false; 2])
|
||||||
.show(ui, |ui| {
|
.show(ui, |ui| {
|
||||||
ui.add_space(1.0);
|
ui.add_space(1.0);
|
||||||
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
|
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
|
||||||
self.stratum_server_setup.ui(ui, cb);
|
self.stratum_server_setup.ui(ui, cb);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ui.add_space(1.0);
|
ui.add_space(1.0);
|
||||||
|
|
||||||
// Show stratum mining server info.
|
// Show stratum mining server info.
|
||||||
View::sub_title(ui, format!("{} {}", HARD_DRIVES, t!("network_mining.server")));
|
View::sub_title(
|
||||||
ui.columns(2, |columns| {
|
ui,
|
||||||
columns[0].vertical_centered(|ui| {
|
format!("{} {}", HARD_DRIVES, t!("network_mining.server")),
|
||||||
let (stratum_addr, stratum_port) = NodeConfig::get_stratum_address();
|
);
|
||||||
View::label_box(ui,
|
ui.columns(2, |columns| {
|
||||||
format!("{}:{}", stratum_addr, stratum_port),
|
columns[0].vertical_centered(|ui| {
|
||||||
t!("network_mining.address"),
|
let (stratum_addr, stratum_port) = NodeConfig::get_stratum_address();
|
||||||
[true, false, true, false]);
|
View::label_box(
|
||||||
});
|
ui,
|
||||||
columns[1].vertical_centered(|ui| {
|
format!("{}:{}", stratum_addr, stratum_port),
|
||||||
View::label_box(ui,
|
t!("network_mining.address"),
|
||||||
self.stratum_server_setup
|
[true, false, true, false],
|
||||||
.wallet_name
|
);
|
||||||
.clone()
|
});
|
||||||
.unwrap_or("-".to_string()),
|
columns[1].vertical_centered(|ui| {
|
||||||
t!("network_mining.rewards_wallet"),
|
View::label_box(
|
||||||
[false, true, false, true]);
|
ui,
|
||||||
});
|
self.stratum_server_setup
|
||||||
});
|
.wallet_name
|
||||||
ui.add_space(4.0);
|
.clone()
|
||||||
|
.unwrap_or("-".to_string()),
|
||||||
|
t!("network_mining.rewards_wallet"),
|
||||||
|
[false, true, false, true],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
ui.add_space(4.0);
|
||||||
|
|
||||||
// Show network info.
|
// Show network info.
|
||||||
View::sub_title(ui, format!("{} {}", POLYGON, t!("network.self")));
|
View::sub_title(ui, format!("{} {}", POLYGON, t!("network.self")));
|
||||||
ui.columns(3, |columns| {
|
ui.columns(3, |columns| {
|
||||||
columns[0].vertical_centered(|ui| {
|
columns[0].vertical_centered(|ui| {
|
||||||
let difficulty = if stratum_stats.network_difficulty > 0 {
|
let difficulty = if stratum_stats.network_difficulty > 0 {
|
||||||
stratum_stats.network_difficulty.to_string()
|
stratum_stats.network_difficulty.to_string()
|
||||||
} else {
|
} else {
|
||||||
"-".into()
|
"-".into()
|
||||||
};
|
};
|
||||||
View::label_box(ui,
|
View::label_box(
|
||||||
difficulty,
|
ui,
|
||||||
t!("network_node.difficulty"),
|
difficulty,
|
||||||
[true, false, true, false]);
|
t!("network_node.difficulty"),
|
||||||
});
|
[true, false, true, false],
|
||||||
columns[1].vertical_centered(|ui| {
|
);
|
||||||
let block_height = if stratum_stats.block_height > 0 {
|
});
|
||||||
stratum_stats.block_height.to_string()
|
columns[1].vertical_centered(|ui| {
|
||||||
} else {
|
let block_height = if stratum_stats.block_height > 0 {
|
||||||
"-".into()
|
stratum_stats.block_height.to_string()
|
||||||
};
|
} else {
|
||||||
View::label_box(ui,
|
"-".into()
|
||||||
block_height,
|
};
|
||||||
t!("network_node.header"),
|
View::label_box(
|
||||||
[false, false, false, false]);
|
ui,
|
||||||
});
|
block_height,
|
||||||
columns[2].vertical_centered(|ui| {
|
t!("network_node.header"),
|
||||||
let hashrate = if stratum_stats.network_hashrate > 0.0 {
|
[false, false, false, false],
|
||||||
format!("{:.*}", 2, stratum_stats.network_hashrate)
|
);
|
||||||
} else {
|
});
|
||||||
"-".into()
|
columns[2].vertical_centered(|ui| {
|
||||||
};
|
let hashrate = if stratum_stats.network_hashrate > 0.0 {
|
||||||
View::label_box(ui,
|
format!("{:.*}", 2, stratum_stats.network_hashrate)
|
||||||
hashrate,
|
} else {
|
||||||
t!("network_mining.hashrate", "bits" => stratum_stats.edge_bits),
|
"-".into()
|
||||||
[false, true, false, true]);
|
};
|
||||||
});
|
View::label_box(
|
||||||
});
|
ui,
|
||||||
ui.add_space(4.0);
|
hashrate,
|
||||||
|
t!("network_mining.hashrate", "bits" => stratum_stats.edge_bits),
|
||||||
|
[false, true, false, true],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
ui.add_space(4.0);
|
||||||
|
|
||||||
// Show mining info.
|
// Show mining info.
|
||||||
View::sub_title(ui, format!("{} {}", CPU, t!("network_mining.miners")));
|
View::sub_title(ui, format!("{} {}", CPU, t!("network_mining.miners")));
|
||||||
ui.columns(2, |columns| {
|
ui.columns(2, |columns| {
|
||||||
columns[0].vertical_centered(|ui| {
|
columns[0].vertical_centered(|ui| {
|
||||||
View::label_box(ui,
|
View::label_box(
|
||||||
stratum_stats.num_workers.to_string(),
|
ui,
|
||||||
t!("network_mining.devices"),
|
stratum_stats.num_workers.to_string(),
|
||||||
[true, false, true, false]);
|
t!("network_mining.devices"),
|
||||||
});
|
[true, false, true, false],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
columns[1].vertical_centered(|ui| {
|
columns[1].vertical_centered(|ui| {
|
||||||
View::label_box(ui,
|
View::label_box(
|
||||||
stratum_stats.blocks_found.to_string(),
|
ui,
|
||||||
t!("network_mining.blocks_found"),
|
stratum_stats.blocks_found.to_string(),
|
||||||
[false, true, false, true]);
|
t!("network_mining.blocks_found"),
|
||||||
});
|
[false, true, false, true],
|
||||||
});
|
);
|
||||||
ui.add_space(4.0);
|
});
|
||||||
|
});
|
||||||
|
ui.add_space(4.0);
|
||||||
|
|
||||||
// Show workers stats or info text when possible.
|
// Show workers stats or info text when possible.
|
||||||
let workers_size = stratum_stats.worker_stats.len();
|
let workers_size = stratum_stats.worker_stats.len();
|
||||||
if workers_size != 0 && stratum_stats.num_workers > 0 {
|
if workers_size != 0 && stratum_stats.num_workers > 0 {
|
||||||
ui.add_space(4.0);
|
ui.add_space(4.0);
|
||||||
View::horizontal_line(ui, Colors::item_stroke());
|
View::horizontal_line(ui, Colors::item_stroke());
|
||||||
ui.add_space(4.0);
|
ui.add_space(4.0);
|
||||||
ScrollArea::vertical()
|
ScrollArea::vertical()
|
||||||
.id_salt("stratum_workers_scroll")
|
.id_salt("stratum_workers_scroll")
|
||||||
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
|
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
|
||||||
.auto_shrink([false; 2])
|
.auto_shrink([false; 2])
|
||||||
.show_rows(
|
.show_rows(ui, WORKER_ITEM_HEIGHT, workers_size, |ui, row_range| {
|
||||||
ui,
|
for index in row_range {
|
||||||
WORKER_ITEM_HEIGHT,
|
// Add space before the first item.
|
||||||
workers_size,
|
if index == 0 {
|
||||||
|ui, row_range| {
|
ui.add_space(4.0);
|
||||||
for index in row_range {
|
}
|
||||||
// Add space before the first item.
|
let worker = stratum_stats.worker_stats.get(index).unwrap();
|
||||||
if index == 0 {
|
let item_rounding = View::item_rounding(index, workers_size, false);
|
||||||
ui.add_space(4.0);
|
worker_item_ui(ui, worker, item_rounding);
|
||||||
}
|
}
|
||||||
let worker = stratum_stats.worker_stats.get(index).unwrap();
|
});
|
||||||
let item_rounding = View::item_rounding(index, workers_size, false);
|
} else if ui.available_height() > 142.0 {
|
||||||
worker_item_ui(ui, worker, item_rounding);
|
View::center_content(ui, 142.0, |ui| {
|
||||||
}
|
ui.label(
|
||||||
},
|
RichText::new(t!("network_mining.info", "settings" => FADERS))
|
||||||
);
|
.size(16.0)
|
||||||
} else if ui.available_height() > 142.0 {
|
.color(Colors::inactive_text()),
|
||||||
View::center_content(ui, 142.0, |ui| {
|
);
|
||||||
ui.label(RichText::new(t!("network_mining.info", "settings" => FADERS))
|
});
|
||||||
.size(16.0)
|
}
|
||||||
.color(Colors::inactive_text())
|
}
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Height of Stratum server worker list item.
|
/// Height of Stratum server worker list item.
|
||||||
const WORKER_ITEM_HEIGHT: f32 = 76.0;
|
const WORKER_ITEM_HEIGHT: f32 = 76.0;
|
||||||
|
|
||||||
/// Draw worker statistics item.
|
/// Draw worker statistics item.
|
||||||
fn worker_item_ui(ui: &mut egui::Ui, ws: &WorkerStats, rounding: Rounding) {
|
fn worker_item_ui(ui: &mut egui::Ui, ws: &WorkerStats, rounding: CornerRadius) {
|
||||||
ui.horizontal_wrapped(|ui| {
|
ui.horizontal_wrapped(|ui| {
|
||||||
ui.vertical_centered_justified(|ui| {
|
ui.vertical_centered_justified(|ui| {
|
||||||
// Draw round background.
|
// Draw round background.
|
||||||
let mut rect = ui.available_rect_before_wrap();
|
let mut rect = ui.available_rect_before_wrap();
|
||||||
rect.set_height(WORKER_ITEM_HEIGHT);
|
rect.set_height(WORKER_ITEM_HEIGHT);
|
||||||
ui.painter().rect(rect, rounding, Colors::white_or_black(false), View::item_stroke());
|
ui.painter().rect(
|
||||||
|
rect,
|
||||||
|
rounding,
|
||||||
|
Colors::white_or_black(false),
|
||||||
|
View::item_stroke(),
|
||||||
|
StrokeKind::Outside,
|
||||||
|
);
|
||||||
|
|
||||||
ui.add_space(2.0);
|
ui.add_space(2.0);
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.add_space(5.0);
|
ui.add_space(5.0);
|
||||||
|
|
||||||
// Draw worker connection status.
|
// Draw worker connection status.
|
||||||
let (status_text, status_icon, status_color) = match ws.is_connected {
|
let (status_text, status_icon, status_color) = match ws.is_connected {
|
||||||
true => (
|
true => (
|
||||||
t!("network_mining.connected"),
|
t!("network_mining.connected"),
|
||||||
PLUGS_CONNECTED,
|
PLUGS_CONNECTED,
|
||||||
Colors::white_or_black(true)
|
Colors::white_or_black(true),
|
||||||
),
|
),
|
||||||
false => (t!("network_mining.disconnected"), PLUGS, Colors::inactive_text())
|
false => (
|
||||||
};
|
t!("network_mining.disconnected"),
|
||||||
let status_line_text = format!("{} {} {}", status_icon, ws.id, status_text);
|
PLUGS,
|
||||||
ui.heading(RichText::new(status_line_text)
|
Colors::inactive_text(),
|
||||||
.color(status_color)
|
),
|
||||||
.size(17.0));
|
};
|
||||||
ui.add_space(2.0);
|
let status_line_text = format!("{} {} {}", status_icon, ws.id, status_text);
|
||||||
});
|
ui.heading(
|
||||||
ui.horizontal(|ui| {
|
RichText::new(status_line_text)
|
||||||
ui.add_space(6.0);
|
.color(status_color)
|
||||||
|
.size(17.0),
|
||||||
|
);
|
||||||
|
ui.add_space(2.0);
|
||||||
|
});
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.add_space(6.0);
|
||||||
|
|
||||||
// Draw difficulty.
|
// Draw difficulty.
|
||||||
let diff_text = format!("{} {}", BARBELL, ws.pow_difficulty);
|
let diff_text = format!("{} {}", BARBELL, ws.pow_difficulty);
|
||||||
ui.heading(RichText::new(diff_text)
|
ui.heading(
|
||||||
.color(Colors::title(false))
|
RichText::new(diff_text)
|
||||||
.size(16.0));
|
.color(Colors::title(false))
|
||||||
ui.add_space(6.0);
|
.size(16.0),
|
||||||
|
);
|
||||||
|
ui.add_space(6.0);
|
||||||
|
|
||||||
// Draw accepted shares.
|
// Draw accepted shares.
|
||||||
let accepted_text = format!("{} {}", FOLDER_SIMPLE_PLUS, ws.num_accepted);
|
let accepted_text = format!("{} {}", FOLDER_SIMPLE_PLUS, ws.num_accepted);
|
||||||
ui.heading(RichText::new(accepted_text)
|
ui.heading(
|
||||||
.color(Colors::green())
|
RichText::new(accepted_text)
|
||||||
.size(16.0));
|
.color(Colors::green())
|
||||||
ui.add_space(6.0);
|
.size(16.0),
|
||||||
|
);
|
||||||
|
ui.add_space(6.0);
|
||||||
|
|
||||||
// Draw rejected shares.
|
// Draw rejected shares.
|
||||||
let rejected_text = format!("{} {}", FOLDER_SIMPLE_MINUS, ws.num_rejected);
|
let rejected_text = format!("{} {}", FOLDER_SIMPLE_MINUS, ws.num_rejected);
|
||||||
ui.heading(RichText::new(rejected_text)
|
ui.heading(RichText::new(rejected_text).color(Colors::red()).size(16.0));
|
||||||
.color(Colors::red())
|
ui.add_space(6.0);
|
||||||
.size(16.0));
|
|
||||||
ui.add_space(6.0);
|
|
||||||
|
|
||||||
// Draw stale shares.
|
// Draw stale shares.
|
||||||
let stale_text = format!("{} {}", FOLDER_DASHED, ws.num_stale);
|
let stale_text = format!("{} {}", FOLDER_DASHED, ws.num_stale);
|
||||||
ui.heading(RichText::new(stale_text)
|
ui.heading(RichText::new(stale_text).color(Colors::gray()).size(16.0));
|
||||||
.color(Colors::gray())
|
ui.add_space(6.0);
|
||||||
.size(16.0));
|
|
||||||
ui.add_space(6.0);
|
|
||||||
|
|
||||||
// Draw blocks found.
|
// Draw blocks found.
|
||||||
let blocks_found_text = format!("{} {}", CUBE, ws.num_blocks_found);
|
let blocks_found_text = format!("{} {}", CUBE, ws.num_blocks_found);
|
||||||
ui.heading(RichText::new(blocks_found_text)
|
ui.heading(
|
||||||
.color(Colors::title(false))
|
RichText::new(blocks_found_text)
|
||||||
.size(16.0));
|
.color(Colors::title(false))
|
||||||
});
|
.size(16.0),
|
||||||
ui.horizontal(|ui| {
|
);
|
||||||
ui.add_space(6.0);
|
});
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.add_space(6.0);
|
||||||
|
|
||||||
// Draw block time
|
// Draw block time
|
||||||
let seen_ts = ws.last_seen.duration_since(std::time::UNIX_EPOCH).unwrap().as_secs();
|
let seen_ts = ws
|
||||||
let seen_time = View::format_time(seen_ts as i64);
|
.last_seen
|
||||||
let seen_text = format!("{} {}", CLOCK_AFTERNOON, seen_time);
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
ui.heading(RichText::new(seen_text)
|
.unwrap()
|
||||||
.color(Colors::gray())
|
.as_secs();
|
||||||
.size(16.0));
|
let seen_time = View::format_time(seen_ts as i64);
|
||||||
});
|
let seen_text = format!("{} {}", CLOCK_AFTERNOON, seen_time);
|
||||||
});
|
ui.heading(RichText::new(seen_text).color(Colors::gray()).size(16.0));
|
||||||
});
|
});
|
||||||
}
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -33,5 +33,5 @@ pub use content::*;
|
|||||||
mod connections;
|
mod connections;
|
||||||
pub use connections::*;
|
pub use connections::*;
|
||||||
|
|
||||||
|
pub mod modals;
|
||||||
pub mod types;
|
pub mod types;
|
||||||
pub mod modals;
|
|
||||||
@@ -14,160 +14,260 @@
|
|||||||
|
|
||||||
use egui::{Id, RichText};
|
use egui::{Id, RichText};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use crate::gui::Colors;
|
use crate::gui::Colors;
|
||||||
|
use crate::gui::icons::SCAN;
|
||||||
use crate::gui::platform::PlatformCallbacks;
|
use crate::gui::platform::PlatformCallbacks;
|
||||||
use crate::gui::views::{Modal, View};
|
use crate::gui::views::network::types::ShareConnection;
|
||||||
use crate::gui::views::types::TextEditOptions;
|
use crate::gui::views::{CameraContent, Modal, TextEdit, View};
|
||||||
use crate::wallet::{ConnectionsConfig, ExternalConnection};
|
use crate::wallet::{ConnectionsConfig, ExternalConnection};
|
||||||
|
|
||||||
/// Content to create or update external wallet connection.
|
/// Content to create or update external wallet connection.
|
||||||
pub struct ExternalConnectionModal {
|
pub struct ExternalConnectionModal {
|
||||||
/// Flag to check if [`Modal`] was just opened to focus on input field.
|
/// Flag to check if content was just rendered.
|
||||||
first_modal_launch: bool,
|
first_draw: bool,
|
||||||
/// External connection URL value for [`Modal`].
|
|
||||||
ext_node_url_edit: String,
|
/// Editing external connection identifier.
|
||||||
/// External connection API secret value for [`Modal`].
|
id: Option<i64>,
|
||||||
ext_node_secret_edit: String,
|
|
||||||
/// Flag to show URL format error at [`Modal`].
|
/// External connection URL.
|
||||||
ext_node_url_error: bool,
|
url_edit: String,
|
||||||
/// Editing external connection identifier for [`Modal`].
|
/// Flag to show URL format error.
|
||||||
ext_conn_id: Option<i64>,
|
url_error: bool,
|
||||||
|
|
||||||
|
// /// External connection username.
|
||||||
|
// username_edit: String,
|
||||||
|
/// External connection API secret.
|
||||||
|
secret_edit: String,
|
||||||
|
|
||||||
|
/// QR code scanner content.
|
||||||
|
scan_qr_content: Option<CameraContent>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
impl ExternalConnectionModal {
|
impl ExternalConnectionModal {
|
||||||
/// Network [`Modal`] identifier.
|
/// Network [`Modal`] identifier.
|
||||||
pub const NETWORK_ID: &'static str = "net_ext_conn_modal";
|
pub const NETWORK_ID: &'static str = "net_ext_conn_modal";
|
||||||
/// Wallet [`Modal`] identifier.
|
/// Wallet [`Modal`] identifier.
|
||||||
pub const WALLET_ID: &'static str = "wallet_ext_conn_modal";
|
pub const WALLET_ID: &'static str = "wallet_ext_conn_modal";
|
||||||
|
|
||||||
/// Create new instance from optional provided connection to update.
|
/// Create new instance from optional provided connection to update.
|
||||||
pub fn new(conn: Option<ExternalConnection>) -> Self {
|
pub fn new(conn: Option<ExternalConnection>) -> Self {
|
||||||
let (ext_node_url_edit, ext_node_secret_edit, ext_conn_id) = if let Some(c) = conn {
|
let (url_edit, secret_edit, id) = if let Some(c) = conn {
|
||||||
(c.url, c.secret.unwrap_or("".to_string()), Some(c.id))
|
// let username = c.username.unwrap_or("grin".to_string());
|
||||||
} else {
|
let secret = c.secret.unwrap_or("".to_string());
|
||||||
("".to_string(), "".to_string(), None)
|
(c.url, secret, Some(c.id))
|
||||||
};
|
} else {
|
||||||
Self {
|
("".to_string(), "".to_string(), None)
|
||||||
first_modal_launch: true,
|
};
|
||||||
ext_node_url_edit,
|
Self {
|
||||||
ext_node_secret_edit,
|
first_draw: true,
|
||||||
ext_node_url_error: false,
|
url_edit,
|
||||||
ext_conn_id,
|
url_error: false,
|
||||||
}
|
secret_edit,
|
||||||
}
|
id,
|
||||||
|
scan_qr_content: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Draw external connection [`Modal`] content.
|
/// Draw external connection [`Modal`] content.
|
||||||
pub fn ui(&mut self,
|
pub fn ui(
|
||||||
ui: &mut egui::Ui,
|
&mut self,
|
||||||
cb: &dyn PlatformCallbacks,
|
ui: &mut egui::Ui,
|
||||||
modal: &Modal,
|
cb: &dyn PlatformCallbacks,
|
||||||
on_save: impl Fn(ExternalConnection)) {
|
modal: &Modal,
|
||||||
ui.add_space(6.0);
|
on_save: impl Fn(ExternalConnection),
|
||||||
ui.vertical_centered(|ui| {
|
) {
|
||||||
ui.label(RichText::new(t!("wallets.node_url"))
|
// Show QR code scanner content.
|
||||||
.size(17.0)
|
if let Some(scan_content) = self.scan_qr_content.as_mut() {
|
||||||
.color(Colors::gray()));
|
if let Some(result) = scan_content.qr_scan_result() {
|
||||||
ui.add_space(8.0);
|
cb.stop_camera();
|
||||||
|
modal.enable_closing();
|
||||||
|
self.scan_qr_content = None;
|
||||||
|
// Parse scan result.
|
||||||
|
if let Ok(c) = serde_json::from_str::<ShareConnection>(&result.text()) {
|
||||||
|
let ext_conn = ExternalConnection::new(c.url, Some(c.username), Some(c.secret));
|
||||||
|
ConnectionsConfig::add_ext_conn(ext_conn.clone());
|
||||||
|
ExternalConnection::check(Some(ext_conn.id), ui.ctx());
|
||||||
|
Modal::close();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
scan_content.ui(ui, cb);
|
||||||
|
}
|
||||||
|
ui.add_space(8.0);
|
||||||
|
|
||||||
// Draw node URL text edit.
|
// Setup spacing between buttons.
|
||||||
let url_edit_id = Id::from(modal.id).with(self.ext_conn_id);
|
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
|
||||||
let mut url_edit_opts = TextEditOptions::new(url_edit_id).paste().no_focus();
|
|
||||||
if self.first_modal_launch {
|
|
||||||
self.first_modal_launch = false;
|
|
||||||
url_edit_opts.focus = true;
|
|
||||||
}
|
|
||||||
View::text_edit(ui, cb, &mut self.ext_node_url_edit, &mut url_edit_opts);
|
|
||||||
ui.add_space(8.0);
|
|
||||||
|
|
||||||
ui.label(RichText::new(t!("wallets.node_secret"))
|
// Show buttons to close modal or scanner.
|
||||||
.size(17.0)
|
ui.columns(2, |cols| {
|
||||||
.color(Colors::gray()));
|
cols[0].vertical_centered_justified(|ui| {
|
||||||
ui.add_space(8.0);
|
View::button(ui, t!("close"), Colors::white_or_black(false), || {
|
||||||
|
cb.stop_camera();
|
||||||
|
self.scan_qr_content = None;
|
||||||
|
Modal::close();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
cols[1].vertical_centered_justified(|ui| {
|
||||||
|
View::button(ui, t!("back"), Colors::white_or_black(false), || {
|
||||||
|
cb.stop_camera();
|
||||||
|
self.scan_qr_content = None;
|
||||||
|
modal.enable_closing();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
ui.add_space(6.0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Add connection button callback.
|
||||||
|
let on_add = |ui: &mut egui::Ui, m: &mut ExternalConnectionModal| {
|
||||||
|
let url = if !m.url_edit.starts_with("http") {
|
||||||
|
format!("https://{}", m.url_edit)
|
||||||
|
} else {
|
||||||
|
m.url_edit.clone()
|
||||||
|
};
|
||||||
|
let error = Url::parse(url.trim()).is_err();
|
||||||
|
m.url_error = error;
|
||||||
|
if !error {
|
||||||
|
let username = if m.secret_edit.is_empty() {
|
||||||
|
Some("grin".to_string())
|
||||||
|
} else {
|
||||||
|
Some(m.secret_edit.clone())
|
||||||
|
};
|
||||||
|
let secret = if m.secret_edit.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(m.secret_edit.clone())
|
||||||
|
};
|
||||||
|
// Update or create new connection.
|
||||||
|
let mut ext_conn = ExternalConnection::new(url, username, secret);
|
||||||
|
if let Some(id) = m.id {
|
||||||
|
ext_conn.id = id;
|
||||||
|
}
|
||||||
|
ConnectionsConfig::add_ext_conn(ext_conn.clone());
|
||||||
|
ExternalConnection::check(Some(ext_conn.id), ui.ctx());
|
||||||
|
on_save(ext_conn);
|
||||||
|
|
||||||
// Draw node API secret text edit.
|
// Close modal.
|
||||||
let secret_edit_id = Id::from(modal.id).with(self.ext_conn_id).with("node_secret");
|
m.url_edit = "".to_string();
|
||||||
let mut secret_edit_opts = TextEditOptions::new(secret_edit_id).paste().no_focus();
|
m.secret_edit = "".to_string();
|
||||||
View::text_edit(ui, cb, &mut self.ext_node_secret_edit, &mut secret_edit_opts);
|
m.url_error = false;
|
||||||
|
Modal::close();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Show error when specified URL is not valid.
|
ui.vertical_centered(|ui| {
|
||||||
if self.ext_node_url_error {
|
ui.add_space(6.0);
|
||||||
ui.add_space(12.0);
|
ui.label(
|
||||||
ui.label(RichText::new(t!("wallets.invalid_url"))
|
RichText::new(t!("wallets.node_url"))
|
||||||
.size(17.0)
|
.size(17.0)
|
||||||
.color(Colors::red()));
|
.color(Colors::gray()),
|
||||||
}
|
);
|
||||||
ui.add_space(12.0);
|
ui.add_space(8.0);
|
||||||
});
|
|
||||||
|
|
||||||
// Show modal buttons.
|
// Draw node URL text edit.
|
||||||
ui.scope(|ui| {
|
let url_edit_id = Id::from(modal.id).with(self.id).with("node_url");
|
||||||
// Setup spacing between buttons.
|
let mut url_edit = TextEdit::new(url_edit_id).paste().focus(self.first_draw);
|
||||||
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
|
let url_edit_before = self.url_edit.clone();
|
||||||
|
url_edit.ui(ui, &mut self.url_edit, cb);
|
||||||
|
if self.url_edit != url_edit_before {
|
||||||
|
self.url_error = false;
|
||||||
|
}
|
||||||
|
|
||||||
ui.columns(2, |columns| {
|
// ui.add_space(8.0);
|
||||||
columns[0].vertical_centered_justified(|ui| {
|
// ui.label(RichText::new(t!("wallets.name"))
|
||||||
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
|
// .size(17.0)
|
||||||
// Close modal.
|
// .color(Colors::gray()));
|
||||||
self.ext_node_url_edit = "".to_string();
|
// ui.add_space(8.0);
|
||||||
self.ext_node_secret_edit = "".to_string();
|
//
|
||||||
self.ext_node_url_error = false;
|
// // Draw node username text edit (disabled by default).
|
||||||
cb.hide_keyboard();
|
// let username_edit_id = Id::from(modal.id).with(self.id).with("node_username");
|
||||||
modal.close();
|
// let mut username_edit = TextEdit::new(username_edit_id).focus(false).disable();
|
||||||
});
|
// username_edit.ui(ui, &mut self.username_edit, cb);
|
||||||
});
|
|
||||||
columns[1].vertical_centered_justified(|ui| {
|
|
||||||
// Add connection button callback.
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
let error = Url::parse(self.ext_node_url_edit.as_str()).is_err();
|
|
||||||
self.ext_node_url_error = error;
|
|
||||||
if !error {
|
|
||||||
let url = self.ext_node_url_edit.to_owned();
|
|
||||||
let secret = if self.ext_node_secret_edit.is_empty() {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(self.ext_node_secret_edit.to_owned())
|
|
||||||
};
|
|
||||||
|
|
||||||
// Update or create new connection.
|
ui.add_space(8.0);
|
||||||
let mut ext_conn = ExternalConnection::new(url, secret);
|
ui.label(
|
||||||
if let Some(id) = self.ext_conn_id {
|
RichText::new(t!("wallets.node_secret"))
|
||||||
ext_conn.id = id;
|
.size(17.0)
|
||||||
}
|
.color(Colors::gray()),
|
||||||
ConnectionsConfig::add_ext_conn(ext_conn.clone());
|
);
|
||||||
ExternalConnection::check(Some(ext_conn.id), ui.ctx());
|
ui.add_space(8.0);
|
||||||
on_save(ext_conn);
|
|
||||||
|
|
||||||
// Close modal.
|
// Draw node API secret text edit.
|
||||||
self.ext_node_url_edit = "".to_string();
|
let secret_edit_id = Id::from(modal.id).with(self.id).with("node_secret");
|
||||||
self.ext_node_secret_edit = "".to_string();
|
let mut secret_edit = TextEdit::new(secret_edit_id)
|
||||||
self.ext_node_url_error = false;
|
.h_center()
|
||||||
cb.hide_keyboard();
|
.password()
|
||||||
modal.close();
|
.paste()
|
||||||
}
|
.focus(false);
|
||||||
};
|
if url_edit.enter_pressed {
|
||||||
|
secret_edit.focus_request();
|
||||||
|
}
|
||||||
|
secret_edit.ui(ui, &mut self.secret_edit, cb);
|
||||||
|
if secret_edit.enter_pressed {
|
||||||
|
on_add(ui, self);
|
||||||
|
}
|
||||||
|
|
||||||
// Handle Enter key press.
|
// Show error when specified URL is not valid.
|
||||||
let mut enter = false;
|
if self.url_error {
|
||||||
View::on_enter_key(ui, || {
|
ui.add_space(12.0);
|
||||||
enter = true;
|
ui.label(
|
||||||
});
|
RichText::new(t!("wallets.invalid_url"))
|
||||||
if enter {
|
.size(17.0)
|
||||||
(on_add)(ui);
|
.color(Colors::red()),
|
||||||
}
|
);
|
||||||
|
}
|
||||||
|
ui.add_space(12.0);
|
||||||
|
|
||||||
View::button_ui(ui, if self.ext_conn_id.is_some() {
|
let scan_text = format!("{} {}", SCAN, t!("scan"));
|
||||||
t!("modal.save")
|
View::button(ui, scan_text, Colors::white_or_black(false), || {
|
||||||
} else {
|
modal.disable_closing();
|
||||||
t!("modal.add")
|
self.scan_qr_content = Some(CameraContent::default());
|
||||||
}, Colors::white_or_black(false), on_add);
|
cb.start_camera();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
ui.add_space(6.0);
|
|
||||||
});
|
ui.add_space(8.0);
|
||||||
}
|
View::horizontal_line(ui, Colors::item_stroke());
|
||||||
}
|
ui.add_space(8.0);
|
||||||
|
|
||||||
|
// Show modal buttons.
|
||||||
|
ui.scope(|ui| {
|
||||||
|
// Setup spacing between buttons.
|
||||||
|
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
|
||||||
|
|
||||||
|
ui.columns(2, |columns| {
|
||||||
|
columns[0].vertical_centered_justified(|ui| {
|
||||||
|
View::button(
|
||||||
|
ui,
|
||||||
|
t!("modal.cancel"),
|
||||||
|
Colors::white_or_black(false),
|
||||||
|
|| {
|
||||||
|
// Close modal.
|
||||||
|
self.url_edit = "".to_string();
|
||||||
|
self.secret_edit = "".to_string();
|
||||||
|
self.url_error = false;
|
||||||
|
Modal::close();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
columns[1].vertical_centered_justified(|ui| {
|
||||||
|
View::button_ui(
|
||||||
|
ui,
|
||||||
|
if self.id.is_some() {
|
||||||
|
t!("modal.save")
|
||||||
|
} else {
|
||||||
|
t!("modal.add")
|
||||||
|
},
|
||||||
|
Colors::white_or_black(false),
|
||||||
|
|ui| {
|
||||||
|
(on_add)(ui, self);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
ui.add_space(6.0);
|
||||||
|
});
|
||||||
|
|
||||||
|
self.first_draw = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -13,4 +13,7 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
mod ext_conn;
|
mod ext_conn;
|
||||||
pub use ext_conn::*;
|
pub use ext_conn::*;
|
||||||
|
|
||||||
|
mod share_conn;
|
||||||
|
pub use share_conn::*;
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
// Copyright 2024 The Grim Developers
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
use crate::AppConfig;
|
||||||
|
use crate::gui::Colors;
|
||||||
|
use crate::gui::platform::PlatformCallbacks;
|
||||||
|
use crate::gui::views::network::types::ShareConnection;
|
||||||
|
use crate::gui::views::{Modal, QrCodeContent, View};
|
||||||
|
|
||||||
|
/// [`Modal`] content to share connection with QR code.
|
||||||
|
pub struct ShareConnectionContent {
|
||||||
|
/// QR code content.
|
||||||
|
pub qr_details_content: QrCodeContent,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ShareConnectionContent {
|
||||||
|
/// Create new content instance from connection details.
|
||||||
|
pub fn new(details: ShareConnection) -> Result<Self, serde_json::Error> {
|
||||||
|
let details = serde_json::to_string_pretty(&details)?;
|
||||||
|
let c = Self {
|
||||||
|
qr_details_content: QrCodeContent::new(details, false).hide_text().no_copy(),
|
||||||
|
};
|
||||||
|
Ok(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw QR code content.
|
||||||
|
pub fn ui(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
|
||||||
|
let dark_theme = AppConfig::dark_theme().unwrap_or(false);
|
||||||
|
// Set light theme for better scanning.
|
||||||
|
AppConfig::set_dark_theme(false);
|
||||||
|
modal.set_background_color(Colors::FILL_DEEP);
|
||||||
|
crate::setup_visuals(ui.ctx());
|
||||||
|
// Draw QR code content.
|
||||||
|
ui.add_space(6.0);
|
||||||
|
self.qr_details_content.ui(ui, cb);
|
||||||
|
ui.vertical_centered_justified(|ui| {
|
||||||
|
View::button(ui, t!("close"), Colors::white_or_black(false), || {
|
||||||
|
Modal::close();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
ui.add_space(6.0);
|
||||||
|
// Set color theme back.
|
||||||
|
AppConfig::set_dark_theme(dark_theme);
|
||||||
|
crate::setup_visuals(ui.ctx());
|
||||||
|
}
|
||||||
|
}
|
||||||
+209
-175
@@ -12,15 +12,15 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
use egui::{RichText, Rounding, ScrollArea};
|
|
||||||
use egui::scroll_area::ScrollBarVisibility;
|
use egui::scroll_area::ScrollBarVisibility;
|
||||||
|
use egui::{CornerRadius, RichText, ScrollArea, StrokeKind};
|
||||||
use grin_servers::PeerStats;
|
use grin_servers::PeerStats;
|
||||||
|
|
||||||
use crate::gui::Colors;
|
use crate::gui::Colors;
|
||||||
use crate::gui::icons::{AT, CUBE, DEVICES, FLOW_ARROW, HANDSHAKE, PACKAGE, SHARE_NETWORK};
|
use crate::gui::icons::{AT, CUBE, DEVICES, FLOW_ARROW, HANDSHAKE, PACKAGE, SHARE_NETWORK};
|
||||||
use crate::gui::platform::PlatformCallbacks;
|
use crate::gui::platform::PlatformCallbacks;
|
||||||
use crate::gui::views::{Content, View};
|
|
||||||
use crate::gui::views::network::types::{NodeTab, NodeTabType};
|
use crate::gui::views::network::types::{NodeTab, NodeTabType};
|
||||||
|
use crate::gui::views::{Content, View};
|
||||||
use crate::node::{Node, NodeConfig};
|
use crate::node::{Node, NodeConfig};
|
||||||
|
|
||||||
/// Integrated node tab content.
|
/// Integrated node tab content.
|
||||||
@@ -28,193 +28,227 @@ use crate::node::{Node, NodeConfig};
|
|||||||
pub struct NetworkNode;
|
pub struct NetworkNode;
|
||||||
|
|
||||||
impl NodeTab for NetworkNode {
|
impl NodeTab for NetworkNode {
|
||||||
fn get_type(&self) -> NodeTabType {
|
fn get_type(&self) -> NodeTabType {
|
||||||
NodeTabType::Info
|
NodeTabType::Info
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ui(&mut self, ui: &mut egui::Ui, _: &dyn PlatformCallbacks) {
|
fn tab_ui(&mut self, ui: &mut egui::Ui, _: &dyn PlatformCallbacks) {
|
||||||
ScrollArea::vertical()
|
ScrollArea::vertical()
|
||||||
.id_salt("integrated_node_info_scroll")
|
.id_salt("integrated_node_info_scroll")
|
||||||
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
|
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
|
||||||
.auto_shrink([false; 2])
|
.auto_shrink([false; 2])
|
||||||
.show(ui, |ui| {
|
.show(ui, |ui| {
|
||||||
ui.add_space(2.0);
|
ui.add_space(2.0);
|
||||||
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
|
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
|
||||||
// Show node stats content.
|
// Show node stats content.
|
||||||
node_stats_ui(ui);
|
node_stats_ui(ui);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Draw node statistics content.
|
/// Draw node statistics content.
|
||||||
fn node_stats_ui(ui: &mut egui::Ui) {
|
fn node_stats_ui(ui: &mut egui::Ui) {
|
||||||
let server_stats = Node::get_stats();
|
let server_stats = Node::get_stats();
|
||||||
let stats = server_stats.as_ref().unwrap();
|
let stats = server_stats.as_ref().unwrap();
|
||||||
|
|
||||||
// Show header info.
|
// Show header info.
|
||||||
View::sub_title(ui, format!("{} {}", FLOW_ARROW, t!("network_node.header")));
|
View::sub_title(ui, format!("{} {}", FLOW_ARROW, t!("network_node.header")));
|
||||||
ui.columns(2, |columns| {
|
ui.columns(2, |columns| {
|
||||||
columns[0].vertical_centered(|ui| {
|
columns[0].vertical_centered(|ui| {
|
||||||
View::label_box(ui,
|
View::label_box(
|
||||||
stats.header_stats.last_block_h.to_string(),
|
ui,
|
||||||
t!("network_node.hash"),
|
stats.header_stats.last_block_h.to_string(),
|
||||||
[true, false, false, false]);
|
t!("network_node.hash"),
|
||||||
});
|
[true, false, false, false],
|
||||||
columns[1].vertical_centered(|ui| {
|
);
|
||||||
View::label_box(ui,
|
});
|
||||||
stats.header_stats.height.to_string(),
|
columns[1].vertical_centered(|ui| {
|
||||||
t!("network_node.height"),
|
View::label_box(
|
||||||
[false, true, false, false]);
|
ui,
|
||||||
});
|
stats.header_stats.height.to_string(),
|
||||||
});
|
t!("network_node.height"),
|
||||||
ui.columns(2, |columns| {
|
[false, true, false, false],
|
||||||
columns[0].vertical_centered(|ui| {
|
);
|
||||||
View::label_box(ui,
|
});
|
||||||
stats.header_stats.total_difficulty.to_string(),
|
});
|
||||||
t!("network_node.difficulty"),
|
ui.columns(2, |columns| {
|
||||||
[false, false, true, false]);
|
columns[0].vertical_centered(|ui| {
|
||||||
});
|
View::label_box(
|
||||||
columns[1].vertical_centered(|ui| {
|
ui,
|
||||||
let h_ts = stats.header_stats.latest_timestamp.timestamp();
|
stats.header_stats.total_difficulty.to_string(),
|
||||||
let h_time = View::format_time(h_ts);
|
t!("network_node.difficulty"),
|
||||||
View::label_box(ui,
|
[false, false, true, false],
|
||||||
h_time,
|
);
|
||||||
t!("network_node.time"),
|
});
|
||||||
[false, false, false, true]);
|
columns[1].vertical_centered(|ui| {
|
||||||
});
|
let h_ts = stats.header_stats.latest_timestamp.timestamp();
|
||||||
});
|
let h_time = View::format_time(h_ts);
|
||||||
ui.add_space(5.0);
|
View::label_box(
|
||||||
|
ui,
|
||||||
|
h_time,
|
||||||
|
t!("network_node.time"),
|
||||||
|
[false, false, false, true],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
ui.add_space(5.0);
|
||||||
|
|
||||||
// Show block info.
|
// Show block info.
|
||||||
View::sub_title(ui, format!("{} {}", CUBE, t!("network_node.block")));
|
View::sub_title(ui, format!("{} {}", CUBE, t!("network_node.block")));
|
||||||
ui.columns(2, |columns| {
|
ui.columns(2, |columns| {
|
||||||
columns[0].vertical_centered(|ui| {
|
columns[0].vertical_centered(|ui| {
|
||||||
View::label_box(ui,
|
View::label_box(
|
||||||
stats.chain_stats.last_block_h.to_string(),
|
ui,
|
||||||
t!("network_node.hash"),
|
stats.chain_stats.last_block_h.to_string(),
|
||||||
[true, false, false, false]);
|
t!("network_node.hash"),
|
||||||
});
|
[true, false, false, false],
|
||||||
columns[1].vertical_centered(|ui| {
|
);
|
||||||
View::label_box(ui,
|
});
|
||||||
stats.chain_stats.height.to_string(),
|
columns[1].vertical_centered(|ui| {
|
||||||
t!("network_node.height"),
|
View::label_box(
|
||||||
[false, true, false, false]);
|
ui,
|
||||||
});
|
stats.chain_stats.height.to_string(),
|
||||||
});
|
t!("network_node.height"),
|
||||||
ui.columns(2, |columns| {
|
[false, true, false, false],
|
||||||
columns[0].vertical_centered(|ui| {
|
);
|
||||||
View::label_box(ui,
|
});
|
||||||
stats.chain_stats.total_difficulty.to_string(),
|
});
|
||||||
t!("network_node.difficulty"),
|
ui.columns(2, |columns| {
|
||||||
[false, false, true, false]);
|
columns[0].vertical_centered(|ui| {
|
||||||
});
|
View::label_box(
|
||||||
columns[1].vertical_centered(|ui| {
|
ui,
|
||||||
let b_ts = stats.chain_stats.latest_timestamp.timestamp();
|
stats.chain_stats.total_difficulty.to_string(),
|
||||||
let b_time = View::format_time(b_ts);
|
t!("network_node.difficulty"),
|
||||||
View::label_box(ui,
|
[false, false, true, false],
|
||||||
b_time,
|
);
|
||||||
t!("network_node.time"),
|
});
|
||||||
[false, false, false, true]);
|
columns[1].vertical_centered(|ui| {
|
||||||
});
|
let b_ts = stats.chain_stats.latest_timestamp.timestamp();
|
||||||
});
|
let b_time = View::format_time(b_ts);
|
||||||
ui.add_space(5.0);
|
View::label_box(
|
||||||
|
ui,
|
||||||
|
b_time,
|
||||||
|
t!("network_node.time"),
|
||||||
|
[false, false, false, true],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
ui.add_space(5.0);
|
||||||
|
|
||||||
// Show data info.
|
// Show data info.
|
||||||
View::sub_title(ui, format!("{} {}", SHARE_NETWORK, t!("network_node.data")));
|
View::sub_title(ui, format!("{} {}", SHARE_NETWORK, t!("network_node.data")));
|
||||||
ui.columns(2, |columns| {
|
ui.columns(2, |columns| {
|
||||||
columns[0].vertical_centered(|ui| {
|
columns[0].vertical_centered(|ui| {
|
||||||
let tx_stat = match &stats.tx_stats {
|
let tx_stat = match &stats.tx_stats {
|
||||||
None => "0 (0)".to_string(),
|
None => "0 (0)".to_string(),
|
||||||
Some(tx) => format!("{} ({})", tx.tx_pool_size, tx.tx_pool_kernels)
|
Some(tx) => format!("{} ({})", tx.tx_pool_size, tx.tx_pool_kernels),
|
||||||
};
|
};
|
||||||
View::label_box(ui,
|
View::label_box(
|
||||||
tx_stat,
|
ui,
|
||||||
t!("network_node.main_pool"),
|
tx_stat,
|
||||||
[true, false, false, false]);
|
t!("network_node.main_pool"),
|
||||||
});
|
[true, false, false, false],
|
||||||
columns[1].vertical_centered(|ui| {
|
);
|
||||||
let stem_tx_stat = match &stats.tx_stats {
|
});
|
||||||
None => "0 (0)".to_string(),
|
columns[1].vertical_centered(|ui| {
|
||||||
Some(stx) => format!("{} ({})",
|
let stem_tx_stat = match &stats.tx_stats {
|
||||||
stx.stem_pool_size,
|
None => "0 (0)".to_string(),
|
||||||
stx.stem_pool_kernels)
|
Some(stx) => format!("{} ({})", stx.stem_pool_size, stx.stem_pool_kernels),
|
||||||
};
|
};
|
||||||
View::label_box(ui,
|
View::label_box(
|
||||||
stem_tx_stat,
|
ui,
|
||||||
t!("network_node.stem_pool"),
|
stem_tx_stat,
|
||||||
[false, true, false, false]);
|
t!("network_node.stem_pool"),
|
||||||
});
|
[false, true, false, false],
|
||||||
});
|
);
|
||||||
ui.columns(2, |columns| {
|
});
|
||||||
columns[0].vertical_centered(|ui| {
|
});
|
||||||
View::label_box(ui,
|
ui.columns(2, |columns| {
|
||||||
stats.disk_usage_gb.to_string(),
|
columns[0].vertical_centered(|ui| {
|
||||||
t!("network_node.size"),
|
View::label_box(
|
||||||
[false, false, true, false]);
|
ui,
|
||||||
});
|
stats.disk_usage_gb.to_string(),
|
||||||
columns[1].vertical_centered(|ui| {
|
t!("network_node.size"),
|
||||||
let peers_txt = format!("{} ({})",
|
[false, false, true, false],
|
||||||
stats.peer_count,
|
);
|
||||||
NodeConfig::get_max_outbound_peers());
|
});
|
||||||
View::label_box(ui, peers_txt, t!("network_node.peers"), [false, false, false, true]);
|
columns[1].vertical_centered(|ui| {
|
||||||
});
|
let peers_txt = format!(
|
||||||
});
|
"{} ({})",
|
||||||
ui.add_space(5.0);
|
stats.peer_count,
|
||||||
|
NodeConfig::get_max_outbound_peers()
|
||||||
|
);
|
||||||
|
View::label_box(
|
||||||
|
ui,
|
||||||
|
peers_txt,
|
||||||
|
t!("network_node.peers"),
|
||||||
|
[false, false, false, true],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
ui.add_space(5.0);
|
||||||
|
|
||||||
// Show peer stats when available.
|
// Show peer stats when available.
|
||||||
if stats.peer_count > 0 {
|
if stats.peer_count > 0 {
|
||||||
View::sub_title(ui, format!("{} {}", HANDSHAKE, t!("network_node.peers")));
|
View::sub_title(ui, format!("{} {}", HANDSHAKE, t!("network_node.peers")));
|
||||||
let peers = &stats.peer_stats;
|
let peers = &stats.peer_stats;
|
||||||
for (index, ps) in peers.iter().enumerate() {
|
for (index, ps) in peers.iter().enumerate() {
|
||||||
peer_item_ui(ui, ps, View::item_rounding(index, peers.len(), false));
|
peer_item_ui(ui, ps, View::item_rounding(index, peers.len(), false));
|
||||||
}
|
}
|
||||||
ui.add_space(5.0);
|
ui.add_space(5.0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const PEER_ITEM_HEIGHT: f32 = 77.0;
|
const PEER_ITEM_HEIGHT: f32 = 77.0;
|
||||||
|
|
||||||
/// Draw connected peer info item.
|
/// Draw connected peer info item.
|
||||||
fn peer_item_ui(ui: &mut egui::Ui, peer: &PeerStats, rounding: Rounding) {
|
fn peer_item_ui(ui: &mut egui::Ui, peer: &PeerStats, r: CornerRadius) {
|
||||||
let mut rect = ui.available_rect_before_wrap();
|
let mut rect = ui.available_rect_before_wrap();
|
||||||
rect.set_height(PEER_ITEM_HEIGHT);
|
rect.set_height(PEER_ITEM_HEIGHT);
|
||||||
ui.allocate_ui(rect.size(), |ui| {
|
ui.allocate_ui(rect.size(), |ui| {
|
||||||
ui.vertical(|ui| {
|
ui.vertical(|ui| {
|
||||||
ui.add_space(4.0);
|
ui.add_space(4.0);
|
||||||
|
|
||||||
// Draw round background.
|
// Draw round background.
|
||||||
ui.painter().rect(rect, rounding, Colors::fill_lite(), View::item_stroke());
|
ui.painter().rect(
|
||||||
|
rect,
|
||||||
|
r,
|
||||||
|
Colors::fill(),
|
||||||
|
View::item_stroke(),
|
||||||
|
StrokeKind::Outside,
|
||||||
|
);
|
||||||
|
|
||||||
// Draw IP address.
|
// Draw IP address.
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.add_space(7.0);
|
ui.add_space(7.0);
|
||||||
ui.label(RichText::new(&peer.addr)
|
ui.label(
|
||||||
.color(Colors::white_or_black(true))
|
RichText::new(&peer.addr)
|
||||||
.size(17.0));
|
.color(Colors::white_or_black(true))
|
||||||
});
|
.size(17.0),
|
||||||
// Draw difficulty and height.
|
);
|
||||||
ui.horizontal(|ui| {
|
});
|
||||||
ui.add_space(6.0);
|
// Draw difficulty and height.
|
||||||
let diff_text = format!("{} {} {} {}",
|
ui.horizontal(|ui| {
|
||||||
PACKAGE,
|
ui.add_space(6.0);
|
||||||
peer.total_difficulty,
|
let diff_text = format!(
|
||||||
AT,
|
"{} {} {} {}",
|
||||||
peer.height);
|
PACKAGE, peer.total_difficulty, AT, peer.height
|
||||||
ui.label(RichText::new(diff_text)
|
);
|
||||||
.color(Colors::title(false))
|
ui.label(
|
||||||
.size(15.0));
|
RichText::new(diff_text)
|
||||||
});
|
.color(Colors::title(false))
|
||||||
// Draw user-agent.
|
.size(15.0),
|
||||||
ui.horizontal(|ui| {
|
);
|
||||||
ui.add_space(6.0);
|
});
|
||||||
let agent_text = format!("{} {}", DEVICES, &peer.user_agent);
|
// Draw user-agent.
|
||||||
ui.label(RichText::new(agent_text)
|
ui.horizontal(|ui| {
|
||||||
.color(Colors::gray())
|
ui.add_space(6.0);
|
||||||
.size(15.0));
|
let agent_text = format!("{} {}", DEVICES, &peer.user_agent);
|
||||||
});
|
ui.label(RichText::new(agent_text).color(Colors::gray()).size(15.0));
|
||||||
|
});
|
||||||
|
|
||||||
ui.add_space(3.0);
|
ui.add_space(3.0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
+295
-209
@@ -12,252 +12,338 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
use egui::{RichText, ScrollArea};
|
|
||||||
use egui::scroll_area::ScrollBarVisibility;
|
|
||||||
|
|
||||||
use crate::gui::Colors;
|
use crate::gui::Colors;
|
||||||
use crate::gui::icons::ARROW_COUNTER_CLOCKWISE;
|
use crate::gui::icons::{ARROW_COUNTER_CLOCKWISE, TRASH};
|
||||||
use crate::gui::platform::PlatformCallbacks;
|
use crate::gui::platform::PlatformCallbacks;
|
||||||
use crate::gui::views::{Modal, Content, View};
|
use crate::gui::views::network::setup::{
|
||||||
use crate::gui::views::network::setup::{DandelionSetup, NodeSetup, P2PSetup, PoolSetup, StratumSetup};
|
DandelionSetup, NodeSetup, P2PSetup, PoolSetup, StratumSetup,
|
||||||
|
};
|
||||||
use crate::gui::views::network::types::{NodeTab, NodeTabType};
|
use crate::gui::views::network::types::{NodeTab, NodeTabType};
|
||||||
use crate::gui::views::types::{ModalContainer, ModalPosition};
|
use crate::gui::views::types::{ContentContainer, ModalPosition};
|
||||||
|
use crate::gui::views::{Content, Modal, View};
|
||||||
use crate::node::{Node, NodeConfig};
|
use crate::node::{Node, NodeConfig};
|
||||||
|
|
||||||
|
use egui::scroll_area::ScrollBarVisibility;
|
||||||
|
use egui::{RichText, ScrollArea};
|
||||||
|
|
||||||
/// Integrated node settings tab content.
|
/// Integrated node settings tab content.
|
||||||
pub struct NetworkSettings {
|
pub struct NetworkSettings {
|
||||||
/// Integrated node general setup content.
|
/// Integrated node general setup content.
|
||||||
node: NodeSetup,
|
node: NodeSetup,
|
||||||
/// P2P server setup content.
|
/// P2P server setup content.
|
||||||
p2p: P2PSetup,
|
p2p: P2PSetup,
|
||||||
/// Stratum server setup content.
|
/// Stratum server setup content.
|
||||||
stratum: StratumSetup,
|
stratum: StratumSetup,
|
||||||
/// Pool setup content.
|
/// Pool setup content.
|
||||||
pool: PoolSetup,
|
pool: PoolSetup,
|
||||||
/// Dandelion server setup content.
|
/// Dandelion server setup content.
|
||||||
dandelion: DandelionSetup,
|
dandelion: DandelionSetup,
|
||||||
|
|
||||||
/// [`Modal`] identifiers allowed at this ui container.
|
/// Flag to check if reset of data was called.
|
||||||
modal_ids: Vec<&'static str>
|
data_reset: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Identifier for settings reset confirmation [`Modal`].
|
/// Identifier for settings reset confirmation [`Modal`].
|
||||||
pub const RESET_SETTINGS_CONFIRMATION_MODAL: &'static str = "reset_settings_confirmation";
|
pub const RESET_SETTINGS_CONFIRMATION_MODAL: &'static str = "reset_settings_confirmation";
|
||||||
|
|
||||||
impl Default for NetworkSettings {
|
impl Default for NetworkSettings {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
node: NodeSetup::default(),
|
node: NodeSetup::default(),
|
||||||
p2p: P2PSetup::default(),
|
p2p: P2PSetup::default(),
|
||||||
stratum: StratumSetup::default(),
|
stratum: StratumSetup::default(),
|
||||||
pool: PoolSetup::default(),
|
pool: PoolSetup::default(),
|
||||||
dandelion: DandelionSetup::default(),
|
dandelion: DandelionSetup::default(),
|
||||||
modal_ids: vec![
|
data_reset: false,
|
||||||
RESET_SETTINGS_CONFIRMATION_MODAL
|
}
|
||||||
]
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ModalContainer for NetworkSettings {
|
impl ContentContainer for NetworkSettings {
|
||||||
fn modal_ids(&self) -> &Vec<&'static str> {
|
fn modal_ids(&self) -> Vec<&'static str> {
|
||||||
&self.modal_ids
|
vec![RESET_SETTINGS_CONFIRMATION_MODAL]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn modal_ui(&mut self,
|
fn modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal, _: &dyn PlatformCallbacks) {
|
||||||
ui: &mut egui::Ui,
|
match modal.id {
|
||||||
modal: &Modal,
|
RESET_SETTINGS_CONFIRMATION_MODAL => reset_settings_confirmation_modal(ui),
|
||||||
_: &dyn PlatformCallbacks) {
|
_ => {}
|
||||||
match modal.id {
|
}
|
||||||
RESET_SETTINGS_CONFIRMATION_MODAL => reset_settings_confirmation_modal(ui, modal),
|
}
|
||||||
_ => {}
|
|
||||||
}
|
fn container_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||||
}
|
ScrollArea::vertical()
|
||||||
|
.id_salt("node_settings_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| {
|
||||||
|
// Draw node setup section.
|
||||||
|
self.node.ui(ui, cb);
|
||||||
|
|
||||||
|
ui.add_space(6.0);
|
||||||
|
View::horizontal_line(ui, Colors::stroke());
|
||||||
|
ui.add_space(4.0);
|
||||||
|
|
||||||
|
// Draw P2P server setup section.
|
||||||
|
self.p2p.ui(ui, cb);
|
||||||
|
|
||||||
|
ui.add_space(6.0);
|
||||||
|
View::horizontal_line(ui, Colors::stroke());
|
||||||
|
ui.add_space(4.0);
|
||||||
|
|
||||||
|
// Draw Stratum server setup section.
|
||||||
|
self.stratum.ui(ui, cb);
|
||||||
|
|
||||||
|
ui.add_space(6.0);
|
||||||
|
View::horizontal_line(ui, Colors::stroke());
|
||||||
|
ui.add_space(4.0);
|
||||||
|
|
||||||
|
// Draw pool setup section.
|
||||||
|
self.pool.ui(ui, cb);
|
||||||
|
|
||||||
|
ui.add_space(6.0);
|
||||||
|
View::horizontal_line(ui, Colors::stroke());
|
||||||
|
ui.add_space(4.0);
|
||||||
|
|
||||||
|
// Draw Dandelion server setup section.
|
||||||
|
self.dandelion.ui(ui, cb);
|
||||||
|
|
||||||
|
// Draw content to reset the data.
|
||||||
|
if !Node::is_restarting() && !self.data_reset {
|
||||||
|
ui.add_space(4.0);
|
||||||
|
View::horizontal_line(ui, Colors::item_stroke());
|
||||||
|
ui.add_space(6.0);
|
||||||
|
|
||||||
|
self.reset_data_ui(ui);
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.add_space(6.0);
|
||||||
|
View::horizontal_line(ui, Colors::stroke());
|
||||||
|
ui.add_space(6.0);
|
||||||
|
|
||||||
|
// Draw reset settings content.
|
||||||
|
reset_settings_ui(ui);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NodeTab for NetworkSettings {
|
impl NodeTab for NetworkSettings {
|
||||||
fn get_type(&self) -> NodeTabType {
|
fn get_type(&self) -> NodeTabType {
|
||||||
NodeTabType::Settings
|
NodeTabType::Settings
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
fn tab_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||||
// Draw modal content for current ui container.
|
self.ui(ui, cb);
|
||||||
self.current_modal_ui(ui, cb);
|
}
|
||||||
|
|
||||||
ScrollArea::vertical()
|
|
||||||
.id_salt("node_settings_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| {
|
|
||||||
// Draw node setup section.
|
|
||||||
self.node.ui(ui, cb);
|
|
||||||
|
|
||||||
ui.add_space(6.0);
|
|
||||||
View::horizontal_line(ui, Colors::stroke());
|
|
||||||
ui.add_space(4.0);
|
|
||||||
|
|
||||||
// Draw P2P server setup section.
|
|
||||||
self.p2p.ui(ui, cb);
|
|
||||||
|
|
||||||
ui.add_space(6.0);
|
|
||||||
View::horizontal_line(ui, Colors::stroke());
|
|
||||||
ui.add_space(4.0);
|
|
||||||
|
|
||||||
// Draw Stratum server setup section.
|
|
||||||
self.stratum.ui(ui, cb);
|
|
||||||
|
|
||||||
ui.add_space(6.0);
|
|
||||||
View::horizontal_line(ui, Colors::stroke());
|
|
||||||
ui.add_space(4.0);
|
|
||||||
|
|
||||||
// Draw pool setup section.
|
|
||||||
self.pool.ui(ui, cb);
|
|
||||||
|
|
||||||
ui.add_space(6.0);
|
|
||||||
View::horizontal_line(ui, Colors::stroke());
|
|
||||||
ui.add_space(4.0);
|
|
||||||
|
|
||||||
// Draw Dandelion server setup section.
|
|
||||||
self.dandelion.ui(ui, cb);
|
|
||||||
|
|
||||||
ui.add_space(6.0);
|
|
||||||
View::horizontal_line(ui, Colors::stroke());
|
|
||||||
ui.add_space(6.0);
|
|
||||||
|
|
||||||
// Draw reset settings content.
|
|
||||||
reset_settings_ui(ui);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NetworkSettings {
|
impl NetworkSettings {
|
||||||
/// Reminder to restart enabled node to show on edit setting at [`Modal`].
|
/// Reminder to restart enabled node to show on edit setting at [`Modal`].
|
||||||
pub fn node_restart_required_ui(ui: &mut egui::Ui) {
|
pub fn node_restart_required_ui(ui: &mut egui::Ui) {
|
||||||
if Node::is_running() {
|
if Node::is_running() {
|
||||||
ui.add_space(12.0);
|
ui.add_space(12.0);
|
||||||
ui.label(RichText::new(t!("network_settings.restart_node_required"))
|
ui.label(
|
||||||
.size(16.0)
|
RichText::new(t!("network_settings.restart_node_required"))
|
||||||
.color(Colors::green())
|
.size(16.0)
|
||||||
);
|
.color(Colors::green()),
|
||||||
}
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Draw IP addresses as radio buttons.
|
/// Draw IP addresses as radio buttons.
|
||||||
pub fn ip_addrs_ui(ui: &mut egui::Ui,
|
pub fn ip_addrs_ui(
|
||||||
saved_ip: &String,
|
ui: &mut egui::Ui,
|
||||||
ips: &Vec<String>,
|
saved_ip: &String,
|
||||||
on_change: impl FnOnce(&String)) {
|
ips: &Vec<String>,
|
||||||
let mut selected_ip = saved_ip;
|
on_change: impl FnOnce(&String),
|
||||||
|
) {
|
||||||
|
let mut all = NodeConfig::ALL_INTERFACES.to_string();
|
||||||
|
let all_ips = saved_ip == &all || saved_ip == &format!("[{}]", &all);
|
||||||
|
if all_ips {
|
||||||
|
all = saved_ip.clone();
|
||||||
|
}
|
||||||
|
|
||||||
// Set first IP address as current if saved is not present at system.
|
let mut selected_ip = saved_ip.clone();
|
||||||
if !ips.contains(saved_ip) {
|
|
||||||
selected_ip = ips.get(0).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
ui.add_space(2.0);
|
let mut listen_all_changed = false;
|
||||||
|
View::checkbox(ui, all_ips, t!("network_settings.ip_listen_all"), || {
|
||||||
|
listen_all_changed = true;
|
||||||
|
});
|
||||||
|
if listen_all_changed {
|
||||||
|
let new_ip = if all_ips {
|
||||||
|
ips.get(0).unwrap_or(&all).clone()
|
||||||
|
} else {
|
||||||
|
all.clone()
|
||||||
|
};
|
||||||
|
selected_ip = new_ip;
|
||||||
|
}
|
||||||
|
|
||||||
// Show available IP addresses on the system.
|
ui.add_space(8.0);
|
||||||
let _ = ips.chunks(2).map(|x| {
|
|
||||||
if x.len() == 2 {
|
|
||||||
ui.columns(2, |columns| {
|
|
||||||
let ip_left = x.get(0).unwrap();
|
|
||||||
columns[0].vertical_centered(|ui| {
|
|
||||||
View::radio_value(ui, &mut selected_ip, ip_left, ip_left.to_string());
|
|
||||||
});
|
|
||||||
let ip_right = x.get(1).unwrap();
|
|
||||||
columns[1].vertical_centered(|ui| {
|
|
||||||
View::radio_value(ui, &mut selected_ip, ip_right, ip_right.to_string());
|
|
||||||
})
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
let ip = x.get(0).unwrap();
|
|
||||||
View::radio_value(ui, &mut selected_ip, ip, ip.to_string());
|
|
||||||
}
|
|
||||||
ui.add_space(12.0);
|
|
||||||
}).collect::<Vec<_>>();
|
|
||||||
|
|
||||||
if saved_ip != selected_ip {
|
if selected_ip != all {
|
||||||
(on_change)(&selected_ip.to_string());
|
// Set first IP address as current if saved is not present at system.
|
||||||
}
|
if !ips.contains(&saved_ip) {
|
||||||
}
|
selected_ip = ips.get(0).unwrap().clone();
|
||||||
|
}
|
||||||
|
|
||||||
/// Show message when IP addresses are not available at system.
|
// Show available IP addresses on the system.
|
||||||
pub fn no_ip_address_ui(ui: &mut egui::Ui) {
|
let _ = ips
|
||||||
ui.vertical_centered(|ui| {
|
.chunks(2)
|
||||||
ui.label(RichText::new(t!("network.no_ips"))
|
.map(|x| {
|
||||||
.size(16.0)
|
if x.len() == 2 {
|
||||||
.color(Colors::inactive_text())
|
ui.columns(2, |columns| {
|
||||||
);
|
let ip_left = x.get(0).unwrap();
|
||||||
ui.add_space(6.0);
|
let val = if all_ips {
|
||||||
});
|
&mut ip_left.clone()
|
||||||
}
|
} else {
|
||||||
|
&mut selected_ip
|
||||||
|
};
|
||||||
|
columns[0].vertical_centered(|ui| {
|
||||||
|
View::radio_value(ui, val, ip_left.clone(), ip_left.to_string());
|
||||||
|
});
|
||||||
|
let ip_right = x.get(1).unwrap();
|
||||||
|
let val = if all_ips {
|
||||||
|
&mut ip_right.clone()
|
||||||
|
} else {
|
||||||
|
&mut selected_ip
|
||||||
|
};
|
||||||
|
columns[1].vertical_centered(|ui| {
|
||||||
|
View::radio_value(ui, val, ip_right.clone(), ip_right.to_string());
|
||||||
|
})
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
let ip = x.get(0).unwrap();
|
||||||
|
let val = if all_ips {
|
||||||
|
&mut ip.clone()
|
||||||
|
} else {
|
||||||
|
&mut selected_ip
|
||||||
|
};
|
||||||
|
View::radio_value(ui, val, ip.clone(), ip.to_string());
|
||||||
|
}
|
||||||
|
ui.add_space(12.0);
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
}
|
||||||
|
|
||||||
|
if saved_ip != &selected_ip {
|
||||||
|
on_change(&selected_ip.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Show message when IP addresses are not available at system.
|
||||||
|
pub fn no_ip_address_ui(ui: &mut egui::Ui) {
|
||||||
|
ui.vertical_centered(|ui| {
|
||||||
|
ui.label(
|
||||||
|
RichText::new(t!("network.no_ips"))
|
||||||
|
.size(16.0)
|
||||||
|
.color(Colors::inactive_text()),
|
||||||
|
);
|
||||||
|
ui.add_space(6.0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw content to reset data.
|
||||||
|
fn reset_data_ui(&mut self, ui: &mut egui::Ui) {
|
||||||
|
ui.add_space(4.0);
|
||||||
|
View::colored_text_button(
|
||||||
|
ui,
|
||||||
|
format!("{} {}", TRASH, t!("network_settings.reset_data")),
|
||||||
|
Colors::red(),
|
||||||
|
Colors::white_or_black(false),
|
||||||
|
|| {
|
||||||
|
Node::reset_data(false);
|
||||||
|
self.data_reset = true;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
ui.add_space(6.0);
|
||||||
|
ui.label(
|
||||||
|
RichText::new(t!("network_settings.reset_data_desc"))
|
||||||
|
.size(16.0)
|
||||||
|
.color(Colors::inactive_text()),
|
||||||
|
);
|
||||||
|
ui.add_space(4.0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Draw button to reset integrated node settings to default values.
|
/// Draw button to reset integrated node settings to default values.
|
||||||
fn reset_settings_ui(ui: &mut egui::Ui) {
|
fn reset_settings_ui(ui: &mut egui::Ui) {
|
||||||
ui.vertical_centered(|ui| {
|
ui.label(
|
||||||
ui.label(RichText::new(t!("network_settings.reset_settings_desc"))
|
RichText::new(t!("network_settings.reset_settings_desc"))
|
||||||
.size(16.0)
|
.size(16.0)
|
||||||
.color(Colors::text(false)));
|
.color(Colors::text(false)),
|
||||||
ui.add_space(8.0);
|
);
|
||||||
let button_text = format!("{} {}",
|
ui.add_space(8.0);
|
||||||
ARROW_COUNTER_CLOCKWISE,
|
let button_text = format!(
|
||||||
t!("network_settings.reset_settings"));
|
"{} {}",
|
||||||
View::action_button(ui, button_text, || {
|
ARROW_COUNTER_CLOCKWISE,
|
||||||
// Show modal to confirm settings reset.
|
t!("network_settings.reset_settings")
|
||||||
Modal::new(RESET_SETTINGS_CONFIRMATION_MODAL)
|
);
|
||||||
.position(ModalPosition::Center)
|
View::action_button(ui, button_text, || {
|
||||||
.title(t!("confirmation"))
|
// Show modal to confirm settings reset.
|
||||||
.show();
|
Modal::new(RESET_SETTINGS_CONFIRMATION_MODAL)
|
||||||
});
|
.position(ModalPosition::Center)
|
||||||
|
.title(t!("confirmation"))
|
||||||
// Show reminder to restart enabled node.
|
.show();
|
||||||
if Node::is_running() {
|
});
|
||||||
ui.add_space(12.0);
|
|
||||||
ui.label(RichText::new(t!("network_settings.restart_node_required"))
|
|
||||||
.size(16.0)
|
|
||||||
.color(Colors::gray())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
ui.add_space(12.0);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
// Show reminder to restart enabled node.
|
||||||
|
if Node::is_running() {
|
||||||
|
ui.add_space(12.0);
|
||||||
|
ui.label(
|
||||||
|
RichText::new(t!("network_settings.restart_node_required"))
|
||||||
|
.size(16.0)
|
||||||
|
.color(Colors::gray()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
ui.add_space(10.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Confirmation to reset settings to default values.
|
/// Confirmation to reset settings to default values.
|
||||||
fn reset_settings_confirmation_modal(ui: &mut egui::Ui, modal: &Modal) {
|
fn reset_settings_confirmation_modal(ui: &mut egui::Ui) {
|
||||||
ui.add_space(6.0);
|
ui.add_space(6.0);
|
||||||
ui.vertical_centered(|ui| {
|
ui.vertical_centered(|ui| {
|
||||||
let reset_text = format!("{}?", t!("network_settings.reset_settings_desc"));
|
let reset_text = format!("{}?", t!("network_settings.reset_settings_desc"));
|
||||||
ui.label(RichText::new(reset_text)
|
ui.label(
|
||||||
.size(17.0)
|
RichText::new(reset_text)
|
||||||
.color(Colors::text(false)));
|
.size(17.0)
|
||||||
ui.add_space(8.0);
|
.color(Colors::text(false)),
|
||||||
});
|
);
|
||||||
|
ui.add_space(8.0);
|
||||||
|
});
|
||||||
|
|
||||||
// Show modal buttons.
|
// Show modal buttons.
|
||||||
ui.scope(|ui| {
|
ui.scope(|ui| {
|
||||||
// Setup spacing between buttons.
|
// Setup spacing between buttons.
|
||||||
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
|
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
|
||||||
|
|
||||||
ui.columns(2, |columns| {
|
ui.columns(2, |columns| {
|
||||||
columns[0].vertical_centered_justified(|ui| {
|
columns[0].vertical_centered_justified(|ui| {
|
||||||
View::button(ui, t!("network_settings.reset"), Colors::white_or_black(false), || {
|
View::button(
|
||||||
NodeConfig::reset_to_default();
|
ui,
|
||||||
modal.close();
|
t!("network_settings.reset"),
|
||||||
});
|
Colors::white_or_black(false),
|
||||||
});
|
|| {
|
||||||
columns[1].vertical_centered_justified(|ui| {
|
NodeConfig::reset_to_default();
|
||||||
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
|
Modal::close();
|
||||||
modal.close();
|
},
|
||||||
});
|
);
|
||||||
});
|
});
|
||||||
});
|
columns[1].vertical_centered_justified(|ui| {
|
||||||
ui.add_space(6.0);
|
View::button(
|
||||||
});
|
ui,
|
||||||
}
|
t!("modal.cancel"),
|
||||||
|
Colors::white_or_black(false),
|
||||||
|
|| {
|
||||||
|
Modal::close();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
ui.add_space(6.0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -17,27 +17,24 @@ use egui::{Id, RichText};
|
|||||||
use crate::gui::Colors;
|
use crate::gui::Colors;
|
||||||
use crate::gui::icons::{CLOCK_COUNTDOWN, GRAPH, TIMER, WATCH};
|
use crate::gui::icons::{CLOCK_COUNTDOWN, GRAPH, TIMER, WATCH};
|
||||||
use crate::gui::platform::PlatformCallbacks;
|
use crate::gui::platform::PlatformCallbacks;
|
||||||
use crate::gui::views::{Modal, View};
|
use crate::gui::views::network::NetworkSettings;
|
||||||
use crate::gui::views::network::settings::NetworkSettings;
|
use crate::gui::views::types::{ContentContainer, ModalPosition};
|
||||||
use crate::gui::views::types::{ModalContainer, ModalPosition, TextEditOptions};
|
use crate::gui::views::{Modal, TextEdit, View};
|
||||||
use crate::node::NodeConfig;
|
use crate::node::NodeConfig;
|
||||||
|
|
||||||
/// Dandelion server setup section content.
|
/// Dandelion server setup section content.
|
||||||
pub struct DandelionSetup {
|
pub struct DandelionSetup {
|
||||||
/// Epoch duration value in seconds.
|
/// Epoch duration value in seconds.
|
||||||
epoch_edit: String,
|
epoch_edit: String,
|
||||||
|
|
||||||
/// Embargo expiration time value in seconds to fluff and broadcast if tx not seen on network.
|
/// Embargo expiration time value in seconds to fluff and broadcast if tx not seen on network.
|
||||||
embargo_edit: String,
|
embargo_edit: String,
|
||||||
|
|
||||||
/// Aggregation period value in seconds.
|
/// Aggregation period value in seconds.
|
||||||
aggregation_edit: String,
|
aggregation_edit: String,
|
||||||
|
|
||||||
/// Stem phase probability value (stem 90% of the time, fluff 10% of the time by default).
|
/// Stem phase probability value (stem 90% of the time, fluff 10% of the time by default).
|
||||||
stem_prob_edit: String,
|
stem_prob_edit: String,
|
||||||
|
|
||||||
/// [`Modal`] identifiers allowed at this ui container.
|
|
||||||
modal_ids: Vec<&'static str>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Identifier epoch duration value [`Modal`].
|
/// Identifier epoch duration value [`Modal`].
|
||||||
@@ -50,392 +47,453 @@ pub const AGGREGATION_MODAL: &'static str = "aggregation_secs";
|
|||||||
pub const STEM_PROBABILITY_MODAL: &'static str = "stem_probability";
|
pub const STEM_PROBABILITY_MODAL: &'static str = "stem_probability";
|
||||||
|
|
||||||
impl Default for DandelionSetup {
|
impl Default for DandelionSetup {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
epoch_edit: NodeConfig::get_dandelion_epoch(),
|
epoch_edit: NodeConfig::get_dandelion_epoch(),
|
||||||
embargo_edit: NodeConfig::get_reorg_cache_period(),
|
embargo_edit: NodeConfig::get_reorg_cache_period(),
|
||||||
aggregation_edit: NodeConfig::get_dandelion_aggregation(),
|
aggregation_edit: NodeConfig::get_dandelion_aggregation(),
|
||||||
stem_prob_edit: NodeConfig::get_stem_probability(),
|
stem_prob_edit: NodeConfig::get_stem_probability(),
|
||||||
modal_ids: vec![
|
}
|
||||||
EPOCH_MODAL,
|
}
|
||||||
EMBARGO_MODAL,
|
|
||||||
AGGREGATION_MODAL,
|
|
||||||
STEM_PROBABILITY_MODAL
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ModalContainer for DandelionSetup {
|
impl ContentContainer for DandelionSetup {
|
||||||
fn modal_ids(&self) -> &Vec<&'static str> {
|
fn modal_ids(&self) -> Vec<&'static str> {
|
||||||
&self.modal_ids
|
vec![
|
||||||
}
|
EPOCH_MODAL,
|
||||||
|
EMBARGO_MODAL,
|
||||||
|
AGGREGATION_MODAL,
|
||||||
|
STEM_PROBABILITY_MODAL,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
fn modal_ui(&mut self,
|
fn modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
|
||||||
ui: &mut egui::Ui,
|
match modal.id {
|
||||||
modal: &Modal,
|
EPOCH_MODAL => self.epoch_modal(ui, modal, cb),
|
||||||
cb: &dyn PlatformCallbacks) {
|
EMBARGO_MODAL => self.embargo_modal(ui, modal, cb),
|
||||||
match modal.id {
|
AGGREGATION_MODAL => self.aggregation_modal(ui, modal, cb),
|
||||||
EPOCH_MODAL => self.epoch_modal(ui, modal, cb),
|
STEM_PROBABILITY_MODAL => self.stem_prob_modal(ui, modal, cb),
|
||||||
EMBARGO_MODAL => self.embargo_modal(ui, modal, cb),
|
_ => {}
|
||||||
AGGREGATION_MODAL => self.aggregation_modal(ui, modal, cb),
|
}
|
||||||
STEM_PROBABILITY_MODAL => self.stem_prob_modal(ui, modal, cb),
|
}
|
||||||
_ => {}
|
|
||||||
}
|
fn container_ui(&mut self, ui: &mut egui::Ui, _: &dyn PlatformCallbacks) {
|
||||||
}
|
View::sub_title(ui, format!("{} {}", GRAPH, "Dandelion"));
|
||||||
|
View::horizontal_line(ui, Colors::stroke());
|
||||||
|
ui.add_space(6.0);
|
||||||
|
|
||||||
|
ui.vertical_centered(|ui| {
|
||||||
|
// Show epoch duration setup.
|
||||||
|
self.epoch_ui(ui);
|
||||||
|
|
||||||
|
ui.add_space(6.0);
|
||||||
|
View::horizontal_line(ui, Colors::item_stroke());
|
||||||
|
ui.add_space(6.0);
|
||||||
|
|
||||||
|
// Show embargo expiration time setup.
|
||||||
|
self.embargo_ui(ui);
|
||||||
|
|
||||||
|
ui.add_space(6.0);
|
||||||
|
View::horizontal_line(ui, Colors::item_stroke());
|
||||||
|
ui.add_space(6.0);
|
||||||
|
|
||||||
|
// Show aggregation period setup.
|
||||||
|
self.aggregation_ui(ui);
|
||||||
|
|
||||||
|
ui.add_space(6.0);
|
||||||
|
View::horizontal_line(ui, Colors::item_stroke());
|
||||||
|
ui.add_space(6.0);
|
||||||
|
|
||||||
|
// Show Stem phase probability setup.
|
||||||
|
self.stem_prob_ui(ui);
|
||||||
|
|
||||||
|
ui.add_space(6.0);
|
||||||
|
View::horizontal_line(ui, Colors::item_stroke());
|
||||||
|
ui.add_space(8.0);
|
||||||
|
|
||||||
|
// Show setup to always stem our txs.
|
||||||
|
let always_stem = NodeConfig::always_stem_our_txs();
|
||||||
|
View::checkbox(ui, always_stem, t!("network_settings.stem_txs"), || {
|
||||||
|
NodeConfig::toggle_always_stem_our_txs();
|
||||||
|
});
|
||||||
|
ui.add_space(6.0);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DandelionSetup {
|
impl DandelionSetup {
|
||||||
pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
/// Draw epoch duration setup content.
|
||||||
// Draw modal content for current ui container.
|
fn epoch_ui(&mut self, ui: &mut egui::Ui) {
|
||||||
self.current_modal_ui(ui, cb);
|
ui.label(
|
||||||
|
RichText::new(t!("network_settings.epoch_duration"))
|
||||||
|
.size(16.0)
|
||||||
|
.color(Colors::gray()),
|
||||||
|
);
|
||||||
|
ui.add_space(6.0);
|
||||||
|
|
||||||
View::sub_title(ui, format!("{} {}", GRAPH, "Dandelion"));
|
let epoch = NodeConfig::get_dandelion_epoch();
|
||||||
View::horizontal_line(ui, Colors::stroke());
|
View::button(
|
||||||
ui.add_space(6.0);
|
ui,
|
||||||
|
format!("{} {}", WATCH, &epoch),
|
||||||
|
Colors::white_or_black(false),
|
||||||
|
|| {
|
||||||
|
// Setup values for modal.
|
||||||
|
self.epoch_edit = epoch;
|
||||||
|
// Show epoch setup modal.
|
||||||
|
Modal::new(EPOCH_MODAL)
|
||||||
|
.position(ModalPosition::CenterTop)
|
||||||
|
.title(t!("network_settings.change_value"))
|
||||||
|
.show();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
ui.add_space(6.0);
|
||||||
|
}
|
||||||
|
|
||||||
ui.vertical_centered(|ui| {
|
/// Draw epoch duration [`Modal`] content.
|
||||||
// Show epoch duration setup.
|
fn epoch_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
|
||||||
self.epoch_ui(ui, cb);
|
// Save button callback.
|
||||||
|
let on_save = |c: &mut DandelionSetup| {
|
||||||
|
if let Ok(epoch) = c.epoch_edit.parse::<u16>() {
|
||||||
|
NodeConfig::save_dandelion_epoch(epoch);
|
||||||
|
Modal::close();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
ui.add_space(6.0);
|
ui.add_space(6.0);
|
||||||
View::horizontal_line(ui, Colors::item_stroke());
|
ui.vertical_centered(|ui| {
|
||||||
ui.add_space(6.0);
|
ui.label(
|
||||||
|
RichText::new(t!("network_settings.epoch_duration"))
|
||||||
|
.size(17.0)
|
||||||
|
.color(Colors::gray()),
|
||||||
|
);
|
||||||
|
ui.add_space(8.0);
|
||||||
|
|
||||||
// Show embargo expiration time setup.
|
// Draw epoch text edit.
|
||||||
self.embargo_ui(ui, cb);
|
let mut epoch_edit = TextEdit::new(Id::from(modal.id)).h_center().numeric();
|
||||||
|
epoch_edit.ui(ui, &mut self.epoch_edit, cb);
|
||||||
|
if epoch_edit.enter_pressed {
|
||||||
|
on_save(self);
|
||||||
|
}
|
||||||
|
|
||||||
ui.add_space(6.0);
|
// Show error when specified value is not valid or reminder to restart enabled node.
|
||||||
View::horizontal_line(ui, Colors::item_stroke());
|
if self.epoch_edit.parse::<u16>().is_err() {
|
||||||
ui.add_space(6.0);
|
ui.add_space(12.0);
|
||||||
|
ui.label(
|
||||||
|
RichText::new(t!("network_settings.not_valid_value"))
|
||||||
|
.size(17.0)
|
||||||
|
.color(Colors::red()),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
NetworkSettings::node_restart_required_ui(ui);
|
||||||
|
}
|
||||||
|
ui.add_space(12.0);
|
||||||
|
});
|
||||||
|
|
||||||
// Show aggregation period setup.
|
// Show modal buttons.
|
||||||
self.aggregation_ui(ui, cb);
|
ui.scope(|ui| {
|
||||||
|
// Setup spacing between buttons.
|
||||||
|
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
|
||||||
|
|
||||||
ui.add_space(6.0);
|
ui.columns(2, |columns| {
|
||||||
View::horizontal_line(ui, Colors::item_stroke());
|
columns[0].vertical_centered_justified(|ui| {
|
||||||
ui.add_space(6.0);
|
View::button(
|
||||||
|
ui,
|
||||||
|
t!("modal.cancel"),
|
||||||
|
Colors::white_or_black(false),
|
||||||
|
|| {
|
||||||
|
// Close modal.
|
||||||
|
Modal::close();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
columns[1].vertical_centered_justified(|ui| {
|
||||||
|
View::button(ui, t!("modal.save"), Colors::white_or_black(false), || {
|
||||||
|
on_save(self);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
ui.add_space(6.0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Show Stem phase probability setup.
|
/// Draw embargo expiration time setup content.
|
||||||
self.stem_prob_ui(ui, cb);
|
fn embargo_ui(&mut self, ui: &mut egui::Ui) {
|
||||||
|
ui.label(
|
||||||
|
RichText::new(t!("network_settings.embargo_timer"))
|
||||||
|
.size(16.0)
|
||||||
|
.color(Colors::gray()),
|
||||||
|
);
|
||||||
|
ui.add_space(6.0);
|
||||||
|
|
||||||
ui.add_space(6.0);
|
let embargo = NodeConfig::get_dandelion_embargo();
|
||||||
View::horizontal_line(ui, Colors::item_stroke());
|
View::button(
|
||||||
ui.add_space(8.0);
|
ui,
|
||||||
|
format!("{} {}", TIMER, &embargo),
|
||||||
|
Colors::white_or_black(false),
|
||||||
|
|| {
|
||||||
|
self.embargo_edit = embargo;
|
||||||
|
// Show embargo setup modal.
|
||||||
|
Modal::new(EMBARGO_MODAL)
|
||||||
|
.position(ModalPosition::CenterTop)
|
||||||
|
.title(t!("network_settings.change_value"))
|
||||||
|
.show();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
ui.add_space(6.0);
|
||||||
|
}
|
||||||
|
|
||||||
// Show setup to always stem our txs.
|
/// Draw epoch duration [`Modal`] content.
|
||||||
let always_stem = NodeConfig::always_stem_our_txs();
|
fn embargo_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
|
||||||
View::checkbox(ui, always_stem, t!("network_settings.stem_txs"), || {
|
// Save button callback.
|
||||||
NodeConfig::toggle_always_stem_our_txs();
|
let on_save = |c: &mut DandelionSetup| {
|
||||||
});
|
if let Ok(embargo) = c.embargo_edit.parse::<u16>() {
|
||||||
ui.add_space(6.0);
|
NodeConfig::save_dandelion_embargo(embargo);
|
||||||
});
|
Modal::close();
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/// Draw epoch duration setup content.
|
ui.add_space(6.0);
|
||||||
fn epoch_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
ui.vertical_centered(|ui| {
|
||||||
ui.label(RichText::new(t!("network_settings.epoch_duration"))
|
ui.label(
|
||||||
.size(16.0)
|
RichText::new(t!("network_settings.embargo_timer"))
|
||||||
.color(Colors::gray())
|
.size(17.0)
|
||||||
);
|
.color(Colors::gray()),
|
||||||
ui.add_space(6.0);
|
);
|
||||||
|
ui.add_space(8.0);
|
||||||
|
|
||||||
let epoch = NodeConfig::get_dandelion_epoch();
|
// Draw embargo text edit.
|
||||||
View::button(ui, format!("{} {}", WATCH, &epoch), Colors::white_or_black(false), || {
|
let mut embargo_edit = TextEdit::new(Id::from(modal.id)).h_center().numeric();
|
||||||
// Setup values for modal.
|
embargo_edit.ui(ui, &mut self.embargo_edit, cb);
|
||||||
self.epoch_edit = epoch;
|
if embargo_edit.enter_pressed {
|
||||||
// Show epoch setup modal.
|
on_save(self);
|
||||||
Modal::new(EPOCH_MODAL)
|
}
|
||||||
.position(ModalPosition::CenterTop)
|
|
||||||
.title(t!("network_settings.change_value"))
|
|
||||||
.show();
|
|
||||||
cb.show_keyboard();
|
|
||||||
});
|
|
||||||
ui.add_space(6.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Draw epoch duration [`Modal`] content.
|
// Show error when specified value is not valid or reminder to restart enabled node.
|
||||||
fn epoch_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
|
if self.embargo_edit.parse::<u16>().is_err() {
|
||||||
ui.add_space(6.0);
|
ui.add_space(12.0);
|
||||||
ui.vertical_centered(|ui| {
|
ui.label(
|
||||||
ui.label(RichText::new(t!("network_settings.epoch_duration"))
|
RichText::new(t!("network_settings.not_valid_value"))
|
||||||
.size(17.0)
|
.size(17.0)
|
||||||
.color(Colors::gray()));
|
.color(Colors::red()),
|
||||||
ui.add_space(8.0);
|
);
|
||||||
|
} else {
|
||||||
|
NetworkSettings::node_restart_required_ui(ui);
|
||||||
|
}
|
||||||
|
ui.add_space(12.0);
|
||||||
|
});
|
||||||
|
|
||||||
// Draw epoch text edit.
|
// Show modal buttons.
|
||||||
let mut epoch_edit_opts = TextEditOptions::new(Id::from(modal.id)).h_center();
|
ui.scope(|ui| {
|
||||||
View::text_edit(ui, cb, &mut self.epoch_edit, &mut epoch_edit_opts);
|
// Setup spacing between buttons.
|
||||||
|
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
|
||||||
|
|
||||||
// Show error when specified value is not valid or reminder to restart enabled node.
|
ui.columns(2, |columns| {
|
||||||
if self.epoch_edit.parse::<u16>().is_err() {
|
columns[0].vertical_centered_justified(|ui| {
|
||||||
ui.add_space(12.0);
|
View::button(
|
||||||
ui.label(RichText::new(t!("network_settings.not_valid_value"))
|
ui,
|
||||||
.size(17.0)
|
t!("modal.cancel"),
|
||||||
.color(Colors::red()));
|
Colors::white_or_black(false),
|
||||||
} else {
|
|| {
|
||||||
NetworkSettings::node_restart_required_ui(ui);
|
// Close modal.
|
||||||
}
|
Modal::close();
|
||||||
ui.add_space(12.0);
|
},
|
||||||
});
|
);
|
||||||
|
});
|
||||||
|
columns[1].vertical_centered_justified(|ui| {
|
||||||
|
View::button(ui, t!("modal.save"), Colors::white_or_black(false), || {
|
||||||
|
on_save(self);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
ui.add_space(6.0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Show modal buttons.
|
/// Draw aggregation period setup content.
|
||||||
ui.scope(|ui| {
|
fn aggregation_ui(&mut self, ui: &mut egui::Ui) {
|
||||||
// Setup spacing between buttons.
|
ui.label(
|
||||||
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
|
RichText::new(t!("network_settings.aggregation_period"))
|
||||||
|
.size(16.0)
|
||||||
|
.color(Colors::gray()),
|
||||||
|
);
|
||||||
|
ui.add_space(6.0);
|
||||||
|
|
||||||
// Save button callback.
|
let ag = NodeConfig::get_dandelion_aggregation();
|
||||||
let on_save = || {
|
View::button(
|
||||||
if let Ok(epoch) = self.epoch_edit.parse::<u16>() {
|
ui,
|
||||||
NodeConfig::save_dandelion_epoch(epoch);
|
format!("{} {}", CLOCK_COUNTDOWN, &ag),
|
||||||
cb.hide_keyboard();
|
Colors::white_or_black(false),
|
||||||
modal.close();
|
|| {
|
||||||
}
|
// Setup values for modal.
|
||||||
};
|
self.aggregation_edit = ag;
|
||||||
|
// Show aggregation setup modal.
|
||||||
|
Modal::new(AGGREGATION_MODAL)
|
||||||
|
.position(ModalPosition::CenterTop)
|
||||||
|
.title(t!("network_settings.change_value"))
|
||||||
|
.show();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
ui.add_space(6.0);
|
||||||
|
}
|
||||||
|
|
||||||
ui.columns(2, |columns| {
|
/// Draw aggregation period [`Modal`] content.
|
||||||
columns[0].vertical_centered_justified(|ui| {
|
fn aggregation_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
|
||||||
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
|
// Save button callback.
|
||||||
// Close modal.
|
let on_save = |c: &mut DandelionSetup| {
|
||||||
cb.hide_keyboard();
|
if let Ok(embargo) = c.aggregation_edit.parse::<u16>() {
|
||||||
modal.close();
|
NodeConfig::save_dandelion_aggregation(embargo);
|
||||||
});
|
Modal::close();
|
||||||
});
|
}
|
||||||
columns[1].vertical_centered_justified(|ui| {
|
};
|
||||||
View::button(ui, t!("modal.save"), Colors::white_or_black(false), on_save);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
ui.add_space(6.0);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Draw embargo expiration time setup content.
|
ui.add_space(6.0);
|
||||||
fn embargo_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
ui.vertical_centered(|ui| {
|
||||||
ui.label(RichText::new(t!("network_settings.embargo_timer"))
|
ui.label(
|
||||||
.size(16.0)
|
RichText::new(t!("network_settings.aggregation_period"))
|
||||||
.color(Colors::gray())
|
.size(17.0)
|
||||||
);
|
.color(Colors::gray()),
|
||||||
ui.add_space(6.0);
|
);
|
||||||
|
ui.add_space(8.0);
|
||||||
|
|
||||||
let embargo = NodeConfig::get_dandelion_embargo();
|
// Draw aggregation period text edit.
|
||||||
View::button(ui, format!("{} {}", TIMER, &embargo), Colors::white_or_black(false), || {
|
let mut aggregation_edit = TextEdit::new(Id::from(modal.id)).h_center().numeric();
|
||||||
self.embargo_edit = embargo;
|
aggregation_edit.ui(ui, &mut self.aggregation_edit, cb);
|
||||||
// Show embargo setup modal.
|
if aggregation_edit.enter_pressed {
|
||||||
Modal::new(EMBARGO_MODAL)
|
on_save(self);
|
||||||
.position(ModalPosition::CenterTop)
|
}
|
||||||
.title(t!("network_settings.change_value"))
|
|
||||||
.show();
|
|
||||||
cb.show_keyboard();
|
|
||||||
});
|
|
||||||
ui.add_space(6.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Draw epoch duration [`Modal`] content.
|
// Show error when specified value is not valid or reminder to restart enabled node.
|
||||||
fn embargo_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
|
if self.aggregation_edit.parse::<u16>().is_err() {
|
||||||
ui.add_space(6.0);
|
ui.add_space(12.0);
|
||||||
ui.vertical_centered(|ui| {
|
ui.label(
|
||||||
ui.label(RichText::new(t!("network_settings.embargo_timer"))
|
RichText::new(t!("network_settings.not_valid_value"))
|
||||||
.size(17.0)
|
.size(17.0)
|
||||||
.color(Colors::gray()));
|
.color(Colors::red()),
|
||||||
ui.add_space(8.0);
|
);
|
||||||
|
} else {
|
||||||
|
NetworkSettings::node_restart_required_ui(ui);
|
||||||
|
}
|
||||||
|
ui.add_space(12.0);
|
||||||
|
});
|
||||||
|
|
||||||
// Draw embargo text edit.
|
// Show modal buttons.
|
||||||
let mut embargo_edit_opts = TextEditOptions::new(Id::from(modal.id)).h_center();
|
ui.scope(|ui| {
|
||||||
View::text_edit(ui, cb, &mut self.embargo_edit, &mut embargo_edit_opts);
|
// Setup spacing between buttons.
|
||||||
|
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
|
||||||
|
|
||||||
// Show error when specified value is not valid or reminder to restart enabled node.
|
ui.columns(2, |columns| {
|
||||||
if self.embargo_edit.parse::<u16>().is_err() {
|
columns[0].vertical_centered_justified(|ui| {
|
||||||
ui.add_space(12.0);
|
View::button(
|
||||||
ui.label(RichText::new(t!("network_settings.not_valid_value"))
|
ui,
|
||||||
.size(17.0)
|
t!("modal.cancel"),
|
||||||
.color(Colors::red()));
|
Colors::white_or_black(false),
|
||||||
} else {
|
|| {
|
||||||
NetworkSettings::node_restart_required_ui(ui);
|
// Close modal.
|
||||||
}
|
Modal::close();
|
||||||
ui.add_space(12.0);
|
},
|
||||||
});
|
);
|
||||||
|
});
|
||||||
|
columns[1].vertical_centered_justified(|ui| {
|
||||||
|
View::button(ui, t!("modal.save"), Colors::white_or_black(false), || {
|
||||||
|
on_save(self);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
ui.add_space(6.0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Show modal buttons.
|
/// Draw stem phase probability setup content.
|
||||||
ui.scope(|ui| {
|
fn stem_prob_ui(&mut self, ui: &mut egui::Ui) {
|
||||||
// Setup spacing between buttons.
|
ui.label(
|
||||||
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
|
RichText::new(t!("network_settings.stem_probability"))
|
||||||
|
.size(16.0)
|
||||||
|
.color(Colors::gray()),
|
||||||
|
);
|
||||||
|
ui.add_space(6.0);
|
||||||
|
|
||||||
// Save button callback.
|
let stem_prob = NodeConfig::get_stem_probability();
|
||||||
let on_save = || {
|
View::button(
|
||||||
if let Ok(embargo) = self.embargo_edit.parse::<u16>() {
|
ui,
|
||||||
NodeConfig::save_dandelion_embargo(embargo);
|
format!("{}%", &stem_prob),
|
||||||
cb.hide_keyboard();
|
Colors::white_or_black(false),
|
||||||
modal.close();
|
|| {
|
||||||
}
|
// Setup values for modal.
|
||||||
};
|
self.stem_prob_edit = stem_prob;
|
||||||
|
// Show stem probability setup modal.
|
||||||
|
Modal::new(STEM_PROBABILITY_MODAL)
|
||||||
|
.position(ModalPosition::CenterTop)
|
||||||
|
.title(t!("network_settings.change_value"))
|
||||||
|
.show();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
ui.add_space(6.0);
|
||||||
|
}
|
||||||
|
|
||||||
ui.columns(2, |columns| {
|
/// Draw stem phase probability [`Modal`] content.
|
||||||
columns[0].vertical_centered_justified(|ui| {
|
fn stem_prob_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
|
||||||
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
|
// Save button callback.
|
||||||
// Close modal.
|
let on_save = |c: &mut DandelionSetup| {
|
||||||
cb.hide_keyboard();
|
if let Ok(prob) = c.stem_prob_edit.parse::<u8>() {
|
||||||
modal.close();
|
NodeConfig::save_stem_probability(prob);
|
||||||
});
|
Modal::close();
|
||||||
});
|
}
|
||||||
columns[1].vertical_centered_justified(|ui| {
|
};
|
||||||
View::button(ui, t!("modal.save"), Colors::white_or_black(false), on_save);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
ui.add_space(6.0);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Draw aggregation period setup content.
|
ui.add_space(6.0);
|
||||||
fn aggregation_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
ui.vertical_centered(|ui| {
|
||||||
ui.label(RichText::new(t!("network_settings.aggregation_period"))
|
ui.label(
|
||||||
.size(16.0)
|
RichText::new(t!("network_settings.stem_probability"))
|
||||||
.color(Colors::gray())
|
.size(17.0)
|
||||||
);
|
.color(Colors::gray()),
|
||||||
ui.add_space(6.0);
|
);
|
||||||
|
ui.add_space(8.0);
|
||||||
|
|
||||||
let ag = NodeConfig::get_dandelion_aggregation();
|
// Draw stem phase probability text edit.
|
||||||
View::button(ui, format!("{} {}", CLOCK_COUNTDOWN, &ag), Colors::white_or_black(false), || {
|
let mut stem_prob_edit = TextEdit::new(Id::from(modal.id)).h_center().numeric();
|
||||||
// Setup values for modal.
|
stem_prob_edit.ui(ui, &mut self.stem_prob_edit, cb);
|
||||||
self.aggregation_edit = ag;
|
if stem_prob_edit.enter_pressed {
|
||||||
// Show aggregation setup modal.
|
on_save(self);
|
||||||
Modal::new(AGGREGATION_MODAL)
|
}
|
||||||
.position(ModalPosition::CenterTop)
|
|
||||||
.title(t!("network_settings.change_value"))
|
|
||||||
.show();
|
|
||||||
cb.show_keyboard();
|
|
||||||
});
|
|
||||||
ui.add_space(6.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Draw aggregation period [`Modal`] content.
|
// Show error when specified value is not valid or reminder to restart enabled node.
|
||||||
fn aggregation_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
|
if self.stem_prob_edit.parse::<u8>().is_err() {
|
||||||
ui.add_space(6.0);
|
ui.add_space(12.0);
|
||||||
ui.vertical_centered(|ui| {
|
ui.label(
|
||||||
ui.label(RichText::new(t!("network_settings.aggregation_period"))
|
RichText::new(t!("network_settings.not_valid_value"))
|
||||||
.size(17.0)
|
.size(17.0)
|
||||||
.color(Colors::gray()));
|
.color(Colors::red()),
|
||||||
ui.add_space(8.0);
|
);
|
||||||
|
} else {
|
||||||
|
NetworkSettings::node_restart_required_ui(ui);
|
||||||
|
}
|
||||||
|
ui.add_space(12.0);
|
||||||
|
});
|
||||||
|
|
||||||
// Draw aggregation period text edit.
|
// Show modal buttons.
|
||||||
let mut aggregation_edit_opts = TextEditOptions::new(Id::from(modal.id)).h_center();
|
ui.scope(|ui| {
|
||||||
View::text_edit(ui, cb, &mut self.aggregation_edit, &mut aggregation_edit_opts);
|
// Setup spacing between buttons.
|
||||||
|
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
|
||||||
|
|
||||||
// Show error when specified value is not valid or reminder to restart enabled node.
|
ui.columns(2, |columns| {
|
||||||
if self.aggregation_edit.parse::<u16>().is_err() {
|
columns[0].vertical_centered_justified(|ui| {
|
||||||
ui.add_space(12.0);
|
View::button(
|
||||||
ui.label(RichText::new(t!("network_settings.not_valid_value"))
|
ui,
|
||||||
.size(17.0)
|
t!("modal.cancel"),
|
||||||
.color(Colors::red()));
|
Colors::white_or_black(false),
|
||||||
} else {
|
|| {
|
||||||
NetworkSettings::node_restart_required_ui(ui);
|
// Close modal.
|
||||||
}
|
Modal::close();
|
||||||
ui.add_space(12.0);
|
},
|
||||||
});
|
);
|
||||||
|
});
|
||||||
// Show modal buttons.
|
columns[1].vertical_centered_justified(|ui| {
|
||||||
ui.scope(|ui| {
|
View::button(ui, t!("modal.save"), Colors::white_or_black(false), || {
|
||||||
// Setup spacing between buttons.
|
on_save(self);
|
||||||
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
|
});
|
||||||
|
});
|
||||||
// Save button callback.
|
});
|
||||||
let on_save = || {
|
ui.add_space(6.0);
|
||||||
if let Ok(embargo) = self.aggregation_edit.parse::<u16>() {
|
});
|
||||||
NodeConfig::save_dandelion_aggregation(embargo);
|
}
|
||||||
cb.hide_keyboard();
|
}
|
||||||
modal.close();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
ui.columns(2, |columns| {
|
|
||||||
columns[0].vertical_centered_justified(|ui| {
|
|
||||||
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
|
|
||||||
// Close modal.
|
|
||||||
cb.hide_keyboard();
|
|
||||||
modal.close();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
columns[1].vertical_centered_justified(|ui| {
|
|
||||||
View::button(ui, t!("modal.save"), Colors::white_or_black(false), on_save);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
ui.add_space(6.0);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Draw stem phase probability setup content.
|
|
||||||
fn stem_prob_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
|
||||||
ui.label(RichText::new(t!("network_settings.stem_probability"))
|
|
||||||
.size(16.0)
|
|
||||||
.color(Colors::gray())
|
|
||||||
);
|
|
||||||
ui.add_space(6.0);
|
|
||||||
|
|
||||||
let stem_prob = NodeConfig::get_stem_probability();
|
|
||||||
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.
|
|
||||||
Modal::new(STEM_PROBABILITY_MODAL)
|
|
||||||
.position(ModalPosition::CenterTop)
|
|
||||||
.title(t!("network_settings.change_value"))
|
|
||||||
.show();
|
|
||||||
cb.show_keyboard();
|
|
||||||
});
|
|
||||||
ui.add_space(6.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Draw stem phase probability [`Modal`] content.
|
|
||||||
fn stem_prob_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.stem_probability"))
|
|
||||||
.size(17.0)
|
|
||||||
.color(Colors::gray()));
|
|
||||||
ui.add_space(8.0);
|
|
||||||
|
|
||||||
// Draw stem phase probability text edit.
|
|
||||||
let mut stem_prob_edit_opts = TextEditOptions::new(Id::from(modal.id)).h_center();
|
|
||||||
View::text_edit(ui, cb, &mut self.stem_prob_edit, &mut stem_prob_edit_opts);
|
|
||||||
|
|
||||||
// Show error when specified value is not valid or reminder to restart enabled node.
|
|
||||||
if self.stem_prob_edit.parse::<u8>().is_err() {
|
|
||||||
ui.add_space(12.0);
|
|
||||||
ui.label(RichText::new(t!("network_settings.not_valid_value"))
|
|
||||||
.size(17.0)
|
|
||||||
.color(Colors::red()));
|
|
||||||
} else {
|
|
||||||
NetworkSettings::node_restart_required_ui(ui);
|
|
||||||
}
|
|
||||||
ui.add_space(12.0);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Show modal buttons.
|
|
||||||
ui.scope(|ui| {
|
|
||||||
// Setup spacing between buttons.
|
|
||||||
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
|
|
||||||
|
|
||||||
// Save button callback.
|
|
||||||
let on_save = || {
|
|
||||||
if let Ok(prob) = self.stem_prob_edit.parse::<u8>() {
|
|
||||||
NodeConfig::save_stem_probability(prob);
|
|
||||||
cb.hide_keyboard();
|
|
||||||
modal.close();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
ui.columns(2, |columns| {
|
|
||||||
columns[0].vertical_centered_justified(|ui| {
|
|
||||||
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
|
|
||||||
// Close modal.
|
|
||||||
cb.hide_keyboard();
|
|
||||||
modal.close();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
columns[1].vertical_centered_justified(|ui| {
|
|
||||||
View::button(ui, t!("modal.save"), Colors::white_or_black(false), on_save);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
ui.add_space(6.0);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -25,4 +25,4 @@ mod dandelion;
|
|||||||
pub use dandelion::DandelionSetup;
|
pub use dandelion::DandelionSetup;
|
||||||
|
|
||||||
mod stratum;
|
mod stratum;
|
||||||
pub use stratum::StratumSetup;
|
pub use stratum::StratumSetup;
|
||||||
|
|||||||
+674
-469
File diff suppressed because it is too large
Load Diff
+838
-741
File diff suppressed because it is too large
Load Diff
+517
-451
File diff suppressed because it is too large
Load Diff
@@ -18,42 +18,39 @@ use grin_chain::SyncStatus;
|
|||||||
use crate::gui::Colors;
|
use crate::gui::Colors;
|
||||||
use crate::gui::icons::{BARBELL, HARD_DRIVES, PLUG, POWER, TIMER};
|
use crate::gui::icons::{BARBELL, HARD_DRIVES, PLUG, POWER, TIMER};
|
||||||
use crate::gui::platform::PlatformCallbacks;
|
use crate::gui::platform::PlatformCallbacks;
|
||||||
use crate::gui::views::{Modal, View};
|
|
||||||
use crate::gui::views::network::settings::NetworkSettings;
|
use crate::gui::views::network::settings::NetworkSettings;
|
||||||
use crate::gui::views::types::{ModalContainer, ModalPosition, TextEditOptions};
|
use crate::gui::views::types::{ContentContainer, ModalPosition};
|
||||||
use crate::gui::views::wallets::modals::WalletsModal;
|
use crate::gui::views::wallets::modals::WalletListModal;
|
||||||
|
use crate::gui::views::{Modal, TextEdit, View};
|
||||||
use crate::node::{Node, NodeConfig};
|
use crate::node::{Node, NodeConfig};
|
||||||
use crate::wallet::{WalletConfig, WalletList};
|
use crate::wallet::{WalletConfig, WalletList};
|
||||||
|
|
||||||
/// Stratum server setup section content.
|
/// Stratum server setup section content.
|
||||||
pub struct StratumSetup {
|
pub struct StratumSetup {
|
||||||
/// Wallet list to select for mining rewards.
|
/// Wallet list to select for mining rewards.
|
||||||
wallets: WalletList,
|
wallets: WalletList,
|
||||||
/// Wallets [`Modal`] content.
|
/// Wallets [`Modal`] content.
|
||||||
wallets_modal: WalletsModal,
|
wallets_modal: WalletListModal,
|
||||||
|
|
||||||
/// IP Addresses available at system.
|
/// IP Addresses available at system.
|
||||||
available_ips: Vec<String>,
|
available_ips: Vec<String>,
|
||||||
|
|
||||||
/// Stratum port value.
|
/// Stratum port value.
|
||||||
stratum_port_edit: String,
|
stratum_port_edit: String,
|
||||||
/// Flag to check if stratum port is available.
|
/// Flag to check if stratum port is available.
|
||||||
stratum_port_available_edit: bool,
|
stratum_port_available_edit: bool,
|
||||||
|
|
||||||
/// Flag to check if stratum port from saved config value is available.
|
/// Flag to check if stratum port from saved config value is available.
|
||||||
is_port_available: bool,
|
is_port_available: bool,
|
||||||
|
|
||||||
/// Wallet name to receive rewards.
|
/// Wallet name to receive rewards.
|
||||||
pub wallet_name: Option<String>,
|
pub wallet_name: Option<String>,
|
||||||
|
|
||||||
/// Attempt time value in seconds to mine on a particular header.
|
/// Attempt time value in seconds to mine on a particular header.
|
||||||
attempt_time_edit: String,
|
attempt_time_edit: String,
|
||||||
|
|
||||||
/// Minimum share difficulty value to request from miners.
|
/// Minimum share difficulty value to request from miners.
|
||||||
min_share_diff_edit: String,
|
min_share_diff_edit: String,
|
||||||
|
|
||||||
/// [`Modal`] identifiers allowed at this ui container.
|
|
||||||
modal_ids: Vec<&'static str>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Identifier for wallet selection [`Modal`].
|
/// Identifier for wallet selection [`Modal`].
|
||||||
@@ -66,461 +63,514 @@ const ATTEMPT_TIME_MODAL: &'static str = "stratum_attempt_time";
|
|||||||
const MIN_SHARE_DIFF_MODAL: &'static str = "stratum_min_share_diff";
|
const MIN_SHARE_DIFF_MODAL: &'static str = "stratum_min_share_diff";
|
||||||
|
|
||||||
impl Default for StratumSetup {
|
impl Default for StratumSetup {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
let (ip, port) = NodeConfig::get_stratum_address();
|
let (ip, port) = NodeConfig::get_stratum_address();
|
||||||
let is_port_available = NodeConfig::is_stratum_port_available(&ip, &port);
|
let is_port_available = NodeConfig::is_stratum_port_available(&ip, &port);
|
||||||
|
|
||||||
// Setup mining rewards wallet name and identifier.
|
// Setup mining rewards wallet name and identifier.
|
||||||
let mut wallet_id = NodeConfig::get_stratum_wallet_id();
|
let mut wallet_id = NodeConfig::get_stratum_wallet_id();
|
||||||
let wallet_name = if let Some(id) = wallet_id {
|
let wallet_name = if let Some(id) = wallet_id {
|
||||||
WalletConfig::name_by_id(id)
|
WalletConfig::read_name_by_id(id)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
if wallet_name.is_none() {
|
if wallet_name.is_none() {
|
||||||
wallet_id = None;
|
wallet_id = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
wallets: WalletList::default(),
|
wallets: WalletList::default(),
|
||||||
wallets_modal: WalletsModal::new(wallet_id, None, false),
|
wallets_modal: WalletListModal::new(wallet_id, None, false),
|
||||||
available_ips: NodeConfig::get_ip_addrs(),
|
available_ips: NodeConfig::get_ip_addrs(),
|
||||||
stratum_port_edit: port,
|
stratum_port_edit: port,
|
||||||
stratum_port_available_edit: is_port_available,
|
stratum_port_available_edit: is_port_available,
|
||||||
is_port_available,
|
is_port_available,
|
||||||
wallet_name,
|
wallet_name,
|
||||||
attempt_time_edit: NodeConfig::get_stratum_attempt_time(),
|
attempt_time_edit: NodeConfig::get_stratum_attempt_time(),
|
||||||
min_share_diff_edit: NodeConfig::get_stratum_min_share_diff(),
|
min_share_diff_edit: NodeConfig::get_stratum_min_share_diff(),
|
||||||
modal_ids: vec![
|
}
|
||||||
WALLET_SELECTION_MODAL,
|
}
|
||||||
STRATUM_PORT_MODAL,
|
|
||||||
ATTEMPT_TIME_MODAL,
|
|
||||||
MIN_SHARE_DIFF_MODAL
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ModalContainer for StratumSetup {
|
impl ContentContainer for StratumSetup {
|
||||||
fn modal_ids(&self) -> &Vec<&'static str> {
|
fn modal_ids(&self) -> Vec<&'static str> {
|
||||||
&self.modal_ids
|
vec![
|
||||||
}
|
WALLET_SELECTION_MODAL,
|
||||||
|
STRATUM_PORT_MODAL,
|
||||||
|
ATTEMPT_TIME_MODAL,
|
||||||
|
MIN_SHARE_DIFF_MODAL,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
fn modal_ui(&mut self,
|
fn modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
|
||||||
ui: &mut egui::Ui,
|
match modal.id {
|
||||||
modal: &Modal,
|
WALLET_SELECTION_MODAL => self.wallets_modal.ui(ui, &mut self.wallets, |wallet, _| {
|
||||||
cb: &dyn PlatformCallbacks) {
|
let id = wallet.get_config().id;
|
||||||
match modal.id {
|
NodeConfig::save_stratum_wallet_id(id);
|
||||||
WALLET_SELECTION_MODAL => {
|
self.wallet_name = WalletConfig::read_name_by_id(id);
|
||||||
self.wallets_modal.ui(ui, modal, &mut self.wallets, cb, |wallet, _| {
|
}),
|
||||||
let id = wallet.get_config().id;
|
STRATUM_PORT_MODAL => self.port_modal(ui, modal, cb),
|
||||||
NodeConfig::save_stratum_wallet_id(id);
|
ATTEMPT_TIME_MODAL => self.attempt_modal(ui, modal, cb),
|
||||||
self.wallet_name = WalletConfig::name_by_id(id);
|
MIN_SHARE_DIFF_MODAL => self.min_diff_modal(ui, modal, cb),
|
||||||
})
|
_ => {}
|
||||||
},
|
}
|
||||||
STRATUM_PORT_MODAL => self.port_modal(ui, modal, cb),
|
}
|
||||||
ATTEMPT_TIME_MODAL => self.attempt_modal(ui, modal, cb),
|
|
||||||
MIN_SHARE_DIFF_MODAL => self.min_diff_modal(ui, modal, cb),
|
fn container_ui(&mut self, ui: &mut egui::Ui, _: &dyn PlatformCallbacks) {
|
||||||
_ => {}
|
View::sub_title(
|
||||||
}
|
ui,
|
||||||
}
|
format!("{} {}", HARD_DRIVES, t!("network_mining.server")),
|
||||||
|
);
|
||||||
|
View::horizontal_line(ui, Colors::stroke());
|
||||||
|
ui.add_space(6.0);
|
||||||
|
|
||||||
|
ui.vertical_centered(|ui| {
|
||||||
|
// Show loading indicator or controls to start/stop stratum server.
|
||||||
|
if Node::get_sync_status().unwrap_or(SyncStatus::Initial) == SyncStatus::NoSync
|
||||||
|
&& self.is_port_available
|
||||||
|
&& self.wallet_name.is_some()
|
||||||
|
{
|
||||||
|
if Node::is_stratum_starting() || Node::is_stratum_stopping() {
|
||||||
|
ui.vertical_centered(|ui| {
|
||||||
|
ui.add_space(8.0);
|
||||||
|
View::small_loading_spinner(ui);
|
||||||
|
ui.add_space(8.0);
|
||||||
|
});
|
||||||
|
} else if Node::get_stratum_stats().is_running {
|
||||||
|
ui.add_space(6.0);
|
||||||
|
let disable_text = format!("{} {}", POWER, t!("network_settings.disable"));
|
||||||
|
View::action_button(ui, disable_text, || {
|
||||||
|
Node::stop_stratum();
|
||||||
|
let (ip, port) = NodeConfig::get_stratum_address();
|
||||||
|
self.is_port_available = NodeConfig::is_stratum_port_available(&ip, &port);
|
||||||
|
});
|
||||||
|
ui.add_space(6.0);
|
||||||
|
} else {
|
||||||
|
ui.add_space(6.0);
|
||||||
|
let enable_text = format!("{} {}", POWER, t!("network_settings.enable"));
|
||||||
|
View::action_button(ui, enable_text, || {
|
||||||
|
Node::start_stratum();
|
||||||
|
});
|
||||||
|
ui.add_space(6.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show stratum server autorun checkbox.
|
||||||
|
let stratum_enabled = NodeConfig::is_stratum_autorun_enabled();
|
||||||
|
View::checkbox(ui, stratum_enabled, t!("network.autorun"), || {
|
||||||
|
NodeConfig::toggle_stratum_autorun();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Show reminder to restart running server.
|
||||||
|
if Node::get_stratum_stats().is_running {
|
||||||
|
ui.add_space(2.0);
|
||||||
|
ui.label(
|
||||||
|
RichText::new(t!("network_mining.restart_server_required"))
|
||||||
|
.size(16.0)
|
||||||
|
.color(Colors::inactive_text()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
ui.add_space(8.0);
|
||||||
|
View::horizontal_line(ui, Colors::item_stroke());
|
||||||
|
ui.add_space(8.0);
|
||||||
|
|
||||||
|
// Show wallet name.
|
||||||
|
ui.label(
|
||||||
|
RichText::new(self.wallet_name.as_ref().unwrap_or(&"-".to_string()))
|
||||||
|
.size(16.0)
|
||||||
|
.color(Colors::white_or_black(true)),
|
||||||
|
);
|
||||||
|
ui.add_space(8.0);
|
||||||
|
|
||||||
|
// Show button to select wallet.
|
||||||
|
View::button(
|
||||||
|
ui,
|
||||||
|
t!("network_settings.choose_wallet"),
|
||||||
|
Colors::white_or_black(false),
|
||||||
|
|| {
|
||||||
|
self.show_wallets_modal();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
ui.add_space(12.0);
|
||||||
|
|
||||||
|
if self.wallet_name.is_some() {
|
||||||
|
ui.label(
|
||||||
|
RichText::new(t!("network_settings.stratum_wallet_warning"))
|
||||||
|
.size(16.0)
|
||||||
|
.color(Colors::inactive_text()),
|
||||||
|
);
|
||||||
|
ui.add_space(12.0);
|
||||||
|
}
|
||||||
|
View::horizontal_line(ui, Colors::item_stroke());
|
||||||
|
ui.add_space(6.0);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Show message when IP addresses are not available on the system.
|
||||||
|
if self.available_ips.is_empty() {
|
||||||
|
NetworkSettings::no_ip_address_ui(ui);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.vertical_centered(|ui| {
|
||||||
|
ui.label(
|
||||||
|
RichText::new(t!("network_settings.stratum_ip"))
|
||||||
|
.size(16.0)
|
||||||
|
.color(Colors::gray()),
|
||||||
|
);
|
||||||
|
ui.add_space(6.0);
|
||||||
|
// Show stratum IP addresses to select.
|
||||||
|
let (ip, port) = NodeConfig::get_stratum_address();
|
||||||
|
NetworkSettings::ip_addrs_ui(ui, &ip, &self.available_ips, |selected_ip| {
|
||||||
|
NodeConfig::save_stratum_address(selected_ip, &port);
|
||||||
|
self.is_port_available = NodeConfig::is_stratum_port_available(selected_ip, &port);
|
||||||
|
});
|
||||||
|
// Show stratum port setup.
|
||||||
|
self.port_setup_ui(ui);
|
||||||
|
|
||||||
|
View::horizontal_line(ui, Colors::item_stroke());
|
||||||
|
ui.add_space(6.0);
|
||||||
|
|
||||||
|
// Show attempt time setup.
|
||||||
|
self.attempt_time_ui(ui);
|
||||||
|
|
||||||
|
View::horizontal_line(ui, Colors::item_stroke());
|
||||||
|
ui.add_space(6.0);
|
||||||
|
|
||||||
|
// Show minimum acceptable share difficulty setup.
|
||||||
|
self.min_diff_ui(ui);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StratumSetup {
|
impl StratumSetup {
|
||||||
pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
/// Show wallet selection [`Modal`].
|
||||||
// Draw modal content for current ui container.
|
fn show_wallets_modal(&mut self) {
|
||||||
self.current_modal_ui(ui, cb);
|
self.wallets_modal = WalletListModal::new(NodeConfig::get_stratum_wallet_id(), None, false);
|
||||||
|
// Show modal.
|
||||||
|
Modal::new(WALLET_SELECTION_MODAL)
|
||||||
|
.position(ModalPosition::Center)
|
||||||
|
.title(t!("network_settings.choose_wallet"))
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
|
||||||
View::sub_title(ui, format!("{} {}", HARD_DRIVES, t!("network_mining.server")));
|
/// Draw stratum port value setup content.
|
||||||
View::horizontal_line(ui, Colors::stroke());
|
fn port_setup_ui(&mut self, ui: &mut egui::Ui) {
|
||||||
ui.add_space(6.0);
|
ui.label(
|
||||||
|
RichText::new(t!("network_settings.stratum_port"))
|
||||||
|
.size(16.0)
|
||||||
|
.color(Colors::gray()),
|
||||||
|
);
|
||||||
|
ui.add_space(6.0);
|
||||||
|
|
||||||
ui.vertical_centered(|ui| {
|
let (_, port) = NodeConfig::get_stratum_address();
|
||||||
// Show loading indicator or controls to start/stop stratum server.
|
View::button(
|
||||||
if Node::get_sync_status().unwrap_or(SyncStatus::Initial) == SyncStatus::NoSync &&
|
ui,
|
||||||
self.is_port_available && self.wallet_name.is_some() {
|
format!("{} {}", PLUG, &port),
|
||||||
if Node::is_stratum_starting() || Node::is_stratum_stopping() {
|
Colors::white_or_black(false),
|
||||||
ui.vertical_centered(|ui| {
|
|| {
|
||||||
ui.add_space(8.0);
|
// Setup values for modal.
|
||||||
View::small_loading_spinner(ui);
|
self.stratum_port_edit = port;
|
||||||
ui.add_space(8.0);
|
self.stratum_port_available_edit = self.is_port_available;
|
||||||
});
|
// Show stratum port modal.
|
||||||
} else if Node::get_stratum_stats().is_running {
|
Modal::new(STRATUM_PORT_MODAL)
|
||||||
ui.add_space(6.0);
|
.position(ModalPosition::CenterTop)
|
||||||
let disable_text = format!("{} {}", POWER, t!("network_settings.disable"));
|
.title(t!("network_settings.change_value"))
|
||||||
View::action_button(ui, disable_text, || {
|
.show();
|
||||||
Node::stop_stratum();
|
},
|
||||||
let (ip, port) = NodeConfig::get_stratum_address();
|
);
|
||||||
self.is_port_available = NodeConfig::is_stratum_port_available(&ip, &port);
|
ui.add_space(12.0);
|
||||||
});
|
|
||||||
ui.add_space(6.0);
|
|
||||||
} else {
|
|
||||||
ui.add_space(6.0);
|
|
||||||
let enable_text = format!("{} {}", POWER, t!("network_settings.enable"));
|
|
||||||
View::action_button(ui, enable_text, || {
|
|
||||||
Node::start_stratum();
|
|
||||||
});
|
|
||||||
ui.add_space(6.0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show stratum server autorun checkbox.
|
// Show error when stratum server port is unavailable.
|
||||||
let stratum_enabled = NodeConfig::is_stratum_autorun_enabled();
|
if !self.is_port_available {
|
||||||
View::checkbox(ui, stratum_enabled, t!("network.autorun"), || {
|
ui.add_space(6.0);
|
||||||
NodeConfig::toggle_stratum_autorun();
|
ui.label(
|
||||||
});
|
RichText::new(t!("network_settings.port_unavailable"))
|
||||||
|
.size(16.0)
|
||||||
|
.color(Colors::red()),
|
||||||
|
);
|
||||||
|
ui.add_space(12.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Show reminder to restart running server.
|
/// Draw stratum port [`Modal`] content.
|
||||||
if Node::get_stratum_stats().is_running {
|
fn port_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
|
||||||
ui.add_space(2.0);
|
let on_save = |c: &mut StratumSetup| {
|
||||||
ui.label(RichText::new(t!("network_mining.restart_server_required"))
|
// Check if port is available.
|
||||||
.size(16.0)
|
let (stratum_ip, _) = NodeConfig::get_stratum_address();
|
||||||
.color(Colors::inactive_text())
|
let available =
|
||||||
);
|
NodeConfig::is_stratum_port_available(&stratum_ip, &c.stratum_port_edit);
|
||||||
}
|
c.stratum_port_available_edit = available;
|
||||||
ui.add_space(8.0);
|
|
||||||
View::horizontal_line(ui, Colors::item_stroke());
|
|
||||||
ui.add_space(8.0);
|
|
||||||
|
|
||||||
// Show wallet name.
|
// Save port at config if it's available.
|
||||||
ui.label(RichText::new(self.wallet_name.as_ref().unwrap_or(&"-".to_string()))
|
if available {
|
||||||
.size(16.0)
|
NodeConfig::save_stratum_address(&stratum_ip, &c.stratum_port_edit);
|
||||||
.color(Colors::white_or_black(true)));
|
|
||||||
ui.add_space(8.0);
|
|
||||||
|
|
||||||
// Show button to select wallet.
|
c.is_port_available = true;
|
||||||
View::button(ui,
|
Modal::close();
|
||||||
t!("network_settings.choose_wallet"),
|
}
|
||||||
Colors::white_or_black(false), || {
|
};
|
||||||
self.show_wallets_modal();
|
|
||||||
});
|
|
||||||
ui.add_space(12.0);
|
|
||||||
|
|
||||||
if self.wallet_name.is_some() {
|
ui.add_space(6.0);
|
||||||
ui.label(RichText::new(t!("network_settings.stratum_wallet_warning"))
|
ui.vertical_centered(|ui| {
|
||||||
.size(16.0)
|
ui.label(
|
||||||
.color(Colors::inactive_text())
|
RichText::new(t!("network_settings.stratum_port"))
|
||||||
);
|
.size(17.0)
|
||||||
ui.add_space(12.0);
|
.color(Colors::gray()),
|
||||||
}
|
);
|
||||||
View::horizontal_line(ui, Colors::item_stroke());
|
ui.add_space(8.0);
|
||||||
ui.add_space(6.0);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Show message when IP addresses are not available on the system.
|
// Draw stratum port text edit.
|
||||||
if self.available_ips.is_empty() {
|
let mut edit = TextEdit::new(Id::from(modal.id)).h_center().numeric();
|
||||||
NetworkSettings::no_ip_address_ui(ui);
|
edit.ui(ui, &mut self.stratum_port_edit, cb);
|
||||||
return;
|
if edit.enter_pressed {
|
||||||
}
|
on_save(self);
|
||||||
|
}
|
||||||
|
|
||||||
ui.vertical_centered(|ui| {
|
// Show error when specified port is unavailable.
|
||||||
ui.label(RichText::new(t!("network_settings.stratum_ip"))
|
if !self.stratum_port_available_edit {
|
||||||
.size(16.0)
|
ui.add_space(12.0);
|
||||||
.color(Colors::gray())
|
ui.label(
|
||||||
);
|
RichText::new(t!("network_settings.port_unavailable"))
|
||||||
ui.add_space(6.0);
|
.size(17.0)
|
||||||
// Show stratum IP addresses to select.
|
.color(Colors::red()),
|
||||||
let (ip, port) = NodeConfig::get_stratum_address();
|
);
|
||||||
NetworkSettings::ip_addrs_ui(ui, &ip, &self.available_ips, |selected_ip| {
|
} else {
|
||||||
NodeConfig::save_stratum_address(selected_ip, &port);
|
server_restart_required_ui(ui);
|
||||||
self.is_port_available = NodeConfig::is_stratum_port_available(selected_ip, &port);
|
}
|
||||||
|
|
||||||
});
|
ui.add_space(12.0);
|
||||||
// Show stratum port setup.
|
|
||||||
self.port_setup_ui(ui, cb);
|
|
||||||
|
|
||||||
View::horizontal_line(ui, Colors::item_stroke());
|
// Show modal buttons.
|
||||||
ui.add_space(6.0);
|
ui.scope(|ui| {
|
||||||
|
// Setup spacing between buttons.
|
||||||
|
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
|
||||||
|
|
||||||
// Show attempt time setup.
|
ui.columns(2, |columns| {
|
||||||
self.attempt_time_ui(ui, cb);
|
columns[0].vertical_centered_justified(|ui| {
|
||||||
|
View::button(
|
||||||
|
ui,
|
||||||
|
t!("modal.cancel"),
|
||||||
|
Colors::white_or_black(false),
|
||||||
|
|| {
|
||||||
|
// Close modal.
|
||||||
|
Modal::close();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
columns[1].vertical_centered_justified(|ui| {
|
||||||
|
View::button(ui, t!("modal.save"), Colors::white_or_black(false), || {
|
||||||
|
on_save(self);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
ui.add_space(6.0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
View::horizontal_line(ui, Colors::item_stroke());
|
/// Draw attempt time value setup content.
|
||||||
ui.add_space(6.0);
|
fn attempt_time_ui(&mut self, ui: &mut egui::Ui) {
|
||||||
|
ui.label(
|
||||||
|
RichText::new(t!("network_settings.attempt_time"))
|
||||||
|
.size(16.0)
|
||||||
|
.color(Colors::gray()),
|
||||||
|
);
|
||||||
|
ui.add_space(6.0);
|
||||||
|
|
||||||
// Show minimum acceptable share difficulty setup.
|
let time = NodeConfig::get_stratum_attempt_time();
|
||||||
self.min_diff_ui(ui, cb);
|
View::button(
|
||||||
});
|
ui,
|
||||||
}
|
format!("{} {}", TIMER, &time),
|
||||||
|
Colors::white_or_black(false),
|
||||||
|
|| {
|
||||||
|
// Setup values for modal.
|
||||||
|
self.attempt_time_edit = time;
|
||||||
|
|
||||||
/// Show wallet selection [`Modal`].
|
// Show attempt time modal.
|
||||||
fn show_wallets_modal(&mut self) {
|
Modal::new(ATTEMPT_TIME_MODAL)
|
||||||
self.wallets_modal = WalletsModal::new(NodeConfig::get_stratum_wallet_id(), None, false);
|
.position(ModalPosition::CenterTop)
|
||||||
// Show modal.
|
.title(t!("network_settings.change_value"))
|
||||||
Modal::new(WALLET_SELECTION_MODAL)
|
.show();
|
||||||
.position(ModalPosition::Center)
|
},
|
||||||
.title(t!("network_settings.choose_wallet"))
|
);
|
||||||
.show();
|
ui.add_space(6.0);
|
||||||
}
|
ui.label(
|
||||||
|
RichText::new(t!("network_settings.attempt_time_desc"))
|
||||||
|
.size(16.0)
|
||||||
|
.color(Colors::inactive_text()),
|
||||||
|
);
|
||||||
|
ui.add_space(6.0);
|
||||||
|
}
|
||||||
|
|
||||||
/// Draw stratum port value setup content.
|
/// Draw attempt time [`Modal`] content.
|
||||||
fn port_setup_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
fn attempt_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
|
||||||
ui.label(RichText::new(t!("network_settings.stratum_port"))
|
let on_save = |c: &mut StratumSetup| {
|
||||||
.size(16.0)
|
if let Ok(time) = c.attempt_time_edit.parse::<u32>() {
|
||||||
.color(Colors::gray())
|
NodeConfig::save_stratum_attempt_time(time);
|
||||||
);
|
Modal::close();
|
||||||
ui.add_space(6.0);
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let (_, port) = NodeConfig::get_stratum_address();
|
ui.add_space(6.0);
|
||||||
View::button(ui, format!("{} {}", PLUG, &port), Colors::white_or_black(false), || {
|
ui.vertical_centered(|ui| {
|
||||||
// Setup values for modal.
|
ui.label(
|
||||||
self.stratum_port_edit = port;
|
RichText::new(t!("network_settings.attempt_time"))
|
||||||
self.stratum_port_available_edit = self.is_port_available;
|
.size(17.0)
|
||||||
// Show stratum port modal.
|
.color(Colors::gray()),
|
||||||
Modal::new(STRATUM_PORT_MODAL)
|
);
|
||||||
.position(ModalPosition::CenterTop)
|
ui.add_space(8.0);
|
||||||
.title(t!("network_settings.change_value"))
|
|
||||||
.show();
|
|
||||||
cb.show_keyboard();
|
|
||||||
});
|
|
||||||
ui.add_space(12.0);
|
|
||||||
|
|
||||||
// Show error when stratum server port is unavailable.
|
// Draw attempt time text edit.
|
||||||
if !self.is_port_available {
|
let mut edit = TextEdit::new(Id::from(modal.id)).h_center().numeric();
|
||||||
ui.add_space(6.0);
|
edit.ui(ui, &mut self.attempt_time_edit, cb);
|
||||||
ui.label(RichText::new(t!("network_settings.port_unavailable"))
|
if edit.enter_pressed {
|
||||||
.size(16.0)
|
on_save(self);
|
||||||
.color(Colors::red()));
|
}
|
||||||
ui.add_space(12.0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Draw stratum port [`Modal`] content.
|
// Show error when specified value is not valid or reminder to restart enabled node.
|
||||||
fn port_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
|
if self.attempt_time_edit.parse::<u32>().is_err() {
|
||||||
ui.add_space(6.0);
|
ui.add_space(12.0);
|
||||||
ui.vertical_centered(|ui| {
|
ui.label(
|
||||||
ui.label(RichText::new(t!("network_settings.stratum_port"))
|
RichText::new(t!("network_settings.not_valid_value"))
|
||||||
.size(17.0)
|
.size(17.0)
|
||||||
.color(Colors::gray()));
|
.color(Colors::red()),
|
||||||
ui.add_space(8.0);
|
);
|
||||||
|
} else {
|
||||||
|
server_restart_required_ui(ui);
|
||||||
|
}
|
||||||
|
ui.add_space(12.0);
|
||||||
|
});
|
||||||
|
|
||||||
// Draw stratum port text edit.
|
// Show modal buttons.
|
||||||
let mut text_edit_opts = TextEditOptions::new(Id::from(modal.id)).h_center();
|
ui.scope(|ui| {
|
||||||
View::text_edit(ui, cb, &mut self.stratum_port_edit, &mut text_edit_opts);
|
// Setup spacing between buttons.
|
||||||
|
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
|
||||||
|
|
||||||
// Show error when specified port is unavailable.
|
ui.columns(2, |columns| {
|
||||||
if !self.stratum_port_available_edit {
|
columns[0].vertical_centered_justified(|ui| {
|
||||||
ui.add_space(12.0);
|
View::button(
|
||||||
ui.label(RichText::new(t!("network_settings.port_unavailable"))
|
ui,
|
||||||
.size(17.0)
|
t!("modal.cancel"),
|
||||||
.color(Colors::red()));
|
Colors::white_or_black(false),
|
||||||
} else {
|
|| {
|
||||||
server_restart_required_ui(ui);
|
// Close modal.
|
||||||
}
|
Modal::close();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
columns[1].vertical_centered_justified(|ui| {
|
||||||
|
View::button(ui, t!("modal.save"), Colors::white_or_black(false), || {
|
||||||
|
on_save(self);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
ui.add_space(6.0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
ui.add_space(12.0);
|
/// Draw minimum share difficulty value setup content.
|
||||||
|
fn min_diff_ui(&mut self, ui: &mut egui::Ui) {
|
||||||
|
ui.label(
|
||||||
|
RichText::new(t!("network_settings.min_share_diff"))
|
||||||
|
.size(16.0)
|
||||||
|
.color(Colors::gray()),
|
||||||
|
);
|
||||||
|
ui.add_space(6.0);
|
||||||
|
|
||||||
// Show modal buttons.
|
let diff = NodeConfig::get_stratum_min_share_diff();
|
||||||
ui.scope(|ui| {
|
View::button(
|
||||||
// Setup spacing between buttons.
|
ui,
|
||||||
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
|
format!("{} {}", BARBELL, &diff),
|
||||||
|
Colors::white_or_black(false),
|
||||||
|
|| {
|
||||||
|
// Setup values for modal.
|
||||||
|
self.min_share_diff_edit = diff;
|
||||||
|
|
||||||
// Save button callback.
|
// Show share difficulty setup modal.
|
||||||
let on_save = || {
|
Modal::new(MIN_SHARE_DIFF_MODAL)
|
||||||
// Check if port is available.
|
.position(ModalPosition::CenterTop)
|
||||||
let (stratum_ip, _) = NodeConfig::get_stratum_address();
|
.title(t!("network_settings.change_value"))
|
||||||
let available = NodeConfig::is_stratum_port_available(
|
.show();
|
||||||
&stratum_ip,
|
},
|
||||||
&self.stratum_port_edit
|
);
|
||||||
);
|
ui.add_space(6.0);
|
||||||
self.stratum_port_available_edit = available;
|
}
|
||||||
|
|
||||||
// Save port at config if it's available.
|
/// Draw minimum acceptable share difficulty [`Modal`] content.
|
||||||
if available {
|
fn min_diff_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
|
||||||
NodeConfig::save_stratum_address(&stratum_ip, &self.stratum_port_edit);
|
let on_save = |c: &mut StratumSetup| {
|
||||||
|
if let Ok(diff) = c.min_share_diff_edit.parse::<u64>() {
|
||||||
|
NodeConfig::save_stratum_min_share_diff(diff);
|
||||||
|
Modal::close();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
self.is_port_available = true;
|
ui.add_space(6.0);
|
||||||
cb.hide_keyboard();
|
ui.vertical_centered(|ui| {
|
||||||
modal.close();
|
ui.label(
|
||||||
}
|
RichText::new(t!("network_settings.min_share_diff"))
|
||||||
};
|
.size(17.0)
|
||||||
|
.color(Colors::gray()),
|
||||||
|
);
|
||||||
|
ui.add_space(8.0);
|
||||||
|
|
||||||
ui.columns(2, |columns| {
|
// Draw share difficulty text edit.
|
||||||
columns[0].vertical_centered_justified(|ui| {
|
let mut edit = TextEdit::new(Id::from(modal.id)).h_center().numeric();
|
||||||
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
|
edit.ui(ui, &mut self.min_share_diff_edit, cb);
|
||||||
// Close modal.
|
if edit.enter_pressed {
|
||||||
cb.hide_keyboard();
|
on_save(self);
|
||||||
modal.close();
|
}
|
||||||
});
|
|
||||||
});
|
|
||||||
columns[1].vertical_centered_justified(|ui| {
|
|
||||||
View::button(ui, t!("modal.save"), Colors::white_or_black(false), on_save);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
ui.add_space(6.0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Draw attempt time value setup content.
|
// Show error when specified value is not valid or reminder to restart enabled node.
|
||||||
fn attempt_time_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
if self.min_share_diff_edit.parse::<u64>().is_err() {
|
||||||
ui.label(RichText::new(t!("network_settings.attempt_time"))
|
ui.add_space(12.0);
|
||||||
.size(16.0)
|
ui.label(
|
||||||
.color(Colors::gray())
|
RichText::new(t!("network_settings.not_valid_value"))
|
||||||
);
|
.size(17.0)
|
||||||
ui.add_space(6.0);
|
.color(Colors::red()),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
server_restart_required_ui(ui);
|
||||||
|
}
|
||||||
|
ui.add_space(12.0);
|
||||||
|
});
|
||||||
|
|
||||||
let time = NodeConfig::get_stratum_attempt_time();
|
// Show modal buttons.
|
||||||
View::button(ui, format!("{} {}", TIMER, &time), Colors::white_or_black(false), || {
|
ui.scope(|ui| {
|
||||||
// Setup values for modal.
|
// Setup spacing between buttons.
|
||||||
self.attempt_time_edit = time;
|
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
|
||||||
|
|
||||||
// Show attempt time modal.
|
ui.columns(2, |columns| {
|
||||||
Modal::new(ATTEMPT_TIME_MODAL)
|
columns[0].vertical_centered_justified(|ui| {
|
||||||
.position(ModalPosition::CenterTop)
|
View::button(
|
||||||
.title(t!("network_settings.change_value"))
|
ui,
|
||||||
.show();
|
t!("modal.cancel"),
|
||||||
cb.show_keyboard();
|
Colors::white_or_black(false),
|
||||||
});
|
|| {
|
||||||
ui.add_space(6.0);
|
// Close modal.
|
||||||
ui.label(RichText::new(t!("network_settings.attempt_time_desc"))
|
Modal::close();
|
||||||
.size(16.0)
|
},
|
||||||
.color(Colors::inactive_text())
|
);
|
||||||
);
|
});
|
||||||
ui.add_space(6.0);
|
columns[1].vertical_centered_justified(|ui| {
|
||||||
}
|
View::button(ui, t!("modal.save"), Colors::white_or_black(false), || {
|
||||||
|
on_save(self);
|
||||||
/// Draw attempt time [`Modal`] content.
|
});
|
||||||
fn attempt_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
|
});
|
||||||
ui.add_space(6.0);
|
});
|
||||||
ui.vertical_centered(|ui| {
|
ui.add_space(6.0);
|
||||||
ui.label(RichText::new(t!("network_settings.attempt_time"))
|
});
|
||||||
.size(17.0)
|
}
|
||||||
.color(Colors::gray()));
|
|
||||||
ui.add_space(8.0);
|
|
||||||
|
|
||||||
// Draw attempt time text edit.
|
|
||||||
let mut text_edit_opts = TextEditOptions::new(Id::from(modal.id)).h_center();
|
|
||||||
View::text_edit(ui, cb, &mut self.attempt_time_edit, &mut text_edit_opts);
|
|
||||||
|
|
||||||
// Show error when specified value is not valid or reminder to restart enabled node.
|
|
||||||
if self.attempt_time_edit.parse::<u32>().is_err() {
|
|
||||||
ui.add_space(12.0);
|
|
||||||
ui.label(RichText::new(t!("network_settings.not_valid_value"))
|
|
||||||
.size(17.0)
|
|
||||||
.color(Colors::red()));
|
|
||||||
} else {
|
|
||||||
server_restart_required_ui(ui);
|
|
||||||
}
|
|
||||||
ui.add_space(12.0);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Show modal buttons.
|
|
||||||
ui.scope(|ui| {
|
|
||||||
// Setup spacing between buttons.
|
|
||||||
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
|
|
||||||
|
|
||||||
// Save button callback.
|
|
||||||
let on_save = || {
|
|
||||||
if let Ok(time) = self.attempt_time_edit.parse::<u32>() {
|
|
||||||
NodeConfig::save_stratum_attempt_time(time);
|
|
||||||
cb.hide_keyboard();
|
|
||||||
modal.close();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
ui.columns(2, |columns| {
|
|
||||||
columns[0].vertical_centered_justified(|ui| {
|
|
||||||
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
|
|
||||||
// Close modal.
|
|
||||||
cb.hide_keyboard();
|
|
||||||
modal.close();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
columns[1].vertical_centered_justified(|ui| {
|
|
||||||
View::button(ui, t!("modal.save"), Colors::white_or_black(false), on_save);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
ui.add_space(6.0);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Draw minimum share difficulty value setup content.
|
|
||||||
fn min_diff_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
|
||||||
ui.label(RichText::new(t!("network_settings.min_share_diff"))
|
|
||||||
.size(16.0)
|
|
||||||
.color(Colors::gray())
|
|
||||||
);
|
|
||||||
ui.add_space(6.0);
|
|
||||||
|
|
||||||
let diff = NodeConfig::get_stratum_min_share_diff();
|
|
||||||
View::button(ui, format!("{} {}", BARBELL, &diff), Colors::white_or_black(false), || {
|
|
||||||
// Setup values for modal.
|
|
||||||
self.min_share_diff_edit = diff;
|
|
||||||
|
|
||||||
// Show share difficulty setup modal.
|
|
||||||
Modal::new(MIN_SHARE_DIFF_MODAL)
|
|
||||||
.position(ModalPosition::CenterTop)
|
|
||||||
.title(t!("network_settings.change_value"))
|
|
||||||
.show();
|
|
||||||
cb.show_keyboard();
|
|
||||||
});
|
|
||||||
ui.add_space(6.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Draw minimum acceptable share difficulty [`Modal`] content.
|
|
||||||
fn min_diff_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.min_share_diff"))
|
|
||||||
.size(17.0)
|
|
||||||
.color(Colors::gray()));
|
|
||||||
ui.add_space(8.0);
|
|
||||||
|
|
||||||
// Draw share difficulty text edit.
|
|
||||||
let mut text_edit_opts = TextEditOptions::new(Id::from(modal.id)).h_center();
|
|
||||||
View::text_edit(ui, cb, &mut self.min_share_diff_edit, &mut text_edit_opts);
|
|
||||||
|
|
||||||
// Show error when specified value is not valid or reminder to restart enabled node.
|
|
||||||
if self.min_share_diff_edit.parse::<u64>().is_err() {
|
|
||||||
ui.add_space(12.0);
|
|
||||||
ui.label(RichText::new(t!("network_settings.not_valid_value"))
|
|
||||||
.size(17.0)
|
|
||||||
.color(Colors::red()));
|
|
||||||
} else {
|
|
||||||
server_restart_required_ui(ui);
|
|
||||||
}
|
|
||||||
ui.add_space(12.0);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Show modal buttons.
|
|
||||||
ui.scope(|ui| {
|
|
||||||
// Setup spacing between buttons.
|
|
||||||
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
|
|
||||||
|
|
||||||
// Save button callback.
|
|
||||||
let on_save = || {
|
|
||||||
if let Ok(diff) = self.min_share_diff_edit.parse::<u64>() {
|
|
||||||
NodeConfig::save_stratum_min_share_diff(diff);
|
|
||||||
cb.hide_keyboard();
|
|
||||||
modal.close();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
ui.columns(2, |columns| {
|
|
||||||
columns[0].vertical_centered_justified(|ui| {
|
|
||||||
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
|
|
||||||
// Close modal.
|
|
||||||
cb.hide_keyboard();
|
|
||||||
modal.close();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
columns[1].vertical_centered_justified(|ui| {
|
|
||||||
View::button(ui, t!("modal.save"), Colors::white_or_black(false), on_save);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
ui.add_space(6.0);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reminder to restart enabled node to show on edit setting at [`Modal`].
|
/// Reminder to restart enabled node to show on edit setting at [`Modal`].
|
||||||
pub fn server_restart_required_ui(ui: &mut egui::Ui) {
|
pub fn server_restart_required_ui(ui: &mut egui::Ui) {
|
||||||
if Node::get_stratum_stats().is_running {
|
if Node::get_stratum_stats().is_running {
|
||||||
ui.add_space(12.0);
|
ui.add_space(12.0);
|
||||||
ui.label(RichText::new(t!("network_mining.restart_server_required"))
|
ui.label(
|
||||||
.size(16.0)
|
RichText::new(t!("network_mining.restart_server_required"))
|
||||||
.color(Colors::green())
|
.size(16.0)
|
||||||
);
|
.color(Colors::green()),
|
||||||
}
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -13,29 +13,39 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
use crate::gui::platform::PlatformCallbacks;
|
use crate::gui::platform::PlatformCallbacks;
|
||||||
|
use serde_derive::{Deserialize, Serialize};
|
||||||
|
|
||||||
/// Integrated node tab content interface.
|
/// Integrated node tab content interface.
|
||||||
pub trait NodeTab {
|
pub trait NodeTab {
|
||||||
fn get_type(&self) -> NodeTabType;
|
fn get_type(&self) -> NodeTabType;
|
||||||
fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks);
|
fn tab_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Type of [`NodeTab`] content.
|
/// Type of [`NodeTab`] content.
|
||||||
#[derive(PartialEq)]
|
#[derive(PartialEq)]
|
||||||
pub enum NodeTabType {
|
pub enum NodeTabType {
|
||||||
Info,
|
Info,
|
||||||
Metrics,
|
Metrics,
|
||||||
Mining,
|
Mining,
|
||||||
Settings
|
Settings,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NodeTabType {
|
impl NodeTabType {
|
||||||
pub fn title(&self) -> String {
|
pub fn title(&self) -> String {
|
||||||
match *self {
|
match *self {
|
||||||
NodeTabType::Info => { t!("network.node") }
|
NodeTabType::Info => t!("network.node").into(),
|
||||||
NodeTabType::Metrics => { t!("network.metrics") }
|
NodeTabType::Metrics => t!("network.metrics").into(),
|
||||||
NodeTabType::Mining => { t!("network.mining") }
|
NodeTabType::Mining => t!("network.mining").into(),
|
||||||
NodeTabType::Settings => { t!("network.settings") }
|
NodeTabType::Settings => t!("network.settings").into(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Connection details to share.
|
||||||
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
|
pub struct ShareConnection {
|
||||||
|
#[serde(rename(serialize = "ipPort", deserialize = "ipPort"))]
|
||||||
|
pub url: String,
|
||||||
|
pub username: String,
|
||||||
|
pub secret: String,
|
||||||
|
}
|
||||||
|
|||||||
+297
-288
@@ -12,361 +12,370 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
|
use egui::epaint::{Pos2, Shape, Stroke, emath::lerp, vec2};
|
||||||
use egui::scroll_area::ScrollAreaOutput;
|
use egui::scroll_area::ScrollAreaOutput;
|
||||||
use egui::{Sense, Align2, Area, Color32, Id, Rect, Response, Widget, Vec2, UiBuilder};
|
use egui::{Align2, Area, Color32, Id, Rect, Response, Sense, UiBuilder, Vec2, Widget};
|
||||||
use egui::epaint::{emath::lerp, vec2, Pos2, Shape, Stroke};
|
|
||||||
|
|
||||||
/// A spinner widget used to indicate loading.
|
/// A spinner widget used to indicate loading.
|
||||||
/// This was taken from egui and modified slightly to allow passing a progress value
|
/// This was taken from egui and modified slightly to allow passing a progress value
|
||||||
#[must_use = "You should put this widget in an ui with `ui.add(widget);`"]
|
#[must_use = "You should put this widget in an ui with `ui.add(widget);`"]
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct ProgressSpinner {
|
pub struct ProgressSpinner {
|
||||||
/// Uses the style's `interact_size` if `None`.
|
/// Uses the style's `interact_size` if `None`.
|
||||||
size: Option<f32>,
|
size: Option<f32>,
|
||||||
color: Option<Color32>,
|
color: Option<Color32>,
|
||||||
progress: Option<f64>,
|
progress: Option<f64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ProgressSpinner {
|
impl ProgressSpinner {
|
||||||
/// Create a new spinner that uses the style's `interact_size` unless changed.
|
/// Create a new spinner that uses the style's `interact_size` unless changed.
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self::default()
|
Self::default()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets the spinner's size. The size sets both the height and width, as the spinner is always
|
/// Sets the spinner's size. The size sets both the height and width, as the spinner is always
|
||||||
/// square. If the size isn't set explicitly, the active style's `interact_size` is used.
|
/// square. If the size isn't set explicitly, the active style's `interact_size` is used.
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
pub fn size(mut self, size: f32) -> Self {
|
pub fn size(mut self, size: f32) -> Self {
|
||||||
self.size = Some(size);
|
self.size = Some(size);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets the spinner's color.
|
/// Sets the spinner's color.
|
||||||
pub fn color(mut self, color: impl Into<Color32>) -> Self {
|
pub fn color(mut self, color: impl Into<Color32>) -> Self {
|
||||||
self.color = Some(color.into());
|
self.color = Some(color.into());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets the spinner's progress.
|
/// Sets the spinner's progress.
|
||||||
/// Should be in the range `[0.0, 1.0]`.
|
/// Should be in the range `[0.0, 1.0]`.
|
||||||
pub fn progress(mut self, progress: impl Into<Option<f64>>) -> Self {
|
pub fn progress(mut self, progress: impl Into<Option<f64>>) -> Self {
|
||||||
self.progress = progress.into();
|
self.progress = progress.into();
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Paint the spinner in the given rectangle.
|
/// Paint the spinner in the given rectangle.
|
||||||
pub fn paint_at(&self, ui: &egui::Ui, rect: Rect) {
|
pub fn paint_at(&self, ui: &egui::Ui, rect: Rect) {
|
||||||
if ui.is_rect_visible(rect) {
|
if ui.is_rect_visible(rect) {
|
||||||
ui.ctx().request_repaint(); // because it is animated
|
ui.ctx().request_repaint(); // because it is animated
|
||||||
|
|
||||||
let color = self
|
let color = self
|
||||||
.color
|
.color
|
||||||
.unwrap_or_else(|| ui.visuals().strong_text_color());
|
.unwrap_or_else(|| ui.visuals().strong_text_color());
|
||||||
let radius = (rect.height() / 2.0) - 2.0;
|
let radius = (rect.height() / 2.0) - 2.0;
|
||||||
let n_points = 20;
|
let n_points = 20;
|
||||||
|
|
||||||
let (start_angle, end_angle) = if let Some(progress) = self.progress {
|
let (start_angle, end_angle) = if let Some(progress) = self.progress {
|
||||||
let start_angle = 0f64.to_radians();
|
let start_angle = 0f64.to_radians();
|
||||||
let end_angle = start_angle + 360f64.to_radians() * progress;
|
let end_angle = start_angle + 360f64.to_radians() * progress;
|
||||||
(start_angle, end_angle)
|
(start_angle, end_angle)
|
||||||
} else {
|
} else {
|
||||||
let time = ui.input(|i| i.time);
|
let time = ui.input(|i| i.time);
|
||||||
let start_angle = time * std::f64::consts::TAU;
|
let start_angle = time * std::f64::consts::TAU;
|
||||||
let end_angle = start_angle + 240f64.to_radians() * time.sin();
|
let end_angle = start_angle + 240f64.to_radians() * time.sin();
|
||||||
(start_angle, end_angle)
|
(start_angle, end_angle)
|
||||||
};
|
};
|
||||||
|
|
||||||
let points: Vec<Pos2> = (0..=n_points)
|
let points: Vec<Pos2> = (0..=n_points)
|
||||||
.map(|i| {
|
.map(|i| {
|
||||||
let angle = lerp(start_angle..=end_angle, i as f64 / n_points as f64);
|
let angle = lerp(start_angle..=end_angle, i as f64 / n_points as f64);
|
||||||
let (sin, cos) = angle.sin_cos();
|
let (sin, cos) = angle.sin_cos();
|
||||||
rect.center() + radius * vec2(cos as f32, sin as f32)
|
rect.center() + radius * vec2(cos as f32, sin as f32)
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
ui.painter()
|
ui.painter()
|
||||||
.add(Shape::line(points, Stroke::new(3.0, color)));
|
.add(Shape::line(points, Stroke::new(3.0, color)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Widget for ProgressSpinner {
|
impl Widget for ProgressSpinner {
|
||||||
fn ui(self, ui: &mut egui::Ui) -> Response {
|
fn ui(self, ui: &mut egui::Ui) -> Response {
|
||||||
let size = self
|
let size = self
|
||||||
.size
|
.size
|
||||||
.unwrap_or_else(|| ui.style().spacing.interact_size.y);
|
.unwrap_or_else(|| ui.style().spacing.interact_size.y);
|
||||||
let (rect, response) = ui.allocate_exact_size(vec2(size, size), Sense::hover());
|
let (rect, response) = ui.allocate_exact_size(vec2(size, size), Sense::hover());
|
||||||
self.paint_at(ui, rect);
|
self.paint_at(ui, rect);
|
||||||
|
|
||||||
response
|
response
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The current state of the pull to refresh widget.
|
/// The current state of the pull to refresh widget.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum PullToRefreshState {
|
pub enum PullToRefreshState {
|
||||||
/// The widget is idle, no refresh is happening.
|
/// The widget is idle, no refresh is happening.
|
||||||
Idle,
|
Idle,
|
||||||
/// The user is dragging.
|
/// The user is dragging.
|
||||||
Dragging {
|
Dragging {
|
||||||
/// `distance` is the distance the user dragged.
|
/// `distance` is the distance the user dragged.
|
||||||
distance: f32,
|
distance: f32,
|
||||||
/// `far_enough` is true if the user dragged far enough to trigger a refresh.
|
/// `far_enough` is true if the user dragged far enough to trigger a refresh.
|
||||||
far_enough: bool,
|
far_enough: bool,
|
||||||
},
|
},
|
||||||
/// The user dragged far enough to trigger a refresh and released the pointer.
|
/// The user dragged far enough to trigger a refresh.
|
||||||
DoRefresh,
|
DoRefresh,
|
||||||
/// The refresh is currently happening.
|
/// The refresh is currently happening.
|
||||||
Refreshing,
|
Refreshing,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PullToRefreshState {
|
impl PullToRefreshState {
|
||||||
fn progress(&self, min_distance: f32) -> Option<f64> {
|
fn progress(&self, min_distance: f32) -> Option<f64> {
|
||||||
match self {
|
match self {
|
||||||
PullToRefreshState::Idle => Some(0.0),
|
PullToRefreshState::Idle => Some(0.0),
|
||||||
PullToRefreshState::Dragging { distance, .. } => {
|
PullToRefreshState::Dragging { distance, .. } => {
|
||||||
Some((distance / min_distance).min(1.0).max(0.0) as f64)
|
Some((distance / min_distance).min(1.0).max(0.0) as f64)
|
||||||
}
|
}
|
||||||
PullToRefreshState::DoRefresh => Some(1.0),
|
PullToRefreshState::DoRefresh => Some(1.0),
|
||||||
PullToRefreshState::Refreshing => None,
|
PullToRefreshState::Refreshing => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The response of the pull to refresh widget.
|
/// The response of the pull to refresh widget.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct PullToRefreshResponse<T> {
|
pub struct PullToRefreshResponse<T> {
|
||||||
/// Current state of the pull to refresh widget.
|
/// Current state of the pull to refresh widget.
|
||||||
pub state: PullToRefreshState,
|
pub state: PullToRefreshState,
|
||||||
/// The inner response of the widget you wrapped in [`PullToRefresh::ui`] or [`PullToRefresh::scroll_area_ui`].
|
/// The inner response of the widget you wrapped in [`PullToRefresh::ui`] or [`PullToRefresh::scroll_area_ui`].
|
||||||
pub inner: T,
|
pub inner: T,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> PullToRefreshResponse<T> {
|
impl<T> PullToRefreshResponse<T> {
|
||||||
/// Returns true if the user dragged far enough to trigger a refresh.
|
/// Returns true if the user dragged far enough to trigger a refresh.
|
||||||
pub fn should_refresh(&self) -> bool {
|
pub fn should_refresh(&self) -> bool {
|
||||||
matches!(self.state, PullToRefreshState::DoRefresh)
|
matches!(self.state, PullToRefreshState::DoRefresh)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A widget that allows the user to pull to refresh.
|
/// A widget that allows the user to pull to refresh.
|
||||||
pub struct PullToRefresh {
|
pub struct PullToRefresh {
|
||||||
id: Id,
|
id: Id,
|
||||||
loading: bool,
|
loading: bool,
|
||||||
min_refresh_distance: f32,
|
min_refresh_distance: f32,
|
||||||
can_refresh: bool,
|
can_refresh: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PullToRefresh {
|
impl PullToRefresh {
|
||||||
/// Creates a new pull to refresh widget.
|
/// Creates a new pull to refresh widget.
|
||||||
/// If `loading` is true, the widget will show the loading indicator.
|
/// If `loading` is true, the widget will show the loading indicator.
|
||||||
pub fn new(loading: bool) -> Self {
|
pub fn new(loading: bool) -> Self {
|
||||||
Self {
|
Self {
|
||||||
id: Id::new("pull_to_refresh"),
|
id: Id::new("pull_to_refresh"),
|
||||||
loading,
|
loading,
|
||||||
min_refresh_distance: 100.0,
|
min_refresh_distance: 100.0,
|
||||||
can_refresh: true,
|
can_refresh: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets the minimum distance the user needs to drag to trigger a refresh.
|
/// Sets the minimum distance the user needs to drag to trigger a refresh.
|
||||||
pub fn min_refresh_distance(mut self, min_refresh_distance: f32) -> Self {
|
pub fn min_refresh_distance(mut self, min_refresh_distance: f32) -> Self {
|
||||||
self.min_refresh_distance = min_refresh_distance;
|
self.min_refresh_distance = min_refresh_distance;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// You need to provide a id if you use multiple pull to refresh widgets at once.
|
/// You need to provide a id if you use multiple pull to refresh widgets at once.
|
||||||
pub fn id(mut self, id: Id) -> Self {
|
pub fn id(mut self, id: Id) -> Self {
|
||||||
self.id = id;
|
self.id = id;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// If `can_refresh` is false, pulling will not trigger a refresh.
|
/// If `can_refresh` is false, pulling will not trigger a refresh.
|
||||||
pub fn can_refresh(mut self, can_refresh: bool) -> Self {
|
pub fn can_refresh(mut self, can_refresh: bool) -> Self {
|
||||||
self.can_refresh = can_refresh;
|
self.can_refresh = can_refresh;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Shows the pull to refresh widget.
|
/// Shows the pull to refresh widget.
|
||||||
/// Note: If you want to use the pull to refresh widget in a scroll area, use [`Self::scroll_area_ui`].
|
/// Note: If you want to use the pull to refresh widget in a scroll area, use [`Self::scroll_area_ui`].
|
||||||
/// You might want to disable text selection via [`egui::style::Interaction`]
|
/// You might want to disable text selection via [`egui::style::Interaction`]
|
||||||
/// to avoid conflicts with the drag gesture.
|
/// to avoid conflicts with the drag gesture.
|
||||||
pub fn ui<T>(
|
pub fn ui<T>(
|
||||||
self,
|
self,
|
||||||
ui: &mut egui::Ui,
|
ui: &mut egui::Ui,
|
||||||
content: impl FnOnce(&mut egui::Ui) -> T,
|
content: impl FnOnce(&mut egui::Ui) -> T,
|
||||||
) -> PullToRefreshResponse<T> {
|
) -> PullToRefreshResponse<T> {
|
||||||
let mut child = ui.new_child(UiBuilder::new()
|
let mut child = ui.new_child(
|
||||||
.max_rect(ui.available_rect_before_wrap())
|
UiBuilder::new()
|
||||||
.layout(*ui.layout()));
|
.max_rect(ui.available_rect_before_wrap())
|
||||||
|
.layout(*ui.layout()),
|
||||||
|
);
|
||||||
|
|
||||||
let output = content(&mut child);
|
let output = content(&mut child);
|
||||||
|
|
||||||
let can_refresh = self.can_refresh;
|
let can_refresh = self.can_refresh;
|
||||||
let state = self.internal_ui(ui, can_refresh, None, child.min_rect());
|
let state = self.internal_ui(ui, can_refresh, None, child.min_rect());
|
||||||
|
|
||||||
PullToRefreshResponse {
|
PullToRefreshResponse {
|
||||||
state,
|
state,
|
||||||
inner: output,
|
inner: output,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Shows the pull to refresh widget, wrapping a [egui::ScrollArea].
|
/// Shows the pull to refresh widget, wrapping a [egui::ScrollArea].
|
||||||
/// Pass the output of the scroll area to the content function.
|
/// Pass the output of the scroll area to the content function.
|
||||||
pub fn scroll_area_ui<T>(
|
pub fn scroll_area_ui<T>(
|
||||||
self,
|
self,
|
||||||
ui: &mut egui::Ui,
|
ui: &mut egui::Ui,
|
||||||
content: impl FnOnce(&mut egui::Ui) -> ScrollAreaOutput<T>,
|
content: impl FnOnce(&mut egui::Ui) -> ScrollAreaOutput<T>,
|
||||||
) -> PullToRefreshResponse<ScrollAreaOutput<T>> {
|
) -> PullToRefreshResponse<ScrollAreaOutput<T>> {
|
||||||
let scroll_output = content(ui);
|
let scroll_output = content(ui);
|
||||||
let content_rect = scroll_output.inner_rect;
|
let content_rect = scroll_output.inner_rect;
|
||||||
let can_refresh = scroll_output.state.offset.y == 0.0 && self.can_refresh;
|
let can_refresh = scroll_output.state.offset.y == 0.0 && self.can_refresh;
|
||||||
// This is the id used in the Sense of the scroll area
|
// This is the id used in the Sense of the scroll area
|
||||||
// I hope this id is stable across egui patches...
|
// I hope this id is stable across egui patches...
|
||||||
let allow_dragged_id = scroll_output.id.with("area");
|
let allow_dragged_id = scroll_output.id.with("area");
|
||||||
let state = self.internal_ui(ui, can_refresh, Some(allow_dragged_id), content_rect);
|
let state = self.internal_ui(ui, can_refresh, Some(allow_dragged_id), content_rect);
|
||||||
PullToRefreshResponse {
|
PullToRefreshResponse {
|
||||||
state,
|
state,
|
||||||
inner: scroll_output,
|
inner: scroll_output,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn internal_ui(
|
fn internal_ui(
|
||||||
self,
|
self,
|
||||||
ui: &mut egui::Ui,
|
ui: &mut egui::Ui,
|
||||||
can_refresh: bool,
|
can_refresh: bool,
|
||||||
allow_dragged_id: Option<Id>,
|
allow_dragged_id: Option<Id>,
|
||||||
content_rect: Rect,
|
content_rect: Rect,
|
||||||
) -> PullToRefreshState {
|
) -> PullToRefreshState {
|
||||||
let last_state = ui.data_mut(|data| {
|
let last_state = ui.data_mut(|data| {
|
||||||
data.get_temp_mut_or(self.id, PullToRefreshState::Idle)
|
data.get_temp_mut_or(self.id, PullToRefreshState::Idle)
|
||||||
.clone()
|
.clone()
|
||||||
});
|
});
|
||||||
|
|
||||||
let mut state = last_state;
|
let mut state = last_state;
|
||||||
if self.loading {
|
if self.loading {
|
||||||
state = PullToRefreshState::Refreshing;
|
state = PullToRefreshState::Refreshing;
|
||||||
}
|
}
|
||||||
|
|
||||||
if !self.loading && matches!(state, PullToRefreshState::Refreshing) {
|
if !self.loading && matches!(state, PullToRefreshState::Refreshing) {
|
||||||
state = PullToRefreshState::Idle;
|
state = PullToRefreshState::Idle;
|
||||||
}
|
}
|
||||||
|
|
||||||
if can_refresh && !self.loading {
|
if can_refresh && !self.loading {
|
||||||
let sense = ui.interact(content_rect, self.id, Sense::hover());
|
let sense = ui.interact(content_rect, self.id, Sense::hover());
|
||||||
|
|
||||||
let is_something_blocking_drag = ui.ctx().dragged_id().is_some()
|
let is_something_blocking_drag = ui.ctx().dragged_id().is_some()
|
||||||
&& !allow_dragged_id.map_or(false, |id| ui.ctx().is_being_dragged(id));
|
&& !allow_dragged_id.map_or(false, |id| ui.ctx().is_being_dragged(id));
|
||||||
|
|
||||||
if sense.contains_pointer() && !is_something_blocking_drag {
|
if sense.contains_pointer() && !is_something_blocking_drag {
|
||||||
let (delta, any_released) = ui.input(|input| {
|
let (delta, any_released) = ui.input(|input| {
|
||||||
(
|
(
|
||||||
if input.pointer.is_decidedly_dragging() {
|
if input.pointer.is_decidedly_dragging() {
|
||||||
Some(input.pointer.delta())
|
Some(input.pointer.delta())
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
},
|
},
|
||||||
input.pointer.any_released(),
|
input.pointer.any_released(),
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
if let Some(delta) = delta {
|
if let Some(delta) = delta {
|
||||||
if matches!(state, PullToRefreshState::Idle) {
|
if matches!(state, PullToRefreshState::Idle) {
|
||||||
state = PullToRefreshState::Dragging {
|
state = PullToRefreshState::Dragging {
|
||||||
distance: 0.0,
|
distance: 0.0,
|
||||||
far_enough: false,
|
far_enough: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if let PullToRefreshState::Dragging { distance: drag, .. } = state.clone() {
|
if let PullToRefreshState::Dragging { distance: drag, .. } = state.clone() {
|
||||||
let dist = drag + delta.y;
|
let dist = drag + delta.y;
|
||||||
state = PullToRefreshState::Dragging {
|
state = PullToRefreshState::Dragging {
|
||||||
distance: dist,
|
distance: dist,
|
||||||
far_enough: dist > self.min_refresh_distance,
|
far_enough: dist > self.min_refresh_distance,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
state = PullToRefreshState::Idle;
|
state = PullToRefreshState::Idle;
|
||||||
}
|
}
|
||||||
if any_released {
|
if any_released {
|
||||||
if let PullToRefreshState::Dragging {
|
if let PullToRefreshState::Dragging {
|
||||||
far_enough: enough, ..
|
far_enough: enough, ..
|
||||||
} = state.clone()
|
} = state.clone()
|
||||||
{
|
{
|
||||||
if enough {
|
if enough {
|
||||||
state = PullToRefreshState::DoRefresh;
|
state = PullToRefreshState::DoRefresh;
|
||||||
} else {
|
} else {
|
||||||
state = PullToRefreshState::Idle;
|
state = PullToRefreshState::Idle;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
state = PullToRefreshState::Idle;
|
state = PullToRefreshState::Idle;
|
||||||
}
|
}
|
||||||
}
|
} else if let PullToRefreshState::Dragging {
|
||||||
} else {
|
far_enough: enough, ..
|
||||||
state = PullToRefreshState::Idle;
|
} = state.clone()
|
||||||
}
|
{
|
||||||
} else {
|
if enough {
|
||||||
state = PullToRefreshState::Idle;
|
state = PullToRefreshState::DoRefresh;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
state = PullToRefreshState::Idle;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
state = PullToRefreshState::Idle;
|
||||||
|
}
|
||||||
|
|
||||||
if self.loading {
|
if self.loading {
|
||||||
state = PullToRefreshState::Refreshing;
|
state = PullToRefreshState::Refreshing;
|
||||||
}
|
}
|
||||||
|
|
||||||
let spinner_size = Vec2::splat(24.0);
|
let spinner_size = Vec2::splat(24.0);
|
||||||
|
|
||||||
let progress_for_offset = match &state {
|
let progress_for_offset = match &state {
|
||||||
PullToRefreshState::Idle => 0.0,
|
PullToRefreshState::Idle => 0.0,
|
||||||
PullToRefreshState::Dragging { .. } => {
|
PullToRefreshState::Dragging { .. } => {
|
||||||
state.progress(self.min_refresh_distance).unwrap_or(1.0)
|
state.progress(self.min_refresh_distance).unwrap_or(1.0)
|
||||||
}
|
}
|
||||||
PullToRefreshState::DoRefresh => 1.0,
|
PullToRefreshState::DoRefresh => 1.0,
|
||||||
PullToRefreshState::Refreshing => 1.0,
|
PullToRefreshState::Refreshing => 1.0,
|
||||||
} as f32;
|
} as f32;
|
||||||
|
|
||||||
let anim_progress = ui.ctx().animate_value_with_time(
|
let anim_progress = ui.ctx().animate_value_with_time(
|
||||||
self.id.with("offset_top"),
|
self.id.with("offset_top"),
|
||||||
progress_for_offset,
|
progress_for_offset,
|
||||||
ui.style().animation_time,
|
ui.style().animation_time,
|
||||||
);
|
);
|
||||||
|
|
||||||
let offset_top = -spinner_size.y + spinner_size.y * anim_progress * 2.0;
|
let offset_top = -spinner_size.y + spinner_size.y * anim_progress * 2.0;
|
||||||
|
|
||||||
if anim_progress > 0.0 {
|
if anim_progress > 0.0 {
|
||||||
Area::new(Id::new("Pull to refresh indicator"))
|
Area::new(Id::new("Pull to refresh indicator"))
|
||||||
.fixed_pos(content_rect.center_top())
|
.fixed_pos(content_rect.center_top())
|
||||||
.pivot(Align2::CENTER_TOP)
|
.pivot(Align2::CENTER_TOP)
|
||||||
.show(ui.ctx(), |ui| {
|
.show(ui.ctx(), |ui| {
|
||||||
let (rect, _) = ui.allocate_exact_size(spinner_size, Sense::hover());
|
let (rect, _) = ui.allocate_exact_size(spinner_size, Sense::hover());
|
||||||
|
|
||||||
ui.set_clip_rect(Rect::everything_below(rect.min.y));
|
ui.set_clip_rect(Rect::everything_below(rect.min.y));
|
||||||
|
|
||||||
let rect = rect.translate(Vec2::new(0.0, offset_top));
|
let rect = rect.translate(Vec2::new(0.0, offset_top));
|
||||||
|
|
||||||
ui.painter().circle(
|
ui.painter().circle(
|
||||||
rect.center(),
|
rect.center(),
|
||||||
spinner_size.x / 1.5,
|
spinner_size.x / 1.5,
|
||||||
ui.style().visuals.widgets.inactive.bg_fill,
|
ui.style().visuals.widgets.inactive.bg_fill,
|
||||||
ui.visuals().widgets.inactive.bg_stroke,
|
ui.visuals().widgets.inactive.bg_stroke,
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut spinner_color = ui.style().visuals.widgets.inactive.fg_stroke.color;
|
let mut spinner_color = ui.style().visuals.widgets.inactive.fg_stroke.color;
|
||||||
if anim_progress < 1.0 {
|
if anim_progress < 1.0 {
|
||||||
spinner_color = Color32::from_rgba_unmultiplied(
|
spinner_color = Color32::from_rgba_unmultiplied(
|
||||||
spinner_color.r(),
|
spinner_color.r(),
|
||||||
spinner_color.g(),
|
spinner_color.g(),
|
||||||
spinner_color.b(),
|
spinner_color.b(),
|
||||||
(spinner_color.a() as f32 * 0.7).round() as u8,
|
(spinner_color.a() as f32 * 0.7).round() as u8,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
ProgressSpinner::new()
|
ProgressSpinner::new()
|
||||||
.color(spinner_color)
|
.color(spinner_color)
|
||||||
.progress(state.progress(self.min_refresh_distance))
|
.progress(state.progress(self.min_refresh_distance))
|
||||||
.paint_at(ui, rect);
|
.paint_at(ui, rect);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
ui.data_mut(|data| {
|
ui.data_mut(|data| {
|
||||||
data.insert_temp(self.id, state.clone());
|
data.insert_temp(self.id, state.clone());
|
||||||
});
|
});
|
||||||
|
|
||||||
state
|
state
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+465
-372
@@ -12,421 +12,514 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
|
use egui::epaint::RectShape;
|
||||||
|
use egui::{SizeHint, TextureHandle, UiBuilder};
|
||||||
|
use image::codecs::png::{CompressionType, FilterType, PngEncoder};
|
||||||
|
use image::{ExtendedColorType, ImageEncoder};
|
||||||
|
use parking_lot::RwLock;
|
||||||
|
use qrcodegen::QrCode;
|
||||||
use std::mem::size_of;
|
use std::mem::size_of;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use parking_lot::RwLock;
|
|
||||||
use std::thread;
|
use std::thread;
|
||||||
use egui::{SizeHint, TextureHandle, UiBuilder};
|
|
||||||
use egui::epaint::RectShape;
|
|
||||||
use image::{ExtendedColorType, ImageEncoder};
|
|
||||||
use image::codecs::png::{CompressionType, FilterType, PngEncoder};
|
|
||||||
use qrcodegen::QrCode;
|
|
||||||
|
|
||||||
use crate::gui::Colors;
|
use crate::gui::Colors;
|
||||||
use crate::gui::icons::IMAGES_SQUARE;
|
use crate::gui::icons::{COPY, IMAGES_SQUARE};
|
||||||
use crate::gui::platform::PlatformCallbacks;
|
use crate::gui::platform::PlatformCallbacks;
|
||||||
use crate::gui::views::types::QrImageState;
|
|
||||||
use crate::gui::views::View;
|
use crate::gui::views::View;
|
||||||
|
use crate::gui::views::types::QrImageState;
|
||||||
|
|
||||||
/// QR code image from text.
|
/// QR code image from text.
|
||||||
pub struct QrCodeContent {
|
pub struct QrCodeContent {
|
||||||
/// QR code text.
|
/// QR code text.
|
||||||
text: String,
|
pub text: String,
|
||||||
|
/// Flag to show text below QR code.
|
||||||
|
show_text: bool,
|
||||||
|
/// Flag to copy text below QR code.
|
||||||
|
can_copy_text: bool,
|
||||||
|
|
||||||
/// Flag to draw animated QR with Uniform Resources
|
/// Maximum QR code size.
|
||||||
/// https://github.com/BlockchainCommons/Research/blob/master/papers/bcr-2020-005-ur.md
|
max_size: f32,
|
||||||
animated: bool,
|
|
||||||
/// Index of current image at animation.
|
|
||||||
animated_index: Option<usize>,
|
|
||||||
/// Time of last image draw.
|
|
||||||
animation_time: Option<i64>,
|
|
||||||
|
|
||||||
/// Texture handle to show image when created.
|
/// Flag to draw animated QR with Uniform Resources
|
||||||
texture_handle: Option<TextureHandle>,
|
/// https://github.com/BlockchainCommons/Research/blob/master/papers/bcr-2020-005-ur.md
|
||||||
/// QR code view data state.
|
animated: bool,
|
||||||
qr_image_state: Arc<RwLock<QrImageState>>,
|
/// Index of current image at animation.
|
||||||
|
animated_index: Option<usize>,
|
||||||
|
/// Time of last image draw.
|
||||||
|
animation_time: Option<i64>,
|
||||||
|
|
||||||
|
/// Texture handle to show image when created.
|
||||||
|
texture_handle: Option<TextureHandle>,
|
||||||
|
/// QR code view data state.
|
||||||
|
qr_image_state: Arc<RwLock<QrImageState>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_QR_SIZE: u32 = 512;
|
const DEFAULT_QR_SIZE: u32 = 512;
|
||||||
|
|
||||||
impl QrCodeContent {
|
impl QrCodeContent {
|
||||||
pub fn new(text: String, animated: bool) -> Self {
|
pub fn new(text: String, animated: bool) -> Self {
|
||||||
Self {
|
Self {
|
||||||
text,
|
text,
|
||||||
animated,
|
show_text: true,
|
||||||
animated_index: None,
|
can_copy_text: true,
|
||||||
animation_time: None,
|
max_size: DEFAULT_QR_SIZE as f32,
|
||||||
texture_handle: None,
|
animated,
|
||||||
qr_image_state: Arc::new(RwLock::new(QrImageState::default())),
|
animated_index: None,
|
||||||
}
|
animation_time: None,
|
||||||
}
|
texture_handle: None,
|
||||||
|
qr_image_state: Arc::new(RwLock::new(QrImageState::default())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Draw QR code.
|
/// Setup maximum QR code size.
|
||||||
pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
pub fn with_max_size(mut self, max_size: f32) -> Self {
|
||||||
if self.animated {
|
self.max_size = max_size;
|
||||||
// Show animated QR code.
|
self
|
||||||
self.animated_ui(ui, cb);
|
}
|
||||||
} else {
|
|
||||||
// Show static QR code.
|
|
||||||
self.static_ui(ui, cb);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Draw animated QR code content.
|
/// Hide text below QR code.
|
||||||
fn animated_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
pub fn hide_text(mut self) -> Self {
|
||||||
if !self.has_image() {
|
self.show_text = false;
|
||||||
let space = (ui.available_width() - View::BIG_SPINNER_SIZE) / 2.0;
|
self
|
||||||
ui.vertical_centered(|ui| {
|
}
|
||||||
ui.add_space(space);
|
|
||||||
View::big_loading_spinner(ui);
|
|
||||||
ui.add_space(space);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create multiple vector images from text if not creating.
|
/// Do not show button to copy QR code text.
|
||||||
if !self.loading() {
|
pub fn no_copy(mut self) -> Self {
|
||||||
self.create_svg_list();
|
self.can_copy_text = false;
|
||||||
}
|
self
|
||||||
} else {
|
}
|
||||||
let svg_list = {
|
|
||||||
let r_create = self.qr_image_state.read();
|
|
||||||
r_create.svg_list.clone().unwrap()
|
|
||||||
};
|
|
||||||
|
|
||||||
// Setup animated index.
|
/// Draw QR code.
|
||||||
let now = chrono::Utc::now().timestamp_millis();
|
pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||||
if now - *self.animation_time.get_or_insert(now) > 100 {
|
if self.animated {
|
||||||
if let Some(i) = self.animated_index {
|
// Show animated QR code.
|
||||||
self.animated_index = Some(i + 1);
|
self.animated_ui(ui, cb);
|
||||||
}
|
} else {
|
||||||
if *self.animated_index.get_or_insert(0) == svg_list.len() {
|
// Show static QR code.
|
||||||
self.animated_index = Some(0);
|
self.static_ui(ui, cb);
|
||||||
}
|
}
|
||||||
self.animation_time = Some(now);
|
ui.add_space(8.0);
|
||||||
}
|
View::horizontal_line(ui, Colors::item_stroke());
|
||||||
|
ui.add_space(8.0);
|
||||||
|
}
|
||||||
|
|
||||||
let svg = svg_list[self.animated_index.unwrap_or(0)].clone();
|
/// Draw animated QR code content.
|
||||||
|
fn animated_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||||
|
if !self.has_image() {
|
||||||
|
let space = (ui.available_width() - View::BIG_SPINNER_SIZE) / 2.0;
|
||||||
|
ui.vertical_centered(|ui| {
|
||||||
|
ui.add_space(space);
|
||||||
|
View::big_loading_spinner(ui);
|
||||||
|
ui.add_space(space);
|
||||||
|
});
|
||||||
|
|
||||||
// Create images from SVG data.
|
// Create multiple vector images from text if not creating.
|
||||||
self.qr_image_ui(svg, ui);
|
if !self.loading() {
|
||||||
|
self.create_svg_list();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let svg_list = {
|
||||||
|
let r_create = self.qr_image_state.read();
|
||||||
|
r_create.svg_list.clone().unwrap()
|
||||||
|
};
|
||||||
|
|
||||||
// Show QR code text.
|
// Setup animated index.
|
||||||
ui.add_space(6.0);
|
let now = chrono::Utc::now().timestamp_millis();
|
||||||
View::ellipsize_text(ui, self.text.clone(), 16.0, Colors::inactive_text());
|
if now - *self.animation_time.get_or_insert(now) > 100 {
|
||||||
ui.add_space(6.0);
|
if let Some(i) = self.animated_index {
|
||||||
|
self.animated_index = Some(i + 1);
|
||||||
|
}
|
||||||
|
if *self.animated_index.get_or_insert(0) == svg_list.len() {
|
||||||
|
self.animated_index = Some(0);
|
||||||
|
}
|
||||||
|
self.animation_time = Some(now);
|
||||||
|
}
|
||||||
|
|
||||||
ui.vertical_centered(|ui| {
|
let svg = svg_list[self.animated_index.unwrap_or(0)].clone();
|
||||||
let sharing = {
|
|
||||||
let r_state = self.qr_image_state.read();
|
|
||||||
r_state.exporting || r_state.gif_creating
|
|
||||||
};
|
|
||||||
if !sharing {
|
|
||||||
// Show button to share QR.
|
|
||||||
let share_text = format!("{} {}", IMAGES_SQUARE, t!("share"));
|
|
||||||
View::colored_text_button(ui,
|
|
||||||
share_text,
|
|
||||||
Colors::blue(),
|
|
||||||
Colors::white_or_black(false), || {
|
|
||||||
{
|
|
||||||
let mut w_state = self.qr_image_state.write();
|
|
||||||
w_state.exporting = true;
|
|
||||||
}
|
|
||||||
// Create GIF to export.
|
|
||||||
self.create_qr_gif();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
ui.vertical_centered(|ui| {
|
|
||||||
ui.add_space(2.0);
|
|
||||||
View::small_loading_spinner(ui);
|
|
||||||
ui.add_space(1.0);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
ui.add_space(8.0);
|
// Create images from SVG data.
|
||||||
View::horizontal_line(ui, Colors::item_stroke());
|
self.qr_image_ui(svg, ui);
|
||||||
ui.add_space(8.0);
|
|
||||||
|
|
||||||
// Check if GIF was created to share.
|
// Show QR code text.
|
||||||
let has_gif = {
|
if self.show_text {
|
||||||
let r_state = self.qr_image_state.read();
|
self.text_ui(ui);
|
||||||
r_state.gif_data.is_some()
|
}
|
||||||
};
|
|
||||||
if has_gif {
|
|
||||||
let data = {
|
|
||||||
let r_state = self.qr_image_state.read();
|
|
||||||
r_state.gif_data.clone().unwrap()
|
|
||||||
};
|
|
||||||
let name = format!("{}.gif", chrono::Utc::now().timestamp());
|
|
||||||
cb.share_data(name, data).unwrap_or_default();
|
|
||||||
// Clear GIF data and exporting flag.
|
|
||||||
{
|
|
||||||
let mut w_state = self.qr_image_state.write();
|
|
||||||
w_state.gif_data = None;
|
|
||||||
w_state.exporting = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
ui.ctx().request_repaint();
|
ui.vertical_centered(|ui| {
|
||||||
}
|
let sharing = {
|
||||||
}
|
let r_state = self.qr_image_state.read();
|
||||||
|
r_state.exporting || r_state.gif_creating
|
||||||
|
};
|
||||||
|
if !sharing {
|
||||||
|
ui.vertical_centered(|ui| {
|
||||||
|
// Show button to share QR.
|
||||||
|
let share_text = format!("{} {}", IMAGES_SQUARE, t!("share"));
|
||||||
|
View::colored_text_button(
|
||||||
|
ui,
|
||||||
|
share_text,
|
||||||
|
Colors::blue(),
|
||||||
|
Colors::white_or_black(false),
|
||||||
|
|| {
|
||||||
|
{
|
||||||
|
let mut w_state = self.qr_image_state.write();
|
||||||
|
w_state.exporting = true;
|
||||||
|
}
|
||||||
|
// Create GIF to export.
|
||||||
|
self.create_qr_gif();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
ui.vertical_centered(|ui| {
|
||||||
|
ui.add_space(2.0);
|
||||||
|
View::small_loading_spinner(ui);
|
||||||
|
ui.add_space(1.0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/// Draw static QR code content.
|
// Check if GIF was created to share.
|
||||||
fn static_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
let has_gif = {
|
||||||
if !self.has_image() {
|
let r_state = self.qr_image_state.read();
|
||||||
let space = (ui.available_width() - View::BIG_SPINNER_SIZE) / 2.0;
|
r_state.gif_data.is_some()
|
||||||
ui.vertical_centered(|ui| {
|
};
|
||||||
ui.add_space(space);
|
if has_gif {
|
||||||
View::big_loading_spinner(ui);
|
let data = {
|
||||||
ui.add_space(space);
|
let r_state = self.qr_image_state.read();
|
||||||
});
|
r_state.gif_data.clone().unwrap()
|
||||||
|
};
|
||||||
|
let name = format!("{}.gif", chrono::Utc::now().timestamp());
|
||||||
|
cb.share_data(name, data).unwrap_or_default();
|
||||||
|
// Clear GIF data and exporting flag.
|
||||||
|
{
|
||||||
|
let mut w_state = self.qr_image_state.write();
|
||||||
|
w_state.gif_data = None;
|
||||||
|
w_state.exporting = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Create vector image from text if not creating.
|
ui.ctx().request_repaint();
|
||||||
if !self.loading() {
|
}
|
||||||
self.create_svg();
|
}
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Create image from SVG data.
|
|
||||||
let svg = {
|
|
||||||
let r_state = self.qr_image_state.read();
|
|
||||||
r_state.svg.clone().unwrap()
|
|
||||||
};
|
|
||||||
self.qr_image_ui(svg, ui);
|
|
||||||
|
|
||||||
// Show QR code text.
|
/// Draw static QR code content.
|
||||||
ui.add_space(6.0);
|
fn static_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||||
View::ellipsize_text(ui, self.text.clone(), 16.0, Colors::inactive_text());
|
if !self.has_image() {
|
||||||
ui.add_space(6.0);
|
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);
|
||||||
|
});
|
||||||
|
|
||||||
// Show button to share QR.
|
// Create vector image from text if not creating.
|
||||||
ui.vertical_centered(|ui| {
|
if !self.loading() {
|
||||||
let share_text = format!("{} {}", IMAGES_SQUARE, t!("share"));
|
self.create_svg();
|
||||||
View::colored_text_button(ui,
|
}
|
||||||
share_text,
|
} else {
|
||||||
Colors::blue(),
|
// Create image from SVG data.
|
||||||
Colors::white_or_black(false), || {
|
let svg = {
|
||||||
let text = self.text.as_str();
|
let r_state = self.qr_image_state.read();
|
||||||
if let Ok(qr) = QrCode::encode_text(text, qrcodegen::QrCodeEcc::Low) {
|
r_state.svg.clone().unwrap()
|
||||||
if let Some(data) = Self::qr_to_image_data(qr, DEFAULT_QR_SIZE as usize) {
|
};
|
||||||
let mut png = vec![];
|
self.qr_image_ui(svg, ui);
|
||||||
let png_enc = PngEncoder::new_with_quality(&mut png,
|
|
||||||
CompressionType::Best,
|
|
||||||
FilterType::NoFilter);
|
|
||||||
if let Ok(()) = png_enc.write_image(data.as_slice(),
|
|
||||||
DEFAULT_QR_SIZE,
|
|
||||||
DEFAULT_QR_SIZE,
|
|
||||||
ExtendedColorType::L8) {
|
|
||||||
let name = format!("{}.png", chrono::Utc::now().timestamp());
|
|
||||||
cb.share_data(name, png).unwrap_or_default();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
ui.add_space(8.0);
|
|
||||||
View::horizontal_line(ui, Colors::item_stroke());
|
|
||||||
ui.add_space(8.0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Draw QR code image content.
|
// Show QR code text.
|
||||||
fn qr_image_ui(&mut self, svg: Vec<u8>, ui: &mut egui::Ui) {
|
if self.show_text {
|
||||||
let mut rect = ui.available_rect_before_wrap();
|
self.text_ui(ui);
|
||||||
rect.min += egui::emath::vec2(10.0, 0.0);
|
} else {
|
||||||
rect.max -= egui::emath::vec2(10.0, 0.0);
|
ui.add_space(8.0);
|
||||||
|
}
|
||||||
|
|
||||||
// Create background shape.
|
if self.can_copy_text {
|
||||||
let mut bg_shape = RectShape::new(
|
// Setup spacing between buttons.
|
||||||
rect,
|
ui.spacing_mut().item_spacing = egui::Vec2::new(6.0, 0.0);
|
||||||
egui::Rounding::default(),
|
|
||||||
egui::Color32::WHITE,
|
|
||||||
egui::Stroke::NONE
|
|
||||||
);
|
|
||||||
let bg_idx = ui.painter().add(bg_shape);
|
|
||||||
|
|
||||||
// Draw QR code image content.
|
ui.columns(2, |columns| {
|
||||||
let mut content_rect = ui.allocate_new_ui(UiBuilder::new().max_rect(rect), |ui| {
|
columns[0].vertical_centered_justified(|ui| {
|
||||||
ui.add_space(10.0);
|
// Draw copy button.
|
||||||
let size = SizeHint::Size(ui.available_width() as u32, ui.available_width() as u32);
|
let copy_text = format!("{} {}", COPY, t!("copy"));
|
||||||
self.texture_handle = Some(View::svg_image(ui, "qr_code", svg.as_slice(), Some(size)));
|
View::button(ui, copy_text, Colors::white_or_black(false), || {
|
||||||
ui.add_space(10.0);
|
cb.copy_string_to_buffer(self.text.clone());
|
||||||
}).response.rect;
|
});
|
||||||
|
});
|
||||||
|
columns[1].vertical_centered_justified(|ui| {
|
||||||
|
self.share_static_button_ui(ui, cb);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
ui.vertical_centered(|ui| {
|
||||||
|
self.share_static_button_ui(ui, cb);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Setup background size.
|
/// Draw button to share static QR code.
|
||||||
content_rect.min -= egui::emath::vec2(10.0, 0.0);
|
fn share_static_button_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||||
content_rect.max += egui::emath::vec2(10.0, 0.0);
|
let share_text = format!("{} {}", IMAGES_SQUARE, t!("share"));
|
||||||
bg_shape.rect = content_rect;
|
View::colored_text_button(
|
||||||
ui.painter().set(bg_idx, bg_shape);
|
ui,
|
||||||
}
|
share_text,
|
||||||
|
Colors::blue(),
|
||||||
|
Colors::white_or_black(false),
|
||||||
|
|| {
|
||||||
|
self.share_static(cb);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// Check if QR code is loading.
|
/// Share static QR code image.
|
||||||
fn loading(&self) -> bool {
|
fn share_static(&self, cb: &dyn PlatformCallbacks) {
|
||||||
let r_state = self.qr_image_state.read();
|
let text = self.text.as_str();
|
||||||
r_state.loading
|
if let Ok(qr) = QrCode::encode_text(text, qrcodegen::QrCodeEcc::Low) {
|
||||||
}
|
let size = DEFAULT_QR_SIZE as usize;
|
||||||
|
if let Some(data) = Self::qr_to_image_data(qr, size) {
|
||||||
|
let mut png = vec![];
|
||||||
|
let png_enc = PngEncoder::new_with_quality(
|
||||||
|
&mut png,
|
||||||
|
CompressionType::Best,
|
||||||
|
FilterType::NoFilter,
|
||||||
|
);
|
||||||
|
if let Ok(()) = png_enc.write_image(
|
||||||
|
data.as_slice(),
|
||||||
|
DEFAULT_QR_SIZE,
|
||||||
|
DEFAULT_QR_SIZE,
|
||||||
|
ExtendedColorType::L8,
|
||||||
|
) {
|
||||||
|
let name = format!("{}.png", chrono::Utc::now().timestamp());
|
||||||
|
cb.share_data(name, png).unwrap_or_default();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Create multiple vector QR code images at separate thread.
|
/// Draw QR code image content.
|
||||||
fn create_svg_list(&self) {
|
fn qr_image_ui(&mut self, svg: Vec<u8>, ui: &mut egui::Ui) {
|
||||||
let qr_state = self.qr_image_state.clone();
|
View::max_width_ui(ui, self.max_size, |ui| {
|
||||||
let text = self.text.clone();
|
let mut rect = ui.available_rect_before_wrap();
|
||||||
thread::spawn(move || {
|
rect.min += egui::emath::vec2(10.0, 0.0);
|
||||||
let mut encoder = ur::Encoder::bytes(text.as_bytes(), 100).unwrap();
|
rect.max -= egui::emath::vec2(10.0, 0.0);
|
||||||
let mut data = Vec::with_capacity(encoder.fragment_count());
|
|
||||||
for _ in 0..encoder.fragment_count() {
|
|
||||||
let ur = encoder.next_part().unwrap();
|
|
||||||
if let Ok(qr) = QrCode::encode_text(ur.as_str(), qrcodegen::QrCodeEcc::Low) {
|
|
||||||
let svg = Self::qr_to_svg(qr, 0);
|
|
||||||
data.push(svg.into_bytes());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let mut w_state = qr_state.write();
|
|
||||||
if !data.is_empty() {
|
|
||||||
w_state.svg_list = Some(data);
|
|
||||||
}
|
|
||||||
w_state.loading = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if image was created.
|
// Create background shape.
|
||||||
fn has_image(&self) -> bool {
|
let mut bg_shape = RectShape::new(
|
||||||
let r_state = self.qr_image_state.read();
|
rect,
|
||||||
r_state.svg.is_some() || r_state.svg_list.is_some()
|
egui::CornerRadius::default(),
|
||||||
}
|
egui::Color32::WHITE,
|
||||||
|
egui::Stroke::NONE,
|
||||||
|
egui::StrokeKind::Outside,
|
||||||
|
);
|
||||||
|
let bg_idx = ui.painter().add(bg_shape.clone());
|
||||||
|
|
||||||
/// Create vector QR code image at separate thread.
|
// Draw QR code image.
|
||||||
fn create_svg(&self) {
|
let mut content_rect = ui
|
||||||
let qr_state = self.qr_image_state.clone();
|
.scope_builder(UiBuilder::new().max_rect(rect), |ui| {
|
||||||
let text = self.text.clone();
|
ui.add_space(10.0);
|
||||||
thread::spawn(move || {
|
let size = SizeHint::Size {
|
||||||
if let Ok(qr) = QrCode::encode_text(text.as_str(), qrcodegen::QrCodeEcc::Low) {
|
width: ui.available_width() as u32,
|
||||||
let svg = Self::qr_to_svg(qr, 0);
|
height: ui.available_width() as u32,
|
||||||
let mut w_state = qr_state.write();
|
maintain_aspect_ratio: true,
|
||||||
w_state.loading = false;
|
};
|
||||||
w_state.svg = Some(svg.into_bytes());
|
self.texture_handle = Some(View::svg_image(ui, "qr", svg.as_slice(), size));
|
||||||
}
|
ui.add_space(10.0);
|
||||||
});
|
})
|
||||||
}
|
.response
|
||||||
|
.rect;
|
||||||
|
|
||||||
/// Convert QR code to SVG string.
|
// Setup background size.
|
||||||
fn qr_to_svg(qr: QrCode, border: i32) -> String {
|
content_rect.min -= egui::emath::vec2(10.0, 0.0);
|
||||||
let mut result = String::new();
|
content_rect.max += egui::emath::vec2(10.0, 0.0);
|
||||||
let dimension = qr.size().checked_add(border.checked_mul(2).unwrap()).unwrap();
|
bg_shape.rect = content_rect;
|
||||||
result += "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
|
ui.painter().set(bg_idx, bg_shape);
|
||||||
result += "<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n";
|
});
|
||||||
result += &format!(
|
}
|
||||||
"<svg xmlns=\"http://www.w3.org/2000/svg\" version=\"1.1\" viewBox=\"0 0 {0} {0}\" stroke=\"none\">\n", dimension);
|
|
||||||
result += "\t<rect width=\"100%\" height=\"100%\" fill=\"#FFFFFF\"/>\n";
|
|
||||||
result += "\t<path d=\"";
|
|
||||||
for y in 0 .. qr.size() {
|
|
||||||
for x in 0 .. qr.size() {
|
|
||||||
if qr.get_module(x, y) {
|
|
||||||
if x != 0 || y != 0 {
|
|
||||||
result += " ";
|
|
||||||
}
|
|
||||||
result += &format!("M{},{}h1v1h-1z", x + border, y + border);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
result += "\" fill=\"#000000\"/>\n";
|
|
||||||
result += "</svg>\n";
|
|
||||||
result
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create GIF image at separate thread.
|
/// Draw QR code text.
|
||||||
fn create_qr_gif(&self) {
|
fn text_ui(&self, ui: &mut egui::Ui) {
|
||||||
{
|
ui.add_space(6.0);
|
||||||
let mut w_state = self.qr_image_state.write();
|
View::ellipsize_text(ui, self.text.clone(), 15.0, Colors::inactive_text());
|
||||||
w_state.gif_creating = true;
|
ui.add_space(6.0);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
/// Check if QR code is loading.
|
||||||
let qr_state = self.qr_image_state.clone();
|
fn loading(&self) -> bool {
|
||||||
let text = self.text.clone();
|
let r_state = self.qr_image_state.read();
|
||||||
thread::spawn(move || {
|
r_state.loading
|
||||||
// Setup GIF image encoder.
|
}
|
||||||
let mut gif = vec![];
|
|
||||||
{
|
|
||||||
// Generate QR codes from text.
|
|
||||||
let mut qrs = vec![];
|
|
||||||
let mut ur_enc = ur::Encoder::bytes(text.as_bytes(), 100).unwrap();
|
|
||||||
for _ in 0..ur_enc.fragment_count() {
|
|
||||||
let ur = ur_enc.next_part().unwrap();
|
|
||||||
if let Ok(qr) = qrcode::QrCode::with_error_correction_level(
|
|
||||||
ur.as_bytes(),
|
|
||||||
qrcode::EcLevel::L
|
|
||||||
) {
|
|
||||||
// Create an image from QR data.
|
|
||||||
let image = qr.render()
|
|
||||||
.max_dimensions(DEFAULT_QR_SIZE, DEFAULT_QR_SIZE)
|
|
||||||
.dark_color(image::Rgb([0, 0, 0]))
|
|
||||||
.light_color(image::Rgb([255, 255, 255]))
|
|
||||||
.build();
|
|
||||||
qrs.push(image);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !qrs.is_empty() {
|
/// Create multiple vector QR code images at separate thread.
|
||||||
// Generate GIF data.
|
fn create_svg_list(&self) {
|
||||||
let color_map = &[0, 0, 0, 0xFF, 0xFF, 0xFF];
|
let qr_state = self.qr_image_state.clone();
|
||||||
let mut gif_enc = gif::Encoder::new(&mut gif,
|
let text = self.text.clone();
|
||||||
qrs[0].width() as u16,
|
thread::spawn(move || {
|
||||||
qrs[0].height() as u16,
|
let mut encoder = ur::Encoder::bytes(text.as_bytes(), 64).unwrap();
|
||||||
color_map).unwrap();
|
let mut data = Vec::with_capacity(encoder.fragment_count());
|
||||||
gif_enc.set_repeat(gif::Repeat::Infinite).unwrap();
|
for _ in 0..encoder.fragment_count() {
|
||||||
for qr in qrs {
|
let ur = encoder.next_part().unwrap();
|
||||||
let mut frame = gif::Frame::from_rgb(qr.width() as u16,
|
if let Ok(qr) = QrCode::encode_text(ur.as_str(), qrcodegen::QrCodeEcc::Low) {
|
||||||
qr.height() as u16,
|
let svg = Self::qr_to_svg(qr, 0);
|
||||||
qr.as_raw().as_slice());
|
data.push(svg.into_bytes());
|
||||||
frame.delay = 10;
|
}
|
||||||
// Write an image to GIF encoder.
|
}
|
||||||
if let Ok(_) = gif_enc.write_frame(&frame) {
|
let mut w_state = qr_state.write();
|
||||||
continue;
|
if !data.is_empty() {
|
||||||
}
|
w_state.svg_list = Some(data);
|
||||||
// Exit on error.
|
}
|
||||||
let mut w_state = qr_state.write();
|
w_state.loading = false;
|
||||||
w_state.gif_creating = false;
|
});
|
||||||
return;
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Setup GIF image data.
|
|
||||||
let mut w_state = qr_state.write();
|
|
||||||
if !gif.is_empty() {
|
|
||||||
w_state.gif_data = Some(gif);
|
|
||||||
}
|
|
||||||
w_state.gif_creating = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Convert QR code to image data.
|
/// Check if image was created.
|
||||||
fn qr_to_image_data(qr: QrCode, size: usize) -> Option<Vec<u8>> {
|
fn has_image(&self) -> bool {
|
||||||
if size >= 2usize.pow((size_of::<usize>() * 4) as u32) {
|
let r_state = self.qr_image_state.read();
|
||||||
return None;
|
r_state.svg.is_some() || r_state.svg_list.is_some()
|
||||||
}
|
}
|
||||||
let margin_size = 1;
|
|
||||||
let s = qr.size();
|
|
||||||
let data_length = s as usize;
|
|
||||||
let data_length_with_margin = data_length + 2 * margin_size;
|
|
||||||
let point_size = size / data_length_with_margin;
|
|
||||||
if point_size == 0 {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let margin = (size - (point_size * data_length)) / 2;
|
|
||||||
let length = size * size;
|
|
||||||
let mut img_raw: Vec<u8> = vec![255u8; length];
|
|
||||||
for i in 0..s {
|
|
||||||
for j in 0..s {
|
|
||||||
if qr.get_module(i, j) {
|
|
||||||
let x = i as usize * point_size + margin;
|
|
||||||
let y = j as usize * point_size + margin;
|
|
||||||
|
|
||||||
for j in y..(y + point_size) {
|
/// Create vector QR code image at separate thread.
|
||||||
let offset = j * size;
|
fn create_svg(&self) {
|
||||||
for i in x..(x + point_size) {
|
let qr_state = self.qr_image_state.clone();
|
||||||
img_raw[offset + i] = 0;
|
let text = self.text.clone();
|
||||||
}
|
thread::spawn(move || {
|
||||||
}
|
if let Ok(qr) = QrCode::encode_text(text.as_str(), qrcodegen::QrCodeEcc::Low) {
|
||||||
}
|
let svg = Self::qr_to_svg(qr, 0);
|
||||||
}
|
let mut w_state = qr_state.write();
|
||||||
}
|
w_state.loading = false;
|
||||||
Some(img_raw)
|
w_state.svg = Some(svg.into_bytes());
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert QR code to SVG string.
|
||||||
|
fn qr_to_svg(qr: QrCode, border: i32) -> String {
|
||||||
|
let mut result = String::new();
|
||||||
|
let dimension = qr
|
||||||
|
.size()
|
||||||
|
.checked_add(border.checked_mul(2).unwrap())
|
||||||
|
.unwrap();
|
||||||
|
result += "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
|
||||||
|
result += "<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n";
|
||||||
|
result += &format!(
|
||||||
|
"<svg xmlns=\"http://www.w3.org/2000/svg\" version=\"1.1\" viewBox=\"0 0 {0} {0}\" stroke=\"none\">\n",
|
||||||
|
dimension
|
||||||
|
);
|
||||||
|
result += "\t<rect width=\"100%\" height=\"100%\" fill=\"#FFFFFF\"/>\n";
|
||||||
|
result += "\t<path d=\"";
|
||||||
|
for y in 0..qr.size() {
|
||||||
|
for x in 0..qr.size() {
|
||||||
|
if qr.get_module(x, y) {
|
||||||
|
if x != 0 || y != 0 {
|
||||||
|
result += " ";
|
||||||
|
}
|
||||||
|
result += &format!("M{},{}h1v1h-1z", x + border, y + border);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result += "\" fill=\"#000000\"/>\n";
|
||||||
|
result += "</svg>\n";
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create GIF image at separate thread.
|
||||||
|
fn create_qr_gif(&self) {
|
||||||
|
{
|
||||||
|
let mut w_state = self.qr_image_state.write();
|
||||||
|
w_state.gif_creating = true;
|
||||||
|
}
|
||||||
|
let qr_state = self.qr_image_state.clone();
|
||||||
|
let text = self.text.clone();
|
||||||
|
thread::spawn(move || {
|
||||||
|
// Setup GIF image encoder.
|
||||||
|
let mut gif = vec![];
|
||||||
|
{
|
||||||
|
// Generate QR codes from text.
|
||||||
|
let mut qrs = vec![];
|
||||||
|
let mut ur_enc = ur::Encoder::bytes(text.as_bytes(), 100).unwrap();
|
||||||
|
for _ in 0..ur_enc.fragment_count() {
|
||||||
|
let ur = ur_enc.next_part().unwrap();
|
||||||
|
if let Ok(qr) = qrcode::QrCode::with_error_correction_level(
|
||||||
|
ur.as_bytes(),
|
||||||
|
qrcode::EcLevel::L,
|
||||||
|
) {
|
||||||
|
// Create an image from QR data.
|
||||||
|
let image = qr
|
||||||
|
.render()
|
||||||
|
.max_dimensions(DEFAULT_QR_SIZE, DEFAULT_QR_SIZE)
|
||||||
|
.dark_color(image::Rgb([0, 0, 0]))
|
||||||
|
.light_color(image::Rgb([255, 255, 255]))
|
||||||
|
.build();
|
||||||
|
qrs.push(image);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !qrs.is_empty() {
|
||||||
|
// Generate GIF data.
|
||||||
|
let color_map = &[0, 0, 0, 0xFF, 0xFF, 0xFF];
|
||||||
|
let mut gif_enc = gif::Encoder::new(
|
||||||
|
&mut gif,
|
||||||
|
qrs[0].width() as u16,
|
||||||
|
qrs[0].height() as u16,
|
||||||
|
color_map,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
gif_enc.set_repeat(gif::Repeat::Infinite).unwrap();
|
||||||
|
for qr in qrs {
|
||||||
|
let mut frame = gif::Frame::from_rgb(
|
||||||
|
qr.width() as u16,
|
||||||
|
qr.height() as u16,
|
||||||
|
qr.as_raw().as_slice(),
|
||||||
|
);
|
||||||
|
frame.delay = 10;
|
||||||
|
// Write an image to GIF encoder.
|
||||||
|
if let Ok(_) = gif_enc.write_frame(&frame) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Exit on error.
|
||||||
|
let mut w_state = qr_state.write();
|
||||||
|
w_state.gif_creating = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Setup GIF image data.
|
||||||
|
let mut w_state = qr_state.write();
|
||||||
|
if !gif.is_empty() {
|
||||||
|
w_state.gif_data = Some(gif);
|
||||||
|
}
|
||||||
|
w_state.gif_creating = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert QR code to image data.
|
||||||
|
fn qr_to_image_data(qr: QrCode, size: usize) -> Option<Vec<u8>> {
|
||||||
|
if size >= 2usize.pow((size_of::<usize>() * 4) as u32) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let margin_size = 1;
|
||||||
|
let s = qr.size();
|
||||||
|
let data_length = s as usize;
|
||||||
|
let data_length_with_margin = data_length + 2 * margin_size;
|
||||||
|
let point_size = size / data_length_with_margin;
|
||||||
|
if point_size == 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let margin = (size - (point_size * data_length)) / 2;
|
||||||
|
let length = size * size;
|
||||||
|
let mut img_raw: Vec<u8> = vec![255u8; length];
|
||||||
|
for i in 0..s {
|
||||||
|
for j in 0..s {
|
||||||
|
if qr.get_module(i, j) {
|
||||||
|
let x = i as usize * point_size + margin;
|
||||||
|
let y = j as usize * point_size + margin;
|
||||||
|
|
||||||
|
for j in y..(y + point_size) {
|
||||||
|
let offset = j * size;
|
||||||
|
for i in x..(x + point_size) {
|
||||||
|
img_raw[offset + i] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(img_raw)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+122
-100
@@ -18,113 +18,135 @@ use egui::{Id, ScrollArea};
|
|||||||
use crate::gui::Colors;
|
use crate::gui::Colors;
|
||||||
use crate::gui::icons::COPY;
|
use crate::gui::icons::COPY;
|
||||||
use crate::gui::platform::PlatformCallbacks;
|
use crate::gui::platform::PlatformCallbacks;
|
||||||
use crate::gui::views::{CameraContent, Modal, View};
|
|
||||||
use crate::gui::views::types::QrScanResult;
|
use crate::gui::views::types::QrScanResult;
|
||||||
|
use crate::gui::views::{CameraContent, Modal, View};
|
||||||
|
|
||||||
/// QR code scan [`Modal`] content.
|
/// QR code scanning content.
|
||||||
pub struct CameraScanModal {
|
pub struct CameraScanContent {
|
||||||
/// Camera content for QR scan [`Modal`].
|
/// Camera content.
|
||||||
camera_content: Option<CameraContent>,
|
camera_content: Option<CameraContent>,
|
||||||
/// QR code scan result
|
/// Scan result.
|
||||||
qr_scan_result: Option<QrScanResult>,
|
qr_scan_result: Option<QrScanResult>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for CameraScanModal {
|
impl Default for CameraScanContent {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
camera_content: Some(CameraContent::default()),
|
camera_content: Some(CameraContent::default()),
|
||||||
qr_scan_result: None,
|
qr_scan_result: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CameraScanModal {
|
impl CameraScanContent {
|
||||||
/// Draw [`Modal`] content.
|
/// Draw [`Modal`] content.
|
||||||
pub fn ui(&mut self,
|
pub fn modal_ui(
|
||||||
ui: &mut egui::Ui,
|
&mut self,
|
||||||
modal: &Modal,
|
ui: &mut egui::Ui,
|
||||||
cb: &dyn PlatformCallbacks,
|
cb: &dyn PlatformCallbacks,
|
||||||
mut on_result: impl FnMut(&QrScanResult)) {
|
mut on_result: impl FnMut(&QrScanResult),
|
||||||
// Show scan result if exists or show camera content while scanning.
|
) {
|
||||||
if let Some(result) = &self.qr_scan_result {
|
// Show scan result if exists or show camera content while scanning.
|
||||||
let mut result_text = result.text();
|
if let Some(result) = &self.qr_scan_result.clone() {
|
||||||
View::horizontal_line(ui, Colors::item_stroke());
|
Self::result_ui(
|
||||||
ui.add_space(3.0);
|
ui,
|
||||||
ScrollArea::vertical()
|
result,
|
||||||
.id_salt(Id::from("qr_scan_result_input"))
|
cb,
|
||||||
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
|
|| {
|
||||||
.max_height(128.0)
|
Modal::close();
|
||||||
.auto_shrink([false; 2])
|
},
|
||||||
.show(ui, |ui| {
|
|| {
|
||||||
ui.add_space(7.0);
|
self.qr_scan_result = None;
|
||||||
egui::TextEdit::multiline(&mut result_text)
|
cb.start_camera();
|
||||||
.font(egui::TextStyle::Small)
|
Modal::set_title(t!("scan_qr"));
|
||||||
.desired_rows(5)
|
},
|
||||||
.interactive(false)
|
);
|
||||||
.desired_width(f32::INFINITY)
|
} else if let Some(camera_content) = self.camera_content.as_mut() {
|
||||||
.show(ui);
|
if let Some(result) = camera_content.qr_scan_result() {
|
||||||
ui.add_space(6.0);
|
cb.stop_camera();
|
||||||
});
|
self.camera_content = None;
|
||||||
ui.add_space(2.0);
|
on_result(&result);
|
||||||
View::horizontal_line(ui, Colors::item_stroke());
|
|
||||||
ui.add_space(10.0);
|
|
||||||
|
|
||||||
// Show copy button.
|
// Set result and rename modal title.
|
||||||
ui.vertical_centered(|ui| {
|
self.qr_scan_result = Some(result);
|
||||||
let copy_text = format!("{} {}", COPY, t!("copy"));
|
Modal::set_title(t!("scan_result"));
|
||||||
View::button(ui, copy_text, Colors::white_or_black(false), || {
|
} else {
|
||||||
cb.copy_string_to_buffer(result_text.to_string());
|
// Draw camera content.
|
||||||
self.qr_scan_result = None;
|
ui.add_space(6.0);
|
||||||
modal.close();
|
self.camera_content.as_mut().unwrap().ui(ui, cb);
|
||||||
});
|
ui.add_space(12.0);
|
||||||
});
|
ui.vertical_centered_justified(|ui| {
|
||||||
ui.add_space(10.0);
|
View::button(
|
||||||
View::horizontal_line(ui, Colors::item_stroke());
|
ui,
|
||||||
ui.add_space(6.0);
|
t!("modal.cancel"),
|
||||||
|
Colors::white_or_black(false),
|
||||||
|
|| {
|
||||||
|
cb.stop_camera();
|
||||||
|
self.camera_content = None;
|
||||||
|
Modal::close();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ui.add_space(6.0);
|
||||||
|
}
|
||||||
|
|
||||||
// Setup spacing between buttons.
|
/// Draw scan result content.
|
||||||
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
|
pub fn result_ui(
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
result: &QrScanResult,
|
||||||
|
cb: &dyn PlatformCallbacks,
|
||||||
|
on_close: impl FnOnce(),
|
||||||
|
on_repeat: impl FnOnce(),
|
||||||
|
) {
|
||||||
|
let mut result_text = result.text();
|
||||||
|
View::horizontal_line(ui, Colors::item_stroke());
|
||||||
|
ui.add_space(3.0);
|
||||||
|
ScrollArea::vertical()
|
||||||
|
.id_salt(Id::from("qr_scan_result_input"))
|
||||||
|
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
|
||||||
|
.max_height(128.0)
|
||||||
|
.auto_shrink([false; 2])
|
||||||
|
.show(ui, |ui| {
|
||||||
|
ui.add_space(7.0);
|
||||||
|
egui::TextEdit::multiline(&mut result_text)
|
||||||
|
.font(egui::TextStyle::Small)
|
||||||
|
.desired_rows(5)
|
||||||
|
.interactive(false)
|
||||||
|
.desired_width(f32::INFINITY)
|
||||||
|
.show(ui);
|
||||||
|
ui.add_space(6.0);
|
||||||
|
});
|
||||||
|
ui.add_space(2.0);
|
||||||
|
View::horizontal_line(ui, Colors::item_stroke());
|
||||||
|
ui.add_space(10.0);
|
||||||
|
|
||||||
ui.columns(2, |columns| {
|
// Show copy button.
|
||||||
columns[0].vertical_centered_justified(|ui| {
|
ui.vertical_centered(|ui| {
|
||||||
View::button(ui, t!("close"), Colors::white_or_black(false), || {
|
let copy_text = format!("{} {}", COPY, t!("copy"));
|
||||||
self.qr_scan_result = None;
|
View::button(ui, copy_text, Colors::white_or_black(false), || {
|
||||||
self.camera_content = None;
|
cb.copy_string_to_buffer(result_text.to_string());
|
||||||
modal.close();
|
});
|
||||||
});
|
});
|
||||||
});
|
ui.add_space(10.0);
|
||||||
columns[1].vertical_centered_justified(|ui| {
|
View::horizontal_line(ui, Colors::item_stroke());
|
||||||
View::button(ui, t!("repeat"), Colors::white_or_black(false), || {
|
ui.add_space(6.0);
|
||||||
Modal::set_title(t!("scan_qr"));
|
|
||||||
self.qr_scan_result = None;
|
|
||||||
self.camera_content = Some(CameraContent::default());
|
|
||||||
cb.start_camera();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} else if let Some(camera_content) = self.camera_content.as_mut() {
|
|
||||||
if let Some(result) = camera_content.qr_scan_result() {
|
|
||||||
cb.stop_camera();
|
|
||||||
self.camera_content = None;
|
|
||||||
on_result(&result);
|
|
||||||
|
|
||||||
// Set result and rename modal title.
|
// Setup spacing between buttons.
|
||||||
self.qr_scan_result = Some(result);
|
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
|
||||||
Modal::set_title(t!("scan_result"));
|
|
||||||
} else {
|
ui.columns(2, |columns| {
|
||||||
// Draw camera content.
|
columns[0].vertical_centered_justified(|ui| {
|
||||||
ui.add_space(6.0);
|
View::button(ui, t!("close"), Colors::white_or_black(false), || {
|
||||||
self.camera_content.as_mut().unwrap().ui(ui, cb);
|
on_close();
|
||||||
ui.add_space(12.0);
|
});
|
||||||
ui.vertical_centered_justified(|ui| {
|
});
|
||||||
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
|
columns[1].vertical_centered_justified(|ui| {
|
||||||
cb.stop_camera();
|
View::button(ui, t!("repeat"), Colors::white_or_black(false), || {
|
||||||
self.camera_content = None;
|
on_repeat();
|
||||||
modal.close();
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ui.add_space(6.0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
// Copyright 2025 The Grim Developers
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
use crate::AppConfig;
|
||||||
|
use crate::gui::Colors;
|
||||||
|
use crate::gui::icons::GLOBE_SIMPLE;
|
||||||
|
use crate::gui::platform::PlatformCallbacks;
|
||||||
|
use crate::gui::views::View;
|
||||||
|
use crate::gui::views::settings::{InterfaceSettingsContent, NetworkSettingsContent};
|
||||||
|
use crate::gui::views::types::ContentContainer;
|
||||||
|
|
||||||
|
/// Application settings content.
|
||||||
|
pub struct SettingsContent {
|
||||||
|
/// User interface settings.
|
||||||
|
interface_settings: InterfaceSettingsContent,
|
||||||
|
/// Network communication settings.
|
||||||
|
network_settings: NetworkSettingsContent,
|
||||||
|
// tor_settings: TorSettingsContent,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for SettingsContent {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
interface_settings: InterfaceSettingsContent::default(),
|
||||||
|
network_settings: NetworkSettingsContent::default(),
|
||||||
|
//tor_settings: TorSettingsContent::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SettingsContent {
|
||||||
|
/// Draw application settings content.
|
||||||
|
pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||||
|
ui.add_space(5.0);
|
||||||
|
View::checkbox(ui, AppConfig::check_updates(), t!("check_updates"), || {
|
||||||
|
AppConfig::toggle_check_updates();
|
||||||
|
});
|
||||||
|
ui.add_space(6.0);
|
||||||
|
View::horizontal_line(ui, Colors::stroke());
|
||||||
|
|
||||||
|
// Show interface settings.
|
||||||
|
self.interface_settings.ui(ui, cb);
|
||||||
|
|
||||||
|
ui.add_space(8.0);
|
||||||
|
View::horizontal_line(ui, Colors::stroke());
|
||||||
|
ui.add_space(6.0);
|
||||||
|
|
||||||
|
View::sub_title(ui, format!("{} {}", GLOBE_SIMPLE, t!("network.self")));
|
||||||
|
View::horizontal_line(ui, Colors::stroke());
|
||||||
|
ui.add_space(6.0);
|
||||||
|
|
||||||
|
// Show network settings.
|
||||||
|
self.network_settings.ui(ui, cb);
|
||||||
|
ui.add_space(8.0);
|
||||||
|
|
||||||
|
// Do not show Tor settings on Android.
|
||||||
|
// let os = OperatingSystem::from_target_os();
|
||||||
|
// let show_tor = os != OperatingSystem::Android;
|
||||||
|
// if show_tor {
|
||||||
|
// View::horizontal_line(ui, Colors::stroke());
|
||||||
|
// ui.add_space(6.0);
|
||||||
|
//
|
||||||
|
// View::sub_title(ui, format!("{} {}", CIRCLE_HALF, t!("transport.tor_network")));
|
||||||
|
// View::horizontal_line(ui, Colors::stroke());
|
||||||
|
// ui.add_space(6.0);
|
||||||
|
//
|
||||||
|
// // Show Tor settings.
|
||||||
|
// self.tor_settings.ui(ui, cb);
|
||||||
|
// ui.add_space(8.0);
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
// Copyright 2025 The Grim Developers
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
use eframe::epaint::RectShape;
|
||||||
|
use egui::{Align, CursorIcon, Layout, RichText, Sense, StrokeKind, UiBuilder};
|
||||||
|
|
||||||
|
use crate::AppConfig;
|
||||||
|
use crate::gui::Colors;
|
||||||
|
use crate::gui::platform::PlatformCallbacks;
|
||||||
|
use crate::gui::views::types::ContentContainer;
|
||||||
|
use crate::gui::views::{Modal, View};
|
||||||
|
|
||||||
|
/// User interface settings content.
|
||||||
|
pub struct InterfaceSettingsContent {
|
||||||
|
/// Current locale.
|
||||||
|
locale: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ContentContainer for InterfaceSettingsContent {
|
||||||
|
fn modal_ids(&self) -> Vec<&'static str> {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn modal_ui(&mut self, _: &mut egui::Ui, _: &Modal, _: &dyn PlatformCallbacks) {}
|
||||||
|
|
||||||
|
fn container_ui(&mut self, ui: &mut egui::Ui, _: &dyn PlatformCallbacks) {
|
||||||
|
ui.add_space(5.0);
|
||||||
|
|
||||||
|
// Draw theme selection.
|
||||||
|
ui.vertical_centered(|ui| {
|
||||||
|
ui.label(RichText::new(t!("theme")).size(16.0).color(Colors::gray()));
|
||||||
|
});
|
||||||
|
|
||||||
|
let saved_use_dark = AppConfig::dark_theme().unwrap_or(false);
|
||||||
|
let mut selected_use_dark = saved_use_dark;
|
||||||
|
|
||||||
|
ui.add_space(8.0);
|
||||||
|
ui.columns(2, |columns| {
|
||||||
|
columns[0].vertical_centered(|ui| {
|
||||||
|
View::radio_value(ui, &mut selected_use_dark, false, t!("light"));
|
||||||
|
});
|
||||||
|
columns[1].vertical_centered(|ui| {
|
||||||
|
View::radio_value(ui, &mut selected_use_dark, true, t!("dark"));
|
||||||
|
})
|
||||||
|
});
|
||||||
|
ui.add_space(14.0);
|
||||||
|
if saved_use_dark != selected_use_dark {
|
||||||
|
AppConfig::set_dark_theme(selected_use_dark);
|
||||||
|
crate::setup_visuals(ui.ctx());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw language selection.
|
||||||
|
let locales = rust_i18n::available_locales!();
|
||||||
|
for (index, locale) in locales.iter().enumerate() {
|
||||||
|
self.language_item_ui(locale, ui, index, locales.len());
|
||||||
|
}
|
||||||
|
ui.add_space(4.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for InterfaceSettingsContent {
|
||||||
|
fn default() -> Self {
|
||||||
|
let locale = if let Some(lang) = AppConfig::locale() {
|
||||||
|
lang
|
||||||
|
} else {
|
||||||
|
rust_i18n::locale().to_string()
|
||||||
|
};
|
||||||
|
Self { locale }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InterfaceSettingsContent {
|
||||||
|
/// Draw language selection item content.
|
||||||
|
fn language_item_ui(&mut self, locale: &str, ui: &mut egui::Ui, index: usize, len: usize) {
|
||||||
|
let is_current = self.locale == locale;
|
||||||
|
// Draw round background.
|
||||||
|
let mut rect = ui.available_rect_before_wrap();
|
||||||
|
rect.set_height(50.0);
|
||||||
|
let r = View::item_rounding(index, len, false);
|
||||||
|
let bg = if is_current {
|
||||||
|
Colors::fill()
|
||||||
|
} else {
|
||||||
|
Colors::fill_lite()
|
||||||
|
};
|
||||||
|
let mut bg_shape = RectShape::new(rect, r, bg, View::item_stroke(), StrokeKind::Outside);
|
||||||
|
let bg_idx = ui.painter().add(bg_shape.clone());
|
||||||
|
|
||||||
|
let res = ui
|
||||||
|
.scope_builder(
|
||||||
|
UiBuilder::new()
|
||||||
|
.sense(Sense::click())
|
||||||
|
.layout(Layout::right_to_left(Align::Center))
|
||||||
|
.max_rect(rect),
|
||||||
|
|ui| {
|
||||||
|
if is_current {
|
||||||
|
View::selected_item_check(ui);
|
||||||
|
}
|
||||||
|
let layout_size = ui.available_size();
|
||||||
|
ui.allocate_ui_with_layout(
|
||||||
|
layout_size,
|
||||||
|
Layout::left_to_right(Align::Center),
|
||||||
|
|ui| {
|
||||||
|
ui.add_space(12.0);
|
||||||
|
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(14.0);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.response;
|
||||||
|
let clicked = res.clicked() || res.long_touched();
|
||||||
|
// Setup background and cursor.
|
||||||
|
if res.hovered() && !is_current {
|
||||||
|
res.on_hover_cursor(CursorIcon::PointingHand);
|
||||||
|
bg_shape.fill = Colors::fill();
|
||||||
|
}
|
||||||
|
ui.painter().set(bg_idx, bg_shape);
|
||||||
|
// Handle clicks on layout.
|
||||||
|
if clicked && !is_current {
|
||||||
|
rust_i18n::set_locale(locale);
|
||||||
|
AppConfig::save_locale(locale);
|
||||||
|
self.locale = locale.to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
// Copyright 2025 The Grim Developers
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
mod content;
|
||||||
|
pub use content::*;
|
||||||
|
|
||||||
|
mod interface;
|
||||||
|
pub use interface::*;
|
||||||
|
|
||||||
|
mod network;
|
||||||
|
pub use network::*;
|
||||||
|
|
||||||
|
mod tor;
|
||||||
|
pub use tor::*;
|
||||||
@@ -0,0 +1,311 @@
|
|||||||
|
// Copyright 2025 The Grim Developers
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
use eframe::epaint::RectShape;
|
||||||
|
use egui::{Align, CursorIcon, Id, Layout, RichText, Sense, StrokeKind, UiBuilder};
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
use crate::AppConfig;
|
||||||
|
use crate::gui::Colors;
|
||||||
|
use crate::gui::icons::{CLOUD_CHECK, CLOUD_SLASH};
|
||||||
|
use crate::gui::platform::PlatformCallbacks;
|
||||||
|
use crate::gui::views::types::{ContentContainer, ModalPosition};
|
||||||
|
use crate::gui::views::{Modal, TextEdit, View};
|
||||||
|
|
||||||
|
/// Network communication settings content.
|
||||||
|
pub struct NetworkSettingsContent {
|
||||||
|
/// Proxy URL input value for [`Modal`].
|
||||||
|
proxy_url_edit: String,
|
||||||
|
/// Flag to check if entered proxy address was correct.
|
||||||
|
proxy_url_error: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Identifier for proxy URL edit [`Modal`].
|
||||||
|
const PROXY_URL_EDIT_MODAL: &'static str = "settings_proxy_edit_modal";
|
||||||
|
|
||||||
|
impl ContentContainer for NetworkSettingsContent {
|
||||||
|
fn modal_ids(&self) -> Vec<&'static str> {
|
||||||
|
vec![PROXY_URL_EDIT_MODAL]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
|
||||||
|
match modal.id {
|
||||||
|
PROXY_URL_EDIT_MODAL => self.proxy_modal_ui(ui, cb),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn container_ui(&mut self, ui: &mut egui::Ui, _: &dyn PlatformCallbacks) {
|
||||||
|
let use_proxy = AppConfig::use_proxy();
|
||||||
|
View::checkbox(ui, use_proxy, t!("app_settings.proxy"), || {
|
||||||
|
// Show edit modal when both URLs are empty.
|
||||||
|
if AppConfig::http_proxy_url().is_none()
|
||||||
|
&& AppConfig::socks_proxy_url().is_none()
|
||||||
|
&& !use_proxy
|
||||||
|
{
|
||||||
|
Modal::new(PROXY_URL_EDIT_MODAL)
|
||||||
|
.position(ModalPosition::CenterTop)
|
||||||
|
.title(t!("app_settings.proxy"))
|
||||||
|
.show();
|
||||||
|
} else {
|
||||||
|
AppConfig::toggle_use_proxy();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if !use_proxy {
|
||||||
|
ui.add_space(4.0);
|
||||||
|
ui.label(
|
||||||
|
RichText::new(t!("app_settings.proxy_desc"))
|
||||||
|
.size(16.0)
|
||||||
|
.color(Colors::inactive_text()),
|
||||||
|
);
|
||||||
|
ui.add_space(8.0);
|
||||||
|
} else {
|
||||||
|
ui.add_space(8.0);
|
||||||
|
|
||||||
|
// Draw proxy type selection.
|
||||||
|
Self::proxy_type_ui(ui);
|
||||||
|
|
||||||
|
// Draw proxy URL info.
|
||||||
|
self.proxy_item_ui(ui);
|
||||||
|
ui.add_space(6.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for NetworkSettingsContent {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
proxy_url_edit: "".to_string(),
|
||||||
|
proxy_url_error: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NetworkSettingsContent {
|
||||||
|
/// Draw proxy edit modal content.
|
||||||
|
fn proxy_modal_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||||
|
let on_save = |c: &mut NetworkSettingsContent| {
|
||||||
|
let proxy = c.proxy_url_edit.trim().to_string();
|
||||||
|
let use_socks = AppConfig::use_socks_proxy();
|
||||||
|
// Clear value if empty.
|
||||||
|
if proxy.is_empty() {
|
||||||
|
if use_socks {
|
||||||
|
AppConfig::save_socks_proxy_url(None);
|
||||||
|
} else {
|
||||||
|
AppConfig::save_http_proxy_url(None);
|
||||||
|
}
|
||||||
|
Modal::close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Format URL.
|
||||||
|
let http = "http://";
|
||||||
|
let socks = "socks5://";
|
||||||
|
let url = if use_socks {
|
||||||
|
let p = proxy.replace(http, "");
|
||||||
|
if !p.contains(socks) {
|
||||||
|
format!("{}{}", socks, p)
|
||||||
|
} else {
|
||||||
|
p
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let p = proxy.replace(socks, "");
|
||||||
|
if !p.contains(http) {
|
||||||
|
format!("{}{}", http, p)
|
||||||
|
} else {
|
||||||
|
p
|
||||||
|
}
|
||||||
|
};
|
||||||
|
c.proxy_url_error = Url::parse(url.as_str()).is_err();
|
||||||
|
if !c.proxy_url_error {
|
||||||
|
// Save result when no error.
|
||||||
|
if !AppConfig::use_proxy() {
|
||||||
|
AppConfig::toggle_use_proxy();
|
||||||
|
}
|
||||||
|
if use_socks {
|
||||||
|
AppConfig::save_socks_proxy_url(Some(url))
|
||||||
|
} else {
|
||||||
|
AppConfig::save_http_proxy_url(Some(url));
|
||||||
|
}
|
||||||
|
Modal::close();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ui.add_space(6.0);
|
||||||
|
ui.vertical_centered(|ui| {
|
||||||
|
let label = format!("{}:", t!("enter_url"));
|
||||||
|
ui.label(RichText::new(label).size(17.0).color(Colors::gray()));
|
||||||
|
ui.add_space(8.0);
|
||||||
|
|
||||||
|
// Draw proxy URL text edit.
|
||||||
|
let mut edit =
|
||||||
|
TextEdit::new(Id::from("proxy_url_edit").with(PROXY_URL_EDIT_MODAL).with(
|
||||||
|
if AppConfig::use_proxy() {
|
||||||
|
"socks5"
|
||||||
|
} else {
|
||||||
|
"http"
|
||||||
|
},
|
||||||
|
))
|
||||||
|
.paste();
|
||||||
|
edit.ui(ui, &mut self.proxy_url_edit, cb);
|
||||||
|
if edit.enter_pressed {
|
||||||
|
on_save(self);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show error when specified address is incorrect.
|
||||||
|
if self.proxy_url_error {
|
||||||
|
ui.add_space(10.0);
|
||||||
|
ui.label(
|
||||||
|
RichText::new(t!("wallets.invalid_url"))
|
||||||
|
.size(16.0)
|
||||||
|
.color(Colors::red()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
ui.add_space(12.0);
|
||||||
|
|
||||||
|
// Show type selection when both URLs are empty.
|
||||||
|
if AppConfig::socks_proxy_url().is_none() && AppConfig::http_proxy_url().is_none() {
|
||||||
|
ui.add_space(6.0);
|
||||||
|
ui.vertical_centered(|ui| {
|
||||||
|
Self::proxy_type_ui(ui);
|
||||||
|
});
|
||||||
|
ui.add_space(4.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show modal buttons.
|
||||||
|
ui.scope(|ui| {
|
||||||
|
// Setup spacing between buttons.
|
||||||
|
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
|
||||||
|
|
||||||
|
ui.columns(2, |columns| {
|
||||||
|
columns[0].vertical_centered_justified(|ui| {
|
||||||
|
View::button(
|
||||||
|
ui,
|
||||||
|
t!("modal.cancel"),
|
||||||
|
Colors::white_or_black(false),
|
||||||
|
|| {
|
||||||
|
Modal::close();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
columns[1].vertical_centered_justified(|ui| {
|
||||||
|
View::button(ui, t!("modal.save"), Colors::white_or_black(false), || {
|
||||||
|
on_save(self);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
ui.add_space(6.0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw proxy item content.
|
||||||
|
fn proxy_item_ui(&mut self, ui: &mut egui::Ui) {
|
||||||
|
// Draw round background.
|
||||||
|
let mut rect = ui.available_rect_before_wrap();
|
||||||
|
rect.set_height(56.0);
|
||||||
|
let r = View::item_rounding(0, 1, false);
|
||||||
|
let bg = Colors::fill_lite();
|
||||||
|
let mut bg_shape = RectShape::new(rect, r, bg, View::item_stroke(), StrokeKind::Outside);
|
||||||
|
let bg_idx = ui.painter().add(bg_shape.clone());
|
||||||
|
|
||||||
|
let res = ui
|
||||||
|
.scope_builder(
|
||||||
|
UiBuilder::new()
|
||||||
|
.sense(Sense::click())
|
||||||
|
.layout(Layout::right_to_left(Align::Center))
|
||||||
|
.max_rect(rect),
|
||||||
|
|ui| {
|
||||||
|
let layout_size = ui.available_size();
|
||||||
|
ui.allocate_ui_with_layout(
|
||||||
|
layout_size,
|
||||||
|
Layout::left_to_right(Align::Center),
|
||||||
|
|ui| {
|
||||||
|
ui.add_space(12.0);
|
||||||
|
ui.vertical(|ui| {
|
||||||
|
ui.add_space(4.0);
|
||||||
|
let use_socks = AppConfig::use_socks_proxy();
|
||||||
|
let proxy_url = if use_socks {
|
||||||
|
AppConfig::socks_proxy_url()
|
||||||
|
} else {
|
||||||
|
AppConfig::http_proxy_url()
|
||||||
|
};
|
||||||
|
let (url, color, icon, text) = if let Some(url) = proxy_url {
|
||||||
|
(
|
||||||
|
url,
|
||||||
|
Colors::title(false),
|
||||||
|
CLOUD_CHECK,
|
||||||
|
t!("network_settings.enabled"),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
(
|
||||||
|
t!("enter_url").into(),
|
||||||
|
Colors::inactive_text(),
|
||||||
|
CLOUD_SLASH,
|
||||||
|
t!("network_settings.disabled"),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
View::ellipsize_text(ui, url, 18.0, color);
|
||||||
|
ui.add_space(1.0);
|
||||||
|
|
||||||
|
let value = format!("{} {}", icon, text);
|
||||||
|
ui.label(RichText::new(value).size(15.0).color(Colors::gray()));
|
||||||
|
ui.add_space(3.0);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.response;
|
||||||
|
let clicked = res.clicked() || res.long_touched();
|
||||||
|
// Setup background and cursor.
|
||||||
|
if res.hovered() {
|
||||||
|
res.on_hover_cursor(CursorIcon::PointingHand);
|
||||||
|
bg_shape.fill = Colors::fill();
|
||||||
|
}
|
||||||
|
ui.painter().set(bg_idx, bg_shape);
|
||||||
|
// Handle clicks on layout.
|
||||||
|
if clicked {
|
||||||
|
let url = if AppConfig::use_socks_proxy() {
|
||||||
|
AppConfig::socks_proxy_url().unwrap_or("".to_string())
|
||||||
|
} else {
|
||||||
|
AppConfig::http_proxy_url().unwrap_or("".to_string())
|
||||||
|
};
|
||||||
|
self.proxy_url_edit = url;
|
||||||
|
// Show proxy URL edit modal.
|
||||||
|
Modal::new(PROXY_URL_EDIT_MODAL)
|
||||||
|
.position(ModalPosition::CenterTop)
|
||||||
|
.title(t!("app_settings.proxy"))
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw proxy type selection.
|
||||||
|
fn proxy_type_ui(ui: &mut egui::Ui) {
|
||||||
|
// Draw proxy type selection.
|
||||||
|
let saved_use_socks = AppConfig::use_socks_proxy();
|
||||||
|
let mut selected_use_socks = saved_use_socks;
|
||||||
|
ui.columns(2, |columns| {
|
||||||
|
columns[0].vertical_centered(|ui| {
|
||||||
|
View::radio_value(ui, &mut selected_use_socks, true, "SOCKS5".to_string());
|
||||||
|
});
|
||||||
|
columns[1].vertical_centered(|ui| {
|
||||||
|
View::radio_value(ui, &mut selected_use_socks, false, "HTTP".to_string());
|
||||||
|
})
|
||||||
|
});
|
||||||
|
ui.add_space(14.0);
|
||||||
|
if saved_use_socks != selected_use_socks {
|
||||||
|
AppConfig::toggle_use_socks_proxy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,678 @@
|
|||||||
|
// Copyright 2025 The Grim Developers
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
use eframe::epaint::RectShape;
|
||||||
|
use egui::scroll_area::ScrollBarVisibility;
|
||||||
|
use egui::{Align, CursorIcon, Id, Layout, RichText, ScrollArea, Sense, StrokeKind, UiBuilder};
|
||||||
|
use std::fs;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
use crate::gui::Colors;
|
||||||
|
use crate::gui::icons::{CLIPBOARD_TEXT, CLOUD_CHECK, NOTCHES, PENCIL, SCAN, TERMINAL};
|
||||||
|
use crate::gui::platform::PlatformCallbacks;
|
||||||
|
use crate::gui::views::types::{ContentContainer, ModalPosition};
|
||||||
|
use crate::gui::views::{
|
||||||
|
CameraScanContent, FilePickContent, FilePickContentType, Modal, TextEdit, View,
|
||||||
|
};
|
||||||
|
use crate::tor::{TorBridge, TorConfig, TorProxy};
|
||||||
|
|
||||||
|
/// Transport settings content.
|
||||||
|
pub struct TorSettingsContent {
|
||||||
|
/// Flag to check if settings were changed.
|
||||||
|
pub settings_changed: bool,
|
||||||
|
|
||||||
|
/// Proxy URL input value for [`Modal`].
|
||||||
|
proxy_url_edit: String,
|
||||||
|
/// Flag to check if entered proxy address was correct.
|
||||||
|
proxy_url_error: bool,
|
||||||
|
|
||||||
|
/// Tor bridge binary path value for [`Modal`].
|
||||||
|
bridge_bin_path_edit: String,
|
||||||
|
/// Button to pick binary file for bridge.
|
||||||
|
bridge_bin_pick_file: FilePickContent,
|
||||||
|
|
||||||
|
/// Tor bridge connection line value for [`Modal`].
|
||||||
|
bridge_conn_line_edit: String,
|
||||||
|
/// Bridge line QR code scanner [`Modal`] content.
|
||||||
|
bridge_qr_scan_content: Option<CameraScanContent>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Identifier for proxy URL edit [`Modal`].
|
||||||
|
const PROXY_URL_EDIT_MODAL: &'static str = "tor_proxy_edit_modal";
|
||||||
|
/// Identifier for bridge binary path input [`Modal`].
|
||||||
|
const BRIDGE_BIN_EDIT_MODAL: &'static str = "bridge_bin_edit_modal";
|
||||||
|
/// Identifier for bridge connection line input [`Modal`].
|
||||||
|
const BRIDGE_CONN_LINE_EDIT_MODAL: &'static str = "bridge_conn_line_edit_modal";
|
||||||
|
/// Identifier for [`Modal`] to scan bridge line from QR code.
|
||||||
|
const SCAN_BRIDGE_CONN_LINE_MODAL: &'static str = "scan_bridge_conn_line_modal";
|
||||||
|
|
||||||
|
impl Default for TorSettingsContent {
|
||||||
|
fn default() -> Self {
|
||||||
|
// Setup Tor bridge binary path edit text.
|
||||||
|
let bridge = TorConfig::get_bridge();
|
||||||
|
let (bin_path, conn_line) = if let Some(b) = bridge {
|
||||||
|
(b.binary_path(), b.connection_line())
|
||||||
|
} else {
|
||||||
|
("".to_string(), "".to_string())
|
||||||
|
};
|
||||||
|
Self {
|
||||||
|
settings_changed: false,
|
||||||
|
proxy_url_edit: "".to_string(),
|
||||||
|
proxy_url_error: false,
|
||||||
|
bridge_bin_path_edit: bin_path,
|
||||||
|
bridge_bin_pick_file: FilePickContent::new(FilePickContentType::ItemButton(
|
||||||
|
View::item_rounding(0, 1, true),
|
||||||
|
))
|
||||||
|
.no_parse(),
|
||||||
|
bridge_conn_line_edit: conn_line,
|
||||||
|
bridge_qr_scan_content: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ContentContainer for TorSettingsContent {
|
||||||
|
fn modal_ids(&self) -> Vec<&'static str> {
|
||||||
|
vec![
|
||||||
|
PROXY_URL_EDIT_MODAL,
|
||||||
|
BRIDGE_BIN_EDIT_MODAL,
|
||||||
|
BRIDGE_CONN_LINE_EDIT_MODAL,
|
||||||
|
SCAN_BRIDGE_CONN_LINE_MODAL,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
|
||||||
|
match modal.id {
|
||||||
|
PROXY_URL_EDIT_MODAL => self.proxy_modal_ui(ui, cb),
|
||||||
|
BRIDGE_BIN_EDIT_MODAL => self.bridge_bin_edit_modal_ui(ui, cb),
|
||||||
|
BRIDGE_CONN_LINE_EDIT_MODAL => self.bridge_conn_line_edit_modal_ui(ui, cb),
|
||||||
|
SCAN_BRIDGE_CONN_LINE_MODAL => {
|
||||||
|
if let Some(content) = self.bridge_qr_scan_content.as_mut() {
|
||||||
|
let mut close = false;
|
||||||
|
content.modal_ui(ui, cb, |res| {
|
||||||
|
// Save connection line after scanning.
|
||||||
|
let line = res.text();
|
||||||
|
let bridge = TorConfig::get_bridge().unwrap();
|
||||||
|
if bridge.connection_line() != line {
|
||||||
|
TorBridge::save_bridge_conn_line(&bridge, line);
|
||||||
|
self.settings_changed = true;
|
||||||
|
}
|
||||||
|
close = true;
|
||||||
|
});
|
||||||
|
if close {
|
||||||
|
self.bridge_qr_scan_content = None;
|
||||||
|
cb.stop_camera();
|
||||||
|
Modal::close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn container_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||||
|
ui.label(
|
||||||
|
RichText::new(format!("{}:", t!("wallets.conn_method")))
|
||||||
|
.size(17.0)
|
||||||
|
.color(Colors::inactive_text()),
|
||||||
|
);
|
||||||
|
ui.add_space(10.0);
|
||||||
|
|
||||||
|
let mut proxy = TorConfig::get_proxy();
|
||||||
|
let current_proxy = proxy.clone();
|
||||||
|
ui.columns(2, |columns| {
|
||||||
|
columns[0].vertical_centered(|ui| {
|
||||||
|
let name = t!("network_settings.default");
|
||||||
|
View::radio_value(ui, &mut proxy, None, name);
|
||||||
|
});
|
||||||
|
columns[1].vertical_centered(|ui| {
|
||||||
|
let name = t!("app_settings.proxy");
|
||||||
|
let val = current_proxy
|
||||||
|
.clone()
|
||||||
|
.unwrap_or(TorProxy::SOCKS5(TorProxy::DEFAULT_SOCKS5_URL.to_string()));
|
||||||
|
View::radio_value(ui, &mut proxy, Some(val), name);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
ui.add_space(14.0);
|
||||||
|
View::horizontal_line(ui, Colors::item_stroke());
|
||||||
|
ui.add_space(6.0);
|
||||||
|
|
||||||
|
if let Some(p) = proxy.as_mut() {
|
||||||
|
ui.label(
|
||||||
|
RichText::new(format!("{}:", t!("app_settings.proxy")))
|
||||||
|
.size(17.0)
|
||||||
|
.color(Colors::inactive_text()),
|
||||||
|
);
|
||||||
|
ui.add_space(10.0);
|
||||||
|
ui.columns(2, |columns| {
|
||||||
|
columns[0].vertical_centered(|ui| {
|
||||||
|
let value = TorConfig::get_socks5_proxy();
|
||||||
|
View::radio_value(ui, p, value, "SOCKS5".to_string());
|
||||||
|
});
|
||||||
|
columns[1].vertical_centered(|ui| {
|
||||||
|
let value = TorConfig::get_http_proxy();
|
||||||
|
View::radio_value(ui, p, value, "HTTP".to_string());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
ui.add_space(14.0);
|
||||||
|
// Show proxy settings.
|
||||||
|
self.proxy_item_ui(p.url(), ui);
|
||||||
|
ui.add_space(8.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if proxy type was changed to save.
|
||||||
|
if current_proxy != proxy {
|
||||||
|
TorConfig::save_proxy(proxy.clone());
|
||||||
|
self.settings_changed = true;
|
||||||
|
}
|
||||||
|
if proxy.is_some() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let bridge = TorConfig::get_bridge();
|
||||||
|
ui.vertical_centered(|ui| {
|
||||||
|
ui.label(
|
||||||
|
RichText::new(t!("transport.bridges_desc"))
|
||||||
|
.size(17.0)
|
||||||
|
.color(Colors::inactive_text()),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Draw checkbox to enable/disable bridges.
|
||||||
|
View::checkbox(ui, bridge.is_some(), t!("transport.bridges"), || {
|
||||||
|
let value = if bridge.is_some() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
let default_bridge = TorConfig::get_webtunnel();
|
||||||
|
self.bridge_bin_path_edit = default_bridge.binary_path();
|
||||||
|
self.bridge_conn_line_edit = default_bridge.connection_line();
|
||||||
|
Some(default_bridge)
|
||||||
|
};
|
||||||
|
TorConfig::save_bridge(value);
|
||||||
|
self.settings_changed = true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if bridge.is_some() {
|
||||||
|
ui.add_space(6.0);
|
||||||
|
// Show bridge selection for desktop.
|
||||||
|
if View::is_desktop() {
|
||||||
|
let current_bridge = bridge.unwrap();
|
||||||
|
let mut bridge = current_bridge.clone();
|
||||||
|
|
||||||
|
ui.columns(2, |columns| {
|
||||||
|
columns[0].vertical_centered(|ui| {
|
||||||
|
// Show Webtunnel bridge selector.
|
||||||
|
let webtunnel = TorConfig::get_webtunnel();
|
||||||
|
let name = webtunnel.protocol_name().to_uppercase();
|
||||||
|
View::radio_value(ui, &mut bridge, webtunnel, name);
|
||||||
|
});
|
||||||
|
columns[1].vertical_centered(|ui| {
|
||||||
|
// Show Obfs4 bridge selector.
|
||||||
|
let obfs4 = TorConfig::get_obfs4();
|
||||||
|
let name = obfs4.protocol_name().to_uppercase();
|
||||||
|
View::radio_value(ui, &mut bridge, obfs4, name);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
ui.add_space(10.0);
|
||||||
|
ui.vertical_centered(|ui| {
|
||||||
|
// Show Snowflake bridge selector.
|
||||||
|
let snowflake = TorConfig::get_snowflake();
|
||||||
|
let name = snowflake.protocol_name().to_uppercase();
|
||||||
|
View::radio_value(ui, &mut bridge, snowflake, name);
|
||||||
|
});
|
||||||
|
ui.add_space(16.0);
|
||||||
|
|
||||||
|
// Check if bridge type was changed to save.
|
||||||
|
if current_bridge != bridge {
|
||||||
|
TorConfig::save_bridge(Some(bridge.clone()));
|
||||||
|
self.bridge_bin_path_edit = bridge.binary_path();
|
||||||
|
self.bridge_conn_line_edit = bridge.connection_line();
|
||||||
|
self.settings_changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(br) = TorConfig::get_bridge().as_ref() {
|
||||||
|
// Show bridge connection line setup.
|
||||||
|
self.conn_line_ui(ui, br, cb);
|
||||||
|
// Show bridge binary setup for desktop.
|
||||||
|
if View::is_desktop() {
|
||||||
|
self.bridge_bin_ui(ui, br, cb);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.add_space(8.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TorSettingsContent {
|
||||||
|
/// Draw proxy edit modal content.
|
||||||
|
fn proxy_modal_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||||
|
let on_save = |c: &mut TorSettingsContent| {
|
||||||
|
let http = "http://";
|
||||||
|
let socks = "socks5://";
|
||||||
|
let url = c.proxy_url_edit.trim().to_string();
|
||||||
|
c.proxy_url_error = Url::parse(url.as_str()).is_err();
|
||||||
|
if !c.proxy_url_error {
|
||||||
|
let proxy = TorConfig::get_proxy().unwrap();
|
||||||
|
if url.contains(socks) {
|
||||||
|
TorConfig::save_proxy(Some(TorProxy::SOCKS5(url)));
|
||||||
|
} else if url.contains(http) {
|
||||||
|
TorConfig::save_proxy(Some(TorProxy::HTTP(url)));
|
||||||
|
} else {
|
||||||
|
match proxy {
|
||||||
|
TorProxy::SOCKS5(_) => {
|
||||||
|
TorConfig::save_proxy(Some(TorProxy::SOCKS5(url)));
|
||||||
|
}
|
||||||
|
TorProxy::HTTP(_) => {
|
||||||
|
TorConfig::save_proxy(Some(TorProxy::HTTP(url)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Modal::close();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ui.add_space(6.0);
|
||||||
|
ui.vertical_centered(|ui| {
|
||||||
|
let label = format!("{}:", t!("enter_url"));
|
||||||
|
ui.label(RichText::new(label).size(17.0).color(Colors::gray()));
|
||||||
|
ui.add_space(8.0);
|
||||||
|
|
||||||
|
// Draw proxy URL text edit.
|
||||||
|
let mut edit =
|
||||||
|
TextEdit::new(Id::from("proxy_url_edit").with(PROXY_URL_EDIT_MODAL)).paste();
|
||||||
|
edit.ui(ui, &mut self.proxy_url_edit, cb);
|
||||||
|
if edit.enter_pressed {
|
||||||
|
on_save(self);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show error when specified address is incorrect.
|
||||||
|
if self.proxy_url_error {
|
||||||
|
ui.add_space(10.0);
|
||||||
|
ui.label(
|
||||||
|
RichText::new(t!("wallets.invalid_url"))
|
||||||
|
.size(16.0)
|
||||||
|
.color(Colors::red()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
ui.add_space(12.0);
|
||||||
|
|
||||||
|
// Show modal buttons.
|
||||||
|
ui.scope(|ui| {
|
||||||
|
// Setup spacing between buttons.
|
||||||
|
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
|
||||||
|
|
||||||
|
ui.columns(2, |columns| {
|
||||||
|
columns[0].vertical_centered_justified(|ui| {
|
||||||
|
View::button(
|
||||||
|
ui,
|
||||||
|
t!("modal.cancel"),
|
||||||
|
Colors::white_or_black(false),
|
||||||
|
|| {
|
||||||
|
Modal::close();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
columns[1].vertical_centered_justified(|ui| {
|
||||||
|
View::button(ui, t!("modal.save"), Colors::white_or_black(false), || {
|
||||||
|
on_save(self);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
ui.add_space(6.0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw proxy item content.
|
||||||
|
fn proxy_item_ui(&mut self, url: String, ui: &mut egui::Ui) {
|
||||||
|
// Setup layout size.
|
||||||
|
let mut rect = ui.available_rect_before_wrap();
|
||||||
|
rect.set_height(56.0);
|
||||||
|
|
||||||
|
// Draw round background.
|
||||||
|
let bg_rect = rect.clone();
|
||||||
|
let item_rounding = View::item_rounding(0, 1, false);
|
||||||
|
ui.painter().rect(
|
||||||
|
bg_rect,
|
||||||
|
item_rounding,
|
||||||
|
Colors::fill(),
|
||||||
|
View::item_stroke(),
|
||||||
|
StrokeKind::Outside,
|
||||||
|
);
|
||||||
|
|
||||||
|
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
|
||||||
|
View::item_button(ui, View::item_rounding(0, 1, true), PENCIL, None, || {
|
||||||
|
self.proxy_url_edit = url.clone();
|
||||||
|
// Show proxy URL edit modal.
|
||||||
|
Modal::new(PROXY_URL_EDIT_MODAL)
|
||||||
|
.position(ModalPosition::CenterTop)
|
||||||
|
.title(t!("app_settings.proxy"))
|
||||||
|
.show();
|
||||||
|
});
|
||||||
|
let layout_size = ui.available_size();
|
||||||
|
ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| {
|
||||||
|
ui.add_space(12.0);
|
||||||
|
ui.vertical(|ui| {
|
||||||
|
ui.add_space(4.0);
|
||||||
|
View::ellipsize_text(ui, url, 18.0, Colors::title(false));
|
||||||
|
ui.add_space(1.0);
|
||||||
|
|
||||||
|
let value = format!("{} {}", CLOUD_CHECK, t!("network_settings.enabled"));
|
||||||
|
ui.label(RichText::new(value).size(15.0).color(Colors::gray()));
|
||||||
|
ui.add_space(3.0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw bridge connection line setup content.
|
||||||
|
fn conn_line_ui(&mut self, ui: &mut egui::Ui, bridge: &TorBridge, cb: &dyn PlatformCallbacks) {
|
||||||
|
// Draw round background.
|
||||||
|
let mut rect = ui.available_rect_before_wrap();
|
||||||
|
rect.set_height(56.0);
|
||||||
|
let r = if View::is_desktop() {
|
||||||
|
View::item_rounding(0, 2, false)
|
||||||
|
} else {
|
||||||
|
View::item_rounding(0, 1, false)
|
||||||
|
};
|
||||||
|
let bg = Colors::fill_lite();
|
||||||
|
let mut bg_shape = RectShape::new(rect, r, bg, View::item_stroke(), StrokeKind::Outside);
|
||||||
|
let bg_idx = ui.painter().add(bg_shape.clone());
|
||||||
|
|
||||||
|
let res = ui
|
||||||
|
.scope_builder(
|
||||||
|
UiBuilder::new()
|
||||||
|
.sense(Sense::click())
|
||||||
|
.layout(Layout::right_to_left(Align::Center))
|
||||||
|
.max_rect(rect),
|
||||||
|
|ui| {
|
||||||
|
View::item_button(ui, View::item_rounding(0, 1, true), SCAN, None, || {
|
||||||
|
self.show_qr_scan_bridge_modal(cb);
|
||||||
|
});
|
||||||
|
let layout_size = ui.available_size();
|
||||||
|
ui.allocate_ui_with_layout(
|
||||||
|
layout_size,
|
||||||
|
Layout::left_to_right(Align::Center),
|
||||||
|
|ui| {
|
||||||
|
ui.add_space(12.0);
|
||||||
|
ui.vertical(|ui| {
|
||||||
|
ui.add_space(4.0);
|
||||||
|
let line_text = bridge.connection_line();
|
||||||
|
View::ellipsize_text(ui, line_text, 18.0, Colors::title(false));
|
||||||
|
ui.add_space(1.0);
|
||||||
|
let line_desc = t!("transport.conn_line").replace(":", "");
|
||||||
|
let value = format!("{} {}", NOTCHES, line_desc);
|
||||||
|
ui.label(RichText::new(value).size(15.0).color(Colors::gray()));
|
||||||
|
ui.add_space(3.0);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.response;
|
||||||
|
let clicked = res.clicked() || res.long_touched();
|
||||||
|
// Setup background and cursor.
|
||||||
|
if res.hovered() {
|
||||||
|
res.on_hover_cursor(CursorIcon::PointingHand);
|
||||||
|
bg_shape.fill = Colors::fill();
|
||||||
|
}
|
||||||
|
ui.painter().set(bg_idx, bg_shape);
|
||||||
|
// Handle clicks on layout.
|
||||||
|
if clicked {
|
||||||
|
self.bridge_conn_line_edit = bridge.connection_line();
|
||||||
|
// Show connection line edit modal.
|
||||||
|
let title = bridge.protocol_name();
|
||||||
|
Modal::new(BRIDGE_CONN_LINE_EDIT_MODAL)
|
||||||
|
.position(ModalPosition::Center)
|
||||||
|
.title(title)
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Show bridge connection line QR code scanner.
|
||||||
|
fn show_qr_scan_bridge_modal(&mut self, cb: &dyn PlatformCallbacks) {
|
||||||
|
self.bridge_qr_scan_content = Some(CameraScanContent::default());
|
||||||
|
// Show QR code scan modal.
|
||||||
|
Modal::new(SCAN_BRIDGE_CONN_LINE_MODAL)
|
||||||
|
.position(ModalPosition::CenterTop)
|
||||||
|
.title(t!("scan_qr"))
|
||||||
|
.closeable(false)
|
||||||
|
.show();
|
||||||
|
cb.start_camera();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw bridge connection line input [`Modal`] content.
|
||||||
|
fn bridge_conn_line_edit_modal_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||||
|
let on_save = |c: &mut TorSettingsContent| {
|
||||||
|
let bridge = TorConfig::get_bridge().unwrap();
|
||||||
|
if bridge.connection_line() != c.bridge_conn_line_edit {
|
||||||
|
TorBridge::save_bridge_conn_line(&bridge, c.bridge_conn_line_edit.clone());
|
||||||
|
c.settings_changed = true;
|
||||||
|
}
|
||||||
|
Modal::close();
|
||||||
|
};
|
||||||
|
|
||||||
|
ui.add_space(6.0);
|
||||||
|
ui.vertical_centered(|ui| {
|
||||||
|
ui.label(
|
||||||
|
RichText::new(t!("transport.conn_line"))
|
||||||
|
.size(17.0)
|
||||||
|
.color(Colors::gray()),
|
||||||
|
);
|
||||||
|
ui.add_space(8.0);
|
||||||
|
|
||||||
|
// Draw connection line text edit.
|
||||||
|
ui.vertical_centered(|ui| {
|
||||||
|
let scroll_id = Id::from(BRIDGE_CONN_LINE_EDIT_MODAL);
|
||||||
|
View::horizontal_line(ui, Colors::item_stroke());
|
||||||
|
ui.add_space(3.0);
|
||||||
|
ScrollArea::both()
|
||||||
|
.id_salt(scroll_id)
|
||||||
|
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
|
||||||
|
.max_height(128.0)
|
||||||
|
.auto_shrink([false; 2])
|
||||||
|
.show(ui, |ui| {
|
||||||
|
ui.add_space(7.0);
|
||||||
|
let input_id = scroll_id.with("_input");
|
||||||
|
egui::TextEdit::multiline(&mut self.bridge_conn_line_edit)
|
||||||
|
.id(input_id)
|
||||||
|
.font(egui::TextStyle::Body)
|
||||||
|
.desired_rows(5)
|
||||||
|
.interactive(false)
|
||||||
|
.desired_width(f32::INFINITY)
|
||||||
|
.show(ui);
|
||||||
|
ui.add_space(6.0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
ui.add_space(2.0);
|
||||||
|
View::horizontal_line(ui, Colors::item_stroke());
|
||||||
|
ui.add_space(8.0);
|
||||||
|
|
||||||
|
// Setup spacing between buttons.
|
||||||
|
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
|
||||||
|
|
||||||
|
ui.columns(2, |columns| {
|
||||||
|
columns[0].vertical_centered_justified(|ui| {
|
||||||
|
// Draw paste button.
|
||||||
|
let paste_text = format!("{} {}", CLIPBOARD_TEXT, t!("paste"));
|
||||||
|
View::button(ui, paste_text, Colors::white_or_black(false), || {
|
||||||
|
self.bridge_conn_line_edit = cb.get_string_from_buffer();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
columns[1].vertical_centered_justified(|ui| {
|
||||||
|
// Draw button to scan bridge QR code.
|
||||||
|
let scan_text = format!("{} {}", SCAN, t!("scan"));
|
||||||
|
View::button(ui, scan_text, Colors::white_or_black(false), || {
|
||||||
|
self.show_qr_scan_bridge_modal(cb);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
ui.add_space(8.0);
|
||||||
|
View::horizontal_line(ui, Colors::item_stroke());
|
||||||
|
ui.add_space(8.0);
|
||||||
|
|
||||||
|
// Show modal buttons.
|
||||||
|
ui.scope(|ui| {
|
||||||
|
// Setup spacing between buttons.
|
||||||
|
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
|
||||||
|
|
||||||
|
ui.columns(2, |columns| {
|
||||||
|
columns[0].vertical_centered_justified(|ui| {
|
||||||
|
View::button(
|
||||||
|
ui,
|
||||||
|
t!("modal.cancel"),
|
||||||
|
Colors::white_or_black(false),
|
||||||
|
|| {
|
||||||
|
// Close modal.
|
||||||
|
Modal::close();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
columns[1].vertical_centered_justified(|ui| {
|
||||||
|
View::button(ui, t!("modal.save"), Colors::white_or_black(false), || {
|
||||||
|
on_save(self);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
ui.add_space(6.0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw bridge binary setup content.
|
||||||
|
fn bridge_bin_ui(&mut self, ui: &mut egui::Ui, bridge: &TorBridge, cb: &dyn PlatformCallbacks) {
|
||||||
|
// Draw round background.
|
||||||
|
let mut rect = ui.available_rect_before_wrap();
|
||||||
|
rect.set_height(56.0);
|
||||||
|
let r = View::item_rounding(1, 2, false);
|
||||||
|
let bg = Colors::fill_lite();
|
||||||
|
let mut bg_shape = RectShape::new(rect, r, bg, View::item_stroke(), StrokeKind::Outside);
|
||||||
|
let bg_idx = ui.painter().add(bg_shape.clone());
|
||||||
|
|
||||||
|
let res = ui
|
||||||
|
.scope_builder(
|
||||||
|
UiBuilder::new()
|
||||||
|
.sense(Sense::click())
|
||||||
|
.layout(Layout::right_to_left(Align::Center))
|
||||||
|
.max_rect(rect),
|
||||||
|
|ui| {
|
||||||
|
self.bridge_bin_pick_file.ui(ui, cb, |path| {
|
||||||
|
if bridge.binary_path() != path {
|
||||||
|
TorBridge::save_bridge_bin_path(bridge, path);
|
||||||
|
self.settings_changed = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let layout_size = ui.available_size();
|
||||||
|
ui.allocate_ui_with_layout(
|
||||||
|
layout_size,
|
||||||
|
Layout::left_to_right(Align::Center),
|
||||||
|
|ui| {
|
||||||
|
ui.add_space(12.0);
|
||||||
|
ui.vertical(|ui| {
|
||||||
|
ui.add_space(4.0);
|
||||||
|
let bin_text = bridge.binary_path();
|
||||||
|
View::ellipsize_text(ui, bin_text, 18.0, Colors::title(false));
|
||||||
|
ui.add_space(1.0);
|
||||||
|
let bin_desc = t!("transport.bin_file").replace(":", "");
|
||||||
|
let value = format!("{} {}", TERMINAL, bin_desc);
|
||||||
|
ui.label(RichText::new(value).size(15.0).color(Colors::gray()));
|
||||||
|
ui.add_space(3.0);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.response;
|
||||||
|
let clicked = res.clicked() || res.long_touched();
|
||||||
|
// Setup background and cursor.
|
||||||
|
if res.hovered() {
|
||||||
|
res.on_hover_cursor(CursorIcon::PointingHand);
|
||||||
|
bg_shape.fill = Colors::fill();
|
||||||
|
}
|
||||||
|
ui.painter().set(bg_idx, bg_shape);
|
||||||
|
// Handle clicks on layout.
|
||||||
|
if clicked {
|
||||||
|
self.bridge_bin_path_edit = bridge.binary_path();
|
||||||
|
// Show binary path edit modal.
|
||||||
|
let title = bridge.protocol_name();
|
||||||
|
Modal::new(BRIDGE_BIN_EDIT_MODAL)
|
||||||
|
.position(ModalPosition::CenterTop)
|
||||||
|
.title(title)
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw bridge binary input [`Modal`] content.
|
||||||
|
fn bridge_bin_edit_modal_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||||
|
let on_save = |c: &mut TorSettingsContent| {
|
||||||
|
let bridge = TorConfig::get_bridge().unwrap();
|
||||||
|
let exists = fs::exists(&c.bridge_bin_path_edit).unwrap_or_default();
|
||||||
|
if !exists {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if bridge.binary_path() != c.bridge_bin_path_edit {
|
||||||
|
TorBridge::save_bridge_bin_path(&bridge, c.bridge_bin_path_edit.clone());
|
||||||
|
c.settings_changed = true;
|
||||||
|
}
|
||||||
|
Modal::close();
|
||||||
|
};
|
||||||
|
|
||||||
|
ui.add_space(6.0);
|
||||||
|
ui.vertical_centered(|ui| {
|
||||||
|
ui.label(
|
||||||
|
RichText::new(t!("transport.bin_file"))
|
||||||
|
.size(17.0)
|
||||||
|
.color(Colors::gray()),
|
||||||
|
);
|
||||||
|
ui.add_space(8.0);
|
||||||
|
|
||||||
|
// Draw bridge text edit.
|
||||||
|
let mut edit = TextEdit::new(Id::from(BRIDGE_BIN_EDIT_MODAL)).paste();
|
||||||
|
edit.ui(ui, &mut self.bridge_bin_path_edit, cb);
|
||||||
|
if edit.enter_pressed {
|
||||||
|
on_save(self);
|
||||||
|
}
|
||||||
|
ui.add_space(12.0);
|
||||||
|
|
||||||
|
// Show modal buttons.
|
||||||
|
ui.scope(|ui| {
|
||||||
|
// Setup spacing between buttons.
|
||||||
|
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
|
||||||
|
|
||||||
|
ui.columns(2, |columns| {
|
||||||
|
columns[0].vertical_centered_justified(|ui| {
|
||||||
|
View::button(
|
||||||
|
ui,
|
||||||
|
t!("modal.cancel"),
|
||||||
|
Colors::white_or_black(false),
|
||||||
|
|| {
|
||||||
|
// Close modal.
|
||||||
|
Modal::close();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
columns[1].vertical_centered_justified(|ui| {
|
||||||
|
View::button(ui, t!("modal.save"), Colors::white_or_black(false), || {
|
||||||
|
on_save(self);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
ui.add_space(6.0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
+94
-111
@@ -12,128 +12,111 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
use egui::{Margin, Id, Layout, Align, UiBuilder};
|
use egui::{Align, Id, Layout, Margin, UiBuilder};
|
||||||
|
|
||||||
use crate::gui::Colors;
|
use crate::gui::Colors;
|
||||||
|
use crate::gui::views::types::{TitleContentType, TitleType};
|
||||||
use crate::gui::views::{Content, View};
|
use crate::gui::views::{Content, View};
|
||||||
use crate::gui::views::types::{LinePosition, TitleContentType, TitleType};
|
|
||||||
|
|
||||||
/// Title panel with left/right action buttons and text in the middle.
|
/// Title panel with left/right action buttons and text in the middle.
|
||||||
pub struct TitlePanel {
|
pub struct TitlePanel {
|
||||||
/// Widget identifier.
|
/// Widget identifier.
|
||||||
id: Id
|
id: Id,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TitlePanel {
|
impl TitlePanel {
|
||||||
/// Content height.
|
/// Content height.
|
||||||
pub const HEIGHT: f32 = 54.0;
|
pub const HEIGHT: f32 = 54.0;
|
||||||
|
|
||||||
/// Create new title panel with provided identifier.
|
/// Create new title panel with provided identifier.
|
||||||
pub fn new(id: Id) -> Self {
|
pub fn new(id: Id) -> Self {
|
||||||
Self {
|
Self { id }
|
||||||
id,
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn ui(&self,
|
pub fn ui(
|
||||||
title: TitleType,
|
&self,
|
||||||
mut left_content: impl FnMut(&mut egui::Ui),
|
title: TitleType,
|
||||||
mut right_content: impl FnMut(&mut egui::Ui),
|
mut left_content: impl FnMut(&mut egui::Ui),
|
||||||
ui: &mut egui::Ui) {
|
mut right_content: impl FnMut(&mut egui::Ui),
|
||||||
// Draw title panel.
|
ui: &mut egui::Ui,
|
||||||
egui::TopBottomPanel::top(self.id)
|
) {
|
||||||
.resizable(false)
|
// Draw title panel.
|
||||||
.exact_height(Self::HEIGHT + View::get_top_inset())
|
egui::TopBottomPanel::top(self.id)
|
||||||
.frame(egui::Frame {
|
.resizable(false)
|
||||||
inner_margin: Margin {
|
.exact_height(Self::HEIGHT + View::get_top_inset())
|
||||||
left: View::far_left_inset_margin(ui),
|
.frame(egui::Frame {
|
||||||
right: View::far_right_inset_margin(ui),
|
inner_margin: Margin {
|
||||||
top: View::get_top_inset(),
|
left: View::far_left_inset_margin(ui) as i8,
|
||||||
bottom: 0.0,
|
right: View::far_right_inset_margin(ui) as i8,
|
||||||
},
|
top: View::get_top_inset() as i8,
|
||||||
..Default::default()
|
bottom: 0.0 as i8,
|
||||||
})
|
},
|
||||||
.show_inside(ui, |ui| {
|
..Default::default()
|
||||||
let rect = ui.available_rect_before_wrap();
|
})
|
||||||
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Max), |ui| {
|
.show_inside(ui, |ui| {
|
||||||
ui.horizontal_centered(|ui| {
|
let rect = ui.available_rect_before_wrap();
|
||||||
(right_content)(ui);
|
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Max), |ui| {
|
||||||
});
|
ui.horizontal_centered(|ui| {
|
||||||
ui.with_layout(Layout::left_to_right(Align::Min), |ui| {
|
(right_content)(ui);
|
||||||
ui.horizontal_centered(|ui| {
|
});
|
||||||
(left_content)(ui);
|
ui.with_layout(Layout::left_to_right(Align::Min), |ui| {
|
||||||
});
|
ui.horizontal_centered(|ui| {
|
||||||
});
|
(left_content)(ui);
|
||||||
match title {
|
});
|
||||||
TitleType::Single(content) => {
|
});
|
||||||
let content_rect = {
|
match title {
|
||||||
let mut r = rect.clone();
|
TitleType::Single(content) => {
|
||||||
r.min.x += Self::HEIGHT;
|
let content_rect = {
|
||||||
r.max.x -= Self::HEIGHT;
|
let mut r = rect.clone();
|
||||||
r
|
r.min.x += Self::HEIGHT;
|
||||||
};
|
r.max.x -= Self::HEIGHT;
|
||||||
ui.allocate_new_ui(UiBuilder::new().max_rect(content_rect), |ui| {
|
r
|
||||||
Self::title_text_content(ui, content);
|
};
|
||||||
});
|
ui.scope_builder(UiBuilder::new().max_rect(content_rect), |ui| {
|
||||||
}
|
Self::title_text_content(ui, content);
|
||||||
TitleType::Dual(first, second) => {
|
});
|
||||||
let first_rect = {
|
}
|
||||||
let mut r = rect.clone();
|
TitleType::Dual(first, second) => {
|
||||||
r.max.x = r.min.x + Content::SIDE_PANEL_WIDTH - Self::HEIGHT;
|
let first_rect = {
|
||||||
r.min.x += Self::HEIGHT;
|
let mut r = rect.clone();
|
||||||
r
|
r.max.x = r.min.x + Content::SIDE_PANEL_WIDTH - Self::HEIGHT;
|
||||||
};
|
r.min.x += Self::HEIGHT;
|
||||||
// Draw first title content.
|
r
|
||||||
ui.allocate_new_ui(UiBuilder::new().max_rect(first_rect), |ui| {
|
};
|
||||||
Self::title_text_content(ui, first);
|
// Draw first title content.
|
||||||
});
|
ui.scope_builder(UiBuilder::new().max_rect(first_rect), |ui| {
|
||||||
|
Self::title_text_content(ui, first);
|
||||||
|
});
|
||||||
|
|
||||||
let second_rect = {
|
let second_rect = {
|
||||||
let mut r = rect.clone();
|
let mut r = rect.clone();
|
||||||
r.min.x = first_rect.max.x + 2.0 * Self::HEIGHT;
|
r.min.x = first_rect.max.x + 2.0 * Self::HEIGHT;
|
||||||
r.max.x -= Self::HEIGHT;
|
r.max.x -= Self::HEIGHT;
|
||||||
r
|
r
|
||||||
};
|
};
|
||||||
// Draw second title content.
|
// Draw second title content.
|
||||||
ui.allocate_new_ui(UiBuilder::new().max_rect(second_rect), |ui| {
|
ui.scope_builder(UiBuilder::new().max_rect(second_rect), |ui| {
|
||||||
Self::title_text_content(ui, second);
|
Self::title_text_content(ui, second);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Draw content divider line.
|
/// Setup title text content.
|
||||||
let r = {
|
fn title_text_content(ui: &mut egui::Ui, content: TitleContentType) {
|
||||||
let mut r = rect.clone();
|
ui.vertical_centered(|ui| match content {
|
||||||
r.min.x -= View::far_left_inset_margin(ui);
|
TitleContentType::Title(text) => {
|
||||||
r.max.x += View::far_right_inset_margin(ui);
|
ui.add_space(13.0 + if !View::is_desktop() { 1.0 } else { 0.0 });
|
||||||
r
|
View::ellipsize_text(ui, text.to_uppercase(), 19.0, Colors::title(true));
|
||||||
};
|
}
|
||||||
if Content::is_dual_panel_mode(ui.ctx()) {
|
TitleContentType::WithSubTitle(text, subtitle, animate) => {
|
||||||
View::line(ui, LinePosition::BOTTOM, &r, Colors::stroke());
|
ui.add_space(4.0);
|
||||||
}
|
View::ellipsize_text(ui, text.to_uppercase(), 18.0, Colors::title(true));
|
||||||
});
|
ui.add_space(-2.0);
|
||||||
}
|
View::animate_text(ui, subtitle, 15.0, Colors::text(true), animate)
|
||||||
|
}
|
||||||
/// Setup title text content.
|
});
|
||||||
fn title_text_content(ui: &mut egui::Ui, content: TitleContentType) {
|
}
|
||||||
ui.vertical_centered(|ui| {
|
|
||||||
match content {
|
|
||||||
TitleContentType::Title(text) => {
|
|
||||||
ui.add_space(13.0 + if !View::is_desktop() {
|
|
||||||
1.0
|
|
||||||
} else {
|
|
||||||
0.0
|
|
||||||
});
|
|
||||||
View::ellipsize_text(ui, text.to_uppercase(), 19.0, Colors::title(true));
|
|
||||||
}
|
|
||||||
TitleContentType::WithSubTitle(text, subtitle, animate) => {
|
|
||||||
ui.add_space(4.0);
|
|
||||||
View::ellipsize_text(ui, text.to_uppercase(), 18.0, Colors::title(true));
|
|
||||||
ui.add_space(-2.0);
|
|
||||||
View::animate_text(ui, subtitle, 15.0, Colors::text(true), animate)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+92
-164
@@ -12,216 +12,144 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
use grin_util::ZeroingString;
|
|
||||||
use crate::gui::platform::PlatformCallbacks;
|
use crate::gui::platform::PlatformCallbacks;
|
||||||
use crate::gui::views::Modal;
|
use crate::gui::views::Modal;
|
||||||
|
use grin_util::ZeroingString;
|
||||||
|
|
||||||
/// Title type, can be single or dual title in the row.
|
/// Title type, can be single or dual title in the row.
|
||||||
pub enum TitleType {
|
pub enum TitleType {
|
||||||
/// Single title content.
|
/// Single title content.
|
||||||
Single(TitleContentType),
|
Single(TitleContentType),
|
||||||
/// Dual title content, will align first content for default panel size width.
|
/// Dual title content, will align first content for default panel size width.
|
||||||
Dual(TitleContentType, TitleContentType),
|
Dual(TitleContentType, TitleContentType),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Title content type, can be single title or with animated subtitle.
|
/// Title content type, can be single title or with animated subtitle.
|
||||||
pub enum TitleContentType {
|
pub enum TitleContentType {
|
||||||
/// Single text.
|
/// Single text.
|
||||||
Title(String),
|
Title(String),
|
||||||
/// With optionally animated subtitle text.
|
/// With optionally animated subtitle text.
|
||||||
WithSubTitle(String, String, bool)
|
WithSubTitle(String, String, bool),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Stroke position against content.
|
/// Stroke position against content.
|
||||||
pub enum LinePosition {
|
pub enum LinePosition {
|
||||||
TOP, LEFT, RIGHT, BOTTOM
|
TOP,
|
||||||
|
LEFT,
|
||||||
|
RIGHT,
|
||||||
|
BOTTOM,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Position of [`Modal`] on the screen.
|
/// Position of [`Modal`] on the screen.
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub enum ModalPosition {
|
pub enum ModalPosition {
|
||||||
CenterTop,
|
CenterTop,
|
||||||
Center
|
Center,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Global [`Modal`] state.
|
/// Global [`Modal`] state.
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct ModalState {
|
pub struct ModalState {
|
||||||
/// Opened [`Modal`].
|
/// Opened [`Modal`].
|
||||||
pub modal: Option<Modal>,
|
pub modal: Option<Modal>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Contains identifiers to draw opened [`Modal`] content for current ui container.
|
/// Content container to simplify modals management and navigation.
|
||||||
pub trait ModalContainer {
|
pub trait ContentContainer {
|
||||||
/// List of allowed [`Modal`] identifiers.
|
/// List of allowed [`Modal`] identifiers.
|
||||||
fn modal_ids(&self) -> &Vec<&'static str>;
|
fn modal_ids(&self) -> Vec<&'static str>;
|
||||||
|
/// Draw modal content.
|
||||||
/// Draw modal ui content.
|
fn modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks);
|
||||||
fn modal_ui(&mut self,
|
/// Draw container content.
|
||||||
ui: &mut egui::Ui,
|
fn container_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks);
|
||||||
modal: &Modal,
|
/// Draw content, to call by parent container.
|
||||||
cb: &dyn PlatformCallbacks);
|
fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||||
|
// Draw modal content.
|
||||||
/// Draw [`Modal`] for current ui container if it's possible.
|
if let Some(id) = Modal::opened() {
|
||||||
fn current_modal_ui(&mut self,
|
if self.modal_ids().contains(&id) {
|
||||||
ui: &mut egui::Ui,
|
Modal::ui(ui.ctx(), cb, |ui, modal, cb| {
|
||||||
cb: &dyn PlatformCallbacks) {
|
self.modal_ui(ui, modal, cb);
|
||||||
let modal_id = Modal::opened();
|
});
|
||||||
let draw = modal_id.is_some() && self.modal_ids().contains(&modal_id.unwrap());
|
}
|
||||||
if draw {
|
}
|
||||||
Modal::ui(ui.ctx(), |ui, modal| {
|
self.container_ui(ui, cb);
|
||||||
self.modal_ui(ui, modal, cb);
|
}
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Options for [`egui::TextEdit`] view.
|
|
||||||
pub struct TextEditOptions {
|
|
||||||
/// View identifier.
|
|
||||||
pub id: egui::Id,
|
|
||||||
/// Check if horizontal centering is needed.
|
|
||||||
pub h_center: bool,
|
|
||||||
/// Check if initial focus on field is needed.
|
|
||||||
pub focus: bool,
|
|
||||||
/// Hide letters and draw button to show/hide letters.
|
|
||||||
pub password: bool,
|
|
||||||
/// Show copy button.
|
|
||||||
pub copy: bool,
|
|
||||||
/// Show paste button.
|
|
||||||
pub paste: bool,
|
|
||||||
/// Show button to scan QR code into text.
|
|
||||||
pub scan_qr: bool,
|
|
||||||
/// Callback when scan button was pressed.
|
|
||||||
pub scan_pressed: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TextEditOptions {
|
|
||||||
pub fn new(id: egui::Id) -> Self {
|
|
||||||
Self {
|
|
||||||
id,
|
|
||||||
h_center: false,
|
|
||||||
focus: true,
|
|
||||||
password: false,
|
|
||||||
copy: false,
|
|
||||||
paste: false,
|
|
||||||
scan_qr: false,
|
|
||||||
scan_pressed: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Center text horizontally.
|
|
||||||
pub fn h_center(mut self) -> Self {
|
|
||||||
self.h_center = true;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Disable initial focus.
|
|
||||||
pub fn no_focus(mut self) -> Self {
|
|
||||||
self.focus = false;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Hide letters and draw button to show/hide letters.
|
|
||||||
pub fn password(mut self) -> Self {
|
|
||||||
self.password = true;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Show button to copy text.
|
|
||||||
pub fn copy(mut self) -> Self {
|
|
||||||
self.copy = true;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Show button to paste text.
|
|
||||||
pub fn paste(mut self) -> Self {
|
|
||||||
self.paste = true;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Show button to scan QR code to text.
|
|
||||||
pub fn scan_qr(mut self) -> Self {
|
|
||||||
self.scan_qr = true;
|
|
||||||
self.scan_pressed = false;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// QR code scan result.
|
/// QR code scan result.
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub enum QrScanResult {
|
pub enum QrScanResult {
|
||||||
/// Slatepack message.
|
/// Slatepack message.
|
||||||
Slatepack(ZeroingString),
|
Slatepack(String),
|
||||||
/// Slatepack address.
|
/// Slatepack address.
|
||||||
Address(ZeroingString),
|
Address(ZeroingString),
|
||||||
/// Parsed text.
|
/// Parsed text.
|
||||||
Text(ZeroingString),
|
Text(ZeroingString),
|
||||||
/// Recovery phrase in standard or compact SeedQR format.
|
/// Recovery phrase in standard or compact SeedQR format.
|
||||||
/// https://github.com/SeedSigner/seedsigner/blob/dev/docs/seed_qr/README.md
|
/// https://github.com/SeedSigner/seedsigner/blob/dev/docs/seed_qr/README.md
|
||||||
SeedQR(ZeroingString),
|
SeedQR(ZeroingString),
|
||||||
/// Part of Uniform Resources as URI with current index and total messages amount.
|
/// Part of Uniform Resources as URI with current index and total messages amount.
|
||||||
/// https://github.com/BlockchainCommons/Research/blob/master/papers/bcr-2020-005-ur.md
|
/// https://github.com/BlockchainCommons/Research/blob/master/papers/bcr-2020-005-ur.md
|
||||||
URPart(String, usize, usize),
|
URPart(String, usize, usize),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl QrScanResult {
|
impl QrScanResult {
|
||||||
/// Get text scanning result.
|
/// Get text scanning result.
|
||||||
pub fn text(&self) -> String {
|
pub fn text(&self) -> String {
|
||||||
match self {
|
match self {
|
||||||
QrScanResult::Slatepack(text) => text.to_string(),
|
QrScanResult::Slatepack(text) => text.to_string(),
|
||||||
QrScanResult::Address(text) => text.to_string(),
|
QrScanResult::Address(text) => text.to_string(),
|
||||||
QrScanResult::Text(text) => text.to_string(),
|
QrScanResult::Text(text) => text.to_string(),
|
||||||
QrScanResult::SeedQR(text) => text.to_string(),
|
QrScanResult::SeedQR(text) => text.to_string(),
|
||||||
QrScanResult::URPart(uri, _, _) => uri.to_string(),
|
QrScanResult::URPart(uri, _, _) => uri.to_string(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// QR code scanning state.
|
/// QR code scanning state.
|
||||||
pub struct QrScanState {
|
pub struct QrScanState {
|
||||||
/// Flag to check if image is processing to find QR code.
|
/// Flag to check if image is processing to find QR code.
|
||||||
pub image_processing: bool,
|
pub image_processing: bool,
|
||||||
/// Processed QR code result.
|
/// Processed QR code result.
|
||||||
pub qr_scan_result: Option<QrScanResult>
|
pub qr_scan_result: Option<QrScanResult>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for QrScanState {
|
impl Default for QrScanState {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
image_processing: false,
|
image_processing: false,
|
||||||
qr_scan_result: None,
|
qr_scan_result: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// QR code image data state.
|
/// QR code image data state.
|
||||||
pub struct QrImageState {
|
pub struct QrImageState {
|
||||||
/// Flag to check if QR code image is loading.
|
/// Flag to check if QR code image is loading.
|
||||||
pub loading: bool,
|
pub loading: bool,
|
||||||
/// Flag to check if QR code image is exporting.
|
/// Flag to check if QR code image is exporting.
|
||||||
pub exporting: bool,
|
pub exporting: bool,
|
||||||
|
|
||||||
/// Created GIF data from animated QR code.
|
/// Created GIF data from animated QR code.
|
||||||
pub gif_data: Option<Vec<u8>>,
|
pub gif_data: Option<Vec<u8>>,
|
||||||
/// Flag to check if GIF is creating.
|
/// Flag to check if GIF is creating.
|
||||||
pub gif_creating: bool,
|
pub gif_creating: bool,
|
||||||
|
|
||||||
/// Vector image data.
|
/// Vector image data.
|
||||||
pub svg: Option<Vec<u8>>,
|
pub svg: Option<Vec<u8>>,
|
||||||
/// Multiple vector image data for animated QR code.
|
/// Multiple vector image data for animated QR code.
|
||||||
pub svg_list: Option<Vec<Vec<u8>>>
|
pub svg_list: Option<Vec<Vec<u8>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for QrImageState {
|
impl Default for QrImageState {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
loading: false,
|
loading: false,
|
||||||
exporting: false,
|
exporting: false,
|
||||||
gif_data: None,
|
gif_data: None,
|
||||||
gif_creating: false,
|
gif_creating: false,
|
||||||
svg: None,
|
svg: None,
|
||||||
svg_list: None,
|
svg_list: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+753
-800
File diff suppressed because it is too large
Load Diff
+775
-585
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,376 @@
|
|||||||
|
// Copyright 2023 The Grim Developers
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
use egui::scroll_area::ScrollBarVisibility;
|
||||||
|
use egui::{Id, Margin, RichText, ScrollArea};
|
||||||
|
use grin_util::ZeroingString;
|
||||||
|
|
||||||
|
use crate::gui::Colors;
|
||||||
|
use crate::gui::icons::{CHECK, CLIPBOARD_TEXT, COPY, SCAN};
|
||||||
|
use crate::gui::platform::PlatformCallbacks;
|
||||||
|
use crate::gui::views::types::{ContentContainer, LinePosition, ModalPosition, QrScanResult};
|
||||||
|
use crate::gui::views::wallets::ConnectionSettings;
|
||||||
|
use crate::gui::views::wallets::creation::MnemonicSetup;
|
||||||
|
use crate::gui::views::wallets::creation::types::Step;
|
||||||
|
use crate::gui::views::{CameraScanContent, Content, Modal, View};
|
||||||
|
use crate::node::Node;
|
||||||
|
use crate::wallet::types::PhraseMode;
|
||||||
|
use crate::wallet::{ExternalConnection, Wallet};
|
||||||
|
|
||||||
|
/// Wallet creation content.
|
||||||
|
pub struct WalletCreationContent {
|
||||||
|
/// Wallet name.
|
||||||
|
pub name: String,
|
||||||
|
/// Wallet password.
|
||||||
|
pub pass: ZeroingString,
|
||||||
|
|
||||||
|
/// Wallet creation step.
|
||||||
|
step: Step,
|
||||||
|
|
||||||
|
/// QR code scanning [`Modal`] content.
|
||||||
|
scan_modal_content: Option<CameraScanContent>,
|
||||||
|
|
||||||
|
/// Mnemonic phrase setup content.
|
||||||
|
mnemonic_setup: MnemonicSetup,
|
||||||
|
/// Network setup content.
|
||||||
|
network_setup: ConnectionSettings,
|
||||||
|
|
||||||
|
/// Flag to check if an error occurred during wallet creation.
|
||||||
|
creation_error: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
const QR_CODE_PHRASE_SCAN_MODAL: &'static str = "qr_code_rec_phrase_scan_modal";
|
||||||
|
|
||||||
|
impl ContentContainer for WalletCreationContent {
|
||||||
|
fn modal_ids(&self) -> Vec<&'static str> {
|
||||||
|
vec![QR_CODE_PHRASE_SCAN_MODAL]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
|
||||||
|
match modal.id {
|
||||||
|
QR_CODE_PHRASE_SCAN_MODAL => {
|
||||||
|
if let Some(content) = self.scan_modal_content.as_mut() {
|
||||||
|
content.modal_ui(ui, cb, |result| match result {
|
||||||
|
QrScanResult::Text(text) => {
|
||||||
|
self.mnemonic_setup.mnemonic.import(&text);
|
||||||
|
Modal::close();
|
||||||
|
}
|
||||||
|
QrScanResult::SeedQR(text) => {
|
||||||
|
self.mnemonic_setup.mnemonic.import(&text);
|
||||||
|
Modal::close();
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn container_ui(&mut self, _: &mut egui::Ui, _: &dyn PlatformCallbacks) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WalletCreationContent {
|
||||||
|
/// Create new wallet creation content from name and password.
|
||||||
|
pub fn new(name: String, pass: ZeroingString) -> Self {
|
||||||
|
Self {
|
||||||
|
name,
|
||||||
|
pass,
|
||||||
|
step: Step::EnterMnemonic,
|
||||||
|
scan_modal_content: None,
|
||||||
|
mnemonic_setup: MnemonicSetup::default(),
|
||||||
|
network_setup: ConnectionSettings::default(),
|
||||||
|
creation_error: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw wallet creation content.
|
||||||
|
pub fn content_ui(
|
||||||
|
&mut self,
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
cb: &dyn PlatformCallbacks,
|
||||||
|
on_create: impl FnMut(Wallet),
|
||||||
|
) {
|
||||||
|
self.ui(ui, cb);
|
||||||
|
egui::TopBottomPanel::bottom("wallet_creation_step_panel")
|
||||||
|
.frame(egui::Frame {
|
||||||
|
inner_margin: Margin {
|
||||||
|
left: (View::far_left_inset_margin(ui) + View::TAB_ITEMS_PADDING) as i8,
|
||||||
|
right: (View::get_right_inset() + View::TAB_ITEMS_PADDING) as i8,
|
||||||
|
top: View::TAB_ITEMS_PADDING as i8,
|
||||||
|
bottom: (View::get_bottom_inset() + View::TAB_ITEMS_PADDING) as i8,
|
||||||
|
},
|
||||||
|
fill: Colors::fill(),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.show_inside(ui, |ui| {
|
||||||
|
// Draw divider line.
|
||||||
|
let rect = {
|
||||||
|
let mut r = ui.available_rect_before_wrap();
|
||||||
|
r.min.y -= View::TAB_ITEMS_PADDING;
|
||||||
|
r.min.x -= View::far_left_inset_margin(ui) + View::TAB_ITEMS_PADDING;
|
||||||
|
r.max.x += View::get_right_inset() + View::TAB_ITEMS_PADDING;
|
||||||
|
r
|
||||||
|
};
|
||||||
|
View::line(ui, LinePosition::TOP, &rect, Colors::item_stroke());
|
||||||
|
// Show step control content.
|
||||||
|
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
|
||||||
|
self.step_control_ui(ui, on_create, cb);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Show wallet creation step content panel.
|
||||||
|
egui::CentralPanel::default()
|
||||||
|
.frame(egui::Frame {
|
||||||
|
inner_margin: Margin {
|
||||||
|
left: (View::far_left_inset_margin(ui) + 4.0) as i8,
|
||||||
|
right: (View::get_right_inset() + 4.0) as i8,
|
||||||
|
top: 3.0 as i8,
|
||||||
|
bottom: 4.0 as i8,
|
||||||
|
},
|
||||||
|
fill: Colors::fill_lite(),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.show_inside(ui, |ui| {
|
||||||
|
ScrollArea::vertical()
|
||||||
|
.id_salt(Id::from(format!(
|
||||||
|
"creation_step_scroll_{}",
|
||||||
|
self.step.name()
|
||||||
|
)))
|
||||||
|
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
|
||||||
|
.auto_shrink([false; 2])
|
||||||
|
.show(ui, |ui| {
|
||||||
|
let max_width = if self.step == Step::SetupConnection {
|
||||||
|
Content::SIDE_PANEL_WIDTH * 1.3
|
||||||
|
} else {
|
||||||
|
Content::SIDE_PANEL_WIDTH * 2.0
|
||||||
|
};
|
||||||
|
View::max_width_ui(ui, max_width, |ui| {
|
||||||
|
self.step_content_ui(ui, cb);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw [`Step`] description and confirmation control.
|
||||||
|
fn step_control_ui(
|
||||||
|
&mut self,
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
on_create: impl FnOnce(Wallet),
|
||||||
|
cb: &dyn PlatformCallbacks,
|
||||||
|
) {
|
||||||
|
let step = &self.step;
|
||||||
|
// Setup description and next step availability.
|
||||||
|
let (step_text, mut next) = match step {
|
||||||
|
Step::EnterMnemonic => {
|
||||||
|
let mode = &self.mnemonic_setup.mnemonic.mode();
|
||||||
|
let (text, available) = match mode {
|
||||||
|
PhraseMode::Generate => (t!("wallets.create_phrase_desc"), true),
|
||||||
|
PhraseMode::Import => {
|
||||||
|
let available = !self.mnemonic_setup.mnemonic.has_empty_or_invalid();
|
||||||
|
(t!("wallets.restore_phrase_desc"), available)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
(text, available)
|
||||||
|
}
|
||||||
|
Step::ConfirmMnemonic => {
|
||||||
|
let text = t!("wallets.restore_phrase_desc");
|
||||||
|
let available = !self.mnemonic_setup.mnemonic.has_empty_or_invalid();
|
||||||
|
(text, available)
|
||||||
|
}
|
||||||
|
Step::SetupConnection => (t!("wallets.setup_conn_desc"), self.creation_error.is_none()),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Show step description or error.
|
||||||
|
let generate_step = step == &Step::EnterMnemonic
|
||||||
|
&& self.mnemonic_setup.mnemonic.mode() == PhraseMode::Generate;
|
||||||
|
if (self.mnemonic_setup.mnemonic.valid() && self.creation_error.is_none()) || generate_step
|
||||||
|
{
|
||||||
|
ui.label(RichText::new(step_text).size(16.0).color(Colors::gray()));
|
||||||
|
ui.add_space(6.0);
|
||||||
|
} else {
|
||||||
|
next = false;
|
||||||
|
// Show error text.
|
||||||
|
if let Some(err) = &self.creation_error {
|
||||||
|
ui.add_space(10.0);
|
||||||
|
ui.label(RichText::new(err).size(16.0).color(Colors::red()));
|
||||||
|
ui.add_space(10.0);
|
||||||
|
} else {
|
||||||
|
ui.label(
|
||||||
|
RichText::new(t!("wallets.not_valid_phrase"))
|
||||||
|
.size(16.0)
|
||||||
|
.color(Colors::red()),
|
||||||
|
);
|
||||||
|
ui.add_space(4.0);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup spacing between buttons.
|
||||||
|
ui.style_mut().spacing.item_spacing = egui::vec2(8.0, 0.0);
|
||||||
|
// Setup vertical padding inside button.
|
||||||
|
ui.style_mut().spacing.button_padding = egui::vec2(10.0, 7.0);
|
||||||
|
|
||||||
|
match step {
|
||||||
|
Step::EnterMnemonic => {
|
||||||
|
ui.columns(2, |columns| {
|
||||||
|
// Show copy or paste button for mnemonic phrase step.
|
||||||
|
columns[0].vertical_centered_justified(|ui| {
|
||||||
|
match self.mnemonic_setup.mnemonic.mode() {
|
||||||
|
PhraseMode::Generate => {
|
||||||
|
let c_t = format!("{} {}", COPY, t!("copy"));
|
||||||
|
View::button(ui, c_t, Colors::white_or_black(false), || {
|
||||||
|
cb.copy_string_to_buffer(
|
||||||
|
self.mnemonic_setup.mnemonic.get_phrase(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
PhraseMode::Import => {
|
||||||
|
let p_t = format!("{} {}", CLIPBOARD_TEXT, t!("paste"));
|
||||||
|
View::button(ui, p_t, Colors::white_or_black(false), || {
|
||||||
|
let data = ZeroingString::from(cb.get_string_from_buffer());
|
||||||
|
self.mnemonic_setup.mnemonic.import(&data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Show next step or QR code scan button.
|
||||||
|
columns[1].vertical_centered_justified(|ui| {
|
||||||
|
if next {
|
||||||
|
self.next_step_button_ui(ui, on_create);
|
||||||
|
} else {
|
||||||
|
let scan_text = format!("{} {}", SCAN, t!("scan"));
|
||||||
|
View::button(ui, scan_text, Colors::white_or_black(false), || {
|
||||||
|
self.scan_modal_content = Some(CameraScanContent::default());
|
||||||
|
// Show QR code scan modal.
|
||||||
|
Modal::new(QR_CODE_PHRASE_SCAN_MODAL)
|
||||||
|
.position(ModalPosition::CenterTop)
|
||||||
|
.title(t!("scan_qr"))
|
||||||
|
.closeable(false)
|
||||||
|
.show();
|
||||||
|
cb.start_camera();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Step::ConfirmMnemonic => {
|
||||||
|
// Show next step or paste button.
|
||||||
|
if next {
|
||||||
|
self.next_step_button_ui(ui, on_create);
|
||||||
|
} else {
|
||||||
|
let paste_text = format!("{} {}", CLIPBOARD_TEXT, t!("paste"));
|
||||||
|
View::button(ui, paste_text, Colors::white_or_black(false), || {
|
||||||
|
let data = ZeroingString::from(cb.get_string_from_buffer());
|
||||||
|
self.mnemonic_setup.mnemonic.import(&data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Step::SetupConnection => {
|
||||||
|
if next {
|
||||||
|
self.next_step_button_ui(ui, on_create);
|
||||||
|
ui.add_space(2.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw button to go to next [`Step`].
|
||||||
|
fn next_step_button_ui(&mut self, ui: &mut egui::Ui, on_create: impl FnOnce(Wallet)) {
|
||||||
|
// Setup button text.
|
||||||
|
let (next_text, text_color, bg_color) = if self.step == Step::SetupConnection {
|
||||||
|
(
|
||||||
|
format!("{} {}", CHECK, t!("complete")),
|
||||||
|
Colors::title(true),
|
||||||
|
Colors::gold(),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
(
|
||||||
|
t!("continue").into(),
|
||||||
|
Colors::green(),
|
||||||
|
Colors::white_or_black(false),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Show next step button.
|
||||||
|
View::colored_text_button_ui(ui, next_text.to_uppercase(), text_color, bg_color, |ui| {
|
||||||
|
self.step = match self.step {
|
||||||
|
Step::EnterMnemonic => {
|
||||||
|
if self.mnemonic_setup.mnemonic.mode() == PhraseMode::Generate {
|
||||||
|
Step::ConfirmMnemonic
|
||||||
|
} else {
|
||||||
|
Step::SetupConnection
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Step::ConfirmMnemonic => Step::SetupConnection,
|
||||||
|
Step::SetupConnection => {
|
||||||
|
// Create wallet at last step.
|
||||||
|
match Wallet::create(
|
||||||
|
&self.name,
|
||||||
|
&self.pass,
|
||||||
|
&self.mnemonic_setup.mnemonic,
|
||||||
|
&self.network_setup.method,
|
||||||
|
) {
|
||||||
|
Ok(w) => {
|
||||||
|
self.mnemonic_setup.reset();
|
||||||
|
// Pass created wallet to callback.
|
||||||
|
(on_create)(w);
|
||||||
|
Step::EnterMnemonic
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
self.creation_error = Some(format!("{:?}", e));
|
||||||
|
Step::SetupConnection
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check external connections availability on connection setup.
|
||||||
|
if self.step == Step::SetupConnection {
|
||||||
|
ExternalConnection::check(None, ui.ctx());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw wallet creation [`Step`] content.
|
||||||
|
fn step_content_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||||
|
match &self.step {
|
||||||
|
Step::EnterMnemonic => self.mnemonic_setup.enter_ui(ui, cb),
|
||||||
|
Step::ConfirmMnemonic => self.mnemonic_setup.confirm_ui(ui, cb),
|
||||||
|
Step::SetupConnection => {
|
||||||
|
// Redraw if node is running.
|
||||||
|
if Node::is_running() && !Content::is_dual_panel_mode(ui.ctx()) {
|
||||||
|
ui.ctx().request_repaint_after(Node::STATS_UPDATE_DELAY);
|
||||||
|
}
|
||||||
|
self.network_setup.ui(ui, cb);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Back to previous wallet creation [`Step`], return `true` to close creation.
|
||||||
|
pub fn on_back(&mut self) -> bool {
|
||||||
|
match &self.step {
|
||||||
|
Step::ConfirmMnemonic => {
|
||||||
|
self.step = Step::EnterMnemonic;
|
||||||
|
false
|
||||||
|
}
|
||||||
|
Step::SetupConnection => {
|
||||||
|
self.creation_error = None;
|
||||||
|
self.step = Step::EnterMnemonic;
|
||||||
|
false
|
||||||
|
}
|
||||||
|
_ => true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,380 +0,0 @@
|
|||||||
// Copyright 2023 The Grim Developers
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
use egui::{Id, Margin, RichText, ScrollArea};
|
|
||||||
use egui::scroll_area::ScrollBarVisibility;
|
|
||||||
use grin_util::ZeroingString;
|
|
||||||
|
|
||||||
use crate::gui::Colors;
|
|
||||||
use crate::gui::icons::{CHECK, CLIPBOARD_TEXT, COPY, SCAN};
|
|
||||||
use crate::gui::platform::PlatformCallbacks;
|
|
||||||
use crate::gui::views::{Modal, Content, View, CameraScanModal};
|
|
||||||
use crate::gui::views::types::{LinePosition, ModalContainer, ModalPosition, QrScanResult};
|
|
||||||
use crate::gui::views::wallets::creation::MnemonicSetup;
|
|
||||||
use crate::gui::views::wallets::creation::types::Step;
|
|
||||||
use crate::gui::views::wallets::ConnectionSettings;
|
|
||||||
use crate::node::Node;
|
|
||||||
use crate::wallet::{ExternalConnection, Wallet};
|
|
||||||
use crate::wallet::types::PhraseMode;
|
|
||||||
|
|
||||||
/// Wallet creation content.
|
|
||||||
pub struct WalletCreation {
|
|
||||||
/// Wallet name.
|
|
||||||
pub name: String,
|
|
||||||
/// Wallet password.
|
|
||||||
pub pass: ZeroingString,
|
|
||||||
|
|
||||||
/// Wallet creation step.
|
|
||||||
step: Step,
|
|
||||||
|
|
||||||
/// QR code scanning [`Modal`] content.
|
|
||||||
scan_modal_content: Option<CameraScanModal>,
|
|
||||||
|
|
||||||
/// Mnemonic phrase setup content.
|
|
||||||
mnemonic_setup: MnemonicSetup,
|
|
||||||
/// Network setup content.
|
|
||||||
network_setup: ConnectionSettings,
|
|
||||||
|
|
||||||
/// Flag to check if an error occurred during wallet creation.
|
|
||||||
creation_error: Option<String>,
|
|
||||||
|
|
||||||
/// [`Modal`] identifiers allowed at this ui container.
|
|
||||||
modal_ids: Vec<&'static str>
|
|
||||||
}
|
|
||||||
|
|
||||||
const QR_CODE_PHRASE_SCAN_MODAL: &'static str = "qr_code_rec_phrase_scan_modal";
|
|
||||||
|
|
||||||
impl ModalContainer for WalletCreation {
|
|
||||||
fn modal_ids(&self) -> &Vec<&'static str> {
|
|
||||||
&self.modal_ids
|
|
||||||
}
|
|
||||||
|
|
||||||
fn modal_ui(&mut self,
|
|
||||||
ui: &mut egui::Ui,
|
|
||||||
modal: &Modal,
|
|
||||||
cb: &dyn PlatformCallbacks) {
|
|
||||||
match modal.id {
|
|
||||||
QR_CODE_PHRASE_SCAN_MODAL => {
|
|
||||||
if let Some(content) = self.scan_modal_content.as_mut() {
|
|
||||||
content.ui(ui, modal, cb, |result| {
|
|
||||||
match result {
|
|
||||||
QrScanResult::Text(text) => {
|
|
||||||
self.mnemonic_setup.mnemonic.import(&text);
|
|
||||||
modal.close();
|
|
||||||
}
|
|
||||||
QrScanResult::SeedQR(text) => {
|
|
||||||
self.mnemonic_setup.mnemonic.import(&text);
|
|
||||||
modal.close();
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WalletCreation {
|
|
||||||
/// Create new wallet creation instance from name and password.
|
|
||||||
pub fn new(name: String, pass: ZeroingString) -> Self {
|
|
||||||
Self {
|
|
||||||
name,
|
|
||||||
pass,
|
|
||||||
step: Step::EnterMnemonic,
|
|
||||||
scan_modal_content: None,
|
|
||||||
mnemonic_setup: MnemonicSetup::default(),
|
|
||||||
network_setup: ConnectionSettings::default(),
|
|
||||||
creation_error: None,
|
|
||||||
modal_ids: vec![
|
|
||||||
QR_CODE_PHRASE_SCAN_MODAL
|
|
||||||
],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Draw wallet creation content.
|
|
||||||
pub fn ui(&mut self,
|
|
||||||
ui: &mut egui::Ui,
|
|
||||||
cb: &dyn PlatformCallbacks,
|
|
||||||
on_create: impl FnMut(Wallet)) {
|
|
||||||
self.current_modal_ui(ui, cb);
|
|
||||||
|
|
||||||
egui::TopBottomPanel::bottom("wallet_creation_step_panel")
|
|
||||||
.frame(egui::Frame {
|
|
||||||
inner_margin: Margin {
|
|
||||||
left: View::far_left_inset_margin(ui) + View::TAB_ITEMS_PADDING,
|
|
||||||
right: View::get_right_inset() + View::TAB_ITEMS_PADDING,
|
|
||||||
top: View::TAB_ITEMS_PADDING,
|
|
||||||
bottom: View::get_bottom_inset() + View::TAB_ITEMS_PADDING,
|
|
||||||
},
|
|
||||||
fill: Colors::fill_deep(),
|
|
||||||
..Default::default()
|
|
||||||
})
|
|
||||||
.show_inside(ui, |ui| {
|
|
||||||
// Draw divider line.
|
|
||||||
let rect = {
|
|
||||||
let mut r = ui.available_rect_before_wrap();
|
|
||||||
r.min.y -= View::TAB_ITEMS_PADDING;
|
|
||||||
r.min.x -= View::far_left_inset_margin(ui) + View::TAB_ITEMS_PADDING;
|
|
||||||
r.max.x += View::get_right_inset() + View::TAB_ITEMS_PADDING;
|
|
||||||
r
|
|
||||||
};
|
|
||||||
View::line(ui, LinePosition::TOP, &rect, Colors::item_stroke());
|
|
||||||
// Show step control content.
|
|
||||||
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
|
|
||||||
self.step_control_ui(ui, on_create, cb);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Show wallet creation step content panel.
|
|
||||||
egui::CentralPanel::default()
|
|
||||||
.frame(egui::Frame {
|
|
||||||
inner_margin: Margin {
|
|
||||||
left: View::far_left_inset_margin(ui) + 4.0,
|
|
||||||
right: View::get_right_inset() + 4.0,
|
|
||||||
top: 3.0,
|
|
||||||
bottom: 4.0,
|
|
||||||
},
|
|
||||||
..Default::default()
|
|
||||||
})
|
|
||||||
.show_inside(ui, |ui| {
|
|
||||||
ScrollArea::vertical()
|
|
||||||
.id_salt(Id::from(format!("creation_step_scroll_{}", self.step.name())))
|
|
||||||
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
|
|
||||||
.auto_shrink([false; 2])
|
|
||||||
.show(ui, |ui| {
|
|
||||||
let max_width = if self.step == Step::SetupConnection {
|
|
||||||
Content::SIDE_PANEL_WIDTH * 1.3
|
|
||||||
} else {
|
|
||||||
Content::SIDE_PANEL_WIDTH * 2.0
|
|
||||||
};
|
|
||||||
View::max_width_ui(ui, max_width, |ui| {
|
|
||||||
self.step_content_ui(ui, cb);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Draw [`Step`] description and confirmation control.
|
|
||||||
fn step_control_ui(&mut self,
|
|
||||||
ui: &mut egui::Ui,
|
|
||||||
on_create: impl FnOnce(Wallet),
|
|
||||||
cb: &dyn PlatformCallbacks) {
|
|
||||||
let step = &self.step;
|
|
||||||
// Setup description and next step availability.
|
|
||||||
let (step_text, mut next) = match step {
|
|
||||||
Step::EnterMnemonic => {
|
|
||||||
let mode = &self.mnemonic_setup.mnemonic.mode();
|
|
||||||
let (text, available) = match mode {
|
|
||||||
PhraseMode::Generate => (t!("wallets.create_phrase_desc"), true),
|
|
||||||
PhraseMode::Import => {
|
|
||||||
let available = !self.mnemonic_setup.mnemonic.has_empty_or_invalid();
|
|
||||||
(t!("wallets.restore_phrase_desc"), available)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
(text, available)
|
|
||||||
}
|
|
||||||
Step::ConfirmMnemonic => {
|
|
||||||
let text = t!("wallets.restore_phrase_desc");
|
|
||||||
let available = !self.mnemonic_setup.mnemonic.has_empty_or_invalid();
|
|
||||||
(text, available)
|
|
||||||
}
|
|
||||||
Step::SetupConnection => {
|
|
||||||
(t!("wallets.setup_conn_desc"), self.creation_error.is_none())
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Show step description or error.
|
|
||||||
let generate_step = step == &Step::EnterMnemonic &&
|
|
||||||
self.mnemonic_setup.mnemonic.mode() == PhraseMode::Generate;
|
|
||||||
if (self.mnemonic_setup.mnemonic.valid() && self.creation_error.is_none()) ||
|
|
||||||
generate_step {
|
|
||||||
ui.label(RichText::new(step_text).size(16.0).color(Colors::gray()));
|
|
||||||
ui.add_space(6.0);
|
|
||||||
} else {
|
|
||||||
next = false;
|
|
||||||
// Show error text.
|
|
||||||
if let Some(err) = &self.creation_error {
|
|
||||||
ui.add_space(10.0);
|
|
||||||
ui.label(RichText::new(err)
|
|
||||||
.size(16.0)
|
|
||||||
.color(Colors::red()));
|
|
||||||
ui.add_space(10.0);
|
|
||||||
} else {
|
|
||||||
ui.label(RichText::new(&t!("wallets.not_valid_phrase"))
|
|
||||||
.size(16.0)
|
|
||||||
.color(Colors::red()));
|
|
||||||
ui.add_space(4.0);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Setup spacing between buttons.
|
|
||||||
ui.style_mut().spacing.item_spacing = egui::vec2(8.0, 0.0);
|
|
||||||
// Setup vertical padding inside button.
|
|
||||||
ui.style_mut().spacing.button_padding = egui::vec2(10.0, 7.0);
|
|
||||||
|
|
||||||
match step {
|
|
||||||
Step::EnterMnemonic => {
|
|
||||||
ui.columns(2, |columns| {
|
|
||||||
// Show copy or paste button for mnemonic phrase step.
|
|
||||||
columns[0].vertical_centered_justified(|ui| {
|
|
||||||
match self.mnemonic_setup.mnemonic.mode() {
|
|
||||||
PhraseMode::Generate => {
|
|
||||||
let c_t = format!("{} {}",
|
|
||||||
COPY,
|
|
||||||
t!("copy").to_uppercase());
|
|
||||||
View::button(ui, c_t, Colors::white_or_black(false), || {
|
|
||||||
cb.copy_string_to_buffer(self.mnemonic_setup
|
|
||||||
.mnemonic
|
|
||||||
.get_phrase());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
PhraseMode::Import => {
|
|
||||||
let p_t = format!("{} {}",
|
|
||||||
CLIPBOARD_TEXT,
|
|
||||||
t!("paste").to_uppercase());
|
|
||||||
View::button(ui, p_t, Colors::white_or_black(false), || {
|
|
||||||
let data = ZeroingString::from(cb.get_string_from_buffer());
|
|
||||||
self.mnemonic_setup.mnemonic.import(&data);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// Show next step or QR code scan button.
|
|
||||||
columns[1].vertical_centered_justified(|ui| {
|
|
||||||
if next {
|
|
||||||
self.next_step_button_ui(ui, on_create);
|
|
||||||
} else {
|
|
||||||
let scan_text = format!("{} {}",
|
|
||||||
SCAN,
|
|
||||||
t!("scan").to_uppercase());
|
|
||||||
View::button(ui, scan_text, Colors::white_or_black(false), || {
|
|
||||||
self.scan_modal_content = Some(CameraScanModal::default());
|
|
||||||
// Show QR code scan modal.
|
|
||||||
Modal::new(QR_CODE_PHRASE_SCAN_MODAL)
|
|
||||||
.position(ModalPosition::CenterTop)
|
|
||||||
.title(t!("scan_qr"))
|
|
||||||
.closeable(false)
|
|
||||||
.show();
|
|
||||||
cb.start_camera();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Step::ConfirmMnemonic => {
|
|
||||||
// Show next step or paste button.
|
|
||||||
if next {
|
|
||||||
self.next_step_button_ui(ui, on_create);
|
|
||||||
} else {
|
|
||||||
let paste_text = format!("{} {}", CLIPBOARD_TEXT, t!("paste").to_uppercase());
|
|
||||||
View::button(ui, paste_text, Colors::white_or_black(false), || {
|
|
||||||
let data = ZeroingString::from(cb.get_string_from_buffer());
|
|
||||||
self.mnemonic_setup.mnemonic.import(&data);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Step::SetupConnection => {
|
|
||||||
if next {
|
|
||||||
self.next_step_button_ui(ui, on_create);
|
|
||||||
ui.add_space(2.0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Draw button to go to next [`Step`].
|
|
||||||
fn next_step_button_ui(&mut self,
|
|
||||||
ui: &mut egui::Ui,
|
|
||||||
on_create: impl FnOnce(Wallet)) {
|
|
||||||
// Setup button text.
|
|
||||||
let (next_text, text_color, bg_color) = if self.step == Step::SetupConnection {
|
|
||||||
(format!("{} {}", CHECK, t!("complete")), Colors::title(true), Colors::gold())
|
|
||||||
} else {
|
|
||||||
(t!("continue"), Colors::green(), Colors::white_or_black(false))
|
|
||||||
};
|
|
||||||
|
|
||||||
// Show next step button.
|
|
||||||
View::colored_text_button_ui(ui, next_text.to_uppercase(), text_color, bg_color, |ui| {
|
|
||||||
self.step = match self.step {
|
|
||||||
Step::EnterMnemonic => {
|
|
||||||
if self.mnemonic_setup.mnemonic.mode() == PhraseMode::Generate {
|
|
||||||
Step::ConfirmMnemonic
|
|
||||||
} else {
|
|
||||||
Step::SetupConnection
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Step::ConfirmMnemonic => {
|
|
||||||
Step::SetupConnection
|
|
||||||
},
|
|
||||||
Step::SetupConnection => {
|
|
||||||
// Create wallet at last step.
|
|
||||||
match Wallet::create(&self.name,
|
|
||||||
&self.pass,
|
|
||||||
&self.mnemonic_setup.mnemonic,
|
|
||||||
&self.network_setup.method) {
|
|
||||||
Ok(w) => {
|
|
||||||
self.mnemonic_setup.reset();
|
|
||||||
// Pass created wallet to callback.
|
|
||||||
(on_create)(w);
|
|
||||||
Step::EnterMnemonic
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
self.creation_error = Some(format!("{:?}", e));
|
|
||||||
Step::SetupConnection
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Check external connections availability on connection setup.
|
|
||||||
if self.step == Step::SetupConnection {
|
|
||||||
ExternalConnection::check(None, ui.ctx());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Draw wallet creation [`Step`] content.
|
|
||||||
fn step_content_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
|
||||||
match &self.step {
|
|
||||||
Step::EnterMnemonic => self.mnemonic_setup.ui(ui, cb),
|
|
||||||
Step::ConfirmMnemonic => self.mnemonic_setup.confirm_ui(ui, cb),
|
|
||||||
Step::SetupConnection => {
|
|
||||||
// Redraw if node is running.
|
|
||||||
if Node::is_running() && !Content::is_dual_panel_mode(ui.ctx()) {
|
|
||||||
ui.ctx().request_repaint_after(Node::STATS_UPDATE_DELAY);
|
|
||||||
}
|
|
||||||
self.network_setup.create_ui(ui, cb)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Back to previous wallet creation [`Step`], return `true` to close creation.
|
|
||||||
pub fn on_back(&mut self) -> bool {
|
|
||||||
match &self.step {
|
|
||||||
Step::ConfirmMnemonic => {
|
|
||||||
self.step = Step::EnterMnemonic;
|
|
||||||
false
|
|
||||||
},
|
|
||||||
Step::SetupConnection => {
|
|
||||||
self.creation_error = None;
|
|
||||||
self.step = Step::EnterMnemonic;
|
|
||||||
false
|
|
||||||
}
|
|
||||||
_ => true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -17,314 +17,307 @@ use egui::{Id, RichText};
|
|||||||
use crate::gui::Colors;
|
use crate::gui::Colors;
|
||||||
use crate::gui::icons::PENCIL;
|
use crate::gui::icons::PENCIL;
|
||||||
use crate::gui::platform::PlatformCallbacks;
|
use crate::gui::platform::PlatformCallbacks;
|
||||||
use crate::gui::views::{Modal, Content, View};
|
use crate::gui::views::types::{ContentContainer, ModalPosition};
|
||||||
use crate::gui::views::types::{ModalContainer, ModalPosition, TextEditOptions};
|
use crate::gui::views::{Content, Modal, TextEdit, View};
|
||||||
use crate::wallet::Mnemonic;
|
use crate::wallet::Mnemonic;
|
||||||
use crate::wallet::types::{PhraseMode, PhraseSize, PhraseWord};
|
use crate::wallet::types::{PhraseMode, PhraseSize, PhraseWord};
|
||||||
|
|
||||||
/// Mnemonic phrase setup content.
|
/// Mnemonic phrase setup content.
|
||||||
pub struct MnemonicSetup {
|
pub struct MnemonicSetup {
|
||||||
/// Current mnemonic phrase.
|
/// Current mnemonic phrase.
|
||||||
pub mnemonic: Mnemonic,
|
pub mnemonic: Mnemonic,
|
||||||
|
|
||||||
/// Current word number to edit at [`Modal`].
|
/// Current word number to edit at [`Modal`].
|
||||||
word_index_edit: usize,
|
word_index_edit: usize,
|
||||||
/// Entered word value for [`Modal`].
|
/// Entered word value for [`Modal`].
|
||||||
word_edit: String,
|
word_edit: String,
|
||||||
/// Flag to check if entered word is valid at [`Modal`].
|
/// Flag to check if entered word is valid at [`Modal`].
|
||||||
valid_word_edit: bool,
|
valid_word_edit: bool,
|
||||||
|
|
||||||
/// [`Modal`] identifiers allowed at this ui container.
|
|
||||||
modal_ids: Vec<&'static str>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Identifier for word input [`Modal`].
|
/// Identifier for word input [`Modal`].
|
||||||
pub const WORD_INPUT_MODAL: &'static str = "word_input_modal";
|
pub const WORD_INPUT_MODAL: &'static str = "word_input_modal";
|
||||||
|
|
||||||
impl Default for MnemonicSetup {
|
impl Default for MnemonicSetup {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
mnemonic: Mnemonic::default(),
|
mnemonic: Mnemonic::default(),
|
||||||
word_index_edit: 0,
|
word_index_edit: 0,
|
||||||
word_edit: String::from(""),
|
word_edit: String::from(""),
|
||||||
valid_word_edit: true,
|
valid_word_edit: true,
|
||||||
modal_ids: vec![
|
}
|
||||||
WORD_INPUT_MODAL
|
}
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ModalContainer for MnemonicSetup {
|
impl ContentContainer for MnemonicSetup {
|
||||||
fn modal_ids(&self) -> &Vec<&'static str> {
|
fn modal_ids(&self) -> Vec<&'static str> {
|
||||||
&self.modal_ids
|
vec![WORD_INPUT_MODAL]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn modal_ui(&mut self,
|
fn modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
|
||||||
ui: &mut egui::Ui,
|
match modal.id {
|
||||||
modal: &Modal,
|
WORD_INPUT_MODAL => self.word_modal_ui(ui, modal, cb),
|
||||||
cb: &dyn PlatformCallbacks) {
|
_ => {}
|
||||||
match modal.id {
|
}
|
||||||
WORD_INPUT_MODAL => self.word_modal_ui(ui, modal, cb),
|
}
|
||||||
_ => {}
|
|
||||||
}
|
fn container_ui(&mut self, _: &mut egui::Ui, _: &dyn PlatformCallbacks) {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MnemonicSetup {
|
impl MnemonicSetup {
|
||||||
/// Draw content for phrase input step.
|
/// Draw content for phrase enter step.
|
||||||
pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
pub fn enter_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||||
// Draw modal content for current ui container.
|
self.ui(ui, cb);
|
||||||
self.current_modal_ui(ui, cb);
|
ui.add_space(10.0);
|
||||||
|
|
||||||
ui.add_space(10.0);
|
// Show mode setup.
|
||||||
|
let mut mode = self.mnemonic.mode();
|
||||||
|
ui.columns(2, |columns| {
|
||||||
|
columns[0].vertical_centered(|ui| {
|
||||||
|
let create_mode = PhraseMode::Generate;
|
||||||
|
let create_text = t!("create");
|
||||||
|
View::radio_value(ui, &mut mode, create_mode, create_text);
|
||||||
|
});
|
||||||
|
columns[1].vertical_centered(|ui| {
|
||||||
|
let import_mode = PhraseMode::Import;
|
||||||
|
let import_text = t!("wallets.recover");
|
||||||
|
View::radio_value(ui, &mut mode, import_mode, import_text);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
if mode != self.mnemonic.mode() {
|
||||||
|
self.mnemonic.set_mode(mode);
|
||||||
|
}
|
||||||
|
|
||||||
// Show mode and type setup.
|
ui.add_space(10.0);
|
||||||
self.mode_type_ui(ui);
|
ui.vertical_centered(|ui| {
|
||||||
|
ui.label(
|
||||||
|
RichText::new(t!("wallets.words_count"))
|
||||||
|
.size(16.0)
|
||||||
|
.color(Colors::gray()),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
ui.add_space(6.0);
|
||||||
|
|
||||||
ui.add_space(12.0);
|
// Show mnemonic phrase size setup.
|
||||||
View::horizontal_line(ui, Colors::item_stroke());
|
let mut size = self.mnemonic.size();
|
||||||
ui.add_space(6.0);
|
ui.columns(5, |columns| {
|
||||||
|
for (index, word) in PhraseSize::VALUES.iter().enumerate() {
|
||||||
|
columns[index].vertical_centered(|ui| {
|
||||||
|
let text = word.value().to_string();
|
||||||
|
View::radio_value(ui, &mut size, word.clone(), text);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if size != self.mnemonic.size() {
|
||||||
|
self.mnemonic.set_size(size);
|
||||||
|
}
|
||||||
|
|
||||||
// Show words setup.
|
ui.add_space(12.0);
|
||||||
self.word_list_ui(ui, self.mnemonic.mode() == PhraseMode::Import, cb);
|
View::horizontal_line(ui, Colors::item_stroke());
|
||||||
}
|
ui.add_space(6.0);
|
||||||
|
|
||||||
/// Draw content for phrase confirmation step.
|
// Show words setup.
|
||||||
pub fn confirm_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
self.word_list_ui(ui, self.mnemonic.mode() == PhraseMode::Import);
|
||||||
// Draw modal content for current ui container.
|
}
|
||||||
self.current_modal_ui(ui, cb);
|
/// Draw content for phrase confirmation step.
|
||||||
|
pub fn confirm_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||||
|
self.ui(ui, cb);
|
||||||
|
|
||||||
ui.add_space(4.0);
|
ui.add_space(4.0);
|
||||||
ui.vertical_centered(|ui| {
|
ui.vertical_centered(|ui| {
|
||||||
let text = format!("{}:", t!("wallets.recovery_phrase"));
|
let text = format!("{}:", t!("wallets.recovery_phrase"));
|
||||||
ui.label(RichText::new(text).size(16.0).color(Colors::gray()));
|
ui.label(RichText::new(text).size(16.0).color(Colors::gray()));
|
||||||
});
|
});
|
||||||
ui.add_space(4.0);
|
ui.add_space(4.0);
|
||||||
self.word_list_ui(ui, true, cb);
|
self.word_list_ui(ui, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Draw mode and size setup.
|
/// Draw grid of words for mnemonic phrase.
|
||||||
fn mode_type_ui(&mut self, ui: &mut egui::Ui) {
|
fn word_list_ui(&mut self, ui: &mut egui::Ui, edit: bool) {
|
||||||
// Show mode setup.
|
ui.add_space(6.0);
|
||||||
let mut mode = self.mnemonic.mode();
|
ui.scope(|ui| {
|
||||||
ui.columns(2, |columns| {
|
// Setup spacing between columns.
|
||||||
columns[0].vertical_centered(|ui| {
|
ui.spacing_mut().item_spacing = egui::Vec2::new(6.0, 6.0);
|
||||||
let create_mode = PhraseMode::Generate;
|
|
||||||
let create_text = t!("create");
|
|
||||||
View::radio_value(ui, &mut mode, create_mode, create_text);
|
|
||||||
});
|
|
||||||
columns[1].vertical_centered(|ui| {
|
|
||||||
let import_mode = PhraseMode::Import;
|
|
||||||
let import_text = t!("wallets.recover");
|
|
||||||
View::radio_value(ui, &mut mode, import_mode, import_text);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
if mode != self.mnemonic.mode() {
|
|
||||||
self.mnemonic.set_mode(mode);
|
|
||||||
}
|
|
||||||
|
|
||||||
ui.add_space(10.0);
|
// Select list of words based on current mode and edit flag.
|
||||||
ui.vertical_centered(|ui| {
|
let words = self.mnemonic.words(edit);
|
||||||
ui.label(RichText::new(t!("wallets.words_count"))
|
|
||||||
.size(16.0)
|
|
||||||
.color(Colors::gray())
|
|
||||||
);
|
|
||||||
});
|
|
||||||
ui.add_space(6.0);
|
|
||||||
|
|
||||||
// Show mnemonic phrase size setup.
|
let mut word_number = 0;
|
||||||
let mut size = self.mnemonic.size();
|
let cols = list_columns_count(ui);
|
||||||
ui.columns(5, |columns| {
|
let _ = words
|
||||||
for (index, word) in PhraseSize::VALUES.iter().enumerate() {
|
.chunks(cols)
|
||||||
columns[index].vertical_centered(|ui| {
|
.map(|chunk| {
|
||||||
let text = word.value().to_string();
|
let size = chunk.len();
|
||||||
View::radio_value(ui, &mut size, word.clone(), text);
|
word_number += 1;
|
||||||
});
|
if size > 1 {
|
||||||
}
|
ui.columns(cols, |columns| {
|
||||||
});
|
columns[0].horizontal(|ui| {
|
||||||
if size != self.mnemonic.size() {
|
let word = chunk.get(0).unwrap();
|
||||||
self.mnemonic.set_size(size);
|
self.word_item_ui(ui, word_number, word, edit);
|
||||||
}
|
});
|
||||||
}
|
columns[1].horizontal(|ui| {
|
||||||
|
word_number += 1;
|
||||||
|
let word = chunk.get(1).unwrap();
|
||||||
|
self.word_item_ui(ui, word_number, word, edit);
|
||||||
|
});
|
||||||
|
if size > 2 {
|
||||||
|
columns[2].horizontal(|ui| {
|
||||||
|
word_number += 1;
|
||||||
|
let word = chunk.get(2).unwrap();
|
||||||
|
self.word_item_ui(ui, word_number, word, edit);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if size > 3 {
|
||||||
|
columns[3].horizontal(|ui| {
|
||||||
|
word_number += 1;
|
||||||
|
let word = chunk.get(3).unwrap();
|
||||||
|
self.word_item_ui(ui, word_number, word, edit);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
ui.columns(cols, |columns| {
|
||||||
|
columns[0].horizontal(|ui| {
|
||||||
|
let word = chunk.get(0).unwrap();
|
||||||
|
self.word_item_ui(ui, word_number, word, edit);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
});
|
||||||
|
ui.add_space(6.0);
|
||||||
|
}
|
||||||
|
|
||||||
/// Draw grid of words for mnemonic phrase.
|
/// Draw word grid item.
|
||||||
fn word_list_ui(&mut self, ui: &mut egui::Ui, edit: bool, cb: &dyn PlatformCallbacks) {
|
fn word_item_ui(&mut self, ui: &mut egui::Ui, num: usize, word: &PhraseWord, edit: bool) {
|
||||||
ui.add_space(6.0);
|
let color = if !word.valid || (word.text.is_empty() && !self.mnemonic.valid()) {
|
||||||
ui.scope(|ui| {
|
Colors::red()
|
||||||
// Setup spacing between columns.
|
} else {
|
||||||
ui.spacing_mut().item_spacing = egui::Vec2::new(6.0, 6.0);
|
Colors::white_or_black(true)
|
||||||
|
};
|
||||||
|
if edit {
|
||||||
|
ui.add_space(6.0);
|
||||||
|
View::button(
|
||||||
|
ui,
|
||||||
|
PENCIL.to_string(),
|
||||||
|
Colors::white_or_black(false),
|
||||||
|
|| {
|
||||||
|
self.word_index_edit = num - 1;
|
||||||
|
self.word_edit = word.text.clone();
|
||||||
|
self.valid_word_edit = word.valid;
|
||||||
|
// Show word edit modal.
|
||||||
|
Modal::new(WORD_INPUT_MODAL)
|
||||||
|
.position(ModalPosition::CenterTop)
|
||||||
|
.title(t!("wallets.recovery_phrase"))
|
||||||
|
.show();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
ui.label(
|
||||||
|
RichText::new(format!("#{} {}", num, word.text))
|
||||||
|
.size(17.0)
|
||||||
|
.color(color),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
ui.add_space(12.0);
|
||||||
|
let text = format!("#{} {}", num, word.text);
|
||||||
|
ui.label(RichText::new(text).size(17.0).color(color));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Select list of words based on current mode and edit flag.
|
/// Reset mnemonic phrase state to default values.
|
||||||
let words = self.mnemonic.words(edit);
|
pub fn reset(&mut self) {
|
||||||
|
self.mnemonic = Mnemonic::default();
|
||||||
|
}
|
||||||
|
|
||||||
let mut word_number = 0;
|
/// Draw word input [`Modal`] content.
|
||||||
let cols = list_columns_count(ui);
|
fn word_modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
|
||||||
let _ = words.chunks(cols).map(|chunk| {
|
let on_save = |c: &mut MnemonicSetup| {
|
||||||
let size = chunk.len();
|
// Insert word checking validity.
|
||||||
word_number += 1;
|
let word = &c.word_edit.trim().to_string();
|
||||||
if size > 1 {
|
c.valid_word_edit = c.mnemonic.insert(c.word_index_edit, word);
|
||||||
ui.columns(cols, |columns| {
|
if !c.valid_word_edit {
|
||||||
columns[0].horizontal(|ui| {
|
return;
|
||||||
let word = chunk.get(0).unwrap();
|
}
|
||||||
self.word_item_ui(ui, word_number, word, edit, cb);
|
// Close modal or go to next word to edit.
|
||||||
});
|
let next_word = c.mnemonic.get(c.word_index_edit + 1);
|
||||||
columns[1].horizontal(|ui| {
|
let close_modal = next_word.is_none()
|
||||||
word_number += 1;
|
|| (!next_word.as_ref().unwrap().text.is_empty() && next_word.unwrap().valid);
|
||||||
let word = chunk.get(1).unwrap();
|
if close_modal {
|
||||||
self.word_item_ui(ui, word_number, word, edit, cb);
|
Modal::close();
|
||||||
});
|
} else {
|
||||||
if size > 2 {
|
c.word_index_edit += 1;
|
||||||
columns[2].horizontal(|ui| {
|
c.word_edit = String::from("");
|
||||||
word_number += 1;
|
}
|
||||||
let word = chunk.get(2).unwrap();
|
};
|
||||||
self.word_item_ui(ui, word_number, word, edit, cb);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if size > 3 {
|
|
||||||
columns[3].horizontal(|ui| {
|
|
||||||
word_number += 1;
|
|
||||||
let word = chunk.get(3).unwrap();
|
|
||||||
self.word_item_ui(ui, word_number, word, edit, cb);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
ui.columns(cols, |columns| {
|
|
||||||
columns[0].horizontal(|ui| {
|
|
||||||
let word = chunk.get(0).unwrap();
|
|
||||||
self.word_item_ui(ui, word_number, word, edit, cb);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}).collect::<Vec<_>>();
|
|
||||||
});
|
|
||||||
ui.add_space(6.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Draw word grid item.
|
ui.add_space(6.0);
|
||||||
fn word_item_ui(&mut self,
|
ui.vertical_centered(|ui| {
|
||||||
ui: &mut egui::Ui,
|
ui.label(
|
||||||
num: usize,
|
RichText::new(t!("wallets.enter_word", "number" => self.word_index_edit + 1))
|
||||||
word: &PhraseWord,
|
.size(17.0)
|
||||||
edit: bool,
|
.color(Colors::gray()),
|
||||||
cb: &dyn PlatformCallbacks) {
|
);
|
||||||
let color = if !word.valid || (word.text.is_empty() && !self.mnemonic.valid()) {
|
ui.add_space(8.0);
|
||||||
Colors::red()
|
|
||||||
} else {
|
|
||||||
Colors::white_or_black(true)
|
|
||||||
};
|
|
||||||
if edit {
|
|
||||||
ui.add_space(6.0);
|
|
||||||
View::button(ui, PENCIL.to_string(), Colors::white_or_black(false), || {
|
|
||||||
self.word_index_edit = num - 1;
|
|
||||||
self.word_edit = word.text.clone();
|
|
||||||
self.valid_word_edit = word.valid;
|
|
||||||
// Show word edit modal.
|
|
||||||
Modal::new(WORD_INPUT_MODAL)
|
|
||||||
.position(ModalPosition::CenterTop)
|
|
||||||
.title(t!("wallets.recovery_phrase"))
|
|
||||||
.show();
|
|
||||||
cb.show_keyboard();
|
|
||||||
});
|
|
||||||
ui.label(RichText::new(format!("#{} {}", num, word.text))
|
|
||||||
.size(17.0)
|
|
||||||
.color(color));
|
|
||||||
} else {
|
|
||||||
ui.add_space(12.0);
|
|
||||||
let text = format!("#{} {}", num, word.text);
|
|
||||||
ui.label(RichText::new(text).size(17.0).color(color));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Reset mnemonic phrase state to default values.
|
// Draw word value text edit.
|
||||||
pub fn reset(&mut self) {
|
let mut word_edit = TextEdit::new(Id::from(modal.id).with(self.word_index_edit));
|
||||||
self.mnemonic = Mnemonic::default();
|
word_edit.ui(ui, &mut self.word_edit, cb);
|
||||||
}
|
if word_edit.enter_pressed {
|
||||||
|
on_save(self);
|
||||||
|
}
|
||||||
|
|
||||||
/// Draw word input [`Modal`] content.
|
// Show error when specified word is not valid.
|
||||||
fn word_modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
|
if !self.valid_word_edit {
|
||||||
ui.add_space(6.0);
|
ui.add_space(12.0);
|
||||||
ui.vertical_centered(|ui| {
|
ui.label(
|
||||||
ui.label(RichText::new(t!("wallets.enter_word", "number" => self.word_index_edit + 1))
|
RichText::new(t!("wallets.not_valid_word"))
|
||||||
.size(17.0)
|
.size(17.0)
|
||||||
.color(Colors::gray()));
|
.color(Colors::red()),
|
||||||
ui.add_space(8.0);
|
);
|
||||||
|
}
|
||||||
|
ui.add_space(12.0);
|
||||||
|
});
|
||||||
|
|
||||||
// Draw word value text edit.
|
// Show modal buttons.
|
||||||
let mut text_edit_opts = TextEditOptions::new(
|
ui.scope(|ui| {
|
||||||
Id::from(modal.id).with(self.word_index_edit)
|
// Setup spacing between buttons.
|
||||||
);
|
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
|
||||||
View::text_edit(ui, cb, &mut self.word_edit, &mut text_edit_opts);
|
|
||||||
|
|
||||||
// Show error when specified word is not valid.
|
ui.columns(2, |columns| {
|
||||||
if !self.valid_word_edit {
|
columns[0].vertical_centered_justified(|ui| {
|
||||||
ui.add_space(12.0);
|
View::button(
|
||||||
ui.label(RichText::new(t!("wallets.not_valid_word"))
|
ui,
|
||||||
.size(17.0)
|
t!("modal.cancel"),
|
||||||
.color(Colors::red()));
|
Colors::white_or_black(false),
|
||||||
}
|
|| {
|
||||||
ui.add_space(12.0);
|
// Close modal.
|
||||||
});
|
Modal::close();
|
||||||
|
},
|
||||||
// Show modal buttons.
|
);
|
||||||
ui.scope(|ui| {
|
});
|
||||||
// Setup spacing between buttons.
|
columns[1].vertical_centered_justified(|ui| {
|
||||||
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
|
// Show save button.
|
||||||
|
View::button(ui, t!("continue"), Colors::white_or_black(false), || {
|
||||||
ui.columns(2, |columns| {
|
on_save(self);
|
||||||
columns[0].vertical_centered_justified(|ui| {
|
});
|
||||||
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
|
});
|
||||||
// Close modal.
|
});
|
||||||
cb.hide_keyboard();
|
ui.add_space(6.0);
|
||||||
modal.close();
|
});
|
||||||
});
|
}
|
||||||
});
|
|
||||||
columns[1].vertical_centered_justified(|ui| {
|
|
||||||
// Callback to save the word.
|
|
||||||
let mut save = || {
|
|
||||||
// Insert word checking validity.
|
|
||||||
let word = &self.word_edit.trim().to_string();
|
|
||||||
self.valid_word_edit = self.mnemonic.insert(self.word_index_edit, word);
|
|
||||||
if !self.valid_word_edit {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Close modal or go to next word to edit.
|
|
||||||
let next_word = self.mnemonic.get(self.word_index_edit + 1);
|
|
||||||
let close_modal = next_word.is_none() ||
|
|
||||||
(!next_word.as_ref().unwrap().text.is_empty() &&
|
|
||||||
next_word.unwrap().valid);
|
|
||||||
if close_modal {
|
|
||||||
cb.hide_keyboard();
|
|
||||||
modal.close();
|
|
||||||
} else {
|
|
||||||
self.word_index_edit += 1;
|
|
||||||
self.word_edit = String::from("");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
// Call save on Enter key press.
|
|
||||||
View::on_enter_key(ui, || {
|
|
||||||
(save)();
|
|
||||||
});
|
|
||||||
// Show save button.
|
|
||||||
View::button(ui, t!("continue"), Colors::white_or_black(false), save);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
ui.add_space(6.0);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Calculate word list columns count based on available ui width.
|
/// Calculate word list columns count based on available ui width.
|
||||||
fn list_columns_count(ui: &mut egui::Ui) -> usize {
|
fn list_columns_count(ui: &mut egui::Ui) -> usize {
|
||||||
let w = ui.available_width();
|
let w = ui.available_width();
|
||||||
let min_panel_w = Content::SIDE_PANEL_WIDTH - 12.0;
|
let min_panel_w = Content::SIDE_PANEL_WIDTH - 12.0;
|
||||||
let double_min_panel_w = min_panel_w * 2.0;
|
let double_min_panel_w = min_panel_w * 2.0;
|
||||||
if w >= min_panel_w * 1.5 && w < double_min_panel_w {
|
if w >= min_panel_w * 1.5 && w < double_min_panel_w {
|
||||||
3
|
3
|
||||||
} else if w >= double_min_panel_w {
|
} else if w >= double_min_panel_w {
|
||||||
4
|
4
|
||||||
} else {
|
} else {
|
||||||
2
|
2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
mod mnemonic;
|
mod mnemonic;
|
||||||
pub use mnemonic::MnemonicSetup;
|
pub use mnemonic::MnemonicSetup;
|
||||||
|
|
||||||
mod creation;
|
mod content;
|
||||||
pub use creation::WalletCreation;
|
pub use content::WalletCreationContent;
|
||||||
|
|
||||||
pub mod types;
|
pub mod types;
|
||||||
|
|||||||
@@ -15,21 +15,21 @@
|
|||||||
/// Wallet creation step.
|
/// Wallet creation step.
|
||||||
#[derive(PartialEq, Clone)]
|
#[derive(PartialEq, Clone)]
|
||||||
pub enum Step {
|
pub enum Step {
|
||||||
/// Mnemonic phrase input.
|
/// Mnemonic phrase input.
|
||||||
EnterMnemonic,
|
EnterMnemonic,
|
||||||
/// Mnemonic phrase confirmation.
|
/// Mnemonic phrase confirmation.
|
||||||
ConfirmMnemonic,
|
ConfirmMnemonic,
|
||||||
/// Wallet connection setup.
|
/// Wallet connection setup.
|
||||||
SetupConnection
|
SetupConnection,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Step {
|
impl Step {
|
||||||
/// Short name representing creation step.
|
/// Short name representing creation step.
|
||||||
pub fn name(&self) -> String {
|
pub fn name(&self) -> String {
|
||||||
match *self {
|
match *self {
|
||||||
Step::EnterMnemonic => "enter_phrase".to_owned(),
|
Step::EnterMnemonic => "enter_phrase".to_owned(),
|
||||||
Step::ConfirmMnemonic => "confirm_phrase".to_owned(),
|
Step::ConfirmMnemonic => "confirm_phrase".to_owned(),
|
||||||
Step::SetupConnection => "setup_conn".to_owned(),
|
Step::SetupConnection => "setup_conn".to_owned(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,11 +12,11 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
pub mod modals;
|
|
||||||
mod creation;
|
mod creation;
|
||||||
|
pub mod modals;
|
||||||
|
|
||||||
mod content;
|
mod content;
|
||||||
pub use content::*;
|
pub use content::*;
|
||||||
|
|
||||||
mod wallet;
|
mod wallet;
|
||||||
use wallet::*;
|
use wallet::*;
|
||||||
|
|||||||
@@ -17,100 +17,102 @@ use grin_util::ZeroingString;
|
|||||||
|
|
||||||
use crate::gui::Colors;
|
use crate::gui::Colors;
|
||||||
use crate::gui::platform::PlatformCallbacks;
|
use crate::gui::platform::PlatformCallbacks;
|
||||||
use crate::gui::views::{Modal, View};
|
use crate::gui::views::{Modal, TextEdit, View};
|
||||||
use crate::gui::views::types::TextEditOptions;
|
|
||||||
|
|
||||||
/// Initial wallet creation [`Modal`] content.
|
/// Initial wallet creation [`Modal`] content.
|
||||||
pub struct AddWalletModal {
|
pub struct AddWalletModal {
|
||||||
/// Flag to check if it's first draw to focus on first field.
|
/// Wallet name.
|
||||||
first_draw: bool,
|
pub name_edit: String,
|
||||||
/// Wallet name.
|
/// Password to encrypt created wallet.
|
||||||
pub name_edit: String,
|
pub pass_edit: String,
|
||||||
/// Password to encrypt created wallet.
|
|
||||||
pub pass_edit: String,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for AddWalletModal {
|
impl Default for AddWalletModal {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
first_draw: true,
|
name_edit: t!("wallets.default_wallet").into(),
|
||||||
name_edit: t!("wallets.default_wallet"),
|
pass_edit: "".to_string(),
|
||||||
pass_edit: "".to_string(),
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AddWalletModal {
|
impl AddWalletModal {
|
||||||
/// Draw creating wallet name/password input [`Modal`] content.
|
/// Draw creating wallet name/password input [`Modal`] content.
|
||||||
pub fn ui(&mut self,
|
pub fn ui(
|
||||||
ui: &mut egui::Ui,
|
&mut self,
|
||||||
modal: &Modal,
|
ui: &mut egui::Ui,
|
||||||
cb: &dyn PlatformCallbacks,
|
modal: &Modal,
|
||||||
mut on_input: impl FnMut(String, ZeroingString)) {
|
cb: &dyn PlatformCallbacks,
|
||||||
ui.add_space(6.0);
|
mut on_input: impl FnMut(String, ZeroingString),
|
||||||
ui.vertical_centered(|ui| {
|
) {
|
||||||
ui.label(RichText::new(t!("wallets.name"))
|
let mut on_next = |m: &mut AddWalletModal| {
|
||||||
.size(17.0)
|
let name = m.name_edit.clone();
|
||||||
.color(Colors::gray()));
|
let pass = m.pass_edit.clone();
|
||||||
ui.add_space(8.0);
|
if name.is_empty() || pass.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Modal::close();
|
||||||
|
on_input(name, ZeroingString::from(pass));
|
||||||
|
};
|
||||||
|
ui.vertical_centered(|ui| {
|
||||||
|
ui.add_space(6.0);
|
||||||
|
ui.label(
|
||||||
|
RichText::new(t!("wallets.name"))
|
||||||
|
.size(17.0)
|
||||||
|
.color(Colors::gray()),
|
||||||
|
);
|
||||||
|
ui.add_space(8.0);
|
||||||
|
|
||||||
// Show wallet name text edit.
|
// Show wallet name text edit.
|
||||||
let mut name_edit_opts = TextEditOptions::new(Id::from(modal.id).with("name"))
|
let mut name_input = TextEdit::new(Id::from(modal.id).with("name")).focus(false);
|
||||||
.no_focus();
|
name_input.ui(ui, &mut self.name_edit, cb);
|
||||||
if self.first_draw {
|
|
||||||
self.first_draw = false;
|
|
||||||
name_edit_opts.focus = true;
|
|
||||||
}
|
|
||||||
View::text_edit(ui, cb, &mut self.name_edit, &mut name_edit_opts);
|
|
||||||
ui.add_space(8.0);
|
|
||||||
|
|
||||||
ui.label(RichText::new(t!("wallets.pass"))
|
ui.add_space(8.0);
|
||||||
.size(17.0)
|
ui.label(
|
||||||
.color(Colors::gray()));
|
RichText::new(t!("wallets.pass"))
|
||||||
ui.add_space(8.0);
|
.size(17.0)
|
||||||
|
.color(Colors::gray()),
|
||||||
|
);
|
||||||
|
ui.add_space(8.0);
|
||||||
|
|
||||||
// Draw wallet password text edit.
|
// Show wallet password text edit.
|
||||||
let mut pass_text_edit_opts = TextEditOptions::new(Id::from(modal.id).with("pass"))
|
let mut pass_input = TextEdit::new(Id::from(modal.id).with("pass"))
|
||||||
.password()
|
.password()
|
||||||
.no_focus();
|
.focus(Modal::first_draw());
|
||||||
View::text_edit(ui, cb, &mut self.pass_edit, &mut pass_text_edit_opts);
|
if name_input.enter_pressed {
|
||||||
ui.add_space(12.0);
|
pass_input.focus_request();
|
||||||
});
|
}
|
||||||
|
pass_input.ui(ui, &mut self.pass_edit, cb);
|
||||||
|
if pass_input.enter_pressed {
|
||||||
|
on_next(self);
|
||||||
|
}
|
||||||
|
ui.add_space(12.0);
|
||||||
|
});
|
||||||
|
|
||||||
// Show modal buttons.
|
// Show modal buttons.
|
||||||
ui.scope(|ui| {
|
ui.scope(|ui| {
|
||||||
// Setup spacing between buttons.
|
// Setup spacing between buttons.
|
||||||
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
|
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
|
||||||
|
|
||||||
ui.columns(2, |columns| {
|
ui.columns(2, |columns| {
|
||||||
columns[0].vertical_centered_justified(|ui| {
|
columns[0].vertical_centered_justified(|ui| {
|
||||||
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
|
View::button(
|
||||||
// Close modal.
|
ui,
|
||||||
cb.hide_keyboard();
|
t!("modal.cancel"),
|
||||||
modal.close();
|
Colors::white_or_black(false),
|
||||||
});
|
|| {
|
||||||
});
|
// Close modal.
|
||||||
columns[1].vertical_centered_justified(|ui| {
|
Modal::close();
|
||||||
let mut on_next = || {
|
},
|
||||||
let name = self.name_edit.clone();
|
);
|
||||||
let pass = self.pass_edit.clone();
|
});
|
||||||
if name.is_empty() || pass.is_empty() {
|
columns[1].vertical_centered_justified(|ui| {
|
||||||
return;
|
View::button(ui, t!("continue"), Colors::white_or_black(false), || {
|
||||||
}
|
on_next(self);
|
||||||
cb.hide_keyboard();
|
});
|
||||||
modal.close();
|
});
|
||||||
on_input(name, ZeroingString::from(pass));
|
});
|
||||||
};
|
ui.add_space(6.0);
|
||||||
|
});
|
||||||
// Go to next creation step on Enter button press.
|
}
|
||||||
View::on_enter_key(ui, || {
|
}
|
||||||
(on_next)();
|
|
||||||
});
|
|
||||||
|
|
||||||
View::button(ui, t!("continue"), Colors::white_or_black(false), on_next);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
ui.add_space(6.0);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,141 @@
|
|||||||
|
// Copyright 2026 The Grim Developers
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
use egui::scroll_area::ScrollBarVisibility;
|
||||||
|
use egui::{Id, OpenUrl, RichText, ScrollArea};
|
||||||
|
|
||||||
|
use crate::gui::Colors;
|
||||||
|
use crate::gui::icons::{BRACKETS_CURLY, GITHUB_LOGO, TELEGRAM_LOGO};
|
||||||
|
use crate::gui::views::{Modal, View};
|
||||||
|
|
||||||
|
/// Application release changelog content.
|
||||||
|
pub struct ChangelogContent {
|
||||||
|
/// Changelog text.
|
||||||
|
changelog: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Endpoint for GitHub repository.
|
||||||
|
const GITHUB_URL: &'static str = "https://github.com/GetGrin/grim";
|
||||||
|
/// Endpoint for Telegram releases channel.
|
||||||
|
const TELEGRAM_URL: &'static str = "https://t.me/grim_releases";
|
||||||
|
/// Endpoint for git repository.
|
||||||
|
const GIT_URL: &'static str = "https://code.gri.mw/GUI/grim";
|
||||||
|
|
||||||
|
impl ChangelogContent {
|
||||||
|
/// Create new content instance.
|
||||||
|
pub fn new(changelog: String) -> Self {
|
||||||
|
Self { changelog }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Identifier for [`Modal`].
|
||||||
|
pub const MODAL_ID: &'static str = "release_changelog_modal";
|
||||||
|
|
||||||
|
/// Draw changelog [`Modal`] content.
|
||||||
|
pub fn ui(&mut self, ui: &mut egui::Ui) {
|
||||||
|
ui.add_space(6.0);
|
||||||
|
ui.vertical_centered(|ui| {
|
||||||
|
ui.label(
|
||||||
|
RichText::new(t!("changelog"))
|
||||||
|
.size(16.0)
|
||||||
|
.color(Colors::gray()),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
ui.add_space(6.0);
|
||||||
|
|
||||||
|
// Show changelog text.
|
||||||
|
ui.vertical_centered(|ui| {
|
||||||
|
let scroll_id = Id::from("release_changelog");
|
||||||
|
View::horizontal_line(ui, Colors::item_stroke());
|
||||||
|
ui.add_space(3.0);
|
||||||
|
ScrollArea::vertical()
|
||||||
|
.id_salt(scroll_id)
|
||||||
|
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
|
||||||
|
.max_height(128.0)
|
||||||
|
.auto_shrink([false; 2])
|
||||||
|
.show(ui, |ui| {
|
||||||
|
ui.add_space(7.0);
|
||||||
|
let input_id = scroll_id.with("_input");
|
||||||
|
egui::TextEdit::multiline(&mut self.changelog)
|
||||||
|
.id(input_id)
|
||||||
|
.font(egui::TextStyle::Small)
|
||||||
|
.desired_rows(5)
|
||||||
|
.interactive(false)
|
||||||
|
.desired_width(f32::INFINITY)
|
||||||
|
.show(ui);
|
||||||
|
ui.add_space(6.0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
ui.add_space(2.0);
|
||||||
|
View::horizontal_line(ui, Colors::item_stroke());
|
||||||
|
ui.add_space(8.0);
|
||||||
|
|
||||||
|
// Setup spacing between buttons.
|
||||||
|
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
|
||||||
|
|
||||||
|
ui.columns(3, |columns| {
|
||||||
|
columns[0].vertical_centered_justified(|ui| {
|
||||||
|
// Draw button to open GitHub link.
|
||||||
|
let mut github_clicked = false;
|
||||||
|
View::button(ui, GITHUB_LOGO, Colors::white_or_black(false), || {
|
||||||
|
github_clicked = true;
|
||||||
|
});
|
||||||
|
if github_clicked {
|
||||||
|
ui.ctx().open_url(OpenUrl {
|
||||||
|
url: GITHUB_URL.into(),
|
||||||
|
new_tab: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
columns[1].vertical_centered_justified(|ui| {
|
||||||
|
// Draw button to open Telegram link.
|
||||||
|
let mut tg_clicked = false;
|
||||||
|
View::button(ui, TELEGRAM_LOGO, Colors::white_or_black(false), || {
|
||||||
|
tg_clicked = true;
|
||||||
|
});
|
||||||
|
if tg_clicked {
|
||||||
|
ui.ctx().open_url(OpenUrl {
|
||||||
|
url: TELEGRAM_URL.into(),
|
||||||
|
new_tab: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
columns[2].vertical_centered_justified(|ui| {
|
||||||
|
// Draw button to open repository link.
|
||||||
|
let mut git_clicked = false;
|
||||||
|
View::button(ui, BRACKETS_CURLY, Colors::white_or_black(false), || {
|
||||||
|
git_clicked = true;
|
||||||
|
});
|
||||||
|
if git_clicked {
|
||||||
|
ui.ctx().open_url(OpenUrl {
|
||||||
|
url: GIT_URL.into(),
|
||||||
|
new_tab: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
ui.add_space(8.0);
|
||||||
|
View::horizontal_line(ui, Colors::item_stroke());
|
||||||
|
ui.add_space(8.0);
|
||||||
|
|
||||||
|
// Show button to close modal.
|
||||||
|
ui.vertical_centered_justified(|ui| {
|
||||||
|
View::button(ui, t!("close"), Colors::white_or_black(false), || {
|
||||||
|
Modal::close();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
ui.add_space(6.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,151 +0,0 @@
|
|||||||
// Copyright 2024 The Grim Developers
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
use egui::{RichText, ScrollArea};
|
|
||||||
use egui::scroll_area::ScrollBarVisibility;
|
|
||||||
|
|
||||||
use crate::gui::Colors;
|
|
||||||
use crate::gui::icons::{CHECK, CHECK_FAT, PLUS_CIRCLE};
|
|
||||||
use crate::gui::platform::PlatformCallbacks;
|
|
||||||
use crate::gui::views::{Modal, View};
|
|
||||||
use crate::gui::views::network::ConnectionsContent;
|
|
||||||
use crate::gui::views::network::modals::ExternalConnectionModal;
|
|
||||||
use crate::wallet::ConnectionsConfig;
|
|
||||||
use crate::wallet::types::ConnectionMethod;
|
|
||||||
|
|
||||||
/// Wallet connection selection [`Modal`] content.
|
|
||||||
pub struct WalletConnectionModal {
|
|
||||||
/// Current connection method.
|
|
||||||
pub conn: ConnectionMethod,
|
|
||||||
|
|
||||||
/// External connection content.
|
|
||||||
ext_conn_content: Option<ExternalConnectionModal>
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WalletConnectionModal {
|
|
||||||
/// Create from provided wallet connection.
|
|
||||||
pub fn new(conn: ConnectionMethod) -> Self {
|
|
||||||
Self {
|
|
||||||
conn,
|
|
||||||
ext_conn_content: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Draw [`Modal`] content.
|
|
||||||
pub fn ui(&mut self,
|
|
||||||
ui: &mut egui::Ui,
|
|
||||||
modal: &Modal,
|
|
||||||
cb: &dyn PlatformCallbacks,
|
|
||||||
on_select: impl Fn(ConnectionMethod)) {
|
|
||||||
// Draw external connection content.
|
|
||||||
if let Some(ext_content) = self.ext_conn_content.as_mut() {
|
|
||||||
ext_content.ui(ui, cb, modal, |conn| {
|
|
||||||
on_select(ConnectionMethod::External(conn.id, conn.url));
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ui.add_space(4.0);
|
|
||||||
|
|
||||||
let ext_conn_list = ConnectionsConfig::ext_conn_list();
|
|
||||||
ScrollArea::vertical()
|
|
||||||
.max_height(if ext_conn_list.len() < 4 {
|
|
||||||
330.0
|
|
||||||
} else {
|
|
||||||
350.0
|
|
||||||
})
|
|
||||||
.id_salt("connections_scroll")
|
|
||||||
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
|
|
||||||
.auto_shrink([true; 2])
|
|
||||||
.show(ui, |ui| {
|
|
||||||
ui.add_space(2.0);
|
|
||||||
|
|
||||||
// Show integrated node selection.
|
|
||||||
ConnectionsContent::integrated_node_item_ui(ui, |ui| {
|
|
||||||
match self.conn {
|
|
||||||
ConnectionMethod::Integrated => {
|
|
||||||
ui.add_space(14.0);
|
|
||||||
ui.label(RichText::new(CHECK_FAT).size(20.0).color(Colors::green()));
|
|
||||||
ui.add_space(14.0);
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
View::item_button(ui, View::item_rounding(0, 1, true), CHECK, None, || {
|
|
||||||
on_select(ConnectionMethod::Integrated);
|
|
||||||
modal.close();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
ui.add_space(8.0);
|
|
||||||
ui.vertical_centered(|ui| {
|
|
||||||
ui.label(RichText::new(t!("wallets.ext_conn"))
|
|
||||||
.size(16.0)
|
|
||||||
.color(Colors::gray()));
|
|
||||||
ui.add_space(6.0);
|
|
||||||
// Show button to add new external node connection.
|
|
||||||
let add_node_text = format!("{} {}", PLUS_CIRCLE, t!("wallets.add_node"));
|
|
||||||
View::button(ui, add_node_text, Colors::white_or_black(false), || {
|
|
||||||
self.ext_conn_content = Some(ExternalConnectionModal::new(None));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
ui.add_space(4.0);
|
|
||||||
|
|
||||||
if !ext_conn_list.is_empty() {
|
|
||||||
ui.add_space(8.0);
|
|
||||||
for (index, conn) in ext_conn_list.iter().filter(|c| !c.deleted).enumerate() {
|
|
||||||
if conn.deleted {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
ui.horizontal_wrapped(|ui| {
|
|
||||||
let len = ext_conn_list.len();
|
|
||||||
ConnectionsContent::ext_conn_item_ui(ui, conn, index, len, |ui| {
|
|
||||||
let current_ext_conn = match self.conn {
|
|
||||||
ConnectionMethod::Integrated => false,
|
|
||||||
ConnectionMethod::External(id, _) => id == conn.id
|
|
||||||
};
|
|
||||||
if !current_ext_conn {
|
|
||||||
let button_rounding = View::item_rounding(index, len, true);
|
|
||||||
View::item_button(ui, button_rounding, CHECK, None, || {
|
|
||||||
on_select(
|
|
||||||
ConnectionMethod::External(conn.id, conn.url.clone())
|
|
||||||
);
|
|
||||||
modal.close();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
ui.add_space(12.0);
|
|
||||||
ui.label(RichText::new(CHECK_FAT)
|
|
||||||
.size(20.0)
|
|
||||||
.color(Colors::green()));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ui.add_space(4.0);
|
|
||||||
});
|
|
||||||
|
|
||||||
ui.add_space(2.0);
|
|
||||||
View::horizontal_line(ui, Colors::item_stroke());
|
|
||||||
ui.add_space(6.0);
|
|
||||||
|
|
||||||
// Show button to close modal.
|
|
||||||
ui.vertical_centered_justified(|ui| {
|
|
||||||
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
|
|
||||||
modal.close();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
ui.add_space(6.0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,176 @@
|
|||||||
|
// Copyright 2024 The Grim Developers
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
use egui::scroll_area::ScrollBarVisibility;
|
||||||
|
use egui::{Align, Layout, RichText, ScrollArea, StrokeKind};
|
||||||
|
|
||||||
|
use crate::gui::Colors;
|
||||||
|
use crate::gui::icons::{CHECK, COMPUTER_TOWER, FOLDER_OPEN, GLOBE_SIMPLE, PLUGS_CONNECTED};
|
||||||
|
use crate::gui::views::wallets::wallet::types::wallet_status_text;
|
||||||
|
use crate::gui::views::{Modal, View};
|
||||||
|
use crate::wallet::types::ConnectionMethod;
|
||||||
|
use crate::wallet::{Wallet, WalletList};
|
||||||
|
|
||||||
|
/// Wallet list [`Modal`] content
|
||||||
|
pub struct WalletListModal {
|
||||||
|
/// Selected wallet id.
|
||||||
|
selected_id: Option<i64>,
|
||||||
|
|
||||||
|
/// Optional data to pass after wallet selection.
|
||||||
|
data: Option<String>,
|
||||||
|
|
||||||
|
/// Flag to check if wallet can be opened from the list.
|
||||||
|
can_open: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WalletListModal {
|
||||||
|
/// Create new content instance.
|
||||||
|
pub fn new(selected_id: Option<i64>, data: Option<String>, can_open: bool) -> Self {
|
||||||
|
Self {
|
||||||
|
selected_id,
|
||||||
|
data,
|
||||||
|
can_open,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw content.
|
||||||
|
pub fn ui(
|
||||||
|
&mut self,
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
wallets: &WalletList,
|
||||||
|
mut on_select: impl FnMut(Wallet, Option<String>),
|
||||||
|
) {
|
||||||
|
ui.add_space(4.0);
|
||||||
|
ScrollArea::vertical()
|
||||||
|
.max_height(373.0)
|
||||||
|
.id_salt("select_wallet_list_scroll")
|
||||||
|
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
|
||||||
|
.auto_shrink([true; 2])
|
||||||
|
.show(ui, |ui| {
|
||||||
|
ui.add_space(2.0);
|
||||||
|
ui.vertical_centered(|ui| {
|
||||||
|
let data = self.data.clone();
|
||||||
|
for wallet in wallets.list() {
|
||||||
|
// Draw wallet list item.
|
||||||
|
self.wallet_item_ui(ui, wallet, || {
|
||||||
|
Modal::close();
|
||||||
|
on_select(wallet.clone(), data.clone());
|
||||||
|
});
|
||||||
|
ui.add_space(5.0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
ui.add_space(2.0);
|
||||||
|
View::horizontal_line(ui, Colors::item_stroke());
|
||||||
|
ui.add_space(6.0);
|
||||||
|
|
||||||
|
// Show button to close modal.
|
||||||
|
ui.vertical_centered_justified(|ui| {
|
||||||
|
View::button(
|
||||||
|
ui,
|
||||||
|
t!("modal.cancel"),
|
||||||
|
Colors::white_or_black(false),
|
||||||
|
|| {
|
||||||
|
self.data = None;
|
||||||
|
Modal::close();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
ui.add_space(6.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw wallet list item with provided callback on select.
|
||||||
|
fn wallet_item_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, on_select: impl FnOnce()) {
|
||||||
|
let config = wallet.get_config();
|
||||||
|
let id = config.id;
|
||||||
|
|
||||||
|
// Draw round background.
|
||||||
|
let mut rect = ui.available_rect_before_wrap();
|
||||||
|
rect.set_height(78.0);
|
||||||
|
let rounding = View::item_rounding(0, 1, false);
|
||||||
|
ui.painter().rect(
|
||||||
|
rect,
|
||||||
|
rounding,
|
||||||
|
Colors::fill(),
|
||||||
|
View::hover_stroke(),
|
||||||
|
StrokeKind::Outside,
|
||||||
|
);
|
||||||
|
|
||||||
|
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
|
||||||
|
if self.can_open {
|
||||||
|
// Show button to select or open closed wallet.
|
||||||
|
let icon = if wallet.is_open() { CHECK } else { FOLDER_OPEN };
|
||||||
|
View::item_button(ui, View::item_rounding(0, 1, true), icon, None, || {
|
||||||
|
on_select();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Draw button to select wallet.
|
||||||
|
if self.selected_id.unwrap_or(0) == id {
|
||||||
|
View::selected_item_check(ui);
|
||||||
|
} else {
|
||||||
|
View::item_button(ui, View::item_rounding(0, 1, true), CHECK, None, || {
|
||||||
|
on_select();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let layout_size = ui.available_size();
|
||||||
|
ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| {
|
||||||
|
ui.add_space(6.0);
|
||||||
|
ui.vertical(|ui| {
|
||||||
|
ui.add_space(3.0);
|
||||||
|
// Show wallet name text.
|
||||||
|
ui.with_layout(Layout::left_to_right(Align::Min), |ui| {
|
||||||
|
ui.add_space(1.0);
|
||||||
|
View::ellipsize_text(ui, config.name, 18.0, Colors::title(false));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Show wallet connection text.
|
||||||
|
let connection = wallet.get_current_connection();
|
||||||
|
let conn_text = match connection {
|
||||||
|
ConnectionMethod::Integrated => {
|
||||||
|
format!("{} {}", COMPUTER_TOWER, t!("network.node"))
|
||||||
|
}
|
||||||
|
ConnectionMethod::External(_, url) => format!("{} {}", GLOBE_SIMPLE, url),
|
||||||
|
};
|
||||||
|
ui.label(
|
||||||
|
RichText::new(conn_text)
|
||||||
|
.size(15.0)
|
||||||
|
.color(Colors::text(false)),
|
||||||
|
);
|
||||||
|
ui.add_space(1.0);
|
||||||
|
|
||||||
|
// Show wallet API text or open status.
|
||||||
|
if self.can_open {
|
||||||
|
ui.label(
|
||||||
|
RichText::new(wallet_status_text(wallet))
|
||||||
|
.size(15.0)
|
||||||
|
.color(Colors::gray()),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
let address = if let Some(port) = config.api_port {
|
||||||
|
format!("127.0.0.1:{}", port)
|
||||||
|
} else {
|
||||||
|
"-".to_string()
|
||||||
|
};
|
||||||
|
let api_text = format!("{} {}", PLUGS_CONNECTED, address);
|
||||||
|
ui.label(RichText::new(api_text).size(15.0).color(Colors::gray()));
|
||||||
|
}
|
||||||
|
ui.add_space(3.0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,14 +12,17 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
mod conn;
|
mod settings;
|
||||||
pub use conn::*;
|
pub use settings::*;
|
||||||
|
|
||||||
mod wallets;
|
mod list;
|
||||||
pub use wallets::*;
|
pub use list::*;
|
||||||
|
|
||||||
mod open;
|
mod open;
|
||||||
pub use open::*;
|
pub use open::*;
|
||||||
|
|
||||||
mod add;
|
mod add;
|
||||||
pub use add::*;
|
pub use add::*;
|
||||||
|
|
||||||
|
mod changelog;
|
||||||
|
pub use changelog::*;
|
||||||
|
|||||||
@@ -17,107 +17,105 @@ use grin_util::ZeroingString;
|
|||||||
|
|
||||||
use crate::gui::Colors;
|
use crate::gui::Colors;
|
||||||
use crate::gui::platform::PlatformCallbacks;
|
use crate::gui::platform::PlatformCallbacks;
|
||||||
use crate::gui::views::{Modal, View};
|
use crate::gui::views::{Modal, TextEdit, View};
|
||||||
use crate::gui::views::types::TextEditOptions;
|
|
||||||
use crate::wallet::Wallet;
|
|
||||||
|
|
||||||
/// Wallet opening [`Modal`] content.
|
/// Wallet opening [`Modal`] content.
|
||||||
pub struct OpenWalletModal {
|
pub struct OpenWalletModal {
|
||||||
/// Wallet to open.
|
/// Password to open wallet.
|
||||||
wallet: Wallet,
|
pass_edit: String,
|
||||||
|
/// Flag to check if wrong password was entered.
|
||||||
/// Password to open wallet.
|
wrong_pass: bool,
|
||||||
pass_edit: String,
|
|
||||||
/// Flag to check if wrong password was entered.
|
|
||||||
wrong_pass: bool,
|
|
||||||
|
|
||||||
/// Optional data to pass after wallet opening.
|
|
||||||
data: Option<String>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl OpenWalletModal {
|
impl OpenWalletModal {
|
||||||
/// Create new content instance.
|
/// Create new content instance.
|
||||||
pub fn new(wallet: Wallet, data: Option<String>) -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
wallet,
|
pass_edit: "".to_string(),
|
||||||
pass_edit: "".to_string(),
|
wrong_pass: false,
|
||||||
wrong_pass: false,
|
}
|
||||||
data,
|
}
|
||||||
}
|
/// Draw [`Modal`] content.
|
||||||
}
|
pub fn ui(
|
||||||
/// Draw [`Modal`] content.
|
&mut self,
|
||||||
pub fn ui(&mut self,
|
ui: &mut egui::Ui,
|
||||||
ui: &mut egui::Ui,
|
modal: &Modal,
|
||||||
modal: &Modal,
|
cb: &dyn PlatformCallbacks,
|
||||||
cb: &dyn PlatformCallbacks,
|
mut on_continue: impl FnMut(ZeroingString) -> bool,
|
||||||
mut on_continue: impl FnMut(Wallet, Option<String>)) {
|
) {
|
||||||
ui.add_space(6.0);
|
// Callback for button to continue.
|
||||||
ui.vertical_centered(|ui| {
|
let mut on_continue = |m: &mut OpenWalletModal| {
|
||||||
ui.label(RichText::new(t!("wallets.pass"))
|
let pass = m.pass_edit.clone();
|
||||||
.size(17.0)
|
if pass.is_empty() {
|
||||||
.color(Colors::gray()));
|
return;
|
||||||
ui.add_space(8.0);
|
}
|
||||||
|
m.wrong_pass = !on_continue(ZeroingString::from(pass));
|
||||||
|
if !m.wrong_pass {
|
||||||
|
m.pass_edit = "".to_string();
|
||||||
|
Modal::close();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Show password input.
|
ui.vertical_centered(|ui| {
|
||||||
let mut pass_edit_opts = TextEditOptions::new(Id::from(modal.id)).password();
|
ui.add_space(6.0);
|
||||||
View::text_edit(ui, cb, &mut self.pass_edit, &mut pass_edit_opts);
|
ui.label(
|
||||||
|
RichText::new(t!("wallets.pass"))
|
||||||
|
.size(17.0)
|
||||||
|
.color(Colors::gray()),
|
||||||
|
);
|
||||||
|
ui.add_space(8.0);
|
||||||
|
|
||||||
// Show information when password is empty.
|
// Show password input.
|
||||||
if self.pass_edit.is_empty() {
|
let mut pass_edit = TextEdit::new(Id::from(modal.id).with("pass_edit")).password();
|
||||||
self.wrong_pass = false;
|
pass_edit.ui(ui, &mut self.pass_edit, cb);
|
||||||
ui.add_space(10.0);
|
if pass_edit.enter_pressed {
|
||||||
ui.label(RichText::new(t!("wallets.pass_empty"))
|
(on_continue)(self);
|
||||||
.size(17.0)
|
}
|
||||||
.color(Colors::inactive_text()));
|
|
||||||
} else if self.wrong_pass {
|
|
||||||
ui.add_space(10.0);
|
|
||||||
ui.label(RichText::new(t!("wallets.wrong_pass"))
|
|
||||||
.size(17.0)
|
|
||||||
.color(Colors::red()));
|
|
||||||
}
|
|
||||||
ui.add_space(12.0);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Show modal buttons.
|
// Show information when password is empty.
|
||||||
ui.scope(|ui| {
|
if self.pass_edit.is_empty() {
|
||||||
// Setup spacing between buttons.
|
self.wrong_pass = false;
|
||||||
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
|
ui.add_space(10.0);
|
||||||
|
ui.label(
|
||||||
|
RichText::new(t!("wallets.pass_empty"))
|
||||||
|
.size(17.0)
|
||||||
|
.color(Colors::inactive_text()),
|
||||||
|
);
|
||||||
|
} else if self.wrong_pass {
|
||||||
|
ui.add_space(10.0);
|
||||||
|
ui.label(
|
||||||
|
RichText::new(t!("wallets.wrong_pass"))
|
||||||
|
.size(17.0)
|
||||||
|
.color(Colors::red()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
ui.add_space(12.0);
|
||||||
|
});
|
||||||
|
|
||||||
ui.columns(2, |columns| {
|
// Show modal buttons.
|
||||||
columns[0].vertical_centered_justified(|ui| {
|
ui.scope(|ui| {
|
||||||
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
|
// Setup spacing between buttons.
|
||||||
// Close modal.
|
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
|
||||||
cb.hide_keyboard();
|
|
||||||
modal.close();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
columns[1].vertical_centered_justified(|ui| {
|
|
||||||
// Callback for button to continue.
|
|
||||||
let mut on_continue = || {
|
|
||||||
let pass = self.pass_edit.clone();
|
|
||||||
if pass.is_empty() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
match self.wallet.open(ZeroingString::from(pass)) {
|
|
||||||
Ok(_) => {
|
|
||||||
self.pass_edit = "".to_string();
|
|
||||||
cb.hide_keyboard();
|
|
||||||
modal.close();
|
|
||||||
on_continue(self.wallet.clone(), self.data.clone());
|
|
||||||
}
|
|
||||||
Err(_) => self.wrong_pass = true
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Continue on Enter key press.
|
ui.columns(2, |columns| {
|
||||||
View::on_enter_key(ui, || {
|
columns[0].vertical_centered_justified(|ui| {
|
||||||
(on_continue)();
|
View::button(
|
||||||
});
|
ui,
|
||||||
|
t!("modal.cancel"),
|
||||||
View::button(ui, t!("continue"), Colors::white_or_black(false), on_continue);
|
Colors::white_or_black(false),
|
||||||
});
|
|| {
|
||||||
});
|
// Close modal.
|
||||||
ui.add_space(6.0);
|
Modal::close();
|
||||||
});
|
},
|
||||||
}
|
);
|
||||||
}
|
});
|
||||||
|
columns[1].vertical_centered_justified(|ui| {
|
||||||
|
View::button(ui, t!("continue"), Colors::white_or_black(false), || {
|
||||||
|
(on_continue)(self);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
ui.add_space(6.0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,192 @@
|
|||||||
|
// Copyright 2024 The Grim Developers
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
use egui::scroll_area::ScrollBarVisibility;
|
||||||
|
use egui::{RichText, ScrollArea};
|
||||||
|
|
||||||
|
use crate::gui::Colors;
|
||||||
|
use crate::gui::icons::{PLUS_CIRCLE, TRASH};
|
||||||
|
use crate::gui::platform::PlatformCallbacks;
|
||||||
|
use crate::gui::views::network::ConnectionsContent;
|
||||||
|
use crate::gui::views::network::modals::ExternalConnectionModal;
|
||||||
|
use crate::gui::views::types::ModalPosition;
|
||||||
|
use crate::gui::views::wallets::WalletsContent;
|
||||||
|
use crate::gui::views::{Modal, View};
|
||||||
|
use crate::wallet::types::ConnectionMethod;
|
||||||
|
use crate::wallet::{ConnectionsConfig, ExternalConnection};
|
||||||
|
|
||||||
|
/// Wallet connection selection [`Modal`] content.
|
||||||
|
pub struct WalletSettingsModal {
|
||||||
|
/// Current connection method.
|
||||||
|
pub conn: ConnectionMethod,
|
||||||
|
|
||||||
|
/// External connection creation content.
|
||||||
|
new_ext_conn_content: Option<ExternalConnectionModal>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WalletSettingsModal {
|
||||||
|
/// Create from provided wallet connection.
|
||||||
|
pub fn new(conn: ConnectionMethod) -> Self {
|
||||||
|
Self {
|
||||||
|
conn,
|
||||||
|
new_ext_conn_content: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw [`Modal`] content.
|
||||||
|
pub fn ui(
|
||||||
|
&mut self,
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
modal: &Modal,
|
||||||
|
cb: &dyn PlatformCallbacks,
|
||||||
|
on_select: impl Fn(ConnectionMethod),
|
||||||
|
) {
|
||||||
|
// Draw external connection creation content.
|
||||||
|
if let Some(ext_content) = self.new_ext_conn_content.as_mut() {
|
||||||
|
ext_content.ui(ui, cb, modal, |conn| {
|
||||||
|
on_select(ConnectionMethod::External(conn.id, conn.url));
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check connections state on first draw.
|
||||||
|
if Modal::first_draw() {
|
||||||
|
ExternalConnection::check(None, ui.ctx());
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.add_space(4.0);
|
||||||
|
|
||||||
|
let ext_conn_list = ConnectionsConfig::ext_conn_list();
|
||||||
|
ScrollArea::vertical()
|
||||||
|
.max_height(if ext_conn_list.len() < 4 {
|
||||||
|
330.0
|
||||||
|
} else {
|
||||||
|
350.0
|
||||||
|
})
|
||||||
|
.id_salt("connections_scroll")
|
||||||
|
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
|
||||||
|
.auto_shrink([true; 2])
|
||||||
|
.show(ui, |ui| {
|
||||||
|
ui.add_space(2.0);
|
||||||
|
|
||||||
|
// Show integrated node selection.
|
||||||
|
let cur_integrated = self.conn == ConnectionMethod::Integrated;
|
||||||
|
let bg = if cur_integrated {
|
||||||
|
Colors::fill()
|
||||||
|
} else {
|
||||||
|
Colors::fill_lite()
|
||||||
|
};
|
||||||
|
ConnectionsContent::integrated_node_item_ui(
|
||||||
|
ui,
|
||||||
|
bg,
|
||||||
|
(!cur_integrated, || {
|
||||||
|
on_select(ConnectionMethod::Integrated);
|
||||||
|
Modal::close();
|
||||||
|
}),
|
||||||
|
|ui| {
|
||||||
|
if cur_integrated {
|
||||||
|
View::selected_item_check(ui);
|
||||||
|
}
|
||||||
|
cur_integrated
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
ui.add_space(8.0);
|
||||||
|
ui.vertical_centered(|ui| {
|
||||||
|
ui.label(
|
||||||
|
RichText::new(t!("wallets.ext_conn"))
|
||||||
|
.size(16.0)
|
||||||
|
.color(Colors::gray()),
|
||||||
|
);
|
||||||
|
ui.add_space(6.0);
|
||||||
|
// Show button to add new external node connection.
|
||||||
|
let add_node_text = format!("{} {}", PLUS_CIRCLE, t!("wallets.add_node"));
|
||||||
|
View::button(ui, add_node_text, Colors::white_or_black(false), || {
|
||||||
|
self.new_ext_conn_content = Some(ExternalConnectionModal::new(None));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
ui.add_space(4.0);
|
||||||
|
|
||||||
|
if !ext_conn_list.is_empty() {
|
||||||
|
ui.add_space(8.0);
|
||||||
|
}
|
||||||
|
for (i, c) in ext_conn_list.iter().enumerate() {
|
||||||
|
ui.horizontal_wrapped(|ui| {
|
||||||
|
let len = ext_conn_list.len();
|
||||||
|
let is_current = match self.conn {
|
||||||
|
ConnectionMethod::External(id, _) => id == c.id,
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
let bg = if is_current {
|
||||||
|
Colors::fill()
|
||||||
|
} else {
|
||||||
|
Colors::fill_lite()
|
||||||
|
};
|
||||||
|
ConnectionsContent::ext_conn_item_ui(
|
||||||
|
ui,
|
||||||
|
bg,
|
||||||
|
c,
|
||||||
|
i,
|
||||||
|
len,
|
||||||
|
(!is_current, || {
|
||||||
|
on_select(ConnectionMethod::External(c.id, c.url.clone()));
|
||||||
|
Modal::close();
|
||||||
|
}),
|
||||||
|
|ui| {
|
||||||
|
if is_current {
|
||||||
|
View::selected_item_check(ui);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
ui.add_space(4.0);
|
||||||
|
});
|
||||||
|
|
||||||
|
ui.add_space(2.0);
|
||||||
|
View::horizontal_line(ui, Colors::item_stroke());
|
||||||
|
ui.add_space(6.0);
|
||||||
|
ui.vertical_centered(|ui| {
|
||||||
|
// Draw button to delete the wallet.
|
||||||
|
View::colored_text_button(
|
||||||
|
ui,
|
||||||
|
format!("{} {}", TRASH, t!("wallets.delete")),
|
||||||
|
Colors::red(),
|
||||||
|
Colors::white_or_black(false),
|
||||||
|
|| {
|
||||||
|
Modal::new(WalletsContent::DELETE_CONFIRMATION_MODAL)
|
||||||
|
.position(ModalPosition::Center)
|
||||||
|
.title(t!("confirmation"))
|
||||||
|
.show();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
ui.add_space(6.0);
|
||||||
|
View::horizontal_line(ui, Colors::item_stroke());
|
||||||
|
ui.add_space(6.0);
|
||||||
|
|
||||||
|
// Show button to close modal.
|
||||||
|
ui.vertical_centered_justified(|ui| {
|
||||||
|
View::button(
|
||||||
|
ui,
|
||||||
|
t!("modal.cancel"),
|
||||||
|
Colors::white_or_black(false),
|
||||||
|
|| {
|
||||||
|
Modal::close();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
ui.add_space(6.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,185 +0,0 @@
|
|||||||
// Copyright 2024 The Grim Developers
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
use egui::scroll_area::ScrollBarVisibility;
|
|
||||||
use egui::{Align, Layout, RichText, ScrollArea};
|
|
||||||
|
|
||||||
use crate::gui::Colors;
|
|
||||||
use crate::gui::icons::{CHECK, CHECK_FAT, COMPUTER_TOWER, FOLDER_OPEN, GLOBE_SIMPLE, PLUGS_CONNECTED};
|
|
||||||
use crate::gui::platform::PlatformCallbacks;
|
|
||||||
use crate::gui::views::{Modal, View};
|
|
||||||
use crate::gui::views::types::ModalPosition;
|
|
||||||
use crate::gui::views::wallets::modals::OpenWalletModal;
|
|
||||||
use crate::gui::views::wallets::wallet::types::wallet_status_text;
|
|
||||||
use crate::wallet::{Wallet, WalletList};
|
|
||||||
use crate::wallet::types::ConnectionMethod;
|
|
||||||
|
|
||||||
/// Wallet list [`Modal`] content
|
|
||||||
pub struct WalletsModal {
|
|
||||||
/// Selected wallet id.
|
|
||||||
selected_id: Option<i64>,
|
|
||||||
|
|
||||||
/// Optional data to pass after wallet selection.
|
|
||||||
data: Option<String>,
|
|
||||||
|
|
||||||
/// Flag to check if wallet can be opened from the list.
|
|
||||||
can_open: bool,
|
|
||||||
/// Wallet opening content.
|
|
||||||
open_wallet_content: Option<OpenWalletModal>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WalletsModal {
|
|
||||||
/// Create new content instance.
|
|
||||||
pub fn new(selected_id: Option<i64>, data: Option<String>, can_open: bool) -> Self {
|
|
||||||
Self { selected_id, data, can_open, open_wallet_content: None }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Draw content.
|
|
||||||
pub fn ui(&mut self,
|
|
||||||
ui: &mut egui::Ui,
|
|
||||||
modal: &Modal,
|
|
||||||
wallets: &WalletList,
|
|
||||||
cb: &dyn PlatformCallbacks,
|
|
||||||
mut on_select: impl FnMut(Wallet, Option<String>)) {
|
|
||||||
// Draw wallet opening modal content.
|
|
||||||
if let Some(open_content) = self.open_wallet_content.as_mut() {
|
|
||||||
open_content.ui(ui, modal, cb, |wallet, data| {
|
|
||||||
on_select(wallet, data);
|
|
||||||
self.data = None;
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ui.add_space(4.0);
|
|
||||||
ScrollArea::vertical()
|
|
||||||
.max_height(373.0)
|
|
||||||
.id_salt("select_wallet_list_scroll")
|
|
||||||
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
|
|
||||||
.auto_shrink([true; 2])
|
|
||||||
.show(ui, |ui| {
|
|
||||||
ui.add_space(2.0);
|
|
||||||
ui.vertical_centered(|ui| {
|
|
||||||
let data = self.data.clone();
|
|
||||||
for wallet in wallets.list() {
|
|
||||||
// Draw wallet list item.
|
|
||||||
self.wallet_item_ui(ui, wallet, || {
|
|
||||||
modal.close();
|
|
||||||
on_select(wallet.clone(), data.clone());
|
|
||||||
});
|
|
||||||
ui.add_space(5.0);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
ui.add_space(2.0);
|
|
||||||
View::horizontal_line(ui, Colors::item_stroke());
|
|
||||||
ui.add_space(6.0);
|
|
||||||
|
|
||||||
// Show button to close modal.
|
|
||||||
ui.vertical_centered_justified(|ui| {
|
|
||||||
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
|
|
||||||
self.data = None;
|
|
||||||
modal.close();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
ui.add_space(6.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Draw wallet list item with provided callback on select.
|
|
||||||
fn wallet_item_ui(&mut self,
|
|
||||||
ui: &mut egui::Ui,
|
|
||||||
wallet: &Wallet,
|
|
||||||
on_select: impl FnOnce()) {
|
|
||||||
let config = wallet.get_config();
|
|
||||||
let id = config.id;
|
|
||||||
|
|
||||||
// Draw round background.
|
|
||||||
let mut rect = ui.available_rect_before_wrap();
|
|
||||||
rect.set_height(78.0);
|
|
||||||
let rounding = View::item_rounding(0, 1, false);
|
|
||||||
ui.painter().rect(rect, rounding, Colors::fill(), View::hover_stroke());
|
|
||||||
|
|
||||||
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
|
|
||||||
if self.can_open {
|
|
||||||
// Show button to select or open closed wallet.
|
|
||||||
let icon = if wallet.is_open() {
|
|
||||||
CHECK
|
|
||||||
} else {
|
|
||||||
FOLDER_OPEN
|
|
||||||
};
|
|
||||||
View::item_button(ui, View::item_rounding(0, 1, true), icon, None, || {
|
|
||||||
if wallet.is_open() {
|
|
||||||
on_select();
|
|
||||||
} else {
|
|
||||||
Modal::change_position(ModalPosition::CenterTop);
|
|
||||||
self.open_wallet_content = Some(
|
|
||||||
OpenWalletModal::new(wallet.clone(), self.data.clone())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Draw button to select wallet.
|
|
||||||
let current = self.selected_id.unwrap_or(0) == id;
|
|
||||||
if current {
|
|
||||||
ui.add_space(12.0);
|
|
||||||
ui.label(RichText::new(CHECK_FAT).size(20.0).color(Colors::green()));
|
|
||||||
} else {
|
|
||||||
View::item_button(ui, View::item_rounding(0, 1, true), CHECK, None, || {
|
|
||||||
on_select();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let layout_size = ui.available_size();
|
|
||||||
ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| {
|
|
||||||
ui.add_space(6.0);
|
|
||||||
ui.vertical(|ui| {
|
|
||||||
ui.add_space(3.0);
|
|
||||||
// Show wallet name text.
|
|
||||||
ui.with_layout(Layout::left_to_right(Align::Min), |ui| {
|
|
||||||
ui.add_space(1.0);
|
|
||||||
View::ellipsize_text(ui, config.name, 18.0, Colors::title(false));
|
|
||||||
});
|
|
||||||
|
|
||||||
// Show wallet connection text.
|
|
||||||
let connection = wallet.get_current_connection();
|
|
||||||
let conn_text = match connection {
|
|
||||||
ConnectionMethod::Integrated => {
|
|
||||||
format!("{} {}", COMPUTER_TOWER, t!("network.node"))
|
|
||||||
}
|
|
||||||
ConnectionMethod::External(_, url) => format!("{} {}", GLOBE_SIMPLE, url)
|
|
||||||
};
|
|
||||||
ui.label(RichText::new(conn_text).size(15.0).color(Colors::text(false)));
|
|
||||||
ui.add_space(1.0);
|
|
||||||
|
|
||||||
// Show wallet API text or open status.
|
|
||||||
if self.can_open {
|
|
||||||
ui.label(RichText::new(wallet_status_text(wallet))
|
|
||||||
.size(15.0)
|
|
||||||
.color(Colors::gray()));
|
|
||||||
} else {
|
|
||||||
let address = if let Some(port) = config.api_port {
|
|
||||||
format!("127.0.0.1:{}", port)
|
|
||||||
} else {
|
|
||||||
"-".to_string()
|
|
||||||
};
|
|
||||||
let api_text = format!("{} {}", PLUGS_CONNECTED, address);
|
|
||||||
ui.label(RichText::new(api_text).size(15.0).color(Colors::gray()));
|
|
||||||
}
|
|
||||||
ui.add_space(3.0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user