mirror of
https://code.gri.mw/GUI/grim.git
synced 2026-07-06 06:57:28 +00:00
Compare commits
283 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | |||
| 094a5b8969 | |||
| 12a75f8370 | |||
| 1c14b9aa93 | |||
| 8ea388554a | |||
| 1531c201bb | |||
| ed522c56ae | |||
| 4b454ab2f3 | |||
| f6fbf7226e | |||
| ebd09ab1c8 | |||
| 75cf7edc96 | |||
| 5c8b9c40be | |||
| dcaf9945c8 | |||
| f9426287d5 | |||
| 77281e3ab9 | |||
| 64439ad3d3 | |||
| 9494c1292e | |||
| accf123d49 | |||
| d77598c259 | |||
| 4e6dff52fe | |||
| 92d0aac250 | |||
| 5ef310558a | |||
| 683821b667 | |||
| da4cf71fac | |||
| f81ceae940 | |||
| fa6301a1db | |||
| 442fc425f7 | |||
| ea61588ede | |||
| 7f67aa134a | |||
| d7d1c53c52 | |||
| 18f52f877a | |||
| c13195bd61 | |||
| e40d5b6474 | |||
| 92e5d38755 | |||
| ec7e795ba9 | |||
| af220b2a09 | |||
| 846e30cb38 | |||
| d371d4368b | |||
| 85fc8101e4 | |||
| e2f58a8938 | |||
| 7e6954afd9 | |||
| bed041a1c3 | |||
| f955f720d2 | |||
| b627ac1ca6 | |||
| ac0b218376 | |||
| 04bf5a5349 | |||
| 9cce52a7d9 | |||
| 51e0d87d27 | |||
| d6f7e2e976 | |||
| 0bbf395a62 | |||
| 609d7ceb7a | |||
| b91605864d | |||
| 7857b708c9 | |||
| a0f85538e9 | |||
| c52da4f479 | |||
| af597df7b1 | |||
| 2adb29f4ee | |||
| 2b83944f34 | |||
| 71e80f6df7 | |||
| 0ead11ec6c | |||
| 3e249c5314 | |||
| bacc87945c | |||
| 2cfd428c4c | |||
| c155deedb5 | |||
| 3bc8c407b4 | |||
| c3fae38d5c | |||
| d6ec4213ab | |||
| 150a0de1c4 | |||
| 7cedebc70e | |||
| fe5aca6f0e | |||
| 5d83710fed | |||
| 1431e307ee | |||
| 1934dc3377 | |||
| 8af06d8860 | |||
| 9ea0da95b7 | |||
| d39e2ec21e | |||
| 68c9c9df04 | |||
| 6f7156ef17 | |||
| 50638ff54e | |||
| 8594279b98 | |||
| 0205e01b3c | |||
| 17545c1b7c | |||
| bcf821c06a | |||
| 34376d3490 | |||
| 8ed2308340 | |||
| c73cd58eed | |||
| d78ec570b0 | |||
| dd45f7ce38 | |||
| fb7312cb80 | |||
| dbc28205e8 | |||
| a3ed3bd234 | |||
| 21ecf200b8 | |||
| c8bca08bdc | |||
| 68bd2b81ec | |||
| 09cfb84b94 | |||
| 5c1ffb5636 | |||
| 7f79cc0708 | |||
| b0b4f9068a | |||
| cb9e86750c | |||
| 86fbf2e14f | |||
| e0351cea84 | |||
| 040fab6ff8 | |||
| f3db1005b5 | |||
| 0c1e279215 | |||
| 36168442a9 | |||
| 457db333d9 |
@@ -0,0 +1,69 @@
|
||||
name: Test build
|
||||
|
||||
on:
|
||||
push:
|
||||
tags-ignore:
|
||||
- "*"
|
||||
branches-ignore:
|
||||
- master
|
||||
- ci
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
- 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,437 @@
|
||||
name: Release build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- ci
|
||||
tags-ignore:
|
||||
- "*-dev*"
|
||||
|
||||
jobs:
|
||||
version:
|
||||
runs-on: ubuntu
|
||||
outputs:
|
||||
v: ${{ steps.version.outputs.v }}
|
||||
exists: ${{ steps.check.outputs.exists }}
|
||||
last_tag: ${{ steps.check_prev.outputs.last_tag }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
- 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: ardocrat/forgejo-release@grim
|
||||
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
|
||||
- uses: ardocrat/forgejo-release@grim
|
||||
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 }})"
|
||||
- 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
|
||||
last_tag=$(git describe --abbrev=0 --tags $(git rev-list --tags --skip=1 --max-count=1))
|
||||
echo "last_tag=${last_tag}" >> "$FORGEJO_OUTPUT"
|
||||
|
||||
android_libs:
|
||||
if: ${{ forgejo.ref_type == 'branch' || needs.version.outputs.exists == 'false' }}
|
||||
runs-on: ubuntu
|
||||
needs: version
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
- 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: 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 libs
|
||||
run: |
|
||||
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: Upload artifacts
|
||||
run: |
|
||||
cd android/app/src/main
|
||||
tar -czf jniLibs.tar.gz jniLibs
|
||||
curl -v -u ${{ secrets.MAVEN_USER }}:${{ secrets.MAVEN_PASSWORD }} --upload-file jniLibs.tar.gz ${{ secrets.MAVEN_HOST }}/repository/grim-ci-artifacts/${{ forgejo.repository }}/jniLibs.tar.gz
|
||||
|
||||
android_release:
|
||||
if: ${{ forgejo.ref_type == 'branch' || needs.version.outputs.exists == 'false' }}
|
||||
runs-on: ubuntu-android
|
||||
needs: [version, android_libs]
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
- 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: Download artifacts
|
||||
run: |
|
||||
cd android/app/src/main
|
||||
curl -o jniLibs.tar.gz -u ${{ secrets.MAVEN_USER }}:${{ secrets.MAVEN_PASSWORD }} ${{ secrets.MAVEN_HOST }}/repository/grim-ci-artifacts/${{ forgejo.repository }}/jniLibs.tar.gz
|
||||
tar -xzf jniLibs.tar.gz
|
||||
rm jniLibs.tar.gz
|
||||
- name: Setup build
|
||||
run: |
|
||||
chmod +x android/gradlew
|
||||
echo "${{ secrets.ANDROID_KEYSTORE }}" > release.keystore.txt
|
||||
base64 -d release.keystore.txt > android/keystore
|
||||
echo "${{ secrets.ANDROID_KEYSTORE_PROPS }}" > release.keystore.props.txt
|
||||
base64 -d release.keystore.props.txt > android/keystore.properties
|
||||
mkdir ~/.gradle && touch ~/.gradle/gradle.properties
|
||||
printf "mavenHost=${{ secrets.MAVEN_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 -v -u ${{ secrets.MAVEN_USER }}:${{ secrets.MAVEN_PASSWORD }} --upload-file android.tar.gz ${{ secrets.MAVEN_HOST }}/repository/grim-ci-artifacts/${{ forgejo.repository }}/android.tar.gz
|
||||
|
||||
linux_arm:
|
||||
if: ${{ forgejo.ref_type == 'branch' || needs.version.outputs.exists == 'false' }}
|
||||
runs-on: ubuntu
|
||||
needs: [version, android_libs]
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
- 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_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: Release Linux ARM
|
||||
run: 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: AppImage ARM
|
||||
run: |
|
||||
mkdir release
|
||||
cp target/aarch64-unknown-linux-gnu/release/grim linux/Grim.AppDir/AppRun
|
||||
appimagetool linux/Grim.AppDir grim-${{ needs.version.outputs.v }}-linux-arm.AppImage
|
||||
cp 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.tar.gz release
|
||||
curl -v -u ${{ secrets.MAVEN_USER }}:${{ secrets.MAVEN_PASSWORD }} --upload-file linux-arm.tar.gz ${{ secrets.MAVEN_HOST }}/repository/grim-ci-artifacts/${{ forgejo.repository }}/linux-arm.tar.gz
|
||||
|
||||
linux_x86:
|
||||
if: ${{ forgejo.ref_type == 'branch' || needs.version.outputs.exists == 'false' }}
|
||||
runs-on: ubuntu-linux-x86
|
||||
needs: [version]
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
- 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-x86-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: Release Linux x86
|
||||
run: 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
|
||||
cp 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
|
||||
cp 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.tar.gz release
|
||||
curl -v -u ${{ secrets.MAVEN_USER }}:${{ secrets.MAVEN_PASSWORD }} --upload-file linux-x86_64.tar.gz ${{ secrets.MAVEN_HOST }}/repository/grim-ci-artifacts/${{ forgejo.repository }}/linux-x86_64.tar.gz
|
||||
|
||||
macos:
|
||||
if: ${{ forgejo.ref_type == 'branch' || needs.version.outputs.exists == 'false' }}
|
||||
runs-on: ubuntu
|
||||
needs: [version, android_libs, linux]
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
- 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: 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: Release MacOS Universal
|
||||
run: |
|
||||
cargo zigbuild --release --target universal2-apple-darwin
|
||||
cp target/universal2-apple-darwin/release/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.zip Grim.app
|
||||
mv grim-${{ needs.version.outputs.v }}-macos.zip ../release
|
||||
- name: Checksum Release Universal
|
||||
working-directory: release
|
||||
run: sha256sum grim-${{ needs.version.outputs.v }}-macos.zip > grim-${{ needs.version.outputs.v }}-macos-sha256sum.txt
|
||||
- name: Upload artifacts
|
||||
run: |
|
||||
tar -czf macos.tar.gz release
|
||||
curl -v -u ${{ secrets.MAVEN_USER }}:${{ secrets.MAVEN_PASSWORD }} --upload-file macos.tar.gz ${{ secrets.MAVEN_HOST }}/repository/grim-ci-artifacts/${{ 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
|
||||
with:
|
||||
submodules: recursive
|
||||
- run: mkdir release
|
||||
- name: Release Windows x86
|
||||
run: |
|
||||
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 -v -u ${{ secrets.MAVEN_USER }}:${{ secrets.MAVEN_PASSWORD }} --upload-file windows.tar.gz ${{ secrets.MAVEN_HOST }}/repository/grim-ci-artifacts/${{ forgejo.repository }}/windows.tar.gz
|
||||
|
||||
release:
|
||||
if: ${{ forgejo.ref_type == 'branch' || needs.version.outputs.exists == 'false' }}
|
||||
runs-on: debian-release
|
||||
needs: [version, android_release, linux, linux_x86, macos, windows]
|
||||
steps:
|
||||
- name: Download All Artifacts
|
||||
run: |
|
||||
curl -o android.tar.gz -u ${{ secrets.MAVEN_USER }}:${{ secrets.MAVEN_PASSWORD }} ${{ secrets.MAVEN_HOST }}/repository/grim-ci-artifacts/${{ forgejo.repository }}/android.tar.gz
|
||||
tar -xzf android.tar.gz
|
||||
rm android.tar.gz
|
||||
curl -o linux-arm.tar.gz -u ${{ secrets.MAVEN_USER }}:${{ secrets.MAVEN_PASSWORD }} ${{ secrets.MAVEN_HOST }}/repository/grim-ci-artifacts/${{ forgejo.repository }}/linux-arm.tar.gz
|
||||
tar -xzf linux-arm.tar.gz
|
||||
rm linux-arm.tar.gz
|
||||
curl -o linux-x86_64.tar.gz -u ${{ secrets.MAVEN_USER }}:${{ secrets.MAVEN_PASSWORD }} ${{ secrets.MAVEN_HOST }}/repository/grim-ci-artifacts/${{ forgejo.repository }}/linux-x86_64.tar.gz
|
||||
tar -xzf linux-x86_64.tar.gz
|
||||
rm linux-x86_64.tar.gz
|
||||
curl -o macos.tar.gz -u ${{ secrets.MAVEN_USER }}:${{ secrets.MAVEN_PASSWORD }} ${{ secrets.MAVEN_HOST }}/repository/grim-ci-artifacts/${{ forgejo.repository }}/macos.tar.gz
|
||||
tar -xzf macos.tar.gz
|
||||
rm macos.tar.gz
|
||||
curl -o windows.tar.gz -u ${{ secrets.MAVEN_USER }}:${{ secrets.MAVEN_PASSWORD }} ${{ secrets.MAVEN_HOST }}/repository/grim-ci-artifacts/${{ forgejo.repository }}/windows.tar.gz
|
||||
tar -xzf windows.tar.gz
|
||||
rm windows.tar.gz
|
||||
- name: Upload release to Forgejo
|
||||
uses: ardocrat/forgejo-release@grim
|
||||
with:
|
||||
direction: upload
|
||||
token: ${{ secrets.RELEASE_TOKEN }}
|
||||
tag: ${{ needs.version.outputs.v }}
|
||||
override: true
|
||||
prerelease: ${{ needs.version.outputs.pre }}
|
||||
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"
|
||||
- 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.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.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
|
||||
@@ -0,0 +1,33 @@
|
||||
name: Build
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
linux:
|
||||
name: Linux Build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Release build
|
||||
run: cargo build --release
|
||||
|
||||
windows:
|
||||
name: Windows Build
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Release build
|
||||
run: cargo build --release
|
||||
|
||||
macos:
|
||||
name: MacOS Build
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Release build
|
||||
run: cargo build --release
|
||||
+16
-243
@@ -6,253 +6,26 @@ on:
|
||||
- "v*.*.*"
|
||||
|
||||
jobs:
|
||||
android_release:
|
||||
name: Android Release
|
||||
create_release:
|
||||
name: Create Release
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup JDK 17
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'temurin'
|
||||
cache: gradle
|
||||
- name: Setup Rust build
|
||||
run: |
|
||||
cargo install cargo-ndk
|
||||
rustup target add aarch64-linux-android
|
||||
rustup target add armv7-linux-androideabi
|
||||
rustup target add x86_64-linux-android
|
||||
- name: Setup Java build
|
||||
run: |
|
||||
chmod +x android/gradlew
|
||||
echo "${{ secrets.ANDROID_RELEASE_KEYSTORE }}" > release.keystore.asc
|
||||
gpg -d --passphrase "${{ secrets.ANDROID_RELEASE_SECRET }}" --batch release.keystore.asc > android/keystore
|
||||
echo -e "storePassword=${{ secrets.ANDROID_PASS }}\nkeyPassword=${{ secrets.ANDROID_PASS }}\nkeyAlias=grim\nstoreFile=../keystore" > android/keystore.properties
|
||||
- name: Build lib ARMv8 1/2
|
||||
continue-on-error: true
|
||||
run: |
|
||||
sed -i -e 's/"rlib"/"cdylib","rlib"/g' Cargo.toml
|
||||
export CPPFLAGS="-DMDB_USE_ROBUST=0" && export CFLAGS="-DMDB_USE_ROBUST=0" && cargo ndk -t arm64-v8a build --profile release-apk
|
||||
- name: Build lib ARMv8 2/2
|
||||
run: |
|
||||
unset CPPFLAGS && unset CFLAGS && cargo ndk -t arm64-v8a -o android/app/src/main/jniLibs build --profile release-apk
|
||||
sed -i -e 's/"cdylib","rlib"/"rlib"/g' Cargo.toml
|
||||
- name: Build APK ARMv8
|
||||
working-directory: android
|
||||
run: |
|
||||
./gradlew assembleRelease
|
||||
mv app/build/outputs/apk/release/app-release.apk grim-${{ github.ref_name }}-android-armv8.apk
|
||||
- name: Checksum APK ARMv8
|
||||
working-directory: android
|
||||
shell: pwsh
|
||||
run: get-filehash -algorithm sha256 grim-${{ github.ref_name }}-android-armv8.apk | Format-List | Out-String | ForEach-Object { $_.Trim() } > grim-${{ github.ref_name }}-android-armv8-sha256sum.txt
|
||||
- name: Build lib ARMv7 1/2
|
||||
continue-on-error: true
|
||||
run: |
|
||||
sed -i -e 's/"rlib"/"cdylib","rlib"/g' Cargo.toml
|
||||
export CPPFLAGS="-DMDB_USE_ROBUST=0" && export CFLAGS="-DMDB_USE_ROBUST=0" && cargo ndk -t armeabi-v7a build --profile release-apk
|
||||
- name: Build lib ARMv7 2/2
|
||||
run: |
|
||||
unset CPPFLAGS && unset CFLAGS && cargo ndk -t armeabi-v7a -o android/app/src/main/jniLibs build --profile release-apk
|
||||
sed -i -e 's/"cdylib","rlib"/"rlib"/g' Cargo.toml
|
||||
- name: Build APK ARMv7
|
||||
working-directory: android
|
||||
run: |
|
||||
rm -rf app/build
|
||||
./gradlew assembleRelease
|
||||
mv app/build/outputs/apk/release/app-release.apk grim-${{ github.ref_name }}-android-armv7.apk
|
||||
- name: Checksum APK ARMv7
|
||||
working-directory: android
|
||||
shell: pwsh
|
||||
run: get-filehash -algorithm sha256 grim-${{ github.ref_name }}-android-armv7.apk | Format-List | Out-String | ForEach-Object { $_.Trim() } > grim-${{ github.ref_name }}-android-armv7-sha256sum.txt
|
||||
- name: Build lib x86 1/2
|
||||
continue-on-error: true
|
||||
run: |
|
||||
sed -i -e 's/"rlib"/"cdylib","rlib"/g' Cargo.toml
|
||||
export CPPFLAGS="-DMDB_USE_ROBUST=0" && export CFLAGS="-DMDB_USE_ROBUST=0" && cargo ndk -t x86_64 build --profile release-apk
|
||||
- name: Build lib x86 2/2
|
||||
run: |
|
||||
unset CPPFLAGS && unset CFLAGS && cargo ndk -t x86_64 -o android/app/src/main/jniLibs build --profile release-apk
|
||||
sed -i -e 's/"cdylib","rlib"/"rlib"/g' Cargo.toml
|
||||
- name: Build APK x86
|
||||
working-directory: android
|
||||
run: |
|
||||
rm -rf app/build
|
||||
./gradlew assembleRelease
|
||||
mv app/build/outputs/apk/release/app-release.apk grim-${{ github.ref_name }}-android-x86_64.apk
|
||||
- name: Checksum APK x86
|
||||
working-directory: android
|
||||
shell: pwsh
|
||||
run: get-filehash -algorithm sha256 grim-${{ github.ref_name }}-android-x86_64.apk | Format-List | Out-String | ForEach-Object { $_.Trim() } > grim-${{ github.ref_name }}-android-x86_64-sha256sum.txt
|
||||
- name: Download release
|
||||
run: chmod +x .github/download_release.sh && .github/download_release.sh GUI/grim ${{ github.ref_name }}
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
body_path: release_notes.txt
|
||||
overwrite_files: true
|
||||
files: |
|
||||
android/grim-${{ github.ref_name }}-android-armv8.apk
|
||||
android/grim-${{ github.ref_name }}-android-armv8-sha256sum.txt
|
||||
android/grim-${{ github.ref_name }}-android-armv7.apk
|
||||
android/grim-${{ github.ref_name }}-android-armv7-sha256sum.txt
|
||||
android/grim-${{ github.ref_name }}-android-x86_64.apk
|
||||
android/grim-${{ github.ref_name }}-android-x86_64-sha256sum.txt
|
||||
|
||||
linux_release:
|
||||
name: Linux Release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Download appimagetools
|
||||
run: |
|
||||
wget https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage
|
||||
chmod +x appimagetool-x86_64.AppImage
|
||||
sudo apt install libfuse2
|
||||
- name: Zig Setup
|
||||
uses: goto-bus-stop/setup-zig@v2
|
||||
with:
|
||||
version: 0.12.1
|
||||
- name: Install cargo-zigbuild
|
||||
run: cargo install cargo-zigbuild
|
||||
- name: Release x86
|
||||
run: cargo zigbuild --release --target x86_64-unknown-linux-gnu
|
||||
- name: Release ARM
|
||||
run: |
|
||||
rustup target add aarch64-unknown-linux-gnu
|
||||
cargo zigbuild --release --target aarch64-unknown-linux-gnu
|
||||
- name: AppImage x86
|
||||
run: |
|
||||
cp target/x86_64-unknown-linux-gnu/release/grim linux/Grim.AppDir/AppRun
|
||||
./appimagetool-x86_64.AppImage linux/Grim.AppDir target/x86_64-unknown-linux-gnu/release/grim-${{ github.ref_name }}-linux-x86_64.AppImage
|
||||
- name: Checksum AppImage x86
|
||||
working-directory: target/x86_64-unknown-linux-gnu/release
|
||||
shell: pwsh
|
||||
run: get-filehash -algorithm sha256 grim-${{ github.ref_name }}-linux-x86_64.AppImage | Format-List | Out-String | ForEach-Object { $_.Trim() } > grim-${{ github.ref_name }}-linux-x86_64-appimage-sha256sum.txt
|
||||
- name: AppImage ARM
|
||||
run: |
|
||||
cp target/aarch64-unknown-linux-gnu/release/grim linux/Grim.AppDir/AppRun
|
||||
./appimagetool-x86_64.AppImage linux/Grim.AppDir target/aarch64-unknown-linux-gnu/release/grim-${{ github.ref_name }}-linux-arm.AppImage
|
||||
- name: Checksum AppImage ARM
|
||||
working-directory: target/aarch64-unknown-linux-gnu/release
|
||||
shell: pwsh
|
||||
run: get-filehash -algorithm sha256 grim-${{ github.ref_name }}-linux-arm.AppImage | Format-List | Out-String | ForEach-Object { $_.Trim() } > grim-${{ github.ref_name }}-linux-arm-appimage-sha256sum.txt
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
files: |
|
||||
target/x86_64-unknown-linux-gnu/release/grim-${{ github.ref_name }}-linux-x86_64.AppImage
|
||||
target/x86_64-unknown-linux-gnu/release/grim-${{ github.ref_name }}-linux-x86_64-appimage-sha256sum.txt
|
||||
target/aarch64-unknown-linux-gnu/release/grim-${{ github.ref_name }}-linux-arm.AppImage
|
||||
target/aarch64-unknown-linux-gnu/release/grim-${{ github.ref_name }}-linux-arm-appimage-sha256sum.txt
|
||||
|
||||
windows_release:
|
||||
name: Windows Release
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Build release
|
||||
run: cargo build --release
|
||||
- name: Archive release
|
||||
uses: vimtor/action-zip@v1
|
||||
with:
|
||||
files: target/release/grim.exe
|
||||
dest: target/release/grim-${{ github.ref_name }}-win-x86_64.zip
|
||||
- name: Checksum release
|
||||
working-directory: target/release
|
||||
shell: pwsh
|
||||
run: get-filehash -algorithm sha256 grim-${{ github.ref_name }}-win-x86_64.zip | Format-List | Out-String | ForEach-Object { $_.Trim() } > grim-${{ github.ref_name }}-win-x86_64-sha256sum.txt
|
||||
- name: Install cargo-wix
|
||||
run: cargo install cargo-wix
|
||||
- name: Run cargo-wix
|
||||
run: cargo wix -p grim -o ./target/wix/grim-${{ github.ref_name }}-win-x86_64.msi --nocapture
|
||||
- name: Checksum msi
|
||||
working-directory: target/wix
|
||||
shell: pwsh
|
||||
run: get-filehash -algorithm sha256 grim-${{ github.ref_name }}-win-x86_64.msi | Format-List | Out-String | ForEach-Object { $_.Trim() } > grim-${{ github.ref_name }}-win-x86_64-msi-sha256sum.txt
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
files: |
|
||||
target/release/grim-${{ github.ref_name }}-win-x86_64.zip
|
||||
target/release/grim-${{ github.ref_name }}-win-x86_64-sha256sum.txt
|
||||
target/wix/grim-${{ github.ref_name }}-win-x86_64.msi
|
||||
target/wix/grim-${{ github.ref_name }}-win-x86_64-msi-sha256sum.txt
|
||||
|
||||
macos_release:
|
||||
name: MacOS Release
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Zig Setup
|
||||
uses: goto-bus-stop/setup-zig@v2
|
||||
with:
|
||||
version: 0.12.1
|
||||
- name: Install cargo-zigbuild
|
||||
run: cargo install cargo-zigbuild
|
||||
- name: Release x86
|
||||
run: |
|
||||
rustup target add x86_64-apple-darwin
|
||||
cargo zigbuild --release --target x86_64-apple-darwin
|
||||
mkdir macos/Grim.app/Contents/MacOS
|
||||
yes | cp -rf target/x86_64-apple-darwin/release/grim macos/Grim.app/Contents/MacOS
|
||||
- name: Archive x86
|
||||
run: |
|
||||
cd macos
|
||||
zip -r grim-${{ github.ref_name }}-macos-x86_64.zip Grim.app
|
||||
mv grim-${{ github.ref_name }}-macos-x86_64.zip ../target/x86_64-apple-darwin/release
|
||||
cd ..
|
||||
- name: Checksum Release x86
|
||||
working-directory: target/x86_64-apple-darwin/release
|
||||
shell: pwsh
|
||||
run: get-filehash -algorithm sha256 grim-${{ github.ref_name }}-macos-x86_64.zip | Format-List | Out-String | ForEach-Object { $_.Trim() } > grim-${{ github.ref_name }}-macos-x86_64-sha256sum.txt
|
||||
- name: Release ARM
|
||||
run: |
|
||||
rustup target add aarch64-apple-darwin
|
||||
cargo zigbuild --release --target aarch64-apple-darwin
|
||||
yes | cp -rf target/aarch64-apple-darwin/release/grim macos/Grim.app/Contents/MacOS
|
||||
- name: Archive ARM
|
||||
run: |
|
||||
cd macos
|
||||
zip -r grim-${{ github.ref_name }}-macos-arm.zip Grim.app
|
||||
mv grim-${{ github.ref_name }}-macos-arm.zip ../target/aarch64-apple-darwin/release
|
||||
cd ..
|
||||
- name: Checksum Release ARM
|
||||
working-directory: target/aarch64-apple-darwin/release
|
||||
shell: pwsh
|
||||
run: get-filehash -algorithm sha256 grim-${{ github.ref_name }}-macos-arm.zip | Format-List | Out-String | ForEach-Object { $_.Trim() } > grim-${{ github.ref_name }}-macos-arm-sha256sum.txt
|
||||
- name: Release Universal
|
||||
run: |
|
||||
rustup target add aarch64-apple-darwin
|
||||
rustup target add x86_64-apple-darwin
|
||||
cargo zigbuild --release --target universal2-apple-darwin
|
||||
yes | cp -rf target/universal2-apple-darwin/release/grim macos/Grim.app/Contents/MacOS
|
||||
- name: Archive Universal
|
||||
run: |
|
||||
cd macos
|
||||
zip -r grim-${{ github.ref_name }}-macos-universal.zip Grim.app
|
||||
mv grim-${{ github.ref_name }}-macos-universal.zip ../target/universal2-apple-darwin/release
|
||||
cd ..
|
||||
- name: Checksum Release Universal
|
||||
working-directory: target/universal2-apple-darwin/release
|
||||
shell: pwsh
|
||||
run: get-filehash -algorithm sha256 grim-${{ github.ref_name }}-macos-universal.zip | Format-List | Out-String | ForEach-Object { $_.Trim() } > grim-${{ github.ref_name }}-macos-universal-sha256sum.txt
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
files: |
|
||||
target/x86_64-apple-darwin/release/grim-${{ github.ref_name }}-macos-x86_64.zip
|
||||
target/x86_64-apple-darwin/release/grim-${{ github.ref_name }}-macos-x86_64-sha256sum.txt
|
||||
target/aarch64-apple-darwin/release/grim-${{ github.ref_name }}-macos-arm.zip
|
||||
target/aarch64-apple-darwin/release/grim-${{ github.ref_name }}-macos-arm-sha256sum.txt
|
||||
target/universal2-apple-darwin/release/grim-${{ github.ref_name }}-macos-universal.zip
|
||||
target/universal2-apple-darwin/release/grim-${{ github.ref_name }}-macos-universal-sha256sum.txt
|
||||
grim-${{ github.ref_name }}-android.apk
|
||||
grim-${{ github.ref_name }}-android-x86_64.apk
|
||||
grim-${{ github.ref_name }}-linux-arm.AppImage
|
||||
grim-${{ github.ref_name }}-linux-x86_64.AppImage
|
||||
grim-${{ github.ref_name }}-macos.zip
|
||||
grim-${{ github.ref_name }}-win-x86_64.msi
|
||||
grim-${{ github.ref_name }}-win-x86_64.zip
|
||||
@@ -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
|
||||
+6
-2
@@ -1,9 +1,13 @@
|
||||
*.iml
|
||||
android/build
|
||||
android/.idea
|
||||
android/.gradle
|
||||
android/local.properties
|
||||
android/keystore
|
||||
android/keystore.asc
|
||||
android/keystore.properties
|
||||
android/*.apk
|
||||
android/*sha256sum.txt
|
||||
/.idea
|
||||
.DS_Store
|
||||
/captures
|
||||
@@ -13,7 +17,7 @@ android/keystore.properties
|
||||
target
|
||||
.cargo/
|
||||
app/src/main/jniLibs
|
||||
macos/Grim.app/Contents/MacOS/grim
|
||||
macos/cert.pem
|
||||
linux/Grim.AppDir/AppRun
|
||||
.intentionally-empty-file.o
|
||||
.intentionally-empty-file.o
|
||||
Cargo.toml-e
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
[submodule "node"]
|
||||
path = node
|
||||
url = https://code.gri.mw/ardocrat/node
|
||||
[submodule "wallet"]
|
||||
path = wallet
|
||||
url = https://code.gri.mw/ardocrat/wallet
|
||||
branch = grim
|
||||
[submodule "tor/webtunnel"]
|
||||
path = tor/webtunnel
|
||||
url = https://code.gri.mw/WEB/webtunnel
|
||||
branch = grim
|
||||
Generated
+5019
-3584
File diff suppressed because it is too large
Load Diff
+88
-79
@@ -1,12 +1,13 @@
|
||||
[package]
|
||||
name = "grim"
|
||||
version = "0.1.0"
|
||||
authors = ["Ardocrat <ardocrat@proton.me>"]
|
||||
version = "0.3.0"
|
||||
authors = ["Ardocrat <ardocrat@gri.mw>"]
|
||||
description = "Cross-platform GUI for Grin with focus on usability and availability to be used by anyone, anywhere."
|
||||
license = "Apache-2.0"
|
||||
repository = "https://github.com/ardocrat/grim"
|
||||
repository = "https://code.gri.mw/GUI/grim"
|
||||
keywords = [ "crypto", "grin", "mimblewimble" ]
|
||||
edition = "2021"
|
||||
build = "build.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "grim"
|
||||
@@ -25,106 +26,114 @@ codegen-units = 1
|
||||
panic = "abort"
|
||||
|
||||
[dependencies]
|
||||
log = "0.4"
|
||||
log = "0.4.27"
|
||||
|
||||
## node
|
||||
openssl-sys = { version = "0.9.82", features = ["vendored"] }
|
||||
grin_api = "5.3.1"
|
||||
grin_chain = "5.3.1"
|
||||
grin_config = "5.3.1"
|
||||
grin_core = "5.3.1"
|
||||
grin_p2p = "5.3.1"
|
||||
grin_servers = "5.3.1"
|
||||
grin_keychain = "5.3.1"
|
||||
grin_util = "5.3.1"
|
||||
# node
|
||||
grin_api = { path = "node/api" }
|
||||
grin_chain = { path = "node/chain" }
|
||||
grin_config = { path = "node/config" }
|
||||
grin_core = { path = "node/core" }
|
||||
grin_p2p = { path = "node/p2p" }
|
||||
grin_servers = { path = "node/servers" }
|
||||
grin_keychain = { path = "node/keychain" }
|
||||
grin_util = { path = "node/util" }
|
||||
|
||||
## wallet
|
||||
grin_wallet_impls = "5.3.1"
|
||||
grin_wallet_api = "5.3.1"
|
||||
grin_wallet_libwallet = "5.3.1"
|
||||
grin_wallet_util = "5.3.1"
|
||||
grin_wallet_controller = "5.3.1"
|
||||
# wallet
|
||||
grin_wallet_impls = { path = "wallet/impls" }
|
||||
grin_wallet_api = { path = "wallet/api"}
|
||||
grin_wallet_libwallet = { path = "wallet/libwallet" }
|
||||
grin_wallet_util = { path = "wallet/util" }
|
||||
grin_wallet_controller = { path = "wallet/controller" }
|
||||
|
||||
## ui
|
||||
egui = { version = "0.28.1", default-features = false }
|
||||
egui_extras = { version = "0.28.1", features = ["image", "svg"] }
|
||||
rust-i18n = "2.3.1"
|
||||
egui = { version = "0.33.3", default-features = false }
|
||||
egui_extras = { version = "0.33.3", features = ["image", "svg"] }
|
||||
egui-async = "0.3.4"
|
||||
rust-i18n = "3.1.5"
|
||||
|
||||
## other
|
||||
thiserror = "1.0.58"
|
||||
futures = "0.3"
|
||||
dirs = "5.0.1"
|
||||
sys-locale = "0.3.0"
|
||||
chrono = "0.4.31"
|
||||
parking_lot = "0.12.1"
|
||||
lazy_static = "1.4.0"
|
||||
toml = "0.8.2"
|
||||
serde = "1.0.170"
|
||||
local-ip-address = "0.6.1"
|
||||
url = "2.4.0"
|
||||
rand = "0.8.5"
|
||||
serde_derive = "1.0.197"
|
||||
serde_json = "1.0.115"
|
||||
tokio = { version = "1.37.0", features = ["full"] }
|
||||
image = "0.25.1"
|
||||
rqrr = "0.7.1"
|
||||
anyhow = "1.0.97"
|
||||
pin-project = "1.1.10"
|
||||
backtrace = "0.3.76"
|
||||
thiserror = "2.0.18"
|
||||
futures = "0.3.31"
|
||||
dirs = "6.0.0"
|
||||
sys-locale = "0.3.2"
|
||||
chrono = "0.4.43"
|
||||
parking_lot = "0.12.3"
|
||||
lazy_static = "1.5.0"
|
||||
toml = "0.9.11+spec-1.1.0"
|
||||
serde = "1.0.228"
|
||||
local-ip-address = "0.6.9"
|
||||
url = "2.5.8"
|
||||
rand = "0.9.2"
|
||||
serde_derive = "1.0.228"
|
||||
serde_json = "1.0.149"
|
||||
tokio = { version = "1.49.0", features = ["full"] }
|
||||
image = "0.25.9"
|
||||
rqrr = "0.10.1"
|
||||
qrcodegen = "1.8.0"
|
||||
qrcode = "0.14.0"
|
||||
qrcode = "0.14.1"
|
||||
ur = "0.4.1"
|
||||
gif = "0.13.1"
|
||||
rkv = { version = "0.19.0", features = ["lmdb"] }
|
||||
gif = "0.14.1"
|
||||
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
|
||||
arti-client = { version = "0.19.0", features = ["pt-client", "static", "onion-service-service", "onion-service-client"] }
|
||||
tor-rtcompat = { version = "0.19.0", features = ["static"] }
|
||||
tor-config = "0.19.0"
|
||||
fs-mistrust = "0.7.9"
|
||||
tor-hsservice = "0.19.0"
|
||||
tor-hsrproxy = "0.19.0"
|
||||
tor-keymgr = "0.19.0"
|
||||
tor-llcrypto = "0.19.0"
|
||||
tor-hscrypto = "0.19.0"
|
||||
arti-hyper = "0.19.0"
|
||||
sha2 = "0.10.0"
|
||||
arti-client = { version = "0.38.0", features = ["pt-client", "static", "onion-service-service", "onion-service-client"] }
|
||||
tor-rtcompat = { version = "0.38.0", features = ["static"] }
|
||||
tor-config = "0.38.0"
|
||||
fs-mistrust = "0.13.1"
|
||||
tor-hsservice = "0.38.0"
|
||||
tor-hsrproxy = "0.38.0"
|
||||
tor-keymgr = "0.38.0"
|
||||
tor-llcrypto = "0.38.0"
|
||||
tor-hscrypto = "0.38.0"
|
||||
tor-error = "0.38.0"
|
||||
sha2 = "0.10.8"
|
||||
ed25519-dalek = "2.1.1"
|
||||
curve25519-dalek = "4.1.2"
|
||||
hyper = { version = "0.14.28", features = ["full"] }
|
||||
hyper-tls = "0.5.0"
|
||||
tls-api = "0.9.0"
|
||||
tls-api-native-tls = "0.9.0"
|
||||
curve25519-dalek = "4.1.3"
|
||||
hyper-tor = { version = "0.14.32", features = ["full"], package = "hyper" }
|
||||
tls-api = "0.12.0"
|
||||
tls-api-native-tls = "0.12.1"
|
||||
safelog = "0.7.0"
|
||||
|
||||
## 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" }
|
||||
|
||||
[target.'cfg(all(not(target_os = "windows"), not(target_os = "android")))'.dependencies]
|
||||
eye = { version = "0.5.0", default-features = false }
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
nokhwa = { version = "0.10.10", default-features = false, features = ["input-v4l"] }
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
nokhwa = { version = "0.10.4", default-features = false, features = ["input-msmf"] }
|
||||
nokhwa = { version = "0.10.10", default-features = false, features = ["input-msmf"] }
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
tls-api-openssl = "0.9.0"
|
||||
openpnp_capture_sys = "0.4.0"
|
||||
nokhwa = { version = "0.10.10", default-features = false, features = ["input-avfoundation", "output-threaded"] }
|
||||
|
||||
[target.'cfg(not(target_os = "android"))'.dependencies]
|
||||
env_logger = "0.11.3"
|
||||
winit = { version = "0.29.15" }
|
||||
eframe = { version = "0.28.1", features = ["wgpu", "glow"] }
|
||||
winit = { version = "0.30.12" }
|
||||
wgpu = { version = "27.0.1" }
|
||||
eframe = { version = "0.33.2", features = ["wgpu"] }
|
||||
arboard = "3.2.0"
|
||||
rfd = "0.14.1"
|
||||
dark-light = "1.1.1"
|
||||
rfd = "0.17.2"
|
||||
interprocess = { version = "2.2.1", features = ["tokio"] }
|
||||
|
||||
[target.'cfg(target_os = "android")'.dependencies]
|
||||
android_logger = "0.13.1"
|
||||
android_logger = "0.15.0"
|
||||
jni = "0.21.1"
|
||||
android-activity = { version = "0.6.0", features = ["game-activity"] }
|
||||
wgpu = "0.20.1"
|
||||
winit = { version = "0.29.15", features = ["android-game-activity"] }
|
||||
eframe = { version = "0.28.1", features = ["wgpu", "android-game-activity"] }
|
||||
|
||||
[patch.crates-io]
|
||||
### patch grin store
|
||||
#grin_store = { path = "../grin-store" }
|
||||
### fix cross-compilation support for macos
|
||||
openpnp_capture_sys = { git = "https://github.com/ardocrat/openpnp-capture-rs", branch = "cross_compilation_support" }
|
||||
winit = { version = "0.30.12", features = ["android-game-activity"] }
|
||||
eframe = { version = "0.33.2", default-features = false, features = ["glow", "android-game-activity"] }
|
||||
@@ -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/)
|
||||
for maximum compatibility with original [Mimblewimble](https://github.com/mimblewimble/grin) implementation.
|
||||
Initially supported platforms are Linux, Mac, Windows, limited Android and possible web support with help of [egui](https://github.com/emilk/egui) - immediate mode GUI library in pure Rust.
|
||||
|
||||
Named by the character [Grim](http://harrypotter.wikia.com/wiki/Grim) - the shape of a large, black, menacing, spectral giant dog.
|
||||
|
||||

|
||||

|
||||
|
||||
|
||||
## 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:
|
||||
|
||||
```
|
||||
git submodule update --init --recursive
|
||||
cargo build --release
|
||||
./target/release/grim
|
||||
```
|
||||
@@ -31,7 +32,7 @@ Install Android SDK / NDK / Platform Tools for your OS according to this [FAQ](h
|
||||
|
||||
#### Build the project
|
||||
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
|
||||
|
||||
|
||||
+77
-24
@@ -2,36 +2,49 @@ plugins {
|
||||
id 'com.android.application'
|
||||
}
|
||||
|
||||
def keystorePropertiesFile = rootProject.file("keystore.properties")
|
||||
def keystoreProperties = new Properties()
|
||||
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
|
||||
|
||||
android {
|
||||
compileSdk 33
|
||||
ndkVersion '26.0.10792818'
|
||||
compileSdk = 36
|
||||
ndkVersion '29.0.14206865'
|
||||
buildToolsVersion = '36.1.0'
|
||||
|
||||
defaultConfig {
|
||||
applicationId "mw.gri.android"
|
||||
minSdk 24
|
||||
targetSdk 33
|
||||
versionCode 1
|
||||
versionName "0.1.0"
|
||||
targetSdk 36
|
||||
versionCode 5
|
||||
versionName "0.3.0"
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
release {
|
||||
keyAlias keystoreProperties['keyAlias']
|
||||
keyPassword keystoreProperties['keyPassword']
|
||||
storeFile file(keystoreProperties['storeFile'])
|
||||
storePassword keystoreProperties['storePassword']
|
||||
lint {
|
||||
checkReleaseBuilds false
|
||||
}
|
||||
|
||||
def keystorePropertiesFile = rootProject.file("keystore.properties")
|
||||
def keystoreProperties = new Properties()
|
||||
if (keystorePropertiesFile.exists()) {
|
||||
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
|
||||
|
||||
signingConfigs {
|
||||
release {
|
||||
keyAlias keystoreProperties['keyAlias']
|
||||
keyPassword keystoreProperties['keyPassword']
|
||||
storeFile file(keystoreProperties['storeFile'])
|
||||
storePassword keystoreProperties['storePassword']
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
signingConfig signingConfigs.release
|
||||
}
|
||||
if (keystorePropertiesFile.exists()) {
|
||||
signedRelease {
|
||||
initWith release
|
||||
signingConfig signingConfigs.release
|
||||
}
|
||||
}
|
||||
debug {
|
||||
minifyEnabled false
|
||||
@@ -39,21 +52,61 @@ android {
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
targetCompatibility JavaVersion.VERSION_17
|
||||
}
|
||||
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 {
|
||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||
implementation 'androidx.appcompat:appcompat:1.7.0'
|
||||
|
||||
// To use the Games Activity library
|
||||
implementation "androidx.games:games-activity:2.0.2"
|
||||
|
||||
// Android Camera
|
||||
implementation 'androidx.camera:camera-core:1.2.3'
|
||||
implementation 'androidx.camera:camera-camera2:1.2.3'
|
||||
implementation 'androidx.camera:camera-lifecycle:1.2.3'
|
||||
}
|
||||
|
||||
implementation 'androidx.camera:camera-core:1.5.1'
|
||||
implementation 'androidx.camera:camera-camera2:1.5.1'
|
||||
implementation 'androidx.camera:camera-lifecycle:1.5.1'
|
||||
}
|
||||
@@ -1,52 +1,69 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
>
|
||||
|
||||
<manifest xmlns:tools="http://schemas.android.com/tools" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<uses-feature android:name="android.hardware.camera" android:required="false"/>
|
||||
|
||||
<uses-permission android:name="android.permission.EXPAND_STATUS_BAR" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.CAMERA"/>
|
||||
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" tools:ignore="ScopedStorage"/>
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" tools:ignore="ScopedStorage"/>
|
||||
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" tools:ignore="ScopedStorage"/>
|
||||
|
||||
<application
|
||||
android:hardwareAccelerated="true"
|
||||
android:largeHeap="true"
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="Grim"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.Main">
|
||||
android:hardwareAccelerated="true"
|
||||
android:largeHeap="true"
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="Grim"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:theme="@style/Theme.Main"
|
||||
android:enableOnBackInvokedCallback="false"
|
||||
android:extractNativeLibs="true"
|
||||
tools:ignore="UnusedAttribute">
|
||||
|
||||
<receiver android:name=".NotificationActionsReceiver"/>
|
||||
|
||||
<provider
|
||||
android:name=".FileProvider"
|
||||
android:authorities="mw.gri.android.fileprovider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
android:name=".FileProvider"
|
||||
android:authorities="mw.gri.android.fileprovider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/paths" />
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/paths" />
|
||||
</provider>
|
||||
|
||||
<activity
|
||||
android:launchMode="singleTask"
|
||||
android:name=".MainActivity"
|
||||
android:configChanges="orientation|screenSize"
|
||||
android:exported="true">
|
||||
android:launchMode="singleTask"
|
||||
android:name=".MainActivity"
|
||||
android:configChanges="orientation|screenSize"
|
||||
android:exported="true">
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter android:scheme="http" tools:ignore="AppLinkUrlError">
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="text/*" />
|
||||
<data android:pathPattern=".*\\.slatepack" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data android:name="android.app.lib_name" android:value="grim" />
|
||||
</activity>
|
||||
<service android:name=".BackgroundService" android:stopWithTask="true" />
|
||||
|
||||
<service
|
||||
android:name=".BackgroundService"
|
||||
android:stopWithTask="true"
|
||||
android:foregroundServiceType="dataSync" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -2,13 +2,13 @@ package mw.gri.android;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.*;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.os.*;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@@ -16,7 +16,7 @@ import static android.app.Notification.EXTRA_NOTIFICATION_ID;
|
||||
|
||||
public class BackgroundService extends Service {
|
||||
private static final String TAG = BackgroundService.class.getSimpleName();
|
||||
|
||||
|
||||
private PowerManager.WakeLock mWakeLock;
|
||||
|
||||
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_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() {
|
||||
@SuppressLint("RestrictedApi")
|
||||
@@ -101,18 +81,6 @@ public class BackgroundService extends Service {
|
||||
.getBroadcast(BackgroundService.this, 1, startStopIntent, PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_ONE_SHOT);
|
||||
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.
|
||||
@@ -152,13 +120,17 @@ public class BackgroundService extends Service {
|
||||
// Show notification with sync status.
|
||||
Intent i = getPackageManager().getLaunchIntentForPackage(this.getPackageName());
|
||||
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, i, PendingIntent.FLAG_IMMUTABLE);
|
||||
mNotificationBuilder = new NotificationCompat.Builder(this, TAG)
|
||||
.setContentTitle(this.getSyncTitle())
|
||||
.setContentText(this.getSyncStatusText())
|
||||
.setStyle(new NotificationCompat.BigTextStyle().bigText(this.getSyncStatusText()))
|
||||
.setSmallIcon(R.drawable.ic_stat_name)
|
||||
.setPriority(NotificationCompat.PRIORITY_MAX)
|
||||
.setContentIntent(pendingIntent);
|
||||
try {
|
||||
mNotificationBuilder = new NotificationCompat.Builder(this, TAG)
|
||||
.setContentTitle(this.getSyncTitle())
|
||||
.setContentText(this.getSyncStatusText())
|
||||
.setStyle(new NotificationCompat.BigTextStyle().bigText(this.getSyncStatusText()))
|
||||
.setSmallIcon(R.drawable.ic_stat_name)
|
||||
.setPriority(NotificationCompat.PRIORITY_MAX)
|
||||
.setContentIntent(pendingIntent);
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
return;
|
||||
}
|
||||
Notification notification = mNotificationBuilder.build();
|
||||
|
||||
// Start service at foreground state to prevent killing by system.
|
||||
@@ -166,9 +138,6 @@ public class BackgroundService extends Service {
|
||||
|
||||
// Update sync status at notification.
|
||||
mHandler.post(mUpdateSyncStatus);
|
||||
|
||||
// Register receiver to refresh notifications by intent.
|
||||
registerReceiver(mReceiver, new IntentFilter(ACTION_REFRESH));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -199,7 +168,6 @@ public class BackgroundService extends Service {
|
||||
|
||||
// Stop updating the notification.
|
||||
mHandler.removeCallbacks(mUpdateSyncStatus);
|
||||
unregisterReceiver(mReceiver);
|
||||
clearNotification();
|
||||
|
||||
// Remove service from foreground state.
|
||||
@@ -222,12 +190,12 @@ public class BackgroundService extends Service {
|
||||
}
|
||||
|
||||
// Start the service.
|
||||
public static void start(Context context) {
|
||||
if (!isServiceRunning(context)) {
|
||||
public static void start(Context c) {
|
||||
if (!isServiceRunning(c)) {
|
||||
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 {
|
||||
context.startService(new Intent(context, BackgroundService.class));
|
||||
c.startService(new Intent(c, BackgroundService.class));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -266,9 +234,6 @@ public class BackgroundService extends Service {
|
||||
// Check if stop node is possible.
|
||||
private native boolean canStopNode();
|
||||
|
||||
// Get exit text for notification.
|
||||
private native String getExitText();
|
||||
|
||||
// Check if app from the app is needed after node stop.
|
||||
private native boolean exitAppAfterNodeStop();
|
||||
}
|
||||
}
|
||||
@@ -7,15 +7,15 @@ import android.content.*;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.res.Configuration;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.*;
|
||||
import android.os.Process;
|
||||
import android.provider.Settings;
|
||||
import android.system.ErrnoException;
|
||||
import android.system.Os;
|
||||
import android.util.Size;
|
||||
import android.util.Log;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.View;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.contract.ActivityResultContracts;
|
||||
import androidx.annotation.NonNull;
|
||||
@@ -28,9 +28,11 @@ import androidx.core.view.DisplayCutoutCompat;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.core.view.WindowInsetsCompat;
|
||||
import com.google.androidgamesdk.GameActivity;
|
||||
import com.google.androidgamesdk.gametextinput.State;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
@@ -50,15 +52,13 @@ public class MainActivity extends GameActivity {
|
||||
private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context ctx, Intent i) {
|
||||
if (i.getAction().equals(STOP_APP_ACTION)) {
|
||||
onExit();
|
||||
Process.killProcess(Process.myPid());
|
||||
if (Objects.equals(i.getAction(), STOP_APP_ACTION)) {
|
||||
exit();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private final ImageAnalysis mImageAnalysis = new ImageAnalysis.Builder()
|
||||
.setTargetResolution(new Size(640, 480))
|
||||
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
|
||||
.build();
|
||||
|
||||
@@ -67,21 +67,30 @@ public class MainActivity extends GameActivity {
|
||||
private ExecutorService mCameraExecutor = null;
|
||||
private boolean mUseBackCamera = true;
|
||||
|
||||
private ActivityResultLauncher<Intent> mFilePickResultLauncher = null;
|
||||
private ActivityResultLauncher<Intent> mFilePickResult = null;
|
||||
private ActivityResultLauncher<Intent> mOpenFilePermissionsResult = null;
|
||||
|
||||
@SuppressLint("UnspecifiedRegisterReceiverFlag")
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
// Check if activity was launched to exclude from recent apps on exit.
|
||||
if ((getIntent().getFlags() & Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS) != 0) {
|
||||
super.onCreate(null);
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear cache on start.
|
||||
if (savedInstanceState == null) {
|
||||
if (savedInstanceState == null && getExternalCacheDir() != null) {
|
||||
Utils.deleteDirectoryContent(new File(getExternalCacheDir().getPath()), false);
|
||||
}
|
||||
|
||||
// Setup environment variables for native code.
|
||||
try {
|
||||
Os.setenv("HOME", getExternalFilesDir("").getPath(), true);
|
||||
Os.setenv("XDG_CACHE_HOME", getExternalCacheDir().getPath(), true);
|
||||
Os.setenv("HOME", Objects.requireNonNull(getExternalFilesDir("")).getPath(), true);
|
||||
Os.setenv("XDG_CACHE_HOME", Objects.requireNonNull(getExternalCacheDir()).getPath(), true);
|
||||
Os.setenv("ARTI_FS_DISABLE_PERMISSION_CHECKS", "true", true);
|
||||
Os.setenv("NATIVE_LIBS_DIR", getApplicationInfo().nativeLibraryDir, true);
|
||||
} catch (ErrnoException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
@@ -89,17 +98,30 @@ public class MainActivity extends GameActivity {
|
||||
super.onCreate(null);
|
||||
|
||||
// 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 file pick result launcher.
|
||||
mFilePickResultLauncher = registerForActivityResult(
|
||||
// Register associated file opening result.
|
||||
mOpenFilePermissionsResult = registerForActivityResult(
|
||||
new ActivityResultContracts.StartActivityForResult(),
|
||||
result -> {
|
||||
if (Build.VERSION.SDK_INT >= 30) {
|
||||
if (Environment.isExternalStorageManager()) {
|
||||
onFile();
|
||||
}
|
||||
} else if (result.getResultCode() == RESULT_OK) {
|
||||
onFile();
|
||||
}
|
||||
}
|
||||
);
|
||||
// Register file pick result.
|
||||
mFilePickResult = registerForActivityResult(
|
||||
new ActivityResultContracts.StartActivityForResult(),
|
||||
result -> {
|
||||
int resultCode = result.getResultCode();
|
||||
Intent data = result.getData();
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
String path = "";
|
||||
if (data != null) {
|
||||
if (data != null && data.getData() != null) {
|
||||
Uri uri = data.getData();
|
||||
String name = "pick" + Utils.getFileExtension(uri, this);
|
||||
File file = new File(getExternalCacheDir(), name);
|
||||
@@ -107,11 +129,13 @@ public class MainActivity extends GameActivity {
|
||||
OutputStream os = new FileOutputStream(file)) {
|
||||
byte[] buffer = new byte[1024];
|
||||
int length;
|
||||
while ((length = is.read(buffer)) > 0) {
|
||||
while (true) {
|
||||
assert is != null;
|
||||
if (!((length = is.read(buffer)) > 0)) break;
|
||||
os.write(buffer, 0, length);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
Log.e("grim", e.toString());
|
||||
}
|
||||
path = file.getPath();
|
||||
}
|
||||
@@ -124,7 +148,7 @@ public class MainActivity extends GameActivity {
|
||||
// Listener for display insets (cutouts) to pass values into native code.
|
||||
View content = getWindow().getDecorView().findViewById(android.R.id.content);
|
||||
ViewCompat.setOnApplyWindowInsetsListener(content, (v, insets) -> {
|
||||
// Setup cutouts values.
|
||||
// Get display cutouts.
|
||||
DisplayCutoutCompat dc = insets.getDisplayCutout();
|
||||
int cutoutTop = 0;
|
||||
int cutoutRight = 0;
|
||||
@@ -140,7 +164,7 @@ public class MainActivity extends GameActivity {
|
||||
// Get display insets.
|
||||
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
|
||||
|
||||
// Setup values to pass into native code.
|
||||
// Pass values into native code.
|
||||
int[] values = new int[]{0, 0, 0, 0};
|
||||
values[0] = Utils.pxToDp(Integer.max(cutoutTop, systemBars.top), this);
|
||||
values[1] = Utils.pxToDp(Integer.max(cutoutRight, systemBars.right), this);
|
||||
@@ -166,8 +190,65 @@ public class MainActivity extends GameActivity {
|
||||
BackgroundService.start(this);
|
||||
}
|
||||
});
|
||||
|
||||
// Check if intent has data on launch.
|
||||
if (savedInstanceState == null) {
|
||||
onNewIntent(getIntent());
|
||||
}
|
||||
}
|
||||
|
||||
// Pass display insets into native code.
|
||||
public native void onDisplayInsets(int[] cutouts);
|
||||
|
||||
@Override
|
||||
protected void onNewIntent(Intent intent) {
|
||||
super.onNewIntent(intent);
|
||||
String action = intent.getAction();
|
||||
// Check if file was open with the application.
|
||||
if (action != null && action.equals(Intent.ACTION_VIEW)) {
|
||||
Intent i = getIntent();
|
||||
i.setData(intent.getData());
|
||||
setIntent(i);
|
||||
onFile();
|
||||
}
|
||||
}
|
||||
|
||||
// Callback when associated file was open.
|
||||
private void onFile() {
|
||||
Uri data = getIntent().getData();
|
||||
if (data == null) {
|
||||
return;
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= 30) {
|
||||
if (!Environment.isExternalStorageManager()) {
|
||||
Intent i = new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION);
|
||||
mOpenFilePermissionsResult.launch(i);
|
||||
return;
|
||||
}
|
||||
}
|
||||
try {
|
||||
ParcelFileDescriptor parcelFile = getContentResolver().openFileDescriptor(data, "r");
|
||||
assert parcelFile != null;
|
||||
FileReader fileReader = new FileReader(parcelFile.getFileDescriptor());
|
||||
BufferedReader reader = new BufferedReader(fileReader);
|
||||
String line;
|
||||
StringBuilder buff = new StringBuilder();
|
||||
while ((line = reader.readLine()) != null) {
|
||||
buff.append(line);
|
||||
}
|
||||
reader.close();
|
||||
fileReader.close();
|
||||
|
||||
// Provide file content into native code.
|
||||
onData(buff.toString());
|
||||
} catch (Exception e) {
|
||||
Log.e("grim", e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
// Pass data into native code.
|
||||
public native void onData(String data);
|
||||
|
||||
@Override
|
||||
public void onConfigurationChanged(Configuration newConfig) {
|
||||
super.onConfigurationChanged(newConfig);
|
||||
@@ -184,65 +265,99 @@ public class MainActivity extends GameActivity {
|
||||
if (results.length != 0 && results[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
switch (requestCode) {
|
||||
case NOTIFICATIONS_PERMISSION_CODE: {
|
||||
// Start notification service.
|
||||
BackgroundService.start(this);
|
||||
return;
|
||||
}
|
||||
case CAMERA_PERMISSION_CODE: {
|
||||
// Start camera.
|
||||
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
|
||||
public boolean dispatchKeyEvent(KeyEvent event) {
|
||||
// To support non-english input.
|
||||
if (event.getAction() == KeyEvent.ACTION_MULTIPLE && event.getKeyCode() == KeyEvent.KEYCODE_UNKNOWN) {
|
||||
if (!event.getCharacters().isEmpty()) {
|
||||
onInput(event.getCharacters());
|
||||
if (event.getAction() == KeyEvent.ACTION_DOWN) {
|
||||
if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
|
||||
onBack();
|
||||
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;
|
||||
}
|
||||
// Pass any other input values into native code.
|
||||
} else if (event.getAction() == KeyEvent.ACTION_UP &&
|
||||
event.getKeyCode() != KeyEvent.KEYCODE_ENTER &&
|
||||
event.getKeyCode() != KeyEvent.KEYCODE_BACK) {
|
||||
onInput(String.valueOf((char)event.getUnicodeChar()));
|
||||
return false;
|
||||
}
|
||||
return super.dispatchKeyEvent(event);
|
||||
}
|
||||
|
||||
// Provide last entered character from soft keyboard 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.
|
||||
// Pass back navigation event into native code.
|
||||
public native void onBack();
|
||||
|
||||
// Actions on app exit.
|
||||
private void onExit() {
|
||||
unregisterReceiver(mBroadcastReceiver);
|
||||
BackgroundService.stop(this);
|
||||
// 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.
|
||||
public void exit() {
|
||||
finishAndRemoveTask();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
onExit();
|
||||
unregisterReceiver(mBroadcastReceiver);
|
||||
BackgroundService.stop(this);
|
||||
|
||||
// Kill process after 3 seconds if app was terminated from recent apps to prevent app hanging.
|
||||
// Kill process after 3 secs if app was terminated from recent apps to prevent app hang.
|
||||
new Thread(() -> {
|
||||
try {
|
||||
onTermination();
|
||||
@@ -253,9 +368,7 @@ public class MainActivity extends GameActivity {
|
||||
}
|
||||
}).start();
|
||||
|
||||
// Destroy an app and kill process.
|
||||
super.onDestroy();
|
||||
Process.killProcess(Process.myPid());
|
||||
}
|
||||
|
||||
// Notify native code to stop activity (e.g. node) if app was terminated from recent apps.
|
||||
@@ -271,45 +384,33 @@ public class MainActivity extends GameActivity {
|
||||
// Called from native code to get text from clipboard.
|
||||
public String pasteText() {
|
||||
ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
String text;
|
||||
ClipDescription desc = clipboard.getPrimaryClipDescription();
|
||||
ClipData data = clipboard.getPrimaryClip();
|
||||
String text = "";
|
||||
if (!(clipboard.hasPrimaryClip())) {
|
||||
text = "";
|
||||
} else if (!(clipboard.getPrimaryClipDescription().hasMimeType(MIMETYPE_TEXT_PLAIN))
|
||||
&& !(clipboard.getPrimaryClipDescription().hasMimeType(MIMETYPE_TEXT_HTML))) {
|
||||
} else if (desc != null && (!(desc.hasMimeType(MIMETYPE_TEXT_PLAIN))
|
||||
&& !(desc.hasMimeType(MIMETYPE_TEXT_HTML)))) {
|
||||
text = "";
|
||||
} else {
|
||||
ClipData.Item item = clipboard.getPrimaryClip().getItemAt(0);
|
||||
} else if (data != null) {
|
||||
ClipData.Item item = data.getItemAt(0);
|
||||
text = item.getText().toString();
|
||||
}
|
||||
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.
|
||||
public void startCamera() {
|
||||
// Check permissions.
|
||||
String notificationsPermission = Manifest.permission.CAMERA;
|
||||
if (checkSelfPermission(notificationsPermission) != PackageManager.PERMISSION_GRANTED) {
|
||||
requestPermissions(new String[] { notificationsPermission }, CAMERA_PERMISSION_CODE);
|
||||
} else {
|
||||
// Start .
|
||||
if (mCameraProviderFuture == null) {
|
||||
mCameraProviderFuture = ProcessCameraProvider.getInstance(this);
|
||||
mCameraProviderFuture.addListener(() -> {
|
||||
try {
|
||||
mCameraProvider = mCameraProviderFuture.get();
|
||||
// Launch camera.
|
||||
// Start camera.
|
||||
openCamera();
|
||||
} catch (Exception e) {
|
||||
View content = findViewById(android.R.id.content);
|
||||
@@ -381,14 +482,14 @@ public class MainActivity extends GameActivity {
|
||||
// Pass image from camera into native code.
|
||||
public native void onCameraImage(byte[] buff, int rotation);
|
||||
|
||||
// Called from native code to share image from provided path.
|
||||
public void shareImage(String path) {
|
||||
// Called from native code to share data from provided path.
|
||||
public void shareData(String path) {
|
||||
File file = new File(path);
|
||||
Uri uri = FileProvider.getUriForFile(this, "mw.gri.android.fileprovider", file);
|
||||
Intent intent = new Intent(Intent.ACTION_SEND);
|
||||
intent.putExtra(Intent.EXTRA_STREAM, uri);
|
||||
intent.setType("image/*");
|
||||
startActivity(Intent.createChooser(intent, "Share image"));
|
||||
intent.setType("text/*");
|
||||
startActivity(Intent.createChooser(intent, "Share data"));
|
||||
}
|
||||
|
||||
// Called from native code to check if device is using dark theme.
|
||||
@@ -400,9 +501,9 @@ public class MainActivity extends GameActivity {
|
||||
// Called from native code to pick the file.
|
||||
public void pickFile() {
|
||||
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
|
||||
intent.setType("*/*");
|
||||
intent.setType("text/*");
|
||||
try {
|
||||
mFilePickResultLauncher.launch(Intent.createChooser(intent, "Pick file"));
|
||||
mFilePickResult.launch(Intent.createChooser(intent, "Pick file"));
|
||||
} catch (android.content.ActivityNotFoundException ex) {
|
||||
onFilePick("");
|
||||
}
|
||||
|
||||
@@ -4,23 +4,18 @@ import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public class NotificationActionsReceiver extends BroadcastReceiver {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent i) {
|
||||
String a = i.getAction();
|
||||
if (a.equals(BackgroundService.ACTION_START_NODE)) {
|
||||
if (Objects.equals(a, BackgroundService.ACTION_START_NODE)) {
|
||||
startNode();
|
||||
context.sendBroadcast(new Intent(BackgroundService.ACTION_REFRESH));
|
||||
} else if (a.equals(BackgroundService.ACTION_STOP_NODE)) {
|
||||
} else if (Objects.equals(a, BackgroundService.ACTION_STOP_NODE)) {
|
||||
stopNode();
|
||||
context.sendBroadcast(new Intent(BackgroundService.ACTION_REFRESH));
|
||||
} else {
|
||||
if (isNodeRunning()) {
|
||||
stopNodeToExit();
|
||||
context.sendBroadcast(new Intent(BackgroundService.ACTION_REFRESH));
|
||||
} else {
|
||||
context.sendBroadcast(new Intent(MainActivity.STOP_APP_ACTION));
|
||||
}
|
||||
stopNodeToExit();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +25,4 @@ public class NotificationActionsReceiver extends BroadcastReceiver {
|
||||
native void stopNode();
|
||||
// Stop node and exit from the app.
|
||||
native void stopNodeToExit();
|
||||
// Check if node is running.
|
||||
native boolean isNodeRunning();
|
||||
}
|
||||
|
||||
@@ -153,4 +153,4 @@ public class Utils {
|
||||
String fileType = context.getContentResolver().getType(uri);
|
||||
return MimeTypeMap.getSingleton().getExtensionFromMimeType(fileType);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
<item name="android:statusBarColor">@color/yellow</item>
|
||||
<item name="android:windowLightStatusBar">true</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>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<paths>
|
||||
<external-cache-path name="images" path="images/" />
|
||||
<external-cache-path name="share" path="share/" />
|
||||
</paths>
|
||||
@@ -1,10 +1,5 @@
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
plugins {
|
||||
id 'com.android.application' version '8.1.1' apply false
|
||||
id 'com.android.library' version '8.1.1' apply false
|
||||
}
|
||||
|
||||
task clean(type: Delete) {
|
||||
delete rootProject.buildDir
|
||||
}
|
||||
|
||||
id 'com.android.application' version '8.10.0' 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
|
||||
# Specifies the JVM arguments used for the daemon process.
|
||||
# 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.
|
||||
# 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
|
||||
@@ -19,5 +19,4 @@ android.useAndroidX=true
|
||||
# resources declared in the library itself and none from the library's dependencies,
|
||||
# thereby reducing the size of the R class for that library
|
||||
android.nonTransitiveRClass=true
|
||||
android.defaults.buildfeatures.buildconfig=true
|
||||
android.nonFinalResIds=false
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
#Mon May 02 15:39:12 BST 2022
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip
|
||||
distributionUrl=https\://code.gri.mw/DEV/gradle/releases/download/v8.11.1/gradle-8.11.1-bin.zip
|
||||
distributionPath=wrapper/dists
|
||||
zipStorePath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
+25
-12
@@ -1,16 +1,29 @@
|
||||
pluginManagement {
|
||||
repositories {
|
||||
gradlePluginPortal()
|
||||
google()
|
||||
mavenCentral()
|
||||
maven {
|
||||
credentials {
|
||||
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 {
|
||||
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
//rootProject.name = "Rust Template"
|
||||
include ':app'
|
||||
include ':app'
|
||||
@@ -0,0 +1,74 @@
|
||||
use std::process::Command;
|
||||
use std::{env, fs};
|
||||
|
||||
fn main() {
|
||||
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();
|
||||
|
||||
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: 181 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 37 KiB |
@@ -3,4 +3,5 @@ Name=Grim
|
||||
Exec=grim
|
||||
Icon=grim
|
||||
Type=Application
|
||||
Categories=Finance
|
||||
Categories=Finance
|
||||
MimeType=application/x-slatepack;text/plain;
|
||||
@@ -4,7 +4,7 @@ case $2 in
|
||||
x86_64|arm)
|
||||
;;
|
||||
*)
|
||||
echo "Usage: release_linux.sh [version] [platform]\n - platform: 'x86_64', 'arm'" >&2
|
||||
echo "Usage: release_linux.sh [platform] [version]\n - platform: 'x86_64', 'arm'" >&2
|
||||
exit 1
|
||||
esac
|
||||
|
||||
@@ -17,11 +17,11 @@ cd ..
|
||||
[[ $2 == "x86_64" ]] && arch+=(x86_64-unknown-linux-gnu)
|
||||
[[ $2 == "arm" ]] && arch+=(aarch64-unknown-linux-gnu)
|
||||
|
||||
# Start release build with zig linker for cross-compilation
|
||||
rustup target add ${arch}
|
||||
cargo install cargo-zigbuild
|
||||
cargo zigbuild --release --target ${arch}
|
||||
|
||||
# Create AppImage with https://github.com/AppImage/appimagetool
|
||||
cp target/${arch}/release/grim linux/Grim.AppDir/AppRun
|
||||
rm target/${arch}/release/*.AppImage
|
||||
appimagetool linux/Grim.AppDir target/${arch}/release/grim-v$1-linux-$2.AppImage
|
||||
appimagetool linux/Grim.AppDir target/${arch}/release/grim-v$2-linux-$1.AppImage
|
||||
+68
-3
@@ -25,7 +25,20 @@ share: teilen
|
||||
theme: 'Theme:'
|
||||
dark: Dunkel
|
||||
light: Hell
|
||||
file: Datei
|
||||
choose_file: Datei auswählen
|
||||
choose_folder: Ordner auswählen
|
||||
crash_report: Absturzbericht
|
||||
crash_report_warning: Anwendung wurde beim letzten Mal unerwartet geschlossen, Sie können den Absturzbericht mit Entwicklern teilen.
|
||||
confirmation: Bestätigung
|
||||
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:
|
||||
await_conf_amount: Erwarte Bestätigung
|
||||
await_fin_amount: Warten auf die Fertigstellung
|
||||
@@ -80,6 +93,7 @@ wallets:
|
||||
tx_canceled: Abgebrochen
|
||||
tx_cancelling: Abbrechen
|
||||
tx_finalizing: Finalisierung
|
||||
tx_posting: Buchungsvorgang
|
||||
tx_confirmed: Bestätigt
|
||||
txs: Transaktionen
|
||||
tx: Transaktion
|
||||
@@ -123,6 +137,11 @@ wallets:
|
||||
tx_receive_cancel_conf: 'Sind Sie sicher, dass Sie das Empfangen von %{amount} ツ abbrechen wollen?'
|
||||
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.
|
||||
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:'
|
||||
transport:
|
||||
desc: 'Transport verwenden, um Nachrichten synchron zu empfangen oder zu senden:'
|
||||
tor_network: Tor Netzwek
|
||||
@@ -135,7 +154,7 @@ transport:
|
||||
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_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
|
||||
bridges: Brücken
|
||||
bridges_desc: Richten Sie Brücken ein, um die Zensur des Tor-Netzwerks zu umgehen, wenn die normale Verbindung nicht funktioniert.
|
||||
@@ -285,8 +304,54 @@ network_settings:
|
||||
modal:
|
||||
cancel: Abbrechen
|
||||
save: Speichern
|
||||
confirmation: Bestätigung
|
||||
add: Hinzufügen
|
||||
modal_exit:
|
||||
description: Sind Sie sicher, dass Sie die Anwendung beenden wollen?
|
||||
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: '/'
|
||||
+68
-3
@@ -25,7 +25,20 @@ share: Share
|
||||
theme: 'Theme:'
|
||||
dark: Dark
|
||||
light: Light
|
||||
file: File
|
||||
choose_file: Choose file
|
||||
choose_folder: Choose folder
|
||||
crash_report: Crash report
|
||||
crash_report_warning: Application closed unexpectedly last time, you can share crash report with developers.
|
||||
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:
|
||||
await_conf_amount: Awaiting confirmation
|
||||
await_fin_amount: Awaiting finalization
|
||||
@@ -80,6 +93,7 @@ wallets:
|
||||
tx_canceled: Canceled
|
||||
tx_cancelling: Cancelling
|
||||
tx_finalizing: Finalizing
|
||||
tx_posting: Posting
|
||||
tx_confirmed: Confirmed
|
||||
txs: Transactions
|
||||
tx: Transaction
|
||||
@@ -123,6 +137,11 @@ wallets:
|
||||
tx_receive_cancel_conf: 'Are you sure you want to cancel receiving of %{amount} ツ?'
|
||||
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.
|
||||
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:'
|
||||
transport:
|
||||
desc: 'Use transport to receive or send messages synchronously:'
|
||||
tor_network: Tor network
|
||||
@@ -135,7 +154,7 @@ transport:
|
||||
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_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
|
||||
bridges: Bridges
|
||||
bridges_desc: Setup bridges to bypass Tor network censorship if usual connection is not working.
|
||||
@@ -285,8 +304,54 @@ network_settings:
|
||||
modal:
|
||||
cancel: Cancel
|
||||
save: Save
|
||||
confirmation: Confirmation
|
||||
add: Add
|
||||
modal_exit:
|
||||
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: /
|
||||
+68
-3
@@ -25,7 +25,20 @@ share: Partager
|
||||
theme: 'Thème:'
|
||||
dark: Sombre
|
||||
light: Clair
|
||||
file: Fichier
|
||||
choose_file: Choisir un fichier
|
||||
choose_folder: Choisir un dossier
|
||||
crash_report: Rapport d'échec
|
||||
crash_report_warning: L'application s'est fermée de manière inattendue la dernière fois, vous pouvez partager un rapport d'incident avec les développeurs.
|
||||
confirmation: Confirmation
|
||||
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:
|
||||
await_conf_amount: En attente de confirmation
|
||||
await_fin_amount: En attente de finalisation
|
||||
@@ -80,6 +93,7 @@ wallets:
|
||||
tx_canceled: Annulé
|
||||
tx_cancelling: Annulation
|
||||
tx_finalizing: Finalisation
|
||||
tx_posting: Publication
|
||||
tx_confirmed: Confirmé
|
||||
txs: Transactions
|
||||
tx: Transaction
|
||||
@@ -123,6 +137,11 @@ wallets:
|
||||
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.
|
||||
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:"
|
||||
transport:
|
||||
desc: 'Utilisez le transport pour recevoir ou envoyer des messages de manière synchronisée:'
|
||||
tor_network: Réseau Tor
|
||||
@@ -135,7 +154,7 @@ transport:
|
||||
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_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
|
||||
bridges: Passerelles
|
||||
bridges_desc: Configurez des passerelles pour contourner la censure du réseau Tor si la connexion habituelle ne fonctionne pas.
|
||||
@@ -285,8 +304,54 @@ network_settings:
|
||||
modal:
|
||||
cancel: Annuler
|
||||
save: Sauvegarder
|
||||
confirmation: Confirmation
|
||||
add: Ajouter
|
||||
modal_exit:
|
||||
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: /
|
||||
+68
-3
@@ -25,7 +25,20 @@ share: Поделиться
|
||||
theme: 'Тема:'
|
||||
dark: Тёмная
|
||||
light: Светлая
|
||||
file: Файл
|
||||
choose_file: Выбрать файл
|
||||
choose_folder: Выбрать папку
|
||||
crash_report: Отчёт о сбое
|
||||
crash_report_warning: В прошлый раз приложение неожиданно закрылось, вы можете поделиться отчетом о сбое с разработчиками.
|
||||
confirmation: Подтверждение
|
||||
enter_url: Введите URL-адрес
|
||||
max_short: МАКС
|
||||
files_location: Расположение файлов
|
||||
moving_files: Перемещение файлов
|
||||
wrong_path_error: Указан неправильный путь
|
||||
check_updates: Проверять обновления при запуске
|
||||
update_available: Доступно обновление!
|
||||
changelog: 'Журнал изменений:'
|
||||
wallets:
|
||||
await_conf_amount: Ожидает подтверждения
|
||||
await_fin_amount: Ожидает завершения
|
||||
@@ -80,6 +93,7 @@ wallets:
|
||||
tx_canceled: Отменено
|
||||
tx_cancelling: Отмена
|
||||
tx_finalizing: Завершение
|
||||
tx_posting: Публикация
|
||||
tx_confirmed: Подтверждено
|
||||
txs: Транзакции
|
||||
tx: Транзакция
|
||||
@@ -123,6 +137,11 @@ wallets:
|
||||
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: 'Введённое подтверждение оплаты недействительно:'
|
||||
transport:
|
||||
desc: 'Используйте транспорт для синхронных получения или отправки сообщений:'
|
||||
tor_network: Сеть Tor
|
||||
@@ -135,7 +154,7 @@ transport:
|
||||
incorrect_addr_err: 'Введённый адрес неверен:'
|
||||
tor_send_error: Во время отправки через Tor произошла ошибка, убедитесь, что получатель находится онлайн, транзакция была отменена.
|
||||
tor_autorun_desc: Запускать ли Tor сервис при открытии кошелька для синхронного получения транзакций.
|
||||
tor_sending: 'Отправка %{amount} ツ через Tor'
|
||||
tor_sending: Отправка через Tor
|
||||
tor_settings: Настройки Tor
|
||||
bridges: Мосты
|
||||
bridges_desc: Настройте мосты для обхода цензуры сети Tor, если обычное соединение не работает.
|
||||
@@ -285,8 +304,54 @@ network_settings:
|
||||
modal:
|
||||
cancel: Отмена
|
||||
save: Сохранить
|
||||
confirmation: Подтверждение
|
||||
add: Добавить
|
||||
modal_exit:
|
||||
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: ё
|
||||
+68
-3
@@ -25,7 +25,20 @@ share: Paylasmak
|
||||
theme: 'Tema:'
|
||||
dark: Karanlik
|
||||
light: Isik
|
||||
file: Dosya
|
||||
choose_file: Dosya seçin
|
||||
choose_folder: Klasör seç
|
||||
crash_report: Ariza Raporu
|
||||
crash_report_warning: Uygulama beklenmedik bir sekilde kapandi son kez, kilitlenme raporunu gelistiricilerle paylasabilirsiniz.
|
||||
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:
|
||||
await_conf_amount: Onay bekleniyor
|
||||
await_fin_amount: Tamamlanma bekleniyor
|
||||
@@ -80,6 +93,7 @@ wallets:
|
||||
tx_canceled: Iptal edildi
|
||||
tx_cancelling: Iptal ediliyor
|
||||
tx_finalizing: Islem tamamlaniyor
|
||||
tx_posting: Islem kaydetme
|
||||
tx_confirmed: Onaylandi
|
||||
txs: Islemler
|
||||
tx: Islem
|
||||
@@ -123,6 +137,11 @@ wallets:
|
||||
tx_receive_cancel_conf: Gelen tx iptal
|
||||
rec_phrase_not_found: Sifre kelime bulunmuyor
|
||||
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:'
|
||||
transport:
|
||||
desc: 'Adresten senkronize GONDER veya AL:'
|
||||
tor_network: Tor network
|
||||
@@ -135,7 +154,7 @@ transport:
|
||||
incorrect_addr_err: 'Girilen adres hatali:'
|
||||
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_sending: 'Tor adrese %{amount} ツ gonderiliyor.'
|
||||
tor_sending: Tor adrese gonderiliyor
|
||||
tor_settings: Tor Ayarlar
|
||||
bridges: Bridges
|
||||
bridges_desc: Setup bridges to bypass Tor network censorship if usual connection is not working.
|
||||
@@ -285,8 +304,54 @@ network_settings:
|
||||
modal:
|
||||
cancel: Iptal
|
||||
save: Kaydet
|
||||
confirmation: Onay
|
||||
add: Ekle
|
||||
modal_exit:
|
||||
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,357 @@
|
||||
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: 输入 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} or %{transport} 更改钱包设置, 请按屏幕底部的按钮 %{settings} 按钮.'
|
||||
title: 钱包
|
||||
create_desc: 创建或种子单词导入已有钱包.
|
||||
add: 添加钱包
|
||||
name: '用户名:'
|
||||
pass: '密码:'
|
||||
pass_empty: 输入钱包的密码
|
||||
current_pass: '目前密码:'
|
||||
new_pass: '新密码:'
|
||||
min_tx_conf_count: '确认交易的最低数量:'
|
||||
recover: 恢复
|
||||
recovery_phrase: 助记词
|
||||
words_count: '字数:'
|
||||
enter_word: '输入单词 #%{number}:'
|
||||
not_valid_word: 输入的单词无效
|
||||
not_valid_phrase: 输入的助记词无效
|
||||
create_phrase_desc: 已安全地写下并保存助记词.
|
||||
restore_phrase_desc: 从已保存的助记词中输入.
|
||||
setup_conn_desc: 选择钱包连接到网络的方式.
|
||||
conn_method: 连接方式
|
||||
ext_conn: '外部连接:'
|
||||
add_node: 添加节点
|
||||
node_url: '节点网址:'
|
||||
node_secret: 'API 密钥 (可选):'
|
||||
invalid_url: 输入的网址无效
|
||||
open: 打开钱包
|
||||
wrong_pass: 输入的密码错误
|
||||
locked: 已锁定
|
||||
unlocked: 解锁
|
||||
enable_node: '通过选择屏幕底部的按钮 %{settings} 启用集成节点以使用钱包或更改连接设置.'
|
||||
node_loading: '集成节点同步后钱包会加载,你可选择屏幕底部的按钮 %{settings} 更改连接.'
|
||||
loading: 正在加载
|
||||
closing: 正在关闭
|
||||
checking: 检查中
|
||||
default_wallet: 默认钱包
|
||||
new_account_desc: '输入新帐户的名称:'
|
||||
wallet_loading: 加载钱包
|
||||
wallet_closing: 关闭钱包
|
||||
wallet_checking: 检查钱包
|
||||
tx_loading: 加载事务
|
||||
default_account: 默认账户
|
||||
accounts: 账户
|
||||
tx_sent: 已发送
|
||||
tx_received: 已接收
|
||||
tx_sending: 发送中
|
||||
tx_receiving: 接收中
|
||||
tx_confirming: 等待确认
|
||||
tx_canceled: 已取消
|
||||
tx_cancelling: 取消
|
||||
tx_finalizing: 完成
|
||||
tx_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: 使用蒲公英
|
||||
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: '輸入的付款證明無效:'
|
||||
transport:
|
||||
desc: '使用传输同步接收或发送消息:'
|
||||
tor_network: Tor 网络
|
||||
connected: 已连接
|
||||
connecting: 正在连接
|
||||
disconnecting: 断开连接
|
||||
conn_error: 连接错误
|
||||
disconnected: 已断开连接
|
||||
receiver_address: '接收者的地址:'
|
||||
incorrect_addr_err: '输入的地址不正确:'
|
||||
tor_send_error: 通过 Tor 发送时出错,请确保接收方在线, 交易已取消.
|
||||
tor_autorun_desc: 是否在开钱包时启动 Tor 服务以同步接收交易.
|
||||
tor_sending: 通过 Tor 发送
|
||||
tor_settings: Tor 设置
|
||||
bridges: 桥梁
|
||||
bridges_desc: 如果常规连接不正常,设置网桥,可以绕过 Tor 网络审查.
|
||||
bin_file: '二进制文件:'
|
||||
conn_line: '连接线:'
|
||||
bridges_disabled: 网桥已禁用
|
||||
bridge_name: '网桥%{b}'
|
||||
network:
|
||||
self: 网络
|
||||
type: '网络类型:'
|
||||
mainnet: 主网
|
||||
testnet: 测试网
|
||||
connections: 连接
|
||||
node: 集成节点
|
||||
metrics: 指标
|
||||
mining: 挖矿
|
||||
settings: 节点设置
|
||||
enable_node: 启用节点
|
||||
autorun: 自动运行
|
||||
disabled_server: '按屏幕左上角的按钮 %{dots}启用集成节点或添加其他连接方法.'
|
||||
no_ips: T您的系统上没有可用的 IP 地址,服务器无法启动,请检查您的网络连接.
|
||||
available: 可用
|
||||
not_available: 不可用
|
||||
availability_check: 检查是否可用
|
||||
android_warning: Android 用户注意 .要成功同步集成节点,您必须在手机的系统设置中允许访问通知并取消 Grim 应用程序的电池使用限制.这是在后台正确运行应用程序的必要操作.
|
||||
sync_status:
|
||||
node_restarting: 节点正在重新启动
|
||||
node_down: 节点已关闭
|
||||
initial: 节点正在启动
|
||||
no_sync: 节点正在运行
|
||||
awaiting_peers: 等待网络对点
|
||||
header_sync: 正下载标题
|
||||
header_sync_percent: '正在下载标题: %{percent}%'
|
||||
tx_hashset_pibd: 下载状态 (PIBD)
|
||||
tx_hashset_pibd_percent: '下载状态 (PIBD): %{percent}%'
|
||||
tx_hashset_download: 正在下载状态
|
||||
tx_hashset_download_percent: '下载状态: %{percent}%'
|
||||
tx_hashset_setup_history: '正在准备状态(历史记录): %{percent}%'
|
||||
tx_hashset_setup_position: '正在准备状态(位置): %{percent}%'
|
||||
tx_hashset_setup: 正在准备状态
|
||||
tx_hashset_range_proofs_validation: '验证状态(范围证明): %{percent}%'
|
||||
tx_hashset_kernels_validation: '正在验证状态(核心): %{percent}%'
|
||||
tx_hashset_save: 最终确定链状态
|
||||
body_sync: 下载区块
|
||||
body_sync_percent: '下载区块中: %{percent}%'
|
||||
shutdown: 节点正在关闭
|
||||
network_node:
|
||||
header: 标题
|
||||
block: 区块
|
||||
hash: 哈希值
|
||||
height: 高度
|
||||
difficulty: 难度
|
||||
time: 时间
|
||||
main_pool: 主池
|
||||
stem_pool: stem池
|
||||
data: 数据
|
||||
size: 大小 (GB)
|
||||
peers: 网络对点
|
||||
error_clean: 点数据已损坏,需要重新同步.
|
||||
resync: 重新同步
|
||||
error_p2p_api: '%{p2p_api} 服务器初始化时出错,请选择屏幕底部的按钮 %{p2p_api} 来检查 %{settings}设置.'
|
||||
error_config: '配置初始化时出错,请选择屏幕底部的按钮 %{settings} 检查设置.'
|
||||
error_unknown: '初始化时出错,请选择屏幕底部的按钮 %{settings} 来检查集成节点设置,或者重新同步.'
|
||||
network_metrics:
|
||||
loading: 指标在同步后将可用
|
||||
emission: 发射
|
||||
inflation: 通货膨胀
|
||||
supply: 供应
|
||||
block_time: Block time
|
||||
reward: 奖励
|
||||
difficulty_window: '难度窗口 %{size}'
|
||||
network_mining:
|
||||
loading: 同步后即可挖矿
|
||||
info: '挖矿服务器已启用,您可以通过选择屏幕底部的按钮 %{settings} 来更改其设置。连接设备后,数据会更新.'
|
||||
restart_server_required: 需要重启服务器才能应用更改.
|
||||
rewards_wallet: 奖励钱包
|
||||
server: 阶层服务器
|
||||
address: 地址
|
||||
miners: 矿工
|
||||
devices: 设备
|
||||
blocks_found: 找到的区块
|
||||
hashrate: '哈希率 (C%{bits})'
|
||||
connected: 已连接
|
||||
disconnected: 已断开连接
|
||||
network_settings:
|
||||
change_value: 更改值
|
||||
stratum_ip: '层 IP 地址:'
|
||||
stratum_port: '层端口:'
|
||||
port_unavailable: 指定的端口不可用
|
||||
restart_node_required: 需要重启节点才能应用更改.
|
||||
choose_wallet: 选择钱包
|
||||
stratum_wallet_warning: 必须打开钱包才能获得奖励.
|
||||
enable: 启用
|
||||
disable: 禁用
|
||||
restart: 重新启动
|
||||
server: 服务器
|
||||
api_ip: 'API IP 地址:'
|
||||
api_port: 'API 端口:'
|
||||
api_secret: '其它API 和 V2 所有者 API 令牌:'
|
||||
foreign_api_secret: '外部 API 令牌:'
|
||||
disabled: 已禁用
|
||||
enabled: 已启用
|
||||
ftl: '未来时间限制 (FTL):'
|
||||
ftl_description: 限制未来多长时间, 相对于节点的本地时间,以秒为单位, 新区块的时间戳可以被接受.
|
||||
not_valid_value: 输入的值无效
|
||||
full_validation: 完全验证
|
||||
full_validation_description: 在处理每个区块时是否运行全链验证(同步期间除外).
|
||||
archive_mode: 存档模式
|
||||
archive_mode_desc: 以全部存档模式运行全节点(同步需要更多的磁盘空间和时间).
|
||||
attempt_time: '尝试挖矿时间 (秒):'
|
||||
attempt_time_desc: 在停止并从池中重新收集交易之前尝试对特定标题进行挖矿的时间
|
||||
min_share_diff: '可接受的最低份额难度:'
|
||||
reset_settings_desc: 将节点设置重置为默认值
|
||||
reset_settings: 重置设置
|
||||
reset: 重置
|
||||
tx_pool: 交易池
|
||||
pool_fee: '接受到矿池的基本费用:'
|
||||
reorg_period: '重组缓存保留期(以分钟为单位):'
|
||||
max_tx_pool: '池中的最大交易数:'
|
||||
max_tx_stempool: 'stem池中的最大交易数:'
|
||||
max_tx_weight: '可以选择构建区块交易的最大总权重:'
|
||||
epoch_duration: '纪元持续时间(以秒为单位):'
|
||||
embargo_timer: '禁止计时器(以秒为单位):'
|
||||
aggregation_period: '聚合周期(以秒为单位):'
|
||||
stem_probability: 'stem助记词概率:'
|
||||
stem_txs: stem交易
|
||||
p2p_server: P2P 服务器
|
||||
p2p_port: 'P2P 端口:'
|
||||
add_seed: 添加 DNS 种子
|
||||
seed_address: 'DNS 种子地址:'
|
||||
add_peer: 添加网络对点
|
||||
peer_address: '网络对点地址:'
|
||||
peer_address_error: '以正确的格式输入 IP 地址或 DNS 名称(确保指定的主机可用),例如:192.168.0.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_peers_desc: 重置网络对点数据。仅当查找网络对点出现问题时,才请谨慎使用它.
|
||||
reset_peers: 重置网络对点
|
||||
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: /
|
||||
@@ -1,49 +1,65 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Grim</string>
|
||||
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>grim</string>
|
||||
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>AppIcon</string>
|
||||
|
||||
<key>CFBundleIconName</key>
|
||||
<string>AppIcon</string>
|
||||
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>mw.gri.macos</string>
|
||||
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
|
||||
<key>CFBundleName</key>
|
||||
<string>Grim</string>
|
||||
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>0.1.0</string>
|
||||
|
||||
<key>CFBundleSupportedPlatforms</key>
|
||||
<array>
|
||||
<string>MacOSX</string>
|
||||
</array>
|
||||
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
|
||||
<key>LSApplicationCategoryType</key>
|
||||
<string>public.app-category.finance</string>
|
||||
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>2024</string>
|
||||
</dict>
|
||||
</plist>
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Grim</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>grim</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>AppIcon</string>
|
||||
<key>CFBundleIconName</key>
|
||||
<string>AppIcon</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>mw.gri.macos</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>Grim</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>0.2.3</string>
|
||||
<key>CFBundleSupportedPlatforms</key>
|
||||
<array>
|
||||
<string>MacOSX</string>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>Grim needs an access to your camera to scan QR code.</string>
|
||||
<key>CFBundleDocumentTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>Apple SimpleText document</string>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Viewer</string>
|
||||
<key>LSItemContentTypes</key>
|
||||
<array>
|
||||
<string>com.apple.traditional-mac-plain-text</string>
|
||||
</array>
|
||||
<key>NSDocumentClass</key>
|
||||
<string>Document</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>Unknown document</string>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Viewer</string>
|
||||
<key>LSItemContentTypes</key>
|
||||
<array>
|
||||
<string>public.data</string>
|
||||
</array>
|
||||
<key>NSDocumentClass</key>
|
||||
<string>Document</string>
|
||||
</dict>
|
||||
</array>
|
||||
<key>LSApplicationCategoryType</key>
|
||||
<string>public.app-category.finance</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>2024</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
!.gitignore
|
||||
grim
|
||||
+15
-22
@@ -1,22 +1,21 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
case $2 in
|
||||
case $1 in
|
||||
x86_64|arm|universal)
|
||||
;;
|
||||
*)
|
||||
echo "Usage: release_macos.sh [version] [platform]\n - platform: 'x86_64', 'arm', 'universal'" >&2
|
||||
echo "Usage: release_macos.sh [platform] [version]\n - platform: 'x86_64', 'arm', 'universal'" >&2
|
||||
exit 1
|
||||
esac
|
||||
|
||||
if [[ ! -v SDKROOT ]]; then
|
||||
if [[ "$OSTYPE" != "darwin"* ]]; then
|
||||
if [ -z ${SDKROOT+x} ]; then
|
||||
echo "MacOS SDKROOT is not set"
|
||||
exit 1
|
||||
elif [[ -z "SDKROOT" ]]; then
|
||||
echo "MacOS SDKROOT is set to the empty string"
|
||||
exit 1
|
||||
else
|
||||
else
|
||||
echo "Use MacOS SDK: ${SDKROOT}"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Setup build directory
|
||||
@@ -25,31 +24,25 @@ cd ${BASEDIR}
|
||||
cd ..
|
||||
|
||||
# Setup platform
|
||||
[[ $1 == "x86_64" ]] && arch+=(x86_64-apple-darwin)
|
||||
[[ $1 == "arm" ]] && arch+=(aarch64-apple-darwin)
|
||||
|
||||
rustup target add x86_64-apple-darwin
|
||||
rustup target add aarch64-apple-darwin
|
||||
|
||||
rm -rf target/x86_64-apple-darwin
|
||||
rm -rf target/aarch64-apple-darwin
|
||||
|
||||
[[ $2 == "x86_64" ]] && arch+=(x86_64-apple-darwin)
|
||||
[[ $2 == "arm" ]] && arch+=(aarch64-apple-darwin)
|
||||
[[ $2 == "universal" ]] && arch+=(universal2-apple-darwin)
|
||||
|
||||
# Start release build with zig linker for cross-compilation
|
||||
# zig 0.12+ required
|
||||
[[ $1 == "universal" ]]; arch+=(universal2-apple-darwin)
|
||||
cargo install cargo-zigbuild
|
||||
cargo zigbuild --release --target ${arch}
|
||||
rm -rf .intentionally-empty-file.o
|
||||
mkdir macos/Grim.app/Contents/MacOS
|
||||
|
||||
rm -f .intentionally-empty-file.o
|
||||
|
||||
yes | cp -rf target/${arch}/release/grim macos/Grim.app/Contents/MacOS
|
||||
|
||||
### Sign .app resources on change:
|
||||
# Sign .app resources on change:
|
||||
#rcodesign generate-self-signed-certificate
|
||||
#rcodesign sign --pem-file cert.pem macos/Grim.app
|
||||
|
||||
# Create release package
|
||||
FILE_NAME=grim-v$1-macos-$2.zip
|
||||
rm -rf target/${arch}/release/${FILE_NAME}
|
||||
FILE_NAME=grim-v$2-macos-$1.zip
|
||||
cd macos
|
||||
zip -r ${FILE_NAME} Grim.app
|
||||
mv ${FILE_NAME} ../target/${arch}/release
|
||||
|
||||
Submodule
+1
Submodule node added at 376c85bab5
+113
-64
@@ -1,81 +1,130 @@
|
||||
#!/bin/bash
|
||||
|
||||
usage="Usage: build_run_android.sh [type] [platform]\n - type: 'debug', 'release'\n - platform: 'v7', 'v8'"
|
||||
usage="Usage: android.sh [type] [platform|version] [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
|
||||
debug|release)
|
||||
build|lib|release)
|
||||
;;
|
||||
*)
|
||||
printf "$usage"
|
||||
exit 1
|
||||
esac
|
||||
|
||||
case $2 in
|
||||
v7|v8)
|
||||
;;
|
||||
*)
|
||||
printf "$usage"
|
||||
exit 1
|
||||
esac
|
||||
|
||||
# Setup build directory
|
||||
BASEDIR=$(cd $(dirname $0) && pwd)
|
||||
cd ${BASEDIR}
|
||||
cd ..
|
||||
|
||||
# Setup release argument
|
||||
type=$1
|
||||
[[ ${type} == "release" ]] && release_param="--profile release-apk"
|
||||
|
||||
# Setup platform argument
|
||||
[[ $2 == "v7" ]] && arch+=(armeabi-v7a)
|
||||
[[ $2 == "v8" ]] && arch+=(arm64-v8a)
|
||||
|
||||
# Setup platform path
|
||||
[[ $2 == "v7" ]] && platform+=(armv7-linux-androideabi)
|
||||
[[ $2 == "v8" ]] && platform+=(aarch64-linux-android)
|
||||
|
||||
# Install platform
|
||||
[[ $2 == "v7" ]] && rustup target install armv7-linux-androideabi
|
||||
[[ $2 == "v8" ]] && rustup target install aarch64-linux-android
|
||||
|
||||
# Build native code
|
||||
cargo install cargo-ndk
|
||||
|
||||
sed -i -e 's/"rlib"/"cdylib","rlib"/g' Cargo.toml
|
||||
|
||||
# temp fix for https://stackoverflow.com/questions/57193895/error-use-of-undeclared-identifier-pthread-mutex-robust-cargo-build-liblmdb-s
|
||||
success=0
|
||||
export CPPFLAGS="-DMDB_USE_ROBUST=0" && export CFLAGS="-DMDB_USE_ROBUST=0"
|
||||
cargo ndk -t ${arch} build ${release_param}
|
||||
unset CPPFLAGS && unset CFLAGS
|
||||
cargo ndk -t ${arch} -o android/app/src/main/jniLibs build ${release_param}
|
||||
if [ $? -eq 0 ]
|
||||
then
|
||||
success=1
|
||||
if [[ $1 == "build" ]]; then
|
||||
case $2 in
|
||||
v7|v8|x86)
|
||||
;;
|
||||
*)
|
||||
printf "$usage"
|
||||
exit 1
|
||||
esac
|
||||
fi
|
||||
|
||||
sed -i -e 's/"cdylib","rlib"/"rlib"/g' Cargo.toml
|
||||
# Setup build directory
|
||||
BASEDIR=$(cd "$(dirname "$0")" && pwd)
|
||||
cd "${BASEDIR}" || exit 1
|
||||
cd ..
|
||||
|
||||
# Build Android application and launch at all connected devices
|
||||
if [ $success -eq 1 ]
|
||||
then
|
||||
cd android
|
||||
# Install platforms and tools
|
||||
rustup target add armv7-linux-androideabi
|
||||
rustup target add aarch64-linux-android
|
||||
rustup target add x86_64-linux-android
|
||||
cargo install cargo-ndk
|
||||
|
||||
# Setup gradle argument
|
||||
[[ $1 == "release" ]] && gradle_param+=(assembleRelease)
|
||||
[[ $1 == "debug" ]] && gradle_param+=(build)
|
||||
success=1
|
||||
|
||||
### Build native code
|
||||
function build_lib() {
|
||||
[[ $1 == "v7" ]] && arch=armeabi-v7a
|
||||
[[ $1 == "v8" ]] && arch=arm64-v8a
|
||||
[[ $1 == "x86" ]] && arch=x86_64
|
||||
|
||||
sed -i -e 's/"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
|
||||
export CPPFLAGS="-DMDB_USE_ROBUST=0" && export CFLAGS="-DMDB_USE_ROBUST=0"
|
||||
cargo ndk -t "${arch}" -o android/app/src/main/jniLibs build --profile release-apk
|
||||
if [ $? -ne 0 ]; then
|
||||
success=0
|
||||
fi
|
||||
unset CPPFLAGS && unset CFLAGS
|
||||
|
||||
sed -i -e 's/"cdylib","rlib"]/"rlib"]/g' Cargo.toml
|
||||
rm -f Cargo.toml-e
|
||||
}
|
||||
|
||||
### Build application
|
||||
function build_apk() {
|
||||
flavor=$3
|
||||
[[ flavor == "" ]] && flavor="local"
|
||||
cd android || exit 1
|
||||
./gradlew clean
|
||||
./gradlew ${gradle_param}
|
||||
# Build signed apk if keystore exists
|
||||
if [ ! -f keystore.properties ]; then
|
||||
./gradlew assemble${flavor}Debug
|
||||
if [ $? -ne 0 ]; then
|
||||
success=0
|
||||
fi
|
||||
apk_path=app/build/outputs/apk/${flavor}/debug/app-${flavor}-debug.apk
|
||||
else
|
||||
./gradlew assemble${flavor}SignedRelease
|
||||
if [ $? -ne 0 ]; then
|
||||
success=0
|
||||
fi
|
||||
apk_path=app/build/outputs/apk/${flavor}/signedRelease/app-${flavor}-signedRelease.apk
|
||||
fi
|
||||
|
||||
# Setup apk path
|
||||
[[ $1 == "release" ]] && apk_path+=(app/build/outputs/apk/release/app-release.apk)
|
||||
[[ $1 == "debug" ]] && apk_path+=(app/build/outputs/apk/debug/app-debug.apk)
|
||||
if [[ $1 == "" ]] && [ $success -eq 1 ]; then
|
||||
# Launch application at all connected devices.
|
||||
for SERIAL in $(adb devices | grep -v List | cut -f 1);
|
||||
do
|
||||
adb -s "$SERIAL" install ${apk_path}
|
||||
sleep 1s
|
||||
adb -s "$SERIAL" shell am start -n mw.gri.android/.MainActivity;
|
||||
done
|
||||
elif [ $success -eq 1 ]; then
|
||||
# Get version
|
||||
version=$2
|
||||
if [[ -z "$version" ]]; then
|
||||
version=v$(grep -m 1 -Po 'version = "\K[^"]*' ../Cargo.toml)
|
||||
fi
|
||||
# Setup release file name
|
||||
name=grim-${version}-android-$1.apk
|
||||
[[ $1 == "arm" ]] && name=grim-${version}-android.apk
|
||||
rm -f "${name}"
|
||||
mv ${apk_path} "${name}"
|
||||
# Calculate checksum
|
||||
checksum=grim-${version}-android-$1-sha256sum.txt
|
||||
[[ $1 == "arm" ]] && checksum=grim-${version}-android-sha256sum.txt
|
||||
rm -f "${checksum}"
|
||||
sha256sum "${name}" > "${checksum}"
|
||||
fi
|
||||
|
||||
for SERIAL in $(adb devices | grep -v List | cut -f 1);
|
||||
do
|
||||
adb -s $SERIAL install ${apk_path}
|
||||
sleep 1s
|
||||
adb -s $SERIAL shell am start -n mw.gri.android/.MainActivity;
|
||||
done
|
||||
fi
|
||||
cd ..
|
||||
}
|
||||
|
||||
rm -rf android/app/src/main/jniLibs/*
|
||||
if [[ $1 == "lib" ]]; 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"
|
||||
[ $success -eq 1 ] && build_apk "" "" "$3"
|
||||
[ $success -eq 1 ] && exit 0
|
||||
else
|
||||
rm -rf target/release-apk
|
||||
rm -rf target/aarch64-linux-android
|
||||
rm -rf target/x86_64-linux-android
|
||||
rm -rf target/armv7-linux-androideabi
|
||||
build_lib "v7"
|
||||
[ $success -eq 1 ] && build_lib "v8"
|
||||
[ $success -eq 1 ] && build_apk "arm" "$2" "$3"
|
||||
rm -rf android/app/src/main/jniLibs/*
|
||||
[ $success -eq 1 ] && build_lib "x86"
|
||||
[ $success -eq 1 ] && build_apk "x86_64" "$2" "$3"
|
||||
[ $success -eq 1 ] && exit 0
|
||||
fi
|
||||
|
||||
exit 1
|
||||
+10
-8
@@ -1,25 +1,27 @@
|
||||
#!/bin/bash
|
||||
|
||||
case $1 in
|
||||
debug|release)
|
||||
debug|build)
|
||||
;;
|
||||
*)
|
||||
echo "Usage: build_run.sh [type] where is type is 'debug' or 'release'" >&2
|
||||
echo "Usage: build_run.sh [type] where is type is 'debug' or 'build'" >&2
|
||||
exit 1
|
||||
esac
|
||||
|
||||
# Setup build directory
|
||||
BASEDIR=$(cd $(dirname $0) && pwd)
|
||||
cd ${BASEDIR}
|
||||
BASEDIR=$(cd "$(dirname $0)" && pwd)
|
||||
cd "${BASEDIR}" || return
|
||||
cd ..
|
||||
|
||||
# Build application
|
||||
type=$1
|
||||
[[ ${type} == "release" ]] && release_param+=(--release)
|
||||
cargo build ${release_param[@]}
|
||||
[[ ${type} == "build" ]] && release_param+=(--release)
|
||||
cargo --config profile.release.incremental=true build "${release_param[@]}"
|
||||
|
||||
# Start application
|
||||
if [ $? -eq 0 ]
|
||||
then
|
||||
./target/${type}/grim
|
||||
fi
|
||||
path=${type}
|
||||
[[ ${type} == "build" ]] && path="release"
|
||||
./target/"${path}"/grim
|
||||
fi
|
||||
|
||||
Executable
+99
@@ -0,0 +1,99 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Usage to bump version
|
||||
# ./version.sh patch
|
||||
# ./version.sh minor
|
||||
# ./version.sh major
|
||||
|
||||
# Setup base directory
|
||||
BASEDIR=$(cd $(dirname $0) && pwd)
|
||||
cd ${BASEDIR}
|
||||
cd ..
|
||||
|
||||
# Exit script if command fails or uninitialized variables used
|
||||
set -euo pipefail
|
||||
|
||||
# ==================================
|
||||
# Verify repo is clean
|
||||
# ==================================
|
||||
|
||||
# List uncommitted changes and
|
||||
# check if the output is not empty
|
||||
if [ -n "$(git status --porcelain)" ]; then
|
||||
# Print error message
|
||||
printf "\nError: repo has uncommitted changes\n\n"
|
||||
# Exit with error code
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ==================================
|
||||
# Get latest version from git tags
|
||||
# ==================================
|
||||
|
||||
# List git tags sorted lexicographically
|
||||
# so version numbers sorted correctly
|
||||
GIT_TAGS=$(git tag --sort=version:refname)
|
||||
|
||||
# Get last line of output which returns the
|
||||
# last tag (most recent version)
|
||||
GIT_TAG_LATEST=$(echo "$GIT_TAGS" | tail -n 1)
|
||||
|
||||
# If no tag found, default to v0.1.0
|
||||
if [ -z "$GIT_TAG_LATEST" ]; then
|
||||
GIT_TAG_LATEST="v0.1.0"
|
||||
fi
|
||||
|
||||
# Strip prefix 'v' from the tag to easily increment
|
||||
GIT_TAG_LATEST=$(echo "$GIT_TAG_LATEST" | sed 's/^v//')
|
||||
|
||||
# ==================================
|
||||
# Increment version number
|
||||
# ==================================
|
||||
|
||||
# Get version type from first argument passed to script
|
||||
VERSION_TYPE="${1-}"
|
||||
VERSION_NEXT=""
|
||||
|
||||
if [ "$VERSION_TYPE" = "patch" ]; then
|
||||
# Increment patch version
|
||||
VERSION_NEXT="$(echo "$GIT_TAG_LATEST" | awk -F. '{$NF++; print $1"."$2"."$NF}')"
|
||||
elif [ "$VERSION_TYPE" = "minor" ]; then
|
||||
# Increment minor version
|
||||
VERSION_NEXT="$(echo "$GIT_TAG_LATEST" | awk -F. '{$2++; $3=0; print $1"."$2"."$3}')"
|
||||
elif [ "$VERSION_TYPE" = "major" ]; then
|
||||
# Increment major version
|
||||
VERSION_NEXT="$(echo "$GIT_TAG_LATEST" | awk -F. '{$1++; $2=0; $3=0; print $1"."$2"."$3}')"
|
||||
else
|
||||
# Print error for unknown versioning type
|
||||
printf "\nError: invalid VERSION_TYPE arg passed, must be 'patch', 'minor' or 'major'\n\n"
|
||||
# Exit with error code
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Update version for Windows installer.
|
||||
sed -i '' -e 's/" Version="[^\"]*"/" Version="'"$VERSION_NEXT"'"/g' wix/main.wxs
|
||||
sed -i '' -e 's/<Package Id="[^\"]*"/<Package Id="'"$(uuidgen)"'"/g' wix/main.wxs
|
||||
|
||||
# Update Android version in build.gradle
|
||||
sed -i'.bak' -e 's/versionName [0-9a-zA-Z -_]*/versionName "'"$VERSION_NEXT"'"/' android/app/build.gradle
|
||||
rm -f android/app/build.gradle.bak
|
||||
|
||||
# Update version in Cargo.toml
|
||||
sed -i'.bak' -e "s/^version = .*/version = \"$VERSION_NEXT\"/" Cargo.toml
|
||||
rm -f Cargo.toml.bak
|
||||
|
||||
# Update Cargo.lock as this changes when
|
||||
# updating the version in your manifest
|
||||
cargo update -p grim
|
||||
|
||||
# Commit the changes
|
||||
git add .
|
||||
git commit -m "release: v$VERSION_NEXT"
|
||||
|
||||
# ==================================
|
||||
# Create git tag for new version
|
||||
# ==================================
|
||||
|
||||
# Create a tag and push to master branch
|
||||
git tag "v$VERSION_NEXT" master
|
||||
#git push origin master --follow-tags
|
||||
@@ -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
+168
-158
@@ -13,16 +13,16 @@
|
||||
// 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 egui::{Align, Context, CursorIcon, Layout, Modifiers, Rect, ResizeDirection, Rounding, Stroke, ViewportCommand};
|
||||
use egui::epaint::{RectShape};
|
||||
use egui::os::OperatingSystem;
|
||||
|
||||
use crate::AppConfig;
|
||||
use crate::gui::Colors;
|
||||
use crate::gui::icons::{ARROWS_IN, ARROWS_OUT, CARET_DOWN, MOON, SUN, X};
|
||||
use crate::gui::platform::PlatformCallbacks;
|
||||
use crate::gui::views::{Content, TitlePanel, View};
|
||||
use crate::gui::views::types::ContentContainer;
|
||||
use crate::gui::views::{Content, KeyboardContent, Modal, TitlePanel, View};
|
||||
use crate::gui::Colors;
|
||||
use crate::AppConfig;
|
||||
|
||||
lazy_static! {
|
||||
/// State to check if platform Back button was pressed.
|
||||
@@ -31,27 +31,52 @@ lazy_static! {
|
||||
|
||||
/// Implements ui entry point and contains platform-specific callbacks.
|
||||
pub struct App<Platform> {
|
||||
/// Platform specific callbacks handler.
|
||||
pub(crate) platform: Platform,
|
||||
/// Handles platform-specific functionality.
|
||||
pub platform: Platform,
|
||||
|
||||
/// Main ui content.
|
||||
/// Main content.
|
||||
content: Content,
|
||||
|
||||
/// Last window resize direction.
|
||||
resize_direction: Option<ResizeDirection>
|
||||
resize_direction: Option<ResizeDirection>,
|
||||
/// Flag to check if it's first draw.
|
||||
first_draw: bool
|
||||
}
|
||||
|
||||
impl<Platform: PlatformCallbacks> App<Platform> {
|
||||
pub fn new(platform: Platform) -> Self {
|
||||
Self { platform, content: Content::default(), resize_direction: None }
|
||||
Self {
|
||||
platform,
|
||||
content: Content::default(),
|
||||
resize_direction: None,
|
||||
first_draw: true
|
||||
}
|
||||
}
|
||||
|
||||
/// Called of first content draw.
|
||||
fn on_first_draw(&mut self, ctx: &Context) {
|
||||
// Set platform context.
|
||||
if View::is_desktop() {
|
||||
self.platform.set_context(ctx);
|
||||
}
|
||||
// Setup visuals.
|
||||
crate::setup_fonts(ctx);
|
||||
crate::setup_visuals(ctx);
|
||||
}
|
||||
|
||||
/// Draw application content.
|
||||
pub fn ui(&mut self, ctx: &Context) {
|
||||
if self.first_draw {
|
||||
self.on_first_draw(ctx);
|
||||
self.first_draw = false;
|
||||
}
|
||||
|
||||
// Handle Esc keyboard key event and platform Back button key event.
|
||||
let back_pressed = BACK_BUTTON_PRESSED.load(Ordering::Relaxed);
|
||||
if ctx.input_mut(|i| i.consume_key(Modifiers::NONE, egui::Key::Escape)) || back_pressed {
|
||||
self.content.on_back();
|
||||
if back_pressed || ctx.input_mut(|i| i.consume_key(Modifiers::NONE, egui::Key::Escape) ||
|
||||
i.consume_key(Modifiers::NONE, egui::Key::BrowserBack)) {
|
||||
// Pass event to content.
|
||||
self.content.on_back(ctx, &self.platform);
|
||||
if back_pressed {
|
||||
BACK_BUTTON_PRESSED.store(false, Ordering::Relaxed);
|
||||
}
|
||||
@@ -59,16 +84,15 @@ impl<Platform: PlatformCallbacks> App<Platform> {
|
||||
ctx.request_repaint();
|
||||
}
|
||||
|
||||
// Handle Close event (on desktop).
|
||||
if ctx.input(|i| i.viewport().close_requested()) {
|
||||
// Handle Close event on desktop.
|
||||
if View::is_desktop() && ctx.input(|i| i.viewport().close_requested()) {
|
||||
if !self.content.exit_allowed {
|
||||
ctx.send_viewport_cmd(ViewportCommand::CancelClose);
|
||||
Content::show_exit_modal();
|
||||
} else {
|
||||
let (w, h) = View::window_size(ctx);
|
||||
AppConfig::save_window_size(w, h);
|
||||
ctx.input(|i| {
|
||||
if let Some(rect) = i.viewport().inner_rect {
|
||||
AppConfig::save_window_size(rect.width(), rect.height());
|
||||
}
|
||||
if let Some(rect) = i.viewport().outer_rect {
|
||||
AppConfig::save_window_pos(rect.left(), rect.top());
|
||||
}
|
||||
@@ -76,95 +100,105 @@ impl<Platform: PlatformCallbacks> App<Platform> {
|
||||
}
|
||||
}
|
||||
|
||||
// Show main content with custom frame on desktop.
|
||||
// Show main content.
|
||||
egui::CentralPanel::default()
|
||||
.frame(egui::Frame {
|
||||
..Default::default()
|
||||
})
|
||||
.show(ctx, |ui| {
|
||||
let is_mac_os = OperatingSystem::from_target_os() == OperatingSystem::Mac;
|
||||
if View::is_desktop() && !is_mac_os {
|
||||
self.desktop_window_ui(ui);
|
||||
} else {
|
||||
if is_mac_os {
|
||||
self.window_title_ui(ui);
|
||||
ui.add_space(-1.0);
|
||||
if View::is_desktop() {
|
||||
let is_fullscreen = ui.ctx().input(|i| {
|
||||
i.viewport().fullscreen.unwrap_or(false)
|
||||
});
|
||||
let os = egui::os::OperatingSystem::from_target_os();
|
||||
match os {
|
||||
egui::os::OperatingSystem::Mac => {
|
||||
self.window_title_ui(ui, is_fullscreen);
|
||||
ui.add_space(-1.0);
|
||||
Self::title_panel_bg(ui, true);
|
||||
self.content.ui(ui, &self.platform);
|
||||
}
|
||||
egui::os::OperatingSystem::Windows => {
|
||||
Self::title_panel_bg(ui, false);
|
||||
self.content.ui(ui, &self.platform);
|
||||
}
|
||||
_ => {
|
||||
self.custom_frame_ui(ui, is_fullscreen);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Self::title_panel_bg(ui, false);
|
||||
self.content.ui(ui, &self.platform);
|
||||
}
|
||||
});
|
||||
|
||||
// Check if desktop window was focused after requested attention.
|
||||
if self.platform.user_attention_required() &&
|
||||
ctx.input(|i| i.viewport().focused.unwrap_or(true)) {
|
||||
self.platform.clear_user_attention();
|
||||
}
|
||||
|
||||
// Show modal or keyboard window above opened Modal.
|
||||
if Modal::opened().is_some() {
|
||||
ctx.move_to_top(LayerId::new(Order::Middle, egui::Id::new(Modal::WINDOW_ID)));
|
||||
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 custom resizeable window content.
|
||||
fn desktop_window_ui(&mut self, ui: &mut egui::Ui) {
|
||||
let is_fullscreen = ui.ctx().input(|i| {
|
||||
i.viewport().fullscreen.unwrap_or(false)
|
||||
});
|
||||
|
||||
let title_stroke_rect = {
|
||||
let mut rect = ui.max_rect();
|
||||
/// Draw custom desktop window frame content.
|
||||
fn custom_frame_ui(&mut self, ui: &mut egui::Ui, is_fullscreen: bool) {
|
||||
let content_bg_rect = {
|
||||
let mut r = ui.max_rect();
|
||||
if !is_fullscreen {
|
||||
rect = rect.shrink(Content::WINDOW_FRAME_MARGIN);
|
||||
r = r.shrink(Content::WINDOW_FRAME_MARGIN);
|
||||
}
|
||||
rect.max.y = if !is_fullscreen {
|
||||
Content::WINDOW_FRAME_MARGIN
|
||||
} else {
|
||||
0.0
|
||||
} + Content::WINDOW_TITLE_HEIGHT + TitlePanel::DEFAULT_HEIGHT + 0.5;
|
||||
rect
|
||||
r.min.y += Content::WINDOW_TITLE_HEIGHT + TitlePanel::HEIGHT;
|
||||
r
|
||||
};
|
||||
let title_stroke = RectShape {
|
||||
rect: title_stroke_rect,
|
||||
rounding: Rounding {
|
||||
nw: 8.0,
|
||||
ne: 8.0,
|
||||
sw: 0.0,
|
||||
se: 0.0,
|
||||
},
|
||||
fill: Colors::yellow(),
|
||||
stroke: Stroke {
|
||||
width: 1.0,
|
||||
color: egui::Color32::from_gray(200)
|
||||
},
|
||||
blur_width: 0.0,
|
||||
fill_texture_id: Default::default(),
|
||||
uv: Rect::ZERO
|
||||
};
|
||||
// Draw title stroke.
|
||||
ui.painter().add(title_stroke);
|
||||
let content_bg = RectShape::new(content_bg_rect,
|
||||
CornerRadius::ZERO,
|
||||
Colors::fill_lite(),
|
||||
View::default_stroke(),
|
||||
StrokeKind::Outside);
|
||||
// Draw content background.
|
||||
ui.painter().add(content_bg);
|
||||
|
||||
let content_stroke_rect = {
|
||||
let mut rect = ui.max_rect();
|
||||
if !is_fullscreen {
|
||||
rect = rect.shrink(Content::WINDOW_FRAME_MARGIN);
|
||||
}
|
||||
let top = Content::WINDOW_TITLE_HEIGHT + TitlePanel::DEFAULT_HEIGHT + 0.5;
|
||||
rect.min += egui::vec2(0.0, top);
|
||||
rect
|
||||
};
|
||||
let content_stroke = RectShape {
|
||||
rect: content_stroke_rect,
|
||||
rounding: Rounding::ZERO,
|
||||
fill: Colors::fill(),
|
||||
stroke: Stroke {
|
||||
width: 1.0,
|
||||
color: Colors::stroke()
|
||||
},
|
||||
blur_width: 0.0,
|
||||
fill_texture_id: Default::default(),
|
||||
uv: Rect::ZERO
|
||||
};
|
||||
// Draw content stroke.
|
||||
ui.painter().add(content_stroke);
|
||||
|
||||
// Draw window content.
|
||||
let mut content_rect = ui.max_rect();
|
||||
if !is_fullscreen {
|
||||
content_rect = content_rect.shrink(Content::WINDOW_FRAME_MARGIN);
|
||||
}
|
||||
ui.allocate_ui_at_rect(content_rect, |ui| {
|
||||
self.window_title_ui(ui);
|
||||
self.window_content(ui);
|
||||
// Draw window content.
|
||||
ui.scope_builder(UiBuilder::new().max_rect(content_rect), |ui| {
|
||||
// Draw window title.
|
||||
self.window_title_ui(ui, is_fullscreen);
|
||||
ui.add_space(-1.0);
|
||||
|
||||
// Draw title panel background.
|
||||
Self::title_panel_bg(ui, true);
|
||||
|
||||
let content_rect = {
|
||||
let mut rect = ui.max_rect();
|
||||
rect.min.y += Content::WINDOW_TITLE_HEIGHT;
|
||||
rect
|
||||
};
|
||||
let mut content_ui = ui.new_child(UiBuilder::new()
|
||||
.max_rect(content_rect)
|
||||
.layout(*ui.layout()));
|
||||
// Draw main content.
|
||||
self.content.ui(&mut content_ui, &self.platform);
|
||||
});
|
||||
|
||||
// Setup resize areas.
|
||||
@@ -180,57 +214,53 @@ impl<Platform: PlatformCallbacks> App<Platform> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw window content for desktop.
|
||||
fn window_content(&mut self, ui: &mut egui::Ui) {
|
||||
let content_rect = {
|
||||
/// Draw title panel background.
|
||||
fn title_panel_bg(ui: &mut egui::Ui, window_title: bool) {
|
||||
let title_rect = {
|
||||
let mut rect = ui.max_rect();
|
||||
rect.min.y += Content::WINDOW_TITLE_HEIGHT;
|
||||
if window_title {
|
||||
rect.min.y += Content::WINDOW_TITLE_HEIGHT - 0.5;
|
||||
}
|
||||
rect.max.y = rect.min.y + View::get_top_inset() + TitlePanel::HEIGHT;
|
||||
rect
|
||||
};
|
||||
// Draw main content.
|
||||
let mut content_ui = ui.child_ui(content_rect, *ui.layout(), None);
|
||||
self.content.ui(&mut content_ui, &self.platform);
|
||||
let title_bg = RectShape::filled(title_rect, CornerRadius::ZERO, Colors::yellow());
|
||||
ui.painter().add(title_bg);
|
||||
}
|
||||
|
||||
/// Draw custom window title content.
|
||||
fn window_title_ui(&self, ui: &mut egui::Ui) {
|
||||
let content_rect = ui.max_rect();
|
||||
|
||||
fn window_title_ui(&self, ui: &mut egui::Ui, is_fullscreen: bool) {
|
||||
let title_rect = {
|
||||
let mut rect = content_rect;
|
||||
let mut rect = ui.max_rect();
|
||||
rect.max.y = rect.min.y + Content::WINDOW_TITLE_HEIGHT;
|
||||
rect
|
||||
};
|
||||
|
||||
let is_fullscreen = ui.ctx().input(|i| {
|
||||
i.viewport().fullscreen.unwrap_or(false)
|
||||
});
|
||||
|
||||
let window_title_bg = RectShape {
|
||||
rect: title_rect,
|
||||
rounding: if is_fullscreen {
|
||||
Rounding::ZERO
|
||||
} else {
|
||||
Rounding {
|
||||
nw: 8.0,
|
||||
ne: 8.0,
|
||||
sw: 0.0,
|
||||
se: 0.0,
|
||||
}
|
||||
},
|
||||
fill: Colors::yellow_dark(),
|
||||
stroke: Stroke::NONE,
|
||||
blur_width: 0.0,
|
||||
fill_texture_id: Default::default(),
|
||||
uv: Rect::ZERO
|
||||
let title_bg_rect = {
|
||||
let mut r = title_rect.clone();
|
||||
r.max.y += TitlePanel::HEIGHT - 1.0;
|
||||
r
|
||||
};
|
||||
let is_mac = egui::os::OperatingSystem::from_target_os() == egui::os::OperatingSystem::Mac;
|
||||
let window_title_bg = RectShape::new(title_bg_rect, if is_fullscreen || is_mac {
|
||||
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 painter = ui.painter();
|
||||
|
||||
let interact_rect = {
|
||||
let mut rect = title_rect;
|
||||
let mut rect = title_rect.clone();
|
||||
rect.max.x -= 128.0;
|
||||
rect.min.x += 85.0;
|
||||
if !is_fullscreen {
|
||||
rect.min.y += Content::WINDOW_FRAME_MARGIN;
|
||||
}
|
||||
@@ -239,26 +269,15 @@ impl<Platform: PlatformCallbacks> App<Platform> {
|
||||
let title_resp = ui.interact(
|
||||
interact_rect,
|
||||
egui::Id::new("window_title"),
|
||||
egui::Sense::click_and_drag(),
|
||||
egui::Sense::drag(),
|
||||
);
|
||||
// Interact with the window title (drag to move window):
|
||||
if !is_fullscreen && title_resp.drag_started_by(egui::PointerButton::Primary) {
|
||||
ui.ctx().send_viewport_cmd(ViewportCommand::StartDrag);
|
||||
}
|
||||
|
||||
// Paint the title.
|
||||
let dual_wallets_panel =
|
||||
ui.available_width() >= (Content::SIDE_PANEL_WIDTH * 3.0) + View::get_right_inset();
|
||||
let wallet_panel_opened = self.content.wallets.wallet_panel_opened();
|
||||
let hide_app_name = if dual_wallets_panel {
|
||||
!wallet_panel_opened || (AppConfig::show_wallets_at_dual_panel() &&
|
||||
self.content.wallets.showing_wallet() && !self.content.wallets.creating_wallet())
|
||||
} else if Content::is_dual_panel_mode(ui) {
|
||||
!wallet_panel_opened
|
||||
} else {
|
||||
!Content::is_network_panel_open() && !wallet_panel_opened
|
||||
};
|
||||
let title_text = if hide_app_name {
|
||||
"ツ".to_string()
|
||||
} else {
|
||||
format!("Grim {}", crate::VERSION)
|
||||
};
|
||||
let title_text = format!("Grim {} ツ", crate::VERSION);
|
||||
painter.text(
|
||||
title_rect.center(),
|
||||
egui::Align2::CENTER_CENTER,
|
||||
@@ -267,20 +286,13 @@ impl<Platform: PlatformCallbacks> App<Platform> {
|
||||
Colors::title(true),
|
||||
);
|
||||
|
||||
// Interact with the window title (drag to move window):
|
||||
if !is_fullscreen && title_resp.double_clicked() {
|
||||
ui.ctx().send_viewport_cmd(ViewportCommand::Fullscreen(!is_fullscreen));
|
||||
}
|
||||
|
||||
if !is_fullscreen && title_resp.drag_started_by(egui::PointerButton::Primary) {
|
||||
ui.ctx().send_viewport_cmd(ViewportCommand::StartDrag);
|
||||
}
|
||||
|
||||
ui.allocate_ui_at_rect(title_rect, |ui| {
|
||||
ui.scope_builder(UiBuilder::new().max_rect(title_rect), |ui| {
|
||||
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
|
||||
// Draw button to close window.
|
||||
View::title_button_small(ui, X, |_| {
|
||||
Content::show_exit_modal();
|
||||
if Modal::opened().is_none() || Modal::opened_closeable() {
|
||||
Content::show_exit_modal();
|
||||
}
|
||||
});
|
||||
|
||||
// Draw fullscreen button.
|
||||
@@ -388,20 +400,18 @@ impl<Platform: PlatformCallbacks> App<Platform> {
|
||||
/// To draw with egui`s eframe (for wgpu, glow backends and wasm target).
|
||||
impl<Platform: PlatformCallbacks> eframe::App for App<Platform> {
|
||||
fn update(&mut self, ctx: &Context, _: &mut eframe::Frame) {
|
||||
ctx.plugin_or_default::<egui_async::EguiAsyncPlugin>();
|
||||
self.ui(ctx);
|
||||
}
|
||||
|
||||
fn clear_color(&self, _visuals: &egui::Visuals) -> [f32; 4] {
|
||||
if View::is_desktop() {
|
||||
let is_mac_os = OperatingSystem::from_target_os() == OperatingSystem::Mac;
|
||||
if is_mac_os {
|
||||
Colors::fill().to_normalized_gamma_f32()
|
||||
} else {
|
||||
egui::Rgba::TRANSPARENT.to_array()
|
||||
}
|
||||
} else {
|
||||
Colors::fill().to_normalized_gamma_f32()
|
||||
let os = egui::os::OperatingSystem::from_target_os();
|
||||
let is_win = os == egui::os::OperatingSystem::Windows;
|
||||
let is_mac = os == egui::os::OperatingSystem::Mac;
|
||||
if !View::is_desktop() || is_win || is_mac {
|
||||
return Colors::fill_lite().to_normalized_gamma_f32();
|
||||
}
|
||||
Colors::TRANSPARENT.to_normalized_gamma_f32()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+32
-19
@@ -26,21 +26,29 @@ const SEMI_TRANSPARENT: Color32 = Color32::from_black_alpha(100);
|
||||
const DARK_SEMI_TRANSPARENT: Color32 = Color32::from_black_alpha(170);
|
||||
|
||||
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_DARK: Color32 = Color32::from_rgb(239, 229, 3);
|
||||
|
||||
const GREEN: Color32 = Color32::from_rgb(0, 0x64, 0);
|
||||
const GREEN_DARK: Color32 = Color32::from_rgb(0, (0x64 as f32 * 1.3 + 0.5) as u8, 0);
|
||||
|
||||
const RED: Color32 = Color32::from_rgb(0x8B, 0, 0);
|
||||
const RED_DARK: Color32 = Color32::from_rgb((0x8B as f32 * 1.3 + 0.5) as u8, 50, 30);
|
||||
|
||||
const BLUE: Color32 = Color32::from_rgb(0, 0x66, 0xE4);
|
||||
const BLUE_DARK: Color32 =
|
||||
Color32::from_rgb(0, (0x66 as f32 * 1.3 + 0.5) as u8, (0xE4 as f32 * 1.3 + 0.5) as u8);
|
||||
|
||||
const FILL: Color32 = Color32::from_gray(244);
|
||||
const FILL_DARK: Color32 = Color32::from_gray(24);
|
||||
const FILL_DARK: Color32 = Color32::from_gray(26);
|
||||
|
||||
const FILL_DEEP: Color32 = Color32::from_gray(238);
|
||||
const FILL_DEEP_DARK: Color32 = Color32::from_gray(18);
|
||||
const FILL_DEEP_DARK: Color32 = Color32::from_gray(32);
|
||||
|
||||
const FILL_LITE: Color32 = Color32::from_gray(249);
|
||||
const FILL_LITE_DARK: Color32 = Color32::from_gray(21);
|
||||
|
||||
const TEXT: Color32 = Color32::from_gray(80);
|
||||
const TEXT_DARK: Color32 = Color32::from_gray(185);
|
||||
@@ -54,13 +62,9 @@ const TEXT_BUTTON_DARK: Color32 = Color32::from_gray(195);
|
||||
const TITLE: Color32 = Color32::from_gray(60);
|
||||
const TITLE_DARK: Color32 = Color32::from_gray(205);
|
||||
|
||||
const BUTTON: Color32 = Color32::from_gray(249);
|
||||
const BUTTON_DARK: Color32 = Color32::from_gray(16);
|
||||
|
||||
const GRAY: Color32 = Color32::from_gray(120);
|
||||
const GRAY_DARK: Color32 = Color32::from_gray(145);
|
||||
|
||||
const STROKE: Color32 = Color32::from_gray(200);
|
||||
const STROKE_DARK: Color32 = Color32::from_gray(50);
|
||||
|
||||
const INACTIVE_TEXT: Color32 = Color32::from_gray(150);
|
||||
@@ -82,6 +86,7 @@ fn use_dark() -> bool {
|
||||
|
||||
impl Colors {
|
||||
pub const TRANSPARENT: Color32 = Color32::from_rgba_premultiplied(0, 0, 0, 0);
|
||||
pub const STROKE: Color32 = Color32::from_gray(200);
|
||||
|
||||
pub fn white_or_black(black_in_white: bool) -> Color32 {
|
||||
if use_dark() {
|
||||
@@ -115,6 +120,14 @@ impl Colors {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn gold_dark() -> Color32 {
|
||||
if use_dark() {
|
||||
GOLD_DARK.gamma_multiply(0.9)
|
||||
} else {
|
||||
GOLD_DARK
|
||||
}
|
||||
}
|
||||
|
||||
pub fn yellow() -> Color32 {
|
||||
YELLOW
|
||||
}
|
||||
@@ -125,7 +138,7 @@ impl Colors {
|
||||
|
||||
pub fn green() -> Color32 {
|
||||
if use_dark() {
|
||||
GREEN.gamma_multiply(1.3)
|
||||
GREEN_DARK
|
||||
} else {
|
||||
GREEN
|
||||
}
|
||||
@@ -133,7 +146,7 @@ impl Colors {
|
||||
|
||||
pub fn red() -> Color32 {
|
||||
if use_dark() {
|
||||
RED.gamma_multiply(1.3)
|
||||
RED_DARK
|
||||
} else {
|
||||
RED
|
||||
}
|
||||
@@ -141,7 +154,7 @@ impl Colors {
|
||||
|
||||
pub fn blue() -> Color32 {
|
||||
if use_dark() {
|
||||
BLUE.gamma_multiply(1.3)
|
||||
BLUE_DARK
|
||||
} else {
|
||||
BLUE
|
||||
}
|
||||
@@ -163,6 +176,14 @@ impl Colors {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fill_lite() -> Color32 {
|
||||
if use_dark() {
|
||||
FILL_LITE_DARK
|
||||
} else {
|
||||
FILL_LITE
|
||||
}
|
||||
}
|
||||
|
||||
pub fn checkbox() -> Color32 {
|
||||
if use_dark() {
|
||||
CHECKBOX_DARK
|
||||
@@ -195,14 +216,6 @@ impl Colors {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn button() -> Color32 {
|
||||
if use_dark() {
|
||||
BUTTON_DARK
|
||||
} else {
|
||||
BUTTON
|
||||
}
|
||||
}
|
||||
|
||||
pub fn gray() -> Color32 {
|
||||
if use_dark() {
|
||||
GRAY_DARK
|
||||
@@ -215,7 +228,7 @@ impl Colors {
|
||||
if use_dark() {
|
||||
STROKE_DARK
|
||||
} else {
|
||||
STROKE
|
||||
Self::STROKE
|
||||
}
|
||||
}
|
||||
|
||||
@@ -227,7 +240,7 @@ impl Colors {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn item_button() -> Color32 {
|
||||
pub fn item_button_text() -> Color32 {
|
||||
if use_dark() {
|
||||
ITEM_BUTTON_DARK
|
||||
} else {
|
||||
|
||||
@@ -30,7 +30,11 @@ use crate::gui::platform::PlatformCallbacks;
|
||||
/// Android platform implementation.
|
||||
#[derive(Clone)]
|
||||
pub struct Android {
|
||||
/// Android related state.
|
||||
android_app: AndroidApp,
|
||||
|
||||
/// Context to repaint content and handle viewport commands.
|
||||
ctx: Arc<RwLock<Option<egui::Context>>>,
|
||||
}
|
||||
|
||||
impl Android {
|
||||
@@ -38,6 +42,7 @@ impl Android {
|
||||
pub fn new(app: AndroidApp) -> Self {
|
||||
Self {
|
||||
android_app: app,
|
||||
ctx: Arc::new(RwLock::new(None)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,27 +61,22 @@ impl Android {
|
||||
}
|
||||
|
||||
impl PlatformCallbacks for Android {
|
||||
fn show_keyboard(&self) {
|
||||
// Disable NDK soft input show call before fix for egui.
|
||||
// self.android_app.show_soft_input(false);
|
||||
|
||||
self.call_java_method("showKeyboard", "()V", &[]).unwrap();
|
||||
fn set_context(&mut self, ctx: &egui::Context) {
|
||||
let mut w_ctx = self.ctx.write();
|
||||
*w_ctx = Some(ctx.clone());
|
||||
}
|
||||
|
||||
fn hide_keyboard(&self) {
|
||||
// Disable NDK soft input hide call before fix for egui.
|
||||
// self.android_app.hide_soft_input(false);
|
||||
|
||||
self.call_java_method("hideKeyboard", "()V", &[]).unwrap();
|
||||
fn exit(&self) {
|
||||
let _ = self.call_java_method("exit", "()V", &[]);
|
||||
}
|
||||
|
||||
fn copy_string_to_buffer(&self, data: String) {
|
||||
let vm = unsafe { jni::JavaVM::from_raw(self.android_app.vm_as_ptr() as _) }.unwrap();
|
||||
let env = vm.attach_current_thread().unwrap();
|
||||
let arg_value = env.new_string(data).unwrap();
|
||||
self.call_java_method("copyText",
|
||||
"(Ljava/lang/String;)V",
|
||||
&[JValue::Object(&JObject::from(arg_value))]).unwrap();
|
||||
let _ = self.call_java_method("copyText",
|
||||
"(Ljava/lang/String;)V",
|
||||
&[JValue::Object(&JObject::from(arg_value))]);
|
||||
}
|
||||
|
||||
fn get_string_from_buffer(&self) -> String {
|
||||
@@ -95,12 +95,12 @@ impl PlatformCallbacks for Android {
|
||||
let mut w_image = LAST_CAMERA_IMAGE.write();
|
||||
*w_image = None;
|
||||
// Start camera.
|
||||
self.call_java_method("startCamera", "()V", &[]).unwrap();
|
||||
let _ = self.call_java_method("startCamera", "()V", &[]);
|
||||
}
|
||||
|
||||
fn stop_camera(&self) {
|
||||
// Stop camera.
|
||||
self.call_java_method("stopCamera", "()V", &[]).unwrap();
|
||||
let _ = self.call_java_method("stopCamera", "()V", &[]);
|
||||
// Clear image.
|
||||
let mut w_image = LAST_CAMERA_IMAGE.write();
|
||||
*w_image = None;
|
||||
@@ -115,32 +115,39 @@ impl PlatformCallbacks for Android {
|
||||
}
|
||||
|
||||
fn can_switch_camera(&self) -> bool {
|
||||
let result = self.call_java_method("camerasAmount", "()I", &[]).unwrap();
|
||||
let amount = unsafe { result.i };
|
||||
amount > 1
|
||||
if let Some(res) = self.call_java_method("camerasAmount", "()I", &[]) {
|
||||
let amount = unsafe { res.i };
|
||||
return amount > 1;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn switch_camera(&self) {
|
||||
self.call_java_method("switchCamera", "()V", &[]).unwrap();
|
||||
let _ = self.call_java_method("switchCamera", "()V", &[]);
|
||||
}
|
||||
|
||||
fn share_data(&self, name: String, data: Vec<u8>) -> Result<(), std::io::Error> {
|
||||
// Create file at cache dir.
|
||||
let default_cache = OsString::from(dirs::cache_dir().unwrap());
|
||||
let mut cache = PathBuf::from(env::var_os("XDG_CACHE_HOME").unwrap_or(default_cache));
|
||||
cache.push("images");
|
||||
std::fs::create_dir_all(cache.to_str().unwrap())?;
|
||||
cache.push(name);
|
||||
let mut image = File::create_new(cache.clone()).unwrap();
|
||||
image.write_all(data.as_slice()).unwrap();
|
||||
image.sync_all().unwrap();
|
||||
let mut file = PathBuf::from(env::var_os("XDG_CACHE_HOME").unwrap_or(default_cache));
|
||||
// File path for Android provider.
|
||||
file.push("share");
|
||||
if !file.exists() {
|
||||
std::fs::create_dir(file.clone())?;
|
||||
}
|
||||
file.push(name);
|
||||
if file.exists() {
|
||||
std::fs::remove_file(file.clone())?;
|
||||
}
|
||||
let mut image = File::create_new(file.clone())?;
|
||||
image.write_all(data.as_slice())?;
|
||||
image.sync_all()?;
|
||||
// Call share modal at system.
|
||||
let vm = unsafe { jni::JavaVM::from_raw(self.android_app.vm_as_ptr() as _) }.unwrap();
|
||||
let env = vm.attach_current_thread().unwrap();
|
||||
let arg_value = env.new_string(cache.to_str().unwrap()).unwrap();
|
||||
self.call_java_method("shareImage",
|
||||
"(Ljava/lang/String;)V",
|
||||
&[JValue::Object(&JObject::from(arg_value))]).unwrap();
|
||||
let arg_value = env.new_string(file.to_str().unwrap()).unwrap();
|
||||
let _ = self.call_java_method("shareData",
|
||||
"(Ljava/lang/String;)V",
|
||||
&[JValue::Object(&JObject::from(arg_value))]);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -149,7 +156,17 @@ impl PlatformCallbacks for Android {
|
||||
let mut w_path = PICKED_FILE_PATH.write();
|
||||
*w_path = None;
|
||||
// Launch file picker.
|
||||
let _ = self.call_java_method("pickFile", "()V", &[]).unwrap();
|
||||
let _ = self.call_java_method("pickFile", "()V", &[]);
|
||||
// Return empty string to identify async pick.
|
||||
Some("".to_string())
|
||||
}
|
||||
|
||||
fn pick_folder(&self) -> Option<String> {
|
||||
// Clear previous result.
|
||||
let mut w_path = PICKED_FILE_PATH.write();
|
||||
*w_path = None;
|
||||
// Launch file picker.
|
||||
let _ = self.call_java_method("pickFolder", "()V", &[]);
|
||||
// Return empty string to identify async pick.
|
||||
Some("".to_string())
|
||||
}
|
||||
@@ -167,6 +184,14 @@ impl PlatformCallbacks for Android {
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn request_user_attention(&self) {}
|
||||
|
||||
fn user_attention_required(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn clear_user_attention(&self) {}
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
|
||||
+202
-90
@@ -13,12 +13,13 @@
|
||||
// limitations under the License.
|
||||
|
||||
use std::fs::File;
|
||||
use std::io:: Write;
|
||||
use lazy_static::lazy_static;
|
||||
use parking_lot::RwLock;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::io::Write;
|
||||
use std::thread;
|
||||
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
||||
use std::sync::Arc;
|
||||
use parking_lot::RwLock;
|
||||
use lazy_static::lazy_static;
|
||||
use egui::{UserAttentionType, ViewportCommand, WindowLevel};
|
||||
use rfd::FileDialog;
|
||||
|
||||
use crate::gui::platform::PlatformCallbacks;
|
||||
@@ -26,22 +27,158 @@ use crate::gui::platform::PlatformCallbacks;
|
||||
/// Desktop platform related actions.
|
||||
#[derive(Clone)]
|
||||
pub struct Desktop {
|
||||
/// Context to repaint content and handle viewport commands.
|
||||
ctx: Arc<RwLock<Option<egui::Context>>>,
|
||||
|
||||
/// Cameras amount.
|
||||
cameras_amount: Arc<AtomicUsize>,
|
||||
/// Camera index.
|
||||
camera_index: Arc<AtomicUsize>,
|
||||
/// Flag to check if camera stop is needed.
|
||||
stop_camera: Arc<AtomicBool>,
|
||||
|
||||
/// Flag to check if attention required after window focusing.
|
||||
attention_required: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl Default for Desktop {
|
||||
fn default() -> Self {
|
||||
impl Desktop {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
ctx: Arc::new(RwLock::new(None)),
|
||||
cameras_amount: Arc::new(AtomicUsize::new(0)),
|
||||
camera_index: Arc::new(AtomicUsize::new(0)),
|
||||
stop_camera: Arc::new(AtomicBool::new(false)),
|
||||
attention_required: Arc::new(AtomicBool::new(false)),
|
||||
}
|
||||
}
|
||||
|
||||
// #[allow(dead_code)]
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
fn start_camera_capture(cameras_amount: Arc<AtomicUsize>,
|
||||
camera_index: Arc<AtomicUsize>,
|
||||
stop_camera: Arc<AtomicBool>) {
|
||||
use nokhwa::Camera;
|
||||
use nokhwa::pixel_format::RgbFormat;
|
||||
use nokhwa::utils::{CameraIndex, RequestedFormat, RequestedFormatType};
|
||||
use nokhwa::utils::ApiBackend;
|
||||
|
||||
let devices = nokhwa::query(ApiBackend::Auto).unwrap();
|
||||
cameras_amount.store(devices.len(), Ordering::Relaxed);
|
||||
let index = camera_index.load(Ordering::Relaxed);
|
||||
if devices.is_empty() || index >= devices.len() {
|
||||
return;
|
||||
}
|
||||
|
||||
thread::spawn(move || {
|
||||
let index = CameraIndex::Index(camera_index.load(Ordering::Relaxed) as u32);
|
||||
let requested = RequestedFormat::new::<RgbFormat>(
|
||||
RequestedFormatType::AbsoluteHighestFrameRate
|
||||
);
|
||||
// Create and open camera.
|
||||
if let Ok(mut camera) = Camera::new(index, requested) {
|
||||
if let Ok(_) = camera.open_stream() {
|
||||
loop {
|
||||
// Stop if camera was stopped.
|
||||
if stop_camera.load(Ordering::Relaxed) {
|
||||
stop_camera.store(false, Ordering::Relaxed);
|
||||
// Clear image.
|
||||
let mut w_image = LAST_CAMERA_IMAGE.write();
|
||||
*w_image = None;
|
||||
break;
|
||||
}
|
||||
// Get a frame.
|
||||
if let Ok(frame) = camera.frame() {
|
||||
// Save image.
|
||||
let mut w_image = LAST_CAMERA_IMAGE.write();
|
||||
*w_image = Some((frame.buffer().to_vec(), 0));
|
||||
} else {
|
||||
// Clear image.
|
||||
let mut w_image = LAST_CAMERA_IMAGE.write();
|
||||
*w_image = None;
|
||||
break;
|
||||
}
|
||||
}
|
||||
camera.stop_stream().unwrap();
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[cfg(target_os = "macos")]
|
||||
fn start_camera_capture(cameras_amount: Arc<AtomicUsize>,
|
||||
camera_index: Arc<AtomicUsize>,
|
||||
stop_camera: Arc<AtomicBool>) {
|
||||
use nokhwa::nokhwa_initialize;
|
||||
use nokhwa::pixel_format::RgbFormat;
|
||||
use nokhwa::utils::{CameraIndex, RequestedFormat, RequestedFormatType};
|
||||
use nokhwa::utils::ApiBackend;
|
||||
use nokhwa::query;
|
||||
use nokhwa::CallbackCamera;
|
||||
|
||||
// Ask permission to open camera.
|
||||
nokhwa_initialize(|_| {});
|
||||
|
||||
thread::spawn(move || {
|
||||
let cameras = query(ApiBackend::Auto).unwrap();
|
||||
cameras_amount.store(cameras.len(), Ordering::Relaxed);
|
||||
let index = camera_index.load(Ordering::Relaxed);
|
||||
if cameras.is_empty() || index >= cameras.len() {
|
||||
return;
|
||||
}
|
||||
// Start camera.
|
||||
let camera_index = CameraIndex::Index(camera_index.load(Ordering::Relaxed) as u32);
|
||||
let camera_callback = CallbackCamera::new(
|
||||
camera_index,
|
||||
RequestedFormat::new::<RgbFormat>(RequestedFormatType::AbsoluteHighestFrameRate),
|
||||
|_| {}
|
||||
);
|
||||
if let Ok(mut cb) = camera_callback {
|
||||
if cb.open_stream().is_ok() {
|
||||
loop {
|
||||
// Stop if camera was stopped.
|
||||
if stop_camera.load(Ordering::Relaxed) {
|
||||
stop_camera.store(false, Ordering::Relaxed);
|
||||
// Clear image.
|
||||
let mut w_image = LAST_CAMERA_IMAGE.write();
|
||||
*w_image = None;
|
||||
break;
|
||||
}
|
||||
// Get image from camera.
|
||||
if let Ok(frame) = cb.poll_frame() {
|
||||
let image = frame.decode_image::<RgbFormat>().unwrap();
|
||||
let mut bytes: Vec<u8> = Vec::new();
|
||||
let format = image::ImageFormat::Jpeg;
|
||||
// Convert image to Jpeg format.
|
||||
image.write_to(&mut std::io::Cursor::new(&mut bytes), format).unwrap();
|
||||
let mut w_image = LAST_CAMERA_IMAGE.write();
|
||||
*w_image = Some((bytes, 0));
|
||||
} else {
|
||||
// Clear image.
|
||||
let mut w_image = LAST_CAMERA_IMAGE.write();
|
||||
*w_image = None;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl PlatformCallbacks for Desktop {
|
||||
fn show_keyboard(&self) {}
|
||||
fn set_context(&mut self, ctx: &egui::Context) {
|
||||
let mut w_ctx = self.ctx.write();
|
||||
*w_ctx = Some(ctx.clone());
|
||||
}
|
||||
|
||||
fn hide_keyboard(&self) {}
|
||||
fn exit(&self) {
|
||||
let r_ctx = self.ctx.read();
|
||||
if r_ctx.is_some() {
|
||||
let ctx = r_ctx.as_ref().unwrap();
|
||||
ctx.send_viewport_cmd(ViewportCommand::Close);
|
||||
}
|
||||
}
|
||||
|
||||
fn copy_string_to_buffer(&self, data: String) {
|
||||
let mut clipboard = arboard::Clipboard::new().unwrap();
|
||||
@@ -59,15 +196,13 @@ impl PlatformCallbacks for Desktop {
|
||||
let mut w_image = LAST_CAMERA_IMAGE.write();
|
||||
*w_image = None;
|
||||
}
|
||||
|
||||
// Setup stop camera flag.
|
||||
let stop_camera = self.stop_camera.clone();
|
||||
stop_camera.store(false, Ordering::Relaxed);
|
||||
|
||||
// Capture images at separate thread.
|
||||
thread::spawn(move || {
|
||||
Self::start_camera_capture(stop_camera);
|
||||
});
|
||||
Self::start_camera_capture(self.cameras_amount.clone(),
|
||||
self.camera_index.clone(),
|
||||
stop_camera);
|
||||
}
|
||||
|
||||
fn stop_camera(&self) {
|
||||
@@ -84,11 +219,20 @@ impl PlatformCallbacks for Desktop {
|
||||
}
|
||||
|
||||
fn can_switch_camera(&self) -> bool {
|
||||
false
|
||||
let amount = self.cameras_amount.load(Ordering::Relaxed);
|
||||
amount > 1
|
||||
}
|
||||
|
||||
fn switch_camera(&self) {
|
||||
return;
|
||||
self.stop_camera();
|
||||
let index = self.camera_index.load(Ordering::Relaxed);
|
||||
let amount = self.cameras_amount.load(Ordering::Relaxed);
|
||||
if index == amount - 1 {
|
||||
self.camera_index.store(0, Ordering::Relaxed);
|
||||
} else {
|
||||
self.camera_index.store(index + 1, Ordering::Relaxed);
|
||||
}
|
||||
self.start_camera();
|
||||
}
|
||||
|
||||
fn share_data(&self, name: String, data: Vec<u8>) -> Result<(), std::io::Error> {
|
||||
@@ -116,89 +260,57 @@ impl PlatformCallbacks for Desktop {
|
||||
None
|
||||
}
|
||||
|
||||
fn pick_folder(&self) -> Option<String> {
|
||||
let file = FileDialog::new()
|
||||
.set_title(t!("choose_folder"))
|
||||
.set_directory(dirs::home_dir().unwrap())
|
||||
.pick_folder();
|
||||
if let Some(file) = file {
|
||||
return Some(file.to_str().unwrap_or_default().to_string());
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn picked_file(&self) -> Option<String> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl Desktop {
|
||||
#[allow(dead_code)]
|
||||
#[cfg(target_os = "windows")]
|
||||
fn start_camera_capture(stop_camera: Arc<AtomicBool>) {
|
||||
use nokhwa::Camera;
|
||||
use nokhwa::pixel_format::RgbFormat;
|
||||
use nokhwa::utils::{CameraIndex, RequestedFormat, RequestedFormatType};
|
||||
let index = CameraIndex::Index(0);
|
||||
let requested = RequestedFormat::new::<RgbFormat>(
|
||||
RequestedFormatType::AbsoluteHighestFrameRate
|
||||
);
|
||||
// Create and open camera.
|
||||
let mut camera = Camera::new(index, requested).unwrap();
|
||||
if let Ok(_) = camera.open_stream() {
|
||||
loop {
|
||||
// Stop if camera was stopped.
|
||||
if stop_camera.load(Ordering::Relaxed) {
|
||||
stop_camera.store(false, Ordering::Relaxed);
|
||||
// Clear image.
|
||||
let mut w_image = LAST_CAMERA_IMAGE.write();
|
||||
*w_image = None;
|
||||
break;
|
||||
}
|
||||
// Get a frame.
|
||||
if let Ok(frame) = camera.frame() {
|
||||
// Save image.
|
||||
let mut w_image = LAST_CAMERA_IMAGE.write();
|
||||
*w_image = Some((frame.buffer().to_vec(), 0));
|
||||
} else {
|
||||
// Clear image.
|
||||
let mut w_image = LAST_CAMERA_IMAGE.write();
|
||||
*w_image = None;
|
||||
break;
|
||||
}
|
||||
fn request_user_attention(&self) {
|
||||
let r_ctx = self.ctx.read();
|
||||
if r_ctx.is_some() {
|
||||
let ctx = r_ctx.as_ref().unwrap();
|
||||
// Request attention on taskbar.
|
||||
ctx.send_viewport_cmd(
|
||||
ViewportCommand::RequestUserAttention(UserAttentionType::Informational)
|
||||
);
|
||||
// Un-minimize window.
|
||||
if ctx.input(|i| i.viewport().minimized.unwrap_or(false)) {
|
||||
ctx.send_viewport_cmd(ViewportCommand::Minimized(false));
|
||||
}
|
||||
camera.stop_stream().unwrap();
|
||||
};
|
||||
// Focus to window.
|
||||
if !ctx.input(|i| i.viewport().focused.unwrap_or(false)) {
|
||||
ctx.send_viewport_cmd(ViewportCommand::WindowLevel(WindowLevel::AlwaysOnTop));
|
||||
ctx.send_viewport_cmd(ViewportCommand::Focus);
|
||||
}
|
||||
ctx.request_repaint();
|
||||
}
|
||||
self.attention_required.store(true, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
fn start_camera_capture(stop_camera: Arc<AtomicBool>) {
|
||||
use eye::hal::{traits::{Context, Device, Stream}, PlatformContext};
|
||||
use image::ImageEncoder;
|
||||
fn user_attention_required(&self) -> bool {
|
||||
self.attention_required.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
let ctx = PlatformContext::default();
|
||||
let devices = ctx.devices().unwrap();
|
||||
let dev = ctx.open_device(&devices[0].uri).unwrap();
|
||||
|
||||
let streams = dev.streams().unwrap();
|
||||
let stream_desc = streams[0].clone();
|
||||
let w = stream_desc.width;
|
||||
let h = stream_desc.height;
|
||||
|
||||
let mut stream = dev.start_stream(&stream_desc).unwrap();
|
||||
|
||||
loop {
|
||||
// Stop if camera was stopped.
|
||||
if stop_camera.load(Ordering::Relaxed) {
|
||||
stop_camera.store(false, Ordering::Relaxed);
|
||||
// Clear image.
|
||||
let mut w_image = LAST_CAMERA_IMAGE.write();
|
||||
*w_image = None;
|
||||
break;
|
||||
}
|
||||
// Get a frame.
|
||||
let frame = stream.next().expect("Stream is dead").expect("Failed to capture a frame");
|
||||
let mut out = vec![];
|
||||
if let Some(buf) = image::ImageBuffer::<image::Rgb<u8>, &[u8]>::from_raw(w, h, &frame) {
|
||||
image::codecs::jpeg::JpegEncoder::new(&mut out)
|
||||
.write_image(buf.as_raw(), w, h, image::ExtendedColorType::Rgb8).unwrap();
|
||||
} else {
|
||||
out = frame.to_vec();
|
||||
}
|
||||
// Save image.
|
||||
let mut w_image = LAST_CAMERA_IMAGE.write();
|
||||
*w_image = Some((out, 0));
|
||||
fn clear_user_attention(&self) {
|
||||
let r_ctx = self.ctx.read();
|
||||
if r_ctx.is_some() {
|
||||
let ctx = r_ctx.as_ref().unwrap();
|
||||
ctx.send_viewport_cmd(
|
||||
ViewportCommand::RequestUserAttention(UserAttentionType::Reset)
|
||||
);
|
||||
ctx.send_viewport_cmd(ViewportCommand::WindowLevel(WindowLevel::Normal));
|
||||
}
|
||||
self.attention_required.store(false, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,8 +22,8 @@ pub mod platform;
|
||||
pub mod platform;
|
||||
|
||||
pub trait PlatformCallbacks {
|
||||
fn show_keyboard(&self);
|
||||
fn hide_keyboard(&self);
|
||||
fn set_context(&mut self, ctx: &egui::Context);
|
||||
fn exit(&self);
|
||||
fn copy_string_to_buffer(&self, data: String);
|
||||
fn get_string_from_buffer(&self) -> String;
|
||||
fn start_camera(&self);
|
||||
@@ -33,5 +33,9 @@ pub trait PlatformCallbacks {
|
||||
fn switch_camera(&self);
|
||||
fn share_data(&self, name: String, data: Vec<u8>) -> Result<(), std::io::Error>;
|
||||
fn pick_file(&self) -> Option<String>;
|
||||
fn pick_folder(&self) -> Option<String>;
|
||||
fn picked_file(&self) -> Option<String>;
|
||||
fn request_user_attention(&self);
|
||||
fn user_attention_required(&self) -> bool;
|
||||
fn clear_user_attention(&self);
|
||||
}
|
||||
+95
-98
@@ -15,11 +15,9 @@
|
||||
use std::sync::Arc;
|
||||
use parking_lot::RwLock;
|
||||
use std::thread;
|
||||
use eframe::emath::Align;
|
||||
use egui::load::SizedTexture;
|
||||
use egui::{Layout, Pos2, Rect, RichText, TextureOptions, Widget};
|
||||
use image::{DynamicImage, EncodableLayout, ImageFormat};
|
||||
|
||||
use egui::{Pos2, Rect, RichText, TextureOptions, UiBuilder, Widget};
|
||||
use image::{DynamicImage, EncodableLayout};
|
||||
use grin_util::ZeroingString;
|
||||
use grin_wallet_libwallet::SlatepackAddress;
|
||||
use grin_keychain::mnemonic::WORDS;
|
||||
@@ -36,16 +34,15 @@ use crate::wallet::WalletUtils;
|
||||
pub struct CameraContent {
|
||||
/// QR code scanning progress and result.
|
||||
qr_scan_state: Arc<RwLock<QrScanState>>,
|
||||
|
||||
/// Uniform Resources URIs collected from QR code scanning.
|
||||
ur_data: Arc<RwLock<Option<(Vec<String>, usize)>>>,
|
||||
ur_data: Arc<RwLock<Option<(Vec<String>, usize)>>>
|
||||
}
|
||||
|
||||
impl Default for CameraContent {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
qr_scan_state: Arc::new(RwLock::new(QrScanState::default())),
|
||||
ur_data: Arc::new(RwLock::new(None)),
|
||||
ur_data: Arc::new(RwLock::new(None))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -53,102 +50,112 @@ impl Default for CameraContent {
|
||||
impl CameraContent {
|
||||
/// Draw camera content.
|
||||
pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||
// Draw last image from camera or loader.
|
||||
if let Some(img_data) = cb.camera_image() {
|
||||
// Load image to draw.
|
||||
if let Ok(mut img) =
|
||||
image::load_from_memory_with_format(&*img_data.0, ImageFormat::Jpeg) {
|
||||
let rect = if let Some(img_data) = cb.camera_image() {
|
||||
if let Ok(img) =
|
||||
image::load_from_memory(&*img_data.0) {
|
||||
// Process image to find QR code.
|
||||
self.scan_qr(&img);
|
||||
// Setup image rotation.
|
||||
img = match img_data.1 {
|
||||
90 => img.rotate90(),
|
||||
180 => img.rotate180(),
|
||||
270 => img.rotate270(),
|
||||
_ => img
|
||||
};
|
||||
// Convert to ColorImage to add at content.
|
||||
let color_img = match &img {
|
||||
DynamicImage::ImageRgb8(image) => {
|
||||
egui::ColorImage::from_rgb(
|
||||
[image.width() as usize, image.height() as usize],
|
||||
image.as_bytes(),
|
||||
)
|
||||
},
|
||||
other => {
|
||||
let image = other.to_rgba8();
|
||||
egui::ColorImage::from_rgba_unmultiplied(
|
||||
[image.width() as usize, image.height() as usize],
|
||||
image.as_bytes(),
|
||||
)
|
||||
},
|
||||
};
|
||||
// Create image texture.
|
||||
let texture = ui.ctx().load_texture("camera_image",
|
||||
color_img.clone(),
|
||||
TextureOptions::default());
|
||||
let img_size = egui::emath::vec2(color_img.width() as f32,
|
||||
color_img.height() as f32);
|
||||
let sized_img = SizedTexture::new(texture.id(), img_size);
|
||||
// Add image to content.
|
||||
ui.vertical_centered(|ui| {
|
||||
egui::Image::from_texture(sized_img)
|
||||
// Setup to crop image at square.
|
||||
.uv(Rect::from([
|
||||
Pos2::new(1.0 - (img_size.y / img_size.x), 0.0),
|
||||
Pos2::new(1.0, 1.0)
|
||||
]))
|
||||
.max_height(ui.available_width())
|
||||
.maintain_aspect_ratio(false)
|
||||
.shrink_to_fit()
|
||||
.ui(ui);
|
||||
});
|
||||
|
||||
// Draw image.
|
||||
let img_rect = self.image_ui(ui, img, img_data.1);
|
||||
|
||||
// Show UR scan progress.
|
||||
let show_ur_progress = {
|
||||
self.ur_data.clone().read().is_some()
|
||||
};
|
||||
let ur_progress = self.ur_progress();
|
||||
if show_ur_progress && ur_progress != 0 {
|
||||
ui.add_space(-52.0);
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.label(RichText::new(format!("{}%", ur_progress))
|
||||
.size(16.0)
|
||||
.color(Colors::yellow()));
|
||||
});
|
||||
}
|
||||
|
||||
// Show button to switch cameras.
|
||||
if cb.can_switch_camera() {
|
||||
ui.add_space(-52.0);
|
||||
let mut size = ui.available_size();
|
||||
size.y = 48.0;
|
||||
ui.allocate_ui_with_layout(size, Layout::right_to_left(Align::Max), |ui| {
|
||||
ui.add_space(4.0);
|
||||
View::button(ui, CAMERA_ROTATE.to_string(), Colors::white_or_black(false), || {
|
||||
cb.switch_camera();
|
||||
});
|
||||
});
|
||||
}
|
||||
self.ur_progress_ui(ui);
|
||||
img_rect
|
||||
} else {
|
||||
self.loading_content_ui(ui);
|
||||
self.loading_ui(ui)
|
||||
}
|
||||
} else {
|
||||
self.loading_content_ui(ui);
|
||||
}
|
||||
self.loading_ui(ui)
|
||||
};
|
||||
|
||||
// Request redraw.
|
||||
// Show button to switch cameras.
|
||||
if cb.can_switch_camera() {
|
||||
let r = {
|
||||
let mut r = rect.clone();
|
||||
r.min.y = r.max.y - 52.0;
|
||||
r.min.x = r.max.x - 52.0;
|
||||
r
|
||||
};
|
||||
ui.scope_builder(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();
|
||||
});
|
||||
});
|
||||
}
|
||||
ui.ctx().request_repaint();
|
||||
}
|
||||
|
||||
/// Draw camera image.
|
||||
fn image_ui(&mut self, ui: &mut egui::Ui, mut img: DynamicImage, rotation: u32) -> Rect {
|
||||
// Setup image rotation.
|
||||
img = match rotation {
|
||||
90 => img.rotate90(),
|
||||
180 => img.rotate180(),
|
||||
270 => img.rotate270(),
|
||||
_ => img
|
||||
};
|
||||
if View::is_desktop() {
|
||||
img = img.fliph();
|
||||
}
|
||||
// Convert to ColorImage.
|
||||
let color_img = match &img {
|
||||
DynamicImage::ImageRgb8(image) => {
|
||||
egui::ColorImage::from_rgb(
|
||||
[image.width() as usize, image.height() as usize],
|
||||
image.as_bytes(),
|
||||
)
|
||||
},
|
||||
other => {
|
||||
let image = other.to_rgba8();
|
||||
egui::ColorImage::from_rgba_unmultiplied(
|
||||
[image.width() as usize, image.height() as usize],
|
||||
image.as_bytes(),
|
||||
)
|
||||
},
|
||||
};
|
||||
// Create image texture.
|
||||
let texture = ui.ctx().load_texture("camera_image",
|
||||
color_img.clone(),
|
||||
TextureOptions::default());
|
||||
let img_size = egui::emath::vec2(color_img.width() as f32,
|
||||
color_img.height() as f32);
|
||||
let sized_img = SizedTexture::new(texture.id(), img_size);
|
||||
egui::Image::from_texture(sized_img)
|
||||
// Setup to crop image at square.
|
||||
.uv(Rect::from([
|
||||
Pos2::new(1.0 - (img_size.y / img_size.x), 0.0),
|
||||
Pos2::new(1.0, 1.0)
|
||||
]))
|
||||
.max_height(ui.available_width())
|
||||
.maintain_aspect_ratio(false)
|
||||
.shrink_to_fit()
|
||||
.ui(ui).rect
|
||||
}
|
||||
|
||||
/// Draw animated QR code scanning progress.
|
||||
fn ur_progress_ui(&self, ui: &mut egui::Ui) {
|
||||
let show_ur_progress = {
|
||||
self.ur_data.as_ref().read().is_some()
|
||||
};
|
||||
if show_ur_progress {
|
||||
ui.centered_and_justified(|ui| {
|
||||
ui.label(RichText::new(format!("{}%", self.ur_progress()))
|
||||
.size(17.0)
|
||||
.color(Colors::green()));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw camera loading progress content.
|
||||
fn loading_content_ui(&self, ui: &mut egui::Ui) {
|
||||
fn loading_ui(&self, ui: &mut egui::Ui) -> Rect {
|
||||
let space = (ui.available_width() - View::BIG_SPINNER_SIZE) / 2.0;
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.add_space(space);
|
||||
View::big_loading_spinner(ui);
|
||||
ui.add_space(space);
|
||||
});
|
||||
}).response.rect
|
||||
}
|
||||
|
||||
/// Check if image is processing to find QR code.
|
||||
@@ -283,7 +290,7 @@ impl CameraContent {
|
||||
|
||||
// Launch scanner at separate thread.
|
||||
thread::spawn(move || {
|
||||
tokio::runtime::Builder::new_multi_thread()
|
||||
tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap()
|
||||
@@ -304,7 +311,7 @@ impl CameraContent {
|
||||
|
||||
// Check if string contains Slatepack message prefix and postfix.
|
||||
if text.starts_with("BEGINSLATEPACK.") && text.ends_with("ENDSLATEPACK.") {
|
||||
return QrScanResult::Slatepack(ZeroingString::from(text));
|
||||
return QrScanResult::Slatepack(text.to_string());
|
||||
}
|
||||
|
||||
// Check Uniform Resource data.
|
||||
@@ -430,14 +437,4 @@ impl CameraContent {
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Reset camera content state to default.
|
||||
pub fn clear_state(&mut self) {
|
||||
// Clear QR code scanning state.
|
||||
let mut w_scan = self.qr_scan_state.write();
|
||||
*w_scan = QrScanState::default();
|
||||
// Clear UR data.
|
||||
let mut w_data = self.ur_data.write();
|
||||
*w_data = None;
|
||||
}
|
||||
}
|
||||
+149
-227
@@ -12,20 +12,21 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use egui::os::OperatingSystem;
|
||||
use egui::{Align, Layout, RichText};
|
||||
use egui::RichText;
|
||||
use lazy_static::lazy_static;
|
||||
use std::fs;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
use crate::gui::Colors;
|
||||
use crate::gui::icons::FILE_X;
|
||||
use crate::gui::platform::PlatformCallbacks;
|
||||
use crate::gui::views::{Modal, View};
|
||||
use crate::gui::views::types::ModalContainer;
|
||||
use crate::node::Node;
|
||||
use crate::AppConfig;
|
||||
use crate::gui::icons::{CHECK, CHECK_FAT};
|
||||
use crate::gui::views::network::{NetworkContent, NodeSetup};
|
||||
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::Colors;
|
||||
use crate::node::Node;
|
||||
use crate::{AppConfig, Settings};
|
||||
|
||||
lazy_static! {
|
||||
/// Global state to check if [`NetworkContent`] panel is open.
|
||||
@@ -36,19 +37,17 @@ lazy_static! {
|
||||
pub struct Content {
|
||||
/// Side panel [`NetworkContent`] content.
|
||||
network: NetworkContent,
|
||||
/// Central panel [`WalletsContent`] content.
|
||||
pub wallets: WalletsContent,
|
||||
|
||||
/// Check if app exit is allowed on close event of [`eframe::App`] implementation.
|
||||
pub(crate) exit_allowed: bool,
|
||||
/// Central panel [`WalletsContent`] content.
|
||||
wallets: WalletsContent,
|
||||
|
||||
/// Check if app exit is allowed on Desktop close event.
|
||||
pub exit_allowed: bool,
|
||||
/// Flag to show exit progress at [`Modal`].
|
||||
show_exit_progress: bool,
|
||||
|
||||
/// Flag to check it's first draw of content.
|
||||
first_draw: bool,
|
||||
|
||||
/// List of allowed [`Modal`] ids for this [`ModalContainer`].
|
||||
allowed_modal_ids: Vec<&'static str>
|
||||
}
|
||||
|
||||
impl Default for Content {
|
||||
@@ -62,55 +61,39 @@ impl Default for Content {
|
||||
exit_allowed,
|
||||
show_exit_progress: false,
|
||||
first_draw: true,
|
||||
allowed_modal_ids: vec![
|
||||
Self::EXIT_MODAL_ID,
|
||||
Self::SETTINGS_MODAL,
|
||||
Self::ANDROID_INTEGRATED_NODE_WARNING_MODAL,
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ModalContainer for Content {
|
||||
fn modal_ids(&self) -> &Vec<&'static str> {
|
||||
&self.allowed_modal_ids
|
||||
/// Identifier for integrated node warning [`Modal`] on Android.
|
||||
const ANDROID_INTEGRATED_NODE_WARNING_MODAL: &'static str = "android_node_warning_modal";
|
||||
/// Identifier for crash report [`Modal`].
|
||||
const CRASH_REPORT_MODAL: &'static str = "crash_report_modal";
|
||||
|
||||
impl ContentContainer for Content {
|
||||
fn modal_ids(&self) -> Vec<&'static str> {
|
||||
vec![
|
||||
Self::EXIT_CONFIRMATION_MODAL,
|
||||
ANDROID_INTEGRATED_NODE_WARNING_MODAL,
|
||||
CRASH_REPORT_MODAL
|
||||
]
|
||||
}
|
||||
|
||||
fn modal_ui(&mut self,
|
||||
ui: &mut egui::Ui,
|
||||
modal: &Modal,
|
||||
_: &dyn PlatformCallbacks) {
|
||||
fn modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
|
||||
match modal.id {
|
||||
Self::EXIT_MODAL_ID => self.exit_modal_content(ui, modal),
|
||||
Self::SETTINGS_MODAL => self.settings_modal_ui(ui, modal),
|
||||
Self::ANDROID_INTEGRATED_NODE_WARNING_MODAL => self.android_warning_modal_ui(ui, modal),
|
||||
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),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Content {
|
||||
/// Identifier for exit confirmation [`Modal`].
|
||||
pub const EXIT_MODAL_ID: &'static str = "exit_confirmation_modal";
|
||||
/// Identifier for wallet opening [`Modal`].
|
||||
pub const SETTINGS_MODAL: &'static str = "settings_modal";
|
||||
|
||||
/// Identifier for integrated node warning [`Modal`] on Android.
|
||||
const ANDROID_INTEGRATED_NODE_WARNING_MODAL: &'static str = "android_node_warning_modal";
|
||||
|
||||
/// Default width of side panel at application UI.
|
||||
pub const SIDE_PANEL_WIDTH: f32 = 400.0;
|
||||
/// 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) {
|
||||
// Draw modal content for current ui container.
|
||||
self.current_modal_ui(ui, cb);
|
||||
|
||||
let dual_panel = Self::is_dual_panel_mode(ui);
|
||||
let (is_panel_open, panel_width) = Self::network_panel_state_width(ui, dual_panel);
|
||||
fn container_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||
let dual_panel = Self::is_dual_panel_mode(ui.ctx());
|
||||
let (is_panel_open, mut panel_width) = network_panel_state_width(ui.ctx(), dual_panel);
|
||||
if self.network.showing_settings() {
|
||||
panel_width = ui.available_width();
|
||||
}
|
||||
|
||||
// Show network content.
|
||||
egui::SidePanel::left("network_panel")
|
||||
@@ -132,42 +115,56 @@ impl Content {
|
||||
self.wallets.ui(ui, cb);
|
||||
});
|
||||
|
||||
// Show integrated node warning on Android if needed.
|
||||
if self.first_draw && OperatingSystem::from_target_os() == OperatingSystem::Android &&
|
||||
AppConfig::android_integrated_node_warning_needed() {
|
||||
Modal::new(Self::ANDROID_INTEGRATED_NODE_WARNING_MODAL)
|
||||
.title(t!("network.node"))
|
||||
.show();
|
||||
}
|
||||
|
||||
// Setup first draw flag.
|
||||
if self.first_draw {
|
||||
// Show crash report or integrated node Android warning.
|
||||
if Settings::crash_report_path().exists() {
|
||||
Modal::new(CRASH_REPORT_MODAL)
|
||||
.closeable(false)
|
||||
.position(ModalPosition::Center)
|
||||
.title(t!("crash_report"))
|
||||
.show();
|
||||
} else 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get [`NetworkContent`] panel state and width.
|
||||
fn network_panel_state_width(ui: &mut egui::Ui, dual_panel: bool) -> (bool, f32) {
|
||||
let is_panel_open = dual_panel || Self::is_network_panel_open();
|
||||
let panel_width = if dual_panel {
|
||||
Self::SIDE_PANEL_WIDTH + View::get_left_inset()
|
||||
} else {
|
||||
let is_fullscreen = ui.ctx().input(|i| {
|
||||
i.viewport().fullscreen.unwrap_or(false)
|
||||
});
|
||||
View::window_size(ui).0 - if View::is_desktop() && !is_fullscreen &&
|
||||
OperatingSystem::from_target_os() != OperatingSystem::Mac {
|
||||
Self::WINDOW_FRAME_MARGIN * 2.0
|
||||
} else {
|
||||
0.0
|
||||
impl Content {
|
||||
/// Default width of side panel at application UI.
|
||||
pub const SIDE_PANEL_WIDTH: f32 = 400.0;
|
||||
/// 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;
|
||||
|
||||
/// Identifier for exit confirmation [`Modal`].
|
||||
pub const EXIT_CONFIRMATION_MODAL: &'static str = "exit_confirmation_modal";
|
||||
|
||||
/// Called to navigate back, return `true` if action was not consumed.
|
||||
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;
|
||||
}
|
||||
};
|
||||
(is_panel_open, panel_width)
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// Check if ui can show [`NetworkContent`] and [`WalletsContent`] at same time.
|
||||
pub fn is_dual_panel_mode(ui: &egui::Ui) -> bool {
|
||||
let (w, h) = View::window_size(ui);
|
||||
pub fn is_dual_panel_mode(ctx: &egui::Context) -> bool {
|
||||
let (w, h) = View::window_size(ctx);
|
||||
// Screen is wide if width is greater than height or just 20% smaller.
|
||||
let is_wide_screen = w > h || w + (w * 0.2) >= h;
|
||||
// Dual panel mode is available when window is wide and its width is at least 2 times
|
||||
@@ -187,26 +184,31 @@ impl Content {
|
||||
NETWORK_PANEL_OPEN.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// Show exit confirmation modal.
|
||||
/// Show exit confirmation [`Modal`].
|
||||
pub fn show_exit_modal() {
|
||||
Modal::new(Self::EXIT_MODAL_ID)
|
||||
.title(t!("modal.confirmation"))
|
||||
Modal::new(Self::EXIT_CONFIRMATION_MODAL)
|
||||
.title(t!("confirmation"))
|
||||
.show();
|
||||
}
|
||||
|
||||
/// Draw exit confirmation modal content.
|
||||
fn exit_modal_content(&mut self, ui: &mut egui::Ui, modal: &Modal) {
|
||||
fn exit_modal_content(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
|
||||
if self.show_exit_progress {
|
||||
if !Node::is_running() {
|
||||
if !Node::is_running() && !Node::data_dir_changing() {
|
||||
self.exit_allowed = true;
|
||||
ui.ctx().send_viewport_cmd(egui::ViewportCommand::Close);
|
||||
modal.close();
|
||||
cb.exit();
|
||||
Modal::close();
|
||||
}
|
||||
ui.add_space(16.0);
|
||||
ui.vertical_centered(|ui| {
|
||||
View::small_loading_spinner(ui);
|
||||
ui.add_space(12.0);
|
||||
ui.label(RichText::new(t!("sync_status.shutdown"))
|
||||
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)));
|
||||
});
|
||||
@@ -226,15 +228,15 @@ impl Content {
|
||||
ui.columns(2, |columns| {
|
||||
columns[0].vertical_centered_justified(|ui| {
|
||||
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
|
||||
modal.close();
|
||||
Modal::close();
|
||||
});
|
||||
});
|
||||
columns[1].vertical_centered_justified(|ui| {
|
||||
View::button_ui(ui, t!("modal_exit.exit"), Colors::white_or_black(false), |ui| {
|
||||
if !Node::is_running() {
|
||||
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;
|
||||
ui.ctx().send_viewport_cmd(egui::ViewportCommand::Close);
|
||||
modal.close();
|
||||
cb.exit();
|
||||
Modal::close();
|
||||
} else {
|
||||
Node::stop(true);
|
||||
modal.disable_closing();
|
||||
@@ -248,151 +250,71 @@ impl Content {
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle Back key event.
|
||||
pub fn on_back(&mut self) {
|
||||
if Modal::on_back() {
|
||||
if self.wallets.on_back() {
|
||||
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);
|
||||
|
||||
// Draw chain type selection.
|
||||
NodeSetup::chain_type_ui(ui);
|
||||
|
||||
ui.add_space(8.0);
|
||||
View::horizontal_line(ui, Colors::item_stroke());
|
||||
ui.add_space(8.0);
|
||||
|
||||
// Draw theme selection.
|
||||
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.vertical(|ui| {
|
||||
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
|
||||
// Draw button to select language.
|
||||
let is_current = if let Some(lang) = AppConfig::locale() {
|
||||
lang == locale
|
||||
} else {
|
||||
rust_i18n::locale() == locale
|
||||
};
|
||||
if !is_current {
|
||||
View::item_button(ui, View::item_rounding(index, len, true), CHECK, None, || {
|
||||
rust_i18n::set_locale(locale);
|
||||
AppConfig::save_locale(locale);
|
||||
modal.close();
|
||||
});
|
||||
} else {
|
||||
ui.add_space(14.0);
|
||||
ui.label(RichText::new(CHECK_FAT).size(20.0).color(Colors::green()));
|
||||
ui.add_space(14.0);
|
||||
}
|
||||
|
||||
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) {
|
||||
fn android_warning_modal_ui(&mut self, ui: &mut egui::Ui) {
|
||||
ui.add_space(6.0);
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.label(RichText::new(t!("network.android_warning"))
|
||||
.size(15.0)
|
||||
.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();
|
||||
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, 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.
|
||||
fn network_panel_state_width(ctx: &egui::Context, dual_panel: bool) -> (bool, f32) {
|
||||
let is_panel_open = dual_panel || Content::is_network_panel_open();
|
||||
let panel_width = if dual_panel {
|
||||
Content::SIDE_PANEL_WIDTH + View::get_left_inset()
|
||||
} else {
|
||||
let is_fullscreen = ctx.input(|i| {
|
||||
i.viewport().fullscreen.unwrap_or(false)
|
||||
});
|
||||
View::window_size(ctx).0 - if View::is_desktop() && !is_fullscreen &&
|
||||
OperatingSystem::from_target_os() != OperatingSystem::Mac {
|
||||
Content::WINDOW_FRAME_MARGIN * 2.0
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
};
|
||||
(is_panel_open, panel_width)
|
||||
}
|
||||
@@ -1,116 +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 std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::{fs, thread};
|
||||
use parking_lot::RwLock;
|
||||
|
||||
use crate::gui::Colors;
|
||||
use crate::gui::icons::ARCHIVE_BOX;
|
||||
use crate::gui::platform::PlatformCallbacks;
|
||||
use crate::gui::views::View;
|
||||
|
||||
/// Button to pick file and parse its data into text.
|
||||
pub struct FilePickButton {
|
||||
/// Flag to check if file is picking.
|
||||
pub file_picking: Arc<AtomicBool>,
|
||||
/// Flag to check if file is parsing.
|
||||
pub file_parsing: Arc<AtomicBool>,
|
||||
/// File parsing result.
|
||||
pub file_parsing_result: Arc<RwLock<Option<String>>>
|
||||
}
|
||||
|
||||
impl Default for FilePickButton {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
file_picking: Arc::new(AtomicBool::new(false)),
|
||||
file_parsing: Arc::new(AtomicBool::new(false)),
|
||||
file_parsing_result: Arc::new(RwLock::new(None))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 file_text = format!("{} {}", ARCHIVE_BOX, t!("choose_file"));
|
||||
View::colored_text_button(ui, file_text, Colors::blue(), Colors::button(), || {
|
||||
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,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::CornerRadius;
|
||||
use parking_lot::RwLock;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::{fs, thread};
|
||||
|
||||
use crate::gui::icons::ARCHIVE_BOX;
|
||||
use crate::gui::platform::PlatformCallbacks;
|
||||
use crate::gui::views::View;
|
||||
use crate::gui::Colors;
|
||||
|
||||
/// Type of button.
|
||||
pub enum FilePickContentType {
|
||||
Button(String), ItemButton(CornerRadius), Tab
|
||||
}
|
||||
|
||||
/// Button to pick file and parse its data into text.
|
||||
pub struct FilePickContent {
|
||||
/// Content type.
|
||||
content_type: FilePickContentType,
|
||||
|
||||
/// Flag to check if button is active.
|
||||
active: bool,
|
||||
|
||||
/// 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 FilePickContent {
|
||||
/// Create new content from provided type.
|
||||
pub fn new(content_type: FilePickContentType) -> Self {
|
||||
Self {
|
||||
content_type,
|
||||
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());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,453 @@
|
||||
// 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::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};
|
||||
use crate::gui::Colors;
|
||||
|
||||
/// 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)]
|
||||
#[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)]
|
||||
#[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)]
|
||||
#[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,509 @@
|
||||
// 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::string::ToString;
|
||||
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::sync::atomic::Ordering;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::gui::icons::{ARROW_FAT_UP, BACKSPACE, GLOBE_SIMPLE, KEY_RETURN};
|
||||
use crate::gui::views::{KeyboardEvent, KeyboardLayout, KeyboardState, View};
|
||||
use crate::gui::Colors;
|
||||
use crate::AppConfig;
|
||||
|
||||
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,49 @@
|
||||
// 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)),
|
||||
}
|
||||
}
|
||||
}
|
||||
+10
-4
@@ -27,8 +27,8 @@ mod content;
|
||||
pub use content::*;
|
||||
|
||||
pub mod network;
|
||||
|
||||
pub mod wallets;
|
||||
pub mod settings;
|
||||
|
||||
mod camera;
|
||||
pub use camera::*;
|
||||
@@ -36,8 +36,14 @@ pub use camera::*;
|
||||
mod qr;
|
||||
pub use qr::*;
|
||||
|
||||
mod file;
|
||||
pub use file::*;
|
||||
mod file_pick;
|
||||
pub use file_pick::*;
|
||||
|
||||
mod pull_to_refresh;
|
||||
pub use pull_to_refresh::*;
|
||||
pub use pull_to_refresh::*;
|
||||
|
||||
mod scan;
|
||||
pub use scan::*;
|
||||
|
||||
mod input;
|
||||
pub use input::*;
|
||||
Regular → Executable
+166
-139
@@ -12,34 +12,37 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// 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, Rect, RichText, Rounding, Stroke, Vec2};
|
||||
use egui::epaint::{RectShape, Shadow};
|
||||
use egui::os::OperatingSystem;
|
||||
use egui::{Align2, CornerRadius, RichText, Stroke, StrokeKind, UiBuilder, Vec2};
|
||||
use lazy_static::lazy_static;
|
||||
use parking_lot::RwLock;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::gui::Colors;
|
||||
use crate::gui::views::{Content, View};
|
||||
use crate::gui::views::types::{ModalPosition, ModalState};
|
||||
use crate::gui::views::{Content, View};
|
||||
use crate::gui::Colors;
|
||||
use crate::gui::platform::PlatformCallbacks;
|
||||
|
||||
lazy_static! {
|
||||
/// Showing [`Modal`] state to be accessible from different ui parts.
|
||||
static ref MODAL_STATE: Arc<RwLock<ModalState>> = Arc::new(RwLock::new(ModalState::default()));
|
||||
}
|
||||
|
||||
/// Stores data to draw modal [`egui::Window`] at ui.
|
||||
/// Modal [`egui::Window`] container.
|
||||
#[derive(Clone)]
|
||||
pub struct Modal {
|
||||
/// Identifier for modal.
|
||||
pub(crate) id: &'static str,
|
||||
/// Position on the screen.
|
||||
position: ModalPosition,
|
||||
/// To check if it can be closed.
|
||||
pub position: ModalPosition,
|
||||
/// Flag to check if modal can be closed by keys.
|
||||
closeable: Arc<AtomicBool>,
|
||||
/// Title text
|
||||
title: Option<String>
|
||||
/// Title text.
|
||||
title: Option<String>,
|
||||
/// Flag to check first content render.
|
||||
first_draw: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl Modal {
|
||||
@@ -47,6 +50,8 @@ impl Modal {
|
||||
const DEFAULT_MARGIN: f32 = 8.0;
|
||||
/// Maximum width of the content.
|
||||
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.
|
||||
pub fn new(id: &'static str) -> Self {
|
||||
@@ -54,7 +59,8 @@ impl Modal {
|
||||
id,
|
||||
position: ModalPosition::Center,
|
||||
closeable: Arc::new(AtomicBool::new(true)),
|
||||
title: None
|
||||
title: None,
|
||||
first_draw: Arc::new(AtomicBool::new(true)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,8 +70,14 @@ impl Modal {
|
||||
self
|
||||
}
|
||||
|
||||
/// Mark [`Modal`] closed.
|
||||
pub fn close(&self) {
|
||||
/// Change [`Modal`] position on the screen.
|
||||
pub fn change_position(position: ModalPosition) {
|
||||
let mut w_state = MODAL_STATE.write();
|
||||
w_state.modal.as_mut().unwrap().position = position;
|
||||
}
|
||||
|
||||
/// Close [`Modal`] by clearing its state.
|
||||
pub fn close() {
|
||||
let mut w_nav = MODAL_STATE.write();
|
||||
w_nav.modal = None;
|
||||
}
|
||||
@@ -92,34 +104,31 @@ impl Modal {
|
||||
}
|
||||
|
||||
/// Set title text on [`Modal`] creation.
|
||||
pub fn title(mut self, title: String) -> Self {
|
||||
self.title = Some(title.to_uppercase());
|
||||
pub fn title(mut self, title: impl Into<String>) -> Self {
|
||||
self.title = Some(title.into().to_uppercase());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set [`Modal`] instance into state to show at ui.
|
||||
pub fn show(self) {
|
||||
let mut w_nav = MODAL_STATE.write();
|
||||
self.first_draw.store(true, Ordering::Relaxed);
|
||||
w_nav.modal = Some(self);
|
||||
}
|
||||
|
||||
/// Remove [`Modal`] from [`ModalState`] if it's showing and can be closed.
|
||||
/// Return `false` if Modal existed in [`ModalState`] before call.
|
||||
/// Return `false` if modal existed in state before call.
|
||||
pub fn on_back() -> bool {
|
||||
let mut w_state = MODAL_STATE.write();
|
||||
|
||||
// If Modal is showing and closeable, remove it from state.
|
||||
if w_state.modal.is_some() {
|
||||
let modal = w_state.modal.as_ref().unwrap();
|
||||
if modal.is_closeable() {
|
||||
w_state.modal = None;
|
||||
if Self::opened().is_some() {
|
||||
if Self::opened_closeable() {
|
||||
Self::close();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// Return id of opened [`Modal`].
|
||||
/// Return identifier of opened [`Modal`].
|
||||
pub fn opened() -> Option<&'static str> {
|
||||
// Check if modal is showing.
|
||||
{
|
||||
@@ -134,19 +143,42 @@ impl Modal {
|
||||
Some(modal.id)
|
||||
}
|
||||
|
||||
/// Check if [`Modal`] is opened and can be closed.
|
||||
pub fn opened_closeable() -> bool {
|
||||
// Check if modal is showing.
|
||||
{
|
||||
if MODAL_STATE.read().modal.is_none() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
let r_state = MODAL_STATE.read();
|
||||
let modal = r_state.modal.as_ref().unwrap();
|
||||
modal.closeable.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// Set title text for current opened [`Modal`].
|
||||
pub fn set_title(title: String) {
|
||||
// Save state.
|
||||
pub fn set_title(title: impl Into<String>) {
|
||||
let mut w_state = MODAL_STATE.write();
|
||||
if w_state.modal.is_some() {
|
||||
let mut modal = w_state.modal.clone().unwrap();
|
||||
modal.title = Some(title.to_uppercase());
|
||||
modal.title = Some(title.into().to_uppercase());
|
||||
w_state.modal = Some(modal);
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw opened [`Modal`] content.
|
||||
pub fn ui(ctx: &egui::Context, add_content: impl FnOnce(&mut egui::Ui, &Modal)) {
|
||||
/// Check for first [`Modal`] content rendering.
|
||||
pub fn first_draw() -> bool {
|
||||
if Self::opened().is_none() {
|
||||
return false;
|
||||
}
|
||||
let r_state = MODAL_STATE.read();
|
||||
let modal = r_state.modal.as_ref().unwrap();
|
||||
modal.first_draw.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
pub fn ui(ctx: &egui::Context,
|
||||
cb: &dyn PlatformCallbacks,
|
||||
add_content: impl FnOnce(&mut egui::Ui, &Modal, &dyn PlatformCallbacks)) {
|
||||
let has_modal = {
|
||||
MODAL_STATE.read().modal.is_some()
|
||||
};
|
||||
@@ -155,49 +187,55 @@ impl Modal {
|
||||
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.
|
||||
fn window_ui(&self, ctx: &egui::Context, add_content: impl FnOnce(&mut egui::Ui, &Modal)) {
|
||||
fn window_ui(&self,
|
||||
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)
|
||||
});
|
||||
let is_mac_os = OperatingSystem::from_target_os() == OperatingSystem::Mac;
|
||||
|
||||
let mut rect = ctx.screen_rect();
|
||||
if View::is_desktop() && !is_mac_os {
|
||||
let margin = if !is_fullscreen {
|
||||
Content::WINDOW_FRAME_MARGIN
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
rect = rect.shrink(margin - 0.5);
|
||||
rect.min += egui::vec2(0.0, Content::WINDOW_TITLE_HEIGHT + 0.5);
|
||||
rect.max.x += 0.5;
|
||||
}
|
||||
// Setup background rect.
|
||||
let is_win = OperatingSystem::Windows == OperatingSystem::from_target_os();
|
||||
let bg_rect = if View::is_desktop() && !is_win {
|
||||
let mut r = ctx.content_rect();
|
||||
let is_mac = OperatingSystem::Mac == OperatingSystem::from_target_os();
|
||||
if !is_mac && !is_fullscreen {
|
||||
r = r.shrink(Content::WINDOW_FRAME_MARGIN - 1.0);
|
||||
}
|
||||
r.min.y += Content::WINDOW_TITLE_HEIGHT;
|
||||
r
|
||||
} else {
|
||||
ctx.content_rect()
|
||||
};
|
||||
|
||||
// Draw modal background.
|
||||
egui::Window::new("modal_bg_window")
|
||||
.title_bar(false)
|
||||
.resizable(false)
|
||||
.collapsible(false)
|
||||
.fixed_rect(rect)
|
||||
.fixed_rect(bg_rect)
|
||||
.frame(egui::Frame {
|
||||
fill: Colors::semi_transparent(),
|
||||
..Default::default()
|
||||
})
|
||||
.show(ctx, |ui| {
|
||||
ui.set_min_size(rect.size());
|
||||
ui.set_min_size(bg_rect.size());
|
||||
});
|
||||
|
||||
// Setup width of modal content.
|
||||
let side_insets = View::get_left_inset() + View::get_right_inset();
|
||||
let available_width = rect.width() - (side_insets + Self::DEFAULT_MARGIN);
|
||||
let available_width = ctx.content_rect().width() - (side_insets + Self::DEFAULT_MARGIN);
|
||||
let width = f32::min(available_width, Self::DEFAULT_WIDTH);
|
||||
|
||||
// Show main content Window at given position.
|
||||
let (content_align, content_offset) = self.modal_position(is_fullscreen);
|
||||
let layer_id = egui::Window::new(format!("modal_window_{}", self.id))
|
||||
// Show main content window at given position.
|
||||
let (content_align, content_offset) = self.modal_position();
|
||||
egui::Window::new(Self::WINDOW_ID)
|
||||
.title_bar(false)
|
||||
.resizable(false)
|
||||
.collapsible(false)
|
||||
@@ -207,38 +245,40 @@ impl Modal {
|
||||
.frame(egui::Frame {
|
||||
shadow: Shadow {
|
||||
offset: Default::default(),
|
||||
blur: 30.0,
|
||||
spread: 3.0,
|
||||
blur: 30.0 as u8,
|
||||
spread: 3.0 as u8,
|
||||
color: egui::Color32::from_black_alpha(32),
|
||||
},
|
||||
rounding: Rounding::same(8.0),
|
||||
fill: Colors::fill(),
|
||||
corner_radius: CornerRadius::same(8.0 as u8),
|
||||
..Default::default()
|
||||
})
|
||||
.show(ctx, |ui| {
|
||||
if self.title.is_some() {
|
||||
self.title_ui(ui);
|
||||
if let Some(title) = &self.title {
|
||||
title_ui(title, ui);
|
||||
}
|
||||
self.content_ui(ui, add_content);
|
||||
}).unwrap().response.layer_id;
|
||||
|
||||
// Always show main content Window above background Window.
|
||||
ctx.move_to_top(layer_id);
|
||||
self.content_ui(ui, cb, add_content);
|
||||
});
|
||||
|
||||
// Setup first draw flag.
|
||||
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`].
|
||||
fn modal_position(&self, is_fullscreen: bool) -> (Align2, Vec2) {
|
||||
fn modal_position(&self) -> (Align2, Vec2) {
|
||||
let align = match self.position {
|
||||
ModalPosition::CenterTop => Align2::CENTER_TOP,
|
||||
ModalPosition::Center => Align2::CENTER_CENTER
|
||||
};
|
||||
|
||||
let x_align = View::get_left_inset() - View::get_right_inset();
|
||||
|
||||
let is_mac_os = OperatingSystem::from_target_os() == OperatingSystem::Mac;
|
||||
let extra_y = if View::is_desktop() && !is_mac_os {
|
||||
Content::WINDOW_TITLE_HEIGHT + if !is_fullscreen {
|
||||
let is_mac = OperatingSystem::Mac == OperatingSystem::from_target_os();
|
||||
let is_win = OperatingSystem::Windows == OperatingSystem::from_target_os();
|
||||
let extra_y = if View::is_desktop() && !is_win {
|
||||
Content::WINDOW_TITLE_HEIGHT + if !is_mac {
|
||||
Content::WINDOW_FRAME_MARGIN
|
||||
} else {
|
||||
0.0
|
||||
@@ -246,7 +286,7 @@ impl Modal {
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
let y_align = View::get_top_inset() + Self::DEFAULT_MARGIN + extra_y;
|
||||
let y_align = View::get_top_inset() + Self::DEFAULT_MARGIN / 2.0 + extra_y;
|
||||
|
||||
let offset = match self.position {
|
||||
ModalPosition::CenterTop => Vec2::new(x_align, y_align),
|
||||
@@ -256,82 +296,69 @@ impl Modal {
|
||||
}
|
||||
|
||||
/// Draw provided content.
|
||||
fn content_ui(&self, ui: &mut egui::Ui, add_content: impl FnOnce(&mut egui::Ui, &Modal)) {
|
||||
fn content_ui(&self,
|
||||
ui: &mut egui::Ui,
|
||||
cb: &dyn PlatformCallbacks,
|
||||
add_content: impl FnOnce(&mut egui::Ui, &Modal, &dyn PlatformCallbacks)) {
|
||||
let mut rect = ui.available_rect_before_wrap();
|
||||
|
||||
// Create background shape.
|
||||
let mut bg_shape = RectShape::new(rect, if self.title.is_none() {
|
||||
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,
|
||||
}
|
||||
}, Colors::fill(), Stroke::NONE, StrokeKind::Outside);
|
||||
let bg_idx = ui.painter().add(bg_shape.clone());
|
||||
|
||||
rect.min += egui::emath::vec2(6.0, 0.0);
|
||||
rect.max -= egui::emath::vec2(6.0, 0.0);
|
||||
|
||||
// Create background shape.
|
||||
let rounding = if self.title.is_some() {
|
||||
Rounding {
|
||||
nw: 0.0,
|
||||
ne: 0.0,
|
||||
sw: 8.0,
|
||||
se: 8.0,
|
||||
}
|
||||
} else {
|
||||
Rounding::same(8.0)
|
||||
};
|
||||
let mut bg_shape = RectShape {
|
||||
rect,
|
||||
rounding,
|
||||
fill: Colors::fill(),
|
||||
stroke: Stroke::NONE,
|
||||
blur_width: 0.0,
|
||||
fill_texture_id: Default::default(),
|
||||
uv: Rect::ZERO
|
||||
};
|
||||
let bg_idx = ui.painter().add(bg_shape);
|
||||
|
||||
// Draw main content.
|
||||
let mut content_rect = ui.allocate_ui_at_rect(rect, |ui| {
|
||||
(add_content)(ui, self);
|
||||
}).response.rect;
|
||||
|
||||
// Setup background shape to be painted behind main content.
|
||||
content_rect.min -= egui::emath::vec2(6.0, 0.0);
|
||||
content_rect.max += egui::emath::vec2(6.0, 0.0);
|
||||
bg_shape.rect = content_rect;
|
||||
ui.painter().set(bg_idx, bg_shape);
|
||||
}
|
||||
|
||||
/// Draw title content.
|
||||
fn title_ui(&self, ui: &mut egui::Ui) {
|
||||
let rect = ui.available_rect_before_wrap();
|
||||
|
||||
// Create background shape.
|
||||
let mut bg_shape = RectShape {
|
||||
rect,
|
||||
rounding: Rounding {
|
||||
nw: 8.0,
|
||||
ne: 8.0,
|
||||
sw: 0.0,
|
||||
se: 0.0,
|
||||
},
|
||||
fill: Colors::yellow(),
|
||||
stroke: Stroke::NONE,
|
||||
blur_width: 0.0,
|
||||
fill_texture_id: Default::default(),
|
||||
uv: Rect::ZERO
|
||||
};
|
||||
let bg_idx = ui.painter().add(bg_shape);
|
||||
|
||||
// Draw title content.
|
||||
let title_resp = ui.allocate_ui_at_rect(rect, |ui| {
|
||||
ui.vertical_centered_justified(|ui| {
|
||||
ui.add_space(Self::DEFAULT_MARGIN + 1.0);
|
||||
ui.label(RichText::new(self.title.as_ref().unwrap())
|
||||
.size(19.0)
|
||||
.color(Colors::title(true))
|
||||
);
|
||||
ui.add_space(Self::DEFAULT_MARGIN);
|
||||
// Draw line below title.
|
||||
View::horizontal_line(ui, Colors::item_stroke());
|
||||
});
|
||||
let resp = ui.scope_builder(UiBuilder::new().max_rect(rect), |ui| {
|
||||
(add_content)(ui, self, cb);
|
||||
}).response;
|
||||
|
||||
// Setup background shape to be painted behind title content.
|
||||
bg_shape.rect = title_resp.rect;
|
||||
// Setup background size.
|
||||
let bg_rect = {
|
||||
let mut r = resp.rect.clone();
|
||||
r.min -= egui::emath::vec2(6.0, 0.0);
|
||||
r.max += egui::emath::vec2(6.0, 0.0);
|
||||
r
|
||||
};
|
||||
bg_shape.rect = bg_rect;
|
||||
ui.painter().set(bg_idx, bg_shape);
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw title content.
|
||||
fn title_ui(title: &String, ui: &mut egui::Ui) {
|
||||
let rect = ui.available_rect_before_wrap();
|
||||
|
||||
// Create background shape.
|
||||
let mut bg_shape = RectShape::new(rect, CornerRadius {
|
||||
nw: 8.0 as u8,
|
||||
ne: 8.0 as u8,
|
||||
sw: 0.0 as u8,
|
||||
se: 0.0 as u8,
|
||||
}, Colors::yellow(), Stroke::NONE, StrokeKind::Outside);
|
||||
let bg_idx = ui.painter().add(bg_shape.clone());
|
||||
|
||||
// Draw title content.
|
||||
let resp = ui.vertical_centered(|ui| {
|
||||
ui.add_space(Modal::DEFAULT_MARGIN + 2.0);
|
||||
ui.label(RichText::new(title)
|
||||
.size(19.0)
|
||||
.color(Colors::title(true))
|
||||
);
|
||||
ui.add_space(Modal::DEFAULT_MARGIN + 1.0);
|
||||
// Draw line below title.
|
||||
View::horizontal_line(ui, Colors::item_stroke());
|
||||
}).response;
|
||||
|
||||
// Setup background size.
|
||||
bg_shape.rect = resp.rect;
|
||||
ui.painter().set(bg_idx, bg_shape);
|
||||
}
|
||||
@@ -12,43 +12,41 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use egui::{Align, Layout, RichText, Rounding};
|
||||
use egui::{Align, Layout, RichText, CornerRadius, StrokeKind};
|
||||
|
||||
use crate::AppConfig;
|
||||
use crate::gui::Colors;
|
||||
use crate::gui::icons::{CARET_RIGHT, CHECK_CIRCLE, COMPUTER_TOWER, DOTS_THREE_CIRCLE, GLOBE_SIMPLE, PENCIL, PLUS_CIRCLE, POWER, TRASH, X_CIRCLE};
|
||||
use crate::gui::icons::{CARET_RIGHT, CHECK_CIRCLE, COMPUTER_TOWER, DOTS_THREE_CIRCLE, GLOBE_SIMPLE, PENCIL, PLUS_CIRCLE, POWER, TRASH, WARNING_CIRCLE, X_CIRCLE};
|
||||
use crate::gui::platform::PlatformCallbacks;
|
||||
use crate::gui::views::{Modal, View};
|
||||
use crate::gui::views::network::modals::ExternalConnectionModal;
|
||||
use crate::gui::views::network::NodeSetup;
|
||||
use crate::gui::views::types::{ModalContainer, ModalPosition};
|
||||
use crate::gui::views::types::{ContentContainer, ModalPosition};
|
||||
use crate::node::{Node, NodeConfig};
|
||||
use crate::wallet::{ConnectionsConfig, ExternalConnection};
|
||||
|
||||
/// Network connections content.
|
||||
pub struct ConnectionsContent {
|
||||
/// Flag to check connections state on first draw.
|
||||
first_draw: bool,
|
||||
/// External connection [`Modal`] content.
|
||||
ext_conn_modal: ExternalConnectionModal,
|
||||
|
||||
/// [`Modal`] identifiers allowed at this ui container.
|
||||
modal_ids: Vec<&'static str>
|
||||
}
|
||||
|
||||
impl Default for ConnectionsContent {
|
||||
fn default() -> Self {
|
||||
ExternalConnection::check_ext_conn_availability(None);
|
||||
Self {
|
||||
first_draw: true,
|
||||
ext_conn_modal: ExternalConnectionModal::new(None),
|
||||
modal_ids: vec![
|
||||
ExternalConnectionModal::NETWORK_ID
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ModalContainer for ConnectionsContent {
|
||||
fn modal_ids(&self) -> &Vec<&'static str> {
|
||||
&self.modal_ids
|
||||
impl ContentContainer for ConnectionsContent {
|
||||
fn modal_ids(&self) -> Vec<&'static str> {
|
||||
vec![
|
||||
ExternalConnectionModal::NETWORK_ID
|
||||
]
|
||||
}
|
||||
|
||||
fn modal_ui(&mut self,
|
||||
@@ -62,12 +60,13 @@ impl ModalContainer for ConnectionsContent {
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ConnectionsContent {
|
||||
/// Draw connections content.
|
||||
pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||
self.current_modal_ui(ui, 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);
|
||||
|
||||
@@ -78,7 +77,7 @@ impl ConnectionsContent {
|
||||
|
||||
// Check connections availability.
|
||||
if saved_chain_type != AppConfig::chain_type() {
|
||||
ExternalConnection::check_ext_conn_availability(None);
|
||||
ExternalConnection::check(None, ui.ctx());
|
||||
}
|
||||
|
||||
// Show integrated node info content.
|
||||
@@ -97,57 +96,57 @@ impl ConnectionsContent {
|
||||
// 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, cb);
|
||||
self.show_add_ext_conn_modal(None);
|
||||
});
|
||||
|
||||
ui.add_space(4.0);
|
||||
|
||||
let ext_conn_list = ConnectionsConfig::ext_conn_list();
|
||||
if !ext_conn_list.is_empty() {
|
||||
let ext_conn_size = ext_conn_list.len();
|
||||
if ext_conn_size != 0 {
|
||||
ui.add_space(8.0);
|
||||
for (index, conn) in ext_conn_list.iter().enumerate() {
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
// Draw connection list item.
|
||||
let len = ext_conn_list.len();
|
||||
Self::ext_conn_item_ui(ui, conn, index, len, |ui| {
|
||||
// Draw buttons for non-default connections.
|
||||
if conn.url != ExternalConnection::DEFAULT_MAIN_URL {
|
||||
let button_rounding = View::item_rounding(index, len, true);
|
||||
View::item_button(ui, button_rounding, TRASH, None, || {
|
||||
ConnectionsConfig::remove_ext_conn(conn.id);
|
||||
});
|
||||
View::item_button(ui, Rounding::default(), PENCIL, None, || {
|
||||
self.show_add_ext_conn_modal(Some(conn.clone()), cb);
|
||||
});
|
||||
}
|
||||
Self::ext_conn_item_ui(ui, conn, index, ext_conn_size, |ui| {
|
||||
let button_rounding = View::item_rounding(index, ext_conn_size, true);
|
||||
View::item_button(ui, button_rounding, TRASH, None, || {
|
||||
ConnectionsConfig::remove_ext_conn(conn.id);
|
||||
});
|
||||
View::item_button(ui, CornerRadius::default(), PENCIL, None, || {
|
||||
self.show_add_ext_conn_modal(Some(conn.clone()));
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ConnectionsContent {
|
||||
/// Draw integrated node connection item content.
|
||||
pub fn integrated_node_item_ui(ui: &mut egui::Ui, custom_button: impl FnOnce(&mut egui::Ui)) {
|
||||
// Draw round background.
|
||||
let mut rect = ui.available_rect_before_wrap();
|
||||
rect.set_height(78.0);
|
||||
let rounding = View::item_rounding(0, 1, false);
|
||||
ui.painter().rect(rect, rounding, Colors::fill(), View::item_stroke());
|
||||
ui.painter().rect(rect, rounding, Colors::fill(), View::item_stroke(), StrokeKind::Outside);
|
||||
|
||||
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
|
||||
// Draw custom button.
|
||||
custom_button(ui);
|
||||
|
||||
if !Node::is_running() {
|
||||
// Draw button to start integrated node.
|
||||
View::item_button(ui, Rounding::default(), POWER, Some(Colors::green()), || {
|
||||
Node::start();
|
||||
});
|
||||
} else if !Node::is_starting() && !Node::is_stopping() && !Node::is_restarting() {
|
||||
// Draw button to stop integrated node.
|
||||
View::item_button(ui, Rounding::default(), POWER, Some(Colors::red()), || {
|
||||
Node::stop(false);
|
||||
});
|
||||
// Draw buttons to start/stop node.
|
||||
if Node::get_error().is_none() {
|
||||
if !Node::is_running() {
|
||||
View::item_button(ui, CornerRadius::default(), POWER, Some(Colors::green()), || {
|
||||
Node::start();
|
||||
});
|
||||
} else if !Node::is_starting() && !Node::is_stopping() && !Node::is_restarting() {
|
||||
View::item_button(ui, CornerRadius::default(), POWER, Some(Colors::red()), || {
|
||||
Node::stop(false);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let layout_size = ui.available_size();
|
||||
@@ -163,15 +162,22 @@ impl ConnectionsContent {
|
||||
});
|
||||
|
||||
// Setup node status text.
|
||||
let status_icon = if !Node::is_running() {
|
||||
let has_error = Node::get_error().is_some();
|
||||
let status_icon = if has_error {
|
||||
WARNING_CIRCLE
|
||||
} else if !Node::is_running() {
|
||||
X_CIRCLE
|
||||
} else if Node::not_syncing() {
|
||||
CHECK_CIRCLE
|
||||
} else {
|
||||
DOTS_THREE_CIRCLE
|
||||
};
|
||||
let status_text = format!("{} {}", status_icon, Node::get_sync_status_text());
|
||||
ui.label(RichText::new(status_text).size(15.0).color(Colors::text(false)));
|
||||
let status_text = format!("{} {}", status_icon, if has_error {
|
||||
t!("error").into()
|
||||
} else {
|
||||
Node::get_sync_status_text()
|
||||
});
|
||||
View::ellipsize_text(ui, status_text, 15.0, Colors::text(false));
|
||||
ui.add_space(1.0);
|
||||
|
||||
// Setup node API address text.
|
||||
@@ -196,51 +202,50 @@ impl ConnectionsContent {
|
||||
// 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.painter().rect(bg_rect,
|
||||
item_rounding,
|
||||
Colors::fill(),
|
||||
View::item_stroke(),
|
||||
StrokeKind::Outside);
|
||||
|
||||
ui.vertical(|ui| {
|
||||
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
|
||||
// Draw provided buttons.
|
||||
buttons_ui(ui);
|
||||
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
|
||||
// Draw provided buttons.
|
||||
buttons_ui(ui);
|
||||
|
||||
let layout_size = ui.available_size();
|
||||
ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| {
|
||||
ui.add_space(6.0);
|
||||
ui.vertical(|ui| {
|
||||
// Draw connections URL.
|
||||
ui.add_space(4.0);
|
||||
let conn_text = format!("{} {}", GLOBE_SIMPLE, conn.url);
|
||||
View::ellipsize_text(ui, conn_text, 15.0, Colors::title(false));
|
||||
ui.add_space(1.0);
|
||||
let layout_size = ui.available_size();
|
||||
ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| {
|
||||
ui.add_space(6.0);
|
||||
ui.vertical(|ui| {
|
||||
// Draw connections URL.
|
||||
ui.add_space(4.0);
|
||||
let conn_text = format!("{} {}", GLOBE_SIMPLE, conn.url);
|
||||
View::ellipsize_text(ui, conn_text, 15.0, Colors::title(false));
|
||||
ui.add_space(1.0);
|
||||
|
||||
// Setup connection status text.
|
||||
let status_text = if let Some(available) = conn.available {
|
||||
if available {
|
||||
format!("{} {}", CHECK_CIRCLE, t!("network.available"))
|
||||
} else {
|
||||
format!("{} {}", X_CIRCLE, t!("network.not_available"))
|
||||
}
|
||||
// Setup connection status text.
|
||||
let status_text = if let Some(available) = conn.available {
|
||||
if available {
|
||||
format!("{} {}", CHECK_CIRCLE, t!("network.available"))
|
||||
} else {
|
||||
format!("{} {}", DOTS_THREE_CIRCLE, t!("network.availability_check"))
|
||||
};
|
||||
ui.label(RichText::new(status_text).size(15.0).color(Colors::gray()));
|
||||
ui.add_space(3.0);
|
||||
});
|
||||
format!("{} {}", X_CIRCLE, t!("network.not_available"))
|
||||
}
|
||||
} else {
|
||||
format!("{} {}", DOTS_THREE_CIRCLE, t!("network.availability_check"))
|
||||
};
|
||||
ui.label(RichText::new(status_text).size(15.0).color(Colors::gray()));
|
||||
ui.add_space(3.0);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// Show [`Modal`] to add external connection.
|
||||
pub fn show_add_ext_conn_modal(&mut self,
|
||||
conn: Option<ExternalConnection>,
|
||||
cb: &dyn PlatformCallbacks) {
|
||||
pub fn show_add_ext_conn_modal(&mut self, conn: Option<ExternalConnection>) {
|
||||
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();
|
||||
}
|
||||
}
|
||||
+248
-148
@@ -12,27 +12,29 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::time::Duration;
|
||||
use egui::{Id, Margin, RichText, ScrollArea};
|
||||
use egui::scroll_area::ScrollBarVisibility;
|
||||
use egui::{Id, Margin, RichText, ScrollArea};
|
||||
|
||||
use crate::AppConfig;
|
||||
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::{ARROWS_COUNTER_CLOCKWISE, ARROW_LEFT, BRIEFCASE, DATABASE, DOTS_THREE_OUTLINE_VERTICAL, FACTORY, FADERS, GAUGE, GEAR, GLOBE, POWER};
|
||||
use crate::gui::platform::PlatformCallbacks;
|
||||
use crate::gui::views::{Content, TitlePanel, View};
|
||||
use crate::gui::views::network::types::{NodeTab, NodeTabType};
|
||||
use crate::gui::views::network::{ConnectionsContent, NetworkMetrics, NetworkMining, NetworkNode, NetworkSettings};
|
||||
use crate::gui::views::network::types::{NetworkTab, NetworkTabType};
|
||||
use crate::gui::views::types::{TitleContentType, TitleType};
|
||||
use crate::node::{Node, NodeError};
|
||||
use crate::wallet::ExternalConnection;
|
||||
use crate::gui::views::settings::SettingsContent;
|
||||
use crate::gui::views::types::{ContentContainer, LinePosition, TitleContentType, TitleType};
|
||||
use crate::gui::views::{Content, TitlePanel, View};
|
||||
use crate::gui::Colors;
|
||||
use crate::node::{Node, NodeConfig, NodeError};
|
||||
use crate::AppConfig;
|
||||
|
||||
/// Network content.
|
||||
pub struct NetworkContent {
|
||||
/// Current integrated node tab content.
|
||||
node_tab_content: Box<dyn NetworkTab>,
|
||||
node_tab_content: Box<dyn NodeTab>,
|
||||
/// Connections content.
|
||||
connections: ConnectionsContent,
|
||||
|
||||
/// Application settings content.
|
||||
settings_content: Option<SettingsContent>,
|
||||
}
|
||||
|
||||
impl Default for NetworkContent {
|
||||
@@ -40,95 +42,149 @@ impl Default for NetworkContent {
|
||||
Self {
|
||||
node_tab_content: Box::new(NetworkNode::default()),
|
||||
connections: ConnectionsContent::default(),
|
||||
settings_content: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl NetworkContent {
|
||||
pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||
let show_settings = self.showing_settings();
|
||||
let show_connections = AppConfig::show_connections_network_panel();
|
||||
let dual_panel = Content::is_dual_panel_mode(ui);
|
||||
let dual_panel = Content::is_dual_panel_mode(ui.ctx());
|
||||
|
||||
// Show title panel.
|
||||
self.title_ui(ui, show_connections);
|
||||
self.title_ui(ui, dual_panel, show_connections);
|
||||
|
||||
// Show integrated node tabs content.
|
||||
if !show_connections {
|
||||
egui::TopBottomPanel::bottom("node_tabs_content")
|
||||
if !show_connections && !show_settings {
|
||||
let side_padding = View::TAB_ITEMS_PADDING + if View::is_desktop() {
|
||||
0.0
|
||||
} else {
|
||||
4.0
|
||||
};
|
||||
let tabs_margin = Margin {
|
||||
left: (View::get_left_inset() + side_padding) as i8,
|
||||
right: (View::far_right_inset_margin(ui) + side_padding) as i8,
|
||||
top: View::TAB_ITEMS_PADDING as i8,
|
||||
bottom: (View::get_bottom_inset() + View::TAB_ITEMS_PADDING) as i8,
|
||||
};
|
||||
egui::TopBottomPanel::bottom("network_tabs_content")
|
||||
.min_height(0.5)
|
||||
.resizable(false)
|
||||
.frame(egui::Frame {
|
||||
inner_margin: Margin {
|
||||
left: View::get_left_inset() + View::TAB_ITEMS_PADDING,
|
||||
right: View::far_right_inset_margin(ui) + View::TAB_ITEMS_PADDING,
|
||||
top: View::TAB_ITEMS_PADDING,
|
||||
bottom: View::get_bottom_inset() + View::TAB_ITEMS_PADDING,
|
||||
},
|
||||
inner_margin: tabs_margin,
|
||||
fill: Colors::fill(),
|
||||
..Default::default()
|
||||
})
|
||||
.show_inside(ui, |ui| {
|
||||
ui.vertical_centered(|ui| {
|
||||
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
|
||||
self.tabs_ui(ui);
|
||||
});
|
||||
let rect = ui.available_rect_before_wrap();
|
||||
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
|
||||
self.tabs_ui(ui);
|
||||
});
|
||||
// Draw content divider line.
|
||||
let r = {
|
||||
let mut r = rect.clone();
|
||||
r.min.x -= tabs_margin.left as f32;
|
||||
r.min.y -= tabs_margin.top as f32;
|
||||
r.max.x += tabs_margin.right as f32;
|
||||
r
|
||||
};
|
||||
View::line(ui, LinePosition::TOP, &r, Colors::stroke());
|
||||
});
|
||||
}
|
||||
|
||||
// Show current node tab content.
|
||||
egui::SidePanel::right("node_tab_content")
|
||||
// Show settings or integrated node content.
|
||||
egui::SidePanel::right("network_side_content")
|
||||
.resizable(false)
|
||||
.exact_width(ui.available_width())
|
||||
.frame(egui::Frame {
|
||||
..Default::default()
|
||||
})
|
||||
.show_animated_inside(ui, !show_connections, |ui| {
|
||||
.show_animated_inside(ui, show_settings || !show_connections, |ui| {
|
||||
egui::CentralPanel::default()
|
||||
.frame(egui::Frame {
|
||||
fill: Colors::white_or_black(false),
|
||||
stroke: View::item_stroke(),
|
||||
inner_margin: Margin {
|
||||
left: View::get_left_inset() + 4.0,
|
||||
right: View::far_right_inset_margin(ui) + 4.0,
|
||||
top: 3.0,
|
||||
bottom: 4.0,
|
||||
left: (View::get_left_inset() + View::content_padding()) as i8,
|
||||
right: (View::far_right_inset_margin(ui) +
|
||||
View::content_padding()) as i8,
|
||||
top: 3.0 as i8,
|
||||
bottom: 4.0 as i8,
|
||||
},
|
||||
..Default::default()
|
||||
})
|
||||
.show_inside(ui, |ui| {
|
||||
self.node_tab_content.ui(ui, cb);
|
||||
let rect = ui.available_rect_before_wrap();
|
||||
if let Some(c) = &mut self.settings_content {
|
||||
ScrollArea::vertical()
|
||||
.id_salt("app_settings_network")
|
||||
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
|
||||
.auto_shrink([false; 2])
|
||||
.show(ui, |ui| {
|
||||
ui.add_space(1.0);
|
||||
ui.vertical_centered(|ui| {
|
||||
// Show application settings content.
|
||||
View::max_width_ui(ui,
|
||||
Content::SIDE_PANEL_WIDTH * 1.3,
|
||||
|ui| {
|
||||
c.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());
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Show connections content.
|
||||
egui::CentralPanel::default()
|
||||
.frame(egui::Frame {
|
||||
stroke: View::item_stroke(),
|
||||
inner_margin: Margin {
|
||||
left: if show_connections {
|
||||
View::get_left_inset() + 4.0
|
||||
View::get_left_inset() + View::content_padding()
|
||||
} else {
|
||||
0.0
|
||||
},
|
||||
} as i8,
|
||||
right: if show_connections {
|
||||
View::far_right_inset_margin(ui) + 4.0
|
||||
View::far_right_inset_margin(ui) + View::content_padding()
|
||||
} else {
|
||||
0.0
|
||||
},
|
||||
top: 3.0,
|
||||
bottom: if View::is_desktop() && show_connections {
|
||||
6.0
|
||||
} else {
|
||||
4.0
|
||||
},
|
||||
} as i8,
|
||||
top: 3.0 as i8,
|
||||
bottom: 0.0 as i8,
|
||||
},
|
||||
fill: Colors::button(),
|
||||
..Default::default()
|
||||
})
|
||||
.show_inside(ui, |ui| {
|
||||
let rect = ui.available_rect_before_wrap();
|
||||
ScrollArea::vertical()
|
||||
.id_source("connections_content")
|
||||
.id_salt("connections_scroll")
|
||||
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
|
||||
.auto_shrink([false; 2])
|
||||
.show(ui, |ui| {
|
||||
@@ -143,45 +199,72 @@ impl NetworkContent {
|
||||
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());
|
||||
}
|
||||
});
|
||||
|
||||
// Redraw after delay.
|
||||
if Node::is_running() {
|
||||
// Redraw after delay if node is running at non-dual-panel mode.
|
||||
if ((!dual_panel && Content::is_network_panel_open()) || dual_panel) && Node::is_running() {
|
||||
ui.ctx().request_repaint_after(Node::STATS_UPDATE_DELAY);
|
||||
} else if show_connections {
|
||||
ui.ctx().request_repaint_after(Duration::from_millis(1000));
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw tab buttons in the bottom of the screen.
|
||||
/// Navigate back, return `true` if action was not consumed.
|
||||
pub fn on_back(&mut self) -> bool {
|
||||
if self.showing_settings() {
|
||||
// Close settings.
|
||||
self.settings_content = None;
|
||||
return false;
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// Check if application settings content is showing.
|
||||
pub fn showing_settings(&self) -> bool {
|
||||
self.settings_content.is_some()
|
||||
}
|
||||
|
||||
/// Draw tab buttons at bottom of the screen.
|
||||
fn tabs_ui(&mut self, ui: &mut egui::Ui) {
|
||||
ui.vertical_centered(|ui| {
|
||||
// Setup spacing between tabs.
|
||||
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.
|
||||
let current_type = self.node_tab_content.get_type();
|
||||
ui.columns(4, |columns| {
|
||||
columns[0].vertical_centered_justified(|ui| {
|
||||
View::tab_button(ui, DATABASE, current_type == NetworkTabType::Node, || {
|
||||
let active = Some(current_type == NodeTabType::Info);
|
||||
View::tab_button(ui, DATABASE, None, active, |_| {
|
||||
self.node_tab_content = Box::new(NetworkNode::default());
|
||||
});
|
||||
});
|
||||
columns[1].vertical_centered_justified(|ui| {
|
||||
View::tab_button(ui, GAUGE, current_type == NetworkTabType::Metrics, || {
|
||||
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| {
|
||||
View::tab_button(ui, FACTORY, current_type == NetworkTabType::Mining, || {
|
||||
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| {
|
||||
View::tab_button(ui, FADERS, current_type == NetworkTabType::Settings, || {
|
||||
let active = Some(current_type == NodeTabType::Settings);
|
||||
View::tab_button(ui, FADERS, None, active, |_| {
|
||||
self.node_tab_content = Box::new(NetworkSettings::default());
|
||||
});
|
||||
});
|
||||
@@ -190,29 +273,38 @@ impl NetworkContent {
|
||||
}
|
||||
|
||||
/// Draw title content.
|
||||
fn title_ui(&mut self, ui: &mut egui::Ui, show_connections: bool) {
|
||||
fn title_ui(&mut self, ui: &mut egui::Ui, dual_panel: bool, show_connections: bool) {
|
||||
let show_settings = self.showing_settings();
|
||||
|
||||
// Setup values for title panel.
|
||||
let title_text = self.node_tab_content.get_type().title().to_uppercase();
|
||||
let subtitle_text = Node::get_sync_status_text();
|
||||
let not_syncing = Node::not_syncing();
|
||||
let title_content = if !show_connections {
|
||||
let title_text = self.node_tab_content.get_type().title();
|
||||
let subtitle_text = Node::get_sync_status_text().into();
|
||||
let not_syncing = Node::not_syncing() && !Node::data_dir_changing();
|
||||
let title_content = if show_settings {
|
||||
TitleContentType::Title(t!("settings").into())
|
||||
} else if !show_connections {
|
||||
TitleContentType::WithSubTitle(title_text, subtitle_text, !not_syncing)
|
||||
} else {
|
||||
TitleContentType::Title(t!("network.connections").to_uppercase())
|
||||
TitleContentType::Title(t!("network.connections").into())
|
||||
};
|
||||
|
||||
// Draw title panel.
|
||||
TitlePanel::new(Id::from("network_title_panel")).ui(TitleType::Single(title_content), |ui| {
|
||||
if !show_connections {
|
||||
View::title_button_big(ui, DOTS_THREE_OUTLINE_VERTICAL, |_| {
|
||||
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();
|
||||
if AppConfig::show_connections_network_panel() {
|
||||
ExternalConnection::check_ext_conn_availability(None);
|
||||
}
|
||||
});
|
||||
} else if !dual_panel {
|
||||
View::title_button_big(ui, GEAR, |_| {
|
||||
self.settings_content = Some(SettingsContent::default());
|
||||
});
|
||||
}
|
||||
}, |ui| {
|
||||
if !Content::is_dual_panel_mode(ui) {
|
||||
if !dual_panel && !show_settings {
|
||||
View::title_button_big(ui, BRIEFCASE, |_| {
|
||||
Content::toggle_network_panel();
|
||||
});
|
||||
@@ -220,25 +312,8 @@ impl NetworkContent {
|
||||
}, ui);
|
||||
}
|
||||
|
||||
/// Content to draw when node is disabled.
|
||||
pub fn disabled_node_ui(ui: &mut egui::Ui) {
|
||||
View::center_content(ui, 156.0, |ui| {
|
||||
let text = t!("network.disabled_server", "dots" => DOTS_THREE_OUTLINE_VERTICAL);
|
||||
ui.label(RichText::new(text)
|
||||
.size(16.0)
|
||||
.color(Colors::inactive_text())
|
||||
);
|
||||
ui.add_space(8.0);
|
||||
View::action_button(ui, format!("{} {}", POWER, t!("network.enable_node")), || {
|
||||
Node::start();
|
||||
});
|
||||
ui.add_space(2.0);
|
||||
Self::autorun_node_ui(ui);
|
||||
});
|
||||
}
|
||||
|
||||
/// Content to draw on loading.
|
||||
pub fn loading_ui(ui: &mut egui::Ui, text: Option<String>) {
|
||||
pub fn loading_ui(ui: &mut egui::Ui, text: Option<impl Into<String>>) {
|
||||
match text {
|
||||
None => {
|
||||
ui.centered_and_justified(|ui| {
|
||||
@@ -265,73 +340,98 @@ impl NetworkContent {
|
||||
AppConfig::toggle_node_autostart();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw integrated node error content.
|
||||
pub fn node_error_ui(ui: &mut egui::Ui, e: NodeError) {
|
||||
match e {
|
||||
NodeError::Storage => {
|
||||
View::center_content(ui, 156.0, |ui| {
|
||||
ui.label(RichText::new(t!("network_node.error_clean"))
|
||||
.size(16.0)
|
||||
.color(Colors::red())
|
||||
);
|
||||
ui.add_space(8.0);
|
||||
let btn_txt = format!("{} {}",
|
||||
ARROWS_COUNTER_CLOCKWISE,
|
||||
t!("network_node.resync"));
|
||||
View::action_button(ui, btn_txt, || {
|
||||
Node::clean_up_data();
|
||||
Node::start();
|
||||
});
|
||||
ui.add_space(2.0);
|
||||
/// Content to draw when node is disabled.
|
||||
fn disabled_node_ui(ui: &mut egui::Ui) {
|
||||
View::center_content(ui, 156.0, |ui| {
|
||||
let text = t!("network.disabled_server", "dots" => DOTS_THREE_OUTLINE_VERTICAL);
|
||||
ui.label(RichText::new(text)
|
||||
.size(16.0)
|
||||
.color(Colors::inactive_text())
|
||||
);
|
||||
ui.add_space(8.0);
|
||||
View::action_button(ui, format!("{} {}", POWER, t!("network.enable_node")), || {
|
||||
Node::start();
|
||||
});
|
||||
ui.add_space(2.0);
|
||||
NetworkContent::autorun_node_ui(ui);
|
||||
});
|
||||
}
|
||||
|
||||
/// Draw integrated node error content.
|
||||
pub fn node_error_ui(ui: &mut egui::Ui, e: NodeError) {
|
||||
match e {
|
||||
NodeError::Storage => {
|
||||
View::center_content(ui, 156.0, |ui| {
|
||||
ui.label(RichText::new(t!("network_node.error_clean"))
|
||||
.size(16.0)
|
||||
.color(Colors::red())
|
||||
);
|
||||
ui.add_space(8.0);
|
||||
let btn_txt = format!("{} {}",
|
||||
ARROWS_COUNTER_CLOCKWISE,
|
||||
t!("network_node.resync"));
|
||||
View::action_button(ui, btn_txt, || {
|
||||
Node::clean_up_data();
|
||||
Node::start();
|
||||
});
|
||||
return;
|
||||
}
|
||||
NodeError::P2P | NodeError::API => {
|
||||
let msg_type = match e {
|
||||
NodeError::API => "API",
|
||||
_ => "P2P"
|
||||
};
|
||||
View::center_content(ui, 106.0, |ui| {
|
||||
let text = t!(
|
||||
ui.add_space(2.0);
|
||||
});
|
||||
return;
|
||||
}
|
||||
NodeError::P2P | NodeError::API => {
|
||||
let msg_type = match e {
|
||||
NodeError::API => "API",
|
||||
_ => "P2P"
|
||||
};
|
||||
View::center_content(ui, 106.0, |ui| {
|
||||
let text = t!(
|
||||
"network_node.error_p2p_api",
|
||||
"p2p_api" => msg_type,
|
||||
"settings" => FADERS
|
||||
);
|
||||
ui.label(RichText::new(text)
|
||||
.size(16.0)
|
||||
.color(Colors::red())
|
||||
);
|
||||
ui.add_space(2.0);
|
||||
ui.label(RichText::new(text)
|
||||
.size(16.0)
|
||||
.color(Colors::red())
|
||||
);
|
||||
ui.add_space(2.0);
|
||||
});
|
||||
return;
|
||||
}
|
||||
NodeError::Configuration => {
|
||||
View::center_content(ui, 106.0, |ui| {
|
||||
ui.label(RichText::new(t!("network_node.error_config", "settings" => FADERS))
|
||||
.size(16.0)
|
||||
.color(Colors::red())
|
||||
);
|
||||
ui.add_space(8.0);
|
||||
let btn_txt = format!("{} {}",
|
||||
ARROWS_COUNTER_CLOCKWISE,
|
||||
t!("network_settings.reset"));
|
||||
View::action_button(ui, btn_txt, || {
|
||||
NodeConfig::reset_to_default();
|
||||
Node::start();
|
||||
});
|
||||
return;
|
||||
}
|
||||
NodeError::Configuration => {
|
||||
View::center_content(ui, 106.0, |ui| {
|
||||
ui.label(RichText::new(t!("network_node.error_config", "settings" => FADERS))
|
||||
.size(16.0)
|
||||
.color(Colors::red())
|
||||
);
|
||||
ui.add_space(2.0);
|
||||
ui.add_space(2.0);
|
||||
});
|
||||
}
|
||||
NodeError::Unknown => {
|
||||
View::center_content(ui, 156.0, |ui| {
|
||||
ui.label(RichText::new(t!("network_node.error_unknown", "settings" => FADERS))
|
||||
.size(16.0)
|
||||
.color(Colors::red())
|
||||
);
|
||||
ui.add_space(8.0);
|
||||
let btn_txt = format!("{} {}",
|
||||
ARROWS_COUNTER_CLOCKWISE,
|
||||
t!("network_node.resync"));
|
||||
View::action_button(ui, btn_txt, || {
|
||||
Node::clean_up_data();
|
||||
Node::start();
|
||||
});
|
||||
}
|
||||
NodeError::Unknown => {
|
||||
View::center_content(ui, 156.0, |ui| {
|
||||
ui.label(RichText::new(t!("network_node.error_unknown", "settings" => FADERS))
|
||||
.size(16.0)
|
||||
.color(Colors::red())
|
||||
);
|
||||
ui.add_space(8.0);
|
||||
let btn_txt = format!("{} {}",
|
||||
ARROWS_COUNTER_CLOCKWISE,
|
||||
t!("network_node.resync"));
|
||||
View::action_button(ui, btn_txt, || {
|
||||
Node::clean_up_data();
|
||||
Node::start();
|
||||
});
|
||||
ui.add_space(2.0);
|
||||
});
|
||||
}
|
||||
ui.add_space(2.0);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,8 +12,9 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use egui::{RichText, Rounding, ScrollArea, vec2};
|
||||
use egui::{RichText, CornerRadius, ScrollArea, vec2, StrokeKind};
|
||||
use egui::scroll_area::ScrollBarVisibility;
|
||||
use grin_core::consensus::{DAY_HEIGHT, GRIN_BASE, HOUR_SEC, REWARD};
|
||||
use grin_servers::{DiffBlock, ServerStats};
|
||||
|
||||
use crate::gui::Colors;
|
||||
@@ -21,90 +22,64 @@ use crate::gui::icons::{AT, COINS, CUBE_TRANSPARENT, HOURGLASS_LOW, HOURGLASS_ME
|
||||
use crate::gui::platform::PlatformCallbacks;
|
||||
use crate::gui::views::{Content, View};
|
||||
use crate::gui::views::network::NetworkContent;
|
||||
use crate::gui::views::network::types::{NetworkTab, NetworkTabType};
|
||||
use crate::gui::views::network::types::{NodeTab, NodeTabType};
|
||||
use crate::node::Node;
|
||||
|
||||
/// Chain metrics tab content.
|
||||
#[derive(Default)]
|
||||
pub struct NetworkMetrics;
|
||||
|
||||
const BLOCK_REWARD: f64 = 60.0;
|
||||
// 1 year is calculated as 365 days and 6 hours (31557600).
|
||||
const YEARLY_SUPPLY: f64 = ((60 * 60 * 24 * 365) + 6 * 60 * 60) as f64;
|
||||
const BLOCK_REWARD: u64 = REWARD / GRIN_BASE;
|
||||
// 1 year as 365 days and 6 hours (31557600).
|
||||
const YEARLY_SUPPLY: u64 = (BLOCK_REWARD * DAY_HEIGHT * 365) + 6 * HOUR_SEC;
|
||||
|
||||
impl NetworkTab for NetworkMetrics {
|
||||
fn get_type(&self) -> NetworkTabType {
|
||||
NetworkTabType::Metrics
|
||||
impl NodeTab for NetworkMetrics {
|
||||
fn get_type(&self) -> NodeTabType {
|
||||
NodeTabType::Metrics
|
||||
}
|
||||
|
||||
fn ui(&mut self, ui: &mut egui::Ui, _: &dyn PlatformCallbacks) {
|
||||
// Show an error content when available.
|
||||
let node_err = Node::get_error();
|
||||
if node_err.is_some() {
|
||||
NetworkContent::node_error_ui(ui, node_err.unwrap());
|
||||
return;
|
||||
}
|
||||
|
||||
// Show message to enable node when it's not running.
|
||||
if !Node::is_running() {
|
||||
NetworkContent::disabled_node_ui(ui);
|
||||
return;
|
||||
}
|
||||
|
||||
// Show loading spinner when node is stopping.
|
||||
if Node::is_stopping() {
|
||||
NetworkContent::loading_ui(ui, None);
|
||||
return;
|
||||
}
|
||||
|
||||
// Show message when metrics are not available.
|
||||
fn tab_ui(&mut self, ui: &mut egui::Ui, _: &dyn PlatformCallbacks) {
|
||||
let server_stats = Node::get_stats();
|
||||
if server_stats.is_none() || Node::is_restarting()
|
||||
|| server_stats.as_ref().unwrap().diff_stats.height == 0 {
|
||||
let stats = server_stats.as_ref().unwrap();
|
||||
if stats.diff_stats.height == 0 {
|
||||
NetworkContent::loading_ui(ui, Some(t!("network_metrics.loading")));
|
||||
return;
|
||||
}
|
||||
|
||||
ui.add_space(1.0);
|
||||
ui.vertical_centered(|ui| {
|
||||
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
|
||||
let stats = server_stats.as_ref().unwrap();
|
||||
// Show emission and difficulty info.
|
||||
info_ui(ui, stats);
|
||||
// Show difficulty adjustment window blocks.
|
||||
blocks_ui(ui, stats);
|
||||
});
|
||||
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
|
||||
// Show emission and difficulty info.
|
||||
info_ui(ui, stats);
|
||||
// Show difficulty adjustment window blocks.
|
||||
blocks_ui(ui, stats);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const BLOCK_ITEM_HEIGHT: f32 = 78.0;
|
||||
|
||||
/// Draw emission and difficulty info.
|
||||
fn info_ui(ui: &mut egui::Ui, stats: &ServerStats) {
|
||||
// Show emission info.
|
||||
View::sub_title(ui, format!("{} {}", COINS, t!("network_metrics.emission")));
|
||||
ui.columns(3, |columns| {
|
||||
let supply = stats.header_stats.height as f64 * BLOCK_REWARD;
|
||||
let rate = (YEARLY_SUPPLY * 100.0) / supply;
|
||||
let supply = stats.header_stats.height * BLOCK_REWARD;
|
||||
let rate = (YEARLY_SUPPLY * 100) / supply;
|
||||
|
||||
columns[0].vertical_centered(|ui| {
|
||||
View::rounded_box(ui,
|
||||
format!("{}ツ", BLOCK_REWARD),
|
||||
t!("network_metrics.reward"),
|
||||
[true, false, true, false]);
|
||||
View::label_box(ui,
|
||||
format!("{}ツ", BLOCK_REWARD),
|
||||
t!("network_metrics.reward"),
|
||||
[true, false, true, false]);
|
||||
});
|
||||
columns[1].vertical_centered(|ui| {
|
||||
View::rounded_box(ui,
|
||||
format!("{:.2}%", rate),
|
||||
t!("network_metrics.inflation"),
|
||||
[false, false, false, false]);
|
||||
View::label_box(ui,
|
||||
format!("{:.2}%", rate),
|
||||
t!("network_metrics.inflation"),
|
||||
[false, false, false, false]);
|
||||
});
|
||||
columns[2].vertical_centered(|ui| {
|
||||
View::rounded_box(ui,
|
||||
supply.to_string(),
|
||||
t!("network_metrics.supply"),
|
||||
[false, true, false, true]);
|
||||
View::label_box(ui,
|
||||
supply.to_string(),
|
||||
t!("network_metrics.supply"),
|
||||
[false, true, false, true]);
|
||||
});
|
||||
});
|
||||
ui.add_space(5.0);
|
||||
@@ -117,32 +92,34 @@ fn info_ui(ui: &mut egui::Ui, stats: &ServerStats) {
|
||||
View::sub_title(ui, format!("{} {}", HOURGLASS_MEDIUM, difficulty_title));
|
||||
ui.columns(3, |columns| {
|
||||
columns[0].vertical_centered(|ui| {
|
||||
View::rounded_box(ui,
|
||||
stats.diff_stats.height.to_string(),
|
||||
t!("network_node.height"),
|
||||
[true, false, true, false]);
|
||||
View::label_box(ui,
|
||||
stats.diff_stats.height.to_string(),
|
||||
t!("network_node.height"),
|
||||
[true, false, true, false]);
|
||||
});
|
||||
columns[1].vertical_centered(|ui| {
|
||||
View::rounded_box(ui,
|
||||
format!("{}s", stats.diff_stats.average_block_time),
|
||||
t!("network_metrics.block_time"),
|
||||
[false, false, false, false]);
|
||||
View::label_box(ui,
|
||||
format!("{}s", stats.diff_stats.average_block_time),
|
||||
t!("network_metrics.block_time"),
|
||||
[false, false, false, false]);
|
||||
});
|
||||
columns[2].vertical_centered(|ui| {
|
||||
View::rounded_box(ui,
|
||||
stats.diff_stats.average_difficulty.to_string(),
|
||||
t!("network_node.difficulty"),
|
||||
[false, true, false, true]);
|
||||
View::label_box(ui,
|
||||
stats.diff_stats.average_difficulty.to_string(),
|
||||
t!("network_node.difficulty"),
|
||||
[false, true, false, true]);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const BLOCK_ITEM_HEIGHT: f32 = 77.0;
|
||||
|
||||
/// Draw difficulty adjustment window blocks content.
|
||||
fn blocks_ui(ui: &mut egui::Ui, stats: &ServerStats) {
|
||||
let blocks_size = stats.diff_stats.last_blocks.len();
|
||||
ui.add_space(4.0);
|
||||
ScrollArea::vertical()
|
||||
.id_source("difficulty_scroll")
|
||||
.id_salt("mining_difficulty_scroll")
|
||||
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
|
||||
.auto_shrink([false; 2])
|
||||
.stick_to_bottom(true)
|
||||
@@ -151,11 +128,8 @@ fn blocks_ui(ui: &mut egui::Ui, stats: &ServerStats) {
|
||||
BLOCK_ITEM_HEIGHT,
|
||||
blocks_size,
|
||||
|ui, row_range| {
|
||||
ui.add_space(4.0);
|
||||
for index in row_range {
|
||||
// Add space before the first item.
|
||||
if index == 0 {
|
||||
ui.add_space(4.0);
|
||||
}
|
||||
let db = stats.diff_stats.last_blocks.get(index).unwrap();
|
||||
block_item_ui(ui, db, View::item_rounding(index, blocks_size, false));
|
||||
}
|
||||
@@ -164,40 +138,46 @@ fn blocks_ui(ui: &mut egui::Ui, stats: &ServerStats) {
|
||||
}
|
||||
|
||||
/// 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();
|
||||
rect.set_height(BLOCK_ITEM_HEIGHT);
|
||||
ui.allocate_ui_at_rect(rect, |ui| {
|
||||
ui.allocate_ui(rect.size(), |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.add_space(6.0);
|
||||
ui.vertical(|ui| {
|
||||
ui.add_space(3.0);
|
||||
ui.add_space(4.0);
|
||||
|
||||
// Draw round background.
|
||||
rect.min += vec2(8.0, 0.0);
|
||||
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::white_or_black(false),
|
||||
View::item_stroke(),
|
||||
StrokeKind::Outside);
|
||||
|
||||
// Draw block hash.
|
||||
ui.horizontal(|ui| {
|
||||
ui.add_space(7.0);
|
||||
ui.add_space(8.0);
|
||||
ui.label(RichText::new(db.block_hash.to_string())
|
||||
.color(Colors::white_or_black(true))
|
||||
.size(17.0));
|
||||
});
|
||||
// Draw block difficulty and height.
|
||||
ui.horizontal(|ui| {
|
||||
ui.add_space(6.0);
|
||||
ui.add_space(7.0);
|
||||
let diff_text = format!("{} {} {} {}",
|
||||
CUBE_TRANSPARENT,
|
||||
db.difficulty,
|
||||
AT,
|
||||
db.block_height);
|
||||
ui.label(RichText::new(diff_text).color(Colors::title(false)).size(16.0));
|
||||
ui.label(RichText::new(diff_text)
|
||||
.color(Colors::title(false))
|
||||
.size(15.0));
|
||||
});
|
||||
// Draw block date.
|
||||
ui.horizontal(|ui| {
|
||||
ui.add_space(6.0);
|
||||
ui.add_space(7.0);
|
||||
let block_time = View::format_time(db.time as i64);
|
||||
ui.label(RichText::new(format!("{} {}s {} {}",
|
||||
TIMER,
|
||||
@@ -205,7 +185,7 @@ fn block_item_ui(ui: &mut egui::Ui, db: &DiffBlock, rounding: Rounding) {
|
||||
HOURGLASS_LOW,
|
||||
block_time))
|
||||
.color(Colors::gray())
|
||||
.size(16.0));
|
||||
.size(15.0));
|
||||
});
|
||||
ui.add_space(3.0);
|
||||
});
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use egui::{RichText, Rounding, ScrollArea};
|
||||
use egui::{RichText, CornerRadius, ScrollArea, StrokeKind};
|
||||
use egui::scroll_area::ScrollBarVisibility;
|
||||
use grin_chain::SyncStatus;
|
||||
use grin_servers::WorkerStats;
|
||||
@@ -23,62 +23,31 @@ use crate::gui::platform::PlatformCallbacks;
|
||||
use crate::gui::views::{Content, View};
|
||||
use crate::gui::views::network::NetworkContent;
|
||||
use crate::gui::views::network::setup::StratumSetup;
|
||||
use crate::gui::views::network::types::{NetworkTab, NetworkTabType};
|
||||
use crate::gui::views::network::types::{NodeTab, NodeTabType};
|
||||
use crate::gui::views::types::ContentContainer;
|
||||
use crate::node::{Node, NodeConfig};
|
||||
use crate::wallet::WalletConfig;
|
||||
|
||||
/// Mining tab content.
|
||||
pub struct NetworkMining {
|
||||
/// Stratum server setup content.
|
||||
stratum_server_setup: StratumSetup,
|
||||
|
||||
/// Wallet name for rewards.
|
||||
wallet_name: String,
|
||||
}
|
||||
|
||||
impl Default for NetworkMining {
|
||||
fn default() -> Self {
|
||||
let wallet_name = if let Some(id) = NodeConfig::get_stratum_wallet_id() {
|
||||
WalletConfig::name_by_id(id).unwrap_or("-".to_string())
|
||||
} else {
|
||||
"-".to_string()
|
||||
};
|
||||
Self {
|
||||
stratum_server_setup: StratumSetup::default(),
|
||||
wallet_name,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl NetworkTab for NetworkMining {
|
||||
fn get_type(&self) -> NetworkTabType {
|
||||
NetworkTabType::Mining
|
||||
impl NodeTab for NetworkMining {
|
||||
fn get_type(&self) -> NodeTabType {
|
||||
NodeTabType::Mining
|
||||
}
|
||||
|
||||
fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||
// Show an error content when available.
|
||||
let node_err = Node::get_error();
|
||||
if node_err.is_some() {
|
||||
NetworkContent::node_error_ui(ui, node_err.unwrap());
|
||||
return;
|
||||
}
|
||||
|
||||
// Show message to enable node when it's not running.
|
||||
if !Node::is_running() {
|
||||
NetworkContent::disabled_node_ui(ui);
|
||||
return;
|
||||
}
|
||||
|
||||
// Show loading spinner when node is stopping or stratum server is starting.
|
||||
if Node::is_stopping() || Node::is_stratum_starting() {
|
||||
NetworkContent::loading_ui(ui, None);
|
||||
return;
|
||||
}
|
||||
|
||||
// Show message when mining is not available.
|
||||
let server_stats = Node::get_stats();
|
||||
if server_stats.is_none() || Node::is_restarting()
|
||||
|| Node::get_sync_status().unwrap() != SyncStatus::NoSync {
|
||||
fn tab_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||
if Node::is_stratum_starting() || Node::get_sync_status().unwrap() != SyncStatus::NoSync {
|
||||
NetworkContent::loading_ui(ui, Some(t!("network_mining.loading")));
|
||||
return;
|
||||
}
|
||||
@@ -87,15 +56,13 @@ impl NetworkTab for NetworkMining {
|
||||
let stratum_stats = Node::get_stratum_stats();
|
||||
if !stratum_stats.is_running {
|
||||
ScrollArea::vertical()
|
||||
.id_source("stratum_setup_scroll")
|
||||
.id_salt("stratum_setup_scroll")
|
||||
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
|
||||
.auto_shrink([false; 2])
|
||||
.show(ui, |ui| {
|
||||
ui.add_space(1.0);
|
||||
ui.vertical_centered(|ui| {
|
||||
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
|
||||
self.stratum_server_setup.ui(ui, cb);
|
||||
});
|
||||
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
|
||||
self.stratum_server_setup.ui(ui, cb);
|
||||
});
|
||||
});
|
||||
return;
|
||||
@@ -108,16 +75,19 @@ impl NetworkTab for NetworkMining {
|
||||
ui.columns(2, |columns| {
|
||||
columns[0].vertical_centered(|ui| {
|
||||
let (stratum_addr, stratum_port) = NodeConfig::get_stratum_address();
|
||||
View::rounded_box(ui,
|
||||
format!("{}:{}", stratum_addr, stratum_port),
|
||||
t!("network_mining.address"),
|
||||
[true, false, true, false]);
|
||||
View::label_box(ui,
|
||||
format!("{}:{}", stratum_addr, stratum_port),
|
||||
t!("network_mining.address"),
|
||||
[true, false, true, false]);
|
||||
});
|
||||
columns[1].vertical_centered(|ui| {
|
||||
View::rounded_box(ui,
|
||||
self.wallet_name.clone(),
|
||||
t!("network_mining.rewards_wallet"),
|
||||
[false, true, false, true]);
|
||||
View::label_box(ui,
|
||||
self.stratum_server_setup
|
||||
.wallet_name
|
||||
.clone()
|
||||
.unwrap_or("-".to_string()),
|
||||
t!("network_mining.rewards_wallet"),
|
||||
[false, true, false, true]);
|
||||
});
|
||||
});
|
||||
ui.add_space(4.0);
|
||||
@@ -131,10 +101,10 @@ impl NetworkTab for NetworkMining {
|
||||
} else {
|
||||
"-".into()
|
||||
};
|
||||
View::rounded_box(ui,
|
||||
difficulty,
|
||||
t!("network_node.difficulty"),
|
||||
[true, false, true, false]);
|
||||
View::label_box(ui,
|
||||
difficulty,
|
||||
t!("network_node.difficulty"),
|
||||
[true, false, true, false]);
|
||||
});
|
||||
columns[1].vertical_centered(|ui| {
|
||||
let block_height = if stratum_stats.block_height > 0 {
|
||||
@@ -142,10 +112,10 @@ impl NetworkTab for NetworkMining {
|
||||
} else {
|
||||
"-".into()
|
||||
};
|
||||
View::rounded_box(ui,
|
||||
block_height,
|
||||
t!("network_node.header"),
|
||||
[false, false, false, false]);
|
||||
View::label_box(ui,
|
||||
block_height,
|
||||
t!("network_node.header"),
|
||||
[false, false, false, false]);
|
||||
});
|
||||
columns[2].vertical_centered(|ui| {
|
||||
let hashrate = if stratum_stats.network_hashrate > 0.0 {
|
||||
@@ -153,10 +123,10 @@ impl NetworkTab for NetworkMining {
|
||||
} else {
|
||||
"-".into()
|
||||
};
|
||||
View::rounded_box(ui,
|
||||
hashrate,
|
||||
t!("network_mining.hashrate", "bits" => stratum_stats.edge_bits),
|
||||
[false, true, false, true]);
|
||||
View::label_box(ui,
|
||||
hashrate,
|
||||
t!("network_mining.hashrate", "bits" => stratum_stats.edge_bits),
|
||||
[false, true, false, true]);
|
||||
});
|
||||
});
|
||||
ui.add_space(4.0);
|
||||
@@ -165,17 +135,17 @@ impl NetworkTab for NetworkMining {
|
||||
View::sub_title(ui, format!("{} {}", CPU, t!("network_mining.miners")));
|
||||
ui.columns(2, |columns| {
|
||||
columns[0].vertical_centered(|ui| {
|
||||
View::rounded_box(ui,
|
||||
stratum_stats.num_workers.to_string(),
|
||||
t!("network_mining.devices"),
|
||||
[true, false, true, false]);
|
||||
View::label_box(ui,
|
||||
stratum_stats.num_workers.to_string(),
|
||||
t!("network_mining.devices"),
|
||||
[true, false, true, false]);
|
||||
});
|
||||
|
||||
columns[1].vertical_centered(|ui| {
|
||||
View::rounded_box(ui,
|
||||
stratum_stats.blocks_found.to_string(),
|
||||
t!("network_mining.blocks_found"),
|
||||
[false, true, false, true]);
|
||||
View::label_box(ui,
|
||||
stratum_stats.blocks_found.to_string(),
|
||||
t!("network_mining.blocks_found"),
|
||||
[false, true, false, true]);
|
||||
});
|
||||
});
|
||||
ui.add_space(4.0);
|
||||
@@ -187,7 +157,7 @@ impl NetworkTab for NetworkMining {
|
||||
View::horizontal_line(ui, Colors::item_stroke());
|
||||
ui.add_space(4.0);
|
||||
ScrollArea::vertical()
|
||||
.id_source("stratum_workers_scroll")
|
||||
.id_salt("stratum_workers_scroll")
|
||||
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
|
||||
.auto_shrink([false; 2])
|
||||
.show_rows(
|
||||
@@ -221,13 +191,17 @@ impl NetworkTab for NetworkMining {
|
||||
const WORKER_ITEM_HEIGHT: f32 = 76.0;
|
||||
|
||||
/// 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.vertical_centered_justified(|ui| {
|
||||
// Draw round background.
|
||||
let mut rect = ui.available_rect_before_wrap();
|
||||
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.horizontal(|ui| {
|
||||
|
||||
@@ -14,16 +14,17 @@
|
||||
|
||||
use egui::{Id, RichText};
|
||||
use url::Url;
|
||||
|
||||
use crate::gui::Colors;
|
||||
use crate::gui::platform::PlatformCallbacks;
|
||||
use crate::gui::views::{Modal, View};
|
||||
use crate::gui::views::types::TextEditOptions;
|
||||
use crate::gui::views::{Modal, TextEdit, View};
|
||||
use crate::wallet::{ConnectionsConfig, ExternalConnection};
|
||||
|
||||
/// Content to create or update external wallet connection.
|
||||
pub struct ExternalConnectionModal {
|
||||
/// Flag to check if [`Modal`] was just opened to focus on input field.
|
||||
first_modal_launch: bool,
|
||||
/// Flag to check if content was just rendered.
|
||||
first_draw: bool,
|
||||
|
||||
/// External connection URL value for [`Modal`].
|
||||
ext_node_url_edit: String,
|
||||
/// External connection API secret value for [`Modal`].
|
||||
@@ -34,8 +35,6 @@ pub struct ExternalConnectionModal {
|
||||
ext_conn_id: Option<i64>,
|
||||
}
|
||||
|
||||
|
||||
|
||||
impl ExternalConnectionModal {
|
||||
/// Network [`Modal`] identifier.
|
||||
pub const NETWORK_ID: &'static str = "net_ext_conn_modal";
|
||||
@@ -50,7 +49,7 @@ impl ExternalConnectionModal {
|
||||
("".to_string(), "".to_string(), None)
|
||||
};
|
||||
Self {
|
||||
first_modal_launch: true,
|
||||
first_draw: true,
|
||||
ext_node_url_edit,
|
||||
ext_node_secret_edit,
|
||||
ext_node_url_error: false,
|
||||
@@ -64,23 +63,54 @@ impl ExternalConnectionModal {
|
||||
cb: &dyn PlatformCallbacks,
|
||||
modal: &Modal,
|
||||
on_save: impl Fn(ExternalConnection)) {
|
||||
ui.add_space(6.0);
|
||||
// Add connection button callback.
|
||||
let on_add = |ui: &mut egui::Ui, m: &mut ExternalConnectionModal| {
|
||||
let url = if !m.ext_node_url_edit.starts_with("http") {
|
||||
format!("https://{}", m.ext_node_url_edit)
|
||||
} else {
|
||||
m.ext_node_url_edit.clone()
|
||||
};
|
||||
let error = Url::parse(url.trim()).is_err();
|
||||
m.ext_node_url_error = error;
|
||||
if !error {
|
||||
let secret = if m.ext_node_secret_edit.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(m.ext_node_secret_edit.clone())
|
||||
};
|
||||
|
||||
// Update or create new connection.
|
||||
let mut ext_conn = ExternalConnection::new(url, secret);
|
||||
if let Some(id) = m.ext_conn_id {
|
||||
ext_conn.id = id;
|
||||
}
|
||||
ConnectionsConfig::add_ext_conn(ext_conn.clone());
|
||||
ExternalConnection::check(Some(ext_conn.id), ui.ctx());
|
||||
on_save(ext_conn);
|
||||
|
||||
// Close modal.
|
||||
m.ext_node_url_edit = "".to_string();
|
||||
m.ext_node_secret_edit = "".to_string();
|
||||
m.ext_node_url_error = false;
|
||||
Modal::close();
|
||||
}
|
||||
};
|
||||
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.add_space(6.0);
|
||||
ui.label(RichText::new(t!("wallets.node_url"))
|
||||
.size(17.0)
|
||||
.color(Colors::gray()));
|
||||
ui.add_space(8.0);
|
||||
|
||||
// Draw node URL text edit.
|
||||
let url_edit_id = Id::from(modal.id).with(self.ext_conn_id);
|
||||
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);
|
||||
let url_edit_id = Id::from(modal.id).with(self.ext_conn_id).with("node_url");
|
||||
let mut url_edit = TextEdit::new(url_edit_id)
|
||||
.paste()
|
||||
.focus(self.first_draw);
|
||||
url_edit.ui(ui, &mut self.ext_node_url_edit, cb);
|
||||
|
||||
ui.add_space(8.0);
|
||||
ui.label(RichText::new(t!("wallets.node_secret"))
|
||||
.size(17.0)
|
||||
.color(Colors::gray()));
|
||||
@@ -88,8 +118,17 @@ impl ExternalConnectionModal {
|
||||
|
||||
// Draw node API secret text edit.
|
||||
let secret_edit_id = Id::from(modal.id).with(self.ext_conn_id).with("node_secret");
|
||||
let mut secret_edit_opts = TextEditOptions::new(secret_edit_id).paste().no_focus();
|
||||
View::text_edit(ui, cb, &mut self.ext_node_secret_edit, &mut secret_edit_opts);
|
||||
let mut secret_edit = TextEdit::new(secret_edit_id)
|
||||
.password()
|
||||
.paste()
|
||||
.focus(false);
|
||||
if url_edit.enter_pressed {
|
||||
secret_edit.focus_request();
|
||||
}
|
||||
secret_edit.ui(ui, &mut self.ext_node_secret_edit, cb);
|
||||
if secret_edit.enter_pressed {
|
||||
(on_add)(ui, self);
|
||||
}
|
||||
|
||||
// Show error when specified URL is not valid.
|
||||
if self.ext_node_url_error {
|
||||
@@ -113,54 +152,22 @@ impl ExternalConnectionModal {
|
||||
self.ext_node_url_edit = "".to_string();
|
||||
self.ext_node_secret_edit = "".to_string();
|
||||
self.ext_node_url_error = false;
|
||||
cb.hide_keyboard();
|
||||
modal.close();
|
||||
Modal::close();
|
||||
});
|
||||
});
|
||||
columns[1].vertical_centered_justified(|ui| {
|
||||
// Add connection button callback.
|
||||
let mut on_add = || {
|
||||
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.
|
||||
let mut ext_conn = ExternalConnection::new(url, secret);
|
||||
if let Some(id) = self.ext_conn_id {
|
||||
ext_conn.id = id;
|
||||
}
|
||||
ConnectionsConfig::add_ext_conn(ext_conn.clone());
|
||||
ExternalConnection::check_ext_conn_availability(Some(ext_conn.id));
|
||||
on_save(ext_conn);
|
||||
|
||||
// Close modal.
|
||||
self.ext_node_url_edit = "".to_string();
|
||||
self.ext_node_secret_edit = "".to_string();
|
||||
self.ext_node_url_error = false;
|
||||
cb.hide_keyboard();
|
||||
modal.close();
|
||||
}
|
||||
};
|
||||
View::on_enter_key(ui, || {
|
||||
(on_add)();
|
||||
});
|
||||
View::button(ui, if self.ext_conn_id.is_some() {
|
||||
View::button_ui(ui, if self.ext_conn_id.is_some() {
|
||||
t!("modal.save")
|
||||
} else {
|
||||
t!("modal.add")
|
||||
}, Colors::white_or_black(false), on_add);
|
||||
}, Colors::white_or_black(false), |ui| {
|
||||
(on_add)(ui, self);
|
||||
});
|
||||
});
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
});
|
||||
|
||||
self.first_draw = false;
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use egui::{RichText, Rounding, ScrollArea};
|
||||
use egui::{RichText, CornerRadius, ScrollArea, StrokeKind};
|
||||
use egui::scroll_area::ScrollBarVisibility;
|
||||
use grin_servers::PeerStats;
|
||||
|
||||
@@ -20,51 +20,28 @@ use crate::gui::Colors;
|
||||
use crate::gui::icons::{AT, CUBE, DEVICES, FLOW_ARROW, HANDSHAKE, PACKAGE, SHARE_NETWORK};
|
||||
use crate::gui::platform::PlatformCallbacks;
|
||||
use crate::gui::views::{Content, View};
|
||||
use crate::gui::views::network::NetworkContent;
|
||||
use crate::gui::views::network::types::{NetworkTab, NetworkTabType};
|
||||
use crate::gui::views::network::types::{NodeTab, NodeTabType};
|
||||
use crate::node::{Node, NodeConfig};
|
||||
|
||||
/// Integrated node tab content.
|
||||
#[derive(Default)]
|
||||
pub struct NetworkNode;
|
||||
|
||||
impl NetworkTab for NetworkNode {
|
||||
fn get_type(&self) -> NetworkTabType {
|
||||
NetworkTabType::Node
|
||||
impl NodeTab for NetworkNode {
|
||||
fn get_type(&self) -> NodeTabType {
|
||||
NodeTabType::Info
|
||||
}
|
||||
|
||||
fn ui(&mut self, ui: &mut egui::Ui, _: &dyn PlatformCallbacks) {
|
||||
// Show an error content when available.
|
||||
let node_err = Node::get_error();
|
||||
if node_err.is_some() {
|
||||
NetworkContent::node_error_ui(ui, node_err.unwrap());
|
||||
return;
|
||||
}
|
||||
|
||||
// Show message to enable node when it's not running.
|
||||
if !Node::is_running() {
|
||||
NetworkContent::disabled_node_ui(ui);
|
||||
return;
|
||||
}
|
||||
|
||||
// Show loading spinner when stats are not available.
|
||||
let server_stats = Node::get_stats();
|
||||
if server_stats.is_none() || Node::is_restarting() || Node::is_stopping() {
|
||||
NetworkContent::loading_ui(ui, None);
|
||||
return;
|
||||
}
|
||||
|
||||
fn tab_ui(&mut self, ui: &mut egui::Ui, _: &dyn PlatformCallbacks) {
|
||||
ScrollArea::vertical()
|
||||
.id_source("integrated_node")
|
||||
.id_salt("integrated_node_info_scroll")
|
||||
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
|
||||
.auto_shrink([false; 2])
|
||||
.show(ui, |ui| {
|
||||
ui.add_space(2.0);
|
||||
ui.vertical_centered(|ui| {
|
||||
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
|
||||
// Show node stats content.
|
||||
node_stats_ui(ui);
|
||||
});
|
||||
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
|
||||
// Show node stats content.
|
||||
node_stats_ui(ui);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -79,32 +56,32 @@ fn node_stats_ui(ui: &mut egui::Ui) {
|
||||
View::sub_title(ui, format!("{} {}", FLOW_ARROW, t!("network_node.header")));
|
||||
ui.columns(2, |columns| {
|
||||
columns[0].vertical_centered(|ui| {
|
||||
View::rounded_box(ui,
|
||||
stats.header_stats.last_block_h.to_string(),
|
||||
t!("network_node.hash"),
|
||||
[true, false, false, false]);
|
||||
View::label_box(ui,
|
||||
stats.header_stats.last_block_h.to_string(),
|
||||
t!("network_node.hash"),
|
||||
[true, false, false, false]);
|
||||
});
|
||||
columns[1].vertical_centered(|ui| {
|
||||
View::rounded_box(ui,
|
||||
stats.header_stats.height.to_string(),
|
||||
t!("network_node.height"),
|
||||
[false, true, false, false]);
|
||||
View::label_box(ui,
|
||||
stats.header_stats.height.to_string(),
|
||||
t!("network_node.height"),
|
||||
[false, true, false, false]);
|
||||
});
|
||||
});
|
||||
ui.columns(2, |columns| {
|
||||
columns[0].vertical_centered(|ui| {
|
||||
View::rounded_box(ui,
|
||||
stats.header_stats.total_difficulty.to_string(),
|
||||
t!("network_node.difficulty"),
|
||||
[false, false, true, false]);
|
||||
View::label_box(ui,
|
||||
stats.header_stats.total_difficulty.to_string(),
|
||||
t!("network_node.difficulty"),
|
||||
[false, false, true, false]);
|
||||
});
|
||||
columns[1].vertical_centered(|ui| {
|
||||
let h_ts = stats.header_stats.latest_timestamp.timestamp();
|
||||
let h_time = View::format_time(h_ts);
|
||||
View::rounded_box(ui,
|
||||
h_time,
|
||||
t!("network_node.time"),
|
||||
[false, false, false, true]);
|
||||
View::label_box(ui,
|
||||
h_time,
|
||||
t!("network_node.time"),
|
||||
[false, false, false, true]);
|
||||
});
|
||||
});
|
||||
ui.add_space(5.0);
|
||||
@@ -113,32 +90,32 @@ fn node_stats_ui(ui: &mut egui::Ui) {
|
||||
View::sub_title(ui, format!("{} {}", CUBE, t!("network_node.block")));
|
||||
ui.columns(2, |columns| {
|
||||
columns[0].vertical_centered(|ui| {
|
||||
View::rounded_box(ui,
|
||||
stats.chain_stats.last_block_h.to_string(),
|
||||
t!("network_node.hash"),
|
||||
[true, false, false, false]);
|
||||
View::label_box(ui,
|
||||
stats.chain_stats.last_block_h.to_string(),
|
||||
t!("network_node.hash"),
|
||||
[true, false, false, false]);
|
||||
});
|
||||
columns[1].vertical_centered(|ui| {
|
||||
View::rounded_box(ui,
|
||||
stats.chain_stats.height.to_string(),
|
||||
t!("network_node.height"),
|
||||
[false, true, false, false]);
|
||||
View::label_box(ui,
|
||||
stats.chain_stats.height.to_string(),
|
||||
t!("network_node.height"),
|
||||
[false, true, false, false]);
|
||||
});
|
||||
});
|
||||
ui.columns(2, |columns| {
|
||||
columns[0].vertical_centered(|ui| {
|
||||
View::rounded_box(ui,
|
||||
stats.chain_stats.total_difficulty.to_string(),
|
||||
t!("network_node.difficulty"),
|
||||
[false, false, true, false]);
|
||||
View::label_box(ui,
|
||||
stats.chain_stats.total_difficulty.to_string(),
|
||||
t!("network_node.difficulty"),
|
||||
[false, false, true, false]);
|
||||
});
|
||||
columns[1].vertical_centered(|ui| {
|
||||
let b_ts = stats.chain_stats.latest_timestamp.timestamp();
|
||||
let b_time = View::format_time(b_ts);
|
||||
View::rounded_box(ui,
|
||||
b_time,
|
||||
t!("network_node.time"),
|
||||
[false, false, false, true]);
|
||||
View::label_box(ui,
|
||||
b_time,
|
||||
t!("network_node.time"),
|
||||
[false, false, false, true]);
|
||||
});
|
||||
});
|
||||
ui.add_space(5.0);
|
||||
@@ -151,10 +128,10 @@ fn node_stats_ui(ui: &mut egui::Ui) {
|
||||
None => "0 (0)".to_string(),
|
||||
Some(tx) => format!("{} ({})", tx.tx_pool_size, tx.tx_pool_kernels)
|
||||
};
|
||||
View::rounded_box(ui,
|
||||
tx_stat,
|
||||
t!("network_node.main_pool"),
|
||||
[true, false, false, false]);
|
||||
View::label_box(ui,
|
||||
tx_stat,
|
||||
t!("network_node.main_pool"),
|
||||
[true, false, false, false]);
|
||||
});
|
||||
columns[1].vertical_centered(|ui| {
|
||||
let stem_tx_stat = match &stats.tx_stats {
|
||||
@@ -163,24 +140,24 @@ fn node_stats_ui(ui: &mut egui::Ui) {
|
||||
stx.stem_pool_size,
|
||||
stx.stem_pool_kernels)
|
||||
};
|
||||
View::rounded_box(ui,
|
||||
stem_tx_stat,
|
||||
t!("network_node.stem_pool"),
|
||||
[false, true, false, false]);
|
||||
View::label_box(ui,
|
||||
stem_tx_stat,
|
||||
t!("network_node.stem_pool"),
|
||||
[false, true, false, false]);
|
||||
});
|
||||
});
|
||||
ui.columns(2, |columns| {
|
||||
columns[0].vertical_centered(|ui| {
|
||||
View::rounded_box(ui,
|
||||
stats.disk_usage_gb.to_string(),
|
||||
t!("network_node.size"),
|
||||
[false, false, true, false]);
|
||||
View::label_box(ui,
|
||||
stats.disk_usage_gb.to_string(),
|
||||
t!("network_node.size"),
|
||||
[false, false, true, false]);
|
||||
});
|
||||
columns[1].vertical_centered(|ui| {
|
||||
let peers_txt = format!("{} ({})",
|
||||
stats.peer_count,
|
||||
NodeConfig::get_max_outbound_peers());
|
||||
View::rounded_box(ui, peers_txt, t!("network_node.peers"), [false, false, false, true]);
|
||||
View::label_box(ui, peers_txt, t!("network_node.peers"), [false, false, false, true]);
|
||||
});
|
||||
});
|
||||
ui.add_space(5.0);
|
||||
@@ -196,23 +173,27 @@ fn node_stats_ui(ui: &mut egui::Ui) {
|
||||
}
|
||||
}
|
||||
|
||||
const PEER_ITEM_HEIGHT: f32 = 77.0;
|
||||
|
||||
/// Draw connected peer info item.
|
||||
fn peer_item_ui(ui: &mut egui::Ui, peer: &PeerStats, rounding: Rounding) {
|
||||
fn peer_item_ui(ui: &mut egui::Ui, peer: &PeerStats, rounding: CornerRadius) {
|
||||
let mut rect = ui.available_rect_before_wrap();
|
||||
rect.set_height(79.0);
|
||||
ui.allocate_ui_at_rect(rect, |ui| {
|
||||
rect.set_height(PEER_ITEM_HEIGHT);
|
||||
ui.allocate_ui(rect.size(), |ui| {
|
||||
ui.vertical(|ui| {
|
||||
ui.add_space(4.0);
|
||||
|
||||
// Draw round background.
|
||||
ui.painter().rect(rect, rounding, Colors::white_or_black(false), View::item_stroke());
|
||||
ui.painter().rect(rect, rounding, Colors::fill_lite(), View::item_stroke(), StrokeKind::Outside);
|
||||
|
||||
// Draw peer address
|
||||
// Draw IP address.
|
||||
ui.horizontal(|ui| {
|
||||
ui.add_space(7.0);
|
||||
ui.label(RichText::new(&peer.addr).color(Colors::white_or_black(true)).size(17.0));
|
||||
ui.label(RichText::new(&peer.addr)
|
||||
.color(Colors::white_or_black(true))
|
||||
.size(17.0));
|
||||
});
|
||||
// Draw peer difficulty and height
|
||||
// Draw difficulty and height.
|
||||
ui.horizontal(|ui| {
|
||||
ui.add_space(6.0);
|
||||
let diff_text = format!("{} {} {} {}",
|
||||
@@ -220,13 +201,17 @@ fn peer_item_ui(ui: &mut egui::Ui, peer: &PeerStats, rounding: Rounding) {
|
||||
peer.total_difficulty,
|
||||
AT,
|
||||
peer.height);
|
||||
ui.label(RichText::new(diff_text).color(Colors::title(false)).size(16.0));
|
||||
ui.label(RichText::new(diff_text)
|
||||
.color(Colors::title(false))
|
||||
.size(15.0));
|
||||
});
|
||||
// Draw peer user-agent
|
||||
// Draw user-agent.
|
||||
ui.horizontal(|ui| {
|
||||
ui.add_space(6.0);
|
||||
let agent_text = format!("{} {}", DEVICES, &peer.user_agent);
|
||||
ui.label(RichText::new(agent_text).color(Colors::gray()).size(16.0));
|
||||
ui.label(RichText::new(agent_text)
|
||||
.color(Colors::gray())
|
||||
.size(15.0));
|
||||
});
|
||||
|
||||
ui.add_space(3.0);
|
||||
|
||||
@@ -20,8 +20,8 @@ use crate::gui::icons::ARROW_COUNTER_CLOCKWISE;
|
||||
use crate::gui::platform::PlatformCallbacks;
|
||||
use crate::gui::views::{Modal, Content, View};
|
||||
use crate::gui::views::network::setup::{DandelionSetup, NodeSetup, P2PSetup, PoolSetup, StratumSetup};
|
||||
use crate::gui::views::network::types::{NetworkTab, NetworkTabType};
|
||||
use crate::gui::views::types::{ModalContainer, ModalPosition};
|
||||
use crate::gui::views::network::types::{NodeTab, NodeTabType};
|
||||
use crate::gui::views::types::{ContentContainer, ModalPosition};
|
||||
use crate::node::{Node, NodeConfig};
|
||||
|
||||
/// Integrated node settings tab content.
|
||||
@@ -36,13 +36,10 @@ pub struct NetworkSettings {
|
||||
pool: PoolSetup,
|
||||
/// Dandelion server setup content.
|
||||
dandelion: DandelionSetup,
|
||||
|
||||
/// [`Modal`] identifiers allowed at this ui container.
|
||||
modal_ids: Vec<&'static str>
|
||||
}
|
||||
|
||||
/// Identifier for settings reset confirmation [`Modal`].
|
||||
pub const RESET_SETTINGS_MODAL: &'static str = "reset_settings";
|
||||
pub const RESET_SETTINGS_CONFIRMATION_MODAL: &'static str = "reset_settings_confirmation";
|
||||
|
||||
impl Default for NetworkSettings {
|
||||
fn default() -> Self {
|
||||
@@ -52,16 +49,15 @@ impl Default for NetworkSettings {
|
||||
stratum: StratumSetup::default(),
|
||||
pool: PoolSetup::default(),
|
||||
dandelion: DandelionSetup::default(),
|
||||
modal_ids: vec![
|
||||
RESET_SETTINGS_MODAL
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ModalContainer for NetworkSettings {
|
||||
fn modal_ids(&self) -> &Vec<&'static str> {
|
||||
&self.modal_ids
|
||||
impl ContentContainer for NetworkSettings {
|
||||
fn modal_ids(&self) -> Vec<&'static str> {
|
||||
vec![
|
||||
RESET_SETTINGS_CONFIRMATION_MODAL
|
||||
]
|
||||
}
|
||||
|
||||
fn modal_ui(&mut self,
|
||||
@@ -69,23 +65,14 @@ impl ModalContainer for NetworkSettings {
|
||||
modal: &Modal,
|
||||
_: &dyn PlatformCallbacks) {
|
||||
match modal.id {
|
||||
RESET_SETTINGS_MODAL => reset_settings_confirmation_modal(ui, modal),
|
||||
RESET_SETTINGS_CONFIRMATION_MODAL => reset_settings_confirmation_modal(ui),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl NetworkTab for NetworkSettings {
|
||||
fn get_type(&self) -> NetworkTabType {
|
||||
NetworkTabType::Settings
|
||||
}
|
||||
|
||||
fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||
// Draw modal content for current ui container.
|
||||
self.current_modal_ui(ui, cb);
|
||||
|
||||
fn container_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||
ScrollArea::vertical()
|
||||
.id_source("network_settings")
|
||||
.id_salt("node_settings_scroll")
|
||||
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
|
||||
.auto_shrink([false; 2])
|
||||
.show(ui, |ui| {
|
||||
@@ -135,6 +122,16 @@ impl NetworkTab for NetworkSettings {
|
||||
}
|
||||
}
|
||||
|
||||
impl NodeTab for NetworkSettings {
|
||||
fn get_type(&self) -> NodeTabType {
|
||||
NodeTabType::Settings
|
||||
}
|
||||
|
||||
fn tab_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||
self.ui(ui, cb);
|
||||
}
|
||||
}
|
||||
|
||||
impl NetworkSettings {
|
||||
/// Reminder to restart enabled node to show on edit setting at [`Modal`].
|
||||
pub fn node_restart_required_ui(ui: &mut egui::Ui) {
|
||||
@@ -210,9 +207,9 @@ fn reset_settings_ui(ui: &mut egui::Ui) {
|
||||
t!("network_settings.reset_settings"));
|
||||
View::action_button(ui, button_text, || {
|
||||
// Show modal to confirm settings reset.
|
||||
Modal::new(RESET_SETTINGS_MODAL)
|
||||
Modal::new(RESET_SETTINGS_CONFIRMATION_MODAL)
|
||||
.position(ModalPosition::Center)
|
||||
.title(t!("modal.confirmation"))
|
||||
.title(t!("confirmation"))
|
||||
.show();
|
||||
});
|
||||
|
||||
@@ -230,7 +227,7 @@ fn reset_settings_ui(ui: &mut egui::Ui) {
|
||||
}
|
||||
|
||||
/// 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.vertical_centered(|ui| {
|
||||
let reset_text = format!("{}?", t!("network_settings.reset_settings_desc"));
|
||||
@@ -249,12 +246,12 @@ fn reset_settings_confirmation_modal(ui: &mut egui::Ui, modal: &Modal) {
|
||||
columns[0].vertical_centered_justified(|ui| {
|
||||
View::button(ui, t!("network_settings.reset"), Colors::white_or_black(false), || {
|
||||
NodeConfig::reset_to_default();
|
||||
modal.close();
|
||||
Modal::close();
|
||||
});
|
||||
});
|
||||
columns[1].vertical_centered_justified(|ui| {
|
||||
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
|
||||
modal.close();
|
||||
Modal::close();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,12 +14,12 @@
|
||||
|
||||
use egui::{Id, RichText};
|
||||
|
||||
use crate::gui::Colors;
|
||||
use crate::gui::icons::{CLOCK_COUNTDOWN, GRAPH, TIMER, WATCH};
|
||||
use crate::gui::platform::PlatformCallbacks;
|
||||
use crate::gui::views::{Modal, View};
|
||||
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::{Modal, TextEdit, View};
|
||||
use crate::gui::Colors;
|
||||
use crate::gui::views::network::NetworkSettings;
|
||||
use crate::node::NodeConfig;
|
||||
|
||||
/// Dandelion server setup section content.
|
||||
@@ -35,9 +35,6 @@ pub struct DandelionSetup {
|
||||
|
||||
/// Stem phase probability value (stem 90% of the time, fluff 10% of the time by default).
|
||||
stem_prob_edit: String,
|
||||
|
||||
/// [`Modal`] identifiers allowed at this ui container.
|
||||
modal_ids: Vec<&'static str>,
|
||||
}
|
||||
|
||||
/// Identifier epoch duration value [`Modal`].
|
||||
@@ -56,19 +53,18 @@ impl Default for DandelionSetup {
|
||||
embargo_edit: NodeConfig::get_reorg_cache_period(),
|
||||
aggregation_edit: NodeConfig::get_dandelion_aggregation(),
|
||||
stem_prob_edit: NodeConfig::get_stem_probability(),
|
||||
modal_ids: vec![
|
||||
EPOCH_MODAL,
|
||||
EMBARGO_MODAL,
|
||||
AGGREGATION_MODAL,
|
||||
STEM_PROBABILITY_MODAL
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ModalContainer for DandelionSetup {
|
||||
fn modal_ids(&self) -> &Vec<&'static str> {
|
||||
&self.modal_ids
|
||||
impl ContentContainer for DandelionSetup {
|
||||
fn modal_ids(&self) -> Vec<&'static str> {
|
||||
vec![
|
||||
EPOCH_MODAL,
|
||||
EMBARGO_MODAL,
|
||||
AGGREGATION_MODAL,
|
||||
STEM_PROBABILITY_MODAL
|
||||
]
|
||||
}
|
||||
|
||||
fn modal_ui(&mut self,
|
||||
@@ -83,41 +79,36 @@ impl ModalContainer for DandelionSetup {
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DandelionSetup {
|
||||
pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||
// Draw modal content for current ui container.
|
||||
self.current_modal_ui(ui, cb);
|
||||
|
||||
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, cb);
|
||||
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, cb);
|
||||
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, cb);
|
||||
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, cb);
|
||||
self.stem_prob_ui(ui);
|
||||
|
||||
ui.add_space(6.0);
|
||||
View::horizontal_line(ui, Colors::item_stroke());
|
||||
@@ -131,9 +122,11 @@ impl DandelionSetup {
|
||||
ui.add_space(6.0);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl DandelionSetup {
|
||||
/// Draw epoch duration setup content.
|
||||
fn epoch_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||
fn epoch_ui(&mut self, ui: &mut egui::Ui) {
|
||||
ui.label(RichText::new(t!("network_settings.epoch_duration"))
|
||||
.size(16.0)
|
||||
.color(Colors::gray())
|
||||
@@ -141,7 +134,7 @@ impl DandelionSetup {
|
||||
ui.add_space(6.0);
|
||||
|
||||
let epoch = NodeConfig::get_dandelion_epoch();
|
||||
View::button(ui, format!("{} {}", WATCH, epoch.clone()), Colors::button(), || {
|
||||
View::button(ui, format!("{} {}", WATCH, &epoch), Colors::white_or_black(false), || {
|
||||
// Setup values for modal.
|
||||
self.epoch_edit = epoch;
|
||||
// Show epoch setup modal.
|
||||
@@ -149,13 +142,20 @@ impl DandelionSetup {
|
||||
.position(ModalPosition::CenterTop)
|
||||
.title(t!("network_settings.change_value"))
|
||||
.show();
|
||||
cb.show_keyboard();
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
}
|
||||
|
||||
/// Draw epoch duration [`Modal`] content.
|
||||
fn epoch_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
|
||||
// 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.vertical_centered(|ui| {
|
||||
ui.label(RichText::new(t!("network_settings.epoch_duration"))
|
||||
@@ -164,8 +164,11 @@ impl DandelionSetup {
|
||||
ui.add_space(8.0);
|
||||
|
||||
// Draw epoch text edit.
|
||||
let mut epoch_edit_opts = TextEditOptions::new(Id::from(modal.id)).h_center();
|
||||
View::text_edit(ui, cb, &mut self.epoch_edit, &mut epoch_edit_opts);
|
||||
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);
|
||||
}
|
||||
|
||||
// Show error when specified value is not valid or reminder to restart enabled node.
|
||||
if self.epoch_edit.parse::<u16>().is_err() {
|
||||
@@ -184,25 +187,17 @@ impl DandelionSetup {
|
||||
// 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(epoch) = self.epoch_edit.parse::<u16>() {
|
||||
NodeConfig::save_dandelion_epoch(epoch);
|
||||
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();
|
||||
Modal::close();
|
||||
});
|
||||
});
|
||||
columns[1].vertical_centered_justified(|ui| {
|
||||
View::button(ui, t!("modal.save"), Colors::white_or_black(false), on_save);
|
||||
View::button(ui, t!("modal.save"), Colors::white_or_black(false), || {
|
||||
on_save(self);
|
||||
});
|
||||
});
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
@@ -210,7 +205,7 @@ impl DandelionSetup {
|
||||
}
|
||||
|
||||
/// Draw embargo expiration time setup content.
|
||||
fn embargo_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||
fn embargo_ui(&mut self, ui: &mut egui::Ui) {
|
||||
ui.label(RichText::new(t!("network_settings.embargo_timer"))
|
||||
.size(16.0)
|
||||
.color(Colors::gray())
|
||||
@@ -218,21 +213,27 @@ impl DandelionSetup {
|
||||
ui.add_space(6.0);
|
||||
|
||||
let embargo = NodeConfig::get_dandelion_embargo();
|
||||
View::button(ui, format!("{} {}", TIMER, embargo.clone()), Colors::button(), || {
|
||||
// Setup values for modal.
|
||||
View::button(ui, format!("{} {}", TIMER, &embargo), Colors::white_or_black(false), || {
|
||||
self.embargo_edit = embargo;
|
||||
// Show embargo setup modal.
|
||||
Modal::new(EMBARGO_MODAL)
|
||||
.position(ModalPosition::CenterTop)
|
||||
.title(t!("network_settings.change_value"))
|
||||
.show();
|
||||
cb.show_keyboard();
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
}
|
||||
|
||||
/// Draw epoch duration [`Modal`] content.
|
||||
fn embargo_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
|
||||
// Save button callback.
|
||||
let on_save = |c: &mut DandelionSetup| {
|
||||
if let Ok(embargo) = c.embargo_edit.parse::<u16>() {
|
||||
NodeConfig::save_dandelion_embargo(embargo);
|
||||
Modal::close();
|
||||
}
|
||||
};
|
||||
|
||||
ui.add_space(6.0);
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.label(RichText::new(t!("network_settings.embargo_timer"))
|
||||
@@ -241,8 +242,11 @@ impl DandelionSetup {
|
||||
ui.add_space(8.0);
|
||||
|
||||
// Draw embargo text edit.
|
||||
let mut embargo_edit_opts = TextEditOptions::new(Id::from(modal.id)).h_center();
|
||||
View::text_edit(ui, cb, &mut self.embargo_edit, &mut embargo_edit_opts);
|
||||
let mut embargo_edit = TextEdit::new(Id::from(modal.id)).h_center().numeric();
|
||||
embargo_edit.ui(ui, &mut self.embargo_edit, cb);
|
||||
if embargo_edit.enter_pressed {
|
||||
on_save(self);
|
||||
}
|
||||
|
||||
// Show error when specified value is not valid or reminder to restart enabled node.
|
||||
if self.embargo_edit.parse::<u16>().is_err() {
|
||||
@@ -261,25 +265,17 @@ impl DandelionSetup {
|
||||
// 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(embargo) = self.embargo_edit.parse::<u16>() {
|
||||
NodeConfig::save_dandelion_embargo(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();
|
||||
Modal::close();
|
||||
});
|
||||
});
|
||||
columns[1].vertical_centered_justified(|ui| {
|
||||
View::button(ui, t!("modal.save"), Colors::white_or_black(false), on_save);
|
||||
View::button(ui, t!("modal.save"), Colors::white_or_black(false), || {
|
||||
on_save(self);
|
||||
});
|
||||
});
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
@@ -287,29 +283,36 @@ impl DandelionSetup {
|
||||
}
|
||||
|
||||
/// Draw aggregation period setup content.
|
||||
fn aggregation_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||
fn aggregation_ui(&mut self, ui: &mut egui::Ui) {
|
||||
ui.label(RichText::new(t!("network_settings.aggregation_period"))
|
||||
.size(16.0)
|
||||
.color(Colors::gray())
|
||||
);
|
||||
ui.add_space(6.0);
|
||||
|
||||
let agg = NodeConfig::get_dandelion_aggregation();
|
||||
View::button(ui, format!("{} {}", CLOCK_COUNTDOWN, agg.clone()), Colors::button(), || {
|
||||
let ag = NodeConfig::get_dandelion_aggregation();
|
||||
View::button(ui, format!("{} {}", CLOCK_COUNTDOWN, &ag), Colors::white_or_black(false), || {
|
||||
// Setup values for modal.
|
||||
self.aggregation_edit = agg;
|
||||
self.aggregation_edit = ag;
|
||||
// Show aggregation setup modal.
|
||||
Modal::new(AGGREGATION_MODAL)
|
||||
.position(ModalPosition::CenterTop)
|
||||
.title(t!("network_settings.change_value"))
|
||||
.show();
|
||||
cb.show_keyboard();
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
}
|
||||
|
||||
/// Draw aggregation period [`Modal`] content.
|
||||
fn aggregation_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
|
||||
// Save button callback.
|
||||
let on_save = |c: &mut DandelionSetup| {
|
||||
if let Ok(embargo) = c.aggregation_edit.parse::<u16>() {
|
||||
NodeConfig::save_dandelion_aggregation(embargo);
|
||||
Modal::close();
|
||||
}
|
||||
};
|
||||
|
||||
ui.add_space(6.0);
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.label(RichText::new(t!("network_settings.aggregation_period"))
|
||||
@@ -318,8 +321,11 @@ impl DandelionSetup {
|
||||
ui.add_space(8.0);
|
||||
|
||||
// Draw aggregation period text edit.
|
||||
let mut aggregation_edit_opts = TextEditOptions::new(Id::from(modal.id)).h_center();
|
||||
View::text_edit(ui, cb, &mut self.aggregation_edit, &mut aggregation_edit_opts);
|
||||
let mut aggregation_edit = TextEdit::new(Id::from(modal.id)).h_center().numeric();
|
||||
aggregation_edit.ui(ui, &mut self.aggregation_edit, cb);
|
||||
if aggregation_edit.enter_pressed {
|
||||
on_save(self);
|
||||
}
|
||||
|
||||
// Show error when specified value is not valid or reminder to restart enabled node.
|
||||
if self.aggregation_edit.parse::<u16>().is_err() {
|
||||
@@ -338,25 +344,17 @@ impl DandelionSetup {
|
||||
// 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(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();
|
||||
Modal::close();
|
||||
});
|
||||
});
|
||||
columns[1].vertical_centered_justified(|ui| {
|
||||
View::button(ui, t!("modal.save"), Colors::white_or_black(false), on_save);
|
||||
View::button(ui, t!("modal.save"), Colors::white_or_black(false), || {
|
||||
on_save(self);
|
||||
});
|
||||
});
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
@@ -364,7 +362,7 @@ impl DandelionSetup {
|
||||
}
|
||||
|
||||
/// Draw stem phase probability setup content.
|
||||
fn stem_prob_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||
fn stem_prob_ui(&mut self, ui: &mut egui::Ui) {
|
||||
ui.label(RichText::new(t!("network_settings.stem_probability"))
|
||||
.size(16.0)
|
||||
.color(Colors::gray())
|
||||
@@ -372,7 +370,7 @@ impl DandelionSetup {
|
||||
ui.add_space(6.0);
|
||||
|
||||
let stem_prob = NodeConfig::get_stem_probability();
|
||||
View::button(ui, format!("{}%", stem_prob.clone()), Colors::button(), || {
|
||||
View::button(ui, format!("{}%", &stem_prob), Colors::white_or_black(false), || {
|
||||
// Setup values for modal.
|
||||
self.stem_prob_edit = stem_prob;
|
||||
// Show stem probability setup modal.
|
||||
@@ -380,13 +378,20 @@ impl DandelionSetup {
|
||||
.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) {
|
||||
// Save button callback.
|
||||
let on_save = |c: &mut DandelionSetup| {
|
||||
if let Ok(prob) = c.stem_prob_edit.parse::<u8>() {
|
||||
NodeConfig::save_stem_probability(prob);
|
||||
Modal::close();
|
||||
}
|
||||
};
|
||||
|
||||
ui.add_space(6.0);
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.label(RichText::new(t!("network_settings.stem_probability"))
|
||||
@@ -395,8 +400,11 @@ impl DandelionSetup {
|
||||
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);
|
||||
let mut stem_prob_edit = TextEdit::new(Id::from(modal.id)).h_center().numeric();
|
||||
stem_prob_edit.ui(ui, &mut self.stem_prob_edit, cb);
|
||||
if stem_prob_edit.enter_pressed {
|
||||
on_save(self);
|
||||
}
|
||||
|
||||
// Show error when specified value is not valid or reminder to restart enabled node.
|
||||
if self.stem_prob_edit.parse::<u8>().is_err() {
|
||||
@@ -415,25 +423,17 @@ impl DandelionSetup {
|
||||
// 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();
|
||||
Modal::close();
|
||||
});
|
||||
});
|
||||
columns[1].vertical_centered_justified(|ui| {
|
||||
View::button(ui, t!("modal.save"), Colors::white_or_black(false), on_save);
|
||||
View::button(ui, t!("modal.save"), Colors::white_or_black(false), || {
|
||||
on_save(self);
|
||||
});
|
||||
});
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
|
||||
+216
-106
@@ -12,21 +12,28 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use egui::{Id, RichText};
|
||||
use eframe::emath::Align;
|
||||
use eframe::epaint::StrokeKind;
|
||||
use egui::{Id, Layout, RichText};
|
||||
use grin_core::global::ChainTypes;
|
||||
|
||||
use crate::AppConfig;
|
||||
use crate::gui::Colors;
|
||||
use crate::gui::icons::{CLOCK_CLOCKWISE, COMPUTER_TOWER, PLUG, POWER, SHIELD, SHIELD_SLASH};
|
||||
use crate::gui::icons::{CLOCK_CLOCKWISE, COMPUTER_TOWER, FOLDERS, PENCIL, PLUG, POWER, SHIELD, SHIELD_SLASH};
|
||||
use crate::gui::platform::PlatformCallbacks;
|
||||
use crate::gui::views::{Modal, View};
|
||||
use crate::gui::views::network::NetworkContent;
|
||||
use crate::gui::views::network::settings::NetworkSettings;
|
||||
use crate::gui::views::types::{ModalContainer, ModalPosition, TextEditOptions};
|
||||
use crate::gui::views::network::NetworkContent;
|
||||
use crate::gui::views::types::{ContentContainer, ModalPosition};
|
||||
use crate::gui::views::{FilePickContent, FilePickContentType, Modal, TextEdit, View};
|
||||
use crate::gui::Colors;
|
||||
use crate::node::{Node, NodeConfig};
|
||||
use crate::AppConfig;
|
||||
|
||||
/// Integrated node general setup section content.
|
||||
pub struct NodeSetup {
|
||||
/// Data path value value for [`Modal`].
|
||||
data_path_edit: String,
|
||||
/// Button to pick directory for chain data.
|
||||
pick_data_dir: FilePickContent,
|
||||
|
||||
/// IP Addresses available at system.
|
||||
available_ips: Vec<String>,
|
||||
|
||||
@@ -43,44 +50,47 @@ pub struct NodeSetup {
|
||||
|
||||
/// Future Time Limit value.
|
||||
ftl_edit: String,
|
||||
|
||||
/// [`Modal`] identifiers allowed at this ui container.
|
||||
modal_ids: Vec<&'static str>
|
||||
}
|
||||
|
||||
/// Identifier for chain data path value [`Modal`].
|
||||
const DATA_PATH_MODAL: &'static str = "node_data_path";
|
||||
/// Identifier for API port value [`Modal`].
|
||||
pub const API_PORT_MODAL: &'static str = "api_port";
|
||||
const API_PORT_MODAL: &'static str = "node_api_port";
|
||||
/// Identifier for API secret value [`Modal`].
|
||||
pub const API_SECRET_MODAL: &'static str = "api_secret";
|
||||
const API_SECRET_MODAL: &'static str = "node_api_secret";
|
||||
/// Identifier for Foreign API secret value [`Modal`].
|
||||
pub const FOREIGN_API_SECRET_MODAL: &'static str = "foreign_api_secret";
|
||||
const FOREIGN_API_SECRET_MODAL: &'static str = "node_foreign_api_secret";
|
||||
/// Identifier for FTL value [`Modal`].
|
||||
pub const FTL_MODAL: &'static str = "ftl";
|
||||
const FTL_MODAL: &'static str = "node_ftl";
|
||||
|
||||
impl Default for NodeSetup {
|
||||
fn default() -> Self {
|
||||
let (api_ip, api_port) = NodeConfig::get_api_ip_port();
|
||||
let is_api_port_available = NodeConfig::is_api_port_available(&api_ip, &api_port);
|
||||
Self {
|
||||
data_path_edit: NodeConfig::get_chain_data_path(),
|
||||
pick_data_dir: FilePickContent::new(
|
||||
FilePickContentType::ItemButton(View::item_rounding(0, 1, true))
|
||||
).no_parse().pick_folder(),
|
||||
available_ips: NodeConfig::get_ip_addrs(),
|
||||
api_port_edit: api_port,
|
||||
api_port_available_edit: is_api_port_available,
|
||||
is_api_port_available,
|
||||
secret_edit: "".to_string(),
|
||||
ftl_edit: NodeConfig::get_ftl(),
|
||||
modal_ids: vec![
|
||||
API_PORT_MODAL,
|
||||
API_SECRET_MODAL,
|
||||
FOREIGN_API_SECRET_MODAL,
|
||||
FTL_MODAL
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ModalContainer for NodeSetup {
|
||||
fn modal_ids(&self) -> &Vec<&'static str> {
|
||||
&self.modal_ids
|
||||
impl ContentContainer for NodeSetup {
|
||||
fn modal_ids(&self) -> Vec<&'static str> {
|
||||
vec![
|
||||
DATA_PATH_MODAL,
|
||||
API_PORT_MODAL,
|
||||
API_SECRET_MODAL,
|
||||
FOREIGN_API_SECRET_MODAL,
|
||||
FTL_MODAL
|
||||
]
|
||||
}
|
||||
|
||||
fn modal_ui(&mut self,
|
||||
@@ -88,6 +98,7 @@ impl ModalContainer for NodeSetup {
|
||||
modal: &Modal,
|
||||
cb: &dyn PlatformCallbacks) {
|
||||
match modal.id {
|
||||
DATA_PATH_MODAL => self.data_path_edit_modal_ui(ui, cb),
|
||||
API_PORT_MODAL => self.api_port_modal(ui, modal, cb),
|
||||
API_SECRET_MODAL => self.secret_modal(ui, modal, cb),
|
||||
FOREIGN_API_SECRET_MODAL => self.secret_modal(ui, modal, cb),
|
||||
@@ -95,13 +106,8 @@ impl ModalContainer for NodeSetup {
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl NodeSetup {
|
||||
pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||
// Draw modal content for current ui container.
|
||||
self.current_modal_ui(ui, cb);
|
||||
|
||||
fn container_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||
View::sub_title(ui, format!("{} {}", COMPUTER_TOWER, t!("network_settings.server")));
|
||||
View::horizontal_line(ui, Colors::stroke());
|
||||
ui.add_space(6.0);
|
||||
@@ -111,7 +117,8 @@ impl NodeSetup {
|
||||
ui.add_space(2.0);
|
||||
|
||||
// Show loading indicator or controls to stop/start/restart node.
|
||||
if Node::is_stopping() || Node::is_restarting() || Node::is_starting() {
|
||||
if Node::is_stopping() || Node::is_restarting() || Node::is_starting()
|
||||
|| Node::data_dir_changing() {
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.add_space(8.0);
|
||||
View::small_loading_spinner(ui);
|
||||
@@ -163,6 +170,13 @@ impl NodeSetup {
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
|
||||
// Show data location selection for Desktop when it already started or turned off.
|
||||
if !Node::is_restarting() && !Node::is_stopping() && !Node::is_starting() &&
|
||||
View::is_desktop() {
|
||||
self.pick_data_dir_ui(ui, cb);
|
||||
ui.add_space(6.0);
|
||||
}
|
||||
|
||||
if self.available_ips.is_empty() {
|
||||
// Show message when IP addresses are not available on the system.
|
||||
NetworkSettings::no_ip_address_ui(ui);
|
||||
@@ -185,12 +199,12 @@ impl NodeSetup {
|
||||
NodeConfig::save_api_address(selected_ip, &api_port);
|
||||
});
|
||||
// Show API port setup.
|
||||
self.api_port_setup_ui(ui, cb);
|
||||
self.api_port_setup_ui(ui);
|
||||
// Show API secret setup.
|
||||
self.secret_ui(API_SECRET_MODAL, ui, cb);
|
||||
self.secret_ui(API_SECRET_MODAL, ui);
|
||||
ui.add_space(12.0);
|
||||
// Show Foreign API secret setup.
|
||||
self.secret_ui(FOREIGN_API_SECRET_MODAL, ui, cb);
|
||||
self.secret_ui(FOREIGN_API_SECRET_MODAL, ui);
|
||||
ui.add_space(6.0);
|
||||
});
|
||||
}
|
||||
@@ -201,7 +215,7 @@ impl NodeSetup {
|
||||
|
||||
ui.vertical_centered(|ui| {
|
||||
// Show FTL setup.
|
||||
self.ftl_ui(ui, cb);
|
||||
self.ftl_ui(ui);
|
||||
|
||||
ui.add_space(6.0);
|
||||
View::horizontal_line(ui, Colors::item_stroke());
|
||||
@@ -218,6 +232,94 @@ impl NodeSetup {
|
||||
self.archive_mode_ui(ui);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl NodeSetup {
|
||||
/// Draw content to change chain data directory.
|
||||
fn pick_data_dir_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||
// Setup layout size.
|
||||
let mut rect = ui.available_rect_before_wrap();
|
||||
rect.set_height(56.0);
|
||||
|
||||
// Draw round background.
|
||||
let bg_rect = rect.clone();
|
||||
let item_rounding = View::item_rounding(0, 1, false);
|
||||
ui.painter().rect(bg_rect,
|
||||
item_rounding,
|
||||
Colors::fill(),
|
||||
View::item_stroke(),
|
||||
StrokeKind::Outside);
|
||||
|
||||
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
|
||||
self.pick_data_dir.ui(ui, cb, |path| {
|
||||
Node::change_data_dir(path);
|
||||
});
|
||||
View::item_button(ui, View::item_rounding(1, 3, true), PENCIL, None, || {
|
||||
self.data_path_edit = NodeConfig::get_chain_data_path();
|
||||
// Show chain data path edit modal.
|
||||
Modal::new(DATA_PATH_MODAL)
|
||||
.position(ModalPosition::CenterTop)
|
||||
.title(t!("network.node"))
|
||||
.show();
|
||||
});
|
||||
let layout_size = ui.available_size();
|
||||
ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| {
|
||||
ui.add_space(12.0);
|
||||
ui.vertical(|ui| {
|
||||
ui.add_space(4.0);
|
||||
let path = NodeConfig::get_chain_data_path();
|
||||
View::ellipsize_text(ui, path, 18.0, Colors::title(false));
|
||||
ui.add_space(1.0);
|
||||
let desc = format!("{} {}", FOLDERS, t!("files_location"));
|
||||
ui.label(RichText::new(desc).size(15.0).color(Colors::gray()));
|
||||
ui.add_space(8.0);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// Draw data path input [`Modal`] content.
|
||||
fn data_path_edit_modal_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||
ui.add_space(6.0);
|
||||
ui.vertical_centered(|ui| {
|
||||
let on_save = |path: &String| {
|
||||
Node::change_data_dir(path.clone());
|
||||
Modal::close();
|
||||
};
|
||||
ui.label(RichText::new(format!("{}:", t!("files_location")))
|
||||
.size(17.0)
|
||||
.color(Colors::gray()));
|
||||
ui.add_space(8.0);
|
||||
|
||||
// Draw chain data path text edit.
|
||||
let mut edit = TextEdit::new(Id::from(DATA_PATH_MODAL)).paste();
|
||||
edit.ui(ui, &mut self.data_path_edit, cb);
|
||||
if edit.enter_pressed {
|
||||
on_save(&self.data_path_edit);
|
||||
}
|
||||
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.data_path_edit);
|
||||
});
|
||||
});
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// Draw [`ChainTypes`] setup content.
|
||||
pub fn chain_type_ui(ui: &mut egui::Ui) {
|
||||
@@ -250,12 +352,12 @@ impl NodeSetup {
|
||||
}
|
||||
|
||||
/// Draw API port setup content.
|
||||
fn api_port_setup_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||
fn api_port_setup_ui(&mut self, ui: &mut egui::Ui) {
|
||||
ui.label(RichText::new(t!("network_settings.api_port")).size(16.0).color(Colors::gray()));
|
||||
ui.add_space(6.0);
|
||||
|
||||
let (_, port) = NodeConfig::get_api_ip_port();
|
||||
View::button(ui, format!("{} {}", PLUG, port.clone()), Colors::button(), || {
|
||||
View::button(ui, format!("{} {}", PLUG, &port), Colors::white_or_black(false), || {
|
||||
// Setup values for modal.
|
||||
self.api_port_edit = port;
|
||||
self.api_port_available_edit = self.is_api_port_available;
|
||||
@@ -265,7 +367,6 @@ impl NodeSetup {
|
||||
.position(ModalPosition::CenterTop)
|
||||
.title(t!("network_settings.change_value"))
|
||||
.show();
|
||||
cb.show_keyboard();
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
|
||||
@@ -281,14 +382,35 @@ impl NodeSetup {
|
||||
|
||||
/// Draw API port [`Modal`] content.
|
||||
fn api_port_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
|
||||
let on_save = |c: &mut NodeSetup| {
|
||||
// Check if port is available.
|
||||
let (api_ip, _) = NodeConfig::get_api_ip_port();
|
||||
let available = NodeConfig::is_api_port_available(&api_ip, &c.api_port_edit);
|
||||
c.api_port_available_edit = available;
|
||||
if available {
|
||||
// Save port at config if it's available.
|
||||
NodeConfig::save_api_address(&api_ip, &c.api_port_edit);
|
||||
if Node::is_running() {
|
||||
Node::restart();
|
||||
}
|
||||
c.is_api_port_available = true;
|
||||
Modal::close();
|
||||
}
|
||||
};
|
||||
|
||||
ui.add_space(6.0);
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.label(RichText::new(t!("network_settings.api_port")).size(17.0).color(Colors::gray()));
|
||||
ui.label(RichText::new(t!("network_settings.api_port"))
|
||||
.size(17.0)
|
||||
.color(Colors::gray()));
|
||||
ui.add_space(6.0);
|
||||
|
||||
// Draw API port text edit.
|
||||
let mut api_port_edit_opts = TextEditOptions::new(Id::from(modal.id)).h_center();
|
||||
View::text_edit(ui, cb, &mut self.api_port_edit, &mut api_port_edit_opts);
|
||||
let mut api_port_edit = TextEdit::new(Id::from(modal.id)).h_center().numeric();
|
||||
api_port_edit.ui(ui, &mut self.api_port_edit, cb);
|
||||
if api_port_edit.enter_pressed {
|
||||
on_save(self);
|
||||
}
|
||||
|
||||
// Show error when specified port is unavailable or reminder to restart enabled node.
|
||||
if !self.api_port_available_edit {
|
||||
@@ -307,36 +429,16 @@ impl NodeSetup {
|
||||
// Setup spacing between buttons.
|
||||
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
|
||||
|
||||
// Save button callback.
|
||||
let on_save = || {
|
||||
// Check if port is available.
|
||||
let (api_ip, _) = NodeConfig::get_api_ip_port();
|
||||
let available = NodeConfig::is_api_port_available(&api_ip, &self.api_port_edit);
|
||||
self.api_port_available_edit = available;
|
||||
|
||||
if available {
|
||||
// Save port at config if it's available.
|
||||
NodeConfig::save_api_address(&api_ip, &self.api_port_edit);
|
||||
|
||||
if Node::is_running() {
|
||||
Node::restart();
|
||||
}
|
||||
|
||||
self.is_api_port_available = true;
|
||||
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), || {
|
||||
cb.hide_keyboard();
|
||||
modal.close();
|
||||
Modal::close();
|
||||
});
|
||||
});
|
||||
columns[1].vertical_centered_justified(|ui| {
|
||||
View::button(ui, t!("modal.save"), Colors::white_or_black(false), on_save);
|
||||
View::button(ui, t!("modal.save"), Colors::white_or_black(false), || {
|
||||
on_save(self);
|
||||
});
|
||||
});
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
@@ -344,7 +446,7 @@ impl NodeSetup {
|
||||
}
|
||||
|
||||
/// Draw API secret token setup content.
|
||||
fn secret_ui(&mut self, modal_id: &'static str, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||
fn secret_ui(&mut self, modal_id: &'static str, ui: &mut egui::Ui) {
|
||||
let secret_title = match modal_id {
|
||||
API_SECRET_MODAL => t!("network_settings.api_secret"),
|
||||
_ => t!("network_settings.foreign_api_secret")
|
||||
@@ -356,8 +458,8 @@ impl NodeSetup {
|
||||
ui.add_space(6.0);
|
||||
|
||||
let secret_value = match modal_id {
|
||||
API_SECRET_MODAL => NodeConfig::get_api_secret(),
|
||||
_ => NodeConfig::get_foreign_api_secret()
|
||||
API_SECRET_MODAL => NodeConfig::get_api_secret(false),
|
||||
_ => NodeConfig::get_api_secret(true)
|
||||
};
|
||||
|
||||
let secret_text = if secret_value.is_some() {
|
||||
@@ -366,7 +468,7 @@ impl NodeSetup {
|
||||
format!("{} {}", SHIELD_SLASH, t!("network_settings.disabled"))
|
||||
};
|
||||
|
||||
View::button(ui, secret_text, Colors::button(), || {
|
||||
View::button(ui, secret_text, Colors::white_or_black(false), || {
|
||||
// Setup values for modal.
|
||||
self.secret_edit = secret_value.unwrap_or("".to_string());
|
||||
// Show secret edit modal.
|
||||
@@ -374,12 +476,24 @@ impl NodeSetup {
|
||||
.position(ModalPosition::CenterTop)
|
||||
.title(t!("network_settings.change_value"))
|
||||
.show();
|
||||
cb.show_keyboard();
|
||||
});
|
||||
}
|
||||
|
||||
/// Draw API secret token [`Modal`] content.
|
||||
fn secret_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
|
||||
let on_save = |c: &mut NodeSetup| {
|
||||
let secret = c.secret_edit.clone();
|
||||
match modal.id {
|
||||
API_SECRET_MODAL => {
|
||||
NodeConfig::save_api_secret(&secret);
|
||||
}
|
||||
_ => {
|
||||
NodeConfig::save_foreign_api_secret(&secret);
|
||||
}
|
||||
};
|
||||
Modal::close();
|
||||
};
|
||||
|
||||
ui.add_space(6.0);
|
||||
ui.vertical_centered(|ui| {
|
||||
let description = match modal.id {
|
||||
@@ -390,8 +504,14 @@ impl NodeSetup {
|
||||
ui.add_space(8.0);
|
||||
|
||||
// Draw API secret token value text edit.
|
||||
let mut secret_edit_opts = TextEditOptions::new(Id::from(modal.id)).copy().paste();
|
||||
View::text_edit(ui, cb, &mut self.secret_edit, &mut secret_edit_opts);
|
||||
let mut secret_edit = TextEdit::new(Id::from(modal.id))
|
||||
.copy()
|
||||
.paste();
|
||||
secret_edit.ui(ui, &mut self.secret_edit, cb);
|
||||
if secret_edit.enter_pressed {
|
||||
on_save(self);
|
||||
}
|
||||
|
||||
ui.add_space(6.0);
|
||||
|
||||
// Show reminder to restart enabled node.
|
||||
@@ -410,30 +530,16 @@ impl NodeSetup {
|
||||
// Setup spacing between buttons.
|
||||
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
|
||||
|
||||
// Save button callback.
|
||||
let on_save = || {
|
||||
let secret = self.secret_edit.clone();
|
||||
match modal.id {
|
||||
API_SECRET_MODAL => {
|
||||
NodeConfig::save_api_secret(&secret);
|
||||
}
|
||||
_ => {
|
||||
NodeConfig::save_foreign_api_secret(&secret);
|
||||
}
|
||||
};
|
||||
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), || {
|
||||
cb.hide_keyboard();
|
||||
modal.close();
|
||||
Modal::close();
|
||||
});
|
||||
});
|
||||
columns[1].vertical_centered_justified(|ui| {
|
||||
View::button(ui, t!("modal.save"), Colors::white_or_black(false), on_save);
|
||||
View::button(ui, t!("modal.save"), Colors::white_or_black(false), || {
|
||||
on_save(self);
|
||||
});
|
||||
});
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
@@ -441,7 +547,7 @@ impl NodeSetup {
|
||||
}
|
||||
|
||||
/// Draw FTL setup content.
|
||||
fn ftl_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||
fn ftl_ui(&mut self, ui: &mut egui::Ui) {
|
||||
ui.label(RichText::new(t!("network_settings.ftl"))
|
||||
.size(16.0)
|
||||
.color(Colors::gray())
|
||||
@@ -449,7 +555,9 @@ impl NodeSetup {
|
||||
ui.add_space(6.0);
|
||||
|
||||
let ftl = NodeConfig::get_ftl();
|
||||
View::button(ui, format!("{} {}", CLOCK_CLOCKWISE, ftl.clone()), Colors::button(), || {
|
||||
View::button(ui,
|
||||
format!("{} {}", CLOCK_CLOCKWISE, &ftl),
|
||||
Colors::white_or_black(false), || {
|
||||
// Setup values for modal.
|
||||
self.ftl_edit = ftl;
|
||||
// Show ftl value setup modal.
|
||||
@@ -457,7 +565,6 @@ impl NodeSetup {
|
||||
.position(ModalPosition::CenterTop)
|
||||
.title(t!("network_settings.change_value"))
|
||||
.show();
|
||||
cb.show_keyboard();
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
ui.label(RichText::new(t!("network_settings.ftl_description"))
|
||||
@@ -468,6 +575,14 @@ impl NodeSetup {
|
||||
|
||||
/// Draw FTL [`Modal`] content.
|
||||
fn ftl_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
|
||||
// Save button callback.
|
||||
let on_save = |c: &mut NodeSetup| {
|
||||
if let Ok(ftl) = c.ftl_edit.parse::<u64>() {
|
||||
NodeConfig::save_ftl(ftl);
|
||||
Modal::close();
|
||||
}
|
||||
};
|
||||
|
||||
ui.add_space(6.0);
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.label(RichText::new(t!("network_settings.ftl"))
|
||||
@@ -476,8 +591,11 @@ impl NodeSetup {
|
||||
ui.add_space(8.0);
|
||||
|
||||
// Draw ftl value text edit.
|
||||
let mut ftl_edit_opts = TextEditOptions::new(Id::from(modal.id)).h_center();
|
||||
View::text_edit(ui, cb, &mut self.ftl_edit, &mut ftl_edit_opts);
|
||||
let mut ftl_edit = TextEdit::new(Id::from(modal.id)).h_center().numeric();
|
||||
ftl_edit.ui(ui, &mut self.ftl_edit, cb);
|
||||
if ftl_edit.enter_pressed {
|
||||
on_save(self);
|
||||
}
|
||||
|
||||
// Show error when specified value is not valid or reminder to restart enabled node.
|
||||
if self.ftl_edit.parse::<u64>().is_err() {
|
||||
@@ -496,25 +614,17 @@ impl NodeSetup {
|
||||
// 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(ftl) = self.ftl_edit.parse::<u64>() {
|
||||
NodeConfig::save_ftl(ftl);
|
||||
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();
|
||||
Modal::close();
|
||||
});
|
||||
});
|
||||
columns[1].vertical_centered_justified(|ui| {
|
||||
View::button(ui, t!("modal.save"), Colors::white_or_black(false), on_save);
|
||||
View::button(ui, t!("modal.save"), Colors::white_or_black(false), || {
|
||||
on_save(self);
|
||||
});
|
||||
});
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
|
||||
+236
-214
@@ -12,16 +12,17 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use egui::{Align, Id, Layout, RichText};
|
||||
use egui::{Align, Id, Layout, RichText, StrokeKind};
|
||||
use egui_async::Bind;
|
||||
use grin_core::global::ChainTypes;
|
||||
|
||||
use crate::AppConfig;
|
||||
use crate::gui::Colors;
|
||||
use crate::gui::icons::{ARROW_FAT_LINES_DOWN, ARROW_FAT_LINES_UP, GLOBE_SIMPLE, HANDSHAKE, PLUG, PLUS_CIRCLE, PROHIBIT_INSET, TRASH};
|
||||
use crate::gui::platform::PlatformCallbacks;
|
||||
use crate::gui::views::{Modal, View};
|
||||
use crate::gui::views::{Modal, TextEdit, View};
|
||||
use crate::gui::views::network::settings::NetworkSettings;
|
||||
use crate::gui::views::types::{ModalContainer, ModalPosition, TextEditOptions};
|
||||
use crate::gui::views::types::{ContentContainer, ModalPosition};
|
||||
use crate::node::{Node, NodeConfig, PeersConfig};
|
||||
|
||||
/// Type of peer.
|
||||
@@ -44,8 +45,10 @@ pub struct P2PSetup {
|
||||
/// Flag to check if p2p port from saved config value is available.
|
||||
is_port_available: bool,
|
||||
|
||||
/// Flag to check if entered peer address is correct and/or available.
|
||||
is_correct_address_edit: bool,
|
||||
/// Async check entered peer address.
|
||||
address_check: Bind<bool, String>,
|
||||
/// Flag to check if peer is correct and/or available.
|
||||
address_available: Option<bool>,
|
||||
/// Peer edit value for modal.
|
||||
peer_edit: String,
|
||||
|
||||
@@ -65,9 +68,6 @@ pub struct P2PSetup {
|
||||
|
||||
/// Flag to check if reset of peers was called.
|
||||
peers_reset: bool,
|
||||
|
||||
/// [`Modal`] identifiers allowed at this ui container.
|
||||
modal_ids: Vec<&'static str>
|
||||
}
|
||||
|
||||
/// Identifier for port value [`Modal`].
|
||||
@@ -91,18 +91,19 @@ impl Default for P2PSetup {
|
||||
fn default() -> Self {
|
||||
let port = NodeConfig::get_p2p_port();
|
||||
let is_port_available = NodeConfig::is_p2p_port_available(&port);
|
||||
let default_main_seeds = grin_servers::MAINNET_DNS_SEEDS
|
||||
let default_main_seeds = Node::MAINNET_DNS_SEEDS
|
||||
.iter()
|
||||
.map(|s| s.to_string())
|
||||
.collect();
|
||||
let default_test_seeds = grin_servers::TESTNET_DNS_SEEDS
|
||||
let default_test_seeds = Node::TESTNET_DNS_SEEDS
|
||||
.into_iter()
|
||||
.map(|s| s.to_string())
|
||||
.collect();
|
||||
Self {
|
||||
port_edit: port,
|
||||
port_available_edit: is_port_available,
|
||||
is_correct_address_edit: true,
|
||||
address_check: Bind::new(false),
|
||||
address_available: Some(true),
|
||||
is_port_available,
|
||||
peer_edit: "".to_string(),
|
||||
default_main_seeds,
|
||||
@@ -111,23 +112,22 @@ impl Default for P2PSetup {
|
||||
max_inbound_count: NodeConfig::get_max_inbound_peers(),
|
||||
max_outbound_count: NodeConfig::get_max_outbound_peers(),
|
||||
peers_reset: false,
|
||||
modal_ids: vec![
|
||||
PORT_MODAL,
|
||||
CUSTOM_SEED_MODAL,
|
||||
ALLOW_PEER_MODAL,
|
||||
DENY_PEER_MODAL,
|
||||
PREFER_PEER_MODAL,
|
||||
BAN_WINDOW_MODAL,
|
||||
MAX_INBOUND_MODAL,
|
||||
MAX_OUTBOUND_MODAL
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ModalContainer for P2PSetup {
|
||||
fn modal_ids(&self) -> &Vec<&'static str> {
|
||||
&self.modal_ids
|
||||
impl ContentContainer for P2PSetup {
|
||||
fn modal_ids(&self) -> Vec<&'static str> {
|
||||
vec![
|
||||
PORT_MODAL,
|
||||
CUSTOM_SEED_MODAL,
|
||||
ALLOW_PEER_MODAL,
|
||||
DENY_PEER_MODAL,
|
||||
PREFER_PEER_MODAL,
|
||||
BAN_WINDOW_MODAL,
|
||||
MAX_INBOUND_MODAL,
|
||||
MAX_OUTBOUND_MODAL
|
||||
]
|
||||
}
|
||||
|
||||
fn modal_ui(&mut self,
|
||||
@@ -146,30 +146,22 @@ impl ModalContainer for P2PSetup {
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl P2PSetup {
|
||||
/// Title for custom DNS Seeds setup section.
|
||||
const DNS_SEEDS_TITLE: &'static str = "DNS Seeds";
|
||||
|
||||
pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||
// Draw modal content for current ui container.
|
||||
self.current_modal_ui(ui, cb);
|
||||
|
||||
fn container_ui(&mut self, ui: &mut egui::Ui, _: &dyn PlatformCallbacks) {
|
||||
View::sub_title(ui, format!("{} {}", HANDSHAKE, t!("network_settings.p2p_server")));
|
||||
View::horizontal_line(ui, Colors::stroke());
|
||||
ui.add_space(6.0);
|
||||
|
||||
ui.vertical_centered(|ui| {
|
||||
// Show p2p port setup.
|
||||
self.port_ui(ui, cb);
|
||||
self.port_ui(ui);
|
||||
|
||||
ui.add_space(6.0);
|
||||
View::horizontal_line(ui, Colors::item_stroke());
|
||||
ui.add_space(6.0);
|
||||
|
||||
// Show seeding type setup.
|
||||
self.seeding_type_ui(ui, cb);
|
||||
self.seeding_type_ui(ui);
|
||||
|
||||
ui.add_space(6.0);
|
||||
View::horizontal_line(ui, Colors::item_stroke());
|
||||
@@ -180,7 +172,7 @@ impl P2PSetup {
|
||||
.color(Colors::gray()));
|
||||
ui.add_space(6.0);
|
||||
// Show allowed peers setup.
|
||||
self.peer_list_ui(ui, &PeerType::Allowed, cb);
|
||||
self.peer_list_ui(ui, &PeerType::Allowed);
|
||||
|
||||
ui.add_space(6.0);
|
||||
View::horizontal_line(ui, Colors::item_stroke());
|
||||
@@ -191,7 +183,7 @@ impl P2PSetup {
|
||||
.color(Colors::gray()));
|
||||
ui.add_space(6.0);
|
||||
// Show denied peers setup.
|
||||
self.peer_list_ui(ui, &PeerType::Denied, cb);
|
||||
self.peer_list_ui(ui, &PeerType::Denied);
|
||||
|
||||
ui.add_space(6.0);
|
||||
View::horizontal_line(ui, Colors::item_stroke());
|
||||
@@ -202,29 +194,28 @@ impl P2PSetup {
|
||||
.color(Colors::gray()));
|
||||
ui.add_space(6.0);
|
||||
// Show preferred peers setup.
|
||||
self.peer_list_ui(ui, &PeerType::Preferred, cb);
|
||||
|
||||
self.peer_list_ui(ui, &PeerType::Preferred);
|
||||
|
||||
ui.add_space(6.0);
|
||||
View::horizontal_line(ui, Colors::item_stroke());
|
||||
ui.add_space(6.0);
|
||||
|
||||
// Show ban window setup.
|
||||
self.ban_window_ui(ui, cb);
|
||||
self.ban_window_ui(ui);
|
||||
|
||||
ui.add_space(6.0);
|
||||
View::horizontal_line(ui, Colors::item_stroke());
|
||||
ui.add_space(6.0);
|
||||
|
||||
// Show maximum inbound peers value setup.
|
||||
self.max_inbound_ui(ui, cb);
|
||||
self.max_inbound_ui(ui);
|
||||
|
||||
ui.add_space(6.0);
|
||||
View::horizontal_line(ui, Colors::item_stroke());
|
||||
ui.add_space(6.0);
|
||||
|
||||
// Show maximum outbound peers value setup.
|
||||
self.max_outbound_ui(ui, cb);
|
||||
self.max_outbound_ui(ui);
|
||||
|
||||
if !Node::is_restarting() && !self.peers_reset {
|
||||
ui.add_space(6.0);
|
||||
@@ -236,9 +227,14 @@ impl P2PSetup {
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Title for custom DNS Seeds setup section.
|
||||
const DNS_SEEDS_TITLE: &'static str = "DNS Seeds";
|
||||
|
||||
impl P2PSetup {
|
||||
/// Draw p2p port setup content.
|
||||
fn port_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||
fn port_ui(&mut self, ui: &mut egui::Ui) {
|
||||
ui.label(RichText::new(t!("network_settings.p2p_port"))
|
||||
.size(16.0)
|
||||
.color(Colors::gray())
|
||||
@@ -246,17 +242,18 @@ impl P2PSetup {
|
||||
ui.add_space(6.0);
|
||||
|
||||
let port = NodeConfig::get_p2p_port();
|
||||
View::button(ui, format!("{} {}", PLUG, port.clone()), Colors::button(), || {
|
||||
// Setup values for modal.
|
||||
self.port_edit = port;
|
||||
self.port_available_edit = self.is_port_available;
|
||||
// Show p2p port modal.
|
||||
Modal::new(PORT_MODAL)
|
||||
.position(ModalPosition::CenterTop)
|
||||
.title(t!("network_settings.change_value"))
|
||||
.show();
|
||||
cb.show_keyboard();
|
||||
});
|
||||
View::button(ui,
|
||||
format!("{} {}", PLUG, &port),
|
||||
Colors::white_or_black(false), || {
|
||||
// Setup values for modal.
|
||||
self.port_edit = port;
|
||||
self.port_available_edit = self.is_port_available;
|
||||
// Show p2p port modal.
|
||||
Modal::new(PORT_MODAL)
|
||||
.position(ModalPosition::CenterTop)
|
||||
.title(t!("network_settings.change_value"))
|
||||
.show();
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
|
||||
// Show error when p2p port is unavailable.
|
||||
@@ -271,6 +268,22 @@ impl P2PSetup {
|
||||
|
||||
/// Draw p2p port [`Modal`] content.
|
||||
fn port_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
|
||||
let on_save = |c: &mut P2PSetup| {
|
||||
// Check if port is available.
|
||||
let available = NodeConfig::is_p2p_port_available(&c.port_edit);
|
||||
c.port_available_edit = available;
|
||||
|
||||
// Save port at config if it's available.
|
||||
if available {
|
||||
NodeConfig::save_p2p_port(c.port_edit.parse::<u16>().unwrap());
|
||||
if Node::is_running() {
|
||||
Node::restart();
|
||||
}
|
||||
c.is_port_available = true;
|
||||
Modal::close();
|
||||
}
|
||||
};
|
||||
|
||||
ui.add_space(6.0);
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.label(RichText::new(t!("network_settings.p2p_port"))
|
||||
@@ -279,8 +292,11 @@ impl P2PSetup {
|
||||
ui.add_space(8.0);
|
||||
|
||||
// Draw p2p port text edit.
|
||||
let mut text_edit_opts = TextEditOptions::new(Id::from(modal.id)).h_center();
|
||||
View::text_edit(ui, cb, &mut self.port_edit, &mut text_edit_opts);
|
||||
let mut edit = TextEdit::new(Id::from(modal.id)).h_center().numeric();
|
||||
edit.ui(ui, &mut self.port_edit, cb);
|
||||
if edit.enter_pressed {
|
||||
on_save(self);
|
||||
}
|
||||
|
||||
// Show error when specified port is unavailable.
|
||||
if !self.port_available_edit {
|
||||
@@ -297,36 +313,17 @@ impl P2PSetup {
|
||||
// Setup spacing between buttons.
|
||||
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
|
||||
|
||||
// Save button callback.
|
||||
let on_save = || {
|
||||
// Check if port is available.
|
||||
let available = NodeConfig::is_p2p_port_available(&self.port_edit);
|
||||
self.port_available_edit = available;
|
||||
|
||||
// Save port at config if it's available.
|
||||
if available {
|
||||
NodeConfig::save_p2p_port(self.port_edit.parse::<u16>().unwrap());
|
||||
|
||||
if Node::is_running() {
|
||||
Node::restart();
|
||||
}
|
||||
|
||||
self.is_port_available = true;
|
||||
cb.hide_keyboard();
|
||||
modal.close();
|
||||
}
|
||||
};
|
||||
|
||||
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();
|
||||
Modal::close();
|
||||
});
|
||||
});
|
||||
columns[1].vertical_centered_justified(|ui| {
|
||||
View::button(ui, t!("modal.save"), Colors::white_or_black(false), on_save);
|
||||
View::button(ui, t!("modal.save"), Colors::white_or_black(false), || {
|
||||
on_save(self);
|
||||
});
|
||||
});
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
@@ -335,7 +332,7 @@ impl P2PSetup {
|
||||
}
|
||||
|
||||
/// Draw peer list content based on provided [`PeerType`].
|
||||
fn peer_list_ui(&mut self, ui: &mut egui::Ui, peer_type: &PeerType, cb: &dyn PlatformCallbacks) {
|
||||
fn peer_list_ui(&mut self, ui: &mut egui::Ui, peer_type: &PeerType) {
|
||||
let peers = match peer_type {
|
||||
PeerType::DefaultSeed => {
|
||||
if AppConfig::chain_type() == ChainTypes::Testnet {
|
||||
@@ -371,6 +368,8 @@ impl P2PSetup {
|
||||
.size(16.0)
|
||||
.color(Colors::inactive_text()));
|
||||
ui.add_space(12.0);
|
||||
} else if !peers.is_empty() {
|
||||
ui.add_space(12.0);
|
||||
}
|
||||
|
||||
let add_text = if peer_type == &PeerType::CustomSeed {
|
||||
@@ -379,9 +378,10 @@ impl P2PSetup {
|
||||
format!("{} {}", PLUS_CIRCLE, t!("network_settings.add_peer"))
|
||||
|
||||
};
|
||||
View::button(ui, add_text, Colors::button(), || {
|
||||
View::button(ui, add_text, Colors::white_or_black(false), || {
|
||||
// Setup values for modal.
|
||||
self.is_correct_address_edit = true;
|
||||
self.address_check = Bind::new(false);
|
||||
self.address_available = Some(true);
|
||||
self.peer_edit = "".to_string();
|
||||
// Select modal id.
|
||||
let modal_id = match peer_type {
|
||||
@@ -392,17 +392,16 @@ impl P2PSetup {
|
||||
};
|
||||
// Select modal title.
|
||||
let modal_title = match peer_type {
|
||||
PeerType::Allowed => t!("network_settings.allow_list"),
|
||||
PeerType::Denied => t!("network_settings.deny_list"),
|
||||
PeerType::Preferred => t!("network_settings.favourites"),
|
||||
_ => Self::DNS_SEEDS_TITLE.to_string()
|
||||
PeerType::Allowed => t!("network_settings.allow_list").into(),
|
||||
PeerType::Denied => t!("network_settings.deny_list").into(),
|
||||
PeerType::Preferred => t!("network_settings.favourites").into(),
|
||||
_ => DNS_SEEDS_TITLE.to_string()
|
||||
};
|
||||
// Show modal to add peer.
|
||||
Modal::new(modal_id)
|
||||
.position(ModalPosition::CenterTop)
|
||||
.title(modal_title)
|
||||
.show();
|
||||
cb.show_keyboard();
|
||||
});
|
||||
}
|
||||
ui.add_space(6.0);
|
||||
@@ -410,6 +409,35 @@ impl P2PSetup {
|
||||
|
||||
/// Draw peer creation [`Modal`] content.
|
||||
fn peer_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
|
||||
if self.address_available.is_none() {
|
||||
let peer = self.peer_edit.clone();
|
||||
if let Some(res) = self.address_check.read_or_request(|| async {
|
||||
let available = PeersConfig::peer_to_addr(peer).is_some();
|
||||
Ok(available)
|
||||
}) {
|
||||
match res {
|
||||
Ok(available) => {
|
||||
self.address_available = Some(*available);
|
||||
// Save peer at config.
|
||||
if *available {
|
||||
let peer = self.peer_edit.clone();
|
||||
match modal.id {
|
||||
CUSTOM_SEED_MODAL => NodeConfig::save_custom_seed(peer),
|
||||
ALLOW_PEER_MODAL => NodeConfig::allow_peer(peer),
|
||||
DENY_PEER_MODAL => NodeConfig::deny_peer(peer),
|
||||
PREFER_PEER_MODAL => NodeConfig::prefer_peer(peer),
|
||||
&_ => {}
|
||||
}
|
||||
Modal::close();
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
self.address_available = Some(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ui.add_space(6.0);
|
||||
ui.vertical_centered(|ui| {
|
||||
let label_text = match modal.id {
|
||||
@@ -420,15 +448,23 @@ impl P2PSetup {
|
||||
ui.add_space(8.0);
|
||||
|
||||
// Draw peer address text edit.
|
||||
let mut peer_text_edit_opts = TextEditOptions::new(Id::from(modal.id)).paste();
|
||||
View::text_edit(ui, cb, &mut self.peer_edit, &mut peer_text_edit_opts);
|
||||
let mut edit = TextEdit::new(Id::from(modal.id)).paste();
|
||||
if self.address_available.is_none() {
|
||||
edit = edit.disable();
|
||||
}
|
||||
edit.ui(ui, &mut self.peer_edit, cb);
|
||||
if edit.enter_pressed {
|
||||
self.address_available = None;
|
||||
}
|
||||
|
||||
// Show error when specified address is incorrect.
|
||||
if !self.is_correct_address_edit {
|
||||
ui.add_space(10.0);
|
||||
ui.label(RichText::new(t!("network_settings.peer_address_error"))
|
||||
.size(16.0)
|
||||
.color(Colors::red()));
|
||||
if let Some(available) = self.address_available {
|
||||
if !available {
|
||||
ui.add_space(10.0);
|
||||
ui.label(RichText::new(t!("network_settings.peer_address_error"))
|
||||
.size(16.0)
|
||||
.color(Colors::red()));
|
||||
}
|
||||
}
|
||||
ui.add_space(12.0);
|
||||
|
||||
@@ -437,39 +473,19 @@ impl P2PSetup {
|
||||
// Setup spacing between buttons.
|
||||
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
|
||||
|
||||
// Save button callback.
|
||||
let on_save = || {
|
||||
// Check if peer is correct and/or available.
|
||||
let peer = self.peer_edit.clone();
|
||||
let is_correct_address = PeersConfig::peer_to_addr(peer.clone()).is_some();
|
||||
self.is_correct_address_edit = is_correct_address;
|
||||
|
||||
// Save peer at config.
|
||||
if is_correct_address {
|
||||
match modal.id {
|
||||
CUSTOM_SEED_MODAL => NodeConfig::save_custom_seed(peer),
|
||||
ALLOW_PEER_MODAL => NodeConfig::allow_peer(peer),
|
||||
DENY_PEER_MODAL => NodeConfig::deny_peer(peer),
|
||||
PREFER_PEER_MODAL => NodeConfig::prefer_peer(peer),
|
||||
&_ => {}
|
||||
}
|
||||
|
||||
self.is_port_available = true;
|
||||
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();
|
||||
Modal::close();
|
||||
});
|
||||
});
|
||||
columns[1].vertical_centered_justified(|ui| {
|
||||
View::button(ui, t!("modal.save"), Colors::white_or_black(false), on_save);
|
||||
View::button(ui, t!("modal.save"), Colors::white_or_black(false), || {
|
||||
if self.address_available.is_some() {
|
||||
self.address_available = None;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
@@ -478,9 +494,8 @@ impl P2PSetup {
|
||||
}
|
||||
|
||||
/// Draw seeding type setup content.
|
||||
fn seeding_type_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||
let title = Self::DNS_SEEDS_TITLE;
|
||||
ui.label(RichText::new(title).size(16.0).color(Colors::gray()));
|
||||
fn seeding_type_ui(&mut self, ui: &mut egui::Ui) {
|
||||
ui.label(RichText::new(DNS_SEEDS_TITLE).size(16.0).color(Colors::gray()));
|
||||
ui.add_space(2.0);
|
||||
|
||||
let default_seeding = NodeConfig::is_default_seeding_type();
|
||||
@@ -494,11 +509,11 @@ impl P2PSetup {
|
||||
} else {
|
||||
PeerType::CustomSeed
|
||||
};
|
||||
self.peer_list_ui(ui, &peers_type, cb);
|
||||
self.peer_list_ui(ui, &peers_type);
|
||||
}
|
||||
|
||||
/// Draw ban window setup content.
|
||||
fn ban_window_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||
fn ban_window_ui(&mut self, ui: &mut egui::Ui) {
|
||||
ui.label(RichText::new(t!("network_settings.ban_window"))
|
||||
.size(16.0)
|
||||
.color(Colors::gray())
|
||||
@@ -506,16 +521,17 @@ impl P2PSetup {
|
||||
ui.add_space(6.0);
|
||||
|
||||
let ban_window = NodeConfig::get_p2p_ban_window();
|
||||
View::button(ui, format!("{} {}", PROHIBIT_INSET, ban_window.clone()), Colors::button(), || {
|
||||
// Setup values for modal.
|
||||
self.ban_window_edit = ban_window;
|
||||
// Show ban window period setup modal.
|
||||
Modal::new(BAN_WINDOW_MODAL)
|
||||
.position(ModalPosition::CenterTop)
|
||||
.title(t!("network_settings.change_value"))
|
||||
.show();
|
||||
cb.show_keyboard();
|
||||
});
|
||||
View::button(ui,
|
||||
format!("{} {}", PROHIBIT_INSET, &ban_window),
|
||||
Colors::white_or_black(false), || {
|
||||
// Setup values for modal.
|
||||
self.ban_window_edit = ban_window;
|
||||
// Show ban window period setup modal.
|
||||
Modal::new(BAN_WINDOW_MODAL)
|
||||
.position(ModalPosition::CenterTop)
|
||||
.title(t!("network_settings.change_value"))
|
||||
.show();
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
ui.label(RichText::new(t!("network_settings.ban_window_desc"))
|
||||
.size(16.0)
|
||||
@@ -526,6 +542,13 @@ impl P2PSetup {
|
||||
|
||||
/// Draw ban window [`Modal`] content.
|
||||
fn ban_window_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
|
||||
let on_save = |c: &mut P2PSetup| {
|
||||
if let Ok(ban_window) = c.ban_window_edit.parse::<i64>() {
|
||||
NodeConfig::save_p2p_ban_window(ban_window);
|
||||
Modal::close();
|
||||
}
|
||||
};
|
||||
|
||||
ui.add_space(6.0);
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.label(RichText::new(t!("network_settings.ban_window"))
|
||||
@@ -534,8 +557,11 @@ impl P2PSetup {
|
||||
ui.add_space(8.0);
|
||||
|
||||
// Draw ban window text edit.
|
||||
let mut text_edit_opts = TextEditOptions::new(Id::from(modal.id)).h_center();
|
||||
View::text_edit(ui, cb, &mut self.ban_window_edit, &mut text_edit_opts);
|
||||
let mut edit = TextEdit::new(Id::from(modal.id)).h_center().numeric();
|
||||
edit.ui(ui, &mut self.ban_window_edit, cb);
|
||||
if edit.enter_pressed {
|
||||
on_save(self);
|
||||
}
|
||||
|
||||
// Show error when specified value is not valid or reminder to restart enabled node.
|
||||
if self.ban_window_edit.parse::<i64>().is_err() {
|
||||
@@ -554,25 +580,16 @@ impl P2PSetup {
|
||||
// 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(ban_window) = self.ban_window_edit.parse::<i64>() {
|
||||
NodeConfig::save_p2p_ban_window(ban_window);
|
||||
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();
|
||||
Modal::close();
|
||||
});
|
||||
});
|
||||
columns[1].vertical_centered_justified(|ui| {
|
||||
View::button(ui, t!("modal.save"), Colors::white_or_black(false), on_save);
|
||||
View::button(ui, t!("modal.save"), Colors::white_or_black(false), || {
|
||||
on_save(self);
|
||||
});
|
||||
});
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
@@ -580,7 +597,7 @@ impl P2PSetup {
|
||||
}
|
||||
|
||||
/// Draw maximum number of inbound peers setup content.
|
||||
fn max_inbound_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||
fn max_inbound_ui(&mut self, ui: &mut egui::Ui) {
|
||||
ui.label(RichText::new(t!("network_settings.max_inbound_count"))
|
||||
.size(16.0)
|
||||
.color(Colors::gray())
|
||||
@@ -588,22 +605,29 @@ impl P2PSetup {
|
||||
ui.add_space(6.0);
|
||||
|
||||
let max_inbound = NodeConfig::get_max_inbound_peers();
|
||||
let button_text = format!("{} {}", ARROW_FAT_LINES_DOWN, max_inbound.clone());
|
||||
View::button(ui, button_text, Colors::button(), || {
|
||||
// Setup values for modal.
|
||||
self.max_inbound_count = max_inbound;
|
||||
// Show maximum number of inbound peers setup modal.
|
||||
Modal::new(MAX_INBOUND_MODAL)
|
||||
.position(ModalPosition::CenterTop)
|
||||
.title(t!("network_settings.change_value"))
|
||||
.show();
|
||||
cb.show_keyboard();
|
||||
});
|
||||
View::button(ui,
|
||||
format!("{} {}", ARROW_FAT_LINES_DOWN, &max_inbound),
|
||||
Colors::white_or_black(false), || {
|
||||
// Setup values for modal.
|
||||
self.max_inbound_count = max_inbound;
|
||||
// Show maximum number of inbound peers setup modal.
|
||||
Modal::new(MAX_INBOUND_MODAL)
|
||||
.position(ModalPosition::CenterTop)
|
||||
.title(t!("network_settings.change_value"))
|
||||
.show();
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
}
|
||||
|
||||
/// Draw maximum number of inbound peers [`Modal`] content.
|
||||
fn max_inbound_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
|
||||
let on_save = |c: &mut P2PSetup| {
|
||||
if let Ok(max_inbound) = c.max_inbound_count.parse::<u32>() {
|
||||
NodeConfig::save_max_inbound_peers(max_inbound);
|
||||
Modal::close();
|
||||
}
|
||||
};
|
||||
|
||||
ui.add_space(6.0);
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.label(RichText::new(t!("network_settings.max_inbound_count"))
|
||||
@@ -612,8 +636,11 @@ impl P2PSetup {
|
||||
ui.add_space(8.0);
|
||||
|
||||
// Draw maximum number of inbound peers text edit.
|
||||
let mut text_edit_opts = TextEditOptions::new(Id::from(modal.id)).h_center();
|
||||
View::text_edit(ui, cb, &mut self.max_inbound_count, &mut text_edit_opts);
|
||||
let mut edit = TextEdit::new(Id::from(modal.id)).h_center().numeric();
|
||||
edit.ui(ui, &mut self.max_inbound_count, cb);
|
||||
if edit.enter_pressed {
|
||||
on_save(self);
|
||||
}
|
||||
|
||||
// Show error when specified value is not valid or reminder to restart enabled node.
|
||||
if self.max_inbound_count.parse::<u32>().is_err() {
|
||||
@@ -632,25 +659,16 @@ impl P2PSetup {
|
||||
// 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(max_inbound) = self.max_inbound_count.parse::<u32>() {
|
||||
NodeConfig::save_max_inbound_peers(max_inbound);
|
||||
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();
|
||||
Modal::close();
|
||||
});
|
||||
});
|
||||
columns[1].vertical_centered_justified(|ui| {
|
||||
View::button(ui, t!("modal.save"), Colors::white_or_black(false), on_save);
|
||||
View::button(ui, t!("modal.save"), Colors::white_or_black(false), || {
|
||||
on_save(self);
|
||||
});
|
||||
});
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
@@ -658,30 +676,36 @@ impl P2PSetup {
|
||||
}
|
||||
|
||||
/// Draw maximum number of outbound peers setup content.
|
||||
fn max_outbound_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||
fn max_outbound_ui(&mut self, ui: &mut egui::Ui) {
|
||||
ui.label(RichText::new(t!("network_settings.max_outbound_count"))
|
||||
.size(16.0)
|
||||
.color(Colors::gray())
|
||||
);
|
||||
ui.add_space(6.0);
|
||||
|
||||
let max_outbound = NodeConfig::get_max_outbound_peers();
|
||||
let button_text = format!("{} {}", ARROW_FAT_LINES_UP, max_outbound.clone());
|
||||
View::button(ui, button_text, Colors::button(), || {
|
||||
// Setup values for modal.
|
||||
self.max_outbound_count = max_outbound;
|
||||
// Show maximum number of outbound peers setup modal.
|
||||
Modal::new(MAX_OUTBOUND_MODAL)
|
||||
.position(ModalPosition::CenterTop)
|
||||
.title(t!("network_settings.change_value"))
|
||||
.show();
|
||||
cb.show_keyboard();
|
||||
});
|
||||
View::button(ui,
|
||||
format!("{} {}", ARROW_FAT_LINES_UP, &max_outbound),
|
||||
Colors::white_or_black(false), || {
|
||||
// Setup values for modal.
|
||||
self.max_outbound_count = max_outbound;
|
||||
// Show maximum number of outbound peers setup modal.
|
||||
Modal::new(MAX_OUTBOUND_MODAL)
|
||||
.position(ModalPosition::CenterTop)
|
||||
.title(t!("network_settings.change_value"))
|
||||
.show();
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
}
|
||||
|
||||
/// Draw maximum number of outbound peers [`Modal`] content.
|
||||
fn max_outbound_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
|
||||
let on_save = |c: &mut P2PSetup| {
|
||||
if let Ok(max_outbound) = c.max_outbound_count.parse::<u32>() {
|
||||
NodeConfig::save_max_outbound_peers(max_outbound);
|
||||
Modal::close();
|
||||
}
|
||||
};
|
||||
|
||||
ui.add_space(6.0);
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.label(RichText::new(t!("network_settings.max_outbound_count"))
|
||||
@@ -690,8 +714,11 @@ impl P2PSetup {
|
||||
ui.add_space(8.0);
|
||||
|
||||
// Draw maximum number of outbound peers text edit.
|
||||
let mut text_edit_opts = TextEditOptions::new(Id::from(modal.id)).h_center();
|
||||
View::text_edit(ui, cb, &mut self.max_outbound_count, &mut text_edit_opts);
|
||||
let mut edit = TextEdit::new(Id::from(modal.id)).h_center().numeric();
|
||||
edit.ui(ui, &mut self.max_outbound_count, cb);
|
||||
if edit.enter_pressed {
|
||||
on_save(self);
|
||||
}
|
||||
|
||||
// Show error when specified value is not valid or reminder to restart enabled node.
|
||||
if self.max_outbound_count.parse::<u32>().is_err() {
|
||||
@@ -710,25 +737,17 @@ impl P2PSetup {
|
||||
// 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(max_outbound) = self.max_outbound_count.parse::<u32>() {
|
||||
NodeConfig::save_max_outbound_peers(max_outbound);
|
||||
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();
|
||||
Modal::close();
|
||||
});
|
||||
});
|
||||
columns[1].vertical_centered_justified(|ui| {
|
||||
View::button(ui, t!("modal.save"), Colors::white_or_black(false), on_save);
|
||||
View::button(ui, t!("modal.save"), Colors::white_or_black(false), || {
|
||||
on_save(self);
|
||||
});
|
||||
});
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
@@ -738,12 +757,13 @@ impl P2PSetup {
|
||||
/// Draw content to reset peers data.
|
||||
fn reset_peers_ui(&mut self, ui: &mut egui::Ui) {
|
||||
ui.add_space(4.0);
|
||||
|
||||
let button_text = format!("{} {}", TRASH, t!("network_settings.reset_peers"));
|
||||
View::colored_text_button(ui, button_text, Colors::red(), Colors::button(), || {
|
||||
Node::reset_peers(false);
|
||||
self.peers_reset = true;
|
||||
});
|
||||
View::colored_text_button(ui,
|
||||
format!("{} {}", TRASH, t!("network_settings.reset_peers")),
|
||||
Colors::red(),
|
||||
Colors::white_or_black(false), || {
|
||||
Node::reset_peers(false);
|
||||
self.peers_reset = true;
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
ui.label(RichText::new(t!("network_settings.reset_peers_desc"))
|
||||
.size(16.0)
|
||||
@@ -763,10 +783,12 @@ fn peer_item_ui(ui: &mut egui::Ui,
|
||||
rect.set_height(42.0);
|
||||
|
||||
// Draw round background.
|
||||
let mut bg_rect = rect.clone();
|
||||
bg_rect.min += egui::emath::vec2(6.0, 0.0);
|
||||
let item_rounding = View::item_rounding(index, len, false);
|
||||
ui.painter().rect(bg_rect, item_rounding, Colors::white_or_black(false), View::item_stroke());
|
||||
ui.painter().rect(rect,
|
||||
item_rounding,
|
||||
Colors::white_or_black(false),
|
||||
View::item_stroke(),
|
||||
StrokeKind::Outside);
|
||||
|
||||
ui.vertical(|ui| {
|
||||
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
|
||||
@@ -793,7 +815,7 @@ fn peer_item_ui(ui: &mut egui::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.add_space(6.0);
|
||||
// Draw peer address.
|
||||
let peer_text = format!("{} {}", GLOBE_SIMPLE, &peer_addr);
|
||||
ui.label(RichText::new(peer_text)
|
||||
|
||||
+120
-121
@@ -17,42 +17,35 @@ use egui::{Id, RichText};
|
||||
use crate::gui::Colors;
|
||||
use crate::gui::icons::{BEZIER_CURVE, BOUNDING_BOX, CHART_SCATTER, CIRCLES_THREE, CLOCK_COUNTDOWN, HAND_COINS};
|
||||
use crate::gui::platform::PlatformCallbacks;
|
||||
use crate::gui::views::{Modal, View};
|
||||
use crate::gui::views::{Modal, TextEdit, View};
|
||||
use crate::gui::views::network::settings::NetworkSettings;
|
||||
use crate::gui::views::types::{ModalContainer, ModalPosition, TextEditOptions};
|
||||
use crate::gui::views::types::{ContentContainer, ModalPosition};
|
||||
use crate::node::NodeConfig;
|
||||
|
||||
/// Memory pool setup section content.
|
||||
pub struct PoolSetup {
|
||||
/// Base fee value that's accepted into the pool.
|
||||
fee_base_edit: String,
|
||||
|
||||
/// Reorg cache retention period value in minutes.
|
||||
reorg_period_edit: String,
|
||||
|
||||
/// Maximum number of transactions allowed in the pool.
|
||||
pool_size_edit: String,
|
||||
|
||||
/// Maximum number of transactions allowed in the stempool.
|
||||
stempool_size_edit: String,
|
||||
|
||||
/// Maximum total weight of transactions to build a block.
|
||||
max_weight_edit: String,
|
||||
|
||||
/// [`Modal`] identifiers allowed at this ui container.
|
||||
modal_ids: Vec<&'static str>,
|
||||
}
|
||||
|
||||
/// Identifier for base fee value [`Modal`].
|
||||
pub const FEE_BASE_MODAL: &'static str = "fee_base";
|
||||
const FEE_BASE_MODAL: &'static str = "fee_base";
|
||||
/// Identifier for reorg cache retention period value [`Modal`].
|
||||
pub const REORG_PERIOD_MODAL: &'static str = "reorg_period";
|
||||
const REORG_PERIOD_MODAL: &'static str = "reorg_period";
|
||||
/// Identifier for maximum number of transactions in the pool [`Modal`].
|
||||
pub const POOL_SIZE_MODAL: &'static str = "pool_size";
|
||||
const POOL_SIZE_MODAL: &'static str = "pool_size";
|
||||
/// Identifier for maximum number of transactions in the stempool [`Modal`].
|
||||
pub const STEMPOOL_SIZE_MODAL: &'static str = "stempool_size";
|
||||
const STEMPOOL_SIZE_MODAL: &'static str = "stempool_size";
|
||||
/// Identifier for maximum total weight of transactions [`Modal`].
|
||||
pub const MAX_WEIGHT_MODAL: &'static str = "max_weight";
|
||||
const MAX_WEIGHT_MODAL: &'static str = "max_weight";
|
||||
|
||||
impl Default for PoolSetup {
|
||||
fn default() -> Self {
|
||||
@@ -62,20 +55,19 @@ impl Default for PoolSetup {
|
||||
pool_size_edit: NodeConfig::get_max_pool_size(),
|
||||
stempool_size_edit: NodeConfig::get_max_stempool_size(),
|
||||
max_weight_edit: NodeConfig::get_mineable_max_weight(),
|
||||
modal_ids: vec![
|
||||
FEE_BASE_MODAL,
|
||||
REORG_PERIOD_MODAL,
|
||||
POOL_SIZE_MODAL,
|
||||
STEMPOOL_SIZE_MODAL,
|
||||
MAX_WEIGHT_MODAL
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ModalContainer for PoolSetup {
|
||||
fn modal_ids(&self) -> &Vec<&'static str> {
|
||||
&self.modal_ids
|
||||
impl ContentContainer for PoolSetup {
|
||||
fn modal_ids(&self) -> Vec<&'static str> {
|
||||
vec![
|
||||
FEE_BASE_MODAL,
|
||||
REORG_PERIOD_MODAL,
|
||||
POOL_SIZE_MODAL,
|
||||
STEMPOOL_SIZE_MODAL,
|
||||
MAX_WEIGHT_MODAL
|
||||
]
|
||||
}
|
||||
|
||||
fn modal_ui(&mut self,
|
||||
@@ -91,53 +83,50 @@ impl ModalContainer for PoolSetup {
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PoolSetup {
|
||||
pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||
// Draw modal content for current ui container.
|
||||
self.current_modal_ui(ui, cb);
|
||||
|
||||
fn container_ui(&mut self, ui: &mut egui::Ui, _: &dyn PlatformCallbacks) {
|
||||
View::sub_title(ui, format!("{} {}", CHART_SCATTER, t!("network_settings.tx_pool")));
|
||||
View::horizontal_line(ui, Colors::stroke());
|
||||
ui.add_space(6.0);
|
||||
|
||||
ui.vertical_centered(|ui| {
|
||||
// Show base fee setup.
|
||||
self.fee_base_ui(ui, cb);
|
||||
self.fee_base_ui(ui);
|
||||
|
||||
ui.add_space(6.0);
|
||||
View::horizontal_line(ui, Colors::item_stroke());
|
||||
ui.add_space(6.0);
|
||||
|
||||
// Show reorg cache retention period setup.
|
||||
self.reorg_period_ui(ui, cb);
|
||||
self.reorg_period_ui(ui);
|
||||
|
||||
ui.add_space(6.0);
|
||||
View::horizontal_line(ui, Colors::item_stroke());
|
||||
ui.add_space(6.0);
|
||||
|
||||
// Show pool size setup.
|
||||
self.pool_size_ui(ui, cb);
|
||||
self.pool_size_ui(ui);
|
||||
|
||||
ui.add_space(6.0);
|
||||
View::horizontal_line(ui, Colors::item_stroke());
|
||||
ui.add_space(6.0);
|
||||
|
||||
// Show stem pool size setup.
|
||||
self.stem_size_ui(ui, cb);
|
||||
self.stem_size_ui(ui);
|
||||
|
||||
ui.add_space(6.0);
|
||||
View::horizontal_line(ui, Colors::item_stroke());
|
||||
ui.add_space(6.0);
|
||||
|
||||
// Show max weight of transactions setup.
|
||||
self.max_weight_ui(ui, cb);
|
||||
self.max_weight_ui(ui);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl PoolSetup {
|
||||
/// Draw fee base setup content.
|
||||
fn fee_base_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||
fn fee_base_ui(&mut self, ui: &mut egui::Ui) {
|
||||
ui.label(RichText::new(t!("network_settings.pool_fee"))
|
||||
.size(16.0)
|
||||
.color(Colors::gray())
|
||||
@@ -145,7 +134,7 @@ impl PoolSetup {
|
||||
ui.add_space(6.0);
|
||||
|
||||
let fee = NodeConfig::get_base_fee();
|
||||
View::button(ui, format!("{} {}", HAND_COINS, fee.clone()), Colors::button(), || {
|
||||
View::button(ui, format!("{} {}", HAND_COINS, &fee), Colors::white_or_black(false), || {
|
||||
// Setup values for modal.
|
||||
self.fee_base_edit = fee;
|
||||
// Show fee setup modal.
|
||||
@@ -153,13 +142,19 @@ impl PoolSetup {
|
||||
.position(ModalPosition::CenterTop)
|
||||
.title(t!("network_settings.change_value"))
|
||||
.show();
|
||||
cb.show_keyboard();
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
}
|
||||
|
||||
/// Draw fee base [`Modal`] content.
|
||||
fn fee_base_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
|
||||
let on_save = |c: &mut PoolSetup| {
|
||||
if let Ok(fee) = c.fee_base_edit.parse::<u64>() {
|
||||
NodeConfig::save_base_fee(fee);
|
||||
Modal::close();
|
||||
}
|
||||
};
|
||||
|
||||
ui.add_space(6.0);
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.label(RichText::new(t!("network_settings.pool_fee"))
|
||||
@@ -168,8 +163,11 @@ impl PoolSetup {
|
||||
ui.add_space(8.0);
|
||||
|
||||
// Draw fee base text edit.
|
||||
let mut fee_base_edit_opts = TextEditOptions::new(Id::from(modal.id)).h_center();
|
||||
View::text_edit(ui, cb, &mut self.fee_base_edit, &mut fee_base_edit_opts);
|
||||
let mut edit = TextEdit::new(Id::from(modal.id)).h_center().numeric();
|
||||
edit.ui(ui, &mut self.fee_base_edit, cb);
|
||||
if edit.enter_pressed {
|
||||
on_save(self);
|
||||
}
|
||||
|
||||
// Show error when specified value is not valid or reminder to restart enabled node.
|
||||
if self.fee_base_edit.parse::<u64>().is_err() {
|
||||
@@ -187,25 +185,17 @@ impl PoolSetup {
|
||||
// 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(fee) = self.fee_base_edit.parse::<u64>() {
|
||||
NodeConfig::save_base_fee(fee);
|
||||
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();
|
||||
Modal::close();
|
||||
});
|
||||
});
|
||||
columns[1].vertical_centered_justified(|ui| {
|
||||
View::button(ui, t!("modal.save"), Colors::white_or_black(false), on_save);
|
||||
View::button(ui, t!("modal.save"), Colors::white_or_black(false), || {
|
||||
on_save(self);
|
||||
});
|
||||
});
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
@@ -214,15 +204,16 @@ impl PoolSetup {
|
||||
}
|
||||
|
||||
/// Draw reorg cache retention period setup content.
|
||||
fn reorg_period_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||
fn reorg_period_ui(&mut self, ui: &mut egui::Ui) {
|
||||
ui.label(RichText::new(t!("network_settings.reorg_period"))
|
||||
.size(16.0)
|
||||
.color(Colors::gray())
|
||||
);
|
||||
ui.add_space(6.0);
|
||||
|
||||
let period = NodeConfig::get_reorg_cache_period();
|
||||
View::button(ui, format!("{} {}", CLOCK_COUNTDOWN, period.clone()), Colors::button(), || {
|
||||
View::button(ui,
|
||||
format!("{} {}", CLOCK_COUNTDOWN, &period),
|
||||
Colors::white_or_black(false), || {
|
||||
// Setup values for modal.
|
||||
self.reorg_period_edit = period;
|
||||
// Show reorg period setup modal.
|
||||
@@ -230,13 +221,19 @@ impl PoolSetup {
|
||||
.position(ModalPosition::CenterTop)
|
||||
.title(t!("network_settings.change_value"))
|
||||
.show();
|
||||
cb.show_keyboard();
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
}
|
||||
|
||||
/// Draw reorg cache retention period [`Modal`] content.
|
||||
fn reorg_period_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
|
||||
let on_save = |c: &mut PoolSetup| {
|
||||
if let Ok(period) = c.reorg_period_edit.parse::<u32>() {
|
||||
NodeConfig::save_reorg_cache_period(period);
|
||||
Modal::close();
|
||||
}
|
||||
};
|
||||
|
||||
ui.add_space(6.0);
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.label(RichText::new(t!("network_settings.reorg_period"))
|
||||
@@ -245,8 +242,11 @@ impl PoolSetup {
|
||||
ui.add_space(8.0);
|
||||
|
||||
// Draw reorg period text edit.
|
||||
let mut reorg_period_edit_opts = TextEditOptions::new(Id::from(modal.id)).h_center();
|
||||
View::text_edit(ui, cb, &mut self.reorg_period_edit, &mut reorg_period_edit_opts);
|
||||
let mut edit = TextEdit::new(Id::from(modal.id)).h_center().numeric();
|
||||
edit.ui(ui, &mut self.reorg_period_edit, cb);
|
||||
if edit.enter_pressed {
|
||||
on_save(self);
|
||||
}
|
||||
|
||||
// Show error when specified value is not valid or reminder to restart enabled node.
|
||||
if self.reorg_period_edit.parse::<u32>().is_err() {
|
||||
@@ -264,25 +264,17 @@ impl PoolSetup {
|
||||
// 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(period) = self.reorg_period_edit.parse::<u32>() {
|
||||
NodeConfig::save_reorg_cache_period(period);
|
||||
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();
|
||||
Modal::close();
|
||||
});
|
||||
});
|
||||
columns[1].vertical_centered_justified(|ui| {
|
||||
View::button(ui, t!("modal.save"), Colors::white_or_black(false), on_save);
|
||||
View::button(ui, t!("modal.save"), Colors::white_or_black(false), || {
|
||||
on_save(self);
|
||||
});
|
||||
});
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
@@ -291,7 +283,7 @@ impl PoolSetup {
|
||||
}
|
||||
|
||||
/// Draw maximum number of transactions in the pool setup content.
|
||||
fn pool_size_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||
fn pool_size_ui(&mut self, ui: &mut egui::Ui) {
|
||||
ui.label(RichText::new(t!("network_settings.max_tx_pool"))
|
||||
.size(16.0)
|
||||
.color(Colors::gray())
|
||||
@@ -299,7 +291,7 @@ impl PoolSetup {
|
||||
ui.add_space(6.0);
|
||||
|
||||
let size = NodeConfig::get_max_pool_size();
|
||||
View::button(ui, format!("{} {}", CIRCLES_THREE, size.clone()), Colors::button(), || {
|
||||
View::button(ui, format!("{} {}", CIRCLES_THREE, size), Colors::white_or_black(false), || {
|
||||
// Setup values for modal.
|
||||
self.pool_size_edit = size;
|
||||
// Show pool size setup modal.
|
||||
@@ -307,13 +299,19 @@ impl PoolSetup {
|
||||
.position(ModalPosition::CenterTop)
|
||||
.title(t!("network_settings.change_value"))
|
||||
.show();
|
||||
cb.show_keyboard();
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
}
|
||||
|
||||
/// Draw maximum number of transactions in the pool [`Modal`] content.
|
||||
fn pool_size_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
|
||||
let on_save = |c: &mut PoolSetup| {
|
||||
if let Ok(size) = c.pool_size_edit.parse::<usize>() {
|
||||
NodeConfig::save_max_pool_size(size);
|
||||
Modal::close();
|
||||
}
|
||||
};
|
||||
|
||||
ui.add_space(6.0);
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.label(RichText::new(t!("network_settings.max_tx_pool"))
|
||||
@@ -322,8 +320,11 @@ impl PoolSetup {
|
||||
ui.add_space(8.0);
|
||||
|
||||
// Draw pool size text edit.
|
||||
let mut pool_size_edit_opts = TextEditOptions::new(Id::from(modal.id)).h_center();
|
||||
View::text_edit(ui, cb, &mut self.pool_size_edit, &mut pool_size_edit_opts);
|
||||
let mut edit = TextEdit::new(Id::from(modal.id)).h_center().numeric();
|
||||
edit.ui(ui, &mut self.pool_size_edit, cb);
|
||||
if edit.enter_pressed {
|
||||
on_save(self);
|
||||
}
|
||||
|
||||
// Show error when specified value is not valid or reminder to restart enabled node.
|
||||
if self.pool_size_edit.parse::<usize>().is_err() {
|
||||
@@ -341,25 +342,17 @@ impl PoolSetup {
|
||||
// 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(size) = self.pool_size_edit.parse::<usize>() {
|
||||
NodeConfig::save_max_pool_size(size);
|
||||
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();
|
||||
Modal::close();
|
||||
});
|
||||
});
|
||||
columns[1].vertical_centered_justified(|ui| {
|
||||
View::button(ui, t!("modal.save"), Colors::white_or_black(false), on_save);
|
||||
View::button(ui, t!("modal.save"), Colors::white_or_black(false), || {
|
||||
on_save(self);
|
||||
});
|
||||
});
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
@@ -368,7 +361,7 @@ impl PoolSetup {
|
||||
}
|
||||
|
||||
/// Draw maximum number of transactions in the stempool setup content.
|
||||
fn stem_size_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||
fn stem_size_ui(&mut self, ui: &mut egui::Ui) {
|
||||
ui.label(RichText::new(t!("network_settings.max_tx_stempool"))
|
||||
.size(16.0)
|
||||
.color(Colors::gray())
|
||||
@@ -376,7 +369,9 @@ impl PoolSetup {
|
||||
ui.add_space(6.0);
|
||||
|
||||
let size = NodeConfig::get_max_stempool_size();
|
||||
View::button(ui, format!("{} {}", BEZIER_CURVE, size.clone()), Colors::button(), || {
|
||||
View::button(ui,
|
||||
format!("{} {}", BEZIER_CURVE, &size),
|
||||
Colors::white_or_black(false), || {
|
||||
// Setup values for modal.
|
||||
self.stempool_size_edit = size;
|
||||
// Show stempool size setup modal.
|
||||
@@ -384,13 +379,19 @@ impl PoolSetup {
|
||||
.position(ModalPosition::CenterTop)
|
||||
.title(t!("network_settings.change_value"))
|
||||
.show();
|
||||
cb.show_keyboard();
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
}
|
||||
|
||||
/// Draw maximum number of transactions in the stempool [`Modal`] content.
|
||||
fn stem_size_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
|
||||
let on_save = |c: &mut PoolSetup| {
|
||||
if let Ok(size) = c.stempool_size_edit.parse::<usize>() {
|
||||
NodeConfig::save_max_stempool_size(size);
|
||||
Modal::close();
|
||||
}
|
||||
};
|
||||
|
||||
ui.add_space(6.0);
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.label(RichText::new(t!("network_settings.max_tx_stempool"))
|
||||
@@ -399,8 +400,11 @@ impl PoolSetup {
|
||||
ui.add_space(8.0);
|
||||
|
||||
// Draw stempool size text edit.
|
||||
let mut stem_pool_edit_opts = TextEditOptions::new(Id::from(modal.id)).h_center();
|
||||
View::text_edit(ui, cb, &mut self.stempool_size_edit, &mut stem_pool_edit_opts);
|
||||
let mut edit = TextEdit::new(Id::from(modal.id)).h_center().numeric();
|
||||
edit.ui(ui, &mut self.stempool_size_edit, cb);
|
||||
if edit.enter_pressed {
|
||||
on_save(self);
|
||||
}
|
||||
|
||||
// Show error when specified value is not valid or reminder to restart enabled node.
|
||||
if self.stempool_size_edit.parse::<usize>().is_err() {
|
||||
@@ -418,25 +422,17 @@ impl PoolSetup {
|
||||
// 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(size) = self.stempool_size_edit.parse::<usize>() {
|
||||
NodeConfig::save_max_stempool_size(size);
|
||||
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();
|
||||
Modal::close();
|
||||
});
|
||||
});
|
||||
columns[1].vertical_centered_justified(|ui| {
|
||||
View::button(ui, t!("modal.save"), Colors::white_or_black(false), on_save);
|
||||
View::button(ui, t!("modal.save"), Colors::white_or_black(false), || {
|
||||
on_save(self);
|
||||
});
|
||||
});
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
@@ -445,7 +441,7 @@ impl PoolSetup {
|
||||
}
|
||||
|
||||
/// Draw maximum total weight of transactions setup content.
|
||||
fn max_weight_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||
fn max_weight_ui(&mut self, ui: &mut egui::Ui) {
|
||||
ui.label(RichText::new(t!("network_settings.max_tx_weight"))
|
||||
.size(16.0)
|
||||
.color(Colors::gray())
|
||||
@@ -453,7 +449,9 @@ impl PoolSetup {
|
||||
ui.add_space(6.0);
|
||||
|
||||
let weight = NodeConfig::get_mineable_max_weight();
|
||||
View::button(ui, format!("{} {}", BOUNDING_BOX, weight.clone()), Colors::button(), || {
|
||||
View::button(ui,
|
||||
format!("{} {}", BOUNDING_BOX, &weight),
|
||||
Colors::white_or_black(false), || {
|
||||
// Setup values for modal.
|
||||
self.max_weight_edit = weight;
|
||||
// Show total tx weight setup modal.
|
||||
@@ -461,13 +459,19 @@ impl PoolSetup {
|
||||
.position(ModalPosition::CenterTop)
|
||||
.title(t!("network_settings.change_value"))
|
||||
.show();
|
||||
cb.show_keyboard();
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
}
|
||||
|
||||
/// Draw maximum total weight of transactions [`Modal`] content.
|
||||
fn max_weight_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
|
||||
let on_save = |c: &mut PoolSetup| {
|
||||
if let Ok(weight) = c.max_weight_edit.parse::<u64>() {
|
||||
NodeConfig::save_mineable_max_weight(weight);
|
||||
Modal::close();
|
||||
}
|
||||
};
|
||||
|
||||
ui.add_space(6.0);
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.label(RichText::new(t!("network_settings.max_tx_weight"))
|
||||
@@ -476,8 +480,11 @@ impl PoolSetup {
|
||||
ui.add_space(8.0);
|
||||
|
||||
// Draw tx weight text edit.
|
||||
let mut mac_weight_edit_opts = TextEditOptions::new(Id::from(modal.id)).h_center();
|
||||
View::text_edit(ui, cb, &mut self.max_weight_edit, &mut mac_weight_edit_opts);
|
||||
let mut edit = TextEdit::new(Id::from(modal.id)).h_center().numeric();
|
||||
edit.ui(ui, &mut self.max_weight_edit, cb);
|
||||
if edit.enter_pressed {
|
||||
on_save(self);
|
||||
}
|
||||
|
||||
// Show error when specified value is not valid or reminder to restart enabled node.
|
||||
if self.max_weight_edit.parse::<u64>().is_err() {
|
||||
@@ -495,25 +502,17 @@ impl PoolSetup {
|
||||
// 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(weight) = self.max_weight_edit.parse::<u64>() {
|
||||
NodeConfig::save_mineable_max_weight(weight);
|
||||
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();
|
||||
Modal::close();
|
||||
});
|
||||
});
|
||||
columns[1].vertical_centered_justified(|ui| {
|
||||
View::button(ui, t!("modal.save"), Colors::white_or_black(false), on_save);
|
||||
View::button(ui, t!("modal.save"), Colors::white_or_black(false), || {
|
||||
on_save(self);
|
||||
});
|
||||
});
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
|
||||
@@ -18,10 +18,10 @@ use grin_chain::SyncStatus;
|
||||
use crate::gui::Colors;
|
||||
use crate::gui::icons::{BARBELL, HARD_DRIVES, PLUG, POWER, TIMER};
|
||||
use crate::gui::platform::PlatformCallbacks;
|
||||
use crate::gui::views::{Modal, View};
|
||||
use crate::gui::views::{Modal, TextEdit, View};
|
||||
use crate::gui::views::network::settings::NetworkSettings;
|
||||
use crate::gui::views::types::{ModalContainer, ModalPosition, TextEditOptions};
|
||||
use crate::gui::views::wallets::modals::WalletsModal;
|
||||
use crate::gui::views::types::{ContentContainer, ModalPosition};
|
||||
use crate::gui::views::wallets::modals::WalletListModal;
|
||||
use crate::node::{Node, NodeConfig};
|
||||
use crate::wallet::{WalletConfig, WalletList};
|
||||
|
||||
@@ -30,7 +30,7 @@ pub struct StratumSetup {
|
||||
/// Wallet list to select for mining rewards.
|
||||
wallets: WalletList,
|
||||
/// Wallets [`Modal`] content.
|
||||
wallets_modal: WalletsModal,
|
||||
wallets_modal: WalletListModal,
|
||||
|
||||
/// IP Addresses available at system.
|
||||
available_ips: Vec<String>,
|
||||
@@ -44,16 +44,13 @@ pub struct StratumSetup {
|
||||
is_port_available: bool,
|
||||
|
||||
/// Wallet name to receive rewards.
|
||||
wallet_name: Option<String>,
|
||||
pub wallet_name: Option<String>,
|
||||
|
||||
/// Attempt time value in seconds to mine on a particular header.
|
||||
attempt_time_edit: String,
|
||||
|
||||
/// Minimum share difficulty value to request from miners.
|
||||
min_share_diff_edit: String,
|
||||
|
||||
/// [`Modal`] identifiers allowed at this ui container.
|
||||
modal_ids: Vec<&'static str>
|
||||
}
|
||||
|
||||
/// Identifier for wallet selection [`Modal`].
|
||||
@@ -73,7 +70,7 @@ impl Default for StratumSetup {
|
||||
// Setup mining rewards wallet name and identifier.
|
||||
let mut wallet_id = NodeConfig::get_stratum_wallet_id();
|
||||
let wallet_name = if let Some(id) = wallet_id {
|
||||
WalletConfig::name_by_id(id)
|
||||
WalletConfig::read_name_by_id(id)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
@@ -83,7 +80,7 @@ impl Default for StratumSetup {
|
||||
|
||||
Self {
|
||||
wallets: WalletList::default(),
|
||||
wallets_modal: WalletsModal::new(wallet_id),
|
||||
wallets_modal: WalletListModal::new(wallet_id, None, false),
|
||||
available_ips: NodeConfig::get_ip_addrs(),
|
||||
stratum_port_edit: port,
|
||||
stratum_port_available_edit: is_port_available,
|
||||
@@ -91,19 +88,18 @@ impl Default for StratumSetup {
|
||||
wallet_name,
|
||||
attempt_time_edit: NodeConfig::get_stratum_attempt_time(),
|
||||
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 {
|
||||
fn modal_ids(&self) -> &Vec<&'static str> {
|
||||
&self.modal_ids
|
||||
impl ContentContainer for StratumSetup {
|
||||
fn modal_ids(&self) -> Vec<&'static str> {
|
||||
vec![
|
||||
WALLET_SELECTION_MODAL,
|
||||
STRATUM_PORT_MODAL,
|
||||
ATTEMPT_TIME_MODAL,
|
||||
MIN_SHARE_DIFF_MODAL
|
||||
]
|
||||
}
|
||||
|
||||
fn modal_ui(&mut self,
|
||||
@@ -111,23 +107,21 @@ impl ModalContainer for StratumSetup {
|
||||
modal: &Modal,
|
||||
cb: &dyn PlatformCallbacks) {
|
||||
match modal.id {
|
||||
WALLET_SELECTION_MODAL => self.wallets_modal.ui(ui, modal, &self.wallets, |id| {
|
||||
NodeConfig::save_stratum_wallet_id(id);
|
||||
self.wallet_name = WalletConfig::name_by_id(id);
|
||||
}),
|
||||
WALLET_SELECTION_MODAL => {
|
||||
self.wallets_modal.ui(ui, &mut self.wallets, |wallet, _| {
|
||||
let id = wallet.get_config().id;
|
||||
NodeConfig::save_stratum_wallet_id(id);
|
||||
self.wallet_name = WalletConfig::read_name_by_id(id);
|
||||
})
|
||||
},
|
||||
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),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl StratumSetup {
|
||||
pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||
// Draw modal content for current ui container.
|
||||
self.current_modal_ui(ui, cb);
|
||||
|
||||
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);
|
||||
@@ -186,9 +180,11 @@ impl StratumSetup {
|
||||
ui.add_space(8.0);
|
||||
|
||||
// Show button to select wallet.
|
||||
View::button(ui, t!("network_settings.choose_wallet"), Colors::button(), || {
|
||||
self.show_wallets_modal();
|
||||
});
|
||||
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() {
|
||||
@@ -222,25 +218,27 @@ impl StratumSetup {
|
||||
|
||||
});
|
||||
// Show stratum port setup.
|
||||
self.port_setup_ui(ui, cb);
|
||||
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, cb);
|
||||
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, cb);
|
||||
self.min_diff_ui(ui);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl StratumSetup {
|
||||
/// Show wallet selection [`Modal`].
|
||||
fn show_wallets_modal(&mut self) {
|
||||
self.wallets_modal = WalletsModal::new(NodeConfig::get_stratum_wallet_id());
|
||||
self.wallets_modal = WalletListModal::new(NodeConfig::get_stratum_wallet_id(), None, false);
|
||||
// Show modal.
|
||||
Modal::new(WALLET_SELECTION_MODAL)
|
||||
.position(ModalPosition::Center)
|
||||
@@ -249,7 +247,7 @@ impl StratumSetup {
|
||||
}
|
||||
|
||||
/// Draw stratum port value setup content.
|
||||
fn port_setup_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||
fn port_setup_ui(&mut self, ui: &mut egui::Ui) {
|
||||
ui.label(RichText::new(t!("network_settings.stratum_port"))
|
||||
.size(16.0)
|
||||
.color(Colors::gray())
|
||||
@@ -257,7 +255,7 @@ impl StratumSetup {
|
||||
ui.add_space(6.0);
|
||||
|
||||
let (_, port) = NodeConfig::get_stratum_address();
|
||||
View::button(ui, format!("{} {}", PLUG, port.clone()), Colors::button(), || {
|
||||
View::button(ui, format!("{} {}", PLUG, &port), Colors::white_or_black(false), || {
|
||||
// Setup values for modal.
|
||||
self.stratum_port_edit = port;
|
||||
self.stratum_port_available_edit = self.is_port_available;
|
||||
@@ -266,7 +264,6 @@ impl StratumSetup {
|
||||
.position(ModalPosition::CenterTop)
|
||||
.title(t!("network_settings.change_value"))
|
||||
.show();
|
||||
cb.show_keyboard();
|
||||
});
|
||||
ui.add_space(12.0);
|
||||
|
||||
@@ -282,6 +279,24 @@ impl StratumSetup {
|
||||
|
||||
/// Draw stratum port [`Modal`] content.
|
||||
fn port_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
|
||||
let on_save = |c: &mut StratumSetup| {
|
||||
// Check if port is available.
|
||||
let (stratum_ip, _) = NodeConfig::get_stratum_address();
|
||||
let available = NodeConfig::is_stratum_port_available(
|
||||
&stratum_ip,
|
||||
&c.stratum_port_edit
|
||||
);
|
||||
c.stratum_port_available_edit = available;
|
||||
|
||||
// Save port at config if it's available.
|
||||
if available {
|
||||
NodeConfig::save_stratum_address(&stratum_ip, &c.stratum_port_edit);
|
||||
|
||||
c.is_port_available = true;
|
||||
Modal::close();
|
||||
}
|
||||
};
|
||||
|
||||
ui.add_space(6.0);
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.label(RichText::new(t!("network_settings.stratum_port"))
|
||||
@@ -290,8 +305,11 @@ impl StratumSetup {
|
||||
ui.add_space(8.0);
|
||||
|
||||
// Draw stratum port text edit.
|
||||
let mut text_edit_opts = TextEditOptions::new(Id::from(modal.id)).h_center();
|
||||
View::text_edit(ui, cb, &mut self.stratum_port_edit, &mut text_edit_opts);
|
||||
let mut edit = TextEdit::new(Id::from(modal.id)).h_center().numeric();
|
||||
edit.ui(ui, &mut self.stratum_port_edit, cb);
|
||||
if edit.enter_pressed {
|
||||
on_save(self);
|
||||
}
|
||||
|
||||
// Show error when specified port is unavailable.
|
||||
if !self.stratum_port_available_edit {
|
||||
@@ -310,36 +328,17 @@ impl StratumSetup {
|
||||
// Setup spacing between buttons.
|
||||
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
|
||||
|
||||
// Save button callback.
|
||||
let on_save = || {
|
||||
// Check if port is available.
|
||||
let (stratum_ip, _) = NodeConfig::get_stratum_address();
|
||||
let available = NodeConfig::is_stratum_port_available(
|
||||
&stratum_ip,
|
||||
&self.stratum_port_edit
|
||||
);
|
||||
self.stratum_port_available_edit = available;
|
||||
|
||||
// Save port at config if it's available.
|
||||
if available {
|
||||
NodeConfig::save_stratum_address(&stratum_ip, &self.stratum_port_edit);
|
||||
|
||||
self.is_port_available = true;
|
||||
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();
|
||||
Modal::close();
|
||||
});
|
||||
});
|
||||
columns[1].vertical_centered_justified(|ui| {
|
||||
View::button(ui, t!("modal.save"), Colors::white_or_black(false), on_save);
|
||||
View::button(ui, t!("modal.save"), Colors::white_or_black(false), || {
|
||||
on_save(self);
|
||||
});
|
||||
});
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
@@ -348,7 +347,7 @@ impl StratumSetup {
|
||||
}
|
||||
|
||||
/// Draw attempt time value setup content.
|
||||
fn attempt_time_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||
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())
|
||||
@@ -356,7 +355,7 @@ impl StratumSetup {
|
||||
ui.add_space(6.0);
|
||||
|
||||
let time = NodeConfig::get_stratum_attempt_time();
|
||||
View::button(ui, format!("{} {}", TIMER, time.clone()), Colors::button(), || {
|
||||
View::button(ui, format!("{} {}", TIMER, &time), Colors::white_or_black(false), || {
|
||||
// Setup values for modal.
|
||||
self.attempt_time_edit = time;
|
||||
|
||||
@@ -365,7 +364,6 @@ impl StratumSetup {
|
||||
.position(ModalPosition::CenterTop)
|
||||
.title(t!("network_settings.change_value"))
|
||||
.show();
|
||||
cb.show_keyboard();
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
ui.label(RichText::new(t!("network_settings.attempt_time_desc"))
|
||||
@@ -377,6 +375,13 @@ impl StratumSetup {
|
||||
|
||||
/// Draw attempt time [`Modal`] content.
|
||||
fn attempt_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
|
||||
let on_save = |c: &mut StratumSetup| {
|
||||
if let Ok(time) = c.attempt_time_edit.parse::<u32>() {
|
||||
NodeConfig::save_stratum_attempt_time(time);
|
||||
Modal::close();
|
||||
}
|
||||
};
|
||||
|
||||
ui.add_space(6.0);
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.label(RichText::new(t!("network_settings.attempt_time"))
|
||||
@@ -385,8 +390,11 @@ impl StratumSetup {
|
||||
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);
|
||||
let mut edit = TextEdit::new(Id::from(modal.id)).h_center().numeric();
|
||||
edit.ui(ui, &mut self.attempt_time_edit, cb);
|
||||
if edit.enter_pressed {
|
||||
on_save(self);
|
||||
}
|
||||
|
||||
// Show error when specified value is not valid or reminder to restart enabled node.
|
||||
if self.attempt_time_edit.parse::<u32>().is_err() {
|
||||
@@ -405,25 +413,17 @@ impl StratumSetup {
|
||||
// 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();
|
||||
Modal::close();
|
||||
});
|
||||
});
|
||||
columns[1].vertical_centered_justified(|ui| {
|
||||
View::button(ui, t!("modal.save"), Colors::white_or_black(false), on_save);
|
||||
View::button(ui, t!("modal.save"), Colors::white_or_black(false), || {
|
||||
on_save(self);
|
||||
});
|
||||
});
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
@@ -431,7 +431,7 @@ impl StratumSetup {
|
||||
}
|
||||
|
||||
/// Draw minimum share difficulty value setup content.
|
||||
fn min_diff_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||
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())
|
||||
@@ -439,7 +439,7 @@ impl StratumSetup {
|
||||
ui.add_space(6.0);
|
||||
|
||||
let diff = NodeConfig::get_stratum_min_share_diff();
|
||||
View::button(ui, format!("{} {}", BARBELL, diff.clone()), Colors::button(), || {
|
||||
View::button(ui, format!("{} {}", BARBELL, &diff), Colors::white_or_black(false), || {
|
||||
// Setup values for modal.
|
||||
self.min_share_diff_edit = diff;
|
||||
|
||||
@@ -448,13 +448,19 @@ impl StratumSetup {
|
||||
.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) {
|
||||
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();
|
||||
}
|
||||
};
|
||||
|
||||
ui.add_space(6.0);
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.label(RichText::new(t!("network_settings.min_share_diff"))
|
||||
@@ -463,8 +469,11 @@ impl StratumSetup {
|
||||
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);
|
||||
let mut edit = TextEdit::new(Id::from(modal.id)).h_center().numeric();
|
||||
edit.ui(ui, &mut self.min_share_diff_edit, cb);
|
||||
if edit.enter_pressed {
|
||||
on_save(self);
|
||||
}
|
||||
|
||||
// Show error when specified value is not valid or reminder to restart enabled node.
|
||||
if self.min_share_diff_edit.parse::<u64>().is_err() {
|
||||
@@ -483,25 +492,17 @@ impl StratumSetup {
|
||||
// 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();
|
||||
Modal::close();
|
||||
});
|
||||
});
|
||||
columns[1].vertical_centered_justified(|ui| {
|
||||
View::button(ui, t!("modal.save"), Colors::white_or_black(false), on_save);
|
||||
View::button(ui, t!("modal.save"), Colors::white_or_black(false), || {
|
||||
on_save(self);
|
||||
});
|
||||
});
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
|
||||
@@ -14,28 +14,28 @@
|
||||
|
||||
use crate::gui::platform::PlatformCallbacks;
|
||||
|
||||
/// Network tab content interface.
|
||||
pub trait NetworkTab {
|
||||
fn get_type(&self) -> NetworkTabType;
|
||||
fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks);
|
||||
/// Integrated node tab content interface.
|
||||
pub trait NodeTab {
|
||||
fn get_type(&self) -> NodeTabType;
|
||||
fn tab_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks);
|
||||
}
|
||||
|
||||
/// Type of [`NetworkTab`] content.
|
||||
/// Type of [`NodeTab`] content.
|
||||
#[derive(PartialEq)]
|
||||
pub enum NetworkTabType {
|
||||
Node,
|
||||
pub enum NodeTabType {
|
||||
Info,
|
||||
Metrics,
|
||||
Mining,
|
||||
Settings
|
||||
}
|
||||
|
||||
impl NetworkTabType {
|
||||
impl NodeTabType {
|
||||
pub fn title(&self) -> String {
|
||||
match *self {
|
||||
NetworkTabType::Node => { t!("network.node") }
|
||||
NetworkTabType::Metrics => { t!("network.metrics") }
|
||||
NetworkTabType::Mining => { t!("network.mining") }
|
||||
NetworkTabType::Settings => { t!("network.settings") }
|
||||
NodeTabType::Info => t!("network.node").into(),
|
||||
NodeTabType::Metrics => t!("network.metrics").into(),
|
||||
NodeTabType::Mining => t!("network.mining").into(),
|
||||
NodeTabType::Settings => t!("network.settings").into()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
|
||||
use egui::scroll_area::ScrollAreaOutput;
|
||||
use egui::{Sense, Align2, Area, Color32, Id, Rect, Response, Widget, Vec2};
|
||||
use egui::{Sense, Align2, Area, Color32, Id, Rect, Response, Widget, Vec2, UiBuilder};
|
||||
use egui::epaint::{emath::lerp, vec2, Pos2, Shape, Stroke};
|
||||
|
||||
/// A spinner widget used to indicate loading.
|
||||
@@ -195,7 +195,9 @@ impl PullToRefresh {
|
||||
ui: &mut egui::Ui,
|
||||
content: impl FnOnce(&mut egui::Ui) -> T,
|
||||
) -> PullToRefreshResponse<T> {
|
||||
let mut child = ui.child_ui(ui.available_rect_before_wrap(), *ui.layout(), None);
|
||||
let mut child = ui.new_child(UiBuilder::new()
|
||||
.max_rect(ui.available_rect_before_wrap())
|
||||
.layout(*ui.layout()));
|
||||
|
||||
let output = content(&mut child);
|
||||
|
||||
|
||||
+130
-100
@@ -12,26 +12,29 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// 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::sync::Arc;
|
||||
use parking_lot::RwLock;
|
||||
use std::thread;
|
||||
use egui::{SizeHint, TextureHandle};
|
||||
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::icons::IMAGES_SQUARE;
|
||||
use crate::gui::icons::{COPY, IMAGES_SQUARE};
|
||||
use crate::gui::platform::PlatformCallbacks;
|
||||
use crate::gui::views::types::QrImageState;
|
||||
use crate::gui::views::View;
|
||||
use crate::gui::Colors;
|
||||
|
||||
/// QR code image from text.
|
||||
pub struct QrCodeContent {
|
||||
/// Text to create QR code.
|
||||
pub(crate) text: String,
|
||||
/// QR code text.
|
||||
pub text: String,
|
||||
|
||||
/// Maximum QR code size.
|
||||
max_size: f32,
|
||||
|
||||
/// Flag to draw animated QR with Uniform Resources
|
||||
/// https://github.com/BlockchainCommons/Research/blob/master/papers/bcr-2020-005-ur.md
|
||||
@@ -53,6 +56,7 @@ impl QrCodeContent {
|
||||
pub fn new(text: String, animated: bool) -> Self {
|
||||
Self {
|
||||
text,
|
||||
max_size: DEFAULT_QR_SIZE as f32,
|
||||
animated,
|
||||
animated_index: None,
|
||||
animation_time: None,
|
||||
@@ -61,19 +65,28 @@ impl QrCodeContent {
|
||||
}
|
||||
}
|
||||
|
||||
/// Setup maximum QR code size.
|
||||
pub fn with_max_size(mut self, max_size: f32) -> Self {
|
||||
self.max_size = max_size;
|
||||
self
|
||||
}
|
||||
|
||||
/// Draw QR code.
|
||||
pub fn ui(&mut self, ui: &mut egui::Ui, text: String, cb: &dyn PlatformCallbacks) {
|
||||
pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||
if self.animated {
|
||||
// Show animated QR code.
|
||||
self.animated_ui(ui, text, cb);
|
||||
self.animated_ui(ui, cb);
|
||||
} else {
|
||||
// Show static QR code.
|
||||
self.static_ui(ui, text, cb);
|
||||
self.static_ui(ui, cb);
|
||||
}
|
||||
ui.add_space(8.0);
|
||||
View::horizontal_line(ui, Colors::item_stroke());
|
||||
ui.add_space(8.0);
|
||||
}
|
||||
|
||||
/// Draw animated QR code content.
|
||||
fn animated_ui(&mut self, ui: &mut egui::Ui, text: String, cb: &dyn PlatformCallbacks) {
|
||||
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| {
|
||||
@@ -84,7 +97,7 @@ impl QrCodeContent {
|
||||
|
||||
// Create multiple vector images from text if not creating.
|
||||
if !self.loading() {
|
||||
self.create_svg_list(text);
|
||||
self.create_svg_list();
|
||||
}
|
||||
} else {
|
||||
let svg_list = {
|
||||
@@ -110,9 +123,7 @@ impl QrCodeContent {
|
||||
self.qr_image_ui(svg, ui);
|
||||
|
||||
// Show QR code text.
|
||||
ui.add_space(6.0);
|
||||
View::ellipsize_text(ui, text.clone(), 16.0, Colors::inactive_text());
|
||||
ui.add_space(6.0);
|
||||
self.text_ui(ui);
|
||||
|
||||
ui.vertical_centered(|ui| {
|
||||
let sharing = {
|
||||
@@ -120,18 +131,20 @@ impl QrCodeContent {
|
||||
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(text, DEFAULT_QR_SIZE as usize);
|
||||
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| {
|
||||
@@ -141,10 +154,6 @@ impl QrCodeContent {
|
||||
});
|
||||
}
|
||||
|
||||
ui.add_space(8.0);
|
||||
View::horizontal_line(ui, Colors::item_stroke());
|
||||
ui.add_space(8.0);
|
||||
|
||||
// Check if GIF was created to share.
|
||||
let has_gif = {
|
||||
let r_state = self.qr_image_state.read();
|
||||
@@ -171,7 +180,7 @@ impl QrCodeContent {
|
||||
}
|
||||
|
||||
/// Draw static QR code content.
|
||||
fn static_ui(&mut self, ui: &mut egui::Ui, text: String, cb: &dyn PlatformCallbacks) {
|
||||
fn static_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| {
|
||||
@@ -182,7 +191,7 @@ impl QrCodeContent {
|
||||
|
||||
// Create vector image from text if not creating.
|
||||
if !self.loading() {
|
||||
self.create_svg(text);
|
||||
self.create_svg();
|
||||
}
|
||||
} else {
|
||||
// Create image from SVG data.
|
||||
@@ -193,71 +202,95 @@ impl QrCodeContent {
|
||||
self.qr_image_ui(svg, ui);
|
||||
|
||||
// Show QR code text.
|
||||
ui.add_space(6.0);
|
||||
View::ellipsize_text(ui, text.clone(), 16.0, Colors::inactive_text());
|
||||
ui.add_space(6.0);
|
||||
self.text_ui(ui);
|
||||
|
||||
// Show button to share QR.
|
||||
ui.vertical_centered(|ui| {
|
||||
let share_text = format!("{} {}", IMAGES_SQUARE, t!("share"));
|
||||
View::colored_text_button(ui,
|
||||
share_text,
|
||||
Colors::blue(),
|
||||
Colors::white_or_black(false), || {
|
||||
if let Ok(qr) = QrCode::encode_text(text.as_str(), qrcodegen::QrCodeEcc::Low) {
|
||||
if let Some(data) = Self::qr_to_image_data(qr, DEFAULT_QR_SIZE as usize) {
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
// Setup spacing between buttons.
|
||||
ui.spacing_mut().item_spacing = egui::Vec2::new(6.0, 0.0);
|
||||
|
||||
ui.columns(2, |columns| {
|
||||
columns[0].vertical_centered_justified(|ui| {
|
||||
// Draw copy button.
|
||||
let copy_text = format!("{} {}", COPY, t!("copy"));
|
||||
View::button(ui, copy_text, Colors::white_or_black(false), || {
|
||||
cb.copy_string_to_buffer(self.text.clone());
|
||||
});
|
||||
});
|
||||
columns[1].vertical_centered_justified(|ui| {
|
||||
let share_text = format!("{} {}", IMAGES_SQUARE, t!("share"));
|
||||
View::colored_text_button(ui,
|
||||
share_text,
|
||||
Colors::blue(),
|
||||
Colors::white_or_black(false), || {
|
||||
self.share_static(cb);
|
||||
});
|
||||
});
|
||||
});
|
||||
ui.add_space(8.0);
|
||||
View::horizontal_line(ui, Colors::item_stroke());
|
||||
ui.add_space(8.0);
|
||||
}
|
||||
}
|
||||
|
||||
/// Share static QR code image.
|
||||
fn share_static(&self, cb: &dyn PlatformCallbacks) {
|
||||
let text = self.text.as_str();
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw QR code image content.
|
||||
fn qr_image_ui(&mut self, svg: Vec<u8>, ui: &mut egui::Ui) {
|
||||
let mut rect = ui.available_rect_before_wrap();
|
||||
rect.min += egui::emath::vec2(10.0, 0.0);
|
||||
rect.max -= egui::emath::vec2(10.0, 0.0);
|
||||
View::max_width_ui(ui, self.max_size, |ui| {
|
||||
let mut rect = ui.available_rect_before_wrap();
|
||||
rect.min += egui::emath::vec2(10.0, 0.0);
|
||||
rect.max -= egui::emath::vec2(10.0, 0.0);
|
||||
|
||||
// Create background shape.
|
||||
let mut bg_shape = RectShape {
|
||||
rect,
|
||||
rounding: egui::Rounding::default(),
|
||||
fill: egui::Color32::WHITE,
|
||||
stroke: egui::Stroke::NONE,
|
||||
blur_width: 0.0,
|
||||
fill_texture_id: Default::default(),
|
||||
uv: egui::Rect::ZERO
|
||||
};
|
||||
let bg_idx = ui.painter().add(bg_shape);
|
||||
// Create background shape.
|
||||
let mut bg_shape = RectShape::new(
|
||||
rect,
|
||||
egui::CornerRadius::default(),
|
||||
egui::Color32::WHITE,
|
||||
egui::Stroke::NONE,
|
||||
egui::StrokeKind::Outside
|
||||
);
|
||||
let bg_idx = ui.painter().add(bg_shape.clone());
|
||||
|
||||
// Draw QR code image content.
|
||||
let mut content_rect = ui.allocate_ui_at_rect(rect, |ui| {
|
||||
ui.add_space(10.0);
|
||||
let size = SizeHint::Size(ui.available_width() as u32, ui.available_width() as u32);
|
||||
self.texture_handle = Some(View::svg_image(ui, "qr_code", svg.as_slice(), Some(size)));
|
||||
ui.add_space(10.0);
|
||||
}).response.rect;
|
||||
// Draw QR code image.
|
||||
let mut content_rect = ui.scope_builder(UiBuilder::new().max_rect(rect), |ui| {
|
||||
ui.add_space(10.0);
|
||||
let size = SizeHint::Size {
|
||||
width: ui.available_width() as u32,
|
||||
height: ui.available_width() as u32,
|
||||
maintain_aspect_ratio: true,
|
||||
};
|
||||
self.texture_handle = Some(View::svg_image(ui, "qr", svg.as_slice(), size));
|
||||
ui.add_space(10.0);
|
||||
}).response.rect;
|
||||
|
||||
// Setup background shape to be painted behind content.
|
||||
content_rect.min -= egui::emath::vec2(10.0, 0.0);
|
||||
content_rect.max += egui::emath::vec2(10.0, 0.0);
|
||||
bg_shape.rect = content_rect;
|
||||
ui.painter().set(bg_idx, bg_shape);
|
||||
// Setup background size.
|
||||
content_rect.min -= egui::emath::vec2(10.0, 0.0);
|
||||
content_rect.max += egui::emath::vec2(10.0, 0.0);
|
||||
bg_shape.rect = content_rect;
|
||||
ui.painter().set(bg_idx, bg_shape);
|
||||
});
|
||||
}
|
||||
|
||||
/// Draw QR code text.
|
||||
fn text_ui(&self, ui: &mut egui::Ui) {
|
||||
ui.add_space(6.0);
|
||||
View::ellipsize_text(ui, self.text.clone(), 15.0, Colors::inactive_text());
|
||||
ui.add_space(6.0);
|
||||
}
|
||||
|
||||
/// Check if QR code is loading.
|
||||
@@ -267,8 +300,9 @@ impl QrCodeContent {
|
||||
}
|
||||
|
||||
/// Create multiple vector QR code images at separate thread.
|
||||
fn create_svg_list(&self, text: String) {
|
||||
fn create_svg_list(&self) {
|
||||
let qr_state = self.qr_image_state.clone();
|
||||
let text = self.text.clone();
|
||||
thread::spawn(move || {
|
||||
let mut encoder = ur::Encoder::bytes(text.as_bytes(), 100).unwrap();
|
||||
let mut data = Vec::with_capacity(encoder.fragment_count());
|
||||
@@ -294,8 +328,9 @@ impl QrCodeContent {
|
||||
}
|
||||
|
||||
/// Create vector QR code image at separate thread.
|
||||
fn create_svg(&self, text: String) {
|
||||
fn create_svg(&self) {
|
||||
let qr_state = self.qr_image_state.clone();
|
||||
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);
|
||||
@@ -332,13 +367,14 @@ impl QrCodeContent {
|
||||
}
|
||||
|
||||
/// Create GIF image at separate thread.
|
||||
fn create_qr_gif(&self, text: String, size: usize) {
|
||||
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![];
|
||||
@@ -354,7 +390,7 @@ impl QrCodeContent {
|
||||
) {
|
||||
// Create an image from QR data.
|
||||
let image = qr.render()
|
||||
.max_dimensions(size as u32, size as u32)
|
||||
.max_dimensions(DEFAULT_QR_SIZE, DEFAULT_QR_SIZE)
|
||||
.dark_color(image::Rgb([0, 0, 0]))
|
||||
.light_color(image::Rgb([255, 255, 255]))
|
||||
.build();
|
||||
@@ -428,10 +464,4 @@ impl QrCodeContent {
|
||||
}
|
||||
Some(img_raw)
|
||||
}
|
||||
|
||||
/// Reset QR code image content state to default.
|
||||
pub fn clear_state(&mut self) {
|
||||
let mut w_create = self.qr_image_state.write();
|
||||
*w_create = QrImageState::default();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
// Copyright 2024 The Grim Developers
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use egui::scroll_area::ScrollBarVisibility;
|
||||
use egui::{Id, ScrollArea};
|
||||
|
||||
use crate::gui::Colors;
|
||||
use crate::gui::icons::COPY;
|
||||
use crate::gui::platform::PlatformCallbacks;
|
||||
use crate::gui::views::{CameraContent, Modal, View};
|
||||
use crate::gui::views::types::QrScanResult;
|
||||
|
||||
/// QR code scanning content.
|
||||
pub struct CameraScanContent {
|
||||
/// Camera content.
|
||||
camera_content: Option<CameraContent>,
|
||||
/// Scan result.
|
||||
qr_scan_result: Option<QrScanResult>,
|
||||
}
|
||||
|
||||
impl Default for CameraScanContent {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
camera_content: Some(CameraContent::default()),
|
||||
qr_scan_result: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CameraScanContent {
|
||||
/// Draw [`Modal`] content.
|
||||
pub fn modal_ui(&mut self,
|
||||
ui: &mut egui::Ui,
|
||||
cb: &dyn PlatformCallbacks,
|
||||
mut on_result: impl FnMut(&QrScanResult)) {
|
||||
// Show scan result if exists or show camera content while scanning.
|
||||
if let Some(result) = &self.qr_scan_result.clone() {
|
||||
Self::result_ui(ui, result, cb, || {
|
||||
Modal::close();
|
||||
}, || {
|
||||
self.qr_scan_result = None;
|
||||
cb.start_camera();
|
||||
Modal::set_title(t!("scan_qr"));
|
||||
});
|
||||
} else if let Some(camera_content) = self.camera_content.as_mut() {
|
||||
if let Some(result) = camera_content.qr_scan_result() {
|
||||
cb.stop_camera();
|
||||
self.camera_content = None;
|
||||
on_result(&result);
|
||||
|
||||
// Set result and rename modal title.
|
||||
self.qr_scan_result = Some(result);
|
||||
Modal::set_title(t!("scan_result"));
|
||||
} else {
|
||||
// Draw camera content.
|
||||
ui.add_space(6.0);
|
||||
self.camera_content.as_mut().unwrap().ui(ui, cb);
|
||||
ui.add_space(12.0);
|
||||
ui.vertical_centered_justified(|ui| {
|
||||
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
|
||||
cb.stop_camera();
|
||||
self.camera_content = None;
|
||||
Modal::close();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
ui.add_space(6.0);
|
||||
}
|
||||
|
||||
/// Draw scan result content.
|
||||
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);
|
||||
|
||||
// Show copy button.
|
||||
ui.vertical_centered(|ui| {
|
||||
let copy_text = format!("{} {}", COPY, t!("copy"));
|
||||
View::button(ui, copy_text, Colors::white_or_black(false), || {
|
||||
cb.copy_string_to_buffer(result_text.to_string());
|
||||
});
|
||||
});
|
||||
ui.add_space(10.0);
|
||||
View::horizontal_line(ui, Colors::item_stroke());
|
||||
ui.add_space(6.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| {
|
||||
View::button(ui, t!("close"), Colors::white_or_black(false), || {
|
||||
on_close();
|
||||
});
|
||||
});
|
||||
columns[1].vertical_centered_justified(|ui| {
|
||||
View::button(ui, t!("repeat"), Colors::white_or_black(false), || {
|
||||
on_repeat();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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::icons::GLOBE_SIMPLE;
|
||||
use crate::gui::platform::PlatformCallbacks;
|
||||
use crate::gui::views::settings::{InterfaceSettingsContent, NetworkSettingsContent};
|
||||
use crate::gui::views::types::ContentContainer;
|
||||
use crate::gui::views::View;
|
||||
use crate::gui::Colors;
|
||||
|
||||
/// 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,204 @@
|
||||
// Copyright 2025 The Grim Developers
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use egui::scroll_area::ScrollBarVisibility;
|
||||
use egui::{Align, Layout, RichText, ScrollArea, StrokeKind};
|
||||
|
||||
use crate::gui::icons::{CHECK, PENCIL, TRANSLATE};
|
||||
use crate::gui::platform::PlatformCallbacks;
|
||||
use crate::gui::views::types::{ContentContainer, ModalPosition};
|
||||
use crate::gui::views::{Modal, View};
|
||||
use crate::gui::Colors;
|
||||
use crate::AppConfig;
|
||||
|
||||
/// User interface settings content.
|
||||
pub struct InterfaceSettingsContent {
|
||||
/// Current locale.
|
||||
locale: String,
|
||||
}
|
||||
|
||||
/// Identifier for language selection [`Modal`].
|
||||
const LANGUAGE_SELECTION_MODAL: &'static str = "language_selection_modal";
|
||||
|
||||
impl ContentContainer for InterfaceSettingsContent {
|
||||
fn modal_ids(&self) -> Vec<&'static str> {
|
||||
vec![
|
||||
LANGUAGE_SELECTION_MODAL
|
||||
]
|
||||
}
|
||||
|
||||
fn modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal, _: &dyn PlatformCallbacks) {
|
||||
match modal.id {
|
||||
LANGUAGE_SELECTION_MODAL => self.language_selection_ui(ui),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
self.language_item_ui(self.locale.clone().as_str(), ui, true, 0, 1);
|
||||
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 content.
|
||||
fn language_selection_ui(&mut self, ui: &mut egui::Ui) {
|
||||
ui.add_space(4.0);
|
||||
ScrollArea::vertical()
|
||||
.max_height(373.0)
|
||||
.id_salt("select_language_scroll")
|
||||
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
|
||||
.auto_shrink([true; 2])
|
||||
.show(ui, |ui| {
|
||||
ui.add_space(2.0);
|
||||
ui.vertical_centered(|ui| {
|
||||
let locales = rust_i18n::available_locales!();
|
||||
for (index, locale) in locales.iter().enumerate() {
|
||||
self.language_item_ui(locale, ui, false, index, locales.len());
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
ui.add_space(6.0);
|
||||
View::horizontal_line(ui, Colors::item_stroke());
|
||||
ui.add_space(6.0);
|
||||
|
||||
// Show button to close modal.
|
||||
ui.vertical_centered_justified(|ui| {
|
||||
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
|
||||
Modal::close();
|
||||
});
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
}
|
||||
|
||||
/// Draw language selection item content.
|
||||
fn language_item_ui(&mut self, locale: &str, ui: &mut egui::Ui, edit: bool, index: usize, len: usize) {
|
||||
// Setup layout size.
|
||||
let mut rect = ui.available_rect_before_wrap();
|
||||
if edit {
|
||||
rect.set_height(56.0);
|
||||
} else {
|
||||
rect.set_height(50.0);
|
||||
}
|
||||
|
||||
// Draw round background.
|
||||
let bg_rect = rect.clone();
|
||||
let item_rounding = View::item_rounding(index, len, false);
|
||||
ui.painter().rect(bg_rect,
|
||||
item_rounding,
|
||||
Colors::fill(),
|
||||
View::item_stroke(),
|
||||
StrokeKind::Outside);
|
||||
|
||||
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
|
||||
if edit {
|
||||
View::item_button(ui, View::item_rounding(index, len, true), PENCIL, None, || {
|
||||
// Show language selection modal.
|
||||
Modal::new(LANGUAGE_SELECTION_MODAL)
|
||||
.position(ModalPosition::Center)
|
||||
.title(t!("language"))
|
||||
.show();
|
||||
});
|
||||
let layout_size = ui.available_size();
|
||||
ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| {
|
||||
ui.add_space(12.0);
|
||||
ui.vertical(|ui| {
|
||||
ui.add_space(4.0);
|
||||
View::ellipsize_text(ui,
|
||||
t!("lang_name", locale = locale),
|
||||
18.0,
|
||||
Colors::title(false));
|
||||
ui.add_space(1.0);
|
||||
let value = format!("{} {}",
|
||||
TRANSLATE,
|
||||
t!("language"));
|
||||
ui.label(RichText::new(value).size(15.0).color(Colors::gray()));
|
||||
ui.add_space(3.0);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// Draw button to select language.
|
||||
let is_current = self.locale == locale;
|
||||
if !is_current {
|
||||
View::item_button(ui, View::item_rounding(index, len, true), CHECK, None, || {
|
||||
rust_i18n::set_locale(locale);
|
||||
AppConfig::save_locale(locale);
|
||||
self.locale = locale.to_string();
|
||||
Modal::close();
|
||||
});
|
||||
} else {
|
||||
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(3.0);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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,282 @@
|
||||
// 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, Id, Layout, RichText, StrokeKind};
|
||||
use url::Url;
|
||||
|
||||
use crate::gui::icons::{CLOUD_CHECK, CLOUD_SLASH, PENCIL};
|
||||
use crate::gui::platform::PlatformCallbacks;
|
||||
use crate::gui::views::types::{ContentContainer, ModalPosition};
|
||||
use crate::gui::views::{Modal, TextEdit, View};
|
||||
use crate::gui::Colors;
|
||||
use crate::AppConfig;
|
||||
|
||||
/// 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) {
|
||||
// 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, || {
|
||||
let url = if AppConfig::use_socks_proxy() {
|
||||
AppConfig::socks_proxy_url().unwrap_or("".to_string())
|
||||
} else {
|
||||
AppConfig::http_proxy_url().unwrap_or("".to_string())
|
||||
};
|
||||
self.proxy_url_edit = url;
|
||||
// Show proxy URL edit modal.
|
||||
Modal::new(PROXY_URL_EDIT_MODAL)
|
||||
.position(ModalPosition::CenterTop)
|
||||
.title(t!("app_settings.proxy"))
|
||||
.show();
|
||||
});
|
||||
let layout_size = ui.available_size();
|
||||
ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| {
|
||||
ui.add_space(12.0);
|
||||
ui.vertical(|ui| {
|
||||
ui.add_space(4.0);
|
||||
let use_socks = AppConfig::use_socks_proxy();
|
||||
let proxy_url = if use_socks {
|
||||
AppConfig::socks_proxy_url()
|
||||
} else {
|
||||
AppConfig::http_proxy_url()
|
||||
};
|
||||
let (url, color, icon, text) = if let Some(url) = proxy_url {
|
||||
(url, Colors::title(false), CLOUD_CHECK, t!("network_settings.enabled"))
|
||||
} else {
|
||||
(
|
||||
t!("enter_url").into(),
|
||||
Colors::inactive_text(),
|
||||
CLOUD_SLASH,
|
||||
t!("network_settings.disabled")
|
||||
)
|
||||
};
|
||||
View::ellipsize_text(ui, url, 18.0, color);
|
||||
ui.add_space(1.0);
|
||||
|
||||
let value = format!("{} {}", icon, text);
|
||||
ui.label(RichText::new(value).size(15.0).color(Colors::gray()));
|
||||
ui.add_space(3.0);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// 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,573 @@
|
||||
// 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::fs;
|
||||
use egui::{Align, Id, Layout, RichText, StrokeKind};
|
||||
use egui::os::OperatingSystem;
|
||||
use url::Url;
|
||||
|
||||
use crate::gui::icons::{CLOUD_CHECK, NOTCHES, PENCIL, SCAN, TERMINAL};
|
||||
use crate::gui::platform::PlatformCallbacks;
|
||||
use crate::gui::views::types::{ContentContainer, ModalPosition};
|
||||
use crate::gui::views::{CameraScanContent, FilePickContent, FilePickContentType, Modal, TextEdit, View};
|
||||
use crate::gui::Colors;
|
||||
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"), || {
|
||||
// Save value.
|
||||
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 non-Android.
|
||||
let is_android = OperatingSystem::from_target_os() == OperatingSystem::Android;
|
||||
if !is_android {
|
||||
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 binary setup for non-Android.
|
||||
if !is_android {
|
||||
self.bridge_bin_ui(ui, br, cb);
|
||||
ui.add_space(10.0);
|
||||
}
|
||||
// Show bridge connection line setup.
|
||||
self.bridge_conn_line_ui(ui, br, cb);
|
||||
}
|
||||
|
||||
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 binary setup content.
|
||||
fn bridge_bin_ui(&mut self, ui: &mut egui::Ui, bridge: &TorBridge, cb: &dyn PlatformCallbacks) {
|
||||
// Setup layout size.
|
||||
let mut rect = ui.available_rect_before_wrap();
|
||||
rect.set_height(56.0);
|
||||
|
||||
// Draw round background.
|
||||
let bg_rect = rect.clone();
|
||||
let item_rounding = View::item_rounding(0, 1, false);
|
||||
ui.painter().rect(bg_rect,
|
||||
item_rounding,
|
||||
Colors::fill(),
|
||||
View::item_stroke(),
|
||||
StrokeKind::Outside);
|
||||
|
||||
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
|
||||
self.bridge_bin_pick_file.ui(ui, cb, |path| {
|
||||
if bridge.binary_path() != path {
|
||||
TorBridge::save_bridge_bin_path(bridge, path);
|
||||
self.settings_changed = true;
|
||||
}
|
||||
});
|
||||
View::item_button(ui, View::item_rounding(1, 3, true), PENCIL, None, || {
|
||||
self.bridge_bin_path_edit = bridge.binary_path();
|
||||
// Show binary path edit modal.
|
||||
let title = bridge.protocol_name();
|
||||
Modal::new(BRIDGE_BIN_EDIT_MODAL)
|
||||
.position(ModalPosition::CenterTop)
|
||||
.title(title)
|
||||
.show();
|
||||
});
|
||||
let layout_size = ui.available_size();
|
||||
ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| {
|
||||
ui.add_space(12.0);
|
||||
ui.vertical(|ui| {
|
||||
ui.add_space(4.0);
|
||||
View::ellipsize_text(ui, bridge.binary_path(), 18.0, Colors::title(false));
|
||||
ui.add_space(1.0);
|
||||
let value = format!("{} {}",
|
||||
TERMINAL,
|
||||
t!("transport.bin_file").replace(":", ""));
|
||||
ui.label(RichText::new(value).size(15.0).color(Colors::gray()));
|
||||
ui.add_space(3.0);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// Draw bridge binary input [`Modal`] content.
|
||||
fn bridge_bin_edit_modal_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||
let on_save = |c: &mut TorSettingsContent| {
|
||||
let bridge = TorConfig::get_bridge().unwrap();
|
||||
let exists = fs::exists(&c.bridge_bin_path_edit).unwrap_or_default();
|
||||
if !exists {
|
||||
return;
|
||||
}
|
||||
if bridge.binary_path() != c.bridge_bin_path_edit {
|
||||
TorBridge::save_bridge_bin_path(&bridge, c.bridge_bin_path_edit.clone());
|
||||
c.settings_changed = true;
|
||||
}
|
||||
Modal::close();
|
||||
};
|
||||
|
||||
ui.add_space(6.0);
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.label(RichText::new(t!("transport.bin_file"))
|
||||
.size(17.0)
|
||||
.color(Colors::gray()));
|
||||
ui.add_space(8.0);
|
||||
|
||||
// Draw p2p port text edit.
|
||||
let mut edit = TextEdit::new(Id::from(BRIDGE_BIN_EDIT_MODAL)).paste();
|
||||
edit.ui(ui, &mut self.bridge_bin_path_edit, cb);
|
||||
if edit.enter_pressed {
|
||||
on_save(self);
|
||||
}
|
||||
ui.add_space(12.0);
|
||||
|
||||
// Show modal buttons.
|
||||
ui.scope(|ui| {
|
||||
// Setup spacing between buttons.
|
||||
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
|
||||
|
||||
ui.columns(2, |columns| {
|
||||
columns[0].vertical_centered_justified(|ui| {
|
||||
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
|
||||
// Close modal.
|
||||
Modal::close();
|
||||
});
|
||||
});
|
||||
columns[1].vertical_centered_justified(|ui| {
|
||||
View::button(ui, t!("modal.save"), Colors::white_or_black(false), || {
|
||||
on_save(self);
|
||||
});
|
||||
});
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// Draw bridge connection line setup content.
|
||||
fn bridge_conn_line_ui(&mut self,
|
||||
ui: &mut egui::Ui,
|
||||
bridge: &TorBridge,
|
||||
cb: &dyn PlatformCallbacks) {
|
||||
// Setup layout size.
|
||||
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), SCAN, None, || {
|
||||
self.show_qr_scan_bridge_modal(cb);
|
||||
});
|
||||
View::item_button(ui, View::item_rounding(1, 3 , true), PENCIL, None, || {
|
||||
self.bridge_conn_line_edit = bridge.connection_line();
|
||||
// Show connection line edit modal.
|
||||
let title = bridge.protocol_name();
|
||||
Modal::new(BRIDGE_CONN_LINE_EDIT_MODAL)
|
||||
.position(ModalPosition::CenterTop)
|
||||
.title(title)
|
||||
.show();
|
||||
});
|
||||
let layout_size = ui.available_size();
|
||||
ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| {
|
||||
ui.add_space(12.0);
|
||||
ui.vertical(|ui| {
|
||||
ui.add_space(4.0);
|
||||
View::ellipsize_text(ui, bridge.connection_line(), 18.0, Colors::title(false));
|
||||
ui.add_space(1.0);
|
||||
let value = format!("{} {}",
|
||||
NOTCHES,
|
||||
t!("transport.conn_line").replace(":", ""));
|
||||
ui.label(RichText::new(value).size(15.0).color(Colors::gray()));
|
||||
ui.add_space(3.0);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// 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.
|
||||
let mut edit = TextEdit::new(Id::from(BRIDGE_CONN_LINE_EDIT_MODAL)).paste();
|
||||
edit.ui(ui, &mut self.bridge_conn_line_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);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use egui::{Margin, Id, Layout, Align};
|
||||
use egui::{Margin, Id, Layout, Align, UiBuilder};
|
||||
|
||||
use crate::gui::Colors;
|
||||
use crate::gui::views::{Content, View};
|
||||
@@ -25,8 +25,8 @@ pub struct TitlePanel {
|
||||
}
|
||||
|
||||
impl TitlePanel {
|
||||
/// Default [`TitlePanel`] content height.
|
||||
pub const DEFAULT_HEIGHT: f32 = 54.0;
|
||||
/// Content height.
|
||||
pub const HEIGHT: f32 = 54.0;
|
||||
|
||||
/// Create new title panel with provided identifier.
|
||||
pub fn new(id: Id) -> Self {
|
||||
@@ -43,15 +43,14 @@ impl TitlePanel {
|
||||
// Draw title panel.
|
||||
egui::TopBottomPanel::top(self.id)
|
||||
.resizable(false)
|
||||
.exact_height(Self::DEFAULT_HEIGHT + View::get_top_inset())
|
||||
.exact_height(Self::HEIGHT + View::get_top_inset())
|
||||
.frame(egui::Frame {
|
||||
inner_margin: Margin {
|
||||
left: View::far_left_inset_margin(ui),
|
||||
right: View::far_right_inset_margin(ui),
|
||||
top: View::get_top_inset(),
|
||||
bottom: 0.0,
|
||||
left: View::far_left_inset_margin(ui) as i8,
|
||||
right: View::far_right_inset_margin(ui) as i8,
|
||||
top: View::get_top_inset() as i8,
|
||||
bottom: 0.0 as i8,
|
||||
},
|
||||
fill: Colors::yellow(),
|
||||
..Default::default()
|
||||
})
|
||||
.show_inside(ui, |ui| {
|
||||
@@ -68,35 +67,35 @@ impl TitlePanel {
|
||||
match title {
|
||||
TitleType::Single(content) => {
|
||||
let content_rect = {
|
||||
let mut r = rect;
|
||||
r.min.x += Self::DEFAULT_HEIGHT;
|
||||
r.max.x -= Self::DEFAULT_HEIGHT;
|
||||
let mut r = rect.clone();
|
||||
r.min.x += Self::HEIGHT;
|
||||
r.max.x -= Self::HEIGHT;
|
||||
r
|
||||
};
|
||||
ui.allocate_ui_at_rect(content_rect, |ui| {
|
||||
ui.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;
|
||||
r.max.x = r.min.x + Content::SIDE_PANEL_WIDTH - Self::DEFAULT_HEIGHT;
|
||||
r.min.x += Self::DEFAULT_HEIGHT;
|
||||
let mut r = rect.clone();
|
||||
r.max.x = r.min.x + Content::SIDE_PANEL_WIDTH - Self::HEIGHT;
|
||||
r.min.x += Self::HEIGHT;
|
||||
r
|
||||
};
|
||||
// Draw first title content.
|
||||
ui.allocate_ui_at_rect(first_rect, |ui| {
|
||||
ui.scope_builder(UiBuilder::new().max_rect(first_rect), |ui| {
|
||||
Self::title_text_content(ui, first);
|
||||
});
|
||||
|
||||
let second_rect = {
|
||||
let mut r = rect;
|
||||
r.min.x = first_rect.max.x + 2.0 * Self::DEFAULT_HEIGHT;
|
||||
r.max.x -= Self::DEFAULT_HEIGHT;
|
||||
let mut r = rect.clone();
|
||||
r.min.x = first_rect.max.x + 2.0 * Self::HEIGHT;
|
||||
r.max.x -= Self::HEIGHT;
|
||||
r
|
||||
};
|
||||
// Draw second title content.
|
||||
ui.allocate_ui_at_rect(second_rect, |ui| {
|
||||
ui.scope_builder(UiBuilder::new().max_rect(second_rect), |ui| {
|
||||
Self::title_text_content(ui, second);
|
||||
});
|
||||
}
|
||||
@@ -115,11 +114,11 @@ impl TitlePanel {
|
||||
} else {
|
||||
0.0
|
||||
});
|
||||
View::ellipsize_text(ui, text, 19.0, Colors::title(true));
|
||||
View::ellipsize_text(ui, text.to_uppercase(), 19.0, Colors::title(true));
|
||||
}
|
||||
TitleContentType::WithSubTitle(text, subtitle, animate) => {
|
||||
ui.add_space(4.0);
|
||||
View::ellipsize_text(ui, text, 18.0, Colors::title(true));
|
||||
View::ellipsize_text(ui, text.to_uppercase(), 18.0, Colors::title(true));
|
||||
ui.add_space(-2.0);
|
||||
View::animate_text(ui, subtitle, 15.0, Colors::text(true), animate)
|
||||
}
|
||||
|
||||
+23
-93
@@ -32,6 +32,11 @@ pub enum TitleContentType {
|
||||
WithSubTitle(String, String, bool)
|
||||
}
|
||||
|
||||
/// Stroke position against content.
|
||||
pub enum LinePosition {
|
||||
TOP, LEFT, RIGHT, BOTTOM
|
||||
}
|
||||
|
||||
/// Position of [`Modal`] on the screen.
|
||||
#[derive(Clone)]
|
||||
pub enum ModalPosition {
|
||||
@@ -46,100 +51,25 @@ pub struct ModalState {
|
||||
pub modal: Option<Modal>,
|
||||
}
|
||||
|
||||
/// Contains identifiers to draw opened [`Modal`] content for current ui container.
|
||||
pub trait ModalContainer {
|
||||
/// Content container to simplify modals management and navigation.
|
||||
pub trait ContentContainer {
|
||||
/// List of allowed [`Modal`] identifiers.
|
||||
fn modal_ids(&self) -> &Vec<&'static str>;
|
||||
|
||||
/// Draw modal ui content.
|
||||
fn modal_ui(&mut self,
|
||||
ui: &mut egui::Ui,
|
||||
modal: &Modal,
|
||||
cb: &dyn PlatformCallbacks);
|
||||
|
||||
/// Draw [`Modal`] for current ui container if it's possible.
|
||||
fn current_modal_ui(&mut self,
|
||||
ui: &mut egui::Ui,
|
||||
cb: &dyn PlatformCallbacks) {
|
||||
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.modal_ui(ui, modal, cb);
|
||||
});
|
||||
fn modal_ids(&self) -> Vec<&'static str>;
|
||||
/// Draw modal content.
|
||||
fn modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks);
|
||||
/// Draw container content.
|
||||
fn container_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks);
|
||||
/// Draw content, to call by parent container.
|
||||
fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||
// Draw modal content.
|
||||
if let Some(id) = Modal::opened() {
|
||||
if self.modal_ids().contains(&id) {
|
||||
Modal::ui(ui.ctx(), cb, |ui, modal, 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
|
||||
self.container_ui(ui, cb);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,7 +77,7 @@ impl TextEditOptions {
|
||||
#[derive(Clone)]
|
||||
pub enum QrScanResult {
|
||||
/// Slatepack message.
|
||||
Slatepack(ZeroingString),
|
||||
Slatepack(String),
|
||||
/// Slatepack address.
|
||||
Address(ZeroingString),
|
||||
/// Parsed text.
|
||||
|
||||
+245
-283
@@ -12,25 +12,21 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::sync::atomic::{AtomicI32, Ordering};
|
||||
use std::sync::Arc;
|
||||
use parking_lot::RwLock;
|
||||
use lazy_static::lazy_static;
|
||||
|
||||
use egui::{Align, Button, CursorIcon, Layout, lerp, PointerState, Rect, Response, Rgba, RichText, Sense, SizeHint, Spinner, TextBuffer, TextStyle, TextureHandle, TextureOptions, Widget};
|
||||
use egui::epaint::{Color32, FontId, RectShape, Rounding, Stroke};
|
||||
use std::sync::atomic::{AtomicI32, Ordering};
|
||||
use egui::emath::GuiRounding;
|
||||
use egui::epaint::text::TextWrapping;
|
||||
use egui::epaint::{Color32, FontId, PathShape, PathStroke, RectShape, Stroke};
|
||||
use egui::load::SizedTexture;
|
||||
use egui::os::OperatingSystem;
|
||||
use egui::text::{LayoutJob, TextFormat};
|
||||
use egui::text_edit::TextEditState;
|
||||
use egui::{lerp, Button, CornerRadius, CursorIcon, Rect, Response, Rgba, RichText, Sense, SizeHint, Spinner, StrokeKind, TextureHandle, TextureOptions, UiBuilder, Widget};
|
||||
use egui_extras::image::load_svg_bytes_with_size;
|
||||
|
||||
use crate::AppConfig;
|
||||
use crate::gui::icons::{CHECK_FAT, CHECK_SQUARE, SQUARE};
|
||||
use crate::gui::views::types::LinePosition;
|
||||
use crate::gui::Colors;
|
||||
use crate::gui::icons::{CHECK_SQUARE, CLIPBOARD_TEXT, COPY, EYE, EYE_SLASH, SCAN, SQUARE};
|
||||
use crate::gui::platform::PlatformCallbacks;
|
||||
use crate::gui::views::types::TextEditOptions;
|
||||
use crate::AppConfig;
|
||||
|
||||
pub struct View;
|
||||
|
||||
@@ -78,24 +74,19 @@ impl View {
|
||||
rect.set_width(width);
|
||||
|
||||
// Draw content.
|
||||
ui.allocate_ui(rect.size(), |ui| {
|
||||
(add_content)(ui);
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.allocate_ui(rect.size(), |ui| {
|
||||
(add_content)(ui);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// Get width and height of app window.
|
||||
pub fn window_size(ui: &egui::Ui) -> (f32, f32) {
|
||||
let rect = ui.ctx().screen_rect();
|
||||
pub fn window_size(ctx: &egui::Context) -> (f32, f32) {
|
||||
let rect = ctx.content_rect();
|
||||
(rect.width(), rect.height())
|
||||
}
|
||||
|
||||
/// Callback on Enter key press event.
|
||||
pub fn on_enter_key(ui: &mut egui::Ui, cb: impl FnOnce()) {
|
||||
if ui.ctx().input(|i| i.key_pressed(egui::Key::Enter)) {
|
||||
(cb)();
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculate margin for far left view based on display insets (cutouts).
|
||||
pub fn far_left_inset_margin(ui: &mut egui::Ui) -> f32 {
|
||||
if ui.available_rect_before_wrap().min.x == 0.0 {
|
||||
@@ -108,7 +99,7 @@ impl View {
|
||||
/// Calculate margin for far left view based on display insets (cutouts).
|
||||
pub fn far_right_inset_margin(ui: &mut egui::Ui) -> f32 {
|
||||
let container_width = ui.available_rect_before_wrap().max.x as i32;
|
||||
let window_size = Self::window_size(ui);
|
||||
let window_size = Self::window_size(ui.ctx());
|
||||
let display_width = window_size.0 as i32;
|
||||
// Means end of the screen.
|
||||
if container_width == display_width {
|
||||
@@ -118,9 +109,18 @@ impl View {
|
||||
}
|
||||
}
|
||||
|
||||
/// Content padding for current platform.
|
||||
pub fn content_padding() -> f32 {
|
||||
if View::is_desktop() {
|
||||
4.0
|
||||
} else {
|
||||
8.0
|
||||
}
|
||||
}
|
||||
|
||||
/// Cut long text with ﹍ character.
|
||||
fn ellipsize(text: String, size: f32, color: Color32) -> LayoutJob {
|
||||
let mut job = LayoutJob::single_section(text, TextFormat {
|
||||
fn ellipsize(text: impl Into<String>, size: f32, color: Color32) -> LayoutJob {
|
||||
let mut job = LayoutJob::single_section(text.into(), TextFormat {
|
||||
font_id: FontId::proportional(size), color, ..Default::default()
|
||||
});
|
||||
job.wrap = TextWrapping {
|
||||
@@ -133,12 +133,16 @@ impl View {
|
||||
}
|
||||
|
||||
/// Draw ellipsized text.
|
||||
pub fn ellipsize_text(ui: &mut egui::Ui, text: String, size: f32, color: Color32) {
|
||||
pub fn ellipsize_text(ui: &mut egui::Ui, text: impl Into<String>, size: f32, color: Color32) {
|
||||
ui.label(Self::ellipsize(text, size, color));
|
||||
}
|
||||
|
||||
/// Draw animated ellipsized text.
|
||||
pub fn animate_text(ui: &mut egui::Ui, text: String, size: f32, color: Color32, animate: bool) {
|
||||
pub fn animate_text(ui: &mut egui::Ui,
|
||||
text: impl Into<String>,
|
||||
size: f32,
|
||||
color: Color32,
|
||||
animate: bool) {
|
||||
// Setup text color animation if needed.
|
||||
let (dark, bright) = (0.3, 1.0);
|
||||
let color_factor = if animate {
|
||||
@@ -158,7 +162,7 @@ impl View {
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw horizontally centered sub-title with space below.
|
||||
/// Draw horizontally centered subtitle with space below.
|
||||
pub fn sub_title(ui: &mut egui::Ui, text: String) {
|
||||
ui.vertical_centered_justified(|ui| {
|
||||
ui.label(RichText::new(text.to_uppercase()).size(16.0).color(Colors::text(false)));
|
||||
@@ -166,19 +170,6 @@ impl View {
|
||||
ui.add_space(4.0);
|
||||
}
|
||||
|
||||
/// Temporary click optimization for touch screens, return `true` if it was clicked.
|
||||
fn touched(ui: &mut egui::Ui, resp: Response) -> bool {
|
||||
let drag_resp = resp.interact(Sense::click_and_drag());
|
||||
// Clear pointer event if dragging is out of button area
|
||||
if drag_resp.dragged() && !ui.rect_contains_pointer(drag_resp.rect) {
|
||||
ui.input_mut(|i| i.pointer = PointerState::default());
|
||||
}
|
||||
if drag_resp.drag_stopped() || drag_resp.clicked() || drag_resp.secondary_clicked() {
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Draw big size title button.
|
||||
pub fn title_button_big(ui: &mut egui::Ui, icon: &str, action: impl FnOnce(&mut egui::Ui)) {
|
||||
Self::title_button(ui, 22.0, icon, action);
|
||||
@@ -192,23 +183,32 @@ impl View {
|
||||
/// Draw title button with transparent background color, contains only icon.
|
||||
fn title_button(ui: &mut egui::Ui, size: f32, icon: &str, action: impl FnOnce(&mut egui::Ui)) {
|
||||
ui.scope(|ui| {
|
||||
// Setup padding for title buttons.
|
||||
if !View::is_desktop() {
|
||||
ui.style_mut().spacing.button_padding = egui::vec2(20.0, 8.0);
|
||||
}
|
||||
// Disable strokes.
|
||||
ui.style_mut().visuals.widgets.inactive.bg_stroke = Stroke::NONE;
|
||||
ui.style_mut().visuals.widgets.hovered.bg_stroke = Stroke::NONE;
|
||||
ui.style_mut().visuals.widgets.active.bg_stroke = Stroke::NONE;
|
||||
ui.style_mut().visuals.widgets.active.rounding = Rounding::default();
|
||||
ui.style_mut().visuals.widgets.active.corner_radius = CornerRadius::default();
|
||||
ui.style_mut().visuals.widgets.active.expansion = 0.0;
|
||||
|
||||
// Setup text.
|
||||
let wt = RichText::new(icon.to_string()).size(size).color(Colors::title(true));
|
||||
// Draw button.
|
||||
let br = Button::new(wt)
|
||||
.sense(if View::is_desktop() {
|
||||
Sense::click()
|
||||
} else {
|
||||
Sense::click_and_drag()
|
||||
})
|
||||
.fill(Colors::TRANSPARENT)
|
||||
.ui(ui)
|
||||
.on_hover_cursor(CursorIcon::PointingHand);
|
||||
br.surrender_focus();
|
||||
if Self::touched(ui, br) {
|
||||
(action)(ui);
|
||||
if br.clicked() || (!View::is_desktop() && br.drag_stopped()) {
|
||||
action(ui);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -217,22 +217,39 @@ impl View {
|
||||
pub const TAB_ITEMS_PADDING: f32 = 5.0;
|
||||
|
||||
/// Tab button with white background fill color, contains only icon.
|
||||
pub fn tab_button(ui: &mut egui::Ui, icon: &str, active: bool, action: impl FnOnce()) {
|
||||
pub fn tab_button(ui: &mut egui::Ui,
|
||||
icon: &str,
|
||||
color: Option<Color32>,
|
||||
selected: Option<bool>,
|
||||
action: impl FnOnce(&mut egui::Ui)) {
|
||||
ui.scope(|ui| {
|
||||
let text_color = match active {
|
||||
true => Colors::title(false),
|
||||
false => Colors::text(false)
|
||||
let text_color = if let Some(c) = color {
|
||||
if selected.is_none() {
|
||||
Colors::inactive_text()
|
||||
} else {
|
||||
c
|
||||
}
|
||||
} else {
|
||||
if let Some(active) = selected {
|
||||
match active {
|
||||
true => Colors::gray(),
|
||||
false => Colors::item_button_text()
|
||||
}
|
||||
} else {
|
||||
Colors::inactive_text()
|
||||
}
|
||||
};
|
||||
|
||||
let mut button = Button::new(RichText::new(icon).size(22.0).color(text_color));
|
||||
|
||||
if !active {
|
||||
let active_not_selected = selected.is_some() && !selected.unwrap();
|
||||
if active_not_selected {
|
||||
// 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::button();
|
||||
ui.visuals_mut().widgets.hovered.weak_bg_fill = Colors::fill_lite();
|
||||
ui.visuals_mut().widgets.active.weak_bg_fill = Colors::fill();
|
||||
// Setup stroke colors.
|
||||
ui.visuals_mut().widgets.inactive.bg_stroke = Self::default_stroke();
|
||||
@@ -242,10 +259,23 @@ impl View {
|
||||
button = button.fill(Colors::fill()).stroke(Stroke::NONE);
|
||||
}
|
||||
|
||||
let br = button.ui(ui).on_hover_cursor(CursorIcon::PointingHand);
|
||||
// Setup paddings for tab buttons.
|
||||
if Self::is_desktop() {
|
||||
ui.style_mut().spacing.button_padding = egui::vec2(10.0, 4.0);
|
||||
} else {
|
||||
ui.style_mut().spacing.button_padding = egui::vec2(14.0, 8.0);
|
||||
};
|
||||
|
||||
// Setup pointer style.
|
||||
let br = if active_not_selected {
|
||||
button.ui(ui).on_hover_cursor(CursorIcon::PointingHand)
|
||||
} else {
|
||||
button.ui(ui)
|
||||
};
|
||||
|
||||
br.surrender_focus();
|
||||
if Self::touched(ui, br) {
|
||||
(action)();
|
||||
if br.clicked() && active_not_selected {
|
||||
action(ui);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -261,10 +291,10 @@ impl View {
|
||||
}
|
||||
|
||||
/// Draw [`Button`] with specified background fill color and default text color.
|
||||
pub fn button(ui: &mut egui::Ui, text: String, fill: Color32, action: impl FnOnce()) {
|
||||
let br = Self::button_resp(ui, text, Colors::text_button(), fill);
|
||||
if Self::touched(ui, br) {
|
||||
(action)();
|
||||
pub fn button(ui: &mut egui::Ui, text: impl Into<String>, fill: Color32, cb: impl FnOnce()) {
|
||||
let br = Self::button_resp(ui, text.into(), Colors::text_button(), fill);
|
||||
if br.clicked() {
|
||||
cb();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -275,212 +305,103 @@ impl View {
|
||||
fill: Color32,
|
||||
action: impl FnOnce()) {
|
||||
let br = Self::button_resp(ui, text, text_color, fill);
|
||||
if Self::touched(ui, br) {
|
||||
(action)();
|
||||
if br.clicked() {
|
||||
action();
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw [`Button`] with specified background fill color and text color.
|
||||
pub fn colored_text_button_ui(ui: &mut egui::Ui,
|
||||
text: String,
|
||||
text_color: Color32,
|
||||
fill: Color32,
|
||||
action: impl FnOnce(&mut egui::Ui)) {
|
||||
let br = Self::button_resp(ui, text, text_color, fill);
|
||||
if br.clicked() {
|
||||
action(ui);
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw gold action [`Button`].
|
||||
pub fn action_button(ui: &mut egui::Ui,
|
||||
text: String, action: impl FnOnce()) {
|
||||
Self::colored_text_button(ui, text, Colors::title(true), Colors::gold(), action);
|
||||
text: impl Into<String>, action: impl FnOnce()) {
|
||||
Self::colored_text_button(ui, text.into(), Colors::title(true), Colors::gold(), action);
|
||||
}
|
||||
|
||||
/// Draw [`Button`] with specified background fill color and ui at callback.
|
||||
pub fn button_ui(ui: &mut egui::Ui,
|
||||
text: String,
|
||||
text: impl Into<String>,
|
||||
fill: Color32,
|
||||
action: impl FnOnce(&mut egui::Ui)) {
|
||||
let button_text = Self::ellipsize(text.to_uppercase(), 17.0, Colors::text_button());
|
||||
let button_text = Self::ellipsize(text.into().to_uppercase(), 17.0, Colors::text_button());
|
||||
let br = Button::new(button_text)
|
||||
.stroke(Self::default_stroke())
|
||||
.fill(fill)
|
||||
.ui(ui)
|
||||
.on_hover_cursor(CursorIcon::PointingHand);
|
||||
if Self::touched(ui, br) {
|
||||
(action)(ui);
|
||||
if br.clicked() {
|
||||
action(ui);
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw list item [`Button`] with provided rounding.
|
||||
pub fn item_button(ui: &mut egui::Ui,
|
||||
rounding: Rounding,
|
||||
rounding: CornerRadius,
|
||||
text: &'static str,
|
||||
color: Option<Color32>,
|
||||
action: impl FnOnce()) {
|
||||
// Setup button size.
|
||||
let mut rect = ui.available_rect_before_wrap();
|
||||
rect.set_width(32.0);
|
||||
rect.set_width(42.0);
|
||||
let button_size = rect.size();
|
||||
|
||||
ui.scope(|ui| {
|
||||
// Setup padding for item buttons.
|
||||
ui.style_mut().spacing.button_padding = egui::vec2(14.0, 0.0);
|
||||
let padding = if Self::is_desktop() {
|
||||
14.0
|
||||
} else {
|
||||
18.0
|
||||
};
|
||||
ui.style_mut().spacing.button_padding = egui::vec2(padding, 0.0);
|
||||
// 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::button();
|
||||
ui.visuals_mut().widgets.hovered.weak_bg_fill = Colors::fill_lite();
|
||||
ui.visuals_mut().widgets.active.weak_bg_fill = Colors::fill();
|
||||
// Setup stroke colors.
|
||||
ui.visuals_mut().widgets.inactive.bg_stroke = Self::default_stroke();
|
||||
ui.visuals_mut().widgets.hovered.bg_stroke = Self::hover_stroke();
|
||||
ui.visuals_mut().widgets.active.bg_stroke = Self::item_stroke();
|
||||
// Disable strokes.
|
||||
ui.visuals_mut().widgets.inactive.bg_stroke = Stroke::NONE;
|
||||
ui.visuals_mut().widgets.hovered.bg_stroke = Stroke::NONE;
|
||||
ui.visuals_mut().widgets.active.bg_stroke = Stroke::NONE;
|
||||
|
||||
// Setup button text color.
|
||||
let text_color = if let Some(c) = color { c } else { Colors::item_button() };
|
||||
let text_color = if let Some(c) = color { c } else { Colors::item_button_text() };
|
||||
|
||||
// Show button.
|
||||
let br = Button::new(RichText::new(text).size(20.0).color(text_color))
|
||||
.rounding(rounding)
|
||||
.corner_radius(rounding)
|
||||
.min_size(button_size)
|
||||
.ui(ui)
|
||||
.on_hover_cursor(CursorIcon::PointingHand);
|
||||
br.surrender_focus();
|
||||
if Self::touched(ui, br) {
|
||||
(action)();
|
||||
if br.clicked() {
|
||||
action();
|
||||
}
|
||||
|
||||
// Draw stroke.
|
||||
let r = {
|
||||
let mut r = ui.available_rect_before_wrap();
|
||||
r.min = br.rect.min;
|
||||
r.min.x += 0.5;
|
||||
r
|
||||
};
|
||||
Self::line(ui, LinePosition::LEFT, &r, Colors::item_stroke());
|
||||
});
|
||||
}
|
||||
|
||||
/// Default height of [`egui::TextEdit`] view.
|
||||
const TEXT_EDIT_HEIGHT: f32 = 37.0;
|
||||
|
||||
/// Draw [`egui::TextEdit`] widget.
|
||||
pub fn text_edit(ui: &mut egui::Ui,
|
||||
cb: &dyn PlatformCallbacks,
|
||||
value: &mut String,
|
||||
options: &mut TextEditOptions) {
|
||||
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::Center), |ui| {
|
||||
// Setup password button.
|
||||
let mut show_pass = false;
|
||||
if options.password {
|
||||
// Set password button state value.
|
||||
let show_pass_id = egui::Id::new(options.id).with("_show_pass");
|
||||
show_pass = ui.data(|data| {
|
||||
data.get_temp(show_pass_id)
|
||||
}).unwrap_or(true);
|
||||
// Draw button to show/hide current password.
|
||||
let eye_icon = if show_pass { EYE } else { EYE_SLASH };
|
||||
let mut changed = false;
|
||||
View::button(ui, eye_icon.to_string(), Colors::white_or_black(false), || {
|
||||
show_pass = !show_pass;
|
||||
changed = true;
|
||||
});
|
||||
// Save state if changed.
|
||||
if changed {
|
||||
ui.data_mut(|data| {
|
||||
data.insert_temp(show_pass_id, show_pass);
|
||||
});
|
||||
}
|
||||
ui.add_space(8.0);
|
||||
}
|
||||
|
||||
// Setup copy button.
|
||||
if options.copy {
|
||||
let copy_icon = COPY.to_string();
|
||||
View::button(ui, copy_icon, Colors::white_or_black(false), || {
|
||||
cb.copy_string_to_buffer(value.clone());
|
||||
});
|
||||
ui.add_space(8.0);
|
||||
}
|
||||
|
||||
// Setup paste button.
|
||||
if options.paste {
|
||||
let paste_icon = CLIPBOARD_TEXT.to_string();
|
||||
View::button(ui, paste_icon, Colors::white_or_black(false), || {
|
||||
*value = cb.get_string_from_buffer();
|
||||
});
|
||||
ui.add_space(8.0);
|
||||
}
|
||||
|
||||
// Setup scan QR code button.
|
||||
if options.scan_qr {
|
||||
let scan_icon = SCAN.to_string();
|
||||
View::button(ui, scan_icon, Colors::white_or_black(false), || {
|
||||
cb.start_camera();
|
||||
options.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::Center), |ui| {
|
||||
// Setup text edit size.
|
||||
let mut edit_rect = ui.available_rect_before_wrap();
|
||||
edit_rect.set_height(Self::TEXT_EDIT_HEIGHT);
|
||||
|
||||
// Show text edit.
|
||||
let text_edit_resp = egui::TextEdit::singleline(value)
|
||||
.id(options.id)
|
||||
.margin(egui::Vec2::new(2.0, 0.0))
|
||||
.font(TextStyle::Heading)
|
||||
.min_size(edit_rect.size())
|
||||
.horizontal_align(if options.h_center { Align::Center } else { Align::Min })
|
||||
.vertical_align(Align::Center)
|
||||
.password(show_pass)
|
||||
.cursor_at_end(true)
|
||||
.ui(ui);
|
||||
// Show keyboard on click.
|
||||
if text_edit_resp.clicked() {
|
||||
text_edit_resp.request_focus();
|
||||
cb.show_keyboard();
|
||||
}
|
||||
// Setup focus on input field.
|
||||
if options.focus {
|
||||
text_edit_resp.request_focus();
|
||||
cb.show_keyboard();
|
||||
}
|
||||
|
||||
// Apply text from input on Android as temporary fix for egui.
|
||||
if text_edit_resp.has_focus() {
|
||||
Self::on_soft_input(ui, options.id, value);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// Apply soft keyboard input data to provided String.
|
||||
pub fn on_soft_input(ui: &mut egui::Ui, id: egui::Id, value: &mut String) {
|
||||
let os = OperatingSystem::from_target_os();
|
||||
if os == OperatingSystem::Android {
|
||||
let mut w_input = LAST_SOFT_KEYBOARD_INPUT.write();
|
||||
|
||||
if !w_input.is_empty() {
|
||||
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;
|
||||
|
||||
value.insert_text(w_input.as_str(), index);
|
||||
index = index + 1;
|
||||
|
||||
if index == 0 {
|
||||
r.primary.index = value.len();
|
||||
r.secondary.index = r.primary.index;
|
||||
} else {
|
||||
r.primary.index = index;
|
||||
r.secondary.index = r.primary.index;
|
||||
}
|
||||
state.cursor.set_char_range(Some(r));
|
||||
TextEditState::store(state, ui.ctx(), id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
*w_input = "".to_string();
|
||||
ui.ctx().request_repaint();
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculate item background/button rounding based on item index.
|
||||
pub fn item_rounding(index: usize, len: usize, is_button: bool) -> Rounding {
|
||||
pub fn item_rounding(index: usize, len: usize, is_button: bool) -> CornerRadius {
|
||||
let corners = if is_button {
|
||||
if len == 1 {
|
||||
[false, true, true, false]
|
||||
@@ -502,40 +423,44 @@ impl View {
|
||||
[false, false, false, false]
|
||||
}
|
||||
};
|
||||
Rounding {
|
||||
nw: if corners[0] { 8.0 } else { 0.0 },
|
||||
ne: if corners[1] { 8.0 } else { 0.0 },
|
||||
sw: if corners[3] { 8.0 } else { 0.0 },
|
||||
se: if corners[2] { 8.0 } else { 0.0 },
|
||||
CornerRadius {
|
||||
nw: if corners[0] { 8.0 as u8 } else { 0.0 as u8 },
|
||||
ne: if corners[1] { 8.0 as u8 } else { 0.0 as u8 },
|
||||
sw: if corners[3] { 8.0 as u8 } else { 0.0 as u8 },
|
||||
se: if corners[2] { 8.0 as u8 } else { 0.0 as u8 },
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw selected item check.
|
||||
pub fn selected_item_check(ui: &mut egui::Ui) {
|
||||
let padding = if View::is_desktop() {
|
||||
14.0
|
||||
} else {
|
||||
18.0
|
||||
};
|
||||
ui.add_space(padding);
|
||||
ui.label(RichText::new(CHECK_FAT).size(20.0).color(Colors::green()));
|
||||
ui.add_space(padding);
|
||||
}
|
||||
|
||||
/// Draw rounded box with some value and label in the middle,
|
||||
/// where is r = (top_left, top_right, bottom_left, bottom_right).
|
||||
/// | VALUE |
|
||||
/// | label |
|
||||
pub fn rounded_box(ui: &mut egui::Ui, value: String, label: String, r: [bool; 4]) {
|
||||
pub fn label_box(ui: &mut egui::Ui, v: impl Into<String>, l: impl Into<String>, r: [bool; 4]) {
|
||||
let rect = ui.available_rect_before_wrap();
|
||||
|
||||
// Create background shape.
|
||||
let mut bg_shape = RectShape {
|
||||
rect,
|
||||
rounding: Rounding {
|
||||
nw: if r[0] { 8.0 } else { 0.0 },
|
||||
ne: if r[1] { 8.0 } else { 0.0 },
|
||||
sw: if r[2] { 8.0 } else { 0.0 },
|
||||
se: if r[3] { 8.0 } else { 0.0 },
|
||||
},
|
||||
fill: Colors::TRANSPARENT,
|
||||
stroke: Self::item_stroke(),
|
||||
blur_width: 0.0,
|
||||
fill_texture_id: Default::default(),
|
||||
uv: Rect::ZERO
|
||||
};
|
||||
let bg_idx = ui.painter().add(bg_shape);
|
||||
let mut bg_shape = RectShape::new(rect, CornerRadius {
|
||||
nw: if r[0] { 8.0 as u8 } else { 0.0 as u8 },
|
||||
ne: if r[1] { 8.0 as u8 } else { 0.0 as u8 },
|
||||
sw: if r[2] { 8.0 as u8 } else { 0.0 as u8 },
|
||||
se: if r[3] { 8.0 as u8 } else { 0.0 as u8 },
|
||||
}, Colors::fill_lite(), Self::item_stroke(), StrokeKind::Outside);
|
||||
let bg_idx = ui.painter().add(bg_shape.clone());
|
||||
|
||||
// Draw box content.
|
||||
let content_resp = ui.allocate_ui_at_rect(rect, |ui| {
|
||||
let content_resp = ui.scope_builder(UiBuilder::new().max_rect(rect), |ui| {
|
||||
ui.vertical_centered_justified(|ui| {
|
||||
ui.add_space(4.0);
|
||||
ui.scope(|ui| {
|
||||
@@ -543,7 +468,7 @@ impl View {
|
||||
ui.style_mut().spacing.item_spacing.y = -3.0;
|
||||
|
||||
// Draw box value.
|
||||
let mut job = LayoutJob::single_section(value, TextFormat {
|
||||
let mut job = LayoutJob::single_section(v.into(), TextFormat {
|
||||
font_id: FontId::proportional(17.0),
|
||||
color: Colors::white_or_black(true),
|
||||
..Default::default()
|
||||
@@ -557,13 +482,13 @@ impl View {
|
||||
ui.label(job);
|
||||
|
||||
// Draw box label.
|
||||
ui.label(RichText::new(label).color(Colors::gray()).size(15.0));
|
||||
ui.label(RichText::new(l).color(Colors::gray()).size(15.0));
|
||||
});
|
||||
ui.add_space(2.0);
|
||||
});
|
||||
}).response;
|
||||
|
||||
// Setup background shape to be painted behind box content.
|
||||
// Setup background shape size.
|
||||
bg_shape.rect = content_resp.rect;
|
||||
ui.painter().set(bg_idx, bg_shape);
|
||||
}
|
||||
@@ -575,30 +500,38 @@ impl View {
|
||||
let side_margin = 28.0;
|
||||
rect.min += egui::emath::vec2(side_margin, ui.available_height() / 2.0 - height / 2.0);
|
||||
rect.max -= egui::emath::vec2(side_margin, 0.0);
|
||||
ui.allocate_ui_at_rect(rect, |ui| {
|
||||
(content)(ui);
|
||||
ui.scope_builder(UiBuilder::new().max_rect(rect), |ui| {
|
||||
content(ui);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// Draw loading spinner.
|
||||
pub fn loading_spinner(ui: &mut egui::Ui, size: f32) {
|
||||
Spinner::new().size(size).color(Colors::gold()).ui(ui);
|
||||
}
|
||||
|
||||
/// Size of big loading spinner.
|
||||
pub const BIG_SPINNER_SIZE: f32 = 104.0;
|
||||
|
||||
/// Draw big gold loading spinner.
|
||||
pub fn big_loading_spinner(ui: &mut egui::Ui) {
|
||||
Spinner::new().size(Self::BIG_SPINNER_SIZE).color(Colors::gold()).ui(ui);
|
||||
View::loading_spinner(ui, View::BIG_SPINNER_SIZE);
|
||||
}
|
||||
|
||||
/// Size of big loading spinner.
|
||||
pub const SMALL_SPINNER_SIZE: f32 = 32.0;
|
||||
|
||||
/// Draw small gold loading spinner.
|
||||
pub fn small_loading_spinner(ui: &mut egui::Ui) {
|
||||
Spinner::new().size(38.0).color(Colors::gold()).ui(ui);
|
||||
View::loading_spinner(ui, View::SMALL_SPINNER_SIZE);
|
||||
}
|
||||
|
||||
/// Draw the button that looks like checkbox with callback on check.
|
||||
pub fn checkbox(ui: &mut egui::Ui, checked: bool, text: String, callback: impl FnOnce()) {
|
||||
pub fn checkbox(ui: &mut egui::Ui, checked: bool, text: impl Into<String>, cb: impl FnOnce()) {
|
||||
let (text_value, color) = match checked {
|
||||
true => (format!("{} {}", CHECK_SQUARE, text), Colors::text_button()),
|
||||
false => (format!("{} {}", SQUARE, text), Colors::checkbox())
|
||||
true => (format!("{} {}", CHECK_SQUARE, text.into()), Colors::text_button()),
|
||||
false => (format!("{} {}", SQUARE, text.into()), Colors::checkbox())
|
||||
};
|
||||
|
||||
let br = Button::new(RichText::new(text_value).size(17.0).color(color))
|
||||
@@ -607,21 +540,24 @@ impl View {
|
||||
.fill(Colors::TRANSPARENT)
|
||||
.ui(ui)
|
||||
.on_hover_cursor(CursorIcon::PointingHand);
|
||||
if Self::touched(ui, br) {
|
||||
(callback)();
|
||||
if br.clicked() {
|
||||
cb();
|
||||
}
|
||||
}
|
||||
|
||||
/// Show a [`RadioButton`]. It is selected if `*current_value == selected_value`.
|
||||
/// If clicked, `selected_value` is assigned to `*current_value`.
|
||||
pub fn radio_value<T: PartialEq>(ui: &mut egui::Ui, current: &mut T, value: T, text: String) {
|
||||
pub fn radio_value<T: PartialEq>(ui: &mut egui::Ui,
|
||||
current: &mut T,
|
||||
value: T,
|
||||
text: impl Into<String>) {
|
||||
ui.scope(|ui| {
|
||||
// Setup background color.
|
||||
ui.visuals_mut().widgets.inactive.bg_fill = Colors::fill_deep();
|
||||
// Draw radio button.
|
||||
let mut response = ui.radio(*current == value, text)
|
||||
let mut response = ui.radio(*current == value, text.into())
|
||||
.on_hover_cursor(CursorIcon::PointingHand);
|
||||
if Self::touched(ui, response.clone()) && *current != value {
|
||||
if response.clicked() && *current != value {
|
||||
*current = value;
|
||||
response.mark_changed();
|
||||
}
|
||||
@@ -634,16 +570,57 @@ impl View {
|
||||
let (line_rect, _) = ui.allocate_exact_size(line_size, Sense::hover());
|
||||
let painter = ui.painter();
|
||||
painter.hline(line_rect.x_range(),
|
||||
painter.round_to_pixel(line_rect.center().y),
|
||||
line_rect.center().y.round_to_pixels(painter.pixels_per_point()),
|
||||
Stroke { width: 1.0, color });
|
||||
}
|
||||
|
||||
/// Draw line for panel content.
|
||||
pub fn line(ui: &mut egui::Ui, pos: LinePosition, rect: &Rect, color: Color32) {
|
||||
let points = match pos {
|
||||
LinePosition::RIGHT => {
|
||||
vec![{
|
||||
let mut r = rect.clone();
|
||||
r.min.x = r.max.x;
|
||||
r.min
|
||||
}, rect.max]
|
||||
}
|
||||
LinePosition::BOTTOM => {
|
||||
vec![{
|
||||
let mut r = rect.clone();
|
||||
r.min.y = r.max.y;
|
||||
r.min
|
||||
}, rect.max]
|
||||
}
|
||||
LinePosition::LEFT => {
|
||||
vec![rect.min, {
|
||||
let mut r = rect.clone();
|
||||
r.max.x = r.min.x;
|
||||
r.max
|
||||
}]
|
||||
}
|
||||
LinePosition::TOP => {
|
||||
vec![rect.min, {
|
||||
let mut r = rect.clone();
|
||||
r.max.y = r.min.y;
|
||||
r.max
|
||||
}]
|
||||
}
|
||||
};
|
||||
let stroke = PathShape {
|
||||
points,
|
||||
closed: false,
|
||||
fill: Default::default(),
|
||||
stroke: PathStroke::new(1.0, color),
|
||||
};
|
||||
ui.painter().add(stroke);
|
||||
}
|
||||
|
||||
/// Draw SVG image from provided data with optional provided size.
|
||||
pub fn svg_image(ui: &mut egui::Ui,
|
||||
name: &str,
|
||||
svg: &[u8],
|
||||
size: Option<SizeHint>) -> TextureHandle {
|
||||
let color_img = load_svg_bytes_with_size(svg, size).unwrap();
|
||||
size: SizeHint) -> TextureHandle {
|
||||
let color_img = load_svg_bytes_with_size(svg, size, &usvg::Options::default()).unwrap();
|
||||
// Create image texture.
|
||||
let texture_handle = ui.ctx().load_texture(name,
|
||||
color_img.clone(),
|
||||
@@ -683,6 +660,21 @@ impl View {
|
||||
);
|
||||
}
|
||||
|
||||
/// Draw semi-transparent cover at specified area.
|
||||
pub fn content_cover_ui(ui: &mut egui::Ui,
|
||||
rect: Rect,
|
||||
id: impl std::hash::Hash,
|
||||
mut on_click: impl FnMut()) {
|
||||
let resp = ui.interact(rect, egui::Id::new(id), Sense::click_and_drag());
|
||||
if resp.clicked() || resp.dragged() {
|
||||
on_click();
|
||||
}
|
||||
let shape = RectShape::filled(resp.rect,
|
||||
CornerRadius::ZERO,
|
||||
Colors::semi_transparent().gamma_multiply(0.7));
|
||||
ui.painter().add(shape);
|
||||
}
|
||||
|
||||
/// Get top display inset (cutout) size.
|
||||
pub fn get_top_inset() -> f32 {
|
||||
TOP_DISPLAY_INSET.load(Ordering::Relaxed) as f32
|
||||
@@ -734,34 +726,4 @@ pub extern "C" fn Java_mw_gri_android_MainActivity_onDisplayInsets(
|
||||
RIGHT_DISPLAY_INSET.store(array[1], Ordering::Relaxed);
|
||||
BOTTOM_DISPLAY_INSET.store(array[2], Ordering::Relaxed);
|
||||
LEFT_DISPLAY_INSET.store(array[3], Ordering::Relaxed);
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
static ref LAST_SOFT_KEYBOARD_INPUT: Arc<RwLock<String>> = Arc::new(RwLock::new("".into()));
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[cfg(target_os = "android")]
|
||||
#[allow(non_snake_case)]
|
||||
#[no_mangle]
|
||||
/// Callback from Java code with last entered character from soft keyboard.
|
||||
pub extern "C" fn Java_mw_gri_android_MainActivity_onInput(
|
||||
_env: jni::JNIEnv,
|
||||
_class: jni::objects::JObject,
|
||||
char: jni::sys::jstring
|
||||
) {
|
||||
use jni::objects::JString;
|
||||
use std::ops::Add;
|
||||
|
||||
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_INPUT.write();
|
||||
*w_input = w_input.clone().add(str);
|
||||
}
|
||||
Err(_) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
+559
-431
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,379 @@
|
||||
// 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, CameraScanContent};
|
||||
use crate::gui::views::types::{LinePosition, ContentContainer, 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 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").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(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").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").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,459 +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, FOLDER_PLUS, SCAN, SHARE_FAT};
|
||||
use crate::gui::platform::PlatformCallbacks;
|
||||
use crate::gui::views::{Modal, Content, View};
|
||||
use crate::gui::views::types::{ModalPosition, TextEditOptions};
|
||||
use crate::gui::views::wallets::creation::MnemonicSetup;
|
||||
use crate::gui::views::wallets::creation::types::Step;
|
||||
use crate::gui::views::wallets::settings::ConnectionSettings;
|
||||
use crate::node::Node;
|
||||
use crate::wallet::{ExternalConnection, Wallet};
|
||||
use crate::wallet::types::PhraseMode;
|
||||
|
||||
/// Wallet creation content.
|
||||
pub struct WalletCreation {
|
||||
/// Wallet creation step.
|
||||
step: Option<Step>,
|
||||
|
||||
/// Flag to check if wallet creation [`Modal`] was just opened to focus on first field.
|
||||
modal_just_opened: bool,
|
||||
/// Wallet name value.
|
||||
name_edit: String,
|
||||
/// Password to encrypt created wallet.
|
||||
pass_edit: String,
|
||||
|
||||
/// Mnemonic phrase setup content.
|
||||
pub(crate) mnemonic_setup: MnemonicSetup,
|
||||
/// Network setup content.
|
||||
pub(crate) network_setup: ConnectionSettings
|
||||
}
|
||||
|
||||
impl Default for WalletCreation {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
step: None,
|
||||
modal_just_opened: true,
|
||||
name_edit: String::from(""),
|
||||
pass_edit: String::from(""),
|
||||
mnemonic_setup: MnemonicSetup::default(),
|
||||
network_setup: ConnectionSettings::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl WalletCreation {
|
||||
/// Wallet name/password input modal identifier.
|
||||
pub const NAME_PASS_MODAL: &'static str = "name_pass_modal";
|
||||
|
||||
/// Draw wallet creation content.
|
||||
pub fn ui(&mut self,
|
||||
ui: &mut egui::Ui,
|
||||
cb: &dyn PlatformCallbacks,
|
||||
on_create: impl FnOnce(Wallet)) {
|
||||
// Show wallet creation step description and confirmation panel.
|
||||
if self.step.is_some() {
|
||||
egui::TopBottomPanel::bottom("wallet_creation_step_panel")
|
||||
.frame(egui::Frame {
|
||||
fill: Colors::fill(),
|
||||
inner_margin: Margin {
|
||||
left: View::far_left_inset_margin(ui) + 8.0,
|
||||
right: View::get_right_inset() + 8.0,
|
||||
top: 4.0,
|
||||
bottom: View::get_bottom_inset(),
|
||||
},
|
||||
..Default::default()
|
||||
})
|
||||
.show_inside(ui, |ui| {
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.vertical_centered(|ui| {
|
||||
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 2.0, |ui| {
|
||||
self.step_control_ui(ui, on_create, cb);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Show wallet creation step content panel.
|
||||
egui::CentralPanel::default()
|
||||
.frame(egui::Frame {
|
||||
stroke: View::item_stroke(),
|
||||
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| {
|
||||
let id = if let Some(step) = &self.step {
|
||||
format!("creation_step_scroll_{}", step.name())
|
||||
} else {
|
||||
"creation_step_scroll".to_owned()
|
||||
};
|
||||
ScrollArea::vertical()
|
||||
.id_source(id)
|
||||
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
|
||||
.auto_shrink([false; 2])
|
||||
.show(ui, |ui| {
|
||||
ui.vertical_centered(|ui| {
|
||||
let max_width = if self.step == Some(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) {
|
||||
if let Some(step) = self.step.clone() {
|
||||
// Setup step description text and availability.
|
||||
let (step_text, mut step_available) = match step {
|
||||
Step::EnterMnemonic => {
|
||||
let mode = &self.mnemonic_setup.mnemonic.mode;
|
||||
let text = if mode == &PhraseMode::Generate {
|
||||
t!("wallets.create_phrase_desc")
|
||||
} else {
|
||||
t!("wallets.restore_phrase_desc")
|
||||
};
|
||||
let available = !self
|
||||
.mnemonic_setup
|
||||
.mnemonic
|
||||
.words
|
||||
.contains(&String::from(""));
|
||||
(text, available)
|
||||
}
|
||||
Step::ConfirmMnemonic => {
|
||||
let text = t!("wallets.restore_phrase_desc");
|
||||
let available = !self
|
||||
.mnemonic_setup
|
||||
.mnemonic
|
||||
.confirm_words
|
||||
.contains(&String::from(""));
|
||||
(text, available)
|
||||
},
|
||||
Step::SetupConnection => (t!("wallets.setup_conn_desc"), true)
|
||||
};
|
||||
// Show step description.
|
||||
ui.add_space(2.0);
|
||||
ui.label(RichText::new(step_text).size(16.0).color(Colors::gray()));
|
||||
ui.add_space(2.0);
|
||||
// Show error if entered phrase is not valid.
|
||||
if !self.mnemonic_setup.valid_phrase {
|
||||
step_available = false;
|
||||
ui.label(RichText::new(t!("wallets.not_valid_phrase"))
|
||||
.size(16.0)
|
||||
.color(Colors::red()));
|
||||
ui.add_space(2.0);
|
||||
}
|
||||
if step == Step::EnterMnemonic {
|
||||
ui.add_space(4.0);
|
||||
|
||||
// Setup spacing between buttons.
|
||||
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
|
||||
|
||||
ui.columns(2, |columns| {
|
||||
// Show copy or paste button for mnemonic phrase step.
|
||||
columns[0].vertical_centered_justified(|ui| {
|
||||
self.copy_or_paste_button_ui(ui, cb);
|
||||
});
|
||||
|
||||
columns[1].vertical_centered_justified(|ui| {
|
||||
if step_available {
|
||||
// Show next step button if there are no empty words.
|
||||
self.next_step_button_ui(ui, step, on_create);
|
||||
} else {
|
||||
// Show QR code scan button.
|
||||
let scan_text = format!("{} {}", SCAN, t!("scan").to_uppercase());
|
||||
View::button(ui, scan_text, Colors::white_or_black(false), || {
|
||||
self.mnemonic_setup.show_qr_scan_modal(cb);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
ui.add_space(4.0);
|
||||
} else if step == Step::ConfirmMnemonic {
|
||||
ui.add_space(4.0);
|
||||
// Show next step or paste button.
|
||||
if step_available {
|
||||
self.next_step_button_ui(ui, step, 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().trim());
|
||||
self.mnemonic_setup.mnemonic.import_text(&data, true);
|
||||
});
|
||||
}
|
||||
ui.add_space(4.0);
|
||||
} else if step_available {
|
||||
ui.add_space(4.0);
|
||||
self.next_step_button_ui(ui, step, on_create);
|
||||
ui.add_space(4.0);
|
||||
}
|
||||
ui.add_space(4.0);
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw copy or paste button at [`Step::EnterMnemonic`].
|
||||
fn copy_or_paste_button_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||
match self.mnemonic_setup.mnemonic.mode {
|
||||
PhraseMode::Generate => {
|
||||
// Show copy button.
|
||||
let c_t = format!("{} {}", COPY, t!("copy").to_uppercase());
|
||||
View::button(ui, c_t.to_uppercase(), Colors::white_or_black(false), || {
|
||||
cb.copy_string_to_buffer(self.mnemonic_setup.mnemonic.get_phrase());
|
||||
});
|
||||
}
|
||||
PhraseMode::Import => {
|
||||
// Show paste button.
|
||||
let p_t = format!("{} {}", CLIPBOARD_TEXT, t!("paste").to_uppercase());
|
||||
View::button(ui, p_t, Colors::white_or_black(false), || {
|
||||
let data = ZeroingString::from(cb.get_string_from_buffer().trim());
|
||||
self.mnemonic_setup.mnemonic.import_text(&data, false);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw button to go to next [`Step`].
|
||||
fn next_step_button_ui(&mut self,
|
||||
ui: &mut egui::Ui,
|
||||
step: Step,
|
||||
on_create: impl FnOnce(Wallet)) {
|
||||
// Setup button text.
|
||||
let (next_text, text_color, bg_color) = if step == Step::SetupConnection {
|
||||
(format!("{} {}", CHECK, t!("complete")), Colors::title(true), Colors::gold())
|
||||
} else {
|
||||
let text = format!("{} {}", SHARE_FAT, t!("continue"));
|
||||
(text, Colors::text_button(), Colors::white_or_black(false))
|
||||
};
|
||||
|
||||
// Show next step button.
|
||||
View::colored_text_button(ui, next_text.to_uppercase(), text_color, bg_color, || {
|
||||
self.step = if let Some(step) = &self.step {
|
||||
match step {
|
||||
Step::EnterMnemonic => {
|
||||
if self.mnemonic_setup.mnemonic.mode == PhraseMode::Generate {
|
||||
Some(Step::ConfirmMnemonic)
|
||||
} else {
|
||||
// Check if entered phrase was valid.
|
||||
if self.mnemonic_setup.valid_phrase {
|
||||
Some(Step::SetupConnection)
|
||||
} else {
|
||||
Some(Step::EnterMnemonic)
|
||||
}
|
||||
}
|
||||
}
|
||||
Step::ConfirmMnemonic => {
|
||||
Some(Step::SetupConnection)
|
||||
},
|
||||
Step::SetupConnection => {
|
||||
// Create wallet at last step.
|
||||
let name = self.name_edit.clone();
|
||||
let pass = self.pass_edit.clone();
|
||||
let phrase = self.mnemonic_setup.mnemonic.get_phrase();
|
||||
let conn_method = &self.network_setup.method;
|
||||
let mut wallet = Wallet::create(name,
|
||||
pass.clone(),
|
||||
phrase,
|
||||
conn_method).unwrap();
|
||||
// Open created wallet.
|
||||
wallet.open(pass).unwrap();
|
||||
// Pass created wallet to callback.
|
||||
(on_create)(wallet);
|
||||
// Reset input data.
|
||||
self.step = None;
|
||||
self.name_edit = String::from("");
|
||||
self.pass_edit = String::from("");
|
||||
self.mnemonic_setup.reset();
|
||||
None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Some(Step::EnterMnemonic)
|
||||
};
|
||||
|
||||
// Check external connections availability on connection setup.
|
||||
if self.step == Some(Step::SetupConnection) {
|
||||
ExternalConnection::check_ext_conn_availability(None);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Draw wallet creation [`Step`] content.
|
||||
fn step_content_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||
match &self.step {
|
||||
None => {
|
||||
// Show wallet creation message if step is empty.
|
||||
View::center_content(ui, 350.0 + View::get_bottom_inset(), |ui| {
|
||||
// Show app logo.
|
||||
View::app_logo_name_version(ui);
|
||||
ui.add_space(4.0);
|
||||
|
||||
let text = t!("wallets.create_desc");
|
||||
ui.label(RichText::new(text)
|
||||
.size(16.0)
|
||||
.color(Colors::gray())
|
||||
);
|
||||
ui.add_space(8.0);
|
||||
// Show wallet creation button.
|
||||
let add_text = format!("{} {}", FOLDER_PLUS, t!("wallets.add"));
|
||||
View::button(ui, add_text, Colors::white_or_black(false), || {
|
||||
self.show_name_pass_modal(cb);
|
||||
});
|
||||
});
|
||||
}
|
||||
Some(step) => {
|
||||
match 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() {
|
||||
ui.ctx().request_repaint_after(Node::STATS_UPDATE_DELAY);
|
||||
}
|
||||
self.network_setup.create_ui(ui, cb)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if it's possible to go back for current step.
|
||||
pub fn can_go_back(&self) -> bool {
|
||||
self.step.is_some()
|
||||
}
|
||||
|
||||
/// Back to previous wallet creation [`Step`].
|
||||
pub fn back(&mut self) {
|
||||
match &self.step {
|
||||
None => {}
|
||||
Some(step) => {
|
||||
match step {
|
||||
Step::EnterMnemonic => {
|
||||
self.step = None;
|
||||
self.name_edit = String::from("");
|
||||
self.pass_edit = String::from("");
|
||||
self.mnemonic_setup.reset();
|
||||
},
|
||||
Step::ConfirmMnemonic => self.step = Some(Step::EnterMnemonic),
|
||||
Step::SetupConnection => self.step = Some(Step::EnterMnemonic)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Start wallet creation from showing [`Modal`] to enter name and password.
|
||||
pub fn show_name_pass_modal(&mut self, cb: &dyn PlatformCallbacks) {
|
||||
// Reset modal values.
|
||||
self.modal_just_opened = true;
|
||||
self.name_edit = t!("wallets.default_wallet");
|
||||
self.pass_edit = String::from("");
|
||||
// Show modal.
|
||||
Modal::new(Self::NAME_PASS_MODAL)
|
||||
.position(ModalPosition::CenterTop)
|
||||
.title(t!("wallets.add"))
|
||||
.show();
|
||||
cb.show_keyboard();
|
||||
}
|
||||
|
||||
/// Draw creating wallet name/password input [`Modal`] content.
|
||||
pub fn name_pass_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!("wallets.name"))
|
||||
.size(17.0)
|
||||
.color(Colors::gray()));
|
||||
ui.add_space(8.0);
|
||||
|
||||
// Show wallet name text edit.
|
||||
let mut name_edit_opts = TextEditOptions::new(Id::from(modal.id).with("name"))
|
||||
.no_focus();
|
||||
if self.modal_just_opened {
|
||||
self.modal_just_opened = 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"))
|
||||
.size(17.0)
|
||||
.color(Colors::gray()));
|
||||
ui.add_space(8.0);
|
||||
|
||||
// Draw wallet password text edit.
|
||||
let mut pass_text_edit_opts = TextEditOptions::new(Id::from(modal.id).with("pass"))
|
||||
.password()
|
||||
.no_focus();
|
||||
View::text_edit(ui, cb, &mut self.pass_edit, &mut pass_text_edit_opts);
|
||||
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.
|
||||
cb.hide_keyboard();
|
||||
modal.close();
|
||||
});
|
||||
});
|
||||
columns[1].vertical_centered_justified(|ui| {
|
||||
let mut on_next = || {
|
||||
// Check if input values are not empty.
|
||||
if self.name_edit.is_empty() || self.pass_edit.is_empty() {
|
||||
return;
|
||||
}
|
||||
self.step = Some(Step::EnterMnemonic);
|
||||
cb.hide_keyboard();
|
||||
modal.close();
|
||||
};
|
||||
|
||||
// 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -17,62 +17,43 @@ use egui::{Id, RichText};
|
||||
use crate::gui::Colors;
|
||||
use crate::gui::icons::PENCIL;
|
||||
use crate::gui::platform::PlatformCallbacks;
|
||||
use crate::gui::views::{CameraContent, Modal, Content, View};
|
||||
use crate::gui::views::types::{ModalContainer, ModalPosition, QrScanResult, TextEditOptions};
|
||||
use crate::gui::views::{Modal, Content, View, TextEdit};
|
||||
use crate::gui::views::types::{ContentContainer, ModalPosition};
|
||||
use crate::wallet::Mnemonic;
|
||||
use crate::wallet::types::{PhraseMode, PhraseSize};
|
||||
use crate::wallet::types::{PhraseMode, PhraseSize, PhraseWord};
|
||||
|
||||
/// Mnemonic phrase setup content.
|
||||
pub struct MnemonicSetup {
|
||||
/// Current mnemonic phrase.
|
||||
pub(crate) mnemonic: Mnemonic,
|
||||
|
||||
/// Flag to check if entered phrase was valid.
|
||||
pub(crate) valid_phrase: bool,
|
||||
pub mnemonic: Mnemonic,
|
||||
|
||||
/// Current word number to edit at [`Modal`].
|
||||
word_num_edit: usize,
|
||||
word_index_edit: usize,
|
||||
/// Entered word value for [`Modal`].
|
||||
word_edit: String,
|
||||
/// Flag to check if entered word is valid.
|
||||
/// Flag to check if entered word is valid at [`Modal`].
|
||||
valid_word_edit: bool,
|
||||
|
||||
/// Camera content for QR scan [`Modal`].
|
||||
camera_content: CameraContent,
|
||||
/// Flag to check if recovery phrase was found at QR code scanning [`Modal`].
|
||||
scan_phrase_not_found: Option<bool>,
|
||||
|
||||
/// [`Modal`] identifiers allowed at this ui container.
|
||||
modal_ids: Vec<&'static str>
|
||||
}
|
||||
|
||||
/// Identifier for word input [`Modal`].
|
||||
pub const WORD_INPUT_MODAL: &'static str = "word_input_modal";
|
||||
|
||||
/// Identifier for QR code recovery phrase scan [`Modal`].
|
||||
const QR_CODE_PHRASE_SCAN_MODAL: &'static str = "qr_code_rec_phrase_scan_modal";
|
||||
|
||||
impl Default for MnemonicSetup {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
mnemonic: Mnemonic::default(),
|
||||
valid_phrase: true,
|
||||
word_num_edit: 0,
|
||||
word_index_edit: 0,
|
||||
word_edit: String::from(""),
|
||||
valid_word_edit: true,
|
||||
camera_content: CameraContent::default(),
|
||||
scan_phrase_not_found: None,
|
||||
modal_ids: vec![
|
||||
WORD_INPUT_MODAL,
|
||||
QR_CODE_PHRASE_SCAN_MODAL
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ModalContainer for MnemonicSetup {
|
||||
fn modal_ids(&self) -> &Vec<&'static str> {
|
||||
&self.modal_ids
|
||||
impl ContentContainer for MnemonicSetup {
|
||||
fn modal_ids(&self) -> Vec<&'static str> {
|
||||
vec![
|
||||
WORD_INPUT_MODAL
|
||||
]
|
||||
}
|
||||
|
||||
fn modal_ui(&mut self,
|
||||
@@ -81,49 +62,22 @@ impl ModalContainer for MnemonicSetup {
|
||||
cb: &dyn PlatformCallbacks) {
|
||||
match modal.id {
|
||||
WORD_INPUT_MODAL => self.word_modal_ui(ui, modal, cb),
|
||||
QR_CODE_PHRASE_SCAN_MODAL => self.scan_qr_modal_ui(ui, modal, cb),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn container_ui(&mut self, _: &mut egui::Ui, _: &dyn PlatformCallbacks) {
|
||||
}
|
||||
}
|
||||
|
||||
impl MnemonicSetup {
|
||||
/// Draw content for phrase input step.
|
||||
pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||
// Draw modal content for current ui container.
|
||||
self.current_modal_ui(ui, cb);
|
||||
|
||||
/// Draw content for phrase enter step.
|
||||
pub fn enter_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||
self.ui(ui, cb);
|
||||
ui.add_space(10.0);
|
||||
|
||||
// Show mode and type setup.
|
||||
self.mode_type_ui(ui);
|
||||
|
||||
ui.add_space(12.0);
|
||||
View::horizontal_line(ui, Colors::item_stroke());
|
||||
ui.add_space(6.0);
|
||||
|
||||
// Show words setup.
|
||||
self.word_list_ui(ui, self.mnemonic.mode == PhraseMode::Import, cb);
|
||||
}
|
||||
|
||||
/// Draw content for phrase confirmation step.
|
||||
pub fn confirm_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||
// Draw modal content for current ui container.
|
||||
self.current_modal_ui(ui, cb);
|
||||
|
||||
ui.add_space(4.0);
|
||||
ui.vertical_centered(|ui| {
|
||||
let text = format!("{}:", t!("wallets.recovery_phrase"));
|
||||
ui.label(RichText::new(text).size(16.0).color(Colors::gray()));
|
||||
});
|
||||
ui.add_space(4.0);
|
||||
self.word_list_ui(ui, true, cb);
|
||||
}
|
||||
|
||||
/// Draw mode and size setup.
|
||||
fn mode_type_ui(&mut self, ui: &mut egui::Ui) {
|
||||
// Show mode setup.
|
||||
let mut mode = self.mnemonic.mode.clone();
|
||||
let mut mode = self.mnemonic.mode();
|
||||
ui.columns(2, |columns| {
|
||||
columns[0].vertical_centered(|ui| {
|
||||
let create_mode = PhraseMode::Generate;
|
||||
@@ -136,8 +90,8 @@ impl MnemonicSetup {
|
||||
View::radio_value(ui, &mut mode, import_mode, import_text);
|
||||
});
|
||||
});
|
||||
if mode != self.mnemonic.mode {
|
||||
self.mnemonic.set_mode(mode)
|
||||
if mode != self.mnemonic.mode() {
|
||||
self.mnemonic.set_mode(mode);
|
||||
}
|
||||
|
||||
ui.add_space(10.0);
|
||||
@@ -150,7 +104,7 @@ impl MnemonicSetup {
|
||||
ui.add_space(6.0);
|
||||
|
||||
// Show mnemonic phrase size setup.
|
||||
let mut size = self.mnemonic.size.clone();
|
||||
let mut size = self.mnemonic.size();
|
||||
ui.columns(5, |columns| {
|
||||
for (index, word) in PhraseSize::VALUES.iter().enumerate() {
|
||||
columns[index].vertical_centered(|ui| {
|
||||
@@ -159,29 +113,39 @@ impl MnemonicSetup {
|
||||
});
|
||||
}
|
||||
});
|
||||
if size != self.mnemonic.size {
|
||||
if size != self.mnemonic.size() {
|
||||
self.mnemonic.set_size(size);
|
||||
}
|
||||
|
||||
ui.add_space(12.0);
|
||||
View::horizontal_line(ui, Colors::item_stroke());
|
||||
ui.add_space(6.0);
|
||||
|
||||
// Show words setup.
|
||||
self.word_list_ui(ui, self.mnemonic.mode() == PhraseMode::Import);
|
||||
}
|
||||
/// 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.vertical_centered(|ui| {
|
||||
let text = format!("{}:", t!("wallets.recovery_phrase"));
|
||||
ui.label(RichText::new(text).size(16.0).color(Colors::gray()));
|
||||
});
|
||||
ui.add_space(4.0);
|
||||
self.word_list_ui(ui, true);
|
||||
}
|
||||
|
||||
/// Draw list of words for mnemonic phrase.
|
||||
fn word_list_ui(&mut self, ui: &mut egui::Ui, edit_words: bool, cb: &dyn PlatformCallbacks) {
|
||||
/// Draw grid of words for mnemonic phrase.
|
||||
fn word_list_ui(&mut self, ui: &mut egui::Ui, edit: bool) {
|
||||
ui.add_space(6.0);
|
||||
ui.scope(|ui| {
|
||||
// Setup spacing between columns.
|
||||
ui.spacing_mut().item_spacing = egui::Vec2::new(6.0, 6.0);
|
||||
|
||||
// Select list of words based on current mode and edit flag.
|
||||
let words = match self.mnemonic.mode {
|
||||
PhraseMode::Generate => {
|
||||
if edit_words {
|
||||
&self.mnemonic.confirm_words
|
||||
} else {
|
||||
&self.mnemonic.words
|
||||
}
|
||||
}
|
||||
PhraseMode::Import => &self.mnemonic.words
|
||||
}.clone();
|
||||
let words = self.mnemonic.words(edit);
|
||||
|
||||
let mut word_number = 0;
|
||||
let cols = list_columns_count(ui);
|
||||
@@ -192,25 +156,25 @@ impl MnemonicSetup {
|
||||
ui.columns(cols, |columns| {
|
||||
columns[0].horizontal(|ui| {
|
||||
let word = chunk.get(0).unwrap();
|
||||
self.word_item_ui(ui, word_number, word, edit_words, cb);
|
||||
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_words, cb);
|
||||
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_words, cb);
|
||||
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_words, cb);
|
||||
self.word_item_ui(ui, word_number, word, edit);
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -218,7 +182,7 @@ impl MnemonicSetup {
|
||||
ui.columns(cols, |columns| {
|
||||
columns[0].horizontal(|ui| {
|
||||
let word = chunk.get(0).unwrap();
|
||||
self.word_item_ui(ui, word_number, word, edit_words, cb);
|
||||
self.word_item_ui(ui, word_number, word, edit);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -227,56 +191,79 @@ impl MnemonicSetup {
|
||||
ui.add_space(6.0);
|
||||
}
|
||||
|
||||
/// Draw word list item for current mode.
|
||||
/// Draw word grid item.
|
||||
fn word_item_ui(&mut self,
|
||||
ui: &mut egui::Ui,
|
||||
num: usize,
|
||||
word: &String,
|
||||
edit: bool,
|
||||
cb: &dyn PlatformCallbacks) {
|
||||
word: &PhraseWord,
|
||||
edit: bool) {
|
||||
let color = if !word.valid || (word.text.is_empty() && !self.mnemonic.valid()) {
|
||||
Colors::red()
|
||||
} else {
|
||||
Colors::white_or_black(true)
|
||||
};
|
||||
if edit {
|
||||
ui.add_space(6.0);
|
||||
View::button(ui, PENCIL.to_string(), Colors::button(), || {
|
||||
// Setup modal values.
|
||||
self.word_num_edit = num;
|
||||
self.word_edit = word.clone();
|
||||
self.valid_word_edit = true;
|
||||
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))
|
||||
ui.label(RichText::new(format!("#{} {}", num, word.text))
|
||||
.size(17.0)
|
||||
.color(Colors::white_or_black(true)));
|
||||
.color(color));
|
||||
} else {
|
||||
ui.add_space(12.0);
|
||||
let text = format!("#{} {}", num, word);
|
||||
ui.label(RichText::new(text).size(17.0).color(Colors::white_or_black(true)));
|
||||
let text = format!("#{} {}", num, word.text);
|
||||
ui.label(RichText::new(text).size(17.0).color(color));
|
||||
}
|
||||
}
|
||||
|
||||
/// Reset mnemonic phrase to default values.
|
||||
/// Reset mnemonic phrase state to default values.
|
||||
pub fn reset(&mut self) {
|
||||
self.mnemonic = Mnemonic::default();
|
||||
}
|
||||
|
||||
/// Draw word input [`Modal`] content.
|
||||
fn word_modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
|
||||
let on_save = |c: &mut MnemonicSetup| {
|
||||
// Insert word checking validity.
|
||||
let word = &c.word_edit.trim().to_string();
|
||||
c.valid_word_edit = c.mnemonic.insert(c.word_index_edit, word);
|
||||
if !c.valid_word_edit {
|
||||
return;
|
||||
}
|
||||
// Close modal or go to next word to edit.
|
||||
let next_word = c.mnemonic.get(c.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 {
|
||||
Modal::close();
|
||||
} else {
|
||||
c.word_index_edit += 1;
|
||||
c.word_edit = String::from("");
|
||||
}
|
||||
};
|
||||
|
||||
ui.add_space(6.0);
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.label(RichText::new(t!("wallets.enter_word", "number" => self.word_num_edit))
|
||||
ui.label(RichText::new(t!("wallets.enter_word", "number" => self.word_index_edit + 1))
|
||||
.size(17.0)
|
||||
.color(Colors::gray()));
|
||||
ui.add_space(8.0);
|
||||
|
||||
// Draw word value text edit.
|
||||
let mut text_edit_opts = TextEditOptions::new(
|
||||
Id::from(modal.id).with(self.word_num_edit)
|
||||
);
|
||||
View::text_edit(ui, cb, &mut self.word_edit, &mut text_edit_opts);
|
||||
let mut word_edit = TextEdit::new(Id::from(modal.id).with(self.word_index_edit));
|
||||
word_edit.ui(ui, &mut self.word_edit, cb);
|
||||
if word_edit.enter_pressed {
|
||||
on_save(self);
|
||||
}
|
||||
|
||||
// Show error when specified word is not valid.
|
||||
if !self.valid_word_edit {
|
||||
@@ -297,138 +284,19 @@ impl MnemonicSetup {
|
||||
columns[0].vertical_centered_justified(|ui| {
|
||||
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
|
||||
// Close modal.
|
||||
cb.hide_keyboard();
|
||||
modal.close();
|
||||
Modal::close();
|
||||
});
|
||||
});
|
||||
columns[1].vertical_centered_justified(|ui| {
|
||||
// Callback to save the word.
|
||||
let mut save = || {
|
||||
self.word_edit = self.word_edit.trim().to_string();
|
||||
|
||||
// Check if word is valid.
|
||||
let word_index = self.word_num_edit - 1;
|
||||
if !self.mnemonic.is_valid_word(&self.word_edit, word_index) {
|
||||
self.valid_word_edit = false;
|
||||
return;
|
||||
}
|
||||
self.valid_word_edit = true;
|
||||
|
||||
// Select list where to save word.
|
||||
let words = match self.mnemonic.mode {
|
||||
PhraseMode::Generate => &mut self.mnemonic.confirm_words,
|
||||
PhraseMode::Import => &mut self.mnemonic.words
|
||||
};
|
||||
|
||||
// Save word at list.
|
||||
words.remove(word_index);
|
||||
words.insert(word_index, self.word_edit.clone());
|
||||
|
||||
// Close modal or go to next word to edit.
|
||||
let close_modal = words.len() == self.word_num_edit
|
||||
|| !words.get(self.word_num_edit).unwrap().is_empty();
|
||||
if close_modal {
|
||||
// Check if entered phrase was valid when all words were entered.
|
||||
if !self.mnemonic.words.contains(&String::from("")) {
|
||||
self.valid_phrase = self.mnemonic.is_valid_phrase();
|
||||
}
|
||||
cb.hide_keyboard();
|
||||
modal.close();
|
||||
} else {
|
||||
self.word_num_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);
|
||||
View::button(ui, t!("continue"), Colors::white_or_black(false), || {
|
||||
on_save(self);
|
||||
});
|
||||
});
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
});
|
||||
}
|
||||
|
||||
/// Show QR code recovery phrase scanner [`Modal`].
|
||||
pub fn show_qr_scan_modal(&mut self, cb: &dyn PlatformCallbacks) {
|
||||
self.scan_phrase_not_found = None;
|
||||
// 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();
|
||||
}
|
||||
|
||||
/// Draw QR code scan [`Modal`] content.
|
||||
fn scan_qr_modal_ui(&mut self,
|
||||
ui: &mut egui::Ui,
|
||||
modal: &Modal,
|
||||
cb: &dyn PlatformCallbacks) {
|
||||
// Show scan result if exists or show camera content while scanning.
|
||||
if let Some(_) = &self.scan_phrase_not_found {
|
||||
ui.add_space(6.0);
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.label(RichText::new(t!("wallets.rec_phrase_not_found"))
|
||||
.size(17.0)
|
||||
.color(Colors::red()));
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
} else if let Some(result) = self.camera_content.qr_scan_result() {
|
||||
cb.stop_camera();
|
||||
self.camera_content.clear_state();
|
||||
match &result {
|
||||
QrScanResult::Text(text) => {
|
||||
self.mnemonic.import_text(text, false);
|
||||
if self.mnemonic.is_valid_phrase() {
|
||||
modal.close();
|
||||
return;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// Set an error when found phrase was not valid.
|
||||
self.scan_phrase_not_found = Some(true);
|
||||
Modal::set_title(t!("scan_result"));
|
||||
} else {
|
||||
ui.add_space(6.0);
|
||||
self.camera_content.ui(ui, cb);
|
||||
ui.add_space(6.0);
|
||||
}
|
||||
|
||||
if self.scan_phrase_not_found.is_some() {
|
||||
// Setup spacing between buttons.
|
||||
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
|
||||
|
||||
ui.columns(2, |columns| {
|
||||
columns[0].vertical_centered_justified(|ui| {
|
||||
View::button(ui, t!("close"), Colors::white_or_black(false), || {
|
||||
self.scan_phrase_not_found = None;
|
||||
modal.close();
|
||||
});
|
||||
});
|
||||
columns[1].vertical_centered_justified(|ui| {
|
||||
View::button(ui, t!("repeat"), Colors::white_or_black(false), || {
|
||||
Modal::set_title(t!("scan_qr"));
|
||||
self.scan_phrase_not_found = None;
|
||||
cb.start_camera();
|
||||
});
|
||||
});
|
||||
});
|
||||
} else {
|
||||
ui.vertical_centered_justified(|ui| {
|
||||
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
|
||||
cb.stop_camera();
|
||||
modal.close();
|
||||
});
|
||||
});
|
||||
}
|
||||
ui.add_space(6.0);
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculate word list columns count based on available ui width.
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
mod mnemonic;
|
||||
pub use mnemonic::MnemonicSetup;
|
||||
|
||||
mod creation;
|
||||
pub use creation::WalletCreation;
|
||||
mod content;
|
||||
pub use content::WalletCreationContent;
|
||||
|
||||
pub mod types;
|
||||
@@ -0,0 +1,109 @@
|
||||
// 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::{Id, RichText};
|
||||
use grin_util::ZeroingString;
|
||||
|
||||
use crate::gui::Colors;
|
||||
use crate::gui::platform::PlatformCallbacks;
|
||||
use crate::gui::views::{Modal, TextEdit, View};
|
||||
|
||||
/// Initial wallet creation [`Modal`] content.
|
||||
pub struct AddWalletModal {
|
||||
/// Wallet name.
|
||||
pub name_edit: String,
|
||||
/// Password to encrypt created wallet.
|
||||
pub pass_edit: String,
|
||||
}
|
||||
|
||||
impl Default for AddWalletModal {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
name_edit: t!("wallets.default_wallet").into(),
|
||||
pass_edit: "".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AddWalletModal {
|
||||
/// Draw creating wallet name/password input [`Modal`] content.
|
||||
pub fn ui(&mut self,
|
||||
ui: &mut egui::Ui,
|
||||
modal: &Modal,
|
||||
cb: &dyn PlatformCallbacks,
|
||||
mut on_input: impl FnMut(String, ZeroingString)) {
|
||||
let mut on_next = |m: &mut AddWalletModal| {
|
||||
let name = m.name_edit.clone();
|
||||
let pass = m.pass_edit.clone();
|
||||
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.
|
||||
let mut name_input = TextEdit::new(Id::from(modal.id).with("name"))
|
||||
.focus(Modal::first_draw());
|
||||
|
||||
name_input.ui(ui, &mut self.name_edit, cb);
|
||||
|
||||
ui.add_space(8.0);
|
||||
ui.label(RichText::new(t!("wallets.pass"))
|
||||
.size(17.0)
|
||||
.color(Colors::gray()));
|
||||
ui.add_space(8.0);
|
||||
|
||||
// Show wallet password text edit.
|
||||
let mut pass_input = TextEdit::new(Id::from(modal.id).with("pass"))
|
||||
.password()
|
||||
.focus(false);
|
||||
if name_input.enter_pressed {
|
||||
pass_input.focus_request();
|
||||
}
|
||||
pass_input.ui(ui, &mut self.pass_edit, cb);
|
||||
if pass_input.enter_pressed {
|
||||
(on_next)(self);
|
||||
}
|
||||
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!("continue"), Colors::white_or_black(false), || {
|
||||
(on_next)(self);
|
||||
});
|
||||
});
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
// 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::views::{Modal, View};
|
||||
use crate::gui::Colors;
|
||||
use crate::gui::icons::{BRACKETS_CURLY, GITHUB_LOGO, TELEGRAM_LOGO};
|
||||
|
||||
/// 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
// 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::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::gui::Colors;
|
||||
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,8 +12,17 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
mod conn;
|
||||
pub use conn::*;
|
||||
mod settings;
|
||||
pub use settings::*;
|
||||
|
||||
mod wallets;
|
||||
pub use wallets::*;
|
||||
mod list;
|
||||
pub use list::*;
|
||||
|
||||
mod open;
|
||||
pub use open::*;
|
||||
|
||||
mod add;
|
||||
pub use add::*;
|
||||
|
||||
mod changelog;
|
||||
pub use changelog::*;
|
||||
@@ -0,0 +1,108 @@
|
||||
// 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::{Id, RichText};
|
||||
use grin_util::ZeroingString;
|
||||
|
||||
use crate::gui::Colors;
|
||||
use crate::gui::platform::PlatformCallbacks;
|
||||
use crate::gui::views::{Modal, TextEdit, View};
|
||||
|
||||
/// Wallet opening [`Modal`] content.
|
||||
pub struct OpenWalletModal {
|
||||
/// Password to open wallet.
|
||||
pass_edit: String,
|
||||
/// Flag to check if wrong password was entered.
|
||||
wrong_pass: bool,
|
||||
}
|
||||
|
||||
impl OpenWalletModal {
|
||||
/// Create new content instance.
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
pass_edit: "".to_string(),
|
||||
wrong_pass: false,
|
||||
}
|
||||
}
|
||||
/// Draw [`Modal`] content.
|
||||
pub fn ui(&mut self,
|
||||
ui: &mut egui::Ui,
|
||||
modal: &Modal,
|
||||
cb: &dyn PlatformCallbacks,
|
||||
mut on_continue: impl FnMut(ZeroingString) -> bool) {
|
||||
// Callback for button to continue.
|
||||
let mut on_continue = |m: &mut OpenWalletModal| {
|
||||
let pass = m.pass_edit.clone();
|
||||
if pass.is_empty() {
|
||||
return;
|
||||
}
|
||||
m.wrong_pass = !on_continue(ZeroingString::from(pass));
|
||||
if !m.wrong_pass {
|
||||
m.pass_edit = "".to_string();
|
||||
Modal::close();
|
||||
}
|
||||
};
|
||||
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.add_space(6.0);
|
||||
ui.label(RichText::new(t!("wallets.pass"))
|
||||
.size(17.0)
|
||||
.color(Colors::gray()));
|
||||
ui.add_space(8.0);
|
||||
|
||||
// Show password input.
|
||||
let mut pass_edit = TextEdit::new(Id::from(modal.id).with("pass_edit")).password();
|
||||
pass_edit.ui(ui, &mut self.pass_edit, cb);
|
||||
if pass_edit.enter_pressed {
|
||||
(on_continue)(self);
|
||||
}
|
||||
|
||||
// Show information when password is empty.
|
||||
if self.pass_edit.is_empty() {
|
||||
self.wrong_pass = false;
|
||||
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);
|
||||
});
|
||||
|
||||
// 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!("continue"), Colors::white_or_black(false), || {
|
||||
(on_continue)(self);
|
||||
});
|
||||
});
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -12,37 +12,35 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use egui::{RichText, ScrollArea};
|
||||
use egui::scroll_area::ScrollBarVisibility;
|
||||
use egui::{RichText, ScrollArea};
|
||||
|
||||
use crate::gui::Colors;
|
||||
use crate::gui::icons::{CHECK, CHECK_FAT, PLUS_CIRCLE};
|
||||
use crate::gui::icons::{CHECK, PLUS_CIRCLE, TRASH};
|
||||
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::gui::views::network::ConnectionsContent;
|
||||
use crate::gui::views::types::ModalPosition;
|
||||
use crate::gui::views::wallets::WalletsContent;
|
||||
use crate::gui::views::{Modal, View};
|
||||
use crate::gui::Colors;
|
||||
use crate::wallet::types::ConnectionMethod;
|
||||
use crate::wallet::{ConnectionsConfig, ExternalConnection};
|
||||
|
||||
/// Wallet connection [`Modal`] content.
|
||||
pub struct WalletConnectionModal {
|
||||
/// Current external connection.
|
||||
pub ext_conn: Option<ExternalConnection>,
|
||||
|
||||
/// Flag to show connection creation.
|
||||
show_conn_creation: bool,
|
||||
/// Wallet connection selection [`Modal`] content.
|
||||
pub struct WalletSettingsModal {
|
||||
/// Current connection method.
|
||||
pub conn: ConnectionMethod,
|
||||
|
||||
/// External connection creation content.
|
||||
add_ext_conn_content: ExternalConnectionModal
|
||||
new_ext_conn_content: Option<ExternalConnectionModal>
|
||||
}
|
||||
|
||||
impl WalletConnectionModal {
|
||||
impl WalletSettingsModal {
|
||||
/// Create from provided wallet connection.
|
||||
pub fn new(ext_conn: Option<ExternalConnection>) -> Self {
|
||||
ExternalConnection::check_ext_conn_availability(None);
|
||||
pub fn new(conn: ConnectionMethod) -> Self {
|
||||
Self {
|
||||
ext_conn,
|
||||
show_conn_creation: false,
|
||||
add_ext_conn_content: ExternalConnectionModal::new(None),
|
||||
conn,
|
||||
new_ext_conn_content: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,17 +49,22 @@ impl WalletConnectionModal {
|
||||
ui: &mut egui::Ui,
|
||||
modal: &Modal,
|
||||
cb: &dyn PlatformCallbacks,
|
||||
on_select: impl Fn(Option<i64>)) {
|
||||
ui.add_space(4.0);
|
||||
|
||||
on_select: impl Fn(ConnectionMethod)) {
|
||||
// Draw external connection creation content.
|
||||
if self.show_conn_creation {
|
||||
self.add_ext_conn_content.ui(ui, cb, modal, |conn| {
|
||||
on_select(Some(conn.id));
|
||||
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 {
|
||||
@@ -69,7 +72,7 @@ impl WalletConnectionModal {
|
||||
} else {
|
||||
350.0
|
||||
})
|
||||
.id_source("integrated_node")
|
||||
.id_salt("connections_scroll")
|
||||
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
|
||||
.auto_shrink([true; 2])
|
||||
.show(ui, |ui| {
|
||||
@@ -77,30 +80,29 @@ impl WalletConnectionModal {
|
||||
|
||||
// Show integrated node selection.
|
||||
ConnectionsContent::integrated_node_item_ui(ui, |ui| {
|
||||
let is_current_method = self.ext_conn.is_none();
|
||||
if !is_current_method {
|
||||
View::item_button(ui, View::item_rounding(0, 1, true), CHECK, None, || {
|
||||
self.ext_conn = None;
|
||||
on_select(None);
|
||||
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);
|
||||
match self.conn {
|
||||
ConnectionMethod::Integrated => {
|
||||
View::selected_item_check(ui);
|
||||
}
|
||||
_ => {
|
||||
View::item_button(ui, View::item_rounding(0, 1, true), CHECK, None, || {
|
||||
on_select(ConnectionMethod::Integrated);
|
||||
Modal::close();
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Show button to add new external node connection.
|
||||
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::button(), || {
|
||||
self.show_conn_creation = true;
|
||||
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);
|
||||
@@ -109,27 +111,22 @@ impl WalletConnectionModal {
|
||||
ui.add_space(8.0);
|
||||
for (index, conn) in ext_conn_list.iter().enumerate() {
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
// Draw external connection item.
|
||||
let len = ext_conn_list.len();
|
||||
ConnectionsContent::ext_conn_item_ui(ui, conn, index, len, |ui| {
|
||||
// Draw button to select connection.
|
||||
let is_current_method = if let Some(c) = self.ext_conn.as_ref() {
|
||||
c.id == conn.id
|
||||
} else {
|
||||
false
|
||||
let current_ext_conn = match self.conn {
|
||||
ConnectionMethod::Integrated => false,
|
||||
ConnectionMethod::External(id, _) => id == conn.id
|
||||
};
|
||||
if !is_current_method {
|
||||
if current_ext_conn {
|
||||
View::selected_item_check(ui);
|
||||
} else {
|
||||
let button_rounding = View::item_rounding(index, len, true);
|
||||
View::item_button(ui, button_rounding, CHECK, None, || {
|
||||
self.ext_conn = Some(conn.clone());
|
||||
on_select(Some(conn.id));
|
||||
modal.close();
|
||||
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()));
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -139,13 +136,28 @@ impl WalletConnectionModal {
|
||||
});
|
||||
|
||||
ui.add_space(2.0);
|
||||
View::horizontal_line(ui, Colors::stroke());
|
||||
View::horizontal_line(ui, Colors::item_stroke());
|
||||
ui.add_space(6.0);
|
||||
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();
|
||||
Modal::close();
|
||||
});
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
@@ -1,135 +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, GLOBE_SIMPLE, PLUGS_CONNECTED};
|
||||
use crate::gui::views::{Modal, View};
|
||||
use crate::wallet::{Wallet, WalletList};
|
||||
|
||||
/// Wallet list [`Modal`] content
|
||||
pub struct WalletsModal {
|
||||
/// Selected wallet id.
|
||||
selected: Option<i64>
|
||||
}
|
||||
|
||||
impl WalletsModal {
|
||||
pub fn new(selected: Option<i64>) -> Self {
|
||||
Self {
|
||||
selected,
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw [`Modal`] content.
|
||||
pub fn ui(&mut self,
|
||||
ui: &mut egui::Ui,
|
||||
modal: &Modal,
|
||||
wallets: &WalletList,
|
||||
mut on_select: impl FnMut(i64)) {
|
||||
ui.add_space(4.0);
|
||||
ScrollArea::vertical()
|
||||
.max_height(373.0)
|
||||
.id_source("select_wallet_list")
|
||||
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
|
||||
.auto_shrink([true; 2])
|
||||
.show(ui, |ui| {
|
||||
ui.add_space(2.0);
|
||||
ui.vertical_centered(|ui| {
|
||||
for wallet in wallets.list() {
|
||||
// Draw wallet list item.
|
||||
self.wallet_item_ui(ui, wallet, modal, |id| {
|
||||
on_select(id);
|
||||
});
|
||||
ui.add_space(5.0);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
ui.add_space(2.0);
|
||||
View::horizontal_line(ui, Colors::stroke());
|
||||
ui.add_space(6.0);
|
||||
|
||||
// Show button to close modal.
|
||||
ui.vertical_centered_justified(|ui| {
|
||||
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
|
||||
modal.close();
|
||||
});
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
}
|
||||
|
||||
/// Draw wallet list item.
|
||||
fn wallet_item_ui(&mut self,
|
||||
ui: &mut egui::Ui,
|
||||
wallet: &Wallet,
|
||||
modal: &Modal,
|
||||
mut on_select: impl FnMut(i64)) {
|
||||
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| {
|
||||
// Draw button to select wallet.
|
||||
let current = self.selected.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(id);
|
||||
modal.close();
|
||||
});
|
||||
}
|
||||
|
||||
let layout_size = ui.available_size();
|
||||
ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| {
|
||||
ui.add_space(6.0);
|
||||
ui.vertical(|ui| {
|
||||
ui.add_space(3.0);
|
||||
// Setup 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));
|
||||
});
|
||||
|
||||
// Setup wallet connection text.
|
||||
let conn = if let Some(conn) = wallet.get_current_ext_conn() {
|
||||
format!("{} {}", GLOBE_SIMPLE, conn.url)
|
||||
} else {
|
||||
format!("{} {}", COMPUTER_TOWER, t!("network.node"))
|
||||
};
|
||||
View::ellipsize_text(ui, conn, 15.0, Colors::text(false));
|
||||
ui.add_space(1.0);
|
||||
|
||||
// Setup wallet API text.
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,324 @@
|
||||
// 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 distr1ibuted on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use egui::{Align, Layout, RichText, StrokeKind};
|
||||
use grin_core::core::amount_to_hr_string;
|
||||
|
||||
use crate::gui::icons::{FOLDER_USER, PACKAGE, SCAN, SPINNER, USERS_THREE, USER_PLUS};
|
||||
use crate::gui::platform::PlatformCallbacks;
|
||||
use crate::gui::views::types::{ModalPosition, QrScanResult};
|
||||
use crate::gui::views::wallets::wallet::account::create::CreateAccountContent;
|
||||
use crate::gui::views::wallets::wallet::account::list::WalletAccountsContent;
|
||||
use crate::gui::views::wallets::wallet::types::{WalletContentContainer, GRIN};
|
||||
use crate::gui::views::{CameraContent, CameraScanContent, Content, Modal, View};
|
||||
use crate::gui::Colors;
|
||||
use crate::wallet::{Wallet, WalletConfig};
|
||||
use crate::wallet::types::WalletTask;
|
||||
|
||||
/// Wallet account panel content.
|
||||
pub struct AccountContent {
|
||||
/// Account list content.
|
||||
pub list_content: Option<WalletAccountsContent>,
|
||||
/// Account creation [`Modal`] content.
|
||||
create_account_content: CreateAccountContent,
|
||||
|
||||
/// QR code scan content.
|
||||
qr_scan_content: Option<CameraContent>,
|
||||
/// QR code scan result
|
||||
qr_scan_result: Option<QrScanResult>,
|
||||
}
|
||||
|
||||
/// Account creation [`Modal`] identifier.
|
||||
const CREATE_MODAL_ID: &'static str = "create_account_modal";
|
||||
|
||||
impl WalletContentContainer for AccountContent {
|
||||
fn modal_ids(&self) -> Vec<&'static str> {
|
||||
vec![
|
||||
CREATE_MODAL_ID
|
||||
]
|
||||
}
|
||||
|
||||
fn modal_ui(&mut self,
|
||||
ui: &mut egui::Ui,
|
||||
wallet: &Wallet,
|
||||
modal: &Modal,
|
||||
cb: &dyn PlatformCallbacks) {
|
||||
match modal.id {
|
||||
CREATE_MODAL_ID => self.create_account_content.ui(ui, wallet, modal, cb),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn container_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) {
|
||||
if self.qr_scan_showing() {
|
||||
self.qr_scan_ui(ui, wallet, cb);
|
||||
} else {
|
||||
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
|
||||
if self.list_content.is_some() {
|
||||
self.list_ui(ui, wallet);
|
||||
} else {
|
||||
// Show account content.
|
||||
self.account_ui(ui, wallet, cb);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for AccountContent {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
list_content: None,
|
||||
create_account_content: CreateAccountContent::default(),
|
||||
qr_scan_content: None,
|
||||
qr_scan_result: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AccountContent {
|
||||
/// Check if QR code scanner was opened.
|
||||
pub fn qr_scan_showing(&self) -> bool {
|
||||
self.qr_scan_content.is_some() || self.qr_scan_result.is_some()
|
||||
}
|
||||
|
||||
/// Close QR code scanner.
|
||||
pub fn close_qr_scan(&mut self, cb: &dyn PlatformCallbacks) {
|
||||
if !self.qr_scan_showing() {
|
||||
return;
|
||||
}
|
||||
cb.stop_camera();
|
||||
self.qr_scan_content = None;
|
||||
self.qr_scan_result = None;
|
||||
}
|
||||
|
||||
/// Check if it's possible to go back at navigation stack.
|
||||
pub fn can_back(&self) -> bool {
|
||||
self.qr_scan_showing() || self.list_content.is_some()
|
||||
}
|
||||
|
||||
/// Navigate back on navigation stack.
|
||||
pub fn back(&mut self, cb: &dyn PlatformCallbacks) {
|
||||
if self.qr_scan_showing() {
|
||||
self.close_qr_scan(cb);
|
||||
} else if self.list_content.is_some() {
|
||||
self.list_content = None;
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw wallet account content.
|
||||
fn account_ui(&mut self,
|
||||
ui: &mut egui::Ui,
|
||||
wallet: &Wallet,
|
||||
cb: &dyn PlatformCallbacks) {
|
||||
// Check wallet data.
|
||||
if wallet.get_data().is_none() {
|
||||
return;
|
||||
}
|
||||
|
||||
let data = wallet.get_data().unwrap();
|
||||
|
||||
let mut rect = ui.available_rect_before_wrap();
|
||||
rect.set_height(75.0);
|
||||
|
||||
// Draw round background.
|
||||
let rounding = View::item_rounding(0, 2, false);
|
||||
ui.painter().rect(rect,
|
||||
rounding,
|
||||
Colors::fill(),
|
||||
View::item_stroke(),
|
||||
StrokeKind::Outside);
|
||||
|
||||
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
|
||||
// Draw button to show QR code scanner.
|
||||
let wallet_synced = wallet.synced_from_node();
|
||||
if wallet_synced {
|
||||
View::item_button(ui, View::item_rounding(0, 2, true), SCAN, None, || {
|
||||
self.qr_scan_content = Some(CameraContent::default());
|
||||
cb.start_camera();
|
||||
});
|
||||
}
|
||||
|
||||
// Draw button to show list of accounts.
|
||||
let accounts = wallet.accounts();
|
||||
let accounts_icon = if accounts.len() > 1 {
|
||||
USERS_THREE
|
||||
} else {
|
||||
USER_PLUS
|
||||
};
|
||||
let rounding = if wallet_synced {
|
||||
View::item_rounding(1, 3, true)
|
||||
} else {
|
||||
View::item_rounding(0, 2, true)
|
||||
};
|
||||
View::item_button(ui, rounding, accounts_icon, None, || {
|
||||
if accounts.len() == 1 {
|
||||
self.create_account_content = CreateAccountContent::default();
|
||||
Modal::new(CREATE_MODAL_ID)
|
||||
.position(ModalPosition::CenterTop)
|
||||
.title(t!("wallets.accounts"))
|
||||
.show();
|
||||
} else {
|
||||
self.list_content = Some(
|
||||
WalletAccountsContent::new(accounts, wallet.get_config().account)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
let layout_size = ui.available_size();
|
||||
ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| {
|
||||
ui.add_space(8.0);
|
||||
ui.vertical(|ui| {
|
||||
ui.add_space(3.0);
|
||||
// Show spendable amount.
|
||||
let amount = amount_to_hr_string(data.info.amount_currently_spendable, true);
|
||||
let amount_text = format!("{} {}", amount, GRIN);
|
||||
ui.with_layout(Layout::left_to_right(Align::Min), |ui| {
|
||||
ui.add_space(1.0);
|
||||
ui.label(RichText::new(amount_text)
|
||||
.size(18.0)
|
||||
.color(Colors::white_or_black(true)));
|
||||
});
|
||||
ui.add_space(-2.0);
|
||||
|
||||
// Show account label.
|
||||
let account = wallet.get_config().account;
|
||||
let default_acc_label = WalletConfig::DEFAULT_ACCOUNT_LABEL.to_string();
|
||||
let acc_label = if account == default_acc_label {
|
||||
t!("wallets.default_account").into()
|
||||
} else {
|
||||
account.to_owned()
|
||||
};
|
||||
let acc_text = format!("{} {}", FOLDER_USER, acc_label);
|
||||
View::ellipsize_text(ui, acc_text, 15.0, Colors::text(false));
|
||||
|
||||
// Show confirmed height or sync progress.
|
||||
let status_text = if wallet.message_opening() {
|
||||
format!("{} {}", SPINNER, t!("wallets.loading"))
|
||||
} else if !wallet.syncing() {
|
||||
format!("{} {}", PACKAGE, data.info.last_confirmed_height)
|
||||
} else {
|
||||
let info_progress = wallet.info_sync_progress();
|
||||
if info_progress == 100 || info_progress == 0 {
|
||||
format!("{} {}", SPINNER, t!("wallets.wallet_loading"))
|
||||
} else {
|
||||
if wallet.is_repairing() {
|
||||
let rep_progress = wallet.repairing_progress();
|
||||
if rep_progress == 0 {
|
||||
format!("{} {}", SPINNER, t!("wallets.wallet_checking"))
|
||||
} else {
|
||||
format!("{} {}: {}%",
|
||||
SPINNER,
|
||||
t!("wallets.wallet_checking"),
|
||||
rep_progress)
|
||||
}
|
||||
} else {
|
||||
format!("{} {}: {}%",
|
||||
SPINNER,
|
||||
t!("wallets.wallet_loading"),
|
||||
info_progress)
|
||||
}
|
||||
}
|
||||
};
|
||||
let animate = wallet.syncing() || wallet.message_opening();
|
||||
View::animate_text(ui, status_text, 15.0, Colors::gray(), animate);
|
||||
})
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// Draw account list content.
|
||||
fn list_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet) {
|
||||
if let Some(accounts) = self.list_content.as_mut() {
|
||||
let mut selected = false;
|
||||
accounts.ui(ui, |acc| {
|
||||
let _ = wallet.set_active_account(&acc.label);
|
||||
selected = true;
|
||||
});
|
||||
if selected {
|
||||
self.list_content = None;
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
ui.add_space(2.0);
|
||||
View::horizontal_line(ui, Colors::item_stroke());
|
||||
ui.add_space(6.0);
|
||||
|
||||
// Setup spacing between buttons.
|
||||
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
|
||||
|
||||
// Show modal buttons.
|
||||
ui.columns(2, |columns| {
|
||||
columns[0].vertical_centered_justified(|ui| {
|
||||
View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || {
|
||||
self.list_content = None;
|
||||
});
|
||||
});
|
||||
columns[1].vertical_centered_justified(|ui| {
|
||||
View::button(ui, t!("modal.add"), Colors::white_or_black(false), || {
|
||||
self.list_content = None;
|
||||
self.create_account_content = CreateAccountContent::default();
|
||||
Modal::new(CREATE_MODAL_ID)
|
||||
.position(ModalPosition::CenterTop)
|
||||
.title(t!("wallets.accounts"))
|
||||
.show();
|
||||
});
|
||||
});
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
}
|
||||
|
||||
/// Draw QR code scanner content.
|
||||
fn qr_scan_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) {
|
||||
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH, |ui| {
|
||||
if self.qr_scan_content.is_some() {
|
||||
if let Some(result) = self.qr_scan_content.as_ref().unwrap().qr_scan_result() {
|
||||
cb.stop_camera();
|
||||
self.qr_scan_content = None;
|
||||
match result {
|
||||
QrScanResult::Address(_) => {
|
||||
//TODO: send with address
|
||||
}
|
||||
QrScanResult::Slatepack(m) => {
|
||||
wallet.task(WalletTask::OpenMessage(m));
|
||||
}
|
||||
_ => {
|
||||
self.qr_scan_result = Some(result);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Draw QR code scan content.
|
||||
self.qr_scan_content.as_mut().unwrap().ui(ui, cb);
|
||||
ui.add_space(6.0);
|
||||
ui.vertical_centered_justified(|ui| {
|
||||
View::button(ui, t!("close"), Colors::white_or_black(false), || {
|
||||
self.close_qr_scan(cb);
|
||||
});
|
||||
});
|
||||
}
|
||||
} else if let Some(res) = &self.qr_scan_result.clone() {
|
||||
CameraScanContent::result_ui(ui, res, cb, || {
|
||||
self.qr_scan_result = None;
|
||||
}, || {
|
||||
self.qr_scan_content = Some(CameraContent::default());
|
||||
cb.start_camera();
|
||||
});
|
||||
}
|
||||
ui.add_space(6.0);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
// 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::{Id, RichText};
|
||||
|
||||
use crate::gui::platform::PlatformCallbacks;
|
||||
use crate::gui::views::{Modal, TextEdit, View};
|
||||
use crate::gui::Colors;
|
||||
use crate::wallet::Wallet;
|
||||
|
||||
/// Account creation [`Modal`] content.
|
||||
pub struct CreateAccountContent {
|
||||
/// Account label value.
|
||||
account_label_edit: String,
|
||||
/// Flag to check if error occurred during account creation.
|
||||
account_creation_error: bool,
|
||||
}
|
||||
|
||||
impl Default for CreateAccountContent {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
account_label_edit: "".to_string(),
|
||||
account_creation_error: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CreateAccountContent {
|
||||
/// Draw account creation [`Modal`] content.
|
||||
pub fn ui(&mut self,
|
||||
ui: &mut egui::Ui,
|
||||
wallet: &Wallet,
|
||||
modal: &Modal,
|
||||
cb: &dyn PlatformCallbacks) {
|
||||
let on_create = |m: &mut CreateAccountContent| {
|
||||
if m.account_label_edit.is_empty() {
|
||||
return;
|
||||
}
|
||||
let label = &m.account_label_edit;
|
||||
match wallet.create_account(label) {
|
||||
Ok(_) => {
|
||||
let _ = wallet.set_active_account(label);
|
||||
Modal::close();
|
||||
},
|
||||
Err(_) => m.account_creation_error = true
|
||||
};
|
||||
};
|
||||
|
||||
ui.add_space(6.0);
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.label(RichText::new(t!("wallets.new_account_desc"))
|
||||
.size(17.0)
|
||||
.color(Colors::gray()));
|
||||
ui.add_space(8.0);
|
||||
|
||||
// Draw account name edit.
|
||||
let mut name_edit = TextEdit::new(Id::from(modal.id).with(wallet.get_config().id));
|
||||
name_edit.ui(ui, &mut self.account_label_edit, cb);
|
||||
if name_edit.enter_pressed {
|
||||
on_create(self);
|
||||
}
|
||||
|
||||
// Show error occurred during account creation.
|
||||
if self.account_creation_error {
|
||||
ui.add_space(12.0);
|
||||
ui.label(RichText::new(t!("error"))
|
||||
.size(17.0)
|
||||
.color(Colors::red()));
|
||||
}
|
||||
ui.add_space(12.0);
|
||||
});
|
||||
|
||||
// Setup spacing between buttons.
|
||||
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
|
||||
|
||||
// Show modal buttons.
|
||||
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!("create"), Colors::white_or_black(false), || {
|
||||
on_create(self);
|
||||
});
|
||||
});
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
// Copyright 2025 The Grim Developers
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use egui::scroll_area::ScrollBarVisibility;
|
||||
use egui::{Align, Layout, RichText, ScrollArea, StrokeKind};
|
||||
use grin_core::core::amount_to_hr_string;
|
||||
|
||||
use crate::gui::icons::{CHECK, FOLDER_USER, PATH};
|
||||
use crate::gui::views::wallets::wallet::types::GRIN;
|
||||
use crate::gui::views::View;
|
||||
use crate::gui::Colors;
|
||||
use crate::wallet::types::WalletAccount;
|
||||
use crate::wallet::WalletConfig;
|
||||
|
||||
/// Wallet account list content.
|
||||
pub struct WalletAccountsContent {
|
||||
/// List of wallet accounts.
|
||||
accounts: Vec<WalletAccount>,
|
||||
/// Current wallet account label.
|
||||
current_label: String,
|
||||
}
|
||||
|
||||
const ACCOUNT_ITEM_HEIGHT: f32 = 75.0;
|
||||
|
||||
impl WalletAccountsContent {
|
||||
/// Create new accounts content.
|
||||
pub fn new(accounts: Vec<WalletAccount>, current: String) -> Self {
|
||||
Self { accounts, current_label: current }
|
||||
}
|
||||
|
||||
/// Draw account list content.
|
||||
pub fn ui(&mut self, ui: &mut egui::Ui, mut on_select: impl FnMut(WalletAccount)) {
|
||||
let size = self.accounts.len();
|
||||
ScrollArea::vertical()
|
||||
.id_salt("account_list_scroll")
|
||||
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
|
||||
.max_height(411.0)
|
||||
.auto_shrink([true; 2])
|
||||
.show_rows(ui, ACCOUNT_ITEM_HEIGHT, size, |ui, row_range| {
|
||||
for index in row_range {
|
||||
let acc = self.accounts.get(index).unwrap().clone();
|
||||
self.account_item_ui(ui, &acc, index, size, || {
|
||||
on_select(acc.clone());
|
||||
});
|
||||
if index == size - 1 {
|
||||
ui.add_space(4.0);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Draw account item.
|
||||
fn account_item_ui(&mut self,
|
||||
ui: &mut egui::Ui,
|
||||
acc: &WalletAccount,
|
||||
index: usize,
|
||||
size: usize,
|
||||
mut on_select: impl FnMut()) {
|
||||
// Setup layout size.
|
||||
let mut rect = ui.available_rect_before_wrap();
|
||||
rect.set_height(ACCOUNT_ITEM_HEIGHT);
|
||||
|
||||
// Draw round background.
|
||||
let bg_rect = rect.clone();
|
||||
let item_rounding = View::item_rounding(index, size, false);
|
||||
ui.painter().rect(bg_rect,
|
||||
item_rounding,
|
||||
Colors::fill(),
|
||||
View::item_stroke(),
|
||||
StrokeKind::Outside);
|
||||
|
||||
ui.vertical(|ui| {
|
||||
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
|
||||
// Draw button to select account.
|
||||
if self.current_label == acc.label {
|
||||
View::selected_item_check(ui);
|
||||
} else {
|
||||
let button_rounding = View::item_rounding(index, size, true);
|
||||
View::item_button(ui, button_rounding, CHECK, None, || {
|
||||
on_select();
|
||||
});
|
||||
}
|
||||
|
||||
let layout_size = ui.available_size();
|
||||
ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| {
|
||||
ui.add_space(8.0);
|
||||
ui.vertical(|ui| {
|
||||
ui.add_space(3.0);
|
||||
// Show spendable amount.
|
||||
let amount = amount_to_hr_string(acc.spendable_amount, true);
|
||||
let amount_text = format!("{} {}", amount, GRIN);
|
||||
ui.with_layout(Layout::left_to_right(Align::Min), |ui| {
|
||||
ui.add_space(1.0);
|
||||
ui.label(RichText::new(amount_text)
|
||||
.size(18.0)
|
||||
.color(Colors::white_or_black(true)));
|
||||
});
|
||||
ui.add_space(-2.0);
|
||||
|
||||
// Show account name.
|
||||
let default_acc_label = WalletConfig::DEFAULT_ACCOUNT_LABEL.to_string();
|
||||
let acc_label = if acc.label == default_acc_label {
|
||||
t!("wallets.default_account").into()
|
||||
} else {
|
||||
acc.label.to_owned()
|
||||
};
|
||||
let acc_name = format!("{} {}", FOLDER_USER, acc_label);
|
||||
View::ellipsize_text(ui, acc_name, 15.0, Colors::text(false));
|
||||
|
||||
// Show account BIP32 derivation path.
|
||||
let acc_path = format!("{} {}", PATH, acc.path);
|
||||
ui.label(RichText::new(acc_path).size(15.0).color(Colors::gray()));
|
||||
ui.add_space(3.0);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// 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;
|
||||
mod list;
|
||||
mod create;
|
||||
|
||||
pub use content::*;
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user