Compare commits
203 commits
Author | SHA1 | Date | |
---|---|---|---|
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 |
129 changed files with 15914 additions and 13017 deletions
27
.github/workflows/build.yml
vendored
Normal file
27
.github/workflows/build.yml
vendored
Normal file
|
@ -0,0 +1,27 @@
|
|||
name: Build
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
linux:
|
||||
name: Linux Build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Release build
|
||||
run: cargo build --release
|
||||
|
||||
windows:
|
||||
name: Windows Build
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Release build
|
||||
run: cargo build --release
|
||||
|
||||
macos:
|
||||
name: MacOS Build
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Release build
|
||||
run: cargo build --release
|
258
.github/workflows/release.yml
vendored
258
.github/workflows/release.yml
vendored
|
@ -1,258 +0,0 @@
|
|||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*.*.*"
|
||||
|
||||
jobs:
|
||||
android_release:
|
||||
name: Android Release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup JDK 17
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'temurin'
|
||||
cache: gradle
|
||||
- name: Setup Rust build
|
||||
run: |
|
||||
cargo install cargo-ndk
|
||||
rustup target add aarch64-linux-android
|
||||
rustup target add armv7-linux-androideabi
|
||||
rustup target add x86_64-linux-android
|
||||
- name: Setup Java build
|
||||
run: |
|
||||
chmod +x android/gradlew
|
||||
echo "${{ secrets.ANDROID_RELEASE_KEYSTORE }}" > release.keystore.asc
|
||||
gpg -d --passphrase "${{ secrets.ANDROID_RELEASE_SECRET }}" --batch release.keystore.asc > android/keystore
|
||||
echo -e "storePassword=${{ secrets.ANDROID_PASS }}\nkeyPassword=${{ secrets.ANDROID_PASS }}\nkeyAlias=grim\nstoreFile=../keystore" > android/keystore.properties
|
||||
- name: Build lib ARMv8 1/2
|
||||
continue-on-error: true
|
||||
run: |
|
||||
sed -i -e 's/"rlib"/"cdylib","rlib"/g' Cargo.toml
|
||||
export CPPFLAGS="-DMDB_USE_ROBUST=0" && export CFLAGS="-DMDB_USE_ROBUST=0" && cargo ndk -t arm64-v8a build --profile release-apk
|
||||
- name: Build lib ARMv8 2/2
|
||||
run: |
|
||||
unset CPPFLAGS && unset CFLAGS && cargo ndk -t arm64-v8a -o android/app/src/main/jniLibs build --profile release-apk
|
||||
sed -i -e 's/"cdylib","rlib"/"rlib"/g' Cargo.toml
|
||||
- name: Build APK ARMv8
|
||||
working-directory: android
|
||||
run: |
|
||||
./gradlew assembleRelease
|
||||
mv app/build/outputs/apk/release/app-release.apk grim-${{ github.ref_name }}-android-armv8.apk
|
||||
- name: Checksum APK ARMv8
|
||||
working-directory: android
|
||||
shell: pwsh
|
||||
run: get-filehash -algorithm sha256 grim-${{ github.ref_name }}-android-armv8.apk | Format-List | Out-String | ForEach-Object { $_.Trim() } > grim-${{ github.ref_name }}-android-armv8-sha256sum.txt
|
||||
- name: Build lib ARMv7 1/2
|
||||
continue-on-error: true
|
||||
run: |
|
||||
sed -i -e 's/"rlib"/"cdylib","rlib"/g' Cargo.toml
|
||||
export CPPFLAGS="-DMDB_USE_ROBUST=0" && export CFLAGS="-DMDB_USE_ROBUST=0" && cargo ndk -t armeabi-v7a build --profile release-apk
|
||||
- name: Build lib ARMv7 2/2
|
||||
run: |
|
||||
unset CPPFLAGS && unset CFLAGS && cargo ndk -t armeabi-v7a -o android/app/src/main/jniLibs build --profile release-apk
|
||||
sed -i -e 's/"cdylib","rlib"/"rlib"/g' Cargo.toml
|
||||
- name: Build APK ARMv7
|
||||
working-directory: android
|
||||
run: |
|
||||
rm -rf app/build
|
||||
./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: Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
files: |
|
||||
android/grim-${{ github.ref_name }}-android-armv8.apk
|
||||
android/grim-${{ github.ref_name }}-android-armv8-sha256sum.txt
|
||||
android/grim-${{ github.ref_name }}-android-armv7.apk
|
||||
android/grim-${{ github.ref_name }}-android-armv7-sha256sum.txt
|
||||
android/grim-${{ github.ref_name }}-android-x86_64.apk
|
||||
android/grim-${{ github.ref_name }}-android-x86_64-sha256sum.txt
|
||||
|
||||
linux_release:
|
||||
name: Linux Release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Download appimagetools
|
||||
run: |
|
||||
wget https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage
|
||||
chmod +x appimagetool-x86_64.AppImage
|
||||
sudo apt install libfuse2
|
||||
- name: Zig Setup
|
||||
uses: goto-bus-stop/setup-zig@v2
|
||||
with:
|
||||
version: 0.12.1
|
||||
- name: Install cargo-zigbuild
|
||||
run: cargo install cargo-zigbuild
|
||||
- name: Release x86
|
||||
run: cargo zigbuild --release --target x86_64-unknown-linux-gnu
|
||||
- name: Release ARM
|
||||
run: |
|
||||
rustup target add aarch64-unknown-linux-gnu
|
||||
cargo zigbuild --release --target aarch64-unknown-linux-gnu
|
||||
- name: AppImage x86
|
||||
run: |
|
||||
cp target/x86_64-unknown-linux-gnu/release/grim linux/Grim.AppDir/AppRun
|
||||
./appimagetool-x86_64.AppImage linux/Grim.AppDir target/x86_64-unknown-linux-gnu/release/grim-${{ github.ref_name }}-linux-x86_64.AppImage
|
||||
- name: Checksum AppImage x86
|
||||
working-directory: target/x86_64-unknown-linux-gnu/release
|
||||
shell: pwsh
|
||||
run: get-filehash -algorithm sha256 grim-${{ github.ref_name }}-linux-x86_64.AppImage | Format-List | Out-String | ForEach-Object { $_.Trim() } > grim-${{ github.ref_name }}-linux-x86_64-appimage-sha256sum.txt
|
||||
- name: AppImage ARM
|
||||
run: |
|
||||
cp target/aarch64-unknown-linux-gnu/release/grim linux/Grim.AppDir/AppRun
|
||||
./appimagetool-x86_64.AppImage linux/Grim.AppDir target/aarch64-unknown-linux-gnu/release/grim-${{ github.ref_name }}-linux-arm.AppImage
|
||||
- name: Checksum AppImage ARM
|
||||
working-directory: target/aarch64-unknown-linux-gnu/release
|
||||
shell: pwsh
|
||||
run: get-filehash -algorithm sha256 grim-${{ github.ref_name }}-linux-arm.AppImage | Format-List | Out-String | ForEach-Object { $_.Trim() } > grim-${{ github.ref_name }}-linux-arm-appimage-sha256sum.txt
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
files: |
|
||||
target/x86_64-unknown-linux-gnu/release/grim-${{ github.ref_name }}-linux-x86_64.AppImage
|
||||
target/x86_64-unknown-linux-gnu/release/grim-${{ github.ref_name }}-linux-x86_64-appimage-sha256sum.txt
|
||||
target/aarch64-unknown-linux-gnu/release/grim-${{ github.ref_name }}-linux-arm.AppImage
|
||||
target/aarch64-unknown-linux-gnu/release/grim-${{ github.ref_name }}-linux-arm-appimage-sha256sum.txt
|
||||
|
||||
windows_release:
|
||||
name: Windows Release
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Build release
|
||||
run: cargo build --release
|
||||
- name: Archive release
|
||||
uses: vimtor/action-zip@v1
|
||||
with:
|
||||
files: target/release/grim.exe
|
||||
dest: target/release/grim-${{ github.ref_name }}-win-x86_64.zip
|
||||
- name: Checksum release
|
||||
working-directory: target/release
|
||||
shell: pwsh
|
||||
run: get-filehash -algorithm sha256 grim-${{ github.ref_name }}-win-x86_64.zip | Format-List | Out-String | ForEach-Object { $_.Trim() } > grim-${{ github.ref_name }}-win-x86_64-sha256sum.txt
|
||||
- name: Install cargo-wix
|
||||
run: cargo install cargo-wix
|
||||
- name: Run cargo-wix
|
||||
run: cargo wix -p grim -o ./target/wix/grim-${{ github.ref_name }}-win-x86_64.msi --nocapture
|
||||
- name: Checksum msi
|
||||
working-directory: target/wix
|
||||
shell: pwsh
|
||||
run: get-filehash -algorithm sha256 grim-${{ github.ref_name }}-win-x86_64.msi | Format-List | Out-String | ForEach-Object { $_.Trim() } > grim-${{ github.ref_name }}-win-x86_64-msi-sha256sum.txt
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
files: |
|
||||
target/release/grim-${{ github.ref_name }}-win-x86_64.zip
|
||||
target/release/grim-${{ github.ref_name }}-win-x86_64-sha256sum.txt
|
||||
target/wix/grim-${{ github.ref_name }}-win-x86_64.msi
|
||||
target/wix/grim-${{ github.ref_name }}-win-x86_64-msi-sha256sum.txt
|
||||
|
||||
macos_release:
|
||||
name: MacOS Release
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Zig Setup
|
||||
uses: goto-bus-stop/setup-zig@v2
|
||||
with:
|
||||
version: 0.12.1
|
||||
- name: Install cargo-zigbuild
|
||||
run: cargo install cargo-zigbuild
|
||||
- name: Release x86
|
||||
run: |
|
||||
rustup target add x86_64-apple-darwin
|
||||
cargo zigbuild --release --target x86_64-apple-darwin
|
||||
mkdir macos/Grim.app/Contents/MacOS
|
||||
yes | cp -rf target/x86_64-apple-darwin/release/grim macos/Grim.app/Contents/MacOS
|
||||
- name: Archive x86
|
||||
run: |
|
||||
cd macos
|
||||
zip -r grim-${{ github.ref_name }}-macos-x86_64.zip Grim.app
|
||||
mv grim-${{ github.ref_name }}-macos-x86_64.zip ../target/x86_64-apple-darwin/release
|
||||
cd ..
|
||||
- name: Checksum Release x86
|
||||
working-directory: target/x86_64-apple-darwin/release
|
||||
shell: pwsh
|
||||
run: get-filehash -algorithm sha256 grim-${{ github.ref_name }}-macos-x86_64.zip | Format-List | Out-String | ForEach-Object { $_.Trim() } > grim-${{ github.ref_name }}-macos-x86_64-sha256sum.txt
|
||||
- name: Release ARM
|
||||
run: |
|
||||
rustup target add aarch64-apple-darwin
|
||||
cargo zigbuild --release --target aarch64-apple-darwin
|
||||
yes | cp -rf target/aarch64-apple-darwin/release/grim macos/Grim.app/Contents/MacOS
|
||||
- name: Archive ARM
|
||||
run: |
|
||||
cd macos
|
||||
zip -r grim-${{ github.ref_name }}-macos-arm.zip Grim.app
|
||||
mv grim-${{ github.ref_name }}-macos-arm.zip ../target/aarch64-apple-darwin/release
|
||||
cd ..
|
||||
- name: Checksum Release ARM
|
||||
working-directory: target/aarch64-apple-darwin/release
|
||||
shell: pwsh
|
||||
run: get-filehash -algorithm sha256 grim-${{ github.ref_name }}-macos-arm.zip | Format-List | Out-String | ForEach-Object { $_.Trim() } > grim-${{ github.ref_name }}-macos-arm-sha256sum.txt
|
||||
- name: Release Universal
|
||||
run: |
|
||||
rustup target add aarch64-apple-darwin
|
||||
rustup target add x86_64-apple-darwin
|
||||
cargo zigbuild --release --target universal2-apple-darwin
|
||||
yes | cp -rf target/universal2-apple-darwin/release/grim macos/Grim.app/Contents/MacOS
|
||||
- name: Archive Universal
|
||||
run: |
|
||||
cd macos
|
||||
zip -r grim-${{ github.ref_name }}-macos-universal.zip Grim.app
|
||||
mv grim-${{ github.ref_name }}-macos-universal.zip ../target/universal2-apple-darwin/release
|
||||
cd ..
|
||||
- name: Checksum Release Universal
|
||||
working-directory: target/universal2-apple-darwin/release
|
||||
shell: pwsh
|
||||
run: get-filehash -algorithm sha256 grim-${{ github.ref_name }}-macos-universal.zip | Format-List | Out-String | ForEach-Object { $_.Trim() } > grim-${{ github.ref_name }}-macos-universal-sha256sum.txt
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
files: |
|
||||
target/x86_64-apple-darwin/release/grim-${{ github.ref_name }}-macos-x86_64.zip
|
||||
target/x86_64-apple-darwin/release/grim-${{ github.ref_name }}-macos-x86_64-sha256sum.txt
|
||||
target/aarch64-apple-darwin/release/grim-${{ github.ref_name }}-macos-arm.zip
|
||||
target/aarch64-apple-darwin/release/grim-${{ github.ref_name }}-macos-arm-sha256sum.txt
|
||||
target/universal2-apple-darwin/release/grim-${{ github.ref_name }}-macos-universal.zip
|
||||
target/universal2-apple-darwin/release/grim-${{ github.ref_name }}-macos-universal-sha256sum.txt
|
170
.github/workflows/release.yml.bak
vendored
Normal file
170
.github/workflows/release.yml.bak
vendored
Normal file
|
@ -0,0 +1,170 @@
|
|||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*.*.*"
|
||||
|
||||
jobs:
|
||||
linux_release:
|
||||
name: Linux Release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Download appimagetools
|
||||
run: |
|
||||
wget https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage
|
||||
chmod +x appimagetool-x86_64.AppImage
|
||||
sudo apt install libfuse2
|
||||
- name: Zig Setup
|
||||
uses: goto-bus-stop/setup-zig@v2
|
||||
with:
|
||||
version: 0.12.1
|
||||
- name: Install cargo-zigbuild
|
||||
run: cargo install cargo-zigbuild
|
||||
- name: Release x86
|
||||
run: cargo zigbuild --release --target x86_64-unknown-linux-gnu
|
||||
- name: Release ARM
|
||||
run: |
|
||||
rustup target add aarch64-unknown-linux-gnu
|
||||
cargo zigbuild --release --target aarch64-unknown-linux-gnu
|
||||
- name: AppImage x86
|
||||
run: |
|
||||
cp target/x86_64-unknown-linux-gnu/release/grim linux/Grim.AppDir/AppRun
|
||||
./appimagetool-x86_64.AppImage linux/Grim.AppDir target/x86_64-unknown-linux-gnu/release/grim-${{ github.ref_name }}-linux-x86_64.AppImage
|
||||
- name: Checksum AppImage x86
|
||||
working-directory: target/x86_64-unknown-linux-gnu/release
|
||||
shell: bash
|
||||
run: sha256sum grim-${{ github.ref_name }}-linux-x86_64.AppImage > grim-${{ github.ref_name }}-linux-x86_64-appimage-sha256sum.txt
|
||||
- name: AppImage ARM
|
||||
run: |
|
||||
cp target/aarch64-unknown-linux-gnu/release/grim linux/Grim.AppDir/AppRun
|
||||
./appimagetool-x86_64.AppImage linux/Grim.AppDir target/aarch64-unknown-linux-gnu/release/grim-${{ github.ref_name }}-linux-arm.AppImage
|
||||
- name: Checksum AppImage ARM
|
||||
working-directory: target/aarch64-unknown-linux-gnu/release
|
||||
shell: bash
|
||||
run: sha256sum grim-${{ github.ref_name }}-linux-arm.AppImage > grim-${{ github.ref_name }}-linux-arm-appimage-sha256sum.txt
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
files: |
|
||||
target/x86_64-unknown-linux-gnu/release/grim-${{ github.ref_name }}-linux-x86_64.AppImage
|
||||
target/x86_64-unknown-linux-gnu/release/grim-${{ github.ref_name }}-linux-x86_64-appimage-sha256sum.txt
|
||||
target/aarch64-unknown-linux-gnu/release/grim-${{ github.ref_name }}-linux-arm.AppImage
|
||||
target/aarch64-unknown-linux-gnu/release/grim-${{ github.ref_name }}-linux-arm-appimage-sha256sum.txt
|
||||
|
||||
windows_release:
|
||||
name: Windows Release
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Build release
|
||||
run: cargo build --release
|
||||
- name: Archive release
|
||||
uses: vimtor/action-zip@v1
|
||||
with:
|
||||
files: target/release/grim.exe
|
||||
dest: target/release/grim-${{ github.ref_name }}-win-x86_64.zip
|
||||
- name: Checksum release
|
||||
working-directory: target/release
|
||||
shell: bash
|
||||
run: sha256sum grim-${{ github.ref_name }}-win-x86_64.zip > grim-${{ github.ref_name }}-win-x86_64-sha256sum.txt
|
||||
- name: Install cargo-wix
|
||||
run: cargo install cargo-wix
|
||||
- name: Run cargo-wix
|
||||
run: cargo wix -p grim -o ./target/wix/grim-${{ github.ref_name }}-win-x86_64.msi --nocapture
|
||||
- name: Checksum msi
|
||||
working-directory: target/wix
|
||||
shell: bash
|
||||
run: sha256sum grim-${{ github.ref_name }}-win-x86_64.msi > grim-${{ github.ref_name }}-win-x86_64-msi-sha256sum.txt
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
files: |
|
||||
target/release/grim-${{ github.ref_name }}-win-x86_64.zip
|
||||
target/release/grim-${{ github.ref_name }}-win-x86_64-sha256sum.txt
|
||||
target/wix/grim-${{ github.ref_name }}-win-x86_64.msi
|
||||
target/wix/grim-${{ github.ref_name }}-win-x86_64-msi-sha256sum.txt
|
||||
|
||||
macos_release:
|
||||
name: MacOS Release
|
||||
runs-on: macos-12
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Install coreutils
|
||||
run: brew install coreutils
|
||||
- name: Zig Setup
|
||||
uses: goto-bus-stop/setup-zig@v2
|
||||
with:
|
||||
version: 0.12.1
|
||||
- name: Install cargo-zigbuild
|
||||
run: cargo install cargo-zigbuild
|
||||
- name: Download SDK
|
||||
run: wget https://github.com/phracker/MacOSX-SDKs/releases/download/11.3/MacOSX11.0.sdk.tar.xz
|
||||
- name: Setup SDK env
|
||||
run: tar xf ${{ github.workspace }}/MacOSX11.0.sdk.tar.xz && echo "SDKROOT=${{ github.workspace }}/MacOSX11.0.sdk" >> $GITHUB_ENV
|
||||
- name: Setup platform env
|
||||
run: echo "MACOSX_DEPLOYMENT_TARGET=11.0" >> $GITHUB_ENV
|
||||
- name: Release x86
|
||||
run: |
|
||||
rustup target add x86_64-apple-darwin
|
||||
cargo zigbuild --release --target x86_64-apple-darwin
|
||||
yes | cp -rf target/x86_64-apple-darwin/release/grim macos/Grim.app/Contents/MacOS
|
||||
- name: Archive x86
|
||||
run: |
|
||||
cd macos
|
||||
zip -r grim-${{ github.ref_name }}-macos-x86_64.zip Grim.app
|
||||
mv grim-${{ github.ref_name }}-macos-x86_64.zip ../target/x86_64-apple-darwin/release
|
||||
cd ..
|
||||
- name: Checksum Release x86
|
||||
working-directory: target/x86_64-apple-darwin/release
|
||||
shell: bash
|
||||
run: sha256sum grim-${{ github.ref_name }}-macos-x86_64.zip > grim-${{ github.ref_name }}-macos-x86_64-sha256sum.txt
|
||||
- name: Release ARM
|
||||
run: |
|
||||
rustup target add aarch64-apple-darwin
|
||||
cargo zigbuild --release --target aarch64-apple-darwin
|
||||
yes | cp -rf target/aarch64-apple-darwin/release/grim macos/Grim.app/Contents/MacOS
|
||||
- name: Archive ARM
|
||||
run: |
|
||||
cd macos
|
||||
zip -r grim-${{ github.ref_name }}-macos-arm.zip Grim.app
|
||||
mv grim-${{ github.ref_name }}-macos-arm.zip ../target/aarch64-apple-darwin/release
|
||||
cd ..
|
||||
- name: Checksum Release ARM
|
||||
working-directory: target/aarch64-apple-darwin/release
|
||||
shell: bash
|
||||
run: sha256sum grim-${{ github.ref_name }}-macos-arm.zip > grim-${{ github.ref_name }}-macos-arm-sha256sum.txt
|
||||
- name: Release Universal
|
||||
run: |
|
||||
cargo zigbuild --release --target universal2-apple-darwin
|
||||
yes | cp -rf target/universal2-apple-darwin/release/grim macos/Grim.app/Contents/MacOS
|
||||
- name: Archive Universal
|
||||
run: |
|
||||
cd macos
|
||||
zip -r grim-${{ github.ref_name }}-macos-universal.zip Grim.app
|
||||
mv grim-${{ github.ref_name }}-macos-universal.zip ../target/universal2-apple-darwin/release
|
||||
cd ..
|
||||
- name: Checksum Release Universal
|
||||
working-directory: target/universal2-apple-darwin/release
|
||||
shell: pwsh
|
||||
run: sha256sum grim-${{ github.ref_name }}-macos-universal.zip > grim-${{ github.ref_name }}-macos-universal-sha256sum.txt
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
files: |
|
||||
target/x86_64-apple-darwin/release/grim-${{ github.ref_name }}-macos-x86_64.zip
|
||||
target/x86_64-apple-darwin/release/grim-${{ github.ref_name }}-macos-x86_64-sha256sum.txt
|
||||
target/aarch64-apple-darwin/release/grim-${{ github.ref_name }}-macos-arm.zip
|
||||
target/aarch64-apple-darwin/release/grim-${{ github.ref_name }}-macos-arm-sha256sum.txt
|
||||
target/universal2-apple-darwin/release/grim-${{ github.ref_name }}-macos-universal.zip
|
||||
target/universal2-apple-darwin/release/grim-${{ github.ref_name }}-macos-universal-sha256sum.txt
|
8
.gitignore
vendored
8
.gitignore
vendored
|
@ -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
|
6668
Cargo.lock
generated
6668
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
177
Cargo.toml
177
Cargo.toml
|
@ -1,10 +1,10 @@
|
|||
[package]
|
||||
name = "grim"
|
||||
version = "0.1.0"
|
||||
authors = ["Ardocrat <ardocrat@proton.me>"]
|
||||
version = "0.3.0-alpha"
|
||||
authors = ["Ardocrat <ardocrat@gri.mw>"]
|
||||
description = "Cross-platform GUI for Grin with focus on usability and availability to be used by anyone, anywhere."
|
||||
license = "Apache-2.0"
|
||||
repository = "https://github.com/ardocrat/grim"
|
||||
repository = "https://gri.mw/code/GUI/grim"
|
||||
keywords = [ "crypto", "grin", "mimblewimble" ]
|
||||
edition = "2021"
|
||||
|
||||
|
@ -25,106 +25,137 @@ codegen-units = 1
|
|||
panic = "abort"
|
||||
|
||||
[dependencies]
|
||||
log = "0.4"
|
||||
log = "0.4.27"
|
||||
|
||||
## node
|
||||
openssl-sys = { version = "0.9.82", features = ["vendored"] }
|
||||
grin_api = "5.3.1"
|
||||
grin_chain = "5.3.1"
|
||||
grin_config = "5.3.1"
|
||||
grin_core = "5.3.1"
|
||||
grin_p2p = "5.3.1"
|
||||
grin_servers = "5.3.1"
|
||||
grin_keychain = "5.3.1"
|
||||
grin_util = "5.3.1"
|
||||
## grin
|
||||
grin_api = "5.3.3"
|
||||
grin_chain = "5.3.3"
|
||||
grin_config = "5.3.3"
|
||||
grin_core = "5.3.3"
|
||||
grin_p2p = "5.3.3"
|
||||
grin_servers = "5.3.3"
|
||||
grin_keychain = "5.3.3"
|
||||
grin_util = "5.3.3"
|
||||
|
||||
## wallet
|
||||
grin_wallet_impls = "5.3.1"
|
||||
grin_wallet_api = "5.3.1"
|
||||
grin_wallet_libwallet = "5.3.1"
|
||||
grin_wallet_util = "5.3.1"
|
||||
grin_wallet_controller = "5.3.1"
|
||||
#grin_wallet_impls = "5.3.3"
|
||||
#grin_wallet_api = "5.3.3"
|
||||
#grin_wallet_libwallet = "5.3.3"
|
||||
#grin_wallet_util = "5.3.3"
|
||||
#grin_wallet_controller = "5.3.3"
|
||||
|
||||
# local
|
||||
#grin_api = { path = "../grin/api" }
|
||||
#grin_chain = { path = "../grin/chain" }
|
||||
#grin_config = { path = "../grin/config" }
|
||||
#grin_core = { path = "../grin/core" }
|
||||
#grin_p2p = { path = "../grin/p2p" }
|
||||
#grin_servers = { path = "../grin/servers" }
|
||||
#grin_keychain = { path = "../grin/keychain" }
|
||||
#grin_util = { path = "../grin/util" }
|
||||
|
||||
#grin_wallet_impls = { path = "../grin-wallet/impls" }
|
||||
#grin_wallet_api = { path = "../grin-wallet/api"}
|
||||
#grin_wallet_libwallet = { path = "../grin-wallet/libwallet" }
|
||||
#grin_wallet_util = { path = "../grin-wallet/util" }
|
||||
#grin_wallet_controller = { path = "../grin-wallet/controller" }
|
||||
|
||||
# test
|
||||
grin_wallet_impls = { git = "https://github.com/ardocrat/grin-wallet", branch = "wallet_node_client_proxy" }
|
||||
grin_wallet_api = { git = "https://github.com/ardocrat/grin-wallet", branch = "wallet_node_client_proxy" }
|
||||
grin_wallet_libwallet = { git = "https://github.com/ardocrat/grin-wallet", branch = "wallet_node_client_proxy" }
|
||||
grin_wallet_util = { git = "https://github.com/ardocrat/grin-wallet", branch = "wallet_node_client_proxy" }
|
||||
grin_wallet_controller = { git = "https://github.com/ardocrat/grin-wallet", branch = "wallet_node_client_proxy" }
|
||||
|
||||
## ui
|
||||
egui = { version = "0.28.1", default-features = false }
|
||||
egui_extras = { version = "0.28.1", features = ["image", "svg"] }
|
||||
egui = { version = "0.31.1", default-features = false }
|
||||
egui_extras = { version = "0.31.1", features = ["image", "svg"] }
|
||||
rust-i18n = "2.3.1"
|
||||
|
||||
## 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.74"
|
||||
thiserror = "1.0.64"
|
||||
futures = "0.3.31"
|
||||
dirs = "6.0.0"
|
||||
sys-locale = "0.3.1"
|
||||
chrono = "0.4.38"
|
||||
parking_lot = "0.12.3"
|
||||
lazy_static = "1.5.0"
|
||||
toml = "0.8.19"
|
||||
serde = "1.0.210"
|
||||
local-ip-address = "0.6.3"
|
||||
url = "2.5.2"
|
||||
rand = "0.9.0"
|
||||
serde_derive = "1.0.219"
|
||||
serde_json = "1.0.140"
|
||||
tokio = { version = "1.44.1", features = ["full"] }
|
||||
image = "0.25.6"
|
||||
rqrr = "0.8.0"
|
||||
qrcodegen = "1.8.0"
|
||||
qrcode = "0.14.0"
|
||||
qrcode = "0.14.1"
|
||||
ur = "0.4.1"
|
||||
gif = "0.13.1"
|
||||
rkv = { version = "0.19.0", features = ["lmdb"] }
|
||||
usvg = "0.45.1"
|
||||
ring = "0.16.20"
|
||||
hyper = { version = "1.6.0", features = ["full"], package = "hyper" }
|
||||
hyper-util = { version = "0.1.11", features = ["http1", "client", "client-legacy"] }
|
||||
http-body-util = "0.1.3"
|
||||
bytes = "1.10.1"
|
||||
hyper-socks2 = "0.9.1"
|
||||
hyper-proxy2 = "0.1.0"
|
||||
hyper-tls = "0.6.0"
|
||||
|
||||
## 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.30.0", features = ["pt-client", "static", "onion-service-service", "onion-service-client"] }
|
||||
tor-rtcompat = { version = "0.30.0", features = ["static"] }
|
||||
tor-config = "0.30.0"
|
||||
fs-mistrust = "0.9.1"
|
||||
tor-hsservice = "0.30.0"
|
||||
tor-hsrproxy = "0.30.0"
|
||||
tor-keymgr = "0.30.0"
|
||||
tor-llcrypto = "0.30.0"
|
||||
tor-hscrypto = "0.30.0"
|
||||
tor-error = "0.30.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"
|
||||
|
||||
## 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.5", default-features = false, features = ["input-v4l"] }
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
nokhwa = { version = "0.10.4", default-features = false, features = ["input-msmf"] }
|
||||
nokhwa = { version = "0.10.5", default-features = false, features = ["input-msmf"] }
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
tls-api-openssl = "0.9.0"
|
||||
openpnp_capture_sys = "0.4.0"
|
||||
nokhwa-mac = { git = "https://github.com/l1npengtul/nokhwa", rev = "612c861ef153cf0ee575d8dd1413b960e4e19dd6", features = ["input-avfoundation", "output-threaded"], package = "nokhwa" }
|
||||
|
||||
[target.'cfg(not(target_os = "android"))'.dependencies]
|
||||
env_logger = "0.11.3"
|
||||
winit = { version = "0.29.15" }
|
||||
eframe = { version = "0.28.1", features = ["wgpu", "glow"] }
|
||||
winit = { version = "0.30.11" }
|
||||
eframe = { version = "0.31.1", default-features = false, features = ["glow"] }
|
||||
arboard = "3.2.0"
|
||||
rfd = "0.14.1"
|
||||
dark-light = "1.1.1"
|
||||
rfd = "0.15.0"
|
||||
interprocess = { version = "2.2.1", features = ["tokio"] }
|
||||
|
||||
[target.'cfg(target_os = "android")'.dependencies]
|
||||
android_logger = "0.13.1"
|
||||
android_logger = "0.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"] }
|
||||
android-activity = { version = "0.6.0", features = ["native-activity"] }
|
||||
winit = { version = "0.30.11", features = ["android-native-activity"] }
|
||||
eframe = { version = "0.31.1", default-features = false, features = ["glow", "android-native-activity"] }
|
||||
|
||||
[patch.crates-io]
|
||||
egui_extras = { git = "https://github.com/ardocrat/egui", branch = "back_button_android" }
|
||||
egui = { git = "https://github.com/ardocrat/egui", branch = "back_button_android" }
|
||||
eframe = { git = "https://github.com/ardocrat/egui", branch = "back_button_android" }
|
||||
### 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" }
|
|
@ -1,11 +1,11 @@
|
|||
# <img height="22" src="https://github.com/ardocrat/grim/blob/master/android/app/src/main/ic_launcher-playstore.png?raw=true"> Grim <img height="20" src="https://github.com/mimblewimble/site/blob/master/assets/images/grin-logo.png?raw=true"> <img height="20" src="https://github.com/ardocrat/grim/blob/master/img/logo.png?raw=true">
|
||||
# Grim <img height="20" src="https://gri.mw/code/GUI/grim/raw/branch/master/img/grin-logo.png"/> <img height="20" src="https://gri.mw/code/GUI/grim/raw/branch/master/img/logo.png"/>
|
||||
Cross-platform GUI for [GRiN ツ](https://grin.mw) in [Rust](https://www.rust-lang.org/)
|
||||
for maximum compatibility with original [Mimblewimble](https://github.com/mimblewimble/grin) implementation.
|
||||
Initially supported platforms are Linux, Mac, Windows, limited Android and possible web support with help of [egui](https://github.com/emilk/egui) - immediate mode GUI library in pure Rust.
|
||||
|
||||
Named by the character [Grim](http://harrypotter.wikia.com/wiki/Grim) - the shape of a large, black, menacing, spectral giant dog.
|
||||
|
||||

|
||||

|
||||
|
||||
|
||||
## Build instructions
|
||||
|
|
|
@ -2,28 +2,30 @@ plugins {
|
|||
id 'com.android.application'
|
||||
}
|
||||
|
||||
def keystorePropertiesFile = rootProject.file("keystore.properties")
|
||||
def keystoreProperties = new Properties()
|
||||
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
|
||||
|
||||
android {
|
||||
compileSdk 33
|
||||
compileSdk 35
|
||||
ndkVersion '26.0.10792818'
|
||||
|
||||
defaultConfig {
|
||||
applicationId "mw.gri.android"
|
||||
minSdk 24
|
||||
targetSdk 33
|
||||
versionCode 1
|
||||
versionName "0.1.0"
|
||||
targetSdk 35
|
||||
versionCode 4
|
||||
versionName "0.2.4"
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
release {
|
||||
keyAlias keystoreProperties['keyAlias']
|
||||
keyPassword keystoreProperties['keyPassword']
|
||||
storeFile file(keystoreProperties['storeFile'])
|
||||
storePassword keystoreProperties['storePassword']
|
||||
def keystorePropertiesFile = rootProject.file("keystore.properties")
|
||||
def keystoreProperties = new Properties()
|
||||
if (keystorePropertiesFile.exists()) {
|
||||
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
|
||||
|
||||
signingConfigs {
|
||||
release {
|
||||
keyAlias keystoreProperties['keyAlias']
|
||||
keyPassword keystoreProperties['keyPassword']
|
||||
storeFile file(keystoreProperties['storeFile'])
|
||||
storePassword keystoreProperties['storePassword']
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -31,7 +33,12 @@ android {
|
|||
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
|
||||
|
@ -46,14 +53,11 @@ android {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||
|
||||
// To use the Games Activity library
|
||||
implementation "androidx.games:games-activity:2.0.2"
|
||||
implementation 'androidx.appcompat:appcompat:1.7.0'
|
||||
|
||||
// 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.4.2'
|
||||
implementation 'androidx.camera:camera-camera2:1.4.2'
|
||||
implementation 'androidx.camera:camera-lifecycle:1.4.2'
|
||||
}
|
||||
|
||||
|
|
|
@ -1,15 +1,18 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<manifest xmlns:tools="http://schemas.android.com/tools" xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
>
|
||||
|
||||
<uses-feature android:name="android.hardware.camera" android:required="false"/>
|
||||
|
||||
<uses-permission android:name="android.permission.EXPAND_STATUS_BAR" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.CAMERA"/>
|
||||
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" tools:ignore="ScopedStorage"/>
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" tools:ignore="ScopedStorage"/>
|
||||
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" tools:ignore="ScopedStorage"/>
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||
|
||||
<application
|
||||
android:hardwareAccelerated="true"
|
||||
|
@ -18,7 +21,6 @@
|
|||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="Grim"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.Main">
|
||||
|
||||
<receiver android:name=".NotificationActionsReceiver"/>
|
||||
|
@ -44,9 +46,29 @@
|
|||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter android:scheme="http" tools:ignore="AppLinkUrlError">
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="text/*" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter android:scheme="http" tools:ignore="AppLinkUrlError">
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="application/*" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data android:name="android.app.lib_name" android:value="grim" />
|
||||
</activity>
|
||||
<service android:name=".BackgroundService" android:stopWithTask="true" />
|
||||
|
||||
<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;
|
||||
|
||||
|
@ -32,25 +32,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")
|
||||
|
@ -152,13 +133,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 +151,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 +181,6 @@ public class BackgroundService extends Service {
|
|||
|
||||
// Stop updating the notification.
|
||||
mHandler.removeCallbacks(mUpdateSyncStatus);
|
||||
unregisterReceiver(mReceiver);
|
||||
clearNotification();
|
||||
|
||||
// Remove service from foreground state.
|
||||
|
@ -222,12 +203,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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,21 +3,19 @@ package mw.gri.android;
|
|||
import android.Manifest;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.app.NativeActivity;
|
||||
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.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;
|
||||
import androidx.camera.core.*;
|
||||
import androidx.camera.lifecycle.ProcessCameraProvider;
|
||||
|
@ -27,38 +25,41 @@ import androidx.core.graphics.Insets;
|
|||
import androidx.core.view.DisplayCutoutCompat;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.core.view.WindowInsetsCompat;
|
||||
import com.google.androidgamesdk.GameActivity;
|
||||
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
import static android.content.ClipDescription.MIMETYPE_TEXT_HTML;
|
||||
import static android.content.ClipDescription.MIMETYPE_TEXT_PLAIN;
|
||||
|
||||
public class MainActivity extends GameActivity {
|
||||
public static String STOP_APP_ACTION = "STOP_APP";
|
||||
public class MainActivity extends NativeActivity {
|
||||
private static final int FILE_PICK_REQUEST = 1001;
|
||||
private static final int FILE_PERMISSIONS_REQUEST = 1002;
|
||||
|
||||
private static final int NOTIFICATIONS_PERMISSION_CODE = 1;
|
||||
private static final int CAMERA_PERMISSION_CODE = 2;
|
||||
|
||||
public static final String STOP_APP_ACTION = "STOP_APP_ACTION";
|
||||
|
||||
static {
|
||||
System.loadLibrary("grim");
|
||||
}
|
||||
|
||||
private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
|
||||
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
|
||||
@SuppressLint("RestrictedApi")
|
||||
@Override
|
||||
public void onReceive(Context ctx, Intent i) {
|
||||
if (i.getAction().equals(STOP_APP_ACTION)) {
|
||||
onExit();
|
||||
Process.killProcess(Process.myPid());
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (Objects.equals(intent.getAction(), MainActivity.STOP_APP_ACTION)) {
|
||||
exit();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private final ImageAnalysis mImageAnalysis = new ImageAnalysis.Builder()
|
||||
.setTargetResolution(new Size(640, 480))
|
||||
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
|
||||
.build();
|
||||
|
||||
|
@ -67,20 +68,26 @@ public class MainActivity extends GameActivity {
|
|||
private ExecutorService mCameraExecutor = null;
|
||||
private boolean mUseBackCamera = true;
|
||||
|
||||
private ActivityResultLauncher<Intent> mFilePickResultLauncher = 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.
|
||||
String cacheDir = Objects.requireNonNull(getExternalCacheDir()).getPath();
|
||||
if (savedInstanceState == null) {
|
||||
Utils.deleteDirectoryContent(new File(getExternalCacheDir().getPath()), false);
|
||||
Utils.deleteDirectoryContent(new File(cacheDir), 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", cacheDir, true);
|
||||
Os.setenv("ARTI_FS_DISABLE_PERMISSION_CHECKS", "true", true);
|
||||
} catch (ErrnoException e) {
|
||||
throw new RuntimeException(e);
|
||||
|
@ -88,43 +95,12 @@ public class MainActivity extends GameActivity {
|
|||
|
||||
super.onCreate(null);
|
||||
|
||||
// Register receiver to finish activity from the BackgroundService.
|
||||
registerReceiver(mBroadcastReceiver, new IntentFilter(STOP_APP_ACTION));
|
||||
|
||||
// Register file pick result launcher.
|
||||
mFilePickResultLauncher = registerForActivityResult(
|
||||
new ActivityResultContracts.StartActivityForResult(),
|
||||
result -> {
|
||||
int resultCode = result.getResultCode();
|
||||
Intent data = result.getData();
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
String path = "";
|
||||
if (data != null) {
|
||||
Uri uri = data.getData();
|
||||
String name = "pick" + Utils.getFileExtension(uri, this);
|
||||
File file = new File(getExternalCacheDir(), name);
|
||||
try (InputStream is = getContentResolver().openInputStream(uri);
|
||||
OutputStream os = new FileOutputStream(file)) {
|
||||
byte[] buffer = new byte[1024];
|
||||
int length;
|
||||
while ((length = is.read(buffer)) > 0) {
|
||||
os.write(buffer, 0, length);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
path = file.getPath();
|
||||
}
|
||||
onFilePick(path);
|
||||
} else {
|
||||
onFilePick("");
|
||||
}
|
||||
});
|
||||
ContextCompat.registerReceiver(this, mReceiver, new IntentFilter(STOP_APP_ACTION), ContextCompat.RECEIVER_NOT_EXPORTED);
|
||||
|
||||
// Listener for display insets (cutouts) to pass values into native code.
|
||||
View content = getWindow().getDecorView().findViewById(android.R.id.content);
|
||||
View content = findViewById(android.R.id.content).getRootView();
|
||||
ViewCompat.setOnApplyWindowInsetsListener(content, (v, insets) -> {
|
||||
// Setup cutouts values.
|
||||
// Get display cutouts.
|
||||
DisplayCutoutCompat dc = insets.getDisplayCutout();
|
||||
int cutoutTop = 0;
|
||||
int cutoutRight = 0;
|
||||
|
@ -140,7 +116,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);
|
||||
|
@ -151,7 +127,7 @@ public class MainActivity extends GameActivity {
|
|||
return insets;
|
||||
});
|
||||
|
||||
findViewById(android.R.id.content).post(() -> {
|
||||
content.post(() -> {
|
||||
// Request notifications permissions if needed.
|
||||
if (Build.VERSION.SDK_INT >= 33) {
|
||||
String notificationsPermission = Manifest.permission.POST_NOTIFICATIONS;
|
||||
|
@ -166,8 +142,99 @@ public class MainActivity extends GameActivity {
|
|||
BackgroundService.start(this);
|
||||
}
|
||||
});
|
||||
|
||||
// Check if intent has data on launch.
|
||||
if (savedInstanceState == null) {
|
||||
onNewIntent(getIntent());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
switch (requestCode) {
|
||||
case FILE_PICK_REQUEST:
|
||||
if (Build.VERSION.SDK_INT >= 30) {
|
||||
if (Environment.isExternalStorageManager()) {
|
||||
onFile();
|
||||
}
|
||||
} else if (resultCode == RESULT_OK) {
|
||||
onFile();
|
||||
}
|
||||
case FILE_PERMISSIONS_REQUEST:
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
String path = "";
|
||||
if (data != null) {
|
||||
Uri uri = data.getData();
|
||||
String name = "pick" + Utils.getFileExtension(uri, this);
|
||||
File file = new File(getExternalCacheDir(), name);
|
||||
try (InputStream is = getContentResolver().openInputStream(uri);
|
||||
OutputStream os = new FileOutputStream(file)) {
|
||||
byte[] buffer = new byte[1024];
|
||||
int length;
|
||||
while ((length = is.read(buffer)) > 0) {
|
||||
os.write(buffer, 0, length);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
path = file.getPath();
|
||||
}
|
||||
onFilePick(path);
|
||||
} else {
|
||||
onFilePick("");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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);
|
||||
startActivityForResult(i, FILE_PERMISSIONS_REQUEST);
|
||||
return;
|
||||
}
|
||||
}
|
||||
try {
|
||||
ParcelFileDescriptor parcelFile = getContentResolver().openFileDescriptor(data, "r");
|
||||
FileReader fileReader = new FileReader(parcelFile.getFileDescriptor());
|
||||
BufferedReader reader = new BufferedReader(fileReader);
|
||||
String line;
|
||||
StringBuilder buff = new StringBuilder();
|
||||
while ((line = reader.readLine()) != null) {
|
||||
buff.append(line);
|
||||
}
|
||||
reader.close();
|
||||
fileReader.close();
|
||||
|
||||
// Provide file content into native code.
|
||||
onData(buff.toString());
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
// Pass data into native code.
|
||||
public native void onData(String data);
|
||||
|
||||
@Override
|
||||
public void onConfigurationChanged(Configuration newConfig) {
|
||||
super.onConfigurationChanged(newConfig);
|
||||
|
@ -196,53 +263,19 @@ public class MainActivity extends GameActivity {
|
|||
}
|
||||
}
|
||||
|
||||
@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());
|
||||
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.
|
||||
public native void onBack();
|
||||
|
||||
// Actions on app exit.
|
||||
private void onExit() {
|
||||
unregisterReceiver(mBroadcastReceiver);
|
||||
BackgroundService.stop(this);
|
||||
// Called from native code to exit app.
|
||||
public void exit() {
|
||||
finishAndRemoveTask();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
onExit();
|
||||
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 +286,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,14 +302,16 @@ 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;
|
||||
|
@ -298,18 +331,16 @@ public class MainActivity extends GameActivity {
|
|||
|
||||
// Called from native code to start camera.
|
||||
public void startCamera() {
|
||||
// Check permissions.
|
||||
String notificationsPermission = Manifest.permission.CAMERA;
|
||||
if (checkSelfPermission(notificationsPermission) != PackageManager.PERMISSION_GRANTED) {
|
||||
requestPermissions(new String[] { notificationsPermission }, CAMERA_PERMISSION_CODE);
|
||||
} else {
|
||||
// Start .
|
||||
if (mCameraProviderFuture == null) {
|
||||
mCameraProviderFuture = ProcessCameraProvider.getInstance(this);
|
||||
mCameraProviderFuture.addListener(() -> {
|
||||
try {
|
||||
mCameraProvider = mCameraProviderFuture.get();
|
||||
// Launch camera.
|
||||
// Start camera.
|
||||
openCamera();
|
||||
} catch (Exception e) {
|
||||
View content = findViewById(android.R.id.content);
|
||||
|
@ -348,7 +379,7 @@ public class MainActivity extends GameActivity {
|
|||
}
|
||||
// Apply declared configs to CameraX using the same lifecycle owner
|
||||
mCameraProvider.unbindAll();
|
||||
mCameraProvider.bindToLifecycle(this, cameraSelector, mImageAnalysis);
|
||||
// mCameraProvider.bindToLifecycle(this, cameraSelector, mImageAnalysis);
|
||||
}
|
||||
|
||||
// Called from native code to stop camera.
|
||||
|
@ -381,14 +412,14 @@ public class MainActivity extends GameActivity {
|
|||
// Pass image from camera into native code.
|
||||
public native void onCameraImage(byte[] buff, int rotation);
|
||||
|
||||
// Called from native code to share image from provided path.
|
||||
public void shareImage(String path) {
|
||||
// Called from native code to share data from provided path.
|
||||
public void shareData(String path) {
|
||||
File file = new File(path);
|
||||
Uri uri = FileProvider.getUriForFile(this, "mw.gri.android.fileprovider", file);
|
||||
Intent intent = new Intent(Intent.ACTION_SEND);
|
||||
intent.putExtra(Intent.EXTRA_STREAM, uri);
|
||||
intent.setType("image/*");
|
||||
startActivity(Intent.createChooser(intent, "Share image"));
|
||||
intent.setType("*/*");
|
||||
startActivity(Intent.createChooser(intent, "Share data"));
|
||||
}
|
||||
|
||||
// Called from native code to check if device is using dark theme.
|
||||
|
@ -402,8 +433,8 @@ public class MainActivity extends GameActivity {
|
|||
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
|
||||
intent.setType("*/*");
|
||||
try {
|
||||
mFilePickResultLauncher.launch(Intent.createChooser(intent, "Pick file"));
|
||||
} catch (android.content.ActivityNotFoundException ex) {
|
||||
startActivityForResult(Intent.createChooser(intent, "Pick file"), FILE_PICK_REQUEST);
|
||||
} catch (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();
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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,6 +1,6 @@
|
|||
#Mon May 02 15:39:12 BST 2022
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
|
||||
distributionPath=wrapper/dists
|
||||
zipStorePath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
|
BIN
img/cover.png
Normal file
BIN
img/cover.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 181 KiB |
BIN
img/grin-logo.png
Normal file
BIN
img/grin-logo.png
Normal file
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
|
|
@ -25,7 +25,13 @@ 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
|
||||
wallets:
|
||||
await_conf_amount: Erwarte Bestätigung
|
||||
await_fin_amount: Warten auf die Fertigstellung
|
||||
|
@ -285,8 +291,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: '/'
|
|
@ -25,7 +25,13 @@ 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
|
||||
wallets:
|
||||
await_conf_amount: Awaiting confirmation
|
||||
await_fin_amount: Awaiting finalization
|
||||
|
@ -285,8 +291,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: /
|
|
@ -25,7 +25,13 @@ 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
|
||||
wallets:
|
||||
await_conf_amount: En attente de confirmation
|
||||
await_fin_amount: En attente de finalisation
|
||||
|
@ -285,8 +291,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: /
|
|
@ -25,7 +25,13 @@ share: Поделиться
|
|||
theme: 'Тема:'
|
||||
dark: Тёмная
|
||||
light: Светлая
|
||||
file: Файл
|
||||
choose_file: Выбрать файл
|
||||
choose_folder: Выбрать папку
|
||||
crash_report: Отчёт о сбое
|
||||
crash_report_warning: В прошлый раз приложение неожиданно закрылось, вы можете поделиться отчетом о сбое с разработчиками.
|
||||
confirmation: Подтверждение
|
||||
enter_url: Введите URL-адрес
|
||||
wallets:
|
||||
await_conf_amount: Ожидает подтверждения
|
||||
await_fin_amount: Ожидает завершения
|
||||
|
@ -285,8 +291,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: ё
|
|
@ -25,7 +25,13 @@ 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
|
||||
wallets:
|
||||
await_conf_amount: Onay bekleniyor
|
||||
await_fin_amount: Tamamlanma bekleniyor
|
||||
|
@ -285,8 +291,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: /
|
344
locales/zh-CN.yml
Normal file
344
locales/zh-CN.yml
Normal file
|
@ -0,0 +1,344 @@
|
|||
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
|
||||
wallets:
|
||||
await_conf_amount: 等待确认中
|
||||
await_fin_amount: 等待确定中
|
||||
locked_amount: 锁定帐户
|
||||
txs_empty: '手动接收资金或通过传输接收资金 %{message} or %{transport} 更改钱包设置, 请按屏幕底部的按钮 %{settings} 按钮.'
|
||||
title: 钱包
|
||||
create_desc: 创建或种子单词导入已有钱包.
|
||||
add: 添加钱包
|
||||
name: '用户名:'
|
||||
pass: '密码:'
|
||||
pass_empty: 输入钱包的密码
|
||||
current_pass: '目前密码:'
|
||||
new_pass: '新密码:'
|
||||
min_tx_conf_count: '确认交易的最低数量:'
|
||||
recover: 恢复
|
||||
recovery_phrase: 助记词
|
||||
words_count: '字数:'
|
||||
enter_word: '输入单词 #%{number}:'
|
||||
not_valid_word: 输入的单词无效
|
||||
not_valid_phrase: 输入的助记词无效
|
||||
create_phrase_desc: 已安全地写下并保存助记词.
|
||||
restore_phrase_desc: 从已保存的助记词中输入.
|
||||
setup_conn_desc: 选择钱包连接到网络的方式.
|
||||
conn_method: 连接方式
|
||||
ext_conn: '外部连接:'
|
||||
add_node: 添加节点
|
||||
node_url: '节点网址:'
|
||||
node_secret: 'API 密钥 (可选):'
|
||||
invalid_url: 输入的网址无效
|
||||
open: 打开钱包
|
||||
wrong_pass: 输入的密码错误
|
||||
locked: 已锁定
|
||||
unlocked: 解锁
|
||||
enable_node: '通过选择屏幕底部的按钮 %{settings} 启用集成节点以使用钱包或更改连接设置.'
|
||||
node_loading: '集成节点同步后钱包会加载,你可选择屏幕底部的按钮 %{settings} 更改连接.'
|
||||
loading: 正在加载
|
||||
closing: 正在关闭
|
||||
checking: 检查中
|
||||
default_wallet: 默认钱包
|
||||
new_account_desc: '输入新帐户的名称:'
|
||||
wallet_loading: 加载钱包
|
||||
wallet_closing: 关闭钱包
|
||||
wallet_checking: 检查钱包
|
||||
tx_loading: 加载事务
|
||||
default_account: 默认账户
|
||||
accounts: 账户
|
||||
tx_sent: 已发送
|
||||
tx_received: 已接收
|
||||
tx_sending: 发送中
|
||||
tx_receiving: 接收中
|
||||
tx_confirming: 等待确认
|
||||
tx_canceled: 已取消
|
||||
tx_cancelling: 取消
|
||||
tx_finalizing: 完成
|
||||
tx_confirmed: 已确认
|
||||
txs: 所有交易
|
||||
tx: 交易
|
||||
messages: 消息
|
||||
transport: 传输
|
||||
input_slatepack_desc: '输入收到的 Slatepack 消息创建响应或完成的请求:'
|
||||
parse_slatepack_err: '读取消息时出错,请检查输入:'
|
||||
pay_balance_error: '账户余额不足以支付 %{amount} ツ 和网络费用.'
|
||||
parse_i1_slatepack_desc: '要支付 %{amount} ツ 请将此消息发送给接收者:'
|
||||
parse_i2_slatepack_desc: '完成交易以接收 %{amount} ツ:'
|
||||
parse_i3_slatepack_desc: '发布交易以完成 %{amount} ツ的接收 ツ:'
|
||||
parse_s1_slatepack_desc: '要接收 %{amount} ツ 请将此消息发送给发件人:'
|
||||
parse_s2_slatepack_desc: '完成交易以发送 %{amount} ツ:'
|
||||
parse_s3_slatepack_desc: '发布交易以完成 %{amount} ツ的发送:'
|
||||
resp_slatepack_err: '创建响应时出错,请检查输入数据或重试:'
|
||||
resp_exists_err: 此交易已存在.
|
||||
resp_canceled_err: 此交易已被取消.
|
||||
create_request_desc: '创建发送或接收资金的请求:'
|
||||
send_request_desc: '您已创建发送请求 %{amount} ツ. 将此消息发送给接收者:'
|
||||
send_slatepack_err: 创建发送资金请求时出错,请检查输入数据或重试.
|
||||
invoice_desc: '您已创建接收请求 %{amount} ツ. 将此消息发送给发送者:'
|
||||
invoice_slatepack_err: 发票开具时出错,请检查输入数据或重试.
|
||||
finalize_slatepack_err: '完结时出错,请检查输入数据或重试:'
|
||||
finalize: 完成
|
||||
use_dandelion: 使用蒲公英
|
||||
enter_amount_send: '你有 %{amount} ツ. 输入要发送的金额:'
|
||||
enter_amount_receive: '输入要接收的金额:'
|
||||
recovery: 恢复
|
||||
repair_wallet: 修复钱包
|
||||
repair_desc: 检查钱包,必要时修复和恢复丢失的输出. 此操作需要时间.
|
||||
repair_unavailable: 您需要与节点建立有效连接并完成钱包同步.
|
||||
delete: 删除钱包
|
||||
delete_conf: 您确定要删除钱包吗?
|
||||
delete_desc: 确保您已保存恢复助记语,以便日后使用资金。.
|
||||
wallet_loading_err: '同步钱包时出错,你可以通过选择屏幕底部的按钮 %{settings} 来重试或更改连接设置.'
|
||||
wallet: 钱包
|
||||
send: 发送
|
||||
receive: 接收
|
||||
settings: 钱包设置
|
||||
tx_send_cancel_conf: '您确定要取消 %{amount} ツ的发送吗?'
|
||||
tx_receive_cancel_conf: '您确定要取消 %{amount} ツ的接收吗?'
|
||||
rec_phrase_not_found: 找不到恢复助记词.
|
||||
restore_wallet_desc: 如果常规修复没有帮助,通过删除所有文件来恢复钱包.您将需要重新打开您的钱包.
|
||||
transport:
|
||||
desc: '使用传输同步接收或发送消息:'
|
||||
tor_network: Tor 网络
|
||||
connected: 已连接
|
||||
connecting: 正在连接
|
||||
disconnecting: 断开连接
|
||||
conn_error: 连接错误
|
||||
disconnected: 已断开连接
|
||||
receiver_address: '接收者的地址:'
|
||||
incorrect_addr_err: '输入的地址不正确:'
|
||||
tor_send_error: 通过 Tor 发送时出错,请确保接收方在线, 交易已取消.
|
||||
tor_autorun_desc: 是否在开钱包时启动 Tor 服务以同步接收交易.
|
||||
tor_sending: '通过 Tor 发送%{amount} ツ'
|
||||
tor_settings: Tor 设置
|
||||
bridges: 桥梁
|
||||
bridges_desc: 如果常规连接不正常,设置网桥,可以绕过 Tor 网络审查.
|
||||
bin_file: '二进制文件:'
|
||||
conn_line: '连接线:'
|
||||
bridges_disabled: 网桥已禁用
|
||||
bridge_name: '网桥%{b}'
|
||||
network:
|
||||
self: 网络
|
||||
type: '网络类型:'
|
||||
mainnet: 主网
|
||||
testnet: 测试网
|
||||
connections: 连接
|
||||
node: 集成节点
|
||||
metrics: 指标
|
||||
mining: 挖矿
|
||||
settings: 节点设置
|
||||
enable_node: 启用节点
|
||||
autorun: 自动运行
|
||||
disabled_server: '按屏幕左上角的按钮 %{dots}启用集成节点或添加其他连接方法.'
|
||||
no_ips: T您的系统上没有可用的 IP 地址,服务器无法启动,请检查您的网络连接.
|
||||
available: 可用
|
||||
not_available: 不可用
|
||||
availability_check: 检查是否可用
|
||||
android_warning: Android 用户注意 .要成功同步集成节点,您必须在手机的系统设置中允许访问通知并取消 Grim 应用程序的电池使用限制.这是在后台正确运行应用程序的必要操作.
|
||||
sync_status:
|
||||
node_restarting: 节点正在重新启动
|
||||
node_down: 节点已关闭
|
||||
initial: 节点正在启动
|
||||
no_sync: 节点正在运行
|
||||
awaiting_peers: 等待网络对点
|
||||
header_sync: 正下载标题
|
||||
header_sync_percent: '正在下载标题: %{percent}%'
|
||||
tx_hashset_pibd: 下载状态 (PIBD)
|
||||
tx_hashset_pibd_percent: '下载状态 (PIBD): %{percent}%'
|
||||
tx_hashset_download: 正在下载状态
|
||||
tx_hashset_download_percent: '下载状态: %{percent}%'
|
||||
tx_hashset_setup_history: '正在准备状态(历史记录): %{percent}%'
|
||||
tx_hashset_setup_position: '正在准备状态(位置): %{percent}%'
|
||||
tx_hashset_setup: 正在准备状态
|
||||
tx_hashset_range_proofs_validation: '验证状态(范围证明): %{percent}%'
|
||||
tx_hashset_kernels_validation: '正在验证状态(核心): %{percent}%'
|
||||
tx_hashset_save: 最终确定链状态
|
||||
body_sync: 下载区块
|
||||
body_sync_percent: '下载区块中: %{percent}%'
|
||||
shutdown: 节点正在关闭
|
||||
network_node:
|
||||
header: 标题
|
||||
block: 区块
|
||||
hash: 哈希值
|
||||
height: 高度
|
||||
difficulty: 难度
|
||||
time: 时间
|
||||
main_pool: 主池
|
||||
stem_pool: stem池
|
||||
data: 数据
|
||||
size: 大小 (GB)
|
||||
peers: 网络对点
|
||||
error_clean: 点数据已损坏,需要重新同步.
|
||||
resync: 重新同步
|
||||
error_p2p_api: '%{p2p_api} 服务器初始化时出错,请选择屏幕底部的按钮 %{p2p_api} 来检查 %{settings}设置.'
|
||||
error_config: '配置初始化时出错,请选择屏幕底部的按钮 %{settings} 检查设置.'
|
||||
error_unknown: '初始化时出错,请选择屏幕底部的按钮 %{settings} 来检查集成节点设置,或者重新同步.'
|
||||
network_metrics:
|
||||
loading: 指标在同步后将可用
|
||||
emission: 发射
|
||||
inflation: 通货膨胀
|
||||
supply: 供应
|
||||
block_time: Block time
|
||||
reward: 奖励
|
||||
difficulty_window: '难度窗口 %{size}'
|
||||
network_mining:
|
||||
loading: 同步后即可挖矿
|
||||
info: '挖矿服务器已启用,您可以通过选择屏幕底部的按钮 %{settings} 来更改其设置。连接设备后,数据会更新.'
|
||||
restart_server_required: 需要重启服务器才能应用更改.
|
||||
rewards_wallet: 奖励钱包
|
||||
server: 阶层服务器
|
||||
address: 地址
|
||||
miners: 矿工
|
||||
devices: 设备
|
||||
blocks_found: 找到的区块
|
||||
hashrate: '哈希率 (C%{bits})'
|
||||
connected: 已连接
|
||||
disconnected: 已断开连接
|
||||
network_settings:
|
||||
change_value: 更改值
|
||||
stratum_ip: '层 IP 地址:'
|
||||
stratum_port: '层端口:'
|
||||
port_unavailable: 指定的端口不可用
|
||||
restart_node_required: 需要重启节点才能应用更改.
|
||||
choose_wallet: 选择钱包
|
||||
stratum_wallet_warning: 必须打开钱包才能获得奖励.
|
||||
enable: 启用
|
||||
disable: 禁用
|
||||
restart: 重新启动
|
||||
server: 服务器
|
||||
api_ip: 'API IP 地址:'
|
||||
api_port: 'API 端口:'
|
||||
api_secret: '其它API 和 V2 所有者 API 令牌:'
|
||||
foreign_api_secret: '外部 API 令牌:'
|
||||
disabled: 已禁用
|
||||
enabled: 已启用
|
||||
ftl: '未来时间限制 (FTL):'
|
||||
ftl_description: 限制未来多长时间, 相对于节点的本地时间,以秒为单位, 新区块的时间戳可以被接受.
|
||||
not_valid_value: 输入的值无效
|
||||
full_validation: 完全验证
|
||||
full_validation_description: 在处理每个区块时是否运行全链验证(同步期间除外).
|
||||
archive_mode: 存档模式
|
||||
archive_mode_desc: 以全部存档模式运行全节点(同步需要更多的磁盘空间和时间).
|
||||
attempt_time: '尝试挖矿时间 (秒):'
|
||||
attempt_time_desc: 在停止并从池中重新收集交易之前尝试对特定标题进行挖矿的时间
|
||||
min_share_diff: '可接受的最低份额难度:'
|
||||
reset_settings_desc: 将节点设置重置为默认值
|
||||
reset_settings: 重置设置
|
||||
reset: 重置
|
||||
tx_pool: 交易池
|
||||
pool_fee: '接受到矿池的基本费用:'
|
||||
reorg_period: '重组缓存保留期(以分钟为单位):'
|
||||
max_tx_pool: '池中的最大交易数:'
|
||||
max_tx_stempool: 'stem池中的最大交易数:'
|
||||
max_tx_weight: '可以选择构建区块交易的最大总权重:'
|
||||
epoch_duration: '纪元持续时间(以秒为单位):'
|
||||
embargo_timer: '禁止计时器(以秒为单位):'
|
||||
aggregation_period: '聚合周期(以秒为单位):'
|
||||
stem_probability: 'stem助记词概率:'
|
||||
stem_txs: stem交易
|
||||
p2p_server: P2P 服务器
|
||||
p2p_port: 'P2P 端口:'
|
||||
add_seed: 添加 DNS 种子
|
||||
seed_address: 'DNS 种子地址:'
|
||||
add_peer: 添加网络对点
|
||||
peer_address: '网络对点地址:'
|
||||
peer_address_error: '以正确的格式输入 IP 地址或 DNS 名称(确保指定的主机可用),例如:192.168.0.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>
|
||||
|
|
2
macos/Grim.app/Contents/MacOS/.gitignore
vendored
Normal file
2
macos/Grim.app/Contents/MacOS/.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
!.gitignore
|
||||
grim
|
|
@ -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
|
||||
|
|
|
@ -1,81 +1,121 @@
|
|||
#!/bin/bash
|
||||
|
||||
usage="Usage: build_run_android.sh [type] [platform]\n - type: 'debug', 'release'\n - platform: 'v7', 'v8'"
|
||||
usage="Usage: android.sh [type] [platform|version]\n - type: 'build', 'release'\n - platform, for 'build' type: 'v7', 'v8', 'x86'\n - optional version for 'release' (needed on MacOS), example: '0.2.2'"
|
||||
case $1 in
|
||||
debug|release)
|
||||
build|release)
|
||||
;;
|
||||
*)
|
||||
printf "$usage"
|
||||
exit 1
|
||||
esac
|
||||
|
||||
case $2 in
|
||||
v7|v8)
|
||||
;;
|
||||
*)
|
||||
printf "$usage"
|
||||
exit 1
|
||||
esac
|
||||
|
||||
# Setup build directory
|
||||
BASEDIR=$(cd $(dirname $0) && pwd)
|
||||
cd ${BASEDIR}
|
||||
cd ..
|
||||
|
||||
# Setup release argument
|
||||
type=$1
|
||||
[[ ${type} == "release" ]] && release_param="--profile release-apk"
|
||||
|
||||
# Setup platform argument
|
||||
[[ $2 == "v7" ]] && arch+=(armeabi-v7a)
|
||||
[[ $2 == "v8" ]] && arch+=(arm64-v8a)
|
||||
|
||||
# Setup platform path
|
||||
[[ $2 == "v7" ]] && platform+=(armv7-linux-androideabi)
|
||||
[[ $2 == "v8" ]] && platform+=(aarch64-linux-android)
|
||||
|
||||
# Install platform
|
||||
[[ $2 == "v7" ]] && rustup target install armv7-linux-androideabi
|
||||
[[ $2 == "v8" ]] && rustup target install aarch64-linux-android
|
||||
|
||||
# Build native code
|
||||
cargo install cargo-ndk
|
||||
|
||||
sed -i -e 's/"rlib"/"cdylib","rlib"/g' Cargo.toml
|
||||
|
||||
# temp fix for https://stackoverflow.com/questions/57193895/error-use-of-undeclared-identifier-pthread-mutex-robust-cargo-build-liblmdb-s
|
||||
success=0
|
||||
export CPPFLAGS="-DMDB_USE_ROBUST=0" && export CFLAGS="-DMDB_USE_ROBUST=0"
|
||||
cargo ndk -t ${arch} build ${release_param}
|
||||
unset CPPFLAGS && unset CFLAGS
|
||||
cargo ndk -t ${arch} -o android/app/src/main/jniLibs build ${release_param}
|
||||
if [ $? -eq 0 ]
|
||||
then
|
||||
success=1
|
||||
if [[ $1 == "build" ]]; then
|
||||
case $2 in
|
||||
v7|v8|x86)
|
||||
;;
|
||||
*)
|
||||
printf "$usage"
|
||||
exit 1
|
||||
esac
|
||||
fi
|
||||
|
||||
sed -i -e 's/"cdylib","rlib"/"rlib"/g' Cargo.toml
|
||||
# Setup build directory
|
||||
BASEDIR=$(cd "$(dirname "$0")" && pwd)
|
||||
cd "${BASEDIR}" || exit 1
|
||||
cd ..
|
||||
|
||||
# Build Android application and launch at all connected devices
|
||||
if [ $success -eq 1 ]
|
||||
then
|
||||
cd android
|
||||
# Install platforms and tools
|
||||
rustup target add armv7-linux-androideabi
|
||||
rustup target add aarch64-linux-android
|
||||
rustup target add x86_64-linux-android
|
||||
cargo install cargo-ndk
|
||||
|
||||
# Setup gradle argument
|
||||
[[ $1 == "release" ]] && gradle_param+=(assembleRelease)
|
||||
[[ $1 == "debug" ]] && gradle_param+=(build)
|
||||
success=1
|
||||
|
||||
### Build native code
|
||||
function build_lib() {
|
||||
[[ $1 == "v7" ]] && arch=armeabi-v7a
|
||||
[[ $1 == "v8" ]] && arch=arm64-v8a
|
||||
[[ $1 == "x86" ]] && arch=x86_64
|
||||
|
||||
sed -i -e 's/"cdylib","rlib"]/"rlib"]/g' Cargo.toml
|
||||
sed -i -e 's/"rlib"]/"cdylib","rlib"]/g' Cargo.toml
|
||||
|
||||
# Fix for https://stackoverflow.com/questions/57193895/error-use-of-undeclared-identifier-pthread-mutex-robust-cargo-build-liblmdb-s
|
||||
# Uncomment lines below for the 1st build:
|
||||
#export CPPFLAGS="-DMDB_USE_ROBUST=0" && export CFLAGS="-DMDB_USE_ROBUST=0"
|
||||
#cargo ndk -t ${arch} build --profile release-apk
|
||||
#unset CPPFLAGS && unset CFLAGS
|
||||
cargo ndk -t "${arch}" -o android/app/src/main/jniLibs build --profile release-apk
|
||||
if [ $? -eq 0 ]
|
||||
then
|
||||
success=1
|
||||
else
|
||||
success=0
|
||||
fi
|
||||
|
||||
sed -i -e 's/"cdylib","rlib"]/"rlib"]/g' Cargo.toml
|
||||
rm -f Cargo.toml-e
|
||||
}
|
||||
|
||||
### Build application
|
||||
function build_apk() {
|
||||
cd android || exit 1
|
||||
./gradlew clean
|
||||
./gradlew ${gradle_param}
|
||||
# Build signed apk if keystore exists
|
||||
if [ ! -f keystore.properties ]; then
|
||||
./gradlew assembleDebug
|
||||
apk_path=app/build/outputs/apk/debug/app-debug.apk
|
||||
else
|
||||
./gradlew assembleSignedRelease
|
||||
apk_path=app/build/outputs/apk/signedRelease/app-signedRelease.apk
|
||||
fi
|
||||
|
||||
# Setup apk path
|
||||
[[ $1 == "release" ]] && apk_path+=(app/build/outputs/apk/release/app-release.apk)
|
||||
[[ $1 == "debug" ]] && apk_path+=(app/build/outputs/apk/debug/app-debug.apk)
|
||||
if [[ $1 == "" ]]; then
|
||||
# Launch application at all connected devices.
|
||||
for SERIAL in $(adb devices | grep -v List | cut -f 1);
|
||||
do
|
||||
adb -s "$SERIAL" install ${apk_path}
|
||||
sleep 1s
|
||||
adb -s "$SERIAL" shell am start -n mw.gri.android/.MainActivity;
|
||||
done
|
||||
else
|
||||
if [[ "$OSTYPE" != "darwin"* ]]; then
|
||||
version=$(grep -m 1 -Po 'version = "\K[^"]*' Cargo.toml)
|
||||
else
|
||||
version=v$2
|
||||
fi
|
||||
# Setup release file name
|
||||
name=grim-${version}-android-$1.apk
|
||||
[[ $1 == "arm" ]] && name=grim-${version}-android.apk
|
||||
rm -f "${name}"
|
||||
mv ${apk_path} "${name}"
|
||||
|
||||
for SERIAL in $(adb devices | grep -v List | cut -f 1);
|
||||
do
|
||||
adb -s $SERIAL install ${apk_path}
|
||||
sleep 1s
|
||||
adb -s $SERIAL shell am start -n mw.gri.android/.MainActivity;
|
||||
done
|
||||
fi
|
||||
# Calculate checksum
|
||||
checksum=grim-${version}-android-$1-sha256sum.txt
|
||||
[[ $1 == "arm" ]] && checksum=grim-${version}-android-sha256sum.txt
|
||||
rm -f "${checksum}"
|
||||
sha256sum "${name}" > "${checksum}"
|
||||
fi
|
||||
|
||||
cd ..
|
||||
}
|
||||
|
||||
rm -rf android/app/src/main/jniLibs/*
|
||||
|
||||
if [[ $1 == "build" ]]; then
|
||||
build_lib "$2"
|
||||
[ $success -eq 1 ] && build_apk
|
||||
else
|
||||
rm -rf target/release-apk
|
||||
rm -rf target/aarch64-linux-android
|
||||
rm -rf target/x86_64-linux-android
|
||||
rm -rf target/armv7-linux-androideabi
|
||||
|
||||
build_lib "v7"
|
||||
[ $success -eq 1 ] && build_lib "v8"
|
||||
[ $success -eq 1 ] && build_apk "arm" "$2"
|
||||
rm -rf android/app/src/main/jniLibs/*
|
||||
[ $success -eq 1 ] && build_lib "x86"
|
||||
[ $success -eq 1 ] && build_apk "x86_64" "$2"
|
||||
fi
|
||||
|
|
|
@ -1,25 +1,27 @@
|
|||
#!/bin/bash
|
||||
|
||||
case $1 in
|
||||
debug|release)
|
||||
debug|build)
|
||||
;;
|
||||
*)
|
||||
echo "Usage: build_run.sh [type] where is type is 'debug' or 'release'" >&2
|
||||
echo "Usage: build_run.sh [type] where is type is 'debug' or 'build'" >&2
|
||||
exit 1
|
||||
esac
|
||||
|
||||
# Setup build directory
|
||||
BASEDIR=$(cd $(dirname $0) && pwd)
|
||||
cd ${BASEDIR}
|
||||
BASEDIR=$(cd "$(dirname $0)" && pwd)
|
||||
cd "${BASEDIR}" || return
|
||||
cd ..
|
||||
|
||||
# Build application
|
||||
type=$1
|
||||
[[ ${type} == "release" ]] && release_param+=(--release)
|
||||
cargo build ${release_param[@]}
|
||||
[[ ${type} == "build" ]] && release_param+=(--release)
|
||||
cargo --config profile.release.incremental=true build "${release_param[@]}"
|
||||
|
||||
# Start application
|
||||
if [ $? -eq 0 ]
|
||||
then
|
||||
./target/${type}/grim
|
||||
fi
|
||||
path=${type}
|
||||
[[ ${type} == "build" ]] && path="release"
|
||||
./target/"${path}"/grim
|
||||
fi
|
||||
|
|
99
scripts/version.sh
Executable file
99
scripts/version.sh
Executable file
|
@ -0,0 +1,99 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Usage to bump version
|
||||
# ./version.sh patch
|
||||
# ./version.sh minor
|
||||
# ./version.sh major
|
||||
|
||||
# Setup base directory
|
||||
BASEDIR=$(cd $(dirname $0) && pwd)
|
||||
cd ${BASEDIR}
|
||||
cd ..
|
||||
|
||||
# Exit script if command fails or uninitialized variables used
|
||||
set -euo pipefail
|
||||
|
||||
# ==================================
|
||||
# Verify repo is clean
|
||||
# ==================================
|
||||
|
||||
# List uncommitted changes and
|
||||
# check if the output is not empty
|
||||
if [ -n "$(git status --porcelain)" ]; then
|
||||
# Print error message
|
||||
printf "\nError: repo has uncommitted changes\n\n"
|
||||
# Exit with error code
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ==================================
|
||||
# Get latest version from git tags
|
||||
# ==================================
|
||||
|
||||
# List git tags sorted lexicographically
|
||||
# so version numbers sorted correctly
|
||||
GIT_TAGS=$(git tag --sort=version:refname)
|
||||
|
||||
# Get last line of output which returns the
|
||||
# last tag (most recent version)
|
||||
GIT_TAG_LATEST=$(echo "$GIT_TAGS" | tail -n 1)
|
||||
|
||||
# If no tag found, default to v0.1.0
|
||||
if [ -z "$GIT_TAG_LATEST" ]; then
|
||||
GIT_TAG_LATEST="v0.1.0"
|
||||
fi
|
||||
|
||||
# Strip prefix 'v' from the tag to easily increment
|
||||
GIT_TAG_LATEST=$(echo "$GIT_TAG_LATEST" | sed 's/^v//')
|
||||
|
||||
# ==================================
|
||||
# Increment version number
|
||||
# ==================================
|
||||
|
||||
# Get version type from first argument passed to script
|
||||
VERSION_TYPE="${1-}"
|
||||
VERSION_NEXT=""
|
||||
|
||||
if [ "$VERSION_TYPE" = "patch" ]; then
|
||||
# Increment patch version
|
||||
VERSION_NEXT="$(echo "$GIT_TAG_LATEST" | awk -F. '{$NF++; print $1"."$2"."$NF}')"
|
||||
elif [ "$VERSION_TYPE" = "minor" ]; then
|
||||
# Increment minor version
|
||||
VERSION_NEXT="$(echo "$GIT_TAG_LATEST" | awk -F. '{$2++; $3=0; print $1"."$2"."$3}')"
|
||||
elif [ "$VERSION_TYPE" = "major" ]; then
|
||||
# Increment major version
|
||||
VERSION_NEXT="$(echo "$GIT_TAG_LATEST" | awk -F. '{$1++; $2=0; $3=0; print $1"."$2"."$3}')"
|
||||
else
|
||||
# Print error for unknown versioning type
|
||||
printf "\nError: invalid VERSION_TYPE arg passed, must be 'patch', 'minor' or 'major'\n\n"
|
||||
# Exit with error code
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Update version for Windows installer.
|
||||
sed -i '' -e 's/" Version="[^\"]*"/" Version="'"$VERSION_NEXT"'"/g' wix/main.wxs
|
||||
sed -i '' -e 's/<Package Id="[^\"]*"/<Package Id="'"$(uuidgen)"'"/g' wix/main.wxs
|
||||
|
||||
# Update Android version in build.gradle
|
||||
sed -i'.bak' -e 's/versionName [0-9a-zA-Z -_]*/versionName "'"$VERSION_NEXT"'"/' android/app/build.gradle
|
||||
rm -f android/app/build.gradle.bak
|
||||
|
||||
# Update version in Cargo.toml
|
||||
sed -i'.bak' -e "s/^version = .*/version = \"$VERSION_NEXT\"/" Cargo.toml
|
||||
rm -f Cargo.toml.bak
|
||||
|
||||
# Update Cargo.lock as this changes when
|
||||
# updating the version in your manifest
|
||||
cargo update -p grim
|
||||
|
||||
# Commit the changes
|
||||
git add .
|
||||
git commit -m "release: v$VERSION_NEXT"
|
||||
|
||||
# ==================================
|
||||
# Create git tag for new version
|
||||
# ==================================
|
||||
|
||||
# Create a tag and push to master branch
|
||||
git tag "v$VERSION_NEXT" master
|
||||
#git push origin master --follow-tags
|
333
src/gui/app.rs
Normal file → Executable file
333
src/gui/app.rs
Normal file → Executable file
|
@ -12,63 +12,73 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use lazy_static::lazy_static;
|
||||
use egui::{Align, Context, CursorIcon, Layout, Modifiers, Rect, ResizeDirection, Rounding, Stroke, ViewportCommand};
|
||||
use egui::epaint::{RectShape};
|
||||
use egui::os::OperatingSystem;
|
||||
use egui::epaint::RectShape;
|
||||
use egui::{Align, Context, CornerRadius, CursorIcon, LayerId, Layout, Modifiers, Order, ResizeDirection, Stroke, StrokeKind, UiBuilder, ViewportCommand};
|
||||
|
||||
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};
|
||||
|
||||
lazy_static! {
|
||||
/// State to check if platform Back button was pressed.
|
||||
static ref BACK_BUTTON_PRESSED: AtomicBool = AtomicBool::new(false);
|
||||
}
|
||||
use crate::gui::views::types::ContentContainer;
|
||||
use crate::gui::views::{Content, KeyboardContent, Modal, TitlePanel, View};
|
||||
use crate::gui::Colors;
|
||||
use crate::AppConfig;
|
||||
|
||||
/// 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_visuals(ctx);
|
||||
}
|
||||
|
||||
/// Draw application content.
|
||||
pub fn ui(&mut self, ctx: &Context) {
|
||||
// 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 {
|
||||
BACK_BUTTON_PRESSED.store(false, Ordering::Relaxed);
|
||||
}
|
||||
if self.first_draw {
|
||||
self.on_first_draw(ctx);
|
||||
self.first_draw = false;
|
||||
}
|
||||
|
||||
// Handle Esc keyboard key event.
|
||||
if ctx.input_mut(|i| i.consume_key(Modifiers::NONE, egui::Key::Escape)) {
|
||||
self.content.on_back(&self.platform);
|
||||
// Request repaint to update previous content.
|
||||
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 +86,103 @@ 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::Middle);
|
||||
// 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 +198,53 @@ impl<Platform: PlatformCallbacks> App<Platform> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Draw window content for desktop.
|
||||
fn window_content(&mut self, ui: &mut egui::Ui) {
|
||||
let content_rect = {
|
||||
/// Draw title panel background.
|
||||
fn title_panel_bg(ui: &mut egui::Ui, window_title: bool) {
|
||||
let title_rect = {
|
||||
let mut rect = ui.max_rect();
|
||||
rect.min.y += Content::WINDOW_TITLE_HEIGHT;
|
||||
if window_title {
|
||||
rect.min.y += Content::WINDOW_TITLE_HEIGHT - 0.5;
|
||||
}
|
||||
rect.max.y = rect.min.y + View::get_top_inset() + TitlePanel::HEIGHT;
|
||||
rect
|
||||
};
|
||||
// Draw main content.
|
||||
let mut content_ui = ui.child_ui(content_rect, *ui.layout(), None);
|
||||
self.content.ui(&mut content_ui, &self.platform);
|
||||
let title_bg = RectShape::filled(title_rect, 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::Middle);
|
||||
// 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 +253,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 +270,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.
|
||||
|
@ -392,16 +388,13 @@ impl<Platform: PlatformCallbacks> eframe::App for App<Platform> {
|
|||
}
|
||||
|
||||
fn clear_color(&self, _visuals: &egui::Visuals) -> [f32; 4] {
|
||||
if View::is_desktop() {
|
||||
let is_mac_os = OperatingSystem::from_target_os() == OperatingSystem::Mac;
|
||||
if is_mac_os {
|
||||
Colors::fill().to_normalized_gamma_f32()
|
||||
} else {
|
||||
egui::Rgba::TRANSPARENT.to_array()
|
||||
}
|
||||
} else {
|
||||
Colors::fill().to_normalized_gamma_f32()
|
||||
let os = egui::os::OperatingSystem::from_target_os();
|
||||
let is_win = os == egui::os::OperatingSystem::Windows;
|
||||
let is_mac = os == egui::os::OperatingSystem::Mac;
|
||||
if !View::is_desktop() || is_win || is_mac {
|
||||
return Colors::fill_lite().to_normalized_gamma_f32();
|
||||
}
|
||||
Colors::TRANSPARENT.to_normalized_gamma_f32()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -31,16 +31,23 @@ 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 +61,9 @@ const TEXT_BUTTON_DARK: Color32 = Color32::from_gray(195);
|
|||
const TITLE: Color32 = Color32::from_gray(60);
|
||||
const TITLE_DARK: Color32 = Color32::from_gray(205);
|
||||
|
||||
const BUTTON: Color32 = Color32::from_gray(249);
|
||||
const BUTTON_DARK: Color32 = Color32::from_gray(16);
|
||||
|
||||
const GRAY: Color32 = Color32::from_gray(120);
|
||||
const GRAY_DARK: Color32 = Color32::from_gray(145);
|
||||
|
||||
const STROKE: Color32 = Color32::from_gray(200);
|
||||
const STROKE_DARK: Color32 = Color32::from_gray(50);
|
||||
|
||||
const INACTIVE_TEXT: Color32 = Color32::from_gray(150);
|
||||
|
@ -82,6 +85,7 @@ fn use_dark() -> bool {
|
|||
|
||||
impl Colors {
|
||||
pub const TRANSPARENT: Color32 = Color32::from_rgba_premultiplied(0, 0, 0, 0);
|
||||
pub const STROKE: Color32 = Color32::from_gray(200);
|
||||
|
||||
pub fn white_or_black(black_in_white: bool) -> Color32 {
|
||||
if use_dark() {
|
||||
|
@ -125,7 +129,7 @@ impl Colors {
|
|||
|
||||
pub fn green() -> Color32 {
|
||||
if use_dark() {
|
||||
GREEN.gamma_multiply(1.3)
|
||||
GREEN_DARK
|
||||
} else {
|
||||
GREEN
|
||||
}
|
||||
|
@ -133,7 +137,7 @@ impl Colors {
|
|||
|
||||
pub fn red() -> Color32 {
|
||||
if use_dark() {
|
||||
RED.gamma_multiply(1.3)
|
||||
RED_DARK
|
||||
} else {
|
||||
RED
|
||||
}
|
||||
|
@ -141,7 +145,7 @@ impl Colors {
|
|||
|
||||
pub fn blue() -> Color32 {
|
||||
if use_dark() {
|
||||
BLUE.gamma_multiply(1.3)
|
||||
BLUE_DARK
|
||||
} else {
|
||||
BLUE
|
||||
}
|
||||
|
@ -163,6 +167,14 @@ impl Colors {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn fill_lite() -> Color32 {
|
||||
if use_dark() {
|
||||
FILL_LITE_DARK
|
||||
} else {
|
||||
FILL_LITE
|
||||
}
|
||||
}
|
||||
|
||||
pub fn checkbox() -> Color32 {
|
||||
if use_dark() {
|
||||
CHECKBOX_DARK
|
||||
|
@ -195,14 +207,6 @@ impl Colors {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn button() -> Color32 {
|
||||
if use_dark() {
|
||||
BUTTON_DARK
|
||||
} else {
|
||||
BUTTON
|
||||
}
|
||||
}
|
||||
|
||||
pub fn gray() -> Color32 {
|
||||
if use_dark() {
|
||||
GRAY_DARK
|
||||
|
@ -215,7 +219,7 @@ impl Colors {
|
|||
if use_dark() {
|
||||
STROKE_DARK
|
||||
} else {
|
||||
STROKE
|
||||
Self::STROKE
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -227,7 +231,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! {
|
||||
|
|
|
@ -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_mac::nokhwa_initialize;
|
||||
use nokhwa_mac::pixel_format::RgbFormat;
|
||||
use nokhwa_mac::utils::{CameraIndex, RequestedFormat, RequestedFormatType};
|
||||
use nokhwa_mac::utils::ApiBackend;
|
||||
use nokhwa_mac::query;
|
||||
use nokhwa_mac::CallbackCamera;
|
||||
|
||||
// Ask permission to open camera.
|
||||
nokhwa_initialize(|_| {});
|
||||
|
||||
thread::spawn(move || {
|
||||
let cameras = query(ApiBackend::Auto).unwrap();
|
||||
cameras_amount.store(cameras.len(), Ordering::Relaxed);
|
||||
let index = camera_index.load(Ordering::Relaxed);
|
||||
if cameras.is_empty() || index >= cameras.len() {
|
||||
return;
|
||||
}
|
||||
// Start camera.
|
||||
let camera_index = CameraIndex::Index(camera_index.load(Ordering::Relaxed) as u32);
|
||||
let camera_callback = CallbackCamera::new(
|
||||
camera_index,
|
||||
RequestedFormat::new::<RgbFormat>(RequestedFormatType::AbsoluteHighestFrameRate),
|
||||
|_| {}
|
||||
);
|
||||
if let Ok(mut cb) = camera_callback {
|
||||
if cb.open_stream().is_ok() {
|
||||
loop {
|
||||
// Stop if camera was stopped.
|
||||
if stop_camera.load(Ordering::Relaxed) {
|
||||
stop_camera.store(false, Ordering::Relaxed);
|
||||
// Clear image.
|
||||
let mut w_image = LAST_CAMERA_IMAGE.write();
|
||||
*w_image = None;
|
||||
break;
|
||||
}
|
||||
// Get image from camera.
|
||||
if let Ok(frame) = cb.poll_frame() {
|
||||
let image = frame.decode_image::<RgbFormat>().unwrap();
|
||||
let mut bytes: Vec<u8> = Vec::new();
|
||||
let format = image::ImageFormat::Jpeg;
|
||||
// Convert image to Jpeg format.
|
||||
image.write_to(&mut std::io::Cursor::new(&mut bytes), format).unwrap();
|
||||
let mut w_image = LAST_CAMERA_IMAGE.write();
|
||||
*w_image = Some((bytes, 0));
|
||||
} else {
|
||||
// Clear image.
|
||||
let mut w_image = LAST_CAMERA_IMAGE.write();
|
||||
*w_image = None;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl PlatformCallbacks for Desktop {
|
||||
fn 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);
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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,50 @@ 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, cb: &dyn PlatformCallbacks) -> bool {
|
||||
if Modal::on_back() {
|
||||
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,20 +178,20 @@ 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() {
|
||||
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| {
|
||||
|
@ -226,15 +217,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| {
|
||||
View::button_ui(ui, t!("modal_exit.exit"), Colors::white_or_black(false), |_| {
|
||||
if !Node::is_running() {
|
||||
self.exit_allowed = true;
|
||||
ui.ctx().send_viewport_cmd(egui::ViewportCommand::Close);
|
||||
modal.close();
|
||||
cb.exit();
|
||||
Modal::close();
|
||||
} else {
|
||||
Node::stop(true);
|
||||
modal.disable_closing();
|
||||
|
@ -248,151 +239,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)
|
||||
}
|
|
@ -12,42 +12,58 @@
|
|||
// 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 egui::CornerRadius;
|
||||
use parking_lot::RwLock;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::{fs, thread};
|
||||
|
||||
use crate::gui::Colors;
|
||||
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, ItemButton(CornerRadius), Tab
|
||||
}
|
||||
|
||||
/// Button to pick file and parse its data into text.
|
||||
pub struct FilePickButton {
|
||||
pub struct FilePickContent {
|
||||
/// Content type.
|
||||
content_type: FilePickContentType,
|
||||
|
||||
/// Flag to check if file is picking.
|
||||
pub file_picking: Arc<AtomicBool>,
|
||||
file_picking: Arc<AtomicBool>,
|
||||
|
||||
/// Flag to parse file content after pick.
|
||||
parse_file: bool,
|
||||
/// Flag to check if file is parsing.
|
||||
pub file_parsing: Arc<AtomicBool>,
|
||||
file_parsing: Arc<AtomicBool>,
|
||||
/// File parsing result.
|
||||
pub file_parsing_result: Arc<RwLock<Option<String>>>
|
||||
file_parsing_result: Arc<RwLock<Option<String>>>,
|
||||
}
|
||||
|
||||
impl Default for FilePickButton {
|
||||
fn default() -> Self {
|
||||
impl FilePickContent {
|
||||
/// Create new content from provided type.
|
||||
pub fn new(content_type: FilePickContentType) -> Self {
|
||||
Self {
|
||||
content_type,
|
||||
file_picking: Arc::new(AtomicBool::new(false)),
|
||||
parse_file: true,
|
||||
file_parsing: Arc::new(AtomicBool::new(false)),
|
||||
file_parsing_result: Arc::new(RwLock::new(None))
|
||||
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)) {
|
||||
/// Do not parse file content.
|
||||
pub fn no_parse(mut self) -> Self {
|
||||
self.parse_file = false;
|
||||
self
|
||||
}
|
||||
|
||||
/// Draw content with provided callback to return path of the file.
|
||||
pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks, on_pick: impl FnOnce(String)) {
|
||||
if self.file_picking.load(Ordering::Relaxed) {
|
||||
View::small_loading_spinner(ui);
|
||||
// Check file pick result.
|
||||
|
@ -70,7 +86,7 @@ impl FilePickButton {
|
|||
r_res.clone().unwrap()
|
||||
};
|
||||
// Callback on result.
|
||||
on_result(text);
|
||||
on_pick(text);
|
||||
// Clear result.
|
||||
let mut w_res = self.file_parsing_result.write();
|
||||
*w_res = None;
|
||||
|
@ -78,12 +94,48 @@ impl FilePickButton {
|
|||
}
|
||||
} else {
|
||||
// Draw button to pick file.
|
||||
let file_text = format!("{} {}", ARCHIVE_BOX, t!("choose_file"));
|
||||
View::colored_text_button(ui, file_text, Colors::blue(), Colors::button(), || {
|
||||
if let Some(path) = cb.pick_file() {
|
||||
self.on_file_pick(path);
|
||||
match self.content_type {
|
||||
FilePickContentType::Button => {
|
||||
let text = format!("{} {}", ARCHIVE_BOX, t!("choose_file"));
|
||||
View::colored_text_button(ui,
|
||||
text,
|
||||
Colors::blue(),
|
||||
Colors::white_or_black(false),
|
||||
|| {
|
||||
if let Some(path) = cb.pick_file() {
|
||||
if !self.parse_file {
|
||||
on_pick(path);
|
||||
return;
|
||||
}
|
||||
self.on_file_pick(path);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
FilePickContentType::ItemButton(r) => {
|
||||
View::item_button(ui, r, ARCHIVE_BOX, Some(Colors::blue()), || {
|
||||
if let Some(path) = cb.pick_file() {
|
||||
if !self.parse_file {
|
||||
on_pick(path);
|
||||
return;
|
||||
}
|
||||
self.on_file_pick(path);
|
||||
}
|
||||
});
|
||||
}
|
||||
FilePickContentType::Tab => {
|
||||
let active = self.file_parsing.load(Ordering::Relaxed) ||
|
||||
self.file_picking.load(Ordering::Relaxed);
|
||||
View::tab_button(ui, ARCHIVE_BOX, Some(Colors::blue()), Some(active), |_| {
|
||||
if let Some(path) = cb.pick_file() {
|
||||
if !self.parse_file {
|
||||
on_pick(path);
|
||||
return;
|
||||
}
|
||||
self.on_file_pick(path);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -94,6 +146,10 @@ impl FilePickButton {
|
|||
self.file_picking.store(true, Ordering::Relaxed);
|
||||
return;
|
||||
}
|
||||
// Do not parse result.
|
||||
if !self.parse_file {
|
||||
return;
|
||||
}
|
||||
self.file_parsing.store(true, Ordering::Relaxed);
|
||||
let result = self.file_parsing_result.clone();
|
||||
thread::spawn(move || {
|
321
src/gui/views/input/edit.rs
Normal file
321
src/gui/views/input/edit.rs
Normal file
|
@ -0,0 +1,321 @@
|
|||
// 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::{Layout, TextBuffer, TextStyle, Widget, Align};
|
||||
use egui::text_edit::TextEditState;
|
||||
use crate::gui::Colors;
|
||||
use crate::gui::icons::{CLIPBOARD_TEXT, COPY, EYE, EYE_SLASH, SCAN};
|
||||
use crate::gui::platform::PlatformCallbacks;
|
||||
use crate::gui::views::input::keyboard::KeyboardContent;
|
||||
use crate::gui::views::{KeyboardEvent, View};
|
||||
|
||||
/// Text input content.
|
||||
pub struct TextEdit {
|
||||
/// View identifier.
|
||||
id: egui::Id,
|
||||
/// Check if horizontal centering is needed.
|
||||
h_center: bool,
|
||||
/// Check if focus is needed.
|
||||
focus: bool,
|
||||
/// Check if 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,
|
||||
/// Callback when scan button was pressed.
|
||||
pub scan_pressed: bool,
|
||||
/// Callback when Enter key was pressed.
|
||||
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 = 41.0;
|
||||
|
||||
pub fn new(id: egui::Id) -> Self {
|
||||
Self {
|
||||
id,
|
||||
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: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw text input content.
|
||||
pub fn ui(&mut self, ui: &mut egui::Ui, input: &mut String, 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);
|
||||
}
|
||||
|
||||
// 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)
|
||||
.id(self.id)
|
||||
.font(TextStyle::Heading)
|
||||
.min_size(edit_rect.size())
|
||||
.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 {
|
||||
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 key input.
|
||||
if !self.focus_request {
|
||||
if ui.ctx().input(|i| i.key_pressed(egui::Key::Enter)) {
|
||||
self.enter_pressed = true;
|
||||
}
|
||||
}
|
||||
if self.enter_pressed {
|
||||
KeyboardContent::unshift();
|
||||
}
|
||||
if !self.no_soft_keyboard {
|
||||
KeyboardContent::default().window_ui(self.numeric, ui.ctx());
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
if let Some(input) = KeyboardContent::consume_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 input {
|
||||
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
|
||||
}
|
||||
|
||||
/// 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
|
||||
}
|
||||
}
|
509
src/gui/views/input/keyboard.rs
Normal file
509
src/gui/views/input/keyboard.rs
Normal file
|
@ -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.screen_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).as_str(), locale = Self::input_locale().as_str())
|
||||
} 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();
|
||||
}
|
||||
}
|
22
src/gui/views/input/mod.rs
Normal file
22
src/gui/views/input/mod.rs
Normal file
|
@ -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::*;
|
49
src/gui/views/input/types.rs
Normal file
49
src/gui/views/input/types.rs
Normal file
|
@ -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)),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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::*;
|
297
src/gui/views/modal.rs
Normal file → Executable file
297
src/gui/views/modal.rs
Normal file → Executable file
|
@ -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;
|
||||
}
|
||||
|
@ -100,26 +112,21 @@ impl Modal {
|
|||
/// 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() {
|
||||
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,9 +141,21 @@ 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.
|
||||
let mut w_state = MODAL_STATE.write();
|
||||
if w_state.modal.is_some() {
|
||||
let mut modal = w_state.modal.clone().unwrap();
|
||||
|
@ -145,8 +164,19 @@ impl 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 +185,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.screen_rect();
|
||||
let is_mac = OperatingSystem::Mac == OperatingSystem::from_target_os();
|
||||
if !is_mac && !is_fullscreen {
|
||||
r = r.shrink(Content::WINDOW_FRAME_MARGIN - 1.0);
|
||||
}
|
||||
r.min.y += Content::WINDOW_TITLE_HEIGHT;
|
||||
r
|
||||