Compare commits
264 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bb2d8fd7e0 | ||
|
|
2b6d418fe6 | ||
|
|
d8b1126b96 | ||
|
|
2eeddcfa80 | ||
|
|
59e3675a36 | ||
|
|
bc386f080d | ||
|
|
2dc1377154 | ||
|
|
610852e2f2 | ||
|
|
15b19bb8ce | ||
|
|
4a598b1837 | ||
|
|
caee2417d6 | ||
|
|
349ca36d4e | ||
|
|
ec86f5caf2 | ||
|
|
b5a5cdfcd2 | ||
|
|
72d799e065 | ||
|
|
d06f22dcd0 | ||
|
|
cb90630f27 | ||
|
|
59ad9204d0 | ||
|
|
cb97c16f5e | ||
|
|
69b48d5345 | ||
|
|
45ed4708c9 | ||
|
|
f3c77bdb3b | ||
|
|
dc0eb9eec1 | ||
|
|
83dd6443cb | ||
|
|
3d77f2d135 | ||
|
|
1ea219bddc | ||
|
|
39adba62d1 | ||
|
|
3526e84e04 | ||
|
|
bfdb706b60 | ||
|
|
a297e07055 | ||
|
|
56b4664ec7 | ||
|
|
70f7c75a92 | ||
|
|
e414b4de92 | ||
|
|
79e68f473f | ||
|
|
6656604809 | ||
|
|
85b4d11912 | ||
|
|
7769a23f59 | ||
|
|
c442f43090 | ||
|
|
d73670bf43 | ||
|
|
dd1967f0d0 | ||
|
|
e3d2fc64ac | ||
|
|
e07f20bf29 | ||
|
|
34f216181f | ||
|
|
8aef775474 | ||
|
|
f669ad92b6 | ||
|
|
cc0b272770 | ||
|
|
9ea6de340d | ||
|
|
be37f8a2a3 | ||
|
|
8a12fac39f | ||
|
|
0242fe12e3 | ||
|
|
acf2e1a5ec | ||
|
|
626db4be56 | ||
|
|
5941fa1ec7 | ||
|
|
1dd8651a1a | ||
|
|
33dd0ca16b | ||
|
|
dfe7852c25 | ||
|
|
abe6184f63 | ||
|
|
e8afb2143b | ||
|
|
7e1c363bad | ||
|
|
bcf6809deb | ||
|
|
bce5f6cf61 | ||
|
|
5495eebb38 | ||
|
|
d255801666 | ||
|
|
44f2601126 | ||
|
|
3a7366c4bc | ||
|
|
5da289714d | ||
|
|
a18d718744 | ||
|
|
c90fc461d9 | ||
|
|
e80fbe8934 | ||
|
|
e0650ade4f | ||
|
|
378b8458f2 | ||
|
|
182028d9ea | ||
|
|
0c3b8e7610 | ||
|
|
332fdcd2a7 | ||
|
|
aa20d04d3a | ||
|
|
949106bc09 | ||
|
|
67babc2858 | ||
|
|
d087ec510e | ||
|
|
c2ed3da87c | ||
|
|
f2d159f732 | ||
|
|
468fc2207d | ||
|
|
eced8bae82 | ||
|
|
e61ecb3963 | ||
|
|
304f4f8b2c | ||
|
|
48888087e1 | ||
|
|
75a70e70be | ||
|
|
cf825f912c | ||
|
|
1944a49fd8 | ||
|
|
657f343f5c | ||
|
|
74bb90b3d8 | ||
|
|
8bf9828c80 | ||
|
|
06324def38 | ||
|
|
70259a5ec5 | ||
|
|
a63057c594 | ||
|
|
48d5270611 | ||
|
|
712d0f3342 | ||
|
|
d6084aeca1 | ||
|
|
562b9624d7 | ||
|
|
a68d5e8bbe | ||
|
|
d45aa8197e | ||
|
|
314d3ef97a | ||
|
|
e3750ccd51 | ||
|
|
a712efe9d8 | ||
|
|
2266362e24 | ||
|
|
b7056b5baa | ||
|
|
8bd07bf56c | ||
|
|
569183efe9 | ||
|
|
937cf25e9b | ||
|
|
e2b6617577 | ||
|
|
8323d6394b | ||
|
|
e31b892a20 | ||
|
|
65b1518e26 | ||
|
|
865dbd3799 | ||
|
|
7de9d7967a | ||
|
|
aa2d2454e1 | ||
|
|
b850336872 | ||
|
|
138dec35c7 | ||
|
|
569fffa962 | ||
|
|
2a283e6793 | ||
|
|
73a7ba3ac9 | ||
|
|
c0c4ea9f86 | ||
|
|
a13179cd09 | ||
|
|
de089b7b73 | ||
|
|
1b700fb8e0 | ||
|
|
f0febf13f2 | ||
|
|
a002967a92 | ||
|
|
1167b20d89 | ||
|
|
297ff3ae90 | ||
|
|
a39e2ce15a | ||
|
|
78eda275d6 | ||
|
|
1fce0fd77d | ||
|
|
c942393f21 | ||
|
|
040cc30e73 | ||
|
|
6b75ffc928 | ||
|
|
a7f1b21f91 | ||
|
|
c8f7d9d5bc | ||
|
|
2520d45dc4 | ||
|
|
8bdf8d98c3 | ||
|
|
04025f3d32 | ||
|
|
4c7ed9c8ee | ||
|
|
c36f8b0df3 | ||
|
|
5ccec93940 | ||
|
|
14b15e18f3 | ||
|
|
3e6f3b4d80 | ||
|
|
470b3106cb | ||
|
|
e0074bc3ab | ||
|
|
8d04ecdc52 | ||
|
|
c83b1e88b9 | ||
|
|
0446cc499e | ||
|
|
8a6507e834 | ||
|
|
09893b9472 | ||
|
|
13d2290205 | ||
|
|
46e4c85563 | ||
|
|
d925036bd6 | ||
|
|
fbf2799674 | ||
|
|
12c7558b91 | ||
|
|
d55af76260 | ||
|
|
e4c70e2efb | ||
|
|
a971fee132 | ||
|
|
a45e0f78ef | ||
|
|
bf07dcb9ea | ||
|
|
e5f5b8f831 | ||
|
|
a80513fc50 | ||
|
|
f71de1742a | ||
|
|
57c65fdcda | ||
|
|
0a9cf5f9aa | ||
|
|
08fdf2bdad | ||
|
|
690c6bac38 | ||
|
|
af81308097 | ||
|
|
90b79f5c04 | ||
|
|
0c5dcec7bc | ||
|
|
a30dfbc15d | ||
|
|
52f3335977 | ||
|
|
c8b3e953ad | ||
|
|
e9d0526e1b | ||
|
|
408c3be675 | ||
|
|
f67f16733f | ||
|
|
25a173ad7b | ||
|
|
75ec88f7a7 | ||
|
|
e9c5ffb430 | ||
|
|
cb2cdaed12 | ||
|
|
029c7f1e2a | ||
|
|
5a8d6895fa | ||
|
|
bb11e23006 | ||
|
|
37b00d49c8 | ||
|
|
8055aed507 | ||
|
|
e7cef05c6a | ||
|
|
313746b578 | ||
|
|
a7c557222c | ||
|
|
9c902fb264 | ||
|
|
079f74d960 | ||
|
|
00d7de5276 | ||
|
|
3e928365de | ||
|
|
47ba174fb1 | ||
|
|
9446296daa | ||
|
|
7175a6fa7d | ||
|
|
1b06f7d317 | ||
|
|
c739bf6bfb | ||
|
|
dec9a72b41 | ||
|
|
593cbaa067 | ||
|
|
fb8906e371 | ||
|
|
df943250ac | ||
|
|
5a522a1489 | ||
|
|
0b0d64b9d0 | ||
|
|
18876e8a69 | ||
|
|
b668378e23 | ||
|
|
9de2c09a27 | ||
|
|
6b3d2bef12 | ||
|
|
344ed41bc7 | ||
|
|
2e711c3ac9 | ||
|
|
1bf4486cf1 | ||
|
|
cb116286ed | ||
|
|
7f0ae95dfb | ||
|
|
fe7ec9dcf5 | ||
|
|
d88eccdda3 | ||
|
|
78fe01d9a4 | ||
|
|
60cb41c76b | ||
|
|
af78f3bac4 | ||
|
|
fff86dcc8d | ||
|
|
5ec053ca34 | ||
|
|
e9f1631b06 | ||
|
|
3705993330 | ||
|
|
3db338da3e | ||
|
|
d126d0f5b8 | ||
|
|
549adebb30 | ||
|
|
40bada35c6 | ||
|
|
074903a299 | ||
|
|
328bee94e5 | ||
|
|
0db25f14f1 | ||
|
|
877e4f9416 | ||
|
|
8b3e864ffa | ||
|
|
5f5f677b7b | ||
|
|
3933d83d3e | ||
|
|
4abd35fb44 | ||
|
|
e68afb04eb | ||
|
|
bf2be96b29 | ||
|
|
b0b5048b01 | ||
|
|
aff69af690 | ||
|
|
c1d156cd6b | ||
|
|
e58e00be9d | ||
|
|
440fe972f4 | ||
|
|
e24588b961 | ||
|
|
57c8d69e83 | ||
|
|
79c0bebcf5 | ||
|
|
321c9c20d5 | ||
|
|
f6134b47da | ||
|
|
8c282b28a0 | ||
|
|
656cd11876 | ||
|
|
5e77c08872 | ||
|
|
1090f64117 | ||
|
|
470aaa29dc | ||
|
|
c6664af45b | ||
|
|
d6b0ce2565 | ||
|
|
770c9632ae | ||
|
|
cd60773d73 | ||
|
|
06cdd92129 | ||
|
|
315df33bd6 | ||
|
|
f990bda4e5 | ||
|
|
b755ad3602 | ||
|
|
b060b2827e | ||
|
|
73493b288f | ||
|
|
87640fb824 | ||
|
|
fb0a48f9db | ||
|
|
acc8670aa9 |
28
.github/dependabot.yml
vendored
28
.github/dependabot.yml
vendored
@@ -1,28 +0,0 @@
|
|||||||
version: 2
|
|
||||||
updates:
|
|
||||||
- package-ecosystem: github-actions
|
|
||||||
directory: /
|
|
||||||
schedule:
|
|
||||||
interval: daily
|
|
||||||
groups:
|
|
||||||
actions:
|
|
||||||
patterns:
|
|
||||||
- "*"
|
|
||||||
- package-ecosystem: cargo
|
|
||||||
directory: userspace/ksud
|
|
||||||
schedule:
|
|
||||||
interval: daily
|
|
||||||
allow:
|
|
||||||
- dependency-type: "all"
|
|
||||||
groups:
|
|
||||||
crates:
|
|
||||||
patterns:
|
|
||||||
- "*"
|
|
||||||
- package-ecosystem: gradle
|
|
||||||
directory: manager
|
|
||||||
schedule:
|
|
||||||
interval: daily
|
|
||||||
groups:
|
|
||||||
maven:
|
|
||||||
patterns:
|
|
||||||
- "*"
|
|
||||||
28
.github/workflows/build-lkm.yml
vendored
28
.github/workflows/build-lkm.yml
vendored
@@ -3,9 +3,9 @@ on:
|
|||||||
workflow_call:
|
workflow_call:
|
||||||
inputs:
|
inputs:
|
||||||
upload:
|
upload:
|
||||||
required: false
|
required: true
|
||||||
type: boolean
|
type: boolean
|
||||||
default: false
|
default: true
|
||||||
description: "Whether to upload to branch"
|
description: "Whether to upload to branch"
|
||||||
secrets:
|
secrets:
|
||||||
# username:github_pat
|
# username:github_pat
|
||||||
@@ -14,9 +14,9 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
upload:
|
upload:
|
||||||
required: false
|
required: true
|
||||||
type: boolean
|
type: boolean
|
||||||
default: false
|
default: true
|
||||||
description: "Whether to upload to branch"
|
description: "Whether to upload to branch"
|
||||||
jobs:
|
jobs:
|
||||||
build-lkm:
|
build-lkm:
|
||||||
@@ -27,20 +27,20 @@ jobs:
|
|||||||
sub_level: 233
|
sub_level: 233
|
||||||
os_patch_level: 2025-02
|
os_patch_level: 2025-02
|
||||||
- version: "android13-5.10"
|
- version: "android13-5.10"
|
||||||
sub_level: 228
|
sub_level: 234
|
||||||
os_patch_level: 2025-01
|
os_patch_level: 2025-03
|
||||||
- version: "android13-5.15"
|
- version: "android13-5.15"
|
||||||
sub_level: 170
|
sub_level: 178
|
||||||
os_patch_level: 2025-01
|
os_patch_level: 2025-03
|
||||||
- version: "android14-5.15"
|
- version: "android14-5.15"
|
||||||
sub_level: 170
|
sub_level: 178
|
||||||
os_patch_level: 2025-01
|
os_patch_level: 2025-03
|
||||||
- version: "android14-6.1"
|
- version: "android14-6.1"
|
||||||
sub_level: 128
|
sub_level: 129
|
||||||
os_patch_level: 2025-03
|
os_patch_level: 2025-04
|
||||||
- version: "android15-6.6"
|
- version: "android15-6.6"
|
||||||
sub_level: 77
|
sub_level: 82
|
||||||
os_patch_level: 2025-03
|
os_patch_level: 2025-04
|
||||||
# uses: ./.github/workflows/gki-kernel-mock.yml when debugging
|
# uses: ./.github/workflows/gki-kernel-mock.yml when debugging
|
||||||
uses: ./.github/workflows/gki-kernel.yml
|
uses: ./.github/workflows/gki-kernel.yml
|
||||||
with:
|
with:
|
||||||
|
|||||||
22
.github/workflows/build-manager.yml
vendored
22
.github/workflows/build-manager.yml
vendored
@@ -8,7 +8,7 @@ on:
|
|||||||
- 'manager/**'
|
- 'manager/**'
|
||||||
- 'kernel/**'
|
- 'kernel/**'
|
||||||
- 'userspace/ksud/**'
|
- 'userspace/ksud/**'
|
||||||
- 'userspace/zakomksd/**'
|
- 'userspace/susfs/**'
|
||||||
- 'userspace/kpmmgr/**'
|
- 'userspace/kpmmgr/**'
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [ "main" ]
|
branches: [ "main" ]
|
||||||
@@ -29,7 +29,7 @@ on:
|
|||||||
upload_lkm:
|
upload_lkm:
|
||||||
required: true
|
required: true
|
||||||
type: boolean
|
type: boolean
|
||||||
default: false
|
default: true
|
||||||
description: "Whether to upload lkm"
|
description: "Whether to upload lkm"
|
||||||
jobs:
|
jobs:
|
||||||
check-build-lkm:
|
check-build-lkm:
|
||||||
@@ -64,7 +64,7 @@ jobs:
|
|||||||
cd ..
|
cd ..
|
||||||
rm -rf tmp
|
rm -rf tmp
|
||||||
fi
|
fi
|
||||||
if [ "${{ github.event_name }}" == "push" ] && [ "${{ github.ref }}" == 'refs/heads/susfs' ]; then
|
if [ "${{ github.event_name }}" == "push" ] && [ "${{ github.ref }}" == 'refs/heads/main' ]; then
|
||||||
need_upload=true
|
need_upload=true
|
||||||
elif [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
|
elif [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
|
||||||
need_upload="${{ inputs.upload_lkm }}"
|
need_upload="${{ inputs.upload_lkm }}"
|
||||||
@@ -83,7 +83,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
upload: ${{ needs.check-build-lkm.outputs.upload_lkm == 'true' }}
|
upload: ${{ needs.check-build-lkm.outputs.upload_lkm == 'true' }}
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
build-zakomksd:
|
build-susfs:
|
||||||
if: ${{ always() }}
|
if: ${{ always() }}
|
||||||
needs: [ check-build-lkm, build-lkm ]
|
needs: [ check-build-lkm, build-lkm ]
|
||||||
strategy:
|
strategy:
|
||||||
@@ -91,7 +91,7 @@ jobs:
|
|||||||
include:
|
include:
|
||||||
- target: aarch64-linux-android
|
- target: aarch64-linux-android
|
||||||
os: ubuntu-latest
|
os: ubuntu-latest
|
||||||
uses: ./.github/workflows/zakomksd.yml
|
uses: ./.github/workflows/susfs.yml
|
||||||
with:
|
with:
|
||||||
target: ${{ matrix.target }}
|
target: ${{ matrix.target }}
|
||||||
os: ${{ matrix.os }}
|
os: ${{ matrix.os }}
|
||||||
@@ -174,10 +174,10 @@ jobs:
|
|||||||
- name: Setup Android SDK
|
- name: Setup Android SDK
|
||||||
uses: android-actions/setup-android@v3
|
uses: android-actions/setup-android@v3
|
||||||
|
|
||||||
- name: Download arm64 zakomksd
|
- name: Download arm64 susfs
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: zakomksd-aarch64-linux-android
|
name: susfs-aarch64-linux-android
|
||||||
path: .
|
path: .
|
||||||
|
|
||||||
- name: Download arm64 kpmmgr
|
- name: Download arm64 kpmmgr
|
||||||
@@ -202,18 +202,18 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
mkdir -p app/src/main/jniLibs/arm64-v8a
|
mkdir -p app/src/main/jniLibs/arm64-v8a
|
||||||
mkdir -p app/src/main/jniLibs/x86_64
|
mkdir -p app/src/main/jniLibs/x86_64
|
||||||
cp -f ../aarch64-linux-android/release/zakomk ../manager/app/src/main/jniLibs/arm64-v8a/libzakomk.so
|
cp -f ../aarch64-linux-android/release/zakozako ../manager/app/src/main/jniLibs/arm64-v8a/libzakozako.so
|
||||||
cp -f ../x86_64-linux-android/release/zakomk ../manager/app/src/main/jniLibs/x86_64/libzakomk.so
|
cp -f ../x86_64-linux-android/release/zakozako ../manager/app/src/main/jniLibs/x86_64/libzakozako.so
|
||||||
|
|
||||||
- name: Copy kpmmgr to app jniLibs
|
- name: Copy kpmmgr to app jniLibs
|
||||||
run: |
|
run: |
|
||||||
mkdir -p app/src/main/jniLibs/arm64-v8a
|
mkdir -p app/src/main/jniLibs/arm64-v8a
|
||||||
cp -f ../arm64-v8a/kpmmgr ../manager/app/src/main/jniLibs/arm64-v8a/libkpmmgr.so
|
cp -f ../arm64-v8a/kpmmgr ../manager/app/src/main/jniLibs/arm64-v8a/libkpmmgr.so
|
||||||
|
|
||||||
- name: Copy zakomksd to app jniLibs
|
- name: Copy susfs to app jniLibs
|
||||||
run: |
|
run: |
|
||||||
mkdir -p app/src/main/jniLibs/arm64-v8a
|
mkdir -p app/src/main/jniLibs/arm64-v8a
|
||||||
cp -f ../arm64-v8a/zakomksd ../manager/app/src/main/jniLibs/arm64-v8a/libzakomksd.so
|
cp -f ../arm64-v8a/zakozakozako ../manager/app/src/main/jniLibs/arm64-v8a/libzakozakozako.so
|
||||||
|
|
||||||
- name: Build with Gradle
|
- name: Build with Gradle
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
3
.github/workflows/gki-kernel.yml
vendored
3
.github/workflows/gki-kernel.yml
vendored
@@ -198,6 +198,9 @@ jobs:
|
|||||||
- name: Make working directory clean to avoid dirty
|
- name: Make working directory clean to avoid dirty
|
||||||
working-directory: android-kernel
|
working-directory: android-kernel
|
||||||
run: |
|
run: |
|
||||||
|
if [ -e common/BUILD.bazel ]; then
|
||||||
|
sed -i '/^[[:space:]]*"protected_exports_list"[[:space:]]*:[[:space:]]*"android\/abi_gki_protected_exports_aarch64",$/d' common/BUILD.bazel
|
||||||
|
fi
|
||||||
rm common/android/abi_gki_protected_exports_* || echo "No protected exports!"
|
rm common/android/abi_gki_protected_exports_* || echo "No protected exports!"
|
||||||
git config --global user.email "bot@kernelsu.org"
|
git config --global user.email "bot@kernelsu.org"
|
||||||
git config --global user.name "KernelSUBot"
|
git config --global user.name "KernelSUBot"
|
||||||
|
|||||||
2
.github/workflows/ksud.yml
vendored
2
.github/workflows/ksud.yml
vendored
@@ -71,4 +71,4 @@ jobs:
|
|||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: ksud-${{ inputs.target }}
|
name: ksud-${{ inputs.target }}
|
||||||
path: userspace/ksud/target/**/release/zakomk*
|
path: userspace/ksud/target/**/release/zakozako*
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
name: Build zakomksd
|
name: Build susfs
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ "mian" ]
|
branches: [ "mian" ]
|
||||||
paths:
|
paths:
|
||||||
- '.github/workflows/zakomksd.yml'
|
- '.github/workflows/susfs.yml'
|
||||||
- 'userspace/zakomksd/**'
|
- 'userspace/susfs/**'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
workflow_call:
|
workflow_call:
|
||||||
inputs:
|
inputs:
|
||||||
@@ -19,7 +19,7 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-susfs:
|
build-susfs:
|
||||||
name: Build userspace zakomksd
|
name: Build userspace susfs
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@@ -28,13 +28,13 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Build zakomksd
|
- name: Build susfs
|
||||||
working-directory: ./userspace/zakomksd
|
working-directory: ./userspace/susfs
|
||||||
run: |
|
run: |
|
||||||
$ANDROID_NDK_HOME/ndk-build
|
$ANDROID_NDK_HOME/ndk-build
|
||||||
|
|
||||||
- name: Upload a Build Artifact
|
- name: Upload a Build Artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: zakomksd-aarch64-linux-android
|
name: susfs-aarch64-linux-android
|
||||||
path: ./userspace/zakomksd/libs
|
path: ./userspace/susfs/libs
|
||||||
@@ -1,101 +1,112 @@
|
|||||||
# SukiSU
|
# SukiSU Ultra
|
||||||
|
|
||||||
**Enlish** | [简体中文](README.md)
|
**English** | [简体中文](README.md) | [日本語](README-ja.md)
|
||||||
|
|
||||||
|
Android device root solution based on [KernelSU](https://github.com/tiann/KernelSU)
|
||||||
|
|
||||||
Android device root solution based on [KernelSU](https://github.com/KernelSU/KernelSU)
|
**Experimental! Use at your own risk!** This solution is based on [KernelSU](https://github.com/tiann/KernelSU) and is experimental!
|
||||||
|
|
||||||
**Experimental! Use at your own risk! **This solution is based on [KernelSU]() and is experimental!
|
|
||||||
|
|
||||||
|
> This is an unofficial fork. All rights are reserved to [@tiann](https://github.com/tiann)
|
||||||
>
|
>
|
||||||
> This is an unofficial fork, all rights reserved [@tiann](https://github.com/tiann)
|
> However, we will be a separately maintained branch of KSU in the future
|
||||||
>
|
|
||||||
|
|
||||||
- Fully adapted for non-GKI devices (susfs-dev and unsusfs-patched dev branches only)
|
- Fully adapted for non-GKI devices (susfs-dev and unsusfs-patched dev branches only)
|
||||||
|
|
||||||
## How to add
|
## How to add
|
||||||
|
|
||||||
Using the susfs-dev branch (integrated susfs with support for non-GKI devices)
|
Use the susfs-stable or susfs-dev branch (integrated susfs with support for non-GKI devices)
|
||||||
```
|
```
|
||||||
curl -LSs "https://raw.githubusercontent.com/ShirkNeko/SukiSU-Ultra/main/kernel/setup.sh" | bash -s susfs-dev
|
curl -LSs "https://raw.githubusercontent.com/ShirkNeko/SukiSU-Ultra/main/kernel/setup.sh" | bash -s susfs-dev
|
||||||
```
|
```
|
||||||
|
|
||||||
Use main branching (no longer with support for non-GKI devices)
|
Use the main branch
|
||||||
```
|
```
|
||||||
curl -LSs "https://raw.githubusercontent.com/ShirkNeko/SukiSU-Ultra/main/kernel/setup.sh" | bash -s main
|
curl -LSs "https://raw.githubusercontent.com/ShirkNeko/KernelSU/main/kernel/setup.sh" | bash -s main
|
||||||
```
|
```
|
||||||
|
|
||||||
## How to use integrated susfs
|
## How to use integrated susfs
|
||||||
|
|
||||||
Use the susfs-dev branch directly without any patching
|
1. Use the susfs-dev branch directly without any patching
|
||||||
|
|
||||||
|
## KPM support
|
||||||
|
|
||||||
|
- We have removed duplicate KSU functions based on KernelPatch and retained KPM support.
|
||||||
|
- We will introduce more APatch-compatible functions to ensure the integrity of KPM functionality.
|
||||||
|
|
||||||
|
Open source address: https://github.com/ShirkNeko/SukiSU_KernelPatch_patch
|
||||||
|
|
||||||
|
KPM template address: https://github.com/udochina/KPM-Build-Anywhere
|
||||||
|
|
||||||
## More links
|
## More links
|
||||||
|
|
||||||
Projects compiled based on Sukisu and susfs
|
Projects compiled based on Sukisu and susfs
|
||||||
- [GKI](https://github.com/ShirkNeko/GKI_KernelSU_SUSFS)
|
- [GKI](https://github.com/ShirkNeko/GKI_KernelSU_SUSFS)
|
||||||
- [OnePlus](https://github.com/ShirkNeko/Action_OnePlus_MKSU_SUSFS)
|
- [OnePlus](https://github.com/ShirkNeko/Action_OnePlus_MKSU_SUSFS)
|
||||||
|
|
||||||
## Hook method
|
## Hook method
|
||||||
- This method references the hook manual to (https://github.com/rsuntk/KernelSU)
|
- This method references the hook method from (https://github.com/rsuntk/KernelSU)
|
||||||
|
|
||||||
1. **KPROBES hook:**
|
1. **KPROBES hook:**
|
||||||
- This fork only supports GKI (5.10 - 6.x) kernels, all non-GKI kernels must use manual hooks.
|
- Also used for Loadable Kernel Module (LKM)
|
||||||
- For Loadable Kernel Modules (LKM)
|
- Default hook method on GKI kernels.
|
||||||
- Default hooking method for GKI kernels
|
- Need `CONFIG_KPROBES=y`
|
||||||
- Requires `CONFIG_KPROBES=y`. 2.
|
|
||||||
2. **Hooks manual:**
|
|
||||||
- For GKI (5.10 - 6.x) kernels, add `CONFIG_KSU_MANUAL_HOOK=y` to the kernel defconfig and make sure to protect KernelSU hooks by using `#ifdef CONFIG_KSU_MANUAL_HOOK` instead of `#ifdef CONFIG_KSU`.
|
|
||||||
- Standard KernelSU hooks: https://kernelsu.org/guide/how-to-integrate-for-non-gki.html#manually-modify-the-kernel-source
|
|
||||||
- backslashxx syscall hooks: https://github.com/backslashxx/KernelSU/issues/5
|
|
||||||
- Some non-GKI devices that manually integrate KPROBES do not require the manual VFS hook `new_hook.patch` patch
|
|
||||||
|
|
||||||
|
2. **Manual hook:**
|
||||||
|
- Standard KernelSU hook: https://kernelsu.org/guide/how-to-integrate-for-non-gki.html#manually-modify-the-kernel-source
|
||||||
|
- backslashxx's syscall manual hook: https://github.com/backslashxx/KernelSU/issues/5
|
||||||
|
- Default hook method on Non-GKI kernels.
|
||||||
|
- Need `CONFIG_KSU_MANUAL_HOOK=y`
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
[GKI]
|
|
||||||
1. such as millet redmi samsung and other devices (does not include the magic kernel manufacturers such as: meizu, a plus real me oppo)
|
|
||||||
2. find more links in the GKI build project to find the device kernel version directly download with TWRP or kernel flashing tool to brush into the zip with AnyKernel3 suffix can be
|
|
||||||
3. General without the suffix of the .zip compressed package is universal, gz suffix for the special TianGui models, lz4 suffix for Google models, general brush without the suffix can be!
|
|
||||||
|
|
||||||
[OnePlus]
|
### GKI
|
||||||
1. Find the Yiga project in the More link and fill in your own, then build it with cloud compilation, and finally brush in the zip with AnyKernel3 suffix.
|
|
||||||
Note: You only need to fill in the first two kernel versions, such as 5.10, 5.15, 6.1, 6.6.
|
Please follow this guide.
|
||||||
- Please search for the processor codename by yourself, usually it is all English without numbers.
|
|
||||||
- Branching and configuration files, please fill in the kernel open source address.
|
https://kernelsu.org/guide/installation.html
|
||||||
|
|
||||||
|
|
||||||
|
### OnePlus
|
||||||
|
|
||||||
|
1. Use the link mentioned in the 'More Links' section to create a customized build with your device information, and then flash the zip file with the AnyKernel3 suffix.
|
||||||
|
|
||||||
|
> [!Note]
|
||||||
|
> - You only need to fill in the first two parts of kernel versions, such as 5.10, 5.15, 6.1, or 6.6.
|
||||||
|
> - Please search for the processor codename by yourself, usually it is all English without numbers.
|
||||||
|
> - You can find the branch and configuration files from the OnePlus open-source kernel repository.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
1. Kernel-based `su` and root access management.
|
1. Kernel-based `su` and root access management.
|
||||||
2. Not based on [OverlayFS](https://en.wikipedia.org/wiki/OverlayFS) module system. 3.
|
2. Not based on [OverlayFS](https://en.wikipedia.org/wiki/OverlayFS) module system, but based on [Magic Mount](https://github.com/5ec1cff/KernelSU) from 5ec1cff
|
||||||
3. [Application Profiles](https://kernelsu.org/guide/app-profile.html): Lock root privileges in a cage. 4.
|
3. [App Profile](https://kernelsu.org/guide/app-profile.html): Lock root privileges in a cage.
|
||||||
4. Bringing back non-GKI/GKI 1.0 support
|
4. Bringing back non-GKI/GKI 1.0 support
|
||||||
5. More customization
|
5. More customization
|
||||||
|
6. Support for KPM kernel modules
|
||||||
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
- The file in the “kernel” directory is [GPL-2.0-only](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html).
|
- The file in the “kernel” directory is under [GPL-2.0-only](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html) license.
|
||||||
- All other parts except the “kernel” directory are [GPL-3.0 or later](https://www.gnu.org/licenses/gpl-3.0.html).
|
- All other parts except the “kernel” directory are under [GPL-3.0 or later](https://www.gnu.org/licenses/gpl-3.0.html) license.
|
||||||
|
|
||||||
## Sponsorship list
|
## Sponsorship list
|
||||||
|
|
||||||
- [Ktouls](https://github.com/Ktouls) Thanks so much for bringing me support
|
- [Ktouls](https://github.com/Ktouls) Thanks so much for bringing me support
|
||||||
- [zaoqi123](https://github.com/zaoqi123) It's not a bad idea to buy me a milk tea
|
- [zaoqi123](https://github.com/zaoqi123) It's not a bad idea to buy me a milk tea
|
||||||
- [wswzgdg](https://github.com/wswzgdg) Many thanks for supporting this project
|
- [wswzgdg](https://github.com/wswzgdg) Many thanks for supporting this project
|
||||||
|
- [yspbwx2010](https://github.com/yspbwx2010) Many thanks
|
||||||
|
- [DARKWWEE](https://github.com/DARKWWEE) Thanks for the 100 USDT Lao
|
||||||
|
|
||||||
|
If the above list does not have your name, I will update it as soon as possible, and thanks again for your support!
|
||||||
|
|
||||||
|
|
||||||
How the above list does not have your name, I will keep you updated, thanks again for your support!
|
|
||||||
|
|
||||||
## Contributions
|
## Contributions
|
||||||
|
|
||||||
- [KernelSU](https://github.com/tiann/KernelSU): original project
|
- [KernelSU](https://github.com/tiann/KernelSU): original project
|
||||||
- [MKSU](https://github.com/5ec1cff/KernelSU): Used project
|
- [MKSU](https://github.com/5ec1cff/KernelSU): Used project
|
||||||
- [RKSU](https://github.com/rsuntk/KernelsU):Re-support of non-GKI devices using the kernel of this project
|
- [RKSU](https://github.com/rsuntk/KernelsU): Reintroduced the support of non-GKI devices using the kernel of this project
|
||||||
- [susfs](https://gitlab.com/simonpunk/susfs4ksu):Used susfs file system
|
- [susfs](https://gitlab.com/simonpunk/susfs4ksu):Used susfs file system
|
||||||
- [KernelSU](https://git.zx2c4.com/kernel-assisted-superuser/about/): KernelSU conceptualization
|
- [KernelSU](https://git.zx2c4.com/kernel-assisted-superuser/about/): KernelSU conceptualization
|
||||||
- [Magisk](https://github.com/topjohnwu/Magisk): Powerful root utility
|
- [Magisk](https://github.com/topjohnwu/Magisk): Powerful root utility
|
||||||
- [genuine](https://github.com/brevent/genuine/): APK v2 Signature Verification
|
- [genuine](https://github.com/brevent/genuine/): APK v2 Signature Verification
|
||||||
- [Diamorphine](https://github.com/m0nad/Diamorphine): Some rootkit skills.
|
- [Diamorphine](https://github.com/m0nad/Diamorphine): Some rootkit utilities.
|
||||||
|
- [KernelPatch](https://github.com/bmax121/KernelPatch): KernelPatch is a key part of the APatch implementation of the kernel module
|
||||||
|
|||||||
113
docs/README-ja.md
Normal file
113
docs/README-ja.md
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
# SukiSU Ultra
|
||||||
|
|
||||||
|
**日本語** | [简体中文](README.md) | [English](README-en.md)
|
||||||
|
|
||||||
|
[KernelSU](https://github.com/tiann/KernelSU) をベースとした Android デバイスの root ソリューション
|
||||||
|
|
||||||
|
**試験中なビルドです!自己責任で使用してください!**<br>
|
||||||
|
このソリューションは [KernelSU](https://github.com/tiann/KernelSU) に基づいていますが、試験中なビルドです。
|
||||||
|
|
||||||
|
> これは非公式なフォークです。すべての権利は [@tiann](https://github.com/tiann) に帰属します。
|
||||||
|
>
|
||||||
|
> ただし、将来的には KSU とは別に管理されるブランチとなる予定です。
|
||||||
|
|
||||||
|
- GKI 非対応なデバイスに完全に適応 (susfs-dev と unsusfs-patched dev ブランチのみ)
|
||||||
|
|
||||||
|
## 追加方法
|
||||||
|
|
||||||
|
susfs-stable または susfs-dev ブランチ (GKI 非対応デバイスに対応する統合された susfs) 使用してください。
|
||||||
|
```
|
||||||
|
curl -LSs "https://raw.githubusercontent.com/ShirkNeko/SukiSU-Ultra/main/kernel/setup.sh" | bash -s susfs-dev
|
||||||
|
```
|
||||||
|
|
||||||
|
メインブランチを使用する場合
|
||||||
|
```
|
||||||
|
curl -LSs "https://raw.githubusercontent.com/ShirkNeko/KernelSU/main/kernel/setup.sh" | bash -s main
|
||||||
|
```
|
||||||
|
## 統合された susfs の使い方
|
||||||
|
|
||||||
|
1. パッチを当てずに susfs-dev ブランチを直接使用してください。
|
||||||
|
|
||||||
|
## KPM に対応
|
||||||
|
|
||||||
|
- KernelPatch に基づいて重複した KSU の機能を削除、KPM の対応を維持させています。
|
||||||
|
- KPM 機能の整合性を確保するために、APatch の互換機能を更に向上させる予定です。
|
||||||
|
|
||||||
|
オープンソースアドレス: https://github.com/ShirkNeko/SukiSU_KernelPatch_patch
|
||||||
|
|
||||||
|
KPM テンプレートのアドレス: https://github.com/udochina/KPM-Build-Anywhere
|
||||||
|
|
||||||
|
## その他のリンク
|
||||||
|
|
||||||
|
SukiSU と susfs をベースにコンパイルされたプロジェクトです。
|
||||||
|
|
||||||
|
- [GKI](https://github.com/ShirkNeko/GKI_KernelSU_SUSFS)
|
||||||
|
- [OnePlus](https://github.com/ShirkNeko/Action_OnePlus_MKSU_SUSFS)
|
||||||
|
|
||||||
|
## フックの方式
|
||||||
|
|
||||||
|
- この方式は (https://github.com/rsuntk/KernelSU) のフック方式を参照してください。
|
||||||
|
|
||||||
|
1. **KPROBES フック:**
|
||||||
|
- 読み込み可能なカーネルモジュールの場合 (LKM)
|
||||||
|
- GKI カーネルのデフォルトとなるフック方式
|
||||||
|
- `CONFIG_KPROBES=y` が必要です
|
||||||
|
|
||||||
|
2. **手動でフック:**
|
||||||
|
- 標準の KernelSU フック: https://kernelsu.org/guide/how-to-integrate-for-non-gki.html#manually-modify-the-kernel-source
|
||||||
|
- backslashxx syscall フック: https://github.com/backslashxx/KernelSU/issues/5
|
||||||
|
- 非 GKI カーネル用のデフォルトフッキングメソッド
|
||||||
|
- `CONFIG_KSU_MANUAL_HOOK=y` が必要です
|
||||||
|
|
||||||
|
## 使い方
|
||||||
|
|
||||||
|
### GKI
|
||||||
|
|
||||||
|
このガイドに従ってください。
|
||||||
|
|
||||||
|
https://kernelsu.org/ja_JP/guide/installation.html
|
||||||
|
|
||||||
|
### OnePlus
|
||||||
|
|
||||||
|
1. `その他のリンク`の項目に記載されているリンクを開き、デバイス情報を使用してカスタマイズされたカーネルをビルドし、AnyKernel3 の接頭辞を持つ .zip ファイルをフラッシュします。
|
||||||
|
|
||||||
|
> [!Note]
|
||||||
|
> - 5.10、5.15、6.1、6.6 などのカーネルバージョンの最初の 2 文字のみを入力する必要があります。
|
||||||
|
> - SoC のコードネームは自分で検索してください。通常は、数字がなく英語表記のみです。
|
||||||
|
> - ブランチと構成ファイルは、OnePlus オープンソースカーネルリポジトリから見つけることができます。
|
||||||
|
|
||||||
|
## 機能
|
||||||
|
|
||||||
|
1. カーネルベースな `su` および root アクセスの管理。
|
||||||
|
2. [OverlayFS](https://en.wikipedia.org/wiki/OverlayFS) モジュールシステムではなく、 5ec1cff 氏の [Magic Mount](https://github.com/5ec1cff/KernelSU) に基づいています。
|
||||||
|
3. [アプリプロファイル](https://kernelsu.org/guide/app-profile.html): root 権限をケージ内にロックします。
|
||||||
|
4. 非 GKI / GKI 1.0 の対応を復活
|
||||||
|
5. その他のカスタマイズ
|
||||||
|
6. KPM カーネルモジュールに対応
|
||||||
|
|
||||||
|
## ライセンス
|
||||||
|
|
||||||
|
- “kernel” ディレクトリ内のファイルは [GPL-2.0](https://www.gnu.org/licenses/old-licenses/gpl-2.0.ja.html) のみライセンス下にあります。
|
||||||
|
- “kernel” ディレクトリを除くその他すべての部分は [GPL-3.0 またはそれ以降](https://www.gnu.org/licenses/gpl-3.0.html) のライセンス下にあります。
|
||||||
|
|
||||||
|
## スポンサーシップの一覧
|
||||||
|
|
||||||
|
- [Ktouls](https://github.com/Ktouls) 応援をしてくれたことに感謝。
|
||||||
|
- [zaoqi123](https://github.com/zaoqi123) ミルクティーを買ってあげるのも良い考えですね。
|
||||||
|
- [wswzgdg](https://github.com/wswzgdg) このプロジェクトを支援していただき、ありがとうございます。
|
||||||
|
- [yspbwx2010](https://github.com/yspbwx2010) どうもありがとう。
|
||||||
|
- [DARKWWEE](https://github.com/DARKWWEE) ラオウ100USDTありがとう!
|
||||||
|
|
||||||
|
上記の一覧にあなたの名前がない場合は、できるだけ早急に更新しますので再度ご支援をお願いします。
|
||||||
|
|
||||||
|
## 貢献者
|
||||||
|
|
||||||
|
- [KernelSU](https://github.com/tiann/KernelSU): オリジナルのプロジェクトです。
|
||||||
|
- [MKSU](https://github.com/5ec1cff/KernelSU): 使用しているプロジェクトです。
|
||||||
|
- [RKSU](https://github.com/rsuntk/KernelsU): このプロジェクトのカーネルを使用して非 GKI デバイスのサポートを追加しています。
|
||||||
|
- [susfs](https://gitlab.com/simonpunk/susfs4ksu):使用している susfs ファイルシステムです。
|
||||||
|
- [KernelSU](https://git.zx2c4.com/kernel-assisted-superuser/about/): KernelSU について。
|
||||||
|
- [Magisk](https://github.com/topjohnwu/Magisk): パワフルな root ユーティリティです。
|
||||||
|
- [genuine](https://github.com/brevent/genuine/): APK v2 署名認証で使用しています。
|
||||||
|
- [Diamorphine](https://github.com/m0nad/Diamorphine): いくつかの rootkit ユーティリティを使用しています。
|
||||||
|
- [KernelPatch](https://github.com/bmax121/KernelPatch): KernelPatch はカーネルモジュールの APatch 実装での重要な部分となります。
|
||||||
@@ -1,18 +1,17 @@
|
|||||||
# SukiSU
|
# SukiSU Ultra
|
||||||
|
|
||||||
**简体中文** | [English](README-en.md)
|
**简体中文** | [English](README-en.md) | [日本語](README-ja.md)
|
||||||
|
|
||||||
基于 [KernelSU](https://github.com/tiann/KernelSU) 的安卓设备 root 解决方案
|
基于 [KernelSU](https://github.com/tiann/KernelSU) 的安卓设备 root 解决方案
|
||||||
|
|
||||||
**实验性! 使用风险自负!**
|
**实验性! 使用风险自负!**
|
||||||
|
|
||||||
|
|
||||||
>
|
|
||||||
> 这是非官方分支,保留所有权利 [@tiann](https://github.com/tiann)
|
> 这是非官方分支,保留所有权利 [@tiann](https://github.com/tiann)
|
||||||
>
|
>
|
||||||
|
> 但是,我们将会在未来成为一个单独维护的 KSU 分支
|
||||||
|
|
||||||
## 如何添加
|
## 如何添加
|
||||||
|
|
||||||
在内核源码的根目录下执行以下命令:
|
在内核源码的根目录下执行以下命令:
|
||||||
|
|
||||||
使用 susfs-dev 分支(已集成 susfs,带非 GKI 设备的支持)
|
使用 susfs-dev 分支(已集成 susfs,带非 GKI 设备的支持)
|
||||||
@@ -20,61 +19,74 @@
|
|||||||
curl -LSs "https://raw.githubusercontent.com/ShirkNeko/SukiSU-Ultra/main/kernel/setup.sh" | bash -s susfs-dev
|
curl -LSs "https://raw.githubusercontent.com/ShirkNeko/SukiSU-Ultra/main/kernel/setup.sh" | bash -s susfs-dev
|
||||||
```
|
```
|
||||||
|
|
||||||
使用 main 分支(不再带非GKI设备的支持)
|
使用 main 分支
|
||||||
```
|
```
|
||||||
curl -LSs "https://raw.githubusercontent.com/ShirkNeko/SukiSU-Ultra/main/kernel/setup.sh" | bash -s main
|
curl -LSs "https://raw.githubusercontent.com/ShirkNeko/SukiSU-Ultra/main/kernel/setup.sh" | bash -s main
|
||||||
```
|
```
|
||||||
|
|
||||||
## 如何集成 susfs
|
## 如何集成 susfs
|
||||||
|
|
||||||
1. 直接使用 susfs-dev 分支,不需要再集成 susfs
|
1. 直接使用 susfs-stable 或者 susfs-dev 分支,不需要再集成 susfs
|
||||||
|
|
||||||
|
|
||||||
## 钩子方法
|
## 钩子方法
|
||||||
|
|
||||||
- 此部分引用自 [rsuntk 的钩子方法](https://github.com/rsuntk/KernelSU)
|
- 此部分引用自 [rsuntk 的钩子方法](https://github.com/rsuntk/KernelSU)
|
||||||
|
|
||||||
1. **KPROBES 钩子:**
|
1. **KPROBES 钩子:**
|
||||||
- 此方法仅支持 GKI 2.0(5.10 - 6.x)内核,所有非 GKI 2.0 内核都必须使用手动钩子
|
|
||||||
- 用于可加载内核模块 (LKM)
|
- 用于可加载内核模块 (LKM)
|
||||||
- GKI 2.0 内核的默认钩子方法
|
- GKI 2.0 内核的默认钩子方法
|
||||||
- 需要 `CONFIG_KPROBES=y`
|
- 需要 `CONFIG_KPROBES=y`
|
||||||
|
|
||||||
2. **手动钩子:**
|
2. **手动钩子:**
|
||||||
- 对于 GKI 2.0(5.10 - 6.x)内核,需要在对应设备的 defconfig 文件中添加 `CONFIG_KSU_MANUAL_HOOK=y` 并确保使用 `#ifdef CONFIG_KSU_MANUAL_HOOK` 而不是 `#ifdef CONFIG_KSU` 来保护 KernelSU 钩子
|
|
||||||
- 标准的 KernelSU 钩子:https://kernelsu.org/guide/how-to-integrate-for-non-gki.html#manually-modify-the-kernel-source
|
- 标准的 KernelSU 钩子:https://kernelsu.org/guide/how-to-integrate-for-non-gki.html#manually-modify-the-kernel-source
|
||||||
- backslashxx 的 syscall 手动钩子:https://github.com/backslashxx/KernelSU/issues/5
|
- backslashxx 的 syscall 手动钩子:https://github.com/backslashxx/KernelSU/issues/5
|
||||||
- 部分手动集成KPROBES的非 GKI 2.0 设备不需要手动 VFS 钩子 `new_hook.patch` 补丁
|
- 非 GKI 内核的默认挂钩方法
|
||||||
|
- 需要 `CONFIG_KSU_MANUAL_HOOK=y`
|
||||||
|
|
||||||
|
## KPM 支持
|
||||||
|
|
||||||
|
- 我们基于 KernelPatch 去掉了和 KSU 重复的功能,仅保留了 KPM 支持
|
||||||
|
- 我们将会引入更多的兼容 APatch 的函数来确保 KPM 功能的完整性
|
||||||
|
|
||||||
|
开源地址: https://github.com/ShirkNeko/SukiSU_KernelPatch_patch
|
||||||
|
|
||||||
|
KPM 模板地址: https://github.com/udochina/KPM-Build-Anywhere
|
||||||
|
|
||||||
## 更多链接
|
## 更多链接
|
||||||
|
|
||||||
基于 SukiSU 和 susfs 编译的项目
|
基于 SukiSU 和 susfs 编译的项目
|
||||||
- [GKI](https://github.com/ShirkNeko/GKI_KernelSU_SUSFS)
|
- [GKI](https://github.com/ShirkNeko/GKI_KernelSU_SUSFS)
|
||||||
- [一加](https://github.com/ShirkNeko/Action_OnePlus_MKSU_SUSFS)
|
- [一加](https://github.com/ShirkNeko/Action_OnePlus_MKSU_SUSFS)
|
||||||
|
|
||||||
|
|
||||||
## 使用方法
|
## 使用方法
|
||||||
|
|
||||||
### GKI
|
### 普适的 GKI
|
||||||
1. 适用于如小米红米三星等的 GKI 2.0 的设备(不包含魔改内核的厂商如魅族、一加、真我和 oppo)
|
|
||||||
2. 找到更多链接里的 GKI 构建的项目找到设备内核版本直接下载用TWRP或者内核刷写工具刷入带 AnyKernel3 后缀的压缩包即可
|
请**全部**参考 https://kernelsu.org/zh_CN/guide/installation.html
|
||||||
3. 一般不带后缀的 .zip 压缩包是通用,gz 后缀的为天玑机型专用,lz4 后缀的为谷歌系机型专用,一般刷不带后缀的即可
|
|
||||||
|
> [!Note]
|
||||||
|
> 1. 适用于如小米、红米、三星等的 GKI 2.0 的设备 (不包含魔改内核的厂商如魅族、一加、真我和 oppo)
|
||||||
|
> 2. 找到[更多链接](#%E6%9B%B4%E5%A4%9A%E9%93%BE%E6%8E%A5)里的 GKI 构建的项目。找到设备内核版本。然后下载下来,用TWRP或者内核刷写工具刷入带 AnyKernel3 后缀的压缩包即可
|
||||||
|
> 3. 一般不带后缀的 .zip 压缩包是未压缩的,gz 后缀的为天玑机型所使用的压缩方式
|
||||||
|
|
||||||
|
|
||||||
### 一加
|
### 一加
|
||||||
|
|
||||||
1.找到更多链接里的一加项目进行自行填写,然后云编译构建,最后刷入带 AnyKernel3 后缀的压缩包即可
|
1.找到更多链接里的一加项目进行自行填写,然后云编译构建,最后刷入带 AnyKernel3 后缀的压缩包即可
|
||||||
|
|
||||||
注意事项:
|
> [!Note]
|
||||||
- 内核版本只需要填写前两位即可,如 5.10,5.15,6.1,6.6
|
> - 内核版本只需要填写前两位即可,如 5.10,5.15,6.1,6.6
|
||||||
- 处理器代号请自行搜索,一般为全英文不带数字的代号
|
> - 处理器代号请自行搜索,一般为全英文不带数字的代号
|
||||||
- 分支和配置文件请自行到一加内核开源地址进行填写
|
> - 分支和配置文件请自行到一加内核开源地址进行填写
|
||||||
|
|
||||||
|
|
||||||
## 特点
|
## 特点
|
||||||
|
|
||||||
1. 基于内核的 `su` 和 root 访问管理
|
1. 基于内核的 `su` 和 root 访问管理
|
||||||
2. 基于 5ec1cff 的 [Magic Mount](https://github.com/5ec1cff/KernelSU) 的模块系统
|
2. 基于 5ec1cff 的 [Magic Mount](https://github.com/5ec1cff/KernelSU) 的模块系统
|
||||||
3. [App Profile](https://kernelsu.org/guide/app-profile.html):将 root 权限锁在笼子里
|
3. [App Profile](https://kernelsu.org/guide/app-profile.html):将 root 权限锁在笼子里
|
||||||
4. 恢复对非 GKI 2.0 内核的支持(仅限susfs-dev和未进行susfs补丁的dev分支)
|
4. 恢复对非 GKI 2.0 内核的支持
|
||||||
5. 更多自定义功能
|
5. 更多自定义功能
|
||||||
|
6. 对 KPM 内核模块的支持
|
||||||
|
|
||||||
## 许可证
|
## 许可证
|
||||||
|
|
||||||
@@ -82,14 +94,14 @@ curl -LSs "https://raw.githubusercontent.com/ShirkNeko/SukiSU-Ultra/main/kernel/
|
|||||||
- 除 `kernel` 目录外,所有其他部分均为 [GPL-3.0 或更高版本](https://www.gnu.org/licenses/gpl-3.0.html)。
|
- 除 `kernel` 目录外,所有其他部分均为 [GPL-3.0 或更高版本](https://www.gnu.org/licenses/gpl-3.0.html)。
|
||||||
|
|
||||||
## 赞助名单
|
## 赞助名单
|
||||||
|
|
||||||
- [Ktouls](https://github.com/Ktouls) 非常感谢你给我带来的支持
|
- [Ktouls](https://github.com/Ktouls) 非常感谢你给我带来的支持
|
||||||
- [zaoqi123](https://github.com/zaoqi123) 请我喝奶茶也不错
|
- [zaoqi123](https://github.com/zaoqi123) 请我喝奶茶也不错
|
||||||
- [wswzgdg](https://github.com/wswzgdg) 非常感谢对此项目的支持
|
- [wswzgdg](https://github.com/wswzgdg) 非常感谢对此项目的支持
|
||||||
|
- [yspbwx2010](https://github.com/yspbwx2010) 非常感谢
|
||||||
|
- [DARKWWEE](https://github.com/DARKWWEE) 感谢老哥的 100 USDT
|
||||||
|
|
||||||
|
如果以上名单没有你的名称,我会及时更新,再次感谢大家的支持
|
||||||
|
|
||||||
|
|
||||||
如何以上名单没有你的名称,我会及时更新,再次感谢大家的支持
|
|
||||||
|
|
||||||
## 贡献
|
## 贡献
|
||||||
|
|
||||||
@@ -101,3 +113,4 @@ curl -LSs "https://raw.githubusercontent.com/ShirkNeko/SukiSU-Ultra/main/kernel/
|
|||||||
- [Magisk](https://github.com/topjohnwu/Magisk):强大的 root 工具
|
- [Magisk](https://github.com/topjohnwu/Magisk):强大的 root 工具
|
||||||
- [genuine](https://github.com/brevent/genuine/):APK v2 签名验证
|
- [genuine](https://github.com/brevent/genuine/):APK v2 签名验证
|
||||||
- [Diamorphine](https://github.com/m0nad/Diamorphine):一些 rootkit 技能
|
- [Diamorphine](https://github.com/m0nad/Diamorphine):一些 rootkit 技能
|
||||||
|
- [KernelPatch](https://github.com/bmax121/KernelPatch): KernelPatch 是 APatch 实现内核模块的关键部分
|
||||||
@@ -24,11 +24,12 @@ config KSU_HOOK
|
|||||||
override the kernel version check and enable the hook functionality.
|
override the kernel version check and enable the hook functionality.
|
||||||
|
|
||||||
config KPM
|
config KPM
|
||||||
bool "Enable KernelSU KPM"
|
bool "Enable SukiSU KPM"
|
||||||
default n
|
default n
|
||||||
help
|
help
|
||||||
This option enables the KernelSU KPM feature. If enabled, it will
|
Enabling this option will activate the KPM feature of SukiSU.
|
||||||
override the kernel version check and enable the hook functionality.
|
This option is suitable for scenarios where you need to force KPM to be enabled.
|
||||||
|
but it may affect system stability.
|
||||||
|
|
||||||
|
|
||||||
endmenu
|
endmenu
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ obj-$(CONFIG_KPM) += kpm/
|
|||||||
# .git is a text file while the module is imported by 'git submodule add'.
|
# .git is a text file while the module is imported by 'git submodule add'.
|
||||||
ifeq ($(shell test -e $(srctree)/$(src)/../.git; echo $$?),0)
|
ifeq ($(shell test -e $(srctree)/$(src)/../.git; echo $$?),0)
|
||||||
$(shell cd $(srctree)/$(src); /usr/bin/env PATH="$$PATH":/usr/bin:/usr/local/bin [ -f ../.git/shallow ] && git fetch --unshallow)
|
$(shell cd $(srctree)/$(src); /usr/bin/env PATH="$$PATH":/usr/bin:/usr/local/bin [ -f ../.git/shallow ] && git fetch --unshallow)
|
||||||
KSU_GIT_VERSION := $(shell cd $(srctree)/$(src); /usr/bin/env PATH="$$PATH":/usr/bin:/usr/local/bin git rev-list --count HEAD)
|
KSU_GIT_VERSION := $(shell cd $(srctree)/$(src); /usr/bin/env PATH="$$PATH":/usr/bin:/usr/local/bin git rev-list --count main)
|
||||||
# ksu_version: major * 10000 + git version + 606 for historical reasons
|
# ksu_version: major * 10000 + git version + 606 for historical reasons
|
||||||
$(eval KSU_VERSION=$(shell expr 10000 + $(KSU_GIT_VERSION) + 606))
|
$(eval KSU_VERSION=$(shell expr 10000 + $(KSU_GIT_VERSION) + 606))
|
||||||
$(info -- KernelSU version: $(KSU_VERSION))
|
$(info -- KernelSU version: $(KSU_VERSION))
|
||||||
@@ -42,14 +42,32 @@ endif
|
|||||||
|
|
||||||
ifdef KSU_MANAGER_PACKAGE
|
ifdef KSU_MANAGER_PACKAGE
|
||||||
ccflags-y += -DKSU_MANAGER_PACKAGE=\"$(KSU_MANAGER_PACKAGE)\"
|
ccflags-y += -DKSU_MANAGER_PACKAGE=\"$(KSU_MANAGER_PACKAGE)\"
|
||||||
$(info -- KernelSU Manager package name: $(KSU_MANAGER_PACKAGE))
|
$(info -- SukiSU Manager package name: $(KSU_MANAGER_PACKAGE))
|
||||||
endif
|
endif
|
||||||
|
|
||||||
$(info -- KernelSU Manager signature size: $(KSU_EXPECTED_SIZE))
|
$(info -- SukiSU Manager signature size: $(KSU_EXPECTED_SIZE))
|
||||||
$(info -- KernelSU Manager signature hash: $(KSU_EXPECTED_HASH))
|
$(info -- SukiSU Manager signature hash: $(KSU_EXPECTED_HASH))
|
||||||
$(info -- Supported Unofficial Manager: ShirkNeko (GKI) (Non-GKI))
|
$(info -- Supported Unofficial Manager: 5ec1cff (GKI) ShirkNeko udochina (GKI and KPM))
|
||||||
KERNEL_VERSION := $(VERSION).$(PATCHLEVEL)
|
KERNEL_VERSION := $(VERSION).$(PATCHLEVEL)
|
||||||
|
KERNEL_TYPE := Non-GKI
|
||||||
|
# Check for GKI 2.0 (5.10+ or 6.x+)
|
||||||
|
ifneq ($(shell test \( $(VERSION) -ge 5 -a $(PATCHLEVEL) -ge 10 \) -o $(VERSION) -ge 6; echo $$?),0)
|
||||||
|
# Check for GKI 1.0 (5.4)
|
||||||
|
ifeq ($(shell test $(VERSION)-$(PATCHLEVEL) = 5-4; echo $$?),0)
|
||||||
|
KERNEL_TYPE := GKI 1.0
|
||||||
|
endif
|
||||||
|
else
|
||||||
|
KERNEL_TYPE := GKI 2.0
|
||||||
|
endif
|
||||||
$(info -- KERNEL_VERSION: $(KERNEL_VERSION))
|
$(info -- KERNEL_VERSION: $(KERNEL_VERSION))
|
||||||
|
$(info -- KERNEL_TYPE: $(KERNEL_TYPE))
|
||||||
|
|
||||||
|
$(info -- KERNEL_VERSION: $(KERNEL_VERSION))
|
||||||
|
ifeq ($(CONFIG_KPM),y)
|
||||||
|
$(info -- KPM is enabled)
|
||||||
|
else
|
||||||
|
$(info -- KPM is disabled)
|
||||||
|
endif
|
||||||
|
|
||||||
|
|
||||||
ccflags-y += -DEXPECTED_SIZE=$(KSU_EXPECTED_SIZE)
|
ccflags-y += -DEXPECTED_SIZE=$(KSU_EXPECTED_SIZE)
|
||||||
|
|||||||
@@ -419,10 +419,7 @@ int ksu_handle_prctl(int option, unsigned long arg2, unsigned long arg3,
|
|||||||
|
|
||||||
pr_info("KPM: calling before arg2=%d\n", (int) arg2);
|
pr_info("KPM: calling before arg2=%d\n", (int) arg2);
|
||||||
|
|
||||||
res = sukisu_handle_kpm(arg2, arg3, arg4);
|
res = sukisu_handle_kpm(arg2, arg3, arg4, arg5);
|
||||||
copy_to_user(result, &res, sizeof(res));
|
|
||||||
|
|
||||||
pr_info("KPM: calling before arg2=%d res=%d\n", (int) arg2, (int) res);
|
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,2 +1,6 @@
|
|||||||
obj-y += kpm.o
|
obj-y += kpm.o
|
||||||
obj-y += compact.o
|
obj-y += compact.o
|
||||||
|
obj-y += super_access.o
|
||||||
|
|
||||||
|
ccflags-y += -Wno-implicit-function-declaration -Wno-strict-prototypes -Wno-int-conversion -Wno-gcc-compat
|
||||||
|
ccflags-y += -Wno-declaration-after-statement -Wno-unused-function
|
||||||
@@ -26,37 +26,56 @@
|
|||||||
#include <linux/slab.h>
|
#include <linux/slab.h>
|
||||||
#include "kpm.h"
|
#include "kpm.h"
|
||||||
#include "compact.h"
|
#include "compact.h"
|
||||||
|
#include "../allowlist.h"
|
||||||
|
#include "../manager.h"
|
||||||
|
|
||||||
unsigned long sukisu_compact_find_symbol(const char* name);
|
unsigned long sukisu_compact_find_symbol(const char* name);
|
||||||
|
|
||||||
// ======================================================================
|
// ======================================================================
|
||||||
|
// 兼容函数 for KPM
|
||||||
|
|
||||||
const char* kpver = "0.10";
|
static
|
||||||
|
int sukisu_is_su_allow_uid(uid_t uid) {
|
||||||
|
return ksu_is_allow_uid(uid) ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static
|
||||||
|
int sukisu_get_ap_mod_exclude(uid_t uid) {
|
||||||
|
// Not supported
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static
|
||||||
|
int sukisu_is_uid_should_umount(uid_t uid) {
|
||||||
|
return ksu_uid_should_umount(uid) ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static
|
||||||
|
int sukisu_is_current_uid_manager() {
|
||||||
|
return is_manager();
|
||||||
|
}
|
||||||
|
|
||||||
|
static
|
||||||
|
uid_t sukisu_get_manager_uid() {
|
||||||
|
return ksu_manager_uid;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ======================================================================
|
||||||
|
|
||||||
struct CompactAddressSymbol {
|
struct CompactAddressSymbol {
|
||||||
const char* symbol_name;
|
const char* symbol_name;
|
||||||
void* addr;
|
void* addr;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct CompactAliasSymbol {
|
static struct CompactAddressSymbol address_symbol [] = {
|
||||||
const char* symbol_name;
|
|
||||||
const char* compact_symbol_name;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct CompactAddressSymbol address_symbol [] = {
|
|
||||||
{ "kallsyms_lookup_name", &kallsyms_lookup_name },
|
{ "kallsyms_lookup_name", &kallsyms_lookup_name },
|
||||||
{ "compact_find_symbol", &sukisu_compact_find_symbol },
|
{ "compact_find_symbol", &sukisu_compact_find_symbol },
|
||||||
{ "compat_copy_to_user", ©_to_user },
|
{ "is_run_in_sukisu_ultra", (void*)1 },
|
||||||
{ "compat_strncpy_from_user", &strncpy_from_user },
|
{ "is_su_allow_uid", &sukisu_is_su_allow_uid },
|
||||||
{ "kpver", &kpver },
|
{ "get_ap_mod_exclude", &sukisu_get_ap_mod_exclude },
|
||||||
{ "is_run_in_sukisu_ultra", (void*)1 }
|
{ "is_uid_should_umount", &sukisu_is_uid_should_umount },
|
||||||
};
|
{ "is_current_uid_manager", &sukisu_is_current_uid_manager },
|
||||||
|
{ "get_manager_uid", &sukisu_get_manager_uid }
|
||||||
struct CompactAliasSymbol alias_symbol[] = {
|
|
||||||
{"kf_strncat", "strncat"},
|
|
||||||
{"kf_strlen", "strlen" },
|
|
||||||
{"kf_strcpy", "strcpy"},
|
|
||||||
{"compat_copy_to_user", "__arch_copy_to_user"}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
unsigned long sukisu_compact_find_symbol(const char* name) {
|
unsigned long sukisu_compact_find_symbol(const char* name) {
|
||||||
@@ -71,30 +90,13 @@ unsigned long sukisu_compact_find_symbol(const char* name) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 如果符号名以 "kf__" 开头,尝试解析去掉前缀的部分 */
|
|
||||||
if (strncmp(name, "kf__", 4) == 0) {
|
|
||||||
const char *real_name = name + 4; // 去掉 "kf__"
|
|
||||||
addr = (unsigned long)kallsyms_lookup_name(real_name);
|
|
||||||
if (addr) {
|
|
||||||
return addr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 通过内核来查
|
// 通过内核来查
|
||||||
addr = kallsyms_lookup_name(name);
|
addr = kallsyms_lookup_name(name);
|
||||||
if(addr) {
|
if(addr) {
|
||||||
return addr;
|
return addr;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 查不到就查查兼容的符号
|
|
||||||
for(i = 0; i < (sizeof(alias_symbol) / sizeof(struct CompactAliasSymbol)); i++) {
|
|
||||||
struct CompactAliasSymbol* symbol = &alias_symbol[i];
|
|
||||||
if(strcmp(name, symbol->symbol_name) == 0) {
|
|
||||||
addr = kallsyms_lookup_name(symbol->compact_symbol_name);
|
|
||||||
if(addr)
|
|
||||||
return addr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
EXPORT_SYMBOL(sukisu_compact_find_symbol);
|
||||||
|
|||||||
1340
kernel/kpm/kpm.c
1340
kernel/kpm/kpm.c
File diff suppressed because it is too large
Load Diff
@@ -1,44 +1,44 @@
|
|||||||
#ifndef ___SUKISU_KPM_H
|
#ifndef ___SUKISU_KPM_H
|
||||||
#define ___SUKISU_KPM_H
|
#define ___SUKISU_KPM_H
|
||||||
|
|
||||||
int sukisu_handle_kpm(unsigned long arg3, unsigned long arg4, unsigned long arg5);
|
int sukisu_handle_kpm(unsigned long arg2, unsigned long arg3, unsigned long arg4, unsigned long arg5);
|
||||||
int sukisu_is_kpm_control_code(unsigned long arg2);
|
int sukisu_is_kpm_control_code(unsigned long arg2);
|
||||||
|
|
||||||
// KPM控制代码
|
// KPM控制代码
|
||||||
#define CMD_KPM_CONTROL 28
|
#define CMD_KPM_CONTROL 28
|
||||||
#define CMD_KPM_CONTROL_MAX 34
|
#define CMD_KPM_CONTROL_MAX 35
|
||||||
|
|
||||||
// 控制代码
|
// 控制代码
|
||||||
|
|
||||||
// prctl(xxx, xxx, 1, "PATH", "ARGS")
|
// prctl(xxx, 28, "PATH", "ARGS")
|
||||||
// success return 0, error return -N
|
// success return 0, error return -N
|
||||||
#define SUKISU_KPM_LOAD 28
|
#define SUKISU_KPM_LOAD 28
|
||||||
|
|
||||||
// prctl(xxx, xxx, 2, "NAME")
|
// prctl(xxx, 29, "NAME")
|
||||||
// success return 0, error return -N
|
// success return 0, error return -N
|
||||||
#define SUKISU_KPM_UNLOAD 29
|
#define SUKISU_KPM_UNLOAD 29
|
||||||
|
|
||||||
// num = prctl(xxx, xxx, 3)
|
// num = prctl(xxx, 30)
|
||||||
// error return -N
|
// error return -N
|
||||||
// success return +num or 0
|
// success return +num or 0
|
||||||
#define SUKISU_KPM_NUM 30
|
#define SUKISU_KPM_NUM 30
|
||||||
|
|
||||||
// prctl(xxx, xxx, 4, Buffer, BufferSize)
|
// prctl(xxx, 31, Buffer, BufferSize)
|
||||||
// success return +out, error return -N
|
// success return +out, error return -N
|
||||||
#define SUKISU_KPM_LIST 31
|
#define SUKISU_KPM_LIST 31
|
||||||
|
|
||||||
// prctl(xxx, xxx, 5, "NAME", Buffer[256])
|
// prctl(xxx, 32, "NAME", Buffer[256])
|
||||||
// success return +out, error return -N
|
// success return +out, error return -N
|
||||||
#define SUKISU_KPM_INFO 32
|
#define SUKISU_KPM_INFO 32
|
||||||
|
|
||||||
// prctl(xxx, xxx, 6, "NAME", "ARGS")
|
// prctl(xxx, 33, "NAME", "ARGS")
|
||||||
// success return KPM's result value
|
// success return KPM's result value
|
||||||
// error return -N
|
// error return -N
|
||||||
#define SUKISU_KPM_CONTROL 33
|
#define SUKISU_KPM_CONTROL 33
|
||||||
|
|
||||||
// prctl(xxx, xxx, 7)
|
// prctl(xxx, 34, buffer, bufferSize)
|
||||||
// success will printf to stdout and return 0
|
// success return KPM's result value
|
||||||
// error will return -1
|
// error return -N
|
||||||
#define SUKISU_KPM_PRINT 34
|
#define SUKISU_KPM_VERSION 34
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
@@ -0,0 +1,285 @@
|
|||||||
|
#include <linux/export.h>
|
||||||
|
#include <linux/module.h>
|
||||||
|
#include <linux/kernel.h>
|
||||||
|
#include <linux/fs.h>
|
||||||
|
#include <linux/kernfs.h>
|
||||||
|
#include <linux/file.h>
|
||||||
|
#include <linux/slab.h>
|
||||||
|
#include <linux/vmalloc.h>
|
||||||
|
#include <linux/uaccess.h>
|
||||||
|
#include <linux/elf.h>
|
||||||
|
#include <linux/kallsyms.h>
|
||||||
|
#include <linux/version.h>
|
||||||
|
#include <linux/list.h>
|
||||||
|
#include <linux/spinlock.h>
|
||||||
|
#include <linux/rcupdate.h>
|
||||||
|
#include <asm/elf.h> /* 包含 ARM64 重定位类型定义 */
|
||||||
|
#include <linux/vmalloc.h>
|
||||||
|
#include <linux/mm.h>
|
||||||
|
#include <linux/string.h>
|
||||||
|
#include <asm/cacheflush.h>
|
||||||
|
#include <linux/module.h>
|
||||||
|
#include <linux/vmalloc.h>
|
||||||
|
#include <linux/set_memory.h>
|
||||||
|
#include <linux/version.h>
|
||||||
|
#include <linux/export.h>
|
||||||
|
#include <linux/slab.h>
|
||||||
|
#include "kpm.h"
|
||||||
|
#include "compact.h"
|
||||||
|
#include <linux/types.h>
|
||||||
|
#include <linux/stddef.h>
|
||||||
|
|
||||||
|
// 结构体成员元数据
|
||||||
|
struct DynamicStructMember {
|
||||||
|
const char* name;
|
||||||
|
size_t size;
|
||||||
|
size_t offset;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 结构体元数据(包含总大小)
|
||||||
|
struct DynamicStructInfo {
|
||||||
|
const char* name;
|
||||||
|
size_t count;
|
||||||
|
size_t total_size;
|
||||||
|
struct DynamicStructMember* members;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 定义结构体元数据的宏(直接使用 struct 名称)
|
||||||
|
#define DYNAMIC_STRUCT_BEGIN(struct_name) \
|
||||||
|
static struct DynamicStructMember struct_name##_members[] = {
|
||||||
|
|
||||||
|
#define DEFINE_MEMBER(struct_name, member) \
|
||||||
|
{ \
|
||||||
|
.name = #member, \
|
||||||
|
.size = sizeof(((struct struct_name*)0)->member), \
|
||||||
|
.offset = offsetof(struct struct_name, member) \
|
||||||
|
},
|
||||||
|
|
||||||
|
#define DYNAMIC_STRUCT_END(struct_name) \
|
||||||
|
}; \
|
||||||
|
static struct DynamicStructInfo struct_name##_info = { \
|
||||||
|
.name = #struct_name, \
|
||||||
|
.count = sizeof(struct_name##_members) / sizeof(struct DynamicStructMember), \
|
||||||
|
.total_size = sizeof(struct struct_name), \
|
||||||
|
.members = struct_name##_members \
|
||||||
|
};
|
||||||
|
|
||||||
|
// ==================================================================================
|
||||||
|
|
||||||
|
#include <linux/version.h>
|
||||||
|
|
||||||
|
#define KERNEL_VERSION_6_1 KERNEL_VERSION(6, 1, 0)
|
||||||
|
#define KERNEL_VERSION_5_15 KERNEL_VERSION(5, 15, 0)
|
||||||
|
|
||||||
|
#include <../fs/mount.h>
|
||||||
|
#include <linux/mount.h>
|
||||||
|
|
||||||
|
// 定义元数据
|
||||||
|
DYNAMIC_STRUCT_BEGIN(mount)
|
||||||
|
DEFINE_MEMBER(mount, mnt_parent)
|
||||||
|
DEFINE_MEMBER(mount, mnt)
|
||||||
|
DEFINE_MEMBER(mount, mnt_id)
|
||||||
|
DEFINE_MEMBER(mount, mnt_group_id)
|
||||||
|
DEFINE_MEMBER(mount, mnt_expiry_mark)
|
||||||
|
DEFINE_MEMBER(mount, mnt_master)
|
||||||
|
DEFINE_MEMBER(mount, mnt_devname)
|
||||||
|
DYNAMIC_STRUCT_END(mount)
|
||||||
|
|
||||||
|
DYNAMIC_STRUCT_BEGIN(vfsmount)
|
||||||
|
DEFINE_MEMBER(vfsmount, mnt_root)
|
||||||
|
DEFINE_MEMBER(vfsmount, mnt_sb)
|
||||||
|
DEFINE_MEMBER(vfsmount, mnt_flags)
|
||||||
|
DYNAMIC_STRUCT_END(vfsmount)
|
||||||
|
|
||||||
|
DYNAMIC_STRUCT_BEGIN(mnt_namespace)
|
||||||
|
DEFINE_MEMBER(mnt_namespace, ns)
|
||||||
|
DEFINE_MEMBER(mnt_namespace, root)
|
||||||
|
DEFINE_MEMBER(mnt_namespace, seq)
|
||||||
|
DEFINE_MEMBER(mnt_namespace, mounts)
|
||||||
|
#if LINUX_VERSION_CODE < KERNEL_VERSION_5_15
|
||||||
|
DEFINE_MEMBER(mnt_namespace, count)
|
||||||
|
#endif
|
||||||
|
DYNAMIC_STRUCT_END(mnt_namespace)
|
||||||
|
|
||||||
|
#include <linux/kprobes.h>
|
||||||
|
|
||||||
|
#ifdef CONFIG_KPROBES
|
||||||
|
DYNAMIC_STRUCT_BEGIN(kprobe)
|
||||||
|
DEFINE_MEMBER(kprobe, addr)
|
||||||
|
DEFINE_MEMBER(kprobe, symbol_name)
|
||||||
|
DEFINE_MEMBER(kprobe, offset)
|
||||||
|
DEFINE_MEMBER(kprobe, pre_handler)
|
||||||
|
DEFINE_MEMBER(kprobe, post_handler)
|
||||||
|
#if LINUX_VERSION_CODE < KERNEL_VERSION_5_15
|
||||||
|
DEFINE_MEMBER(kprobe, fault_handler)
|
||||||
|
#endif
|
||||||
|
DEFINE_MEMBER(kprobe, flags)
|
||||||
|
DYNAMIC_STRUCT_END(kprobe)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include <linux/mm.h>
|
||||||
|
#include <linux/mm_types.h>
|
||||||
|
|
||||||
|
DYNAMIC_STRUCT_BEGIN(vm_area_struct)
|
||||||
|
DEFINE_MEMBER(vm_area_struct,vm_start)
|
||||||
|
DEFINE_MEMBER(vm_area_struct,vm_end)
|
||||||
|
DEFINE_MEMBER(vm_area_struct,vm_flags)
|
||||||
|
DEFINE_MEMBER(vm_area_struct,anon_vma)
|
||||||
|
DEFINE_MEMBER(vm_area_struct,vm_pgoff)
|
||||||
|
DEFINE_MEMBER(vm_area_struct,vm_file)
|
||||||
|
DEFINE_MEMBER(vm_area_struct,vm_private_data)
|
||||||
|
#ifdef CONFIG_ANON_VMA_NAME
|
||||||
|
DEFINE_MEMBER(vm_area_struct, anon_name)
|
||||||
|
#endif
|
||||||
|
DEFINE_MEMBER(vm_area_struct, vm_ops)
|
||||||
|
DYNAMIC_STRUCT_END(vm_area_struct)
|
||||||
|
|
||||||
|
DYNAMIC_STRUCT_BEGIN(vm_operations_struct)
|
||||||
|
DEFINE_MEMBER(vm_operations_struct, open)
|
||||||
|
DEFINE_MEMBER(vm_operations_struct, close)
|
||||||
|
DEFINE_MEMBER(vm_operations_struct, name)
|
||||||
|
DEFINE_MEMBER(vm_operations_struct, access)
|
||||||
|
DYNAMIC_STRUCT_END(vm_operations_struct)
|
||||||
|
|
||||||
|
#include <linux/netlink.h>
|
||||||
|
|
||||||
|
DYNAMIC_STRUCT_BEGIN(netlink_kernel_cfg)
|
||||||
|
DEFINE_MEMBER(netlink_kernel_cfg, groups)
|
||||||
|
DEFINE_MEMBER(netlink_kernel_cfg, flags)
|
||||||
|
DEFINE_MEMBER(netlink_kernel_cfg, input)
|
||||||
|
DEFINE_MEMBER(netlink_kernel_cfg, cb_mutex)
|
||||||
|
DEFINE_MEMBER(netlink_kernel_cfg, bind)
|
||||||
|
DEFINE_MEMBER(netlink_kernel_cfg, unbind)
|
||||||
|
#if LINUX_VERSION_CODE < KERNEL_VERSION_6_1
|
||||||
|
DEFINE_MEMBER(netlink_kernel_cfg, compare)
|
||||||
|
#endif
|
||||||
|
DYNAMIC_STRUCT_END(netlink_kernel_cfg)
|
||||||
|
|
||||||
|
|
||||||
|
#include <linux/sched.h>
|
||||||
|
DYNAMIC_STRUCT_BEGIN(task_struct)
|
||||||
|
DEFINE_MEMBER(task_struct, pid)
|
||||||
|
DEFINE_MEMBER(task_struct, tgid)
|
||||||
|
DEFINE_MEMBER(task_struct, cred)
|
||||||
|
DEFINE_MEMBER(task_struct, real_cred)
|
||||||
|
DEFINE_MEMBER(task_struct, comm)
|
||||||
|
DEFINE_MEMBER(task_struct, parent)
|
||||||
|
DEFINE_MEMBER(task_struct, group_leader)
|
||||||
|
DEFINE_MEMBER(task_struct, mm)
|
||||||
|
DEFINE_MEMBER(task_struct, active_mm)
|
||||||
|
DEFINE_MEMBER(task_struct, thread_pid)
|
||||||
|
DEFINE_MEMBER(task_struct, files)
|
||||||
|
DEFINE_MEMBER(task_struct, seccomp)
|
||||||
|
#ifdef CONFIG_THREAD_INFO_IN_TASK
|
||||||
|
DEFINE_MEMBER(task_struct, thread_info)
|
||||||
|
#endif
|
||||||
|
#ifdef CONFIG_CGROUPS
|
||||||
|
DEFINE_MEMBER(task_struct, cgroups)
|
||||||
|
#endif
|
||||||
|
#ifdef CONFIG_SECURITY
|
||||||
|
DEFINE_MEMBER(task_struct, security)
|
||||||
|
#endif
|
||||||
|
DEFINE_MEMBER(task_struct, thread)
|
||||||
|
DYNAMIC_STRUCT_END(task_struct)
|
||||||
|
|
||||||
|
// =====================================================================================================================
|
||||||
|
|
||||||
|
#define STRUCT_INFO(name) &(name##_info)
|
||||||
|
|
||||||
|
static
|
||||||
|
struct DynamicStructInfo* dynamic_struct_infos[] = {
|
||||||
|
STRUCT_INFO(mount),
|
||||||
|
STRUCT_INFO(vfsmount),
|
||||||
|
STRUCT_INFO(mnt_namespace),
|
||||||
|
#ifdef CONFIG_KPROBES
|
||||||
|
STRUCT_INFO(kprobe),
|
||||||
|
#endif
|
||||||
|
STRUCT_INFO(vm_area_struct),
|
||||||
|
STRUCT_INFO(vm_operations_struct),
|
||||||
|
STRUCT_INFO(netlink_kernel_cfg),
|
||||||
|
STRUCT_INFO(task_struct)
|
||||||
|
};
|
||||||
|
|
||||||
|
// return 0 if successful
|
||||||
|
// return -1 if struct not defined
|
||||||
|
int sukisu_super_find_struct(
|
||||||
|
const char* struct_name,
|
||||||
|
size_t* out_size,
|
||||||
|
int* out_members
|
||||||
|
) {
|
||||||
|
for(size_t i = 0; i < (sizeof(dynamic_struct_infos) / sizeof(dynamic_struct_infos[0])); i++) {
|
||||||
|
struct DynamicStructInfo* info = dynamic_struct_infos[i];
|
||||||
|
if(strcmp(struct_name, info->name) == 0) {
|
||||||
|
if(out_size)
|
||||||
|
*out_size = info->total_size;
|
||||||
|
if(out_members)
|
||||||
|
*out_members = info->count;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
EXPORT_SYMBOL(sukisu_super_find_struct);
|
||||||
|
|
||||||
|
// Dynamic access struct
|
||||||
|
// return 0 if successful
|
||||||
|
// return -1 if struct not defined
|
||||||
|
// return -2 if member not defined
|
||||||
|
int sukisu_super_access (
|
||||||
|
const char* struct_name,
|
||||||
|
const char* member_name,
|
||||||
|
size_t* out_offset,
|
||||||
|
size_t* out_size
|
||||||
|
) {
|
||||||
|
for(size_t i = 0; i < (sizeof(dynamic_struct_infos) / sizeof(dynamic_struct_infos[0])); i++) {
|
||||||
|
struct DynamicStructInfo* info = dynamic_struct_infos[i];
|
||||||
|
if(strcmp(struct_name, info->name) == 0) {
|
||||||
|
for (size_t i1 = 0; i1 < info->count; i1++) {
|
||||||
|
if (strcmp(info->members[i1].name, member_name) == 0) {
|
||||||
|
if(out_offset)
|
||||||
|
*out_offset = info->members[i].offset;
|
||||||
|
if(out_size)
|
||||||
|
*out_size = info->members[i].size;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
EXPORT_SYMBOL(sukisu_super_access);
|
||||||
|
|
||||||
|
// 动态 container_of 宏
|
||||||
|
#define DYNAMIC_CONTAINER_OF(offset, member_ptr) ({ \
|
||||||
|
(offset != (size_t)-1) ? (void*)((char*)(member_ptr) - offset) : NULL; \
|
||||||
|
})
|
||||||
|
|
||||||
|
// Dynamic container_of
|
||||||
|
// return 0 if success
|
||||||
|
// return -1 if current struct not defined
|
||||||
|
// return -2 if target member not defined
|
||||||
|
int sukisu_super_container_of(
|
||||||
|
const char* struct_name,
|
||||||
|
const char* member_name,
|
||||||
|
void* ptr,
|
||||||
|
void** out_ptr
|
||||||
|
) {
|
||||||
|
if(ptr == NULL) {
|
||||||
|
return -3;
|
||||||
|
}
|
||||||
|
for(size_t i = 0; i < (sizeof(dynamic_struct_infos) / sizeof(dynamic_struct_infos[0])); i++) {
|
||||||
|
struct DynamicStructInfo* info = dynamic_struct_infos[i];
|
||||||
|
if(strcmp(struct_name, info->name) == 0) {
|
||||||
|
for (size_t i1 = 0; i1 < info->count; i1++) {
|
||||||
|
if (strcmp(info->members[i1].name, member_name) == 0) {
|
||||||
|
*out_ptr = (void*) DYNAMIC_CONTAINER_OF(info->members[i1].offset, ptr);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
EXPORT_SYMBOL(sukisu_super_container_of);
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
#ifndef __SUKISU_SUPER_ACCESS_H
|
||||||
|
#define __SUKISU_SUPER_ACCESS_H
|
||||||
|
|
||||||
|
#include <linux/types.h>
|
||||||
|
#include <linux/stddef.h>
|
||||||
|
#include "kpm.h"
|
||||||
|
#include "compact.h"
|
||||||
|
|
||||||
|
// return 0 if successful
|
||||||
|
// return -1 if struct not defined
|
||||||
|
int sukisu_super_find_struct(
|
||||||
|
const char* struct_name,
|
||||||
|
size_t* out_size,
|
||||||
|
int* out_members
|
||||||
|
);
|
||||||
|
|
||||||
|
// Dynamic access struct
|
||||||
|
// return 0 if successful
|
||||||
|
// return -1 if struct not defined
|
||||||
|
// return -2 if member not defined
|
||||||
|
int sukisu_super_access (
|
||||||
|
const char* struct_name,
|
||||||
|
const char* member_name,
|
||||||
|
size_t* out_offset,
|
||||||
|
size_t* out_size
|
||||||
|
);
|
||||||
|
|
||||||
|
// Dynamic container_of
|
||||||
|
// return 0 if success
|
||||||
|
// return -1 if current struct not defined
|
||||||
|
// return -2 if target member not defined
|
||||||
|
int sukisu_super_container_of(
|
||||||
|
const char* struct_name,
|
||||||
|
const char* member_name,
|
||||||
|
void* ptr,
|
||||||
|
void** out_ptr
|
||||||
|
);
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
@file:Suppress("UnstableApiUsage")
|
@file:Suppress("UnstableApiUsage")
|
||||||
|
|
||||||
|
import com.android.build.api.dsl.ApkSigningConfig
|
||||||
import com.android.build.gradle.internal.api.BaseVariantOutputImpl
|
import com.android.build.gradle.internal.api.BaseVariantOutputImpl
|
||||||
import com.android.build.gradle.tasks.PackageAndroidArtifact
|
import com.android.build.gradle.tasks.PackageAndroidArtifact
|
||||||
|
|
||||||
@@ -24,8 +25,18 @@ apksign {
|
|||||||
keyPasswordProperty = "KEY_PASSWORD"
|
keyPasswordProperty = "KEY_PASSWORD"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "shirkneko.zako.sukisu"
|
|
||||||
|
/**signingConfigs {
|
||||||
|
create("Debug") {
|
||||||
|
storeFile = file("D:\\other\\AndroidTool\\android_key\\keystore\\release-key.keystore")
|
||||||
|
storePassword = ""
|
||||||
|
keyAlias = ""
|
||||||
|
keyPassword = ""
|
||||||
|
}
|
||||||
|
}**/
|
||||||
|
namespace = "com.sukisu.ultra"
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
@@ -33,6 +44,9 @@ android {
|
|||||||
isShrinkResources = true
|
isShrinkResources = true
|
||||||
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
|
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
|
||||||
}
|
}
|
||||||
|
/**debug {
|
||||||
|
signingConfig = signingConfigs.named("Debug").get() as ApkSigningConfig
|
||||||
|
}**/
|
||||||
}
|
}
|
||||||
|
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
|
|||||||
@@ -3,8 +3,10 @@
|
|||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
tools:ignore="ScopedStorage" />
|
||||||
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||||
|
tools:ignore="ScopedStorage" />
|
||||||
|
|
||||||
|
|
||||||
<application
|
<application
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
// IKsuInterface.aidl
|
package com.sukisu.zako;
|
||||||
package shirkneko.zako.sukisu;
|
|
||||||
|
|
||||||
import android.content.pm.PackageInfo;
|
import android.content.pm.PackageInfo;
|
||||||
import rikka.parcelablelist.ParcelableListSlice;
|
import rikka.parcelablelist.ParcelableListSlice;
|
||||||
BIN
manager/app/src/main/assets/5_15+-mkbootfs
Normal file
BIN
manager/app/src/main/assets/5_15+-mkbootfs
Normal file
Binary file not shown.
@@ -12,7 +12,7 @@
|
|||||||
|
|
||||||
extern "C"
|
extern "C"
|
||||||
JNIEXPORT jboolean JNICALL
|
JNIEXPORT jboolean JNICALL
|
||||||
Java_shirkneko_zako_sukisu_Natives_becomeManager(JNIEnv *env, jobject, jstring pkg) {
|
Java_com_sukisu_ultra_Natives_becomeManager(JNIEnv *env, jobject, jstring pkg) {
|
||||||
auto cpkg = env->GetStringUTFChars(pkg, nullptr);
|
auto cpkg = env->GetStringUTFChars(pkg, nullptr);
|
||||||
auto result = become_manager(cpkg);
|
auto result = become_manager(cpkg);
|
||||||
env->ReleaseStringUTFChars(pkg, cpkg);
|
env->ReleaseStringUTFChars(pkg, cpkg);
|
||||||
@@ -21,13 +21,13 @@ Java_shirkneko_zako_sukisu_Natives_becomeManager(JNIEnv *env, jobject, jstring p
|
|||||||
|
|
||||||
extern "C"
|
extern "C"
|
||||||
JNIEXPORT jint JNICALL
|
JNIEXPORT jint JNICALL
|
||||||
Java_shirkneko_zako_sukisu_Natives_getVersion(JNIEnv *env, jobject) {
|
Java_com_sukisu_ultra_Natives_getVersion(JNIEnv *env, jobject) {
|
||||||
return get_version();
|
return get_version();
|
||||||
}
|
}
|
||||||
|
|
||||||
extern "C"
|
extern "C"
|
||||||
JNIEXPORT jintArray JNICALL
|
JNIEXPORT jintArray JNICALL
|
||||||
Java_shirkneko_zako_sukisu_Natives_getAllowList(JNIEnv *env, jobject) {
|
Java_com_sukisu_ultra_Natives_getAllowList(JNIEnv *env, jobject) {
|
||||||
int uids[1024];
|
int uids[1024];
|
||||||
int size = 0;
|
int size = 0;
|
||||||
bool result = get_allow_list(uids, &size);
|
bool result = get_allow_list(uids, &size);
|
||||||
@@ -42,13 +42,13 @@ Java_shirkneko_zako_sukisu_Natives_getAllowList(JNIEnv *env, jobject) {
|
|||||||
|
|
||||||
extern "C"
|
extern "C"
|
||||||
JNIEXPORT jboolean JNICALL
|
JNIEXPORT jboolean JNICALL
|
||||||
Java_shirkneko_zako_sukisu_Natives_isSafeMode(JNIEnv *env, jclass clazz) {
|
Java_com_sukisu_ultra_Natives_isSafeMode(JNIEnv *env, jclass clazz) {
|
||||||
return is_safe_mode();
|
return is_safe_mode();
|
||||||
}
|
}
|
||||||
|
|
||||||
extern "C"
|
extern "C"
|
||||||
JNIEXPORT jboolean JNICALL
|
JNIEXPORT jboolean JNICALL
|
||||||
Java_shirkneko_zako_sukisu_Natives_isLkmMode(JNIEnv *env, jclass clazz) {
|
Java_com_sukisu_ultra_Natives_isLkmMode(JNIEnv *env, jclass clazz) {
|
||||||
return is_lkm_mode();
|
return is_lkm_mode();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,7 +111,7 @@ static void fillArrayWithList(JNIEnv *env, jobject list, int *data, int count) {
|
|||||||
|
|
||||||
extern "C"
|
extern "C"
|
||||||
JNIEXPORT jobject JNICALL
|
JNIEXPORT jobject JNICALL
|
||||||
Java_shirkneko_zako_sukisu_Natives_getAppProfile(JNIEnv *env, jobject, jstring pkg, jint uid) {
|
Java_com_sukisu_ultra_Natives_getAppProfile(JNIEnv *env, jobject, jstring pkg, jint uid) {
|
||||||
if (env->GetStringLength(pkg) > KSU_MAX_PACKAGE_NAME) {
|
if (env->GetStringLength(pkg) > KSU_MAX_PACKAGE_NAME) {
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
@@ -129,7 +129,7 @@ Java_shirkneko_zako_sukisu_Natives_getAppProfile(JNIEnv *env, jobject, jstring p
|
|||||||
|
|
||||||
bool useDefaultProfile = !get_app_profile(key, &profile);
|
bool useDefaultProfile = !get_app_profile(key, &profile);
|
||||||
|
|
||||||
auto cls = env->FindClass("shirkneko/zako/sukisu/Natives$Profile");
|
auto cls = env->FindClass("com/sukisu/ultra/Natives$Profile");
|
||||||
auto constructor = env->GetMethodID(cls, "<init>", "()V");
|
auto constructor = env->GetMethodID(cls, "<init>", "()V");
|
||||||
auto obj = env->NewObject(cls, constructor);
|
auto obj = env->NewObject(cls, constructor);
|
||||||
auto keyField = env->GetFieldID(cls, "name", "Ljava/lang/String;");
|
auto keyField = env->GetFieldID(cls, "name", "Ljava/lang/String;");
|
||||||
@@ -207,8 +207,8 @@ Java_shirkneko_zako_sukisu_Natives_getAppProfile(JNIEnv *env, jobject, jstring p
|
|||||||
|
|
||||||
extern "C"
|
extern "C"
|
||||||
JNIEXPORT jboolean JNICALL
|
JNIEXPORT jboolean JNICALL
|
||||||
Java_shirkneko_zako_sukisu_Natives_setAppProfile(JNIEnv *env, jobject clazz, jobject profile) {
|
Java_com_sukisu_ultra_Natives_setAppProfile(JNIEnv *env, jobject clazz, jobject profile) {
|
||||||
auto cls = env->FindClass("shirkneko/zako/sukisu/Natives$Profile");
|
auto cls = env->FindClass("com/sukisu/ultra/Natives$Profile");
|
||||||
|
|
||||||
auto keyField = env->GetFieldID(cls, "name", "Ljava/lang/String;");
|
auto keyField = env->GetFieldID(cls, "name", "Ljava/lang/String;");
|
||||||
auto currentUidField = env->GetFieldID(cls, "currentUid", "I");
|
auto currentUidField = env->GetFieldID(cls, "currentUid", "I");
|
||||||
@@ -293,16 +293,16 @@ Java_shirkneko_zako_sukisu_Natives_setAppProfile(JNIEnv *env, jobject clazz, job
|
|||||||
}
|
}
|
||||||
extern "C"
|
extern "C"
|
||||||
JNIEXPORT jboolean JNICALL
|
JNIEXPORT jboolean JNICALL
|
||||||
Java_shirkneko_zako_sukisu_Natives_uidShouldUmount(JNIEnv *env, jobject thiz, jint uid) {
|
Java_com_sukisu_ultra_Natives_uidShouldUmount(JNIEnv *env, jobject thiz, jint uid) {
|
||||||
return uid_should_umount(uid);
|
return uid_should_umount(uid);
|
||||||
}
|
}
|
||||||
extern "C"
|
extern "C"
|
||||||
JNIEXPORT jboolean JNICALL
|
JNIEXPORT jboolean JNICALL
|
||||||
Java_shirkneko_zako_sukisu_Natives_isSuEnabled(JNIEnv *env, jobject thiz) {
|
Java_com_sukisu_ultra_Natives_isSuEnabled(JNIEnv *env, jobject thiz) {
|
||||||
return is_su_enabled();
|
return is_su_enabled();
|
||||||
}
|
}
|
||||||
extern "C"
|
extern "C"
|
||||||
JNIEXPORT jboolean JNICALL
|
JNIEXPORT jboolean JNICALL
|
||||||
Java_shirkneko_zako_sukisu_Natives_setSuEnabled(JNIEnv *env, jobject thiz, jboolean enabled) {
|
Java_com_sukisu_ultra_Natives_setSuEnabled(JNIEnv *env, jobject thiz, jboolean enabled) {
|
||||||
return set_su_enabled(enabled);
|
return set_su_enabled(enabled);
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package shirkneko.zako.sukisu
|
package com.sukisu.ultra
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import coil.Coil
|
import coil.Coil
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package shirkneko.zako.sukisu
|
package com.sukisu.ultra
|
||||||
|
|
||||||
import android.system.Os
|
import android.system.Os
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package shirkneko.zako.sukisu
|
package com.sukisu.ultra
|
||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import androidx.annotation.Keep
|
import androidx.annotation.Keep
|
||||||
@@ -16,14 +16,14 @@ object Natives {
|
|||||||
// 10946: add capabilities
|
// 10946: add capabilities
|
||||||
// 10977: change groups_count and groups to avoid overflow write
|
// 10977: change groups_count and groups to avoid overflow write
|
||||||
// 11071: Fix the issue of failing to set a custom SELinux type.
|
// 11071: Fix the issue of failing to set a custom SELinux type.
|
||||||
const val MINIMAL_SUPPORTED_KERNEL = 11071
|
const val MINIMAL_SUPPORTED_KERNEL = 12800
|
||||||
|
|
||||||
// 11640: Support query working mode, LKM or GKI
|
// 11640: Support query working mode, LKM or GKI
|
||||||
// when MINIMAL_SUPPORTED_KERNEL > 11640, we can remove this constant.
|
// when MINIMAL_SUPPORTED_KERNEL > 11640, we can remove this constant.
|
||||||
const val MINIMAL_SUPPORTED_KERNEL_LKM = 11648
|
const val MINIMAL_SUPPORTED_KERNEL_LKM = 12800
|
||||||
|
|
||||||
// 12040: Support disable sucompat mode
|
// 12040: Support disable sucompat mode
|
||||||
const val MINIMAL_SUPPORTED_SU_COMPAT = 12040
|
const val MINIMAL_SUPPORTED_SU_COMPAT = 12800
|
||||||
const val KERNEL_SU_DOMAIN = "u:r:su:s0"
|
const val KERNEL_SU_DOMAIN = "u:r:su:s0"
|
||||||
|
|
||||||
const val ROOT_UID = 0
|
const val ROOT_UID = 0
|
||||||
446
manager/app/src/main/java/com/sukisu/ultra/flash/KernelFlash.kt
Normal file
446
manager/app/src/main/java/com/sukisu/ultra/flash/KernelFlash.kt
Normal file
@@ -0,0 +1,446 @@
|
|||||||
|
package com.sukisu.ultra.flash
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.CheckCircle
|
||||||
|
import androidx.compose.material.icons.filled.Error
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.documentfile.provider.DocumentFile
|
||||||
|
import com.sukisu.ultra.R
|
||||||
|
import com.sukisu.ultra.utils.AssetsUtil
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
data class FlashState(
|
||||||
|
val isFlashing: Boolean = false,
|
||||||
|
val isCompleted: Boolean = false,
|
||||||
|
val progress: Float = 0f,
|
||||||
|
val currentStep: String = "",
|
||||||
|
val logs: List<String> = emptyList(),
|
||||||
|
val error: String = ""
|
||||||
|
)
|
||||||
|
|
||||||
|
class HorizonKernelState {
|
||||||
|
private val _state = MutableStateFlow(FlashState())
|
||||||
|
val state: StateFlow<FlashState> = _state.asStateFlow()
|
||||||
|
|
||||||
|
fun updateProgress(progress: Float) {
|
||||||
|
_state.update { it.copy(progress = progress) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateStep(step: String) {
|
||||||
|
_state.update { it.copy(currentStep = step) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addLog(log: String) {
|
||||||
|
_state.update {
|
||||||
|
it.copy(logs = it.logs + log)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setError(error: String) {
|
||||||
|
_state.update { it.copy(error = error) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun startFlashing() {
|
||||||
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
isFlashing = true,
|
||||||
|
isCompleted = false,
|
||||||
|
progress = 0f,
|
||||||
|
currentStep = "under preparation...",
|
||||||
|
logs = emptyList(),
|
||||||
|
error = ""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun completeFlashing() {
|
||||||
|
_state.update { it.copy(isCompleted = true, progress = 1f) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun reset() {
|
||||||
|
_state.value = FlashState()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class HorizonKernelWorker(
|
||||||
|
private val context: Context,
|
||||||
|
private val state: HorizonKernelState,
|
||||||
|
private val slot: String? = null
|
||||||
|
) : Thread() {
|
||||||
|
var uri: Uri? = null
|
||||||
|
private lateinit var filePath: String
|
||||||
|
private lateinit var binaryPath: String
|
||||||
|
|
||||||
|
private var onFlashComplete: (() -> Unit)? = null
|
||||||
|
private var originalSlot: String? = null
|
||||||
|
|
||||||
|
fun setOnFlashCompleteListener(listener: () -> Unit) {
|
||||||
|
onFlashComplete = listener
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun run() {
|
||||||
|
state.startFlashing()
|
||||||
|
state.updateStep(context.getString(R.string.horizon_preparing))
|
||||||
|
|
||||||
|
filePath = "${context.filesDir.absolutePath}/${DocumentFile.fromSingleUri(context, uri!!)?.name}"
|
||||||
|
binaryPath = "${context.filesDir.absolutePath}/META-INF/com/google/android/update-binary"
|
||||||
|
|
||||||
|
try {
|
||||||
|
state.updateStep(context.getString(R.string.horizon_cleaning_files))
|
||||||
|
state.updateProgress(0.1f)
|
||||||
|
cleanup()
|
||||||
|
|
||||||
|
if (!rootAvailable()) {
|
||||||
|
state.setError(context.getString(R.string.root_required))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
state.updateStep(context.getString(R.string.horizon_copying_files))
|
||||||
|
state.updateProgress(0.2f)
|
||||||
|
copy()
|
||||||
|
|
||||||
|
if (!File(filePath).exists()) {
|
||||||
|
state.setError(context.getString(R.string.horizon_copy_failed))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
state.updateStep(context.getString(R.string.horizon_extracting_tool))
|
||||||
|
state.updateProgress(0.4f)
|
||||||
|
getBinary()
|
||||||
|
|
||||||
|
state.updateStep(context.getString(R.string.horizon_patching_script))
|
||||||
|
state.updateProgress(0.6f)
|
||||||
|
patch()
|
||||||
|
|
||||||
|
state.updateStep(context.getString(R.string.horizon_flashing))
|
||||||
|
state.updateProgress(0.7f)
|
||||||
|
|
||||||
|
val isAbDevice = isAbDevice()
|
||||||
|
|
||||||
|
if (isAbDevice && slot != null) {
|
||||||
|
state.updateStep(context.getString(R.string.horizon_getting_original_slot))
|
||||||
|
state.updateProgress(0.72f)
|
||||||
|
originalSlot = runCommandGetOutput(true, "getprop ro.boot.slot_suffix")
|
||||||
|
|
||||||
|
state.updateStep(context.getString(R.string.horizon_setting_target_slot))
|
||||||
|
state.updateProgress(0.74f)
|
||||||
|
runCommand(true, "resetprop -n ro.boot.slot_suffix _$slot")
|
||||||
|
}
|
||||||
|
|
||||||
|
flash()
|
||||||
|
|
||||||
|
if (isAbDevice && !originalSlot.isNullOrEmpty()) {
|
||||||
|
state.updateStep(context.getString(R.string.horizon_restoring_original_slot))
|
||||||
|
state.updateProgress(0.8f)
|
||||||
|
runCommand(true, "resetprop ro.boot.slot_suffix $originalSlot")
|
||||||
|
}
|
||||||
|
|
||||||
|
state.updateStep(context.getString(R.string.horizon_flash_complete_status))
|
||||||
|
state.completeFlashing()
|
||||||
|
|
||||||
|
(context as? Activity)?.runOnUiThread {
|
||||||
|
onFlashComplete?.invoke()
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
state.setError(e.message ?: context.getString(R.string.horizon_unknown_error))
|
||||||
|
|
||||||
|
if (isAbDevice() && !originalSlot.isNullOrEmpty()) {
|
||||||
|
state.updateStep(context.getString(R.string.horizon_restoring_original_slot))
|
||||||
|
state.updateProgress(0.8f)
|
||||||
|
runCommand(true, "resetprop ro.boot.slot_suffix $originalSlot")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查设备是否为AB分区设备
|
||||||
|
private fun isAbDevice(): Boolean {
|
||||||
|
val abUpdate = runCommandGetOutput(true, "getprop ro.build.ab_update")?.trim() ?: ""
|
||||||
|
if (abUpdate.equals("false", ignoreCase = true) || abUpdate.isEmpty()) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
val slotSuffix = runCommandGetOutput(true, "getprop ro.boot.slot_suffix")
|
||||||
|
return !slotSuffix.isNullOrEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun cleanup() {
|
||||||
|
runCommand(false, "find ${context.filesDir.absolutePath} -type f ! -name '*.jpg' ! -name '*.png' -delete")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun copy() {
|
||||||
|
uri?.let { safeUri ->
|
||||||
|
context.contentResolver.openInputStream(safeUri)?.use { input ->
|
||||||
|
FileOutputStream(File(filePath)).use { output ->
|
||||||
|
input.copyTo(output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getBinary() {
|
||||||
|
runCommand(false, "unzip \"$filePath\" \"*/update-binary\" -d ${context.filesDir.absolutePath}")
|
||||||
|
if (!File(binaryPath).exists()) {
|
||||||
|
throw IOException("Failed to extract update-binary")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun patch() {
|
||||||
|
val kernelVersion = runCommandGetOutput(true, "cat /proc/version")
|
||||||
|
val versionRegex = """\d+\.\d+\.\d+""".toRegex()
|
||||||
|
val version = kernelVersion?.let { versionRegex.find(it) }?.value ?: ""
|
||||||
|
val toolName = if (version.isNotEmpty()) {
|
||||||
|
val parts = version.split('.')
|
||||||
|
if (parts.size >= 2) {
|
||||||
|
val major = parts[0].toIntOrNull() ?: 0
|
||||||
|
val minor = parts[1].toIntOrNull() ?: 0
|
||||||
|
if (major < 5 || (major == 5 && minor <= 10)) "5_10" else "5_15+"
|
||||||
|
} else {
|
||||||
|
"5_15+"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
"5_15+"
|
||||||
|
}
|
||||||
|
val toolPath = "${context.filesDir.absolutePath}/mkbootfs"
|
||||||
|
AssetsUtil.exportFiles(context, "$toolName-mkbootfs", toolPath)
|
||||||
|
state.addLog("${context.getString(R.string.kernel_version_log, version)} ${context.getString(R.string.tool_version_log, toolName)}")
|
||||||
|
runCommand(false, "sed -i '/chmod -R 755 tools bin;/i cp -f $toolPath \$AKHOME/tools;' $binaryPath")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun flash() {
|
||||||
|
val process = ProcessBuilder("su")
|
||||||
|
.redirectErrorStream(true)
|
||||||
|
.start()
|
||||||
|
|
||||||
|
try {
|
||||||
|
process.outputStream.bufferedWriter().use { writer ->
|
||||||
|
writer.write("export POSTINSTALL=${context.filesDir.absolutePath}\n")
|
||||||
|
|
||||||
|
// 写入槽位信息到临时文件
|
||||||
|
slot?.let { selectedSlot ->
|
||||||
|
writer.write("echo \"$selectedSlot\" > ${context.filesDir.absolutePath}/bootslot\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建刷写命令
|
||||||
|
val flashCommand = buildString {
|
||||||
|
append("sh $binaryPath 3 1 \"$filePath\"")
|
||||||
|
if (slot != null) {
|
||||||
|
append(" \"$(cat ${context.filesDir.absolutePath}/bootslot)\"")
|
||||||
|
}
|
||||||
|
append(" && touch ${context.filesDir.absolutePath}/done\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.write(flashCommand)
|
||||||
|
writer.write("exit\n")
|
||||||
|
writer.flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
process.inputStream.bufferedReader().use { reader ->
|
||||||
|
reader.lineSequence().forEach { line ->
|
||||||
|
if (line.startsWith("ui_print")) {
|
||||||
|
val logMessage = line.removePrefix("ui_print").trim()
|
||||||
|
state.addLog(logMessage)
|
||||||
|
|
||||||
|
when {
|
||||||
|
logMessage.contains("extracting", ignoreCase = true) -> {
|
||||||
|
state.updateProgress(0.75f)
|
||||||
|
}
|
||||||
|
logMessage.contains("installing", ignoreCase = true) -> {
|
||||||
|
state.updateProgress(0.85f)
|
||||||
|
}
|
||||||
|
logMessage.contains("complete", ignoreCase = true) -> {
|
||||||
|
state.updateProgress(0.95f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
process.destroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!File("${context.filesDir.absolutePath}/done").exists()) {
|
||||||
|
throw IOException(context.getString(R.string.flash_failed_message))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun runCommand(su: Boolean, cmd: String): Int {
|
||||||
|
val process = ProcessBuilder(if (su) "su" else "sh")
|
||||||
|
.redirectErrorStream(true)
|
||||||
|
.start()
|
||||||
|
|
||||||
|
return try {
|
||||||
|
process.outputStream.bufferedWriter().use { writer ->
|
||||||
|
writer.write("$cmd\n")
|
||||||
|
writer.write("exit\n")
|
||||||
|
writer.flush()
|
||||||
|
}
|
||||||
|
process.waitFor()
|
||||||
|
} finally {
|
||||||
|
process.destroy()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun runCommandGetOutput(su: Boolean, cmd: String): String? {
|
||||||
|
val process = ProcessBuilder(if (su) "su" else "sh")
|
||||||
|
.redirectErrorStream(true)
|
||||||
|
.start()
|
||||||
|
|
||||||
|
return try {
|
||||||
|
process.outputStream.bufferedWriter().use { writer ->
|
||||||
|
writer.write("$cmd\n")
|
||||||
|
writer.write("exit\n")
|
||||||
|
writer.flush()
|
||||||
|
}
|
||||||
|
process.inputStream.bufferedReader().use { reader ->
|
||||||
|
reader.readText().trim()
|
||||||
|
}
|
||||||
|
} catch (_: Exception) {
|
||||||
|
""
|
||||||
|
} finally {
|
||||||
|
process.destroy()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun rootAvailable(): Boolean {
|
||||||
|
return try {
|
||||||
|
val process = Runtime.getRuntime().exec("su -c id")
|
||||||
|
val exitValue = process.waitFor()
|
||||||
|
exitValue == 0
|
||||||
|
} catch (_: Exception) {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun HorizonKernelFlashProgress(state: FlashState) {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceVariant
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.horizon_flash_title),
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
modifier = Modifier.padding(bottom = 8.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
LinearProgressIndicator(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 8.dp),
|
||||||
|
progress = { state.progress },
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = state.currentStep,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
modifier = Modifier.padding(vertical = 4.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (state.logs.isNotEmpty()) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.horizon_logs_label),
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.Start)
|
||||||
|
.padding(top = 8.dp, bottom = 4.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.heightIn(max = 230.dp)
|
||||||
|
.padding(vertical = 4.dp),
|
||||||
|
color = MaterialTheme.colorScheme.surface,
|
||||||
|
tonalElevation = 1.dp,
|
||||||
|
shape = MaterialTheme.shapes.small
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(8.dp)
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
) {
|
||||||
|
state.logs.forEach { log ->
|
||||||
|
Text(
|
||||||
|
text = log,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
modifier = Modifier.padding(vertical = 2.dp),
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
maxLines = 1
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.error.isNotEmpty()) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(top = 8.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Error,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.error,
|
||||||
|
modifier = Modifier.padding(end = 8.dp)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = state.error,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.error
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else if (state.isCompleted) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(top = 8.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.CheckCircle,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
|
modifier = Modifier.padding(end = 8.dp)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.horizon_flash_complete),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package shirkneko.zako.sukisu.profile
|
package com.sukisu.ultra.profile
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author weishu
|
* @author weishu
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package shirkneko.zako.sukisu.profile
|
package com.sukisu.ultra.profile
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* https://cs.android.com/android/platform/superproject/main/+/main:system/core/libcutils/include/private/android_filesystem_config.h
|
* https://cs.android.com/android/platform/superproject/main/+/main:system/core/libcutils/include/private/android_filesystem_config.h
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package shirkneko.zako.sukisu.ui;
|
package com.sukisu.ultra.ui;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
@@ -17,7 +17,7 @@ import java.lang.reflect.Method;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import shirkneko.zako.sukisu.IKsuInterface;
|
import com.sukisu.zako.IKsuInterface;
|
||||||
import rikka.parcelablelist.ParcelableListSlice;
|
import rikka.parcelablelist.ParcelableListSlice;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
249
manager/app/src/main/java/com/sukisu/ultra/ui/MainActivity.kt
Normal file
249
manager/app/src/main/java/com/sukisu/ultra/ui/MainActivity.kt
Normal file
@@ -0,0 +1,249 @@
|
|||||||
|
package com.sukisu.ultra.ui
|
||||||
|
|
||||||
|
import android.database.ContentObserver
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.Handler
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.activity.compose.setContent
|
||||||
|
import androidx.activity.enableEdgeToEdge
|
||||||
|
import androidx.compose.animation.*
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.navigation.NavBackStackEntry
|
||||||
|
import androidx.navigation.NavHostController
|
||||||
|
import androidx.navigation.compose.rememberNavController
|
||||||
|
import com.ramcosta.composedestinations.DestinationsNavHost
|
||||||
|
import com.ramcosta.composedestinations.animations.NavHostAnimatedDestinationStyle
|
||||||
|
import com.ramcosta.composedestinations.generated.NavGraphs
|
||||||
|
import com.ramcosta.composedestinations.spec.NavHostGraphSpec
|
||||||
|
import com.ramcosta.composedestinations.spec.RouteOrDirection
|
||||||
|
import com.ramcosta.composedestinations.utils.isRouteOnBackStackAsState
|
||||||
|
import com.ramcosta.composedestinations.utils.rememberDestinationsNavigator
|
||||||
|
import io.sukisu.ultra.UltraToolInstall
|
||||||
|
import com.sukisu.ultra.Natives
|
||||||
|
import com.sukisu.ultra.ksuApp
|
||||||
|
import com.sukisu.ultra.ui.screen.BottomBarDestination
|
||||||
|
import com.sukisu.ultra.ui.theme.*
|
||||||
|
import com.sukisu.ultra.ui.theme.CardConfig.cardAlpha
|
||||||
|
import com.sukisu.ultra.ui.util.*
|
||||||
|
import androidx.core.content.edit
|
||||||
|
import com.sukisu.ultra.ui.theme.CardConfig.cardElevation
|
||||||
|
|
||||||
|
class MainActivity : ComponentActivity() {
|
||||||
|
private inner class ThemeChangeContentObserver(
|
||||||
|
handler: Handler,
|
||||||
|
private val onThemeChanged: () -> Unit
|
||||||
|
) : ContentObserver(handler) {
|
||||||
|
override fun onChange(selfChange: Boolean) {
|
||||||
|
super.onChange(selfChange)
|
||||||
|
onThemeChanged()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
// Enable edge to edge
|
||||||
|
enableEdgeToEdge()
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
window.isNavigationBarContrastEnforced = false
|
||||||
|
}
|
||||||
|
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
val prefs = getSharedPreferences("settings", MODE_PRIVATE)
|
||||||
|
val isFirstRun = prefs.getBoolean("is_first_run", true)
|
||||||
|
|
||||||
|
if (isFirstRun) {
|
||||||
|
ThemeConfig.preventBackgroundRefresh = false
|
||||||
|
getSharedPreferences("theme_prefs", MODE_PRIVATE).edit {
|
||||||
|
putBoolean("prevent_background_refresh", false)
|
||||||
|
}
|
||||||
|
prefs.edit { putBoolean("is_first_run", false) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载保存的背景设置
|
||||||
|
loadThemeMode()
|
||||||
|
loadThemeColors()
|
||||||
|
loadDynamicColorState()
|
||||||
|
CardConfig.load(applicationContext)
|
||||||
|
|
||||||
|
val contentObserver = ThemeChangeContentObserver(Handler(mainLooper)) {
|
||||||
|
runOnUiThread {
|
||||||
|
if (!ThemeConfig.preventBackgroundRefresh) {
|
||||||
|
ThemeConfig.backgroundImageLoaded = false
|
||||||
|
loadCustomBackground()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
contentResolver.registerContentObserver(
|
||||||
|
android.provider.Settings.System.getUriFor("ui_night_mode"),
|
||||||
|
false,
|
||||||
|
contentObserver
|
||||||
|
)
|
||||||
|
|
||||||
|
val destroyListeners = mutableListOf<() -> Unit>()
|
||||||
|
destroyListeners.add {
|
||||||
|
contentResolver.unregisterContentObserver(contentObserver)
|
||||||
|
}
|
||||||
|
|
||||||
|
val isManager = Natives.becomeManager(ksuApp.packageName)
|
||||||
|
if (isManager) {
|
||||||
|
install()
|
||||||
|
UltraToolInstall.tryToInstall()
|
||||||
|
}
|
||||||
|
|
||||||
|
setContent {
|
||||||
|
KernelSUTheme {
|
||||||
|
val navController = rememberNavController()
|
||||||
|
val snackBarHostState = remember { SnackbarHostState() }
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
bottomBar = { BottomBar(navController) },
|
||||||
|
contentWindowInsets = WindowInsets(0, 0, 0, 0)
|
||||||
|
) { innerPadding ->
|
||||||
|
CompositionLocalProvider(
|
||||||
|
LocalSnackbarHost provides snackBarHostState
|
||||||
|
) {
|
||||||
|
DestinationsNavHost(
|
||||||
|
modifier = Modifier.padding(innerPadding),
|
||||||
|
navGraph = NavGraphs.root as NavHostGraphSpec,
|
||||||
|
navController = navController,
|
||||||
|
defaultTransitions = object : NavHostAnimatedDestinationStyle() {
|
||||||
|
override val enterTransition: AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition
|
||||||
|
get() = { fadeIn(animationSpec = tween(340)) }
|
||||||
|
override val exitTransition: AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition
|
||||||
|
get() = { fadeOut(animationSpec = tween(340)) }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
super.onPause()
|
||||||
|
CardConfig.save(applicationContext)
|
||||||
|
getSharedPreferences("theme_prefs", MODE_PRIVATE).edit() {
|
||||||
|
putBoolean("prevent_background_refresh", true)
|
||||||
|
}
|
||||||
|
ThemeConfig.preventBackgroundRefresh = true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
if (!ThemeConfig.backgroundImageLoaded && !ThemeConfig.preventBackgroundRefresh) {
|
||||||
|
loadCustomBackground()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val destroyListeners = mutableListOf<() -> Unit>()
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
destroyListeners.forEach { it() }
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
private fun BottomBar(navController: NavHostController) {
|
||||||
|
val navigator = navController.rememberDestinationsNavigator()
|
||||||
|
val isManager = Natives.becomeManager(ksuApp.packageName)
|
||||||
|
val fullFeatured = isManager && !Natives.requireNewKernel() && rootAvailable()
|
||||||
|
val kpmVersion = getKpmVersion()
|
||||||
|
val containerColor = MaterialTheme.colorScheme.surfaceVariant
|
||||||
|
val cardColor = MaterialTheme.colorScheme.surfaceVariant
|
||||||
|
|
||||||
|
NavigationBar(
|
||||||
|
modifier = Modifier.windowInsetsPadding(
|
||||||
|
WindowInsets.navigationBars.only(WindowInsetsSides.Horizontal)
|
||||||
|
),
|
||||||
|
containerColor = TopAppBarDefaults.topAppBarColors(
|
||||||
|
containerColor = cardColor.copy(alpha = cardAlpha),
|
||||||
|
scrolledContainerColor = containerColor.copy(alpha = cardAlpha)
|
||||||
|
).containerColor,
|
||||||
|
tonalElevation = cardElevation
|
||||||
|
) {
|
||||||
|
BottomBarDestination.entries.forEach { destination ->
|
||||||
|
if (destination == BottomBarDestination.Kpm) {
|
||||||
|
if (kpmVersion.isNotEmpty() && !kpmVersion.startsWith("Error")) {
|
||||||
|
if (!fullFeatured && destination.rootRequired) return@forEach
|
||||||
|
val isCurrentDestOnBackStack by navController.isRouteOnBackStackAsState(destination.direction)
|
||||||
|
NavigationBarItem(
|
||||||
|
selected = isCurrentDestOnBackStack,
|
||||||
|
onClick = {
|
||||||
|
if (!isCurrentDestOnBackStack) {
|
||||||
|
navigator.navigate(destination.direction) {
|
||||||
|
popUpTo(NavGraphs.root as RouteOrDirection) {
|
||||||
|
saveState = true
|
||||||
|
}
|
||||||
|
launchSingleTop = true
|
||||||
|
restoreState = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
icon = {
|
||||||
|
Icon(
|
||||||
|
imageVector = if (isCurrentDestOnBackStack) {
|
||||||
|
destination.iconSelected
|
||||||
|
} else {
|
||||||
|
destination.iconNotSelected
|
||||||
|
},
|
||||||
|
contentDescription = stringResource(destination.label),
|
||||||
|
tint = if (isCurrentDestOnBackStack) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
},
|
||||||
|
label = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(destination.label),
|
||||||
|
style = MaterialTheme.typography.labelMedium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!fullFeatured && destination.rootRequired) return@forEach
|
||||||
|
val isCurrentDestOnBackStack by navController.isRouteOnBackStackAsState(destination.direction)
|
||||||
|
NavigationBarItem(
|
||||||
|
selected = isCurrentDestOnBackStack,
|
||||||
|
onClick = {
|
||||||
|
if (!isCurrentDestOnBackStack) {
|
||||||
|
navigator.navigate(destination.direction) {
|
||||||
|
popUpTo(NavGraphs.root as RouteOrDirection) {
|
||||||
|
saveState = true
|
||||||
|
}
|
||||||
|
launchSingleTop = true
|
||||||
|
restoreState = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
icon = {
|
||||||
|
Icon(
|
||||||
|
imageVector = if (isCurrentDestOnBackStack) {
|
||||||
|
destination.iconSelected
|
||||||
|
} else {
|
||||||
|
destination.iconNotSelected
|
||||||
|
},
|
||||||
|
contentDescription = stringResource(destination.label),
|
||||||
|
tint = if (isCurrentDestOnBackStack) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
},
|
||||||
|
label = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(destination.label),
|
||||||
|
style = MaterialTheme.typography.labelMedium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package shirkneko.zako.sukisu.ui.component
|
package com.sukisu.ultra.ui.component
|
||||||
|
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@@ -31,8 +31,8 @@ import androidx.compose.ui.tooling.preview.Preview
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.compose.ui.window.Dialog
|
import androidx.compose.ui.window.Dialog
|
||||||
import shirkneko.zako.sukisu.BuildConfig
|
import com.sukisu.ultra.BuildConfig
|
||||||
import shirkneko.zako.sukisu.R
|
import com.sukisu.ultra.R
|
||||||
|
|
||||||
@Preview
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
@@ -98,7 +98,7 @@ private fun AboutCardContent() {
|
|||||||
val annotatedString = AnnotatedString.Companion.fromHtml(
|
val annotatedString = AnnotatedString.Companion.fromHtml(
|
||||||
htmlString = stringResource(
|
htmlString = stringResource(
|
||||||
id = R.string.about_source_code,
|
id = R.string.about_source_code,
|
||||||
"<b><a href=\"https://github.com/ShirkNeko/KernelSU\">GitHub</a></b>",
|
"<b><a href=\"https://github.com/ShirkNeko/SukiSU-Ultra\">GitHub</a></b>",
|
||||||
"<b><a href=\"https://t.me/SukiKSU\">Telegram</a></b>"
|
"<b><a href=\"https://t.me/SukiKSU\">Telegram</a></b>"
|
||||||
),
|
),
|
||||||
linkStyles = TextLinkStyles(
|
linkStyles = TextLinkStyles(
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package shirkneko.zako.sukisu.ui.component
|
package com.sukisu.ultra.ui.component
|
||||||
|
|
||||||
import android.graphics.text.LineBreaker
|
import android.graphics.text.LineBreaker
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
@@ -0,0 +1,223 @@
|
|||||||
|
package com.sukisu.ultra.ui.component
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.gestures.detectTransformGestures
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Check
|
||||||
|
import androidx.compose.material.icons.filled.Close
|
||||||
|
import androidx.compose.material.icons.filled.Fullscreen
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.window.Dialog
|
||||||
|
import androidx.compose.ui.window.DialogProperties
|
||||||
|
import coil.compose.AsyncImage
|
||||||
|
import coil.request.ImageRequest
|
||||||
|
import com.sukisu.ultra.R
|
||||||
|
import com.sukisu.ultra.ui.util.BackgroundTransformation
|
||||||
|
import com.sukisu.ultra.ui.util.saveTransformedBackground
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.ui.geometry.Size
|
||||||
|
import androidx.compose.ui.layout.onSizeChanged
|
||||||
|
import kotlin.math.max
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ImageEditorDialog(
|
||||||
|
imageUri: Uri,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
onConfirm: (Uri) -> Unit
|
||||||
|
) {
|
||||||
|
var scale by remember { mutableFloatStateOf(1f) }
|
||||||
|
var offsetX by remember { mutableFloatStateOf(0f) }
|
||||||
|
var offsetY by remember { mutableFloatStateOf(0f) }
|
||||||
|
val context = LocalContext.current
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
var lastScale by remember { mutableFloatStateOf(1f) }
|
||||||
|
var lastOffsetX by remember { mutableFloatStateOf(0f) }
|
||||||
|
var lastOffsetY by remember { mutableFloatStateOf(0f) }
|
||||||
|
var imageSize by remember { mutableStateOf(Size.Zero) }
|
||||||
|
var screenSize by remember { mutableStateOf(Size.Zero) }
|
||||||
|
val animatedScale by animateFloatAsState(
|
||||||
|
targetValue = scale,
|
||||||
|
label = "ScaleAnimation"
|
||||||
|
)
|
||||||
|
val animatedOffsetX by animateFloatAsState(
|
||||||
|
targetValue = offsetX,
|
||||||
|
label = "OffsetXAnimation"
|
||||||
|
)
|
||||||
|
val animatedOffsetY by animateFloatAsState(
|
||||||
|
targetValue = offsetY,
|
||||||
|
label = "OffsetYAnimation"
|
||||||
|
)
|
||||||
|
val updateTransformation = remember {
|
||||||
|
{ newScale: Float, newOffsetX: Float, newOffsetY: Float ->
|
||||||
|
val scaleDiff = kotlin.math.abs(newScale - lastScale)
|
||||||
|
val offsetXDiff = kotlin.math.abs(newOffsetX - lastOffsetX)
|
||||||
|
val offsetYDiff = kotlin.math.abs(newOffsetY - lastOffsetY)
|
||||||
|
if (scaleDiff > 0.01f || offsetXDiff > 1f || offsetYDiff > 1f) {
|
||||||
|
scale = newScale
|
||||||
|
offsetX = newOffsetX
|
||||||
|
offsetY = newOffsetY
|
||||||
|
lastScale = newScale
|
||||||
|
lastOffsetX = newOffsetX
|
||||||
|
lastOffsetY = newOffsetY
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val scaleToFullScreen = remember {
|
||||||
|
{
|
||||||
|
if (imageSize.height > 0 && screenSize.height > 0) {
|
||||||
|
val newScale = screenSize.height / imageSize.height
|
||||||
|
updateTransformation(newScale, 0f, 0f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Dialog(
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
properties = DialogProperties(
|
||||||
|
dismissOnBackPress = true,
|
||||||
|
dismissOnClickOutside = false,
|
||||||
|
usePlatformDefaultWidth = false
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(Color.Black.copy(alpha = 0.9f))
|
||||||
|
.onSizeChanged { size ->
|
||||||
|
screenSize = Size(size.width.toFloat(), size.height.toFloat())
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
AsyncImage(
|
||||||
|
model = ImageRequest.Builder(LocalContext.current)
|
||||||
|
.data(imageUri)
|
||||||
|
.crossfade(true)
|
||||||
|
.build(),
|
||||||
|
contentDescription = stringResource(R.string.settings_custom_background),
|
||||||
|
contentScale = ContentScale.Fit,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.graphicsLayer(
|
||||||
|
scaleX = animatedScale,
|
||||||
|
scaleY = animatedScale,
|
||||||
|
translationX = animatedOffsetX,
|
||||||
|
translationY = animatedOffsetY
|
||||||
|
)
|
||||||
|
.pointerInput(Unit) {
|
||||||
|
detectTransformGestures { _, pan, zoom, _ ->
|
||||||
|
scope.launch {
|
||||||
|
try {
|
||||||
|
val newScale = (scale * zoom).coerceIn(0.5f, 3f)
|
||||||
|
val maxOffsetX = max(0f, size.width * (newScale - 1) / 2)
|
||||||
|
val maxOffsetY = max(0f, size.height * (newScale - 1) / 2)
|
||||||
|
val newOffsetX = if (maxOffsetX > 0) {
|
||||||
|
(offsetX + pan.x).coerceIn(-maxOffsetX, maxOffsetX)
|
||||||
|
} else {
|
||||||
|
0f
|
||||||
|
}
|
||||||
|
val newOffsetY = if (maxOffsetY > 0) {
|
||||||
|
(offsetY + pan.y).coerceIn(-maxOffsetY, maxOffsetY)
|
||||||
|
} else {
|
||||||
|
0f
|
||||||
|
}
|
||||||
|
updateTransformation(newScale, newOffsetX, newOffsetY)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
updateTransformation(lastScale, lastOffsetX, lastOffsetY)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onSizeChanged { size ->
|
||||||
|
imageSize = Size(size.width.toFloat(), size.height.toFloat())
|
||||||
|
}
|
||||||
|
)
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp)
|
||||||
|
.align(Alignment.TopCenter),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
IconButton(
|
||||||
|
onClick = onDismiss,
|
||||||
|
modifier = Modifier
|
||||||
|
.clip(RoundedCornerShape(8.dp))
|
||||||
|
.background(Color.Black.copy(alpha = 0.6f))
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Close,
|
||||||
|
contentDescription = stringResource(R.string.cancel),
|
||||||
|
tint = Color.White
|
||||||
|
)
|
||||||
|
}
|
||||||
|
IconButton(
|
||||||
|
onClick = { scaleToFullScreen() },
|
||||||
|
modifier = Modifier
|
||||||
|
.clip(RoundedCornerShape(8.dp))
|
||||||
|
.background(Color.Black.copy(alpha = 0.6f))
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Fullscreen,
|
||||||
|
contentDescription = stringResource(R.string.reprovision),
|
||||||
|
tint = Color.White
|
||||||
|
)
|
||||||
|
}
|
||||||
|
IconButton(
|
||||||
|
onClick = {
|
||||||
|
scope.launch {
|
||||||
|
try {
|
||||||
|
val transformation = BackgroundTransformation(scale, offsetX, offsetY)
|
||||||
|
val savedUri = context.saveTransformedBackground(imageUri, transformation)
|
||||||
|
savedUri?.let { onConfirm(it) }
|
||||||
|
} catch (e: Exception) {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.clip(RoundedCornerShape(8.dp))
|
||||||
|
.background(Color.Black.copy(alpha = 0.6f))
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Check,
|
||||||
|
contentDescription = stringResource(R.string.confirm),
|
||||||
|
tint = Color.White
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp)
|
||||||
|
.clip(RoundedCornerShape(8.dp))
|
||||||
|
.background(Color.Black.copy(alpha = 0.6f))
|
||||||
|
.padding(16.dp)
|
||||||
|
.align(Alignment.BottomCenter)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.image_editor_hint),
|
||||||
|
color = Color.White,
|
||||||
|
style = MaterialTheme.typography.bodyMedium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package shirkneko.zako.sukisu.ui.component
|
package com.sukisu.ultra.ui.component
|
||||||
|
|
||||||
import androidx.compose.foundation.focusable
|
import androidx.compose.foundation.focusable
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package com.sukisu.ultra.ui.component
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import com.sukisu.ultra.Natives
|
||||||
|
import com.sukisu.ultra.ksuApp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun KsuIsValid(
|
||||||
|
content: @Composable () -> Unit
|
||||||
|
) {
|
||||||
|
val isManager = Natives.becomeManager(ksuApp.packageName)
|
||||||
|
val ksuVersion = if (isManager) Natives.version else null
|
||||||
|
|
||||||
|
if (ksuVersion != null) {
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package shirkneko.zako.sukisu.ui.component
|
package com.sukisu.ultra.ui.component
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
@@ -42,7 +42,7 @@ import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
|||||||
import androidx.compose.ui.text.input.ImeAction
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import shirkneko.zako.sukisu.ui.theme.CardConfig
|
import com.sukisu.ultra.ui.theme.CardConfig
|
||||||
|
|
||||||
private const val TAG = "SearchBar"
|
private const val TAG = "SearchBar"
|
||||||
|
|
||||||
@@ -63,7 +63,7 @@ fun SearchAppBar(
|
|||||||
var onSearch by remember { mutableStateOf(false) }
|
var onSearch by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
// 获取卡片颜色和透明度
|
// 获取卡片颜色和透明度
|
||||||
val cardColor = MaterialTheme.colorScheme.secondaryContainer
|
val cardColor = MaterialTheme.colorScheme.surfaceVariant
|
||||||
val cardAlpha = CardConfig.cardAlpha
|
val cardAlpha = CardConfig.cardAlpha
|
||||||
|
|
||||||
if (onSearch) {
|
if (onSearch) {
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package shirkneko.zako.sukisu.ui.component
|
package com.sukisu.ultra.ui.component
|
||||||
|
|
||||||
import androidx.compose.foundation.LocalIndication
|
import androidx.compose.foundation.LocalIndication
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
@@ -0,0 +1,248 @@
|
|||||||
|
package com.sukisu.ultra.ui.component
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.horizontalScroll
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.sukisu.ultra.R
|
||||||
|
import com.sukisu.ultra.ui.theme.ThemeConfig
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.SdStorage
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 槽位选择对话框组件
|
||||||
|
* 用于Kernel刷写时选择目标槽位
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun SlotSelectionDialog(
|
||||||
|
show: Boolean,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
onSlotSelected: (String) -> Unit
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
var currentSlot by remember { mutableStateOf<String?>(null) }
|
||||||
|
var errorMessage by remember { mutableStateOf<String?>(null) }
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
try {
|
||||||
|
currentSlot = getCurrentSlot(context)
|
||||||
|
errorMessage = null
|
||||||
|
} catch (e: Exception) {
|
||||||
|
errorMessage = e.message
|
||||||
|
currentSlot = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (show) {
|
||||||
|
val cardColor = if (!ThemeConfig.useDynamicColor) {
|
||||||
|
ThemeConfig.currentTheme.ButtonContrast
|
||||||
|
} else {
|
||||||
|
MaterialTheme.colorScheme.surfaceContainerHigh
|
||||||
|
}
|
||||||
|
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.select_slot_title),
|
||||||
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
|
)
|
||||||
|
},
|
||||||
|
text = {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(vertical = 8.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
) {
|
||||||
|
if (errorMessage != null) {
|
||||||
|
Text(
|
||||||
|
text = "Error: $errorMessage",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.error,
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Text(
|
||||||
|
text = stringResource(
|
||||||
|
id = R.string.current_slot,
|
||||||
|
currentSlot ?: "Unknown"
|
||||||
|
),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.select_slot_description),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
|
// Horizontal arrangement for slot options with highlighted current slot
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.horizontalScroll(rememberScrollState()),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
val slotOptions = listOf(
|
||||||
|
ListOption(
|
||||||
|
titleText = stringResource(id = R.string.slot_a),
|
||||||
|
subtitleText = if (currentSlot == "a" || currentSlot == "_a") stringResource(id = R.string.currently_selected) else null,
|
||||||
|
icon = Icons.Filled.SdStorage
|
||||||
|
),
|
||||||
|
ListOption(
|
||||||
|
titleText = stringResource(id = R.string.slot_b),
|
||||||
|
subtitleText = if (currentSlot == "b" || currentSlot == "_b") stringResource(id = R.string.currently_selected) else null,
|
||||||
|
icon = Icons.Filled.SdStorage
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
slotOptions.forEachIndexed { index, option ->
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.padding(horizontal = 8.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clip(MaterialTheme.shapes.medium)
|
||||||
|
.background(
|
||||||
|
color = if (option.subtitleText != null) {
|
||||||
|
MaterialTheme.colorScheme.primary.copy(alpha = 0.9f)
|
||||||
|
} else {
|
||||||
|
MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.clickable {
|
||||||
|
onSlotSelected(
|
||||||
|
when (index) {
|
||||||
|
0 -> "a"
|
||||||
|
else -> "b"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.padding(vertical = 12.dp, horizontal = 16.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = option.icon,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = if (option.subtitleText != null) {
|
||||||
|
MaterialTheme.colorScheme.onPrimary
|
||||||
|
} else {
|
||||||
|
MaterialTheme.colorScheme.primary
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(end = 16.dp)
|
||||||
|
.size(24.dp)
|
||||||
|
)
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = option.titleText,
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
color = if (option.subtitleText != null) {
|
||||||
|
MaterialTheme.colorScheme.onPrimary
|
||||||
|
} else {
|
||||||
|
MaterialTheme.colorScheme.primary
|
||||||
|
}
|
||||||
|
)
|
||||||
|
option.subtitleText?.let {
|
||||||
|
Text(
|
||||||
|
text = it,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = if (true) {
|
||||||
|
MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.8f)
|
||||||
|
} else {
|
||||||
|
MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
currentSlot?.let { onSlotSelected(it) }
|
||||||
|
onDismiss()
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(android.R.string.ok),
|
||||||
|
color = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(
|
||||||
|
onClick = onDismiss
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(android.R.string.cancel),
|
||||||
|
color = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
containerColor = cardColor,
|
||||||
|
shape = MaterialTheme.shapes.extraLarge,
|
||||||
|
tonalElevation = 4.dp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Data class for list options
|
||||||
|
data class ListOption(
|
||||||
|
val titleText: String,
|
||||||
|
val subtitleText: String?,
|
||||||
|
val icon: ImageVector
|
||||||
|
)
|
||||||
|
|
||||||
|
// Utility function to get current slot
|
||||||
|
private fun getCurrentSlot(context: Context): String? {
|
||||||
|
return runCommandGetOutput(true, "getprop ro.boot.slot_suffix")?.let {
|
||||||
|
if (it.startsWith("_")) it.substring(1) else it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun runCommandGetOutput(su: Boolean, cmd: String): String? {
|
||||||
|
return try {
|
||||||
|
val process = ProcessBuilder(if (su) "su" else "sh").start()
|
||||||
|
process.outputStream.bufferedWriter().use { writer ->
|
||||||
|
writer.write("$cmd\n")
|
||||||
|
writer.write("exit\n")
|
||||||
|
writer.flush()
|
||||||
|
}
|
||||||
|
process.inputStream.bufferedReader().use { reader ->
|
||||||
|
reader.readText().trim()
|
||||||
|
}
|
||||||
|
} catch (_: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
package com.sukisu.ultra.ui.component
|
||||||
|
|
||||||
|
import androidx.compose.animation.animateColorAsState
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.ListItem
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Switch
|
||||||
|
import androidx.compose.material3.SwitchDefaults
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SwitchItem(
|
||||||
|
icon: ImageVector,
|
||||||
|
title: String,
|
||||||
|
summary: String? = null,
|
||||||
|
checked: Boolean,
|
||||||
|
enabled: Boolean = true,
|
||||||
|
onCheckedChange: (Boolean) -> Unit
|
||||||
|
) {
|
||||||
|
// 颜色动画
|
||||||
|
val iconTint by animateColorAsState(
|
||||||
|
targetValue = if (checked) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f),
|
||||||
|
animationSpec = tween(300),
|
||||||
|
label = "iconTint"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 开关颜色
|
||||||
|
val switchColors = SwitchDefaults.colors(
|
||||||
|
checkedThumbColor = MaterialTheme.colorScheme.primary,
|
||||||
|
checkedTrackColor = MaterialTheme.colorScheme.primaryContainer,
|
||||||
|
checkedBorderColor = MaterialTheme.colorScheme.primary,
|
||||||
|
checkedIconColor = MaterialTheme.colorScheme.onPrimary,
|
||||||
|
uncheckedThumbColor = MaterialTheme.colorScheme.outline,
|
||||||
|
uncheckedTrackColor = MaterialTheme.colorScheme.surfaceVariant,
|
||||||
|
uncheckedBorderColor = MaterialTheme.colorScheme.outline,
|
||||||
|
uncheckedIconColor = MaterialTheme.colorScheme.surfaceVariant,
|
||||||
|
disabledCheckedThumbColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f),
|
||||||
|
disabledCheckedTrackColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f),
|
||||||
|
disabledCheckedBorderColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f),
|
||||||
|
disabledCheckedIconColor = MaterialTheme.colorScheme.surfaceVariant,
|
||||||
|
disabledUncheckedThumbColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f),
|
||||||
|
disabledUncheckedTrackColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f),
|
||||||
|
disabledUncheckedBorderColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f),
|
||||||
|
disabledUncheckedIconColor = MaterialTheme.colorScheme.surfaceVariant
|
||||||
|
)
|
||||||
|
|
||||||
|
ListItem(
|
||||||
|
headlineContent = {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
},
|
||||||
|
supportingContent = summary?.let {
|
||||||
|
{
|
||||||
|
Text(
|
||||||
|
text = it,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
maxLines = Int.MAX_VALUE,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
leadingContent = {
|
||||||
|
Icon(
|
||||||
|
imageVector = icon,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(24.dp),
|
||||||
|
tint = iconTint
|
||||||
|
)
|
||||||
|
},
|
||||||
|
trailingContent = {
|
||||||
|
Switch(
|
||||||
|
checked = checked,
|
||||||
|
onCheckedChange = null,
|
||||||
|
enabled = enabled,
|
||||||
|
colors = switchColors
|
||||||
|
)
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable(enabled = enabled) {
|
||||||
|
onCheckedChange(!checked)
|
||||||
|
}
|
||||||
|
.padding(vertical = 4.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package shirkneko.zako.sukisu.ui.component.profile
|
package com.sukisu.ultra.ui.component.profile
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.material3.OutlinedTextField
|
import androidx.compose.material3.OutlinedTextField
|
||||||
@@ -11,9 +11,9 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import shirkneko.zako.sukisu.Natives
|
import com.sukisu.ultra.Natives
|
||||||
import shirkneko.zako.sukisu.R
|
import com.sukisu.ultra.R
|
||||||
import shirkneko.zako.sukisu.ui.component.SwitchItem
|
import com.sukisu.ultra.ui.component.SwitchItem
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AppProfileConfig(
|
fun AppProfileConfig(
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package shirkneko.zako.sukisu.ui.component.profile
|
package com.sukisu.ultra.ui.component.profile
|
||||||
|
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@@ -42,12 +42,12 @@ import com.maxkeppeler.sheets.input.models.ValidationResult
|
|||||||
import com.maxkeppeler.sheets.list.ListDialog
|
import com.maxkeppeler.sheets.list.ListDialog
|
||||||
import com.maxkeppeler.sheets.list.models.ListOption
|
import com.maxkeppeler.sheets.list.models.ListOption
|
||||||
import com.maxkeppeler.sheets.list.models.ListSelection
|
import com.maxkeppeler.sheets.list.models.ListSelection
|
||||||
import shirkneko.zako.sukisu.Natives
|
import com.sukisu.ultra.Natives
|
||||||
import shirkneko.zako.sukisu.R
|
import com.sukisu.ultra.R
|
||||||
import shirkneko.zako.sukisu.profile.Capabilities
|
import com.sukisu.ultra.profile.Capabilities
|
||||||
import shirkneko.zako.sukisu.profile.Groups
|
import com.sukisu.ultra.profile.Groups
|
||||||
import shirkneko.zako.sukisu.ui.component.rememberCustomDialog
|
import com.sukisu.ultra.ui.component.rememberCustomDialog
|
||||||
import shirkneko.zako.sukisu.ui.util.isSepolicyValid
|
import com.sukisu.ultra.ui.util.isSepolicyValid
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package shirkneko.zako.sukisu.ui.component.profile
|
package com.sukisu.ultra.ui.component.profile
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
@@ -23,11 +23,11 @@ import androidx.compose.runtime.saveable.rememberSaveable
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import shirkneko.zako.sukisu.Natives
|
import com.sukisu.ultra.Natives
|
||||||
import shirkneko.zako.sukisu.R
|
import com.sukisu.ultra.R
|
||||||
import shirkneko.zako.sukisu.ui.util.listAppProfileTemplates
|
import com.sukisu.ultra.ui.util.listAppProfileTemplates
|
||||||
import shirkneko.zako.sukisu.ui.util.setSepolicy
|
import com.sukisu.ultra.ui.util.setSepolicy
|
||||||
import shirkneko.zako.sukisu.ui.viewmodel.getTemplateInfoById
|
import com.sukisu.ultra.ui.viewmodel.getTemplateInfoById
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author weishu
|
* @author weishu
|
||||||
@@ -0,0 +1,593 @@
|
|||||||
|
package com.sukisu.ultra.ui.screen
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.Crossfade
|
||||||
|
import androidx.compose.animation.expandVertically
|
||||||
|
import androidx.compose.animation.fadeIn
|
||||||
|
import androidx.compose.animation.fadeOut
|
||||||
|
import androidx.compose.animation.shrinkVertically
|
||||||
|
import androidx.compose.foundation.gestures.detectTapGestures
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
|
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.only
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.safeDrawing
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
|
import androidx.compose.material.icons.filled.AccountCircle
|
||||||
|
import androidx.compose.material.icons.filled.Android
|
||||||
|
import androidx.compose.material.icons.filled.Security
|
||||||
|
import androidx.compose.material3.DropdownMenu
|
||||||
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
|
import androidx.compose.material3.ElevatedCard
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.FilterChip
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.IconButtonDefaults
|
||||||
|
import androidx.compose.material3.ListItem
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.SnackbarHost
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TopAppBar
|
||||||
|
import androidx.compose.material3.TopAppBarColors
|
||||||
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
|
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.alpha
|
||||||
|
import androidx.compose.ui.draw.shadow
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.DpOffset
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.compose.dropUnlessResumed
|
||||||
|
import coil.compose.AsyncImage
|
||||||
|
import coil.request.ImageRequest
|
||||||
|
import com.ramcosta.composedestinations.annotation.Destination
|
||||||
|
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||||
|
import com.ramcosta.composedestinations.generated.destinations.AppProfileTemplateScreenDestination
|
||||||
|
import com.ramcosta.composedestinations.generated.destinations.TemplateEditorScreenDestination
|
||||||
|
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||||
|
import com.sukisu.ultra.Natives
|
||||||
|
import com.sukisu.ultra.R
|
||||||
|
import com.sukisu.ultra.ui.component.SwitchItem
|
||||||
|
import com.sukisu.ultra.ui.component.profile.AppProfileConfig
|
||||||
|
import com.sukisu.ultra.ui.component.profile.RootProfileConfig
|
||||||
|
import com.sukisu.ultra.ui.component.profile.TemplateConfig
|
||||||
|
import com.sukisu.ultra.ui.theme.CardConfig
|
||||||
|
import com.sukisu.ultra.ui.util.LocalSnackbarHost
|
||||||
|
import com.sukisu.ultra.ui.util.forceStopApp
|
||||||
|
import com.sukisu.ultra.ui.util.getSepolicy
|
||||||
|
import com.sukisu.ultra.ui.util.launchApp
|
||||||
|
import com.sukisu.ultra.ui.util.restartApp
|
||||||
|
import com.sukisu.ultra.ui.util.setSepolicy
|
||||||
|
import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel
|
||||||
|
import com.sukisu.ultra.ui.viewmodel.getTemplateInfoById
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author weishu
|
||||||
|
* @date 2023/5/16.
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Destination<RootGraph>
|
||||||
|
@Composable
|
||||||
|
fun AppProfileScreen(
|
||||||
|
navigator: DestinationsNavigator,
|
||||||
|
appInfo: SuperUserViewModel.AppInfo,
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val snackBarHost = LocalSnackbarHost.current
|
||||||
|
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
val failToUpdateAppProfile = stringResource(R.string.failed_to_update_app_profile).format(appInfo.label)
|
||||||
|
val failToUpdateSepolicy = stringResource(R.string.failed_to_update_sepolicy).format(appInfo.label)
|
||||||
|
val suNotAllowed = stringResource(R.string.su_not_allowed).format(appInfo.label)
|
||||||
|
|
||||||
|
val packageName = appInfo.packageName
|
||||||
|
val initialProfile = Natives.getAppProfile(packageName, appInfo.uid)
|
||||||
|
if (initialProfile.allowSu) {
|
||||||
|
initialProfile.rules = getSepolicy(packageName)
|
||||||
|
}
|
||||||
|
var profile by rememberSaveable {
|
||||||
|
mutableStateOf(initialProfile)
|
||||||
|
}
|
||||||
|
|
||||||
|
val cardColor = MaterialTheme.colorScheme.surfaceVariant
|
||||||
|
val cardAlpha = CardConfig.cardAlpha
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
TopBar(
|
||||||
|
title = appInfo.label,
|
||||||
|
packageName = packageName,
|
||||||
|
colors = TopAppBarDefaults.topAppBarColors(
|
||||||
|
containerColor = cardColor.copy(alpha = cardAlpha),
|
||||||
|
scrolledContainerColor = cardColor.copy(alpha = cardAlpha)
|
||||||
|
),
|
||||||
|
onBack = dropUnlessResumed { navigator.popBackStack() },
|
||||||
|
scrollBehavior = scrollBehavior
|
||||||
|
)
|
||||||
|
},
|
||||||
|
snackbarHost = { SnackbarHost(hostState = snackBarHost) },
|
||||||
|
contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal)
|
||||||
|
) { paddingValues ->
|
||||||
|
AppProfileInner(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(paddingValues)
|
||||||
|
.nestedScroll(scrollBehavior.nestedScrollConnection)
|
||||||
|
.verticalScroll(rememberScrollState()),
|
||||||
|
packageName = appInfo.packageName,
|
||||||
|
appLabel = appInfo.label,
|
||||||
|
appIcon = {
|
||||||
|
AsyncImage(
|
||||||
|
model = ImageRequest.Builder(context).data(appInfo.packageInfo).crossfade(true).build(),
|
||||||
|
contentDescription = appInfo.label,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(4.dp)
|
||||||
|
.width(48.dp)
|
||||||
|
.height(48.dp)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
profile = profile,
|
||||||
|
onViewTemplate = {
|
||||||
|
getTemplateInfoById(it)?.let { info ->
|
||||||
|
navigator.navigate(TemplateEditorScreenDestination(info))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onManageTemplate = {
|
||||||
|
navigator.navigate(AppProfileTemplateScreenDestination())
|
||||||
|
},
|
||||||
|
onProfileChange = {
|
||||||
|
scope.launch {
|
||||||
|
if (it.allowSu) {
|
||||||
|
// sync with allowlist.c - forbid_system_uid
|
||||||
|
if (appInfo.uid < 2000 && appInfo.uid != 1000) {
|
||||||
|
snackBarHost.showSnackbar(suNotAllowed)
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
if (!it.rootUseDefault && it.rules.isNotEmpty() && !setSepolicy(profile.name, it.rules)) {
|
||||||
|
snackBarHost.showSnackbar(failToUpdateSepolicy)
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!Natives.setAppProfile(it)) {
|
||||||
|
snackBarHost.showSnackbar(failToUpdateAppProfile.format(appInfo.uid))
|
||||||
|
} else {
|
||||||
|
profile = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun AppProfileInner(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
packageName: String,
|
||||||
|
appLabel: String,
|
||||||
|
appIcon: @Composable () -> Unit,
|
||||||
|
profile: Natives.Profile,
|
||||||
|
onViewTemplate: (id: String) -> Unit = {},
|
||||||
|
onManageTemplate: () -> Unit = {},
|
||||||
|
onProfileChange: (Natives.Profile) -> Unit,
|
||||||
|
) {
|
||||||
|
val isRootGranted = profile.allowSu
|
||||||
|
|
||||||
|
Column(modifier = modifier) {
|
||||||
|
ElevatedCard(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||||
|
shape = MaterialTheme.shapes.medium
|
||||||
|
) {
|
||||||
|
AppMenuBox(packageName) {
|
||||||
|
ListItem(
|
||||||
|
headlineContent = {
|
||||||
|
Text(
|
||||||
|
text = appLabel,
|
||||||
|
style = MaterialTheme.typography.titleMedium
|
||||||
|
)
|
||||||
|
},
|
||||||
|
supportingContent = {
|
||||||
|
Text(
|
||||||
|
text = packageName,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
},
|
||||||
|
leadingContent = appIcon,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ElevatedCard(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||||
|
shape = MaterialTheme.shapes.medium
|
||||||
|
) {
|
||||||
|
SwitchItem(
|
||||||
|
icon = Icons.Filled.Security,
|
||||||
|
title = stringResource(id = R.string.superuser),
|
||||||
|
checked = isRootGranted,
|
||||||
|
onCheckedChange = { onProfileChange(profile.copy(allowSu = it)) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Crossfade(
|
||||||
|
targetState = isRootGranted,
|
||||||
|
label = "RootAccess"
|
||||||
|
) { current ->
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(bottom = 6.dp + 48.dp + 6.dp /* SnackBar height */)
|
||||||
|
) {
|
||||||
|
if (current) {
|
||||||
|
val initialMode = if (profile.rootUseDefault) {
|
||||||
|
Mode.Default
|
||||||
|
} else if (profile.rootTemplate != null) {
|
||||||
|
Mode.Template
|
||||||
|
} else {
|
||||||
|
Mode.Custom
|
||||||
|
}
|
||||||
|
var mode by rememberSaveable {
|
||||||
|
mutableStateOf(initialMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
ElevatedCard(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||||
|
shape = MaterialTheme.shapes.medium
|
||||||
|
) {
|
||||||
|
ProfileBox(mode, true) {
|
||||||
|
// template mode shouldn't change profile here!
|
||||||
|
if (it == Mode.Default || it == Mode.Custom) {
|
||||||
|
onProfileChange(profile.copy(rootUseDefault = it == Mode.Default))
|
||||||
|
}
|
||||||
|
mode = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = mode != Mode.Default,
|
||||||
|
enter = fadeIn() + expandVertically(),
|
||||||
|
exit = fadeOut() + shrinkVertically()
|
||||||
|
) {
|
||||||
|
ElevatedCard(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||||
|
shape = MaterialTheme.shapes.medium
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.padding(vertical = 8.dp)) {
|
||||||
|
Crossfade(targetState = mode, label = "ProfileMode") { currentMode ->
|
||||||
|
when (currentMode) {
|
||||||
|
Mode.Template -> {
|
||||||
|
TemplateConfig(
|
||||||
|
profile = profile,
|
||||||
|
onViewTemplate = onViewTemplate,
|
||||||
|
onManageTemplate = onManageTemplate,
|
||||||
|
onProfileChange = onProfileChange
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Mode.Custom -> {
|
||||||
|
RootProfileConfig(
|
||||||
|
fixedName = true,
|
||||||
|
profile = profile,
|
||||||
|
onProfileChange = onProfileChange
|
||||||
|
)
|
||||||
|
}
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val mode = if (profile.nonRootUseDefault) Mode.Default else Mode.Custom
|
||||||
|
|
||||||
|
ElevatedCard(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||||
|
shape = MaterialTheme.shapes.medium
|
||||||
|
) {
|
||||||
|
ProfileBox(mode, false) {
|
||||||
|
onProfileChange(profile.copy(nonRootUseDefault = (it == Mode.Default)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = mode == Mode.Custom,
|
||||||
|
enter = fadeIn() + expandVertically(),
|
||||||
|
exit = fadeOut() + shrinkVertically()
|
||||||
|
) {
|
||||||
|
ElevatedCard(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||||
|
shape = MaterialTheme.shapes.medium
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.padding(vertical = 8.dp)) {
|
||||||
|
AppProfileConfig(
|
||||||
|
fixedName = true,
|
||||||
|
profile = profile,
|
||||||
|
enabled = mode == Mode.Custom,
|
||||||
|
onProfileChange = onProfileChange
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum class Mode(@StringRes private val res: Int) {
|
||||||
|
Default(R.string.profile_default), Template(R.string.profile_template), Custom(R.string.profile_custom);
|
||||||
|
|
||||||
|
val text: String
|
||||||
|
@Composable get() = stringResource(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
private fun TopBar(
|
||||||
|
title: String,
|
||||||
|
packageName: String,
|
||||||
|
onBack: () -> Unit,
|
||||||
|
colors: TopAppBarColors,
|
||||||
|
scrollBehavior: TopAppBarScrollBehavior? = null
|
||||||
|
) {
|
||||||
|
TopAppBar(
|
||||||
|
title = {
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = packageName,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
modifier = Modifier.alpha(0.8f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
colors = colors,
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(
|
||||||
|
onClick = onBack,
|
||||||
|
colors = IconButtonDefaults.iconButtonColors(
|
||||||
|
contentColor = MaterialTheme.colorScheme.onSurface
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||||
|
contentDescription = stringResource(R.string.back)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
windowInsets = WindowInsets.safeDrawing.only(
|
||||||
|
WindowInsetsSides.Top + WindowInsetsSides.Horizontal
|
||||||
|
),
|
||||||
|
scrollBehavior = scrollBehavior,
|
||||||
|
modifier = Modifier.shadow(
|
||||||
|
elevation = if ((scrollBehavior?.state?.overlappedFraction ?: 0f) > 0.01f)
|
||||||
|
4.dp else 0.dp,
|
||||||
|
spotColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ProfileBox(
|
||||||
|
mode: Mode,
|
||||||
|
hasTemplate: Boolean,
|
||||||
|
onModeChange: (Mode) -> Unit,
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.padding(vertical = 8.dp)) {
|
||||||
|
ListItem(
|
||||||
|
headlineContent = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.profile),
|
||||||
|
style = MaterialTheme.typography.titleMedium
|
||||||
|
)
|
||||||
|
},
|
||||||
|
supportingContent = {
|
||||||
|
Text(
|
||||||
|
text = mode.text,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
},
|
||||||
|
leadingContent = {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.AccountCircle,
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
HorizontalDivider(
|
||||||
|
thickness = Dp.Hairline,
|
||||||
|
color = MaterialTheme.colorScheme.outlineVariant
|
||||||
|
)
|
||||||
|
|
||||||
|
ListItem(
|
||||||
|
headlineContent = {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 8.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally)
|
||||||
|
) {
|
||||||
|
FilterChip(
|
||||||
|
selected = mode == Mode.Default,
|
||||||
|
onClick = { onModeChange(Mode.Default) },
|
||||||
|
label = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.profile_default),
|
||||||
|
style = MaterialTheme.typography.bodyMedium
|
||||||
|
)
|
||||||
|
},
|
||||||
|
shape = MaterialTheme.shapes.small
|
||||||
|
)
|
||||||
|
|
||||||
|
if (hasTemplate) {
|
||||||
|
FilterChip(
|
||||||
|
selected = mode == Mode.Template,
|
||||||
|
onClick = { onModeChange(Mode.Template) },
|
||||||
|
label = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.profile_template),
|
||||||
|
style = MaterialTheme.typography.bodyMedium
|
||||||
|
)
|
||||||
|
},
|
||||||
|
shape = MaterialTheme.shapes.small
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
FilterChip(
|
||||||
|
selected = mode == Mode.Custom,
|
||||||
|
onClick = { onModeChange(Mode.Custom) },
|
||||||
|
label = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.profile_custom),
|
||||||
|
style = MaterialTheme.typography.bodyMedium
|
||||||
|
)
|
||||||
|
},
|
||||||
|
shape = MaterialTheme.shapes.small
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("UnusedBoxWithConstraintsScope")
|
||||||
|
@Composable
|
||||||
|
private fun AppMenuBox(packageName: String, content: @Composable () -> Unit) {
|
||||||
|
var expanded by remember { mutableStateOf(false) }
|
||||||
|
var touchPoint: Offset by remember { mutableStateOf(Offset.Zero) }
|
||||||
|
val density = LocalDensity.current
|
||||||
|
|
||||||
|
BoxWithConstraints(
|
||||||
|
Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.pointerInput(Unit) {
|
||||||
|
detectTapGestures(
|
||||||
|
onLongPress = {
|
||||||
|
touchPoint = it
|
||||||
|
expanded = true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
content()
|
||||||
|
|
||||||
|
val (offsetX, offsetY) = with(density) {
|
||||||
|
(touchPoint.x.toDp()) to (touchPoint.y.toDp())
|
||||||
|
}
|
||||||
|
|
||||||
|
DropdownMenu(
|
||||||
|
expanded = expanded,
|
||||||
|
offset = DpOffset(offsetX, -offsetY),
|
||||||
|
onDismissRequest = {
|
||||||
|
expanded = false
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
AppMenuOption(
|
||||||
|
text = stringResource(id = R.string.launch_app),
|
||||||
|
onClick = {
|
||||||
|
expanded = false
|
||||||
|
launchApp(packageName)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
AppMenuOption(
|
||||||
|
text = stringResource(id = R.string.force_stop_app),
|
||||||
|
onClick = {
|
||||||
|
expanded = false
|
||||||
|
forceStopApp(packageName)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
AppMenuOption(
|
||||||
|
text = stringResource(id = R.string.restart_app),
|
||||||
|
onClick = {
|
||||||
|
expanded = false
|
||||||
|
restartApp(packageName)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun AppMenuOption(text: String, onClick: () -> Unit) {
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = {
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
style = MaterialTheme.typography.bodyMedium
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onClick = onClick
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
private fun AppProfilePreview() {
|
||||||
|
var profile by remember { mutableStateOf(Natives.Profile("")) }
|
||||||
|
Surface {
|
||||||
|
AppProfileInner(
|
||||||
|
packageName = "icu.nullptr.test",
|
||||||
|
appLabel = "Test",
|
||||||
|
appIcon = {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.Android,
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
profile = profile,
|
||||||
|
onProfileChange = {
|
||||||
|
profile = it
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package shirkneko.zako.sukisu.ui.screen
|
package com.sukisu.ultra.ui.screen
|
||||||
|
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
@@ -11,7 +11,7 @@ import com.ramcosta.composedestinations.generated.destinations.SuperUserScreenDe
|
|||||||
import com.ramcosta.composedestinations.generated.destinations.SettingScreenDestination
|
import com.ramcosta.composedestinations.generated.destinations.SettingScreenDestination
|
||||||
import com.ramcosta.composedestinations.generated.destinations.KpmScreenDestination
|
import com.ramcosta.composedestinations.generated.destinations.KpmScreenDestination
|
||||||
import com.ramcosta.composedestinations.spec.DirectionDestinationSpec
|
import com.ramcosta.composedestinations.spec.DirectionDestinationSpec
|
||||||
import shirkneko.zako.sukisu.R
|
import com.sukisu.ultra.R
|
||||||
|
|
||||||
enum class BottomBarDestination(
|
enum class BottomBarDestination(
|
||||||
val direction: DirectionDestinationSpec,
|
val direction: DirectionDestinationSpec,
|
||||||
@@ -21,8 +21,8 @@ enum class BottomBarDestination(
|
|||||||
val rootRequired: Boolean,
|
val rootRequired: Boolean,
|
||||||
) {
|
) {
|
||||||
Home(HomeScreenDestination, R.string.home, Icons.Filled.Home, Icons.Outlined.Home, false),
|
Home(HomeScreenDestination, R.string.home, Icons.Filled.Home, Icons.Outlined.Home, false),
|
||||||
|
Kpm(KpmScreenDestination, R.string.kpm_title, Icons.Filled.Build, Icons.Outlined.Build, true),
|
||||||
SuperUser(SuperUserScreenDestination, R.string.superuser, Icons.Filled.Security, Icons.Outlined.Security, true),
|
SuperUser(SuperUserScreenDestination, R.string.superuser, Icons.Filled.Security, Icons.Outlined.Security, true),
|
||||||
Module(ModuleScreenDestination, R.string.module, Icons.Filled.Apps, Icons.Outlined.Apps, true),
|
Module(ModuleScreenDestination, R.string.module, Icons.Filled.Apps, Icons.Outlined.Apps, true),
|
||||||
Kpm(KpmScreenDestination, R.string.kpm_title, Icons.Filled.Build, Icons.Outlined.Build, true),
|
|
||||||
Settings(SettingScreenDestination, R.string.settings, Icons.Filled.Settings, Icons.Outlined.Settings, false),
|
Settings(SettingScreenDestination, R.string.settings, Icons.Filled.Settings, Icons.Outlined.Settings, false),
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package shirkneko.zako.sukisu.ui.screen
|
package com.sukisu.ultra.ui.screen
|
||||||
|
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@@ -37,10 +37,10 @@ import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import shirkneko.zako.sukisu.R
|
import com.sukisu.ultra.R
|
||||||
import shirkneko.zako.sukisu.ui.component.KeyEventBlocker
|
import com.sukisu.ultra.ui.component.KeyEventBlocker
|
||||||
import shirkneko.zako.sukisu.ui.util.LocalSnackbarHost
|
import com.sukisu.ultra.ui.util.LocalSnackbarHost
|
||||||
import shirkneko.zako.sukisu.ui.util.runModuleAction
|
import com.sukisu.ultra.ui.util.runModuleAction
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package shirkneko.zako.sukisu.ui.screen
|
package com.sukisu.ultra.ui.screen
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
@@ -30,9 +30,9 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import shirkneko.zako.sukisu.R
|
import com.sukisu.ultra.ui.component.KeyEventBlocker
|
||||||
import shirkneko.zako.sukisu.ui.component.KeyEventBlocker
|
import com.sukisu.ultra.ui.util.*
|
||||||
import shirkneko.zako.sukisu.ui.util.*
|
import com.sukisu.ultra.R
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
@@ -45,10 +45,6 @@ enum class FlashingStatus {
|
|||||||
|
|
||||||
private var currentFlashingStatus = mutableStateOf(FlashingStatus.FLASHING)
|
private var currentFlashingStatus = mutableStateOf(FlashingStatus.FLASHING)
|
||||||
|
|
||||||
fun getFlashingStatus(): FlashingStatus {
|
|
||||||
return currentFlashingStatus.value
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setFlashingStatus(status: FlashingStatus) {
|
fun setFlashingStatus(status: FlashingStatus) {
|
||||||
currentFlashingStatus.value = status
|
currentFlashingStatus.value = status
|
||||||
}
|
}
|
||||||
1024
manager/app/src/main/java/com/sukisu/ultra/ui/screen/Home.kt
Normal file
1024
manager/app/src/main/java/com/sukisu/ultra/ui/screen/Home.kt
Normal file
File diff suppressed because it is too large
Load Diff
788
manager/app/src/main/java/com/sukisu/ultra/ui/screen/Install.kt
Normal file
788
manager/app/src/main/java/com/sukisu/ultra/ui/screen/Install.kt
Normal file
@@ -0,0 +1,788 @@
|
|||||||
|
package com.sukisu.ultra.ui.screen
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.expandVertically
|
||||||
|
import androidx.compose.animation.fadeIn
|
||||||
|
import androidx.compose.animation.fadeOut
|
||||||
|
import androidx.compose.animation.shrinkVertically
|
||||||
|
import androidx.compose.foundation.LocalIndication
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
|
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.only
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.safeDrawing
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.selection.toggleable
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
|
import androidx.compose.material.icons.filled.AutoFixHigh
|
||||||
|
import androidx.compose.material.icons.filled.FileUpload
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.ButtonDefaults
|
||||||
|
import androidx.compose.material3.CardDefaults
|
||||||
|
import androidx.compose.material3.ElevatedCard
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.ListItem
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.RadioButton
|
||||||
|
import androidx.compose.material3.RadioButtonDefaults
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.material3.TopAppBar
|
||||||
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
|
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||||
|
import androidx.compose.material3.rememberTopAppBarState
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.produceState
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.draw.shadow
|
||||||
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.semantics.Role
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.maxkeppeker.sheets.core.models.base.Header
|
||||||
|
import com.maxkeppeker.sheets.core.models.base.rememberUseCaseState
|
||||||
|
import com.maxkeppeler.sheets.list.ListDialog
|
||||||
|
import com.maxkeppeler.sheets.list.models.ListOption
|
||||||
|
import com.maxkeppeler.sheets.list.models.ListSelection
|
||||||
|
import com.ramcosta.composedestinations.annotation.Destination
|
||||||
|
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||||
|
import com.ramcosta.composedestinations.generated.destinations.FlashScreenDestination
|
||||||
|
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||||
|
import com.ramcosta.composedestinations.navigation.EmptyDestinationsNavigator
|
||||||
|
import com.sukisu.ultra.R
|
||||||
|
import com.sukisu.ultra.flash.HorizonKernelFlashProgress
|
||||||
|
import com.sukisu.ultra.flash.HorizonKernelState
|
||||||
|
import com.sukisu.ultra.flash.HorizonKernelWorker
|
||||||
|
import com.sukisu.ultra.ui.component.DialogHandle
|
||||||
|
import com.sukisu.ultra.ui.component.SlotSelectionDialog
|
||||||
|
import com.sukisu.ultra.ui.component.rememberConfirmDialog
|
||||||
|
import com.sukisu.ultra.ui.component.rememberCustomDialog
|
||||||
|
import com.sukisu.ultra.ui.theme.CardConfig.cardAlpha
|
||||||
|
import com.sukisu.ultra.ui.theme.CardConfig.cardElevation
|
||||||
|
import com.sukisu.ultra.ui.theme.getCardColors
|
||||||
|
import com.sukisu.ultra.ui.util.LkmSelection
|
||||||
|
import com.sukisu.ultra.ui.util.getCurrentKmi
|
||||||
|
import com.sukisu.ultra.ui.util.getSupportedKmis
|
||||||
|
import com.sukisu.ultra.ui.util.isAbDevice
|
||||||
|
import com.sukisu.ultra.ui.util.isInitBoot
|
||||||
|
import com.sukisu.ultra.ui.util.rootAvailable
|
||||||
|
import com.sukisu.ultra.getKernelVersion
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author weishu
|
||||||
|
* @date 2024/3/12.
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Destination<RootGraph>
|
||||||
|
@Composable
|
||||||
|
fun InstallScreen(navigator: DestinationsNavigator) {
|
||||||
|
var installMethod by remember { mutableStateOf<InstallMethod?>(null) }
|
||||||
|
var lkmSelection by remember { mutableStateOf<LkmSelection>(LkmSelection.KmiNone) }
|
||||||
|
val context = LocalContext.current
|
||||||
|
var showRebootDialog by remember { mutableStateOf(false) }
|
||||||
|
var showSlotSelectionDialog by remember { mutableStateOf(false) }
|
||||||
|
var tempKernelUri by remember { mutableStateOf<Uri?>(null) }
|
||||||
|
val horizonKernelState = remember { HorizonKernelState() }
|
||||||
|
val flashState by horizonKernelState.state.collectAsState()
|
||||||
|
val summary = stringResource(R.string.horizon_kernel_summary)
|
||||||
|
val kernelVersion = getKernelVersion()
|
||||||
|
val isGKI = kernelVersion.isGKI()
|
||||||
|
val isAbDevice = isAbDevice()
|
||||||
|
|
||||||
|
val onFlashComplete = {
|
||||||
|
showRebootDialog = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showRebootDialog) {
|
||||||
|
RebootDialog(
|
||||||
|
show = true,
|
||||||
|
onDismiss = { showRebootDialog = false },
|
||||||
|
onConfirm = {
|
||||||
|
showRebootDialog = false
|
||||||
|
try {
|
||||||
|
val process = Runtime.getRuntime().exec("su")
|
||||||
|
process.outputStream.bufferedWriter().use { writer ->
|
||||||
|
writer.write("svc power reboot\n")
|
||||||
|
writer.write("exit\n")
|
||||||
|
}
|
||||||
|
} catch (_: Exception) {
|
||||||
|
Toast.makeText(context, R.string.failed_reboot, Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val onInstall = {
|
||||||
|
installMethod?.let { method ->
|
||||||
|
when (method) {
|
||||||
|
is InstallMethod.HorizonKernel -> {
|
||||||
|
method.uri?.let { uri ->
|
||||||
|
val worker = HorizonKernelWorker(
|
||||||
|
context = context,
|
||||||
|
state = horizonKernelState,
|
||||||
|
slot = method.slot
|
||||||
|
)
|
||||||
|
worker.uri = uri
|
||||||
|
worker.setOnFlashCompleteListener(onFlashComplete)
|
||||||
|
worker.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
val flashIt = FlashIt.FlashBoot(
|
||||||
|
boot = if (method is InstallMethod.SelectFile) method.uri else null,
|
||||||
|
lkm = lkmSelection,
|
||||||
|
ota = method is InstallMethod.DirectInstallToInactiveSlot
|
||||||
|
)
|
||||||
|
navigator.navigate(FlashScreenDestination(flashIt))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Unit
|
||||||
|
}
|
||||||
|
|
||||||
|
// 槽位选择
|
||||||
|
SlotSelectionDialog(
|
||||||
|
show = showSlotSelectionDialog && isAbDevice,
|
||||||
|
onDismiss = { showSlotSelectionDialog = false },
|
||||||
|
onSlotSelected = { slot ->
|
||||||
|
showSlotSelectionDialog = false
|
||||||
|
val horizonMethod = InstallMethod.HorizonKernel(
|
||||||
|
uri = tempKernelUri,
|
||||||
|
slot = slot,
|
||||||
|
summary = summary
|
||||||
|
)
|
||||||
|
installMethod = horizonMethod
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
val currentKmi by produceState(initialValue = "") {
|
||||||
|
value = getCurrentKmi()
|
||||||
|
}
|
||||||
|
|
||||||
|
val selectKmiDialog = rememberSelectKmiDialog { kmi ->
|
||||||
|
kmi?.let {
|
||||||
|
lkmSelection = LkmSelection.KmiString(it)
|
||||||
|
onInstall()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val onClickNext = {
|
||||||
|
if (isGKI && lkmSelection == LkmSelection.KmiNone && currentKmi.isBlank()) {
|
||||||
|
selectKmiDialog.show()
|
||||||
|
} else {
|
||||||
|
onInstall()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val selectLkmLauncher = rememberLauncherForActivityResult(
|
||||||
|
contract = ActivityResultContracts.StartActivityForResult()
|
||||||
|
) {
|
||||||
|
if (it.resultCode == Activity.RESULT_OK) {
|
||||||
|
it.data?.data?.let { uri ->
|
||||||
|
lkmSelection = LkmSelection.LkmUri(uri)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val onLkmUpload = {
|
||||||
|
selectLkmLauncher.launch(Intent(Intent.ACTION_GET_CONTENT).apply {
|
||||||
|
type = "application/octet-stream"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
TopBar(
|
||||||
|
onBack = { navigator.popBackStack() },
|
||||||
|
onLkmUpload = onLkmUpload,
|
||||||
|
scrollBehavior = scrollBehavior
|
||||||
|
)
|
||||||
|
},
|
||||||
|
contentWindowInsets = WindowInsets.safeDrawing.only(
|
||||||
|
WindowInsetsSides.Top + WindowInsetsSides.Horizontal
|
||||||
|
)
|
||||||
|
) { innerPadding ->
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(innerPadding)
|
||||||
|
.nestedScroll(scrollBehavior.nestedScrollConnection)
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
.padding(top = 12.dp)
|
||||||
|
) {
|
||||||
|
SelectInstallMethod(
|
||||||
|
isGKI = isGKI,
|
||||||
|
isAbDevice = isAbDevice,
|
||||||
|
onSelected = { method ->
|
||||||
|
if (method is InstallMethod.HorizonKernel && method.uri != null) {
|
||||||
|
if (isAbDevice) {
|
||||||
|
tempKernelUri = method.uri
|
||||||
|
showSlotSelectionDialog = true
|
||||||
|
} else {
|
||||||
|
installMethod = method
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
installMethod = method
|
||||||
|
}
|
||||||
|
horizonKernelState.reset()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = flashState.isFlashing && installMethod is InstallMethod.HorizonKernel,
|
||||||
|
enter = fadeIn() + expandVertically(),
|
||||||
|
exit = fadeOut() + shrinkVertically()
|
||||||
|
) {
|
||||||
|
HorizonKernelFlashProgress(flashState)
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp)
|
||||||
|
) {
|
||||||
|
(lkmSelection as? LkmSelection.LkmUri)?.let {
|
||||||
|
ElevatedCard(
|
||||||
|
colors = getCardColors(MaterialTheme.colorScheme.surfaceVariant),
|
||||||
|
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(bottom = 12.dp)
|
||||||
|
.clip(MaterialTheme.shapes.medium)
|
||||||
|
.shadow(
|
||||||
|
elevation = cardElevation,
|
||||||
|
shape = MaterialTheme.shapes.medium,
|
||||||
|
spotColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(
|
||||||
|
id = R.string.selected_lkm,
|
||||||
|
it.uri.lastPathSegment ?: "(file)"
|
||||||
|
),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
modifier = Modifier.padding(16.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(installMethod as? InstallMethod.HorizonKernel)?.let { method ->
|
||||||
|
if (method.slot != null) {
|
||||||
|
ElevatedCard(
|
||||||
|
colors = getCardColors(MaterialTheme.colorScheme.surfaceVariant),
|
||||||
|
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(bottom = 12.dp)
|
||||||
|
.clip(MaterialTheme.shapes.medium)
|
||||||
|
.shadow(
|
||||||
|
elevation = cardElevation,
|
||||||
|
shape = MaterialTheme.shapes.medium,
|
||||||
|
spotColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(
|
||||||
|
id = R.string.selected_slot,
|
||||||
|
if (method.slot == "a") stringResource(id = R.string.slot_a)
|
||||||
|
else stringResource(id = R.string.slot_b)
|
||||||
|
),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
modifier = Modifier.padding(16.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Button(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
enabled = installMethod != null && !flashState.isFlashing,
|
||||||
|
onClick = onClickNext,
|
||||||
|
shape = MaterialTheme.shapes.medium,
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.primary,
|
||||||
|
contentColor = MaterialTheme.colorScheme.onPrimary,
|
||||||
|
disabledContainerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.6f),
|
||||||
|
disabledContentColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
stringResource(id = R.string.install_next),
|
||||||
|
style = MaterialTheme.typography.bodyMedium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun RebootDialog(
|
||||||
|
show: Boolean,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
onConfirm: () -> Unit
|
||||||
|
) {
|
||||||
|
if (show) {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
title = { Text(stringResource(id = R.string.reboot_complete_title)) },
|
||||||
|
text = { Text(stringResource(id = R.string.reboot_complete_msg)) },
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(onClick = onConfirm) {
|
||||||
|
Text(stringResource(id = R.string.yes))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = onDismiss) {
|
||||||
|
Text(stringResource(id = R.string.no))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class InstallMethod {
|
||||||
|
data class SelectFile(
|
||||||
|
val uri: Uri? = null,
|
||||||
|
@StringRes override val label: Int = R.string.select_file,
|
||||||
|
override val summary: String?
|
||||||
|
) : InstallMethod()
|
||||||
|
|
||||||
|
data object DirectInstall : InstallMethod() {
|
||||||
|
override val label: Int
|
||||||
|
get() = R.string.direct_install
|
||||||
|
}
|
||||||
|
|
||||||
|
data object DirectInstallToInactiveSlot : InstallMethod() {
|
||||||
|
override val label: Int
|
||||||
|
get() = R.string.install_inactive_slot
|
||||||
|
}
|
||||||
|
|
||||||
|
data class HorizonKernel(
|
||||||
|
val uri: Uri? = null,
|
||||||
|
val slot: String? = null,
|
||||||
|
@StringRes override val label: Int = R.string.horizon_kernel,
|
||||||
|
override val summary: String? = null
|
||||||
|
) : InstallMethod()
|
||||||
|
|
||||||
|
abstract val label: Int
|
||||||
|
open val summary: String? = null
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun SelectInstallMethod(
|
||||||
|
isGKI: Boolean = false,
|
||||||
|
isAbDevice: Boolean = false,
|
||||||
|
onSelected: (InstallMethod) -> Unit = {}
|
||||||
|
) {
|
||||||
|
val rootAvailable = rootAvailable()
|
||||||
|
val isAbDevice = isAbDevice()
|
||||||
|
val horizonKernelSummary = stringResource(R.string.horizon_kernel_summary)
|
||||||
|
val selectFileTip = stringResource(
|
||||||
|
id = R.string.select_file_tip,
|
||||||
|
if (isInitBoot()) "init_boot" else "boot"
|
||||||
|
)
|
||||||
|
|
||||||
|
val radioOptions = mutableListOf<InstallMethod>(
|
||||||
|
InstallMethod.SelectFile(summary = selectFileTip)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (rootAvailable) {
|
||||||
|
radioOptions.add(InstallMethod.DirectInstall)
|
||||||
|
if (isAbDevice) {
|
||||||
|
radioOptions.add(InstallMethod.DirectInstallToInactiveSlot)
|
||||||
|
}
|
||||||
|
radioOptions.add(InstallMethod.HorizonKernel(summary = horizonKernelSummary))
|
||||||
|
}
|
||||||
|
|
||||||
|
var selectedOption by remember { mutableStateOf<InstallMethod?>(null) }
|
||||||
|
var currentSelectingMethod by remember { mutableStateOf<InstallMethod?>(null) }
|
||||||
|
|
||||||
|
val selectImageLauncher = rememberLauncherForActivityResult(
|
||||||
|
contract = ActivityResultContracts.StartActivityForResult()
|
||||||
|
) {
|
||||||
|
if (it.resultCode == Activity.RESULT_OK) {
|
||||||
|
it.data?.data?.let { uri ->
|
||||||
|
val option = when (currentSelectingMethod) {
|
||||||
|
is InstallMethod.SelectFile -> InstallMethod.SelectFile(
|
||||||
|
uri,
|
||||||
|
summary = selectFileTip
|
||||||
|
)
|
||||||
|
|
||||||
|
is InstallMethod.HorizonKernel -> InstallMethod.HorizonKernel(
|
||||||
|
uri,
|
||||||
|
summary = horizonKernelSummary
|
||||||
|
)
|
||||||
|
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
option?.let {
|
||||||
|
selectedOption = it
|
||||||
|
onSelected(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val confirmDialog = rememberConfirmDialog(
|
||||||
|
onConfirm = {
|
||||||
|
selectedOption = InstallMethod.DirectInstallToInactiveSlot
|
||||||
|
onSelected(InstallMethod.DirectInstallToInactiveSlot)
|
||||||
|
},
|
||||||
|
onDismiss = null
|
||||||
|
)
|
||||||
|
|
||||||
|
val dialogTitle = stringResource(id = android.R.string.dialog_alert_title)
|
||||||
|
val dialogContent = stringResource(id = R.string.install_inactive_slot_warning)
|
||||||
|
|
||||||
|
val onClick = { option: InstallMethod ->
|
||||||
|
currentSelectingMethod = option
|
||||||
|
when (option) {
|
||||||
|
is InstallMethod.SelectFile, is InstallMethod.HorizonKernel -> {
|
||||||
|
selectImageLauncher.launch(Intent(Intent.ACTION_GET_CONTENT).apply {
|
||||||
|
type = "application/*"
|
||||||
|
putExtra(
|
||||||
|
Intent.EXTRA_MIME_TYPES,
|
||||||
|
arrayOf("application/octet-stream", "application/zip")
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
is InstallMethod.DirectInstall -> {
|
||||||
|
selectedOption = option
|
||||||
|
onSelected(option)
|
||||||
|
}
|
||||||
|
|
||||||
|
is InstallMethod.DirectInstallToInactiveSlot -> {
|
||||||
|
confirmDialog.showConfirm(dialogTitle, dialogContent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var LKMExpanded by remember { mutableStateOf(false) }
|
||||||
|
var GKIExpanded by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp)
|
||||||
|
) {
|
||||||
|
// LKM 安装/修补
|
||||||
|
if (isGKI) {
|
||||||
|
ElevatedCard(
|
||||||
|
colors = getCardColors(MaterialTheme.colorScheme.surfaceVariant),
|
||||||
|
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(bottom = 12.dp)
|
||||||
|
.clip(MaterialTheme.shapes.large)
|
||||||
|
.shadow(
|
||||||
|
elevation = cardElevation,
|
||||||
|
shape = MaterialTheme.shapes.large,
|
||||||
|
spotColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
ListItem(
|
||||||
|
leadingContent = {
|
||||||
|
Icon(
|
||||||
|
Icons.Filled.AutoFixHigh,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
},
|
||||||
|
headlineContent = {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.Lkm_install_methods),
|
||||||
|
style = MaterialTheme.typography.titleMedium
|
||||||
|
)
|
||||||
|
},
|
||||||
|
modifier = Modifier.clickable {
|
||||||
|
LKMExpanded = !LKMExpanded
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = LKMExpanded,
|
||||||
|
enter = fadeIn() + expandVertically(),
|
||||||
|
exit = shrinkVertically() + fadeOut()
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(
|
||||||
|
start = 16.dp,
|
||||||
|
end = 16.dp,
|
||||||
|
bottom = 16.dp
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
radioOptions.take(3).forEach { option ->
|
||||||
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
|
Surface(
|
||||||
|
color = if (option.javaClass == selectedOption?.javaClass)
|
||||||
|
MaterialTheme.colorScheme.secondaryContainer.copy(alpha = cardAlpha)
|
||||||
|
else
|
||||||
|
MaterialTheme.colorScheme.surfaceContainerHighest.copy(alpha = cardAlpha),
|
||||||
|
shape = MaterialTheme.shapes.medium,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 4.dp)
|
||||||
|
.clip(MaterialTheme.shapes.medium)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.toggleable(
|
||||||
|
value = option.javaClass == selectedOption?.javaClass,
|
||||||
|
onValueChange = { onClick(option) },
|
||||||
|
role = Role.RadioButton,
|
||||||
|
indication = LocalIndication.current,
|
||||||
|
interactionSource = interactionSource
|
||||||
|
)
|
||||||
|
.padding(vertical = 8.dp, horizontal = 12.dp)
|
||||||
|
) {
|
||||||
|
RadioButton(
|
||||||
|
selected = option.javaClass == selectedOption?.javaClass,
|
||||||
|
onClick = null,
|
||||||
|
interactionSource = interactionSource,
|
||||||
|
colors = RadioButtonDefaults.colors(
|
||||||
|
selectedColor = MaterialTheme.colorScheme.primary,
|
||||||
|
unselectedColor = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(start = 10.dp)
|
||||||
|
.weight(1f)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = option.label),
|
||||||
|
style = MaterialTheme.typography.bodyLarge
|
||||||
|
)
|
||||||
|
option.summary?.let {
|
||||||
|
Text(
|
||||||
|
text = it,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// anykernel3 刷写
|
||||||
|
if (rootAvailable) {
|
||||||
|
ElevatedCard(
|
||||||
|
colors = getCardColors(MaterialTheme.colorScheme.surfaceVariant),
|
||||||
|
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(bottom = 12.dp)
|
||||||
|
.clip(MaterialTheme.shapes.large)
|
||||||
|
.shadow(
|
||||||
|
elevation = cardElevation,
|
||||||
|
shape = MaterialTheme.shapes.large,
|
||||||
|
spotColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
ListItem(
|
||||||
|
leadingContent = {
|
||||||
|
Icon(
|
||||||
|
Icons.Filled.FileUpload,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
},
|
||||||
|
headlineContent = {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.GKI_install_methods),
|
||||||
|
style = MaterialTheme.typography.titleMedium
|
||||||
|
)
|
||||||
|
},
|
||||||
|
modifier = Modifier.clickable {
|
||||||
|
GKIExpanded = !GKIExpanded
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = GKIExpanded,
|
||||||
|
enter = fadeIn() + expandVertically(),
|
||||||
|
exit = shrinkVertically() + fadeOut()
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(
|
||||||
|
start = 16.dp,
|
||||||
|
end = 16.dp,
|
||||||
|
bottom = 16.dp
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
radioOptions.filterIsInstance<InstallMethod.HorizonKernel>().forEach { option ->
|
||||||
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
|
Surface(
|
||||||
|
color = if (option.javaClass == selectedOption?.javaClass)
|
||||||
|
MaterialTheme.colorScheme.secondaryContainer.copy(alpha = cardAlpha)
|
||||||
|
else
|
||||||
|
MaterialTheme.colorScheme.surfaceContainerHighest.copy(alpha = cardAlpha),
|
||||||
|
shape = MaterialTheme.shapes.medium,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 4.dp)
|
||||||
|
.clip(MaterialTheme.shapes.medium)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.toggleable(
|
||||||
|
value = option.javaClass == selectedOption?.javaClass,
|
||||||
|
onValueChange = { onClick(option) },
|
||||||
|
role = Role.RadioButton,
|
||||||
|
indication = LocalIndication.current,
|
||||||
|
interactionSource = interactionSource
|
||||||
|
)
|
||||||
|
.padding(vertical = 8.dp, horizontal = 12.dp)
|
||||||
|
) {
|
||||||
|
RadioButton(
|
||||||
|
selected = option.javaClass == selectedOption?.javaClass,
|
||||||
|
onClick = null,
|
||||||
|
interactionSource = interactionSource,
|
||||||
|
colors = RadioButtonDefaults.colors(
|
||||||
|
selectedColor = MaterialTheme.colorScheme.primary,
|
||||||
|
unselectedColor = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(start = 10.dp)
|
||||||
|
.weight(1f)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = option.label),
|
||||||
|
style = MaterialTheme.typography.bodyLarge
|
||||||
|
)
|
||||||
|
option.summary?.let {
|
||||||
|
Text(
|
||||||
|
text = it,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun rememberSelectKmiDialog(onSelected: (String?) -> Unit): DialogHandle {
|
||||||
|
return rememberCustomDialog { dismiss ->
|
||||||
|
val supportedKmi by produceState(initialValue = emptyList<String>()) {
|
||||||
|
value = getSupportedKmis()
|
||||||
|
}
|
||||||
|
val options = supportedKmi.map { value ->
|
||||||
|
ListOption(
|
||||||
|
titleText = value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
var selection by remember { mutableStateOf<String?>(null) }
|
||||||
|
val backgroundColor = MaterialTheme.colorScheme.surfaceContainerHighest
|
||||||
|
|
||||||
|
MaterialTheme(
|
||||||
|
colorScheme = MaterialTheme.colorScheme.copy(
|
||||||
|
surface = backgroundColor
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
ListDialog(state = rememberUseCaseState(visible = true, onFinishedRequest = {
|
||||||
|
onSelected(selection)
|
||||||
|
}, onCloseRequest = {
|
||||||
|
dismiss()
|
||||||
|
}), header = Header.Default(
|
||||||
|
title = stringResource(R.string.select_kmi),
|
||||||
|
), selection = ListSelection.Single(
|
||||||
|
showRadioButtons = true,
|
||||||
|
options = options,
|
||||||
|
) { _, option ->
|
||||||
|
selection = option.titleText
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
private fun TopBar(
|
||||||
|
onBack: () -> Unit = {},
|
||||||
|
onLkmUpload: () -> Unit = {},
|
||||||
|
scrollBehavior: TopAppBarScrollBehavior? = null
|
||||||
|
) {
|
||||||
|
val cardColor = MaterialTheme.colorScheme.surfaceVariant
|
||||||
|
val cardAlpha = cardAlpha
|
||||||
|
|
||||||
|
TopAppBar(
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.install),
|
||||||
|
style = MaterialTheme.typography.titleLarge
|
||||||
|
)
|
||||||
|
},
|
||||||
|
colors = TopAppBarDefaults.topAppBarColors(
|
||||||
|
containerColor = cardColor.copy(alpha = cardAlpha),
|
||||||
|
scrolledContainerColor = cardColor.copy(alpha = cardAlpha)
|
||||||
|
),
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onClick = onBack) {
|
||||||
|
Icon(
|
||||||
|
Icons.AutoMirrored.Filled.ArrowBack,
|
||||||
|
contentDescription = stringResource(R.string.back)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
windowInsets = WindowInsets.safeDrawing.only(
|
||||||
|
WindowInsetsSides.Top + WindowInsetsSides.Horizontal
|
||||||
|
),
|
||||||
|
scrollBehavior = scrollBehavior
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun SelectInstallPreview() {
|
||||||
|
InstallScreen(EmptyDestinationsNavigator)
|
||||||
|
}
|
||||||
783
manager/app/src/main/java/com/sukisu/ultra/ui/screen/Kpm.kt
Normal file
783
manager/app/src/main/java/com/sukisu/ultra/ui/screen/Kpm.kt
Normal file
@@ -0,0 +1,783 @@
|
|||||||
|
package com.sukisu.ultra.ui.screen
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.*
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.draw.shadow
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import com.ramcosta.composedestinations.annotation.Destination
|
||||||
|
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||||
|
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import com.sukisu.ultra.ui.component.*
|
||||||
|
import com.sukisu.ultra.ui.theme.*
|
||||||
|
import com.sukisu.ultra.ui.viewmodel.KpmViewModel
|
||||||
|
import com.sukisu.ultra.ui.util.*
|
||||||
|
import java.io.File
|
||||||
|
import androidx.core.content.edit
|
||||||
|
import com.sukisu.ultra.R
|
||||||
|
import java.io.BufferedReader
|
||||||
|
import java.io.FileInputStream
|
||||||
|
import java.io.InputStreamReader
|
||||||
|
import java.net.*
|
||||||
|
import android.app.Activity
|
||||||
|
import com.sukisu.ultra.ui.theme.CardConfig.cardElevation
|
||||||
|
|
||||||
|
/**
|
||||||
|
* KPM 管理界面
|
||||||
|
* 以下内核模块功能由KernelPatch开发,经过修改后加入SukiSU Ultra的内核模块功能
|
||||||
|
* 开发者:ShirkNeko, Liaokong
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Destination<RootGraph>
|
||||||
|
@Composable
|
||||||
|
fun KpmScreen(
|
||||||
|
navigator: DestinationsNavigator,
|
||||||
|
viewModel: KpmViewModel = viewModel()
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
val snackBarHost = remember { SnackbarHostState() }
|
||||||
|
val confirmDialog = rememberConfirmDialog()
|
||||||
|
|
||||||
|
val moduleConfirmContentMap = viewModel.moduleList.associate { module ->
|
||||||
|
val moduleFileName = module.id
|
||||||
|
module.id to stringResource(R.string.confirm_uninstall_content, moduleFileName)
|
||||||
|
}
|
||||||
|
|
||||||
|
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||||
|
|
||||||
|
val kpmInstallSuccess = stringResource(R.string.kpm_install_success)
|
||||||
|
val kpmInstallFailed = stringResource(R.string.kpm_install_failed)
|
||||||
|
val cancel = stringResource(R.string.cancel)
|
||||||
|
val uninstall = stringResource(R.string.uninstall)
|
||||||
|
val failedToCheckModuleFile = stringResource(R.string.snackbar_failed_to_check_module_file)
|
||||||
|
val kpmUninstallSuccess = stringResource(R.string.kpm_uninstall_success)
|
||||||
|
val kpmUninstallFailed = stringResource(R.string.kpm_uninstall_failed)
|
||||||
|
val kpmInstallMode = stringResource(R.string.kpm_install_mode)
|
||||||
|
val kpmInstallModeLoad = stringResource(R.string.kpm_install_mode_load)
|
||||||
|
val kpmInstallModeEmbed = stringResource(R.string.kpm_install_mode_embed)
|
||||||
|
val invalidFileTypeMessage = stringResource(R.string.invalid_file_type)
|
||||||
|
val confirmTitle = stringResource(R.string.confirm_uninstall_title_with_filename)
|
||||||
|
|
||||||
|
var tempFileForInstall by remember { mutableStateOf<File?>(null) }
|
||||||
|
val installModeDialog = rememberCustomDialog { dismiss ->
|
||||||
|
var moduleName by remember { mutableStateOf<String?>(null) }
|
||||||
|
|
||||||
|
LaunchedEffect(tempFileForInstall) {
|
||||||
|
tempFileForInstall?.let { tempFile ->
|
||||||
|
try {
|
||||||
|
val command = arrayOf("su", "-c", "strings ${tempFile.absolutePath} | grep 'name='")
|
||||||
|
val process = Runtime.getRuntime().exec(command)
|
||||||
|
val inputStream = process.inputStream
|
||||||
|
val reader = BufferedReader(InputStreamReader(inputStream))
|
||||||
|
var line: String?
|
||||||
|
while (reader.readLine().also { line = it } != null) {
|
||||||
|
if (line!!.startsWith("name=")) {
|
||||||
|
moduleName = line.substringAfter("name=").trim()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
process.waitFor()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("KsuCli", "Failed to get module name: ${e.message}", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = {
|
||||||
|
dismiss()
|
||||||
|
tempFileForInstall?.delete()
|
||||||
|
tempFileForInstall = null
|
||||||
|
},
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
text = kpmInstallMode,
|
||||||
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
|
)
|
||||||
|
},
|
||||||
|
text = {
|
||||||
|
Column {
|
||||||
|
moduleName?.let {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.kpm_install_mode_description, it),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
scope.launch {
|
||||||
|
dismiss()
|
||||||
|
tempFileForInstall?.let { tempFile ->
|
||||||
|
handleModuleInstall(
|
||||||
|
tempFile = tempFile,
|
||||||
|
isEmbed = false,
|
||||||
|
viewModel = viewModel,
|
||||||
|
snackBarHost = snackBarHost,
|
||||||
|
kpmInstallSuccess = kpmInstallSuccess,
|
||||||
|
kpmInstallFailed = kpmInstallFailed
|
||||||
|
)
|
||||||
|
}
|
||||||
|
tempFileForInstall = null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.Download,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(18.dp).padding(end = 4.dp)
|
||||||
|
)
|
||||||
|
Text(kpmInstallModeLoad)
|
||||||
|
}
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
scope.launch {
|
||||||
|
dismiss()
|
||||||
|
tempFileForInstall?.let { tempFile ->
|
||||||
|
handleModuleInstall(
|
||||||
|
tempFile = tempFile,
|
||||||
|
isEmbed = true,
|
||||||
|
viewModel = viewModel,
|
||||||
|
snackBarHost = snackBarHost,
|
||||||
|
kpmInstallSuccess = kpmInstallSuccess,
|
||||||
|
kpmInstallFailed = kpmInstallFailed
|
||||||
|
)
|
||||||
|
}
|
||||||
|
tempFileForInstall = null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.secondary
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.Inventory,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(18.dp).padding(end = 4.dp)
|
||||||
|
)
|
||||||
|
Text(kpmInstallModeEmbed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
dismiss()
|
||||||
|
tempFileForInstall?.delete()
|
||||||
|
tempFileForInstall = null
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text(cancel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
|
||||||
|
shape = MaterialTheme.shapes.extraLarge
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val selectPatchLauncher = rememberLauncherForActivityResult(
|
||||||
|
contract = ActivityResultContracts.StartActivityForResult()
|
||||||
|
) { result ->
|
||||||
|
if (result.resultCode != Activity.RESULT_OK) return@rememberLauncherForActivityResult
|
||||||
|
|
||||||
|
val uri = result.data?.data ?: return@rememberLauncherForActivityResult
|
||||||
|
|
||||||
|
scope.launch {
|
||||||
|
val fileName = uri.lastPathSegment ?: "unknown.kpm"
|
||||||
|
val encodedFileName = URLEncoder.encode(fileName, "UTF-8")
|
||||||
|
val tempFile = File(context.cacheDir, encodedFileName)
|
||||||
|
|
||||||
|
context.contentResolver.openInputStream(uri)?.use { input ->
|
||||||
|
tempFile.outputStream().use { output ->
|
||||||
|
input.copyTo(output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val mimeType = context.contentResolver.getType(uri)
|
||||||
|
val isCorrectMimeType = mimeType == null || mimeType.contains("application/octet-stream")
|
||||||
|
|
||||||
|
if (!isCorrectMimeType) {
|
||||||
|
var shouldShowSnackbar = true
|
||||||
|
try {
|
||||||
|
val matchCount = checkStringsCommand(tempFile)
|
||||||
|
val isElf = isElfFile(tempFile)
|
||||||
|
|
||||||
|
if (matchCount >= 1 || isElf) {
|
||||||
|
shouldShowSnackbar = false
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("KsuCli", "Failed to execute checks: ${e.message}", e)
|
||||||
|
}
|
||||||
|
if (shouldShowSnackbar) {
|
||||||
|
snackBarHost.showSnackbar(
|
||||||
|
message = invalidFileTypeMessage,
|
||||||
|
duration = SnackbarDuration.Short
|
||||||
|
)
|
||||||
|
}
|
||||||
|
tempFile.delete()
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
tempFileForInstall = tempFile
|
||||||
|
installModeDialog.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
while(true) {
|
||||||
|
viewModel.fetchModuleList()
|
||||||
|
delay(5000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val sharedPreferences = context.getSharedPreferences("app_preferences", Context.MODE_PRIVATE)
|
||||||
|
var isNoticeClosed by remember { mutableStateOf(sharedPreferences.getBoolean("is_notice_closed", false)) }
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
SearchAppBar(
|
||||||
|
title = { Text(stringResource(R.string.kpm_title)) },
|
||||||
|
searchText = viewModel.search,
|
||||||
|
onSearchTextChange = { viewModel.search = it },
|
||||||
|
onClearClick = { viewModel.search = "" },
|
||||||
|
scrollBehavior = scrollBehavior,
|
||||||
|
dropdownContent = {
|
||||||
|
IconButton(
|
||||||
|
onClick = { viewModel.fetchModuleList() }
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.Refresh,
|
||||||
|
contentDescription = stringResource(R.string.refresh),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
floatingActionButton = {
|
||||||
|
ExtendedFloatingActionButton(
|
||||||
|
onClick = {
|
||||||
|
selectPatchLauncher.launch(
|
||||||
|
Intent(Intent.ACTION_GET_CONTENT).apply {
|
||||||
|
type = "application/octet-stream"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
icon = {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.Add,
|
||||||
|
contentDescription = stringResource(R.string.kpm_install),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
text = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.kpm_install),
|
||||||
|
color = MaterialTheme.colorScheme.onPrimaryContainer
|
||||||
|
)
|
||||||
|
},
|
||||||
|
contentColor = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||||
|
expanded = true,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
snackbarHost = { SnackbarHost(snackBarHost) }
|
||||||
|
) { padding ->
|
||||||
|
Column(modifier = Modifier.padding(padding)) {
|
||||||
|
if (!isNoticeClosed) {
|
||||||
|
Card(
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.secondaryContainer
|
||||||
|
),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp)
|
||||||
|
.clip(MaterialTheme.shapes.medium)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.Info,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(end = 16.dp)
|
||||||
|
.size(24.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.kernel_module_notice),
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSecondaryContainer
|
||||||
|
)
|
||||||
|
|
||||||
|
IconButton(
|
||||||
|
onClick = {
|
||||||
|
isNoticeClosed = true
|
||||||
|
sharedPreferences.edit { putBoolean("is_notice_closed", true) }
|
||||||
|
},
|
||||||
|
modifier = Modifier.size(24.dp),
|
||||||
|
colors = IconButtonDefaults.iconButtonColors(
|
||||||
|
contentColor = MaterialTheme.colorScheme.onSecondaryContainer
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.Close,
|
||||||
|
contentDescription = stringResource(R.string.close_notice)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (viewModel.moduleList.isEmpty()) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.Code,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.6f),
|
||||||
|
modifier = Modifier
|
||||||
|
.size(96.dp)
|
||||||
|
.padding(bottom = 16.dp)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.kpm_empty),
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
) {
|
||||||
|
items(viewModel.moduleList) { module ->
|
||||||
|
KpmModuleItem(
|
||||||
|
module = module,
|
||||||
|
onUninstall = {
|
||||||
|
scope.launch {
|
||||||
|
val confirmContent = moduleConfirmContentMap[module.id] ?: ""
|
||||||
|
handleModuleUninstall(
|
||||||
|
module = module,
|
||||||
|
viewModel = viewModel,
|
||||||
|
snackBarHost = snackBarHost,
|
||||||
|
kpmUninstallSuccess = kpmUninstallSuccess,
|
||||||
|
kpmUninstallFailed = kpmUninstallFailed,
|
||||||
|
failedToCheckModuleFile = failedToCheckModuleFile,
|
||||||
|
uninstall = uninstall,
|
||||||
|
cancel = cancel,
|
||||||
|
confirmDialog = confirmDialog,
|
||||||
|
confirmTitle = confirmTitle,
|
||||||
|
confirmContent = confirmContent
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onControl = {
|
||||||
|
viewModel.loadModuleDetail(module.id)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun handleModuleInstall(
|
||||||
|
tempFile: File,
|
||||||
|
isEmbed: Boolean,
|
||||||
|
viewModel: KpmViewModel,
|
||||||
|
snackBarHost: SnackbarHostState,
|
||||||
|
kpmInstallSuccess: String,
|
||||||
|
kpmInstallFailed: String
|
||||||
|
) {
|
||||||
|
var moduleId: String? = null
|
||||||
|
try {
|
||||||
|
val command = arrayOf("su", "-c", "strings ${tempFile.absolutePath} | grep 'name='")
|
||||||
|
val process = Runtime.getRuntime().exec(command)
|
||||||
|
val inputStream = process.inputStream
|
||||||
|
val reader = BufferedReader(InputStreamReader(inputStream))
|
||||||
|
var line: String?
|
||||||
|
while (reader.readLine().also { line = it } != null) {
|
||||||
|
if (line!!.startsWith("name=")) {
|
||||||
|
moduleId = line.substringAfter("name=").trim()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
process.waitFor()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("KsuCli", "Failed to get module ID from strings command: ${e.message}", e)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (moduleId == null || moduleId.isEmpty()) {
|
||||||
|
Log.e("KsuCli", "Failed to extract module ID from file: ${tempFile.name}")
|
||||||
|
snackBarHost.showSnackbar(
|
||||||
|
message = kpmInstallFailed,
|
||||||
|
duration = SnackbarDuration.Short
|
||||||
|
)
|
||||||
|
tempFile.delete()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val targetPath = "/data/adb/kpm/$moduleId.kpm"
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (isEmbed) {
|
||||||
|
Runtime.getRuntime().exec(arrayOf("su", "-c", "mkdir -p /data/adb/kpm")).waitFor()
|
||||||
|
Runtime.getRuntime().exec(arrayOf("su", "-c", "cp ${tempFile.absolutePath} $targetPath")).waitFor()
|
||||||
|
}
|
||||||
|
|
||||||
|
val loadResult = loadKpmModule(tempFile.absolutePath)
|
||||||
|
if (loadResult.startsWith("Error")) {
|
||||||
|
Log.e("KsuCli", "Failed to load KPM module: $loadResult")
|
||||||
|
snackBarHost.showSnackbar(
|
||||||
|
message = kpmInstallFailed,
|
||||||
|
duration = SnackbarDuration.Short
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
viewModel.fetchModuleList()
|
||||||
|
snackBarHost.showSnackbar(
|
||||||
|
message = kpmInstallSuccess,
|
||||||
|
duration = SnackbarDuration.Short
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("KsuCli", "Failed to load KPM module: ${e.message}", e)
|
||||||
|
snackBarHost.showSnackbar(
|
||||||
|
message = kpmInstallFailed,
|
||||||
|
duration = SnackbarDuration.Short
|
||||||
|
)
|
||||||
|
}
|
||||||
|
tempFile.delete()
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun handleModuleUninstall(
|
||||||
|
module: KpmViewModel.ModuleInfo,
|
||||||
|
viewModel: KpmViewModel,
|
||||||
|
snackBarHost: SnackbarHostState,
|
||||||
|
kpmUninstallSuccess: String,
|
||||||
|
kpmUninstallFailed: String,
|
||||||
|
failedToCheckModuleFile: String,
|
||||||
|
uninstall: String,
|
||||||
|
cancel: String,
|
||||||
|
confirmTitle : String,
|
||||||
|
confirmContent : String,
|
||||||
|
confirmDialog: ConfirmDialogHandle
|
||||||
|
) {
|
||||||
|
val moduleFileName = "${module.id}.kpm"
|
||||||
|
val moduleFilePath = "/data/adb/kpm/$moduleFileName"
|
||||||
|
|
||||||
|
val fileExists = try {
|
||||||
|
val result = Runtime.getRuntime().exec(arrayOf("su", "-c", "ls /data/adb/kpm/$moduleFileName")).waitFor() == 0
|
||||||
|
result
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("KsuCli", "Failed to check module file existence: ${e.message}", e)
|
||||||
|
snackBarHost.showSnackbar(
|
||||||
|
message = failedToCheckModuleFile,
|
||||||
|
duration = SnackbarDuration.Short
|
||||||
|
)
|
||||||
|
false
|
||||||
|
}
|
||||||
|
val confirmResult = confirmDialog.awaitConfirm(
|
||||||
|
title = confirmTitle,
|
||||||
|
content = confirmContent,
|
||||||
|
confirm = uninstall,
|
||||||
|
dismiss = cancel
|
||||||
|
)
|
||||||
|
|
||||||
|
if (confirmResult == ConfirmResult.Confirmed) {
|
||||||
|
try {
|
||||||
|
val unloadResult = unloadKpmModule(module.id)
|
||||||
|
if (unloadResult.startsWith("Error")) {
|
||||||
|
Log.e("KsuCli", "Failed to unload KPM module: $unloadResult")
|
||||||
|
snackBarHost.showSnackbar(
|
||||||
|
message = kpmUninstallFailed,
|
||||||
|
duration = SnackbarDuration.Short
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fileExists) {
|
||||||
|
Runtime.getRuntime().exec(arrayOf("su", "-c", "rm $moduleFilePath")).waitFor()
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel.fetchModuleList()
|
||||||
|
snackBarHost.showSnackbar(
|
||||||
|
message = kpmUninstallSuccess,
|
||||||
|
duration = SnackbarDuration.Short
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("KsuCli", "Failed to unload KPM module: ${e.message}", e)
|
||||||
|
snackBarHost.showSnackbar(
|
||||||
|
message = kpmUninstallFailed,
|
||||||
|
duration = SnackbarDuration.Short
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun KpmModuleItem(
|
||||||
|
module: KpmViewModel.ModuleInfo,
|
||||||
|
onUninstall: () -> Unit,
|
||||||
|
onControl: () -> Unit
|
||||||
|
) {
|
||||||
|
val viewModel: KpmViewModel = viewModel()
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
val snackBarHost = remember { SnackbarHostState() }
|
||||||
|
val successMessage = stringResource(R.string.kpm_control_success)
|
||||||
|
val failureMessage = stringResource(R.string.kpm_control_failed)
|
||||||
|
|
||||||
|
if (viewModel.showInputDialog && viewModel.selectedModuleId == module.id) {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = { viewModel.hideInputDialog() },
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.kpm_control),
|
||||||
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
|
)
|
||||||
|
},
|
||||||
|
text = {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = viewModel.inputArgs,
|
||||||
|
onValueChange = { viewModel.updateInputArgs(it) },
|
||||||
|
label = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.kpm_args),
|
||||||
|
color = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
},
|
||||||
|
placeholder = {
|
||||||
|
Text(
|
||||||
|
text = module.args,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors = OutlinedTextFieldDefaults.colors(
|
||||||
|
focusedBorderColor = MaterialTheme.colorScheme.primary,
|
||||||
|
unfocusedBorderColor = MaterialTheme.colorScheme.outline
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
scope.launch {
|
||||||
|
val result = viewModel.executeControl()
|
||||||
|
val message = when (result) {
|
||||||
|
0 -> successMessage
|
||||||
|
else -> failureMessage
|
||||||
|
}
|
||||||
|
snackBarHost.showSnackbar(message)
|
||||||
|
onControl()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.confirm),
|
||||||
|
color = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = { viewModel.hideInputDialog() }) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.cancel),
|
||||||
|
color = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
|
||||||
|
shape = MaterialTheme.shapes.extraLarge
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Card(
|
||||||
|
colors = getCardColors(MaterialTheme.colorScheme.surfaceContainerHigh),
|
||||||
|
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clip(MaterialTheme.shapes.large)
|
||||||
|
.shadow(
|
||||||
|
elevation = cardElevation,
|
||||||
|
shape = MaterialTheme.shapes.large,
|
||||||
|
spotColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(20.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
|
Text(
|
||||||
|
text = module.name,
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "${stringResource(R.string.kpm_version)}: ${module.version}",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "${stringResource(R.string.kpm_author)}: ${module.author}",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "${stringResource(R.string.kpm_args)}: ${module.args}",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = module.description,
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(20.dp))
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
Button(
|
||||||
|
onClick = { viewModel.showInputDialog(module.id) },
|
||||||
|
enabled = module.hasAction,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.primary,
|
||||||
|
disabledContainerColor = MaterialTheme.colorScheme.surfaceVariant
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.Settings,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(20.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Text(stringResource(R.string.kpm_control))
|
||||||
|
}
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = onUninstall,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.error
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.Delete,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(20.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Text(stringResource(R.string.kpm_uninstall))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkStringsCommand(tempFile: File): Int {
|
||||||
|
val command = arrayOf("su", "-c", "strings ${tempFile.absolutePath} | grep -E 'name=|version=|license=|author='")
|
||||||
|
val process = Runtime.getRuntime().exec(command)
|
||||||
|
val inputStream = process.inputStream
|
||||||
|
val reader = BufferedReader(InputStreamReader(inputStream))
|
||||||
|
var line: String?
|
||||||
|
var matchCount = 0
|
||||||
|
val keywords = listOf("name=", "version=", "license=", "author=")
|
||||||
|
var nameExists = false
|
||||||
|
|
||||||
|
while (reader.readLine().also { line = it } != null) {
|
||||||
|
if (!nameExists && line!!.startsWith("name=")) {
|
||||||
|
nameExists = true
|
||||||
|
matchCount++
|
||||||
|
} else if (nameExists) {
|
||||||
|
for (keyword in keywords) {
|
||||||
|
if (line!!.startsWith(keyword)) {
|
||||||
|
matchCount++
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
process.waitFor()
|
||||||
|
|
||||||
|
return if (nameExists) matchCount else 0
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isElfFile(tempFile: File): Boolean {
|
||||||
|
val elfMagic = byteArrayOf(0x7F, 'E'.code.toByte(), 'L'.code.toByte(), 'F'.code.toByte())
|
||||||
|
val fileBytes = ByteArray(4)
|
||||||
|
FileInputStream(tempFile).use { input ->
|
||||||
|
input.read(fileBytes)
|
||||||
|
}
|
||||||
|
return fileBytes.contentEquals(elfMagic)
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package shirkneko.zako.sukisu.ui.screen
|
package com.sukisu.ultra.ui.screen
|
||||||
|
|
||||||
import android.app.Activity.*
|
import android.app.Activity.*
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
@@ -9,22 +9,7 @@ import androidx.activity.compose.rememberLauncherForActivityResult
|
|||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.foundation.*
|
import androidx.compose.foundation.*
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
|
||||||
import androidx.compose.foundation.layout.WindowInsetsSides
|
|
||||||
import androidx.compose.foundation.layout.defaultMinSize
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.only
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.selection.toggleable
|
import androidx.compose.foundation.selection.toggleable
|
||||||
@@ -32,41 +17,14 @@ import androidx.compose.material.icons.Icons
|
|||||||
import androidx.compose.material.icons.automirrored.outlined.*
|
import androidx.compose.material.icons.automirrored.outlined.*
|
||||||
import androidx.compose.material.icons.filled.*
|
import androidx.compose.material.icons.filled.*
|
||||||
import androidx.compose.material.icons.outlined.*
|
import androidx.compose.material.icons.outlined.*
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.material3.ButtonDefaults
|
|
||||||
import androidx.compose.material3.CardDefaults
|
|
||||||
import androidx.compose.material3.Checkbox
|
|
||||||
import androidx.compose.material3.DropdownMenu
|
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
|
||||||
import androidx.compose.material3.ElevatedCard
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
||||||
import androidx.compose.material3.ExtendedFloatingActionButton
|
|
||||||
import androidx.compose.material3.FilledTonalButton
|
|
||||||
import androidx.compose.material3.HorizontalDivider
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Scaffold
|
|
||||||
import androidx.compose.material3.SnackbarDuration
|
|
||||||
import androidx.compose.material3.SnackbarHost
|
|
||||||
import androidx.compose.material3.SnackbarHostState
|
|
||||||
import androidx.compose.material3.SnackbarResult
|
|
||||||
import androidx.compose.material3.Switch
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
|
||||||
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||||
import androidx.compose.material3.rememberTopAppBarState
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.produceState
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.draw.rotate
|
import androidx.compose.ui.draw.rotate
|
||||||
|
import androidx.compose.ui.draw.shadow
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.platform.*
|
import androidx.compose.ui.platform.*
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
@@ -88,32 +46,31 @@ import com.ramcosta.composedestinations.navigation.EmptyDestinationsNavigator
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import shirkneko.zako.sukisu.Natives
|
import com.sukisu.ultra.Natives
|
||||||
import shirkneko.zako.sukisu.R
|
import com.sukisu.ultra.ui.component.ConfirmResult
|
||||||
import shirkneko.zako.sukisu.ui.component.ConfirmResult
|
import com.sukisu.ultra.ui.component.SearchAppBar
|
||||||
import shirkneko.zako.sukisu.ui.component.SearchAppBar
|
import com.sukisu.ultra.ui.component.rememberConfirmDialog
|
||||||
import shirkneko.zako.sukisu.ui.component.rememberConfirmDialog
|
import com.sukisu.ultra.ui.component.rememberLoadingDialog
|
||||||
import shirkneko.zako.sukisu.ui.component.rememberLoadingDialog
|
import com.sukisu.ultra.ui.util.DownloadListener
|
||||||
import shirkneko.zako.sukisu.ui.util.DownloadListener
|
import com.sukisu.ultra.ui.util.*
|
||||||
import shirkneko.zako.sukisu.ui.util.*
|
import com.sukisu.ultra.ui.util.download
|
||||||
import shirkneko.zako.sukisu.ui.util.download
|
import com.sukisu.ultra.ui.util.hasMagisk
|
||||||
import shirkneko.zako.sukisu.ui.util.hasMagisk
|
import com.sukisu.ultra.ui.util.reboot
|
||||||
import shirkneko.zako.sukisu.ui.util.reboot
|
import com.sukisu.ultra.ui.util.restoreModule
|
||||||
import shirkneko.zako.sukisu.ui.util.restoreModule
|
import com.sukisu.ultra.ui.util.toggleModule
|
||||||
import shirkneko.zako.sukisu.ui.util.toggleModule
|
import com.sukisu.ultra.ui.util.uninstallModule
|
||||||
import shirkneko.zako.sukisu.ui.util.uninstallModule
|
import com.sukisu.ultra.ui.webui.WebUIActivity
|
||||||
import shirkneko.zako.sukisu.ui.webui.WebUIActivity
|
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import shirkneko.zako.sukisu.ui.util.ModuleModify
|
import com.sukisu.ultra.ui.util.ModuleModify
|
||||||
import shirkneko.zako.sukisu.ui.theme.getCardColors
|
import com.sukisu.ultra.ui.theme.getCardColors
|
||||||
import shirkneko.zako.sukisu.ui.theme.getCardElevation
|
import com.sukisu.ultra.ui.viewmodel.ModuleViewModel
|
||||||
import shirkneko.zako.sukisu.ui.viewmodel.ModuleViewModel
|
|
||||||
import java.io.BufferedReader
|
import java.io.BufferedReader
|
||||||
import java.io.InputStreamReader
|
import java.io.InputStreamReader
|
||||||
import java.util.zip.ZipInputStream
|
import java.util.zip.ZipInputStream
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import shirkneko.zako.sukisu.ui.theme.ThemeConfig
|
import com.sukisu.ultra.R
|
||||||
|
import com.sukisu.ultra.ui.theme.CardConfig.cardElevation
|
||||||
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@@ -239,7 +196,7 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
|
|||||||
val backupLauncher = ModuleModify.rememberModuleBackupLauncher(context, snackBarHost)
|
val backupLauncher = ModuleModify.rememberModuleBackupLauncher(context, snackBarHost)
|
||||||
val restoreLauncher = ModuleModify.rememberModuleRestoreLauncher(context, snackBarHost)
|
val restoreLauncher = ModuleModify.rememberModuleRestoreLauncher(context, snackBarHost)
|
||||||
|
|
||||||
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
val prefs = context.getSharedPreferences("settings", MODE_PRIVATE)
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
if (viewModel.moduleList.isEmpty() || viewModel.isNeedRefresh) {
|
if (viewModel.moduleList.isEmpty() || viewModel.isNeedRefresh) {
|
||||||
@@ -275,7 +232,7 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
|
|||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Filled.MoreVert,
|
imageVector = Icons.Filled.MoreVert,
|
||||||
contentDescription = stringResource(id = R.string.settings)
|
contentDescription = stringResource(id = R.string.settings),
|
||||||
)
|
)
|
||||||
|
|
||||||
DropdownMenu(
|
DropdownMenu(
|
||||||
@@ -284,7 +241,16 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
|
|||||||
) {
|
) {
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
text = { Text(stringResource(R.string.module_sort_action_first)) },
|
text = { Text(stringResource(R.string.module_sort_action_first)) },
|
||||||
trailingIcon = { Checkbox(viewModel.sortActionFirst, null) },
|
trailingIcon = {
|
||||||
|
Checkbox(
|
||||||
|
checked = viewModel.sortActionFirst,
|
||||||
|
onCheckedChange = null,
|
||||||
|
colors = CheckboxDefaults.colors(
|
||||||
|
checkedColor = MaterialTheme.colorScheme.primary,
|
||||||
|
uncheckedColor = MaterialTheme.colorScheme.outline
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
onClick = {
|
onClick = {
|
||||||
viewModel.sortActionFirst = !viewModel.sortActionFirst
|
viewModel.sortActionFirst = !viewModel.sortActionFirst
|
||||||
prefs.edit {
|
prefs.edit {
|
||||||
@@ -300,7 +266,16 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
|
|||||||
)
|
)
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
text = { Text(stringResource(R.string.module_sort_enabled_first)) },
|
text = { Text(stringResource(R.string.module_sort_enabled_first)) },
|
||||||
trailingIcon = { Checkbox(viewModel.sortEnabledFirst, null) },
|
trailingIcon = {
|
||||||
|
Checkbox(
|
||||||
|
checked = viewModel.sortEnabledFirst,
|
||||||
|
onCheckedChange = null,
|
||||||
|
colors = CheckboxDefaults.colors(
|
||||||
|
checkedColor = MaterialTheme.colorScheme.primary,
|
||||||
|
uncheckedColor = MaterialTheme.colorScheme.outline
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
onClick = {
|
onClick = {
|
||||||
viewModel.sortEnabledFirst = !viewModel.sortEnabledFirst
|
viewModel.sortEnabledFirst = !viewModel.sortEnabledFirst
|
||||||
prefs.edit {
|
prefs.edit {
|
||||||
@@ -311,12 +286,13 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
HorizontalDivider(thickness = Dp.Hairline, modifier = Modifier.padding(vertical = 4.dp))
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
text = { Text(stringResource(R.string.backup_modules)) },
|
text = { Text(stringResource(R.string.backup_modules)) },
|
||||||
leadingIcon = {
|
leadingIcon = {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Outlined.Download,
|
imageVector = Icons.Outlined.Download,
|
||||||
contentDescription = stringResource(R.string.backup)
|
contentDescription = stringResource(R.string.backup),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
onClick = {
|
onClick = {
|
||||||
@@ -329,7 +305,7 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
|
|||||||
leadingIcon = {
|
leadingIcon = {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Outlined.Refresh,
|
imageVector = Icons.Outlined.Refresh,
|
||||||
contentDescription = stringResource(R.string.restore)
|
contentDescription = stringResource(R.string.restore),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
onClick = {
|
onClick = {
|
||||||
@@ -346,7 +322,6 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
|
|||||||
floatingActionButton = {
|
floatingActionButton = {
|
||||||
if (!hideInstallButton) {
|
if (!hideInstallButton) {
|
||||||
val moduleInstall = stringResource(id = R.string.module_install)
|
val moduleInstall = stringResource(id = R.string.module_install)
|
||||||
val cardColor = MaterialTheme.colorScheme.secondaryContainer
|
|
||||||
ExtendedFloatingActionButton(
|
ExtendedFloatingActionButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
selectZipLauncher.launch(
|
selectZipLauncher.launch(
|
||||||
@@ -359,16 +334,17 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
|
|||||||
icon = {
|
icon = {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Filled.Add,
|
imageVector = Icons.Filled.Add,
|
||||||
contentDescription = moduleInstall
|
contentDescription = moduleInstall,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
text = {
|
text = {
|
||||||
Text(
|
Text(
|
||||||
text = moduleInstall
|
text = moduleInstall,
|
||||||
|
color = MaterialTheme.colorScheme.onPrimaryContainer
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
containerColor = cardColor.copy(alpha = 1f),
|
contentColor = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||||
contentColor = MaterialTheme.colorScheme.onSecondaryContainer
|
expanded = true,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -385,12 +361,27 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
|
|||||||
.padding(24.dp),
|
.padding(24.dp),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.Warning,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.error,
|
||||||
|
modifier = Modifier
|
||||||
|
.size(64.dp)
|
||||||
|
.padding(bottom = 16.dp)
|
||||||
|
)
|
||||||
Text(
|
Text(
|
||||||
stringResource(R.string.module_magisk_conflict),
|
stringResource(R.string.module_magisk_conflict),
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
else -> {
|
else -> {
|
||||||
ModuleList(
|
ModuleList(
|
||||||
navigator = navigator,
|
navigator = navigator,
|
||||||
@@ -587,10 +578,25 @@ private fun ModuleList(
|
|||||||
modifier = Modifier.fillParentMaxSize(),
|
modifier = Modifier.fillParentMaxSize(),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
Text(
|
Column(
|
||||||
stringResource(R.string.module_empty),
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
textAlign = TextAlign.Center
|
verticalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.Extension,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.6f),
|
||||||
|
modifier = Modifier
|
||||||
|
.size(96.dp)
|
||||||
|
.padding(bottom = 16.dp)
|
||||||
)
|
)
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.module_empty),
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -673,8 +679,16 @@ fun ModuleItem(
|
|||||||
onClick: (ModuleViewModel.ModuleInfo) -> Unit
|
onClick: (ModuleViewModel.ModuleInfo) -> Unit
|
||||||
) {
|
) {
|
||||||
ElevatedCard(
|
ElevatedCard(
|
||||||
colors = getCardColors(MaterialTheme.colorScheme.secondaryContainer),
|
colors = getCardColors(MaterialTheme.colorScheme.surfaceContainerHigh),
|
||||||
elevation = CardDefaults.cardElevation(defaultElevation = getCardElevation())
|
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clip(MaterialTheme.shapes.large)
|
||||||
|
.shadow(
|
||||||
|
elevation = cardElevation,
|
||||||
|
shape = MaterialTheme.shapes.large,
|
||||||
|
spotColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
val textDecoration = if (!module.remove) null else TextDecoration.LineThrough
|
val textDecoration = if (!module.remove) null else TextDecoration.LineThrough
|
||||||
val interactionSource = remember { MutableInteractionSource() }
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
@@ -702,6 +716,7 @@ fun ModuleItem(
|
|||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
val moduleVersion = stringResource(id = R.string.module_version)
|
val moduleVersion = stringResource(id = R.string.module_version)
|
||||||
val moduleAuthor = stringResource(id = R.string.module_author)
|
val moduleAuthor = stringResource(id = R.string.module_author)
|
||||||
@@ -716,6 +731,7 @@ fun ModuleItem(
|
|||||||
lineHeight = MaterialTheme.typography.bodySmall.lineHeight,
|
lineHeight = MaterialTheme.typography.bodySmall.lineHeight,
|
||||||
fontFamily = MaterialTheme.typography.titleMedium.fontFamily,
|
fontFamily = MaterialTheme.typography.titleMedium.fontFamily,
|
||||||
textDecoration = textDecoration,
|
textDecoration = textDecoration,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
)
|
)
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
@@ -723,7 +739,8 @@ fun ModuleItem(
|
|||||||
fontSize = MaterialTheme.typography.bodySmall.fontSize,
|
fontSize = MaterialTheme.typography.bodySmall.fontSize,
|
||||||
lineHeight = MaterialTheme.typography.bodySmall.lineHeight,
|
lineHeight = MaterialTheme.typography.bodySmall.lineHeight,
|
||||||
fontFamily = MaterialTheme.typography.bodySmall.fontFamily,
|
fontFamily = MaterialTheme.typography.bodySmall.fontFamily,
|
||||||
textDecoration = textDecoration
|
textDecoration = textDecoration,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
@@ -731,7 +748,8 @@ fun ModuleItem(
|
|||||||
fontSize = MaterialTheme.typography.bodySmall.fontSize,
|
fontSize = MaterialTheme.typography.bodySmall.fontSize,
|
||||||
lineHeight = MaterialTheme.typography.bodySmall.lineHeight,
|
lineHeight = MaterialTheme.typography.bodySmall.lineHeight,
|
||||||
fontFamily = MaterialTheme.typography.bodySmall.fontFamily,
|
fontFamily = MaterialTheme.typography.bodySmall.fontFamily,
|
||||||
textDecoration = textDecoration
|
textDecoration = textDecoration,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -745,7 +763,15 @@ fun ModuleItem(
|
|||||||
enabled = !module.update,
|
enabled = !module.update,
|
||||||
checked = module.enabled,
|
checked = module.enabled,
|
||||||
onCheckedChange = onCheckChanged,
|
onCheckedChange = onCheckChanged,
|
||||||
interactionSource = if (!module.hasWebUi) interactionSource else null
|
interactionSource = if (!module.hasWebUi) interactionSource else null,
|
||||||
|
colors = SwitchDefaults.colors(
|
||||||
|
checkedThumbColor = MaterialTheme.colorScheme.onPrimary,
|
||||||
|
checkedTrackColor = MaterialTheme.colorScheme.primary,
|
||||||
|
checkedIconColor = MaterialTheme.colorScheme.primary,
|
||||||
|
uncheckedThumbColor = MaterialTheme.colorScheme.outline,
|
||||||
|
uncheckedTrackColor = MaterialTheme.colorScheme.surfaceVariant,
|
||||||
|
uncheckedIconColor = MaterialTheme.colorScheme.surfaceVariant
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -760,83 +786,70 @@ fun ModuleItem(
|
|||||||
fontWeight = MaterialTheme.typography.bodySmall.fontWeight,
|
fontWeight = MaterialTheme.typography.bodySmall.fontWeight,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
maxLines = 4,
|
maxLines = 4,
|
||||||
textDecoration = textDecoration
|
textDecoration = textDecoration,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
HorizontalDivider(thickness = Dp.Hairline)
|
HorizontalDivider(thickness = Dp.Hairline)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
|
|
||||||
if (module.hasActionScript) {
|
if (module.hasActionScript) {
|
||||||
FilledTonalButton(
|
FilledTonalButton(
|
||||||
modifier = Modifier.defaultMinSize(52.dp, 32.dp),
|
modifier = Modifier.defaultMinSize(minWidth = 52.dp, minHeight = 32.dp),
|
||||||
enabled = !module.remove && module.enabled,
|
enabled = !module.remove && module.enabled,
|
||||||
onClick = {
|
onClick = {
|
||||||
navigator.navigate(ExecuteModuleActionScreenDestination(module.dirId))
|
navigator.navigate(ExecuteModuleActionScreenDestination(module.dirId))
|
||||||
viewModel.markNeedRefresh()
|
viewModel.markNeedRefresh()
|
||||||
},
|
},
|
||||||
contentPadding = ButtonDefaults.TextButtonContentPadding,
|
contentPadding = ButtonDefaults.TextButtonContentPadding,
|
||||||
colors = if (!ThemeConfig.useDynamicColor) {
|
colors = ButtonDefaults.filledTonalButtonColors()
|
||||||
ButtonDefaults.filledTonalButtonColors(
|
|
||||||
containerColor = ThemeConfig.currentTheme.ButtonContrast
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
ButtonDefaults.filledTonalButtonColors()
|
|
||||||
}
|
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
modifier = Modifier.size(20.dp),
|
modifier = Modifier.size(20.dp),
|
||||||
imageVector = Icons.Outlined.PlayArrow,
|
imageVector = Icons.Outlined.PlayArrow,
|
||||||
contentDescription = null
|
contentDescription = null
|
||||||
)
|
)
|
||||||
if (!module.hasWebUi && updateUrl.isEmpty()) {
|
//if (!module.hasWebUi && updateUrl.isEmpty()) {
|
||||||
Text(
|
//Text(
|
||||||
modifier = Modifier.padding(start = 7.dp),
|
// modifier = Modifier.padding(start = 7.dp),
|
||||||
text = stringResource(R.string.action),
|
// text = stringResource(R.string.action),
|
||||||
fontFamily = MaterialTheme.typography.labelMedium.fontFamily,
|
// fontFamily = MaterialTheme.typography.labelMedium.fontFamily,
|
||||||
fontSize = MaterialTheme.typography.labelMedium.fontSize
|
// fontSize = MaterialTheme.typography.labelMedium.fontSize
|
||||||
)
|
//)
|
||||||
|
//}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.weight(0.1f, true))
|
|
||||||
}
|
|
||||||
|
|
||||||
if (module.hasWebUi) {
|
if (module.hasWebUi) {
|
||||||
FilledTonalButton(
|
FilledTonalButton(
|
||||||
modifier = Modifier.defaultMinSize(52.dp, 32.dp),
|
modifier = Modifier.defaultMinSize(minWidth = 52.dp, minHeight = 32.dp),
|
||||||
enabled = !module.remove && module.enabled,
|
enabled = !module.remove && module.enabled,
|
||||||
onClick = { onClick(module) },
|
onClick = { onClick(module) },
|
||||||
interactionSource = interactionSource,
|
interactionSource = interactionSource,
|
||||||
contentPadding = ButtonDefaults.TextButtonContentPadding,
|
contentPadding = ButtonDefaults.TextButtonContentPadding,
|
||||||
colors = if (!ThemeConfig.useDynamicColor) {
|
colors = ButtonDefaults.filledTonalButtonColors()
|
||||||
ButtonDefaults.filledTonalButtonColors(
|
|
||||||
containerColor = ThemeConfig.currentTheme.ButtonContrast
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
ButtonDefaults.filledTonalButtonColors()
|
|
||||||
}
|
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
modifier = Modifier.size(20.dp),
|
modifier = Modifier.size(20.dp),
|
||||||
imageVector = Icons.AutoMirrored.Outlined.Wysiwyg,
|
imageVector = Icons.AutoMirrored.Outlined.Wysiwyg,
|
||||||
contentDescription = null
|
contentDescription = null
|
||||||
)
|
)
|
||||||
if (!module.hasActionScript && updateUrl.isEmpty()) {
|
//if (!module.hasActionScript && updateUrl.isEmpty()) {
|
||||||
Text(
|
//Text(
|
||||||
modifier = Modifier.padding(start = 7.dp),
|
// modifier = Modifier.padding(start = 7.dp),
|
||||||
fontFamily = MaterialTheme.typography.labelMedium.fontFamily,
|
// fontFamily = MaterialTheme.typography.labelMedium.fontFamily,
|
||||||
fontSize = MaterialTheme.typography.labelMedium.fontSize,
|
// fontSize = MaterialTheme.typography.labelMedium.fontSize,
|
||||||
text = stringResource(R.string.open)
|
// text = stringResource(R.string.open)
|
||||||
)
|
//)
|
||||||
}
|
//}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -844,7 +857,7 @@ fun ModuleItem(
|
|||||||
|
|
||||||
if (updateUrl.isNotEmpty()) {
|
if (updateUrl.isNotEmpty()) {
|
||||||
Button(
|
Button(
|
||||||
modifier = Modifier.defaultMinSize(52.dp, 32.dp),
|
modifier = Modifier.defaultMinSize(minWidth = 52.dp, minHeight = 32.dp),
|
||||||
enabled = !module.remove,
|
enabled = !module.remove,
|
||||||
onClick = { onUpdate(module) },
|
onClick = { onUpdate(module) },
|
||||||
shape = ButtonDefaults.textShape,
|
shape = ButtonDefaults.textShape,
|
||||||
@@ -855,30 +868,23 @@ fun ModuleItem(
|
|||||||
imageVector = Icons.Outlined.Download,
|
imageVector = Icons.Outlined.Download,
|
||||||
contentDescription = null
|
contentDescription = null
|
||||||
)
|
)
|
||||||
if (!module.hasActionScript || !module.hasWebUi) {
|
//if (!module.hasActionScript || !module.hasWebUi) {
|
||||||
Text(
|
//Text(
|
||||||
modifier = Modifier.padding(start = 7.dp),
|
// modifier = Modifier.padding(start = 7.dp),
|
||||||
fontFamily = MaterialTheme.typography.labelMedium.fontFamily,
|
// fontFamily = MaterialTheme.typography.labelMedium.fontFamily,
|
||||||
fontSize = MaterialTheme.typography.labelMedium.fontSize,
|
// fontSize = MaterialTheme.typography.labelMedium.fontSize,
|
||||||
text = stringResource(R.string.module_update)
|
// text = stringResource(R.string.module_update)
|
||||||
)
|
//)
|
||||||
|
//}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.weight(0.1f, true))
|
|
||||||
}
|
|
||||||
|
|
||||||
FilledTonalButton(
|
FilledTonalButton(
|
||||||
modifier = Modifier.defaultMinSize(52.dp, 32.dp),
|
modifier = Modifier.defaultMinSize(minWidth = 52.dp, minHeight = 32.dp),
|
||||||
onClick = { onUninstallClicked(module) },
|
onClick = { onUninstallClicked(module) },
|
||||||
contentPadding = ButtonDefaults.TextButtonContentPadding,
|
contentPadding = ButtonDefaults.TextButtonContentPadding,
|
||||||
colors = if (!ThemeConfig.useDynamicColor) {
|
colors = ButtonDefaults.filledTonalButtonColors(
|
||||||
ButtonDefaults.filledTonalButtonColors(
|
containerColor = if (!module.remove) MaterialTheme.colorScheme.secondaryContainer else MaterialTheme.colorScheme.errorContainer)
|
||||||
containerColor = ThemeConfig.currentTheme.ButtonContrast
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
ButtonDefaults.filledTonalButtonColors()
|
|
||||||
}
|
|
||||||
) {
|
) {
|
||||||
if (!module.remove) {
|
if (!module.remove) {
|
||||||
Icon(
|
Icon(
|
||||||
@@ -890,18 +896,18 @@ fun ModuleItem(
|
|||||||
Icon(
|
Icon(
|
||||||
modifier = Modifier.size(20.dp).rotate(180f),
|
modifier = Modifier.size(20.dp).rotate(180f),
|
||||||
imageVector = Icons.Outlined.Refresh,
|
imageVector = Icons.Outlined.Refresh,
|
||||||
contentDescription = null,
|
contentDescription = null
|
||||||
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (!module.hasActionScript && !module.hasWebUi && updateUrl.isEmpty()) {
|
|
||||||
Text(
|
|
||||||
modifier = Modifier.padding(start = 7.dp),
|
|
||||||
fontFamily = MaterialTheme.typography.labelMedium.fontFamily,
|
|
||||||
fontSize = MaterialTheme.typography.labelMedium.fontSize,
|
|
||||||
text = stringResource(if (!module.remove) R.string.uninstall else R.string.restore)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
//if (!module.hasActionScript && !module.hasWebUi && updateUrl.isEmpty()) {
|
||||||
|
//Text(
|
||||||
|
// modifier = Modifier.padding(start = 7.dp),
|
||||||
|
// fontFamily = MaterialTheme.typography.labelMedium.fontFamily,
|
||||||
|
// fontSize = MaterialTheme.typography.labelMedium.fontSize,
|
||||||
|
// text = stringResource(if (!module.remove) R.string.uninstall else R.string.restore),
|
||||||
|
// color = if (!module.remove) MaterialTheme.colorScheme.onErrorContainer else MaterialTheme.colorScheme.onSecondaryContainer
|
||||||
|
//)
|
||||||
|
//}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -928,4 +934,3 @@ fun ModuleItemPreview() {
|
|||||||
)
|
)
|
||||||
ModuleItem(EmptyDestinationsNavigator, module, "", {}, {}, {}, {})
|
ModuleItem(EmptyDestinationsNavigator, module, "", {}, {}, {}, {})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,925 @@
|
|||||||
|
package com.sukisu.ultra.ui.screen
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.expandVertically
|
||||||
|
import androidx.compose.animation.fadeIn
|
||||||
|
import androidx.compose.animation.fadeOut
|
||||||
|
import androidx.compose.animation.shrinkVertically
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.NavigateNext
|
||||||
|
import androidx.compose.material.icons.filled.Brush
|
||||||
|
import androidx.compose.material.icons.filled.ColorLens
|
||||||
|
import androidx.compose.material.icons.filled.DarkMode
|
||||||
|
import androidx.compose.material.icons.filled.KeyboardArrowDown
|
||||||
|
import androidx.compose.material.icons.filled.KeyboardArrowUp
|
||||||
|
import androidx.compose.material.icons.filled.Opacity
|
||||||
|
import androidx.compose.material.icons.filled.Palette
|
||||||
|
import androidx.compose.material.icons.filled.Security
|
||||||
|
import androidx.compose.material.icons.filled.VisibilityOff
|
||||||
|
import androidx.compose.material.icons.filled.Wallpaper
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.ListItem
|
||||||
|
import androidx.compose.material3.ListItemDefaults
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.RadioButton
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Slider
|
||||||
|
import androidx.compose.material3.SliderDefaults
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Switch
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.material3.TopAppBar
|
||||||
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
|
import androidx.compose.material3.rememberTopAppBarState
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.core.content.edit
|
||||||
|
import com.ramcosta.composedestinations.annotation.Destination
|
||||||
|
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||||
|
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||||
|
import com.sukisu.ultra.Natives
|
||||||
|
import com.sukisu.ultra.R
|
||||||
|
import com.sukisu.ultra.ksuApp
|
||||||
|
import com.sukisu.ultra.ui.component.ImageEditorDialog
|
||||||
|
import com.sukisu.ultra.ui.component.KsuIsValid
|
||||||
|
import com.sukisu.ultra.ui.component.SwitchItem
|
||||||
|
import com.sukisu.ultra.ui.theme.CardConfig
|
||||||
|
import com.sukisu.ultra.ui.theme.ThemeColors
|
||||||
|
import com.sukisu.ultra.ui.theme.ThemeConfig
|
||||||
|
import com.sukisu.ultra.ui.theme.saveAndApplyCustomBackground
|
||||||
|
import com.sukisu.ultra.ui.theme.saveCustomBackground
|
||||||
|
import com.sukisu.ultra.ui.theme.saveDynamicColorState
|
||||||
|
import com.sukisu.ultra.ui.theme.saveThemeColors
|
||||||
|
import com.sukisu.ultra.ui.theme.saveThemeMode
|
||||||
|
import com.sukisu.ultra.ui.util.getSuSFS
|
||||||
|
import com.sukisu.ultra.ui.util.getSuSFSFeatures
|
||||||
|
import com.sukisu.ultra.ui.util.susfsSUS_SU_0
|
||||||
|
import com.sukisu.ultra.ui.util.susfsSUS_SU_2
|
||||||
|
import com.sukisu.ultra.ui.util.susfsSUS_SU_Mode
|
||||||
|
import com.topjohnwu.superuser.Shell
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
fun saveCardConfig(context: Context) {
|
||||||
|
CardConfig.save(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Destination<RootGraph>
|
||||||
|
@Composable
|
||||||
|
fun MoreSettingsScreen(navigator: DestinationsNavigator) {
|
||||||
|
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||||
|
val context = LocalContext.current
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
val prefs = remember { context.getSharedPreferences("settings", Context.MODE_PRIVATE) }
|
||||||
|
val systemIsDark = isSystemInDarkTheme()
|
||||||
|
|
||||||
|
// 主题模式选择
|
||||||
|
var themeMode by remember {
|
||||||
|
mutableIntStateOf(
|
||||||
|
when(ThemeConfig.forceDarkMode) {
|
||||||
|
true -> 2 // 深色
|
||||||
|
false -> 1 // 浅色
|
||||||
|
null -> 0 // 跟随系统
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 动态颜色开关状态
|
||||||
|
var useDynamicColor by remember {
|
||||||
|
mutableStateOf(ThemeConfig.useDynamicColor)
|
||||||
|
}
|
||||||
|
|
||||||
|
var showThemeModeDialog by remember { mutableStateOf(false) }
|
||||||
|
// 主题模式选项
|
||||||
|
val themeOptions = listOf(
|
||||||
|
stringResource(R.string.theme_follow_system),
|
||||||
|
stringResource(R.string.theme_light),
|
||||||
|
stringResource(R.string.theme_dark)
|
||||||
|
)
|
||||||
|
|
||||||
|
// 简洁模式开关状态
|
||||||
|
var isSimpleMode by remember {
|
||||||
|
mutableStateOf(prefs.getBoolean("is_simple_mode", false))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新简洁模式开关状态
|
||||||
|
val onSimpleModeChange = { newValue: Boolean ->
|
||||||
|
prefs.edit { putBoolean("is_simple_mode", newValue) }
|
||||||
|
isSimpleMode = newValue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 隐藏内核版本号开关状态
|
||||||
|
var isHideVersion by remember {
|
||||||
|
mutableStateOf(prefs.getBoolean("is_hide_version", false))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 隐藏内核版本号开关状态
|
||||||
|
val onHideVersionChange = { newValue: Boolean ->
|
||||||
|
prefs.edit { putBoolean("is_hide_version", newValue) }
|
||||||
|
isHideVersion = newValue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 隐藏模块数量等信息开关状态
|
||||||
|
var isHideOtherInfo by remember {
|
||||||
|
mutableStateOf(prefs.getBoolean("is_hide_other_info", false))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 隐藏模块数量等信息开关状态
|
||||||
|
val onHideOtherInfoChange = { newValue: Boolean ->
|
||||||
|
prefs.edit { putBoolean("is_hide_other_info", newValue) }
|
||||||
|
isHideOtherInfo = newValue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 隐藏SuSFS状态开关状态
|
||||||
|
var isHideSusfsStatus by remember {
|
||||||
|
mutableStateOf(prefs.getBoolean("is_hide_susfs_status", false))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 隐藏SuSFS状态开关状态
|
||||||
|
val onHideSusfsStatusChange = { newValue: Boolean ->
|
||||||
|
prefs.edit { putBoolean("is_hide_susfs_status", newValue) }
|
||||||
|
isHideSusfsStatus = newValue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 隐藏链接状态开关状态
|
||||||
|
var isHideLinkCard by remember {
|
||||||
|
mutableStateOf(prefs.getBoolean("is_hide_link_card", false))
|
||||||
|
}
|
||||||
|
|
||||||
|
val onHideLinkCardChange = { newValue: Boolean ->
|
||||||
|
prefs.edit { putBoolean("is_hide_link_card", newValue) }
|
||||||
|
isHideLinkCard = newValue
|
||||||
|
}
|
||||||
|
|
||||||
|
// SELinux状态
|
||||||
|
var selinuxEnabled by remember {
|
||||||
|
mutableStateOf(Shell.cmd("getenforce").exec().out.firstOrNull() == "Enforcing")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 卡片配置状态
|
||||||
|
var cardAlpha by rememberSaveable { mutableFloatStateOf(CardConfig.cardAlpha) }
|
||||||
|
var isCustomBackgroundEnabled by rememberSaveable {
|
||||||
|
mutableStateOf(ThemeConfig.customBackgroundUri != null)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 图片编辑状态
|
||||||
|
var showImageEditor by remember { mutableStateOf(false) }
|
||||||
|
var selectedImageUri by remember { mutableStateOf<Uri?>(null) }
|
||||||
|
|
||||||
|
// 展开/折叠状态
|
||||||
|
var isCustomizeExpanded by remember { mutableStateOf(false) }
|
||||||
|
var isAppearanceExpanded by remember { mutableStateOf(true) }
|
||||||
|
var isAdvancedExpanded by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
// 初始化卡片配置
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
// 加载设置
|
||||||
|
CardConfig.load(context)
|
||||||
|
cardAlpha = CardConfig.cardAlpha
|
||||||
|
isCustomBackgroundEnabled = ThemeConfig.customBackgroundUri != null
|
||||||
|
|
||||||
|
// 设置主题模式
|
||||||
|
themeMode = when (ThemeConfig.forceDarkMode) {
|
||||||
|
true -> 2
|
||||||
|
false -> 1
|
||||||
|
null -> 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保卡片样式跟随主题模式
|
||||||
|
when (themeMode) {
|
||||||
|
2 -> { // 深色
|
||||||
|
CardConfig.isUserDarkModeEnabled = true
|
||||||
|
CardConfig.isUserLightModeEnabled = false
|
||||||
|
}
|
||||||
|
1 -> { // 浅色
|
||||||
|
CardConfig.isUserDarkModeEnabled = false
|
||||||
|
CardConfig.isUserLightModeEnabled = true
|
||||||
|
}
|
||||||
|
0 -> { // 跟随系统
|
||||||
|
CardConfig.isUserDarkModeEnabled = false
|
||||||
|
CardConfig.isUserLightModeEnabled = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果启用了系统跟随且系统是深色模式,应用深色模式默认值
|
||||||
|
if (themeMode == 0 && systemIsDark) {
|
||||||
|
CardConfig.setDarkModeDefaults()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存设置
|
||||||
|
CardConfig.save(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 主题色选项
|
||||||
|
val themeColorOptions = listOf(
|
||||||
|
stringResource(R.string.color_default) to ThemeColors.Default,
|
||||||
|
stringResource(R.string.color_green) to ThemeColors.Green,
|
||||||
|
stringResource(R.string.color_purple) to ThemeColors.Purple,
|
||||||
|
stringResource(R.string.color_orange) to ThemeColors.Orange,
|
||||||
|
stringResource(R.string.color_pink) to ThemeColors.Pink,
|
||||||
|
stringResource(R.string.color_gray) to ThemeColors.Gray,
|
||||||
|
stringResource(R.string.color_yellow) to ThemeColors.Yellow
|
||||||
|
)
|
||||||
|
|
||||||
|
var showThemeColorDialog by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
// 图片选择器
|
||||||
|
val pickImageLauncher = rememberLauncherForActivityResult(
|
||||||
|
ActivityResultContracts.GetContent()
|
||||||
|
) { uri: Uri? ->
|
||||||
|
uri?.let {
|
||||||
|
selectedImageUri = it
|
||||||
|
showImageEditor = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示图片编辑对话框
|
||||||
|
if (showImageEditor && selectedImageUri != null) {
|
||||||
|
ImageEditorDialog(
|
||||||
|
imageUri = selectedImageUri!!,
|
||||||
|
onDismiss = {
|
||||||
|
showImageEditor = false
|
||||||
|
selectedImageUri = null
|
||||||
|
},
|
||||||
|
onConfirm = { transformedUri ->
|
||||||
|
context.saveAndApplyCustomBackground(transformedUri)
|
||||||
|
isCustomBackgroundEnabled = true
|
||||||
|
CardConfig.cardElevation = 0.dp
|
||||||
|
CardConfig.isCustomBackgroundEnabled = true
|
||||||
|
saveCardConfig(context)
|
||||||
|
showImageEditor = false
|
||||||
|
selectedImageUri = null
|
||||||
|
|
||||||
|
// 显示成功提示
|
||||||
|
Toast.makeText(
|
||||||
|
context,
|
||||||
|
context.getString(R.string.background_set_success),
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val cardColor = MaterialTheme.colorScheme.surfaceVariant
|
||||||
|
val cardAlphaUse = CardConfig.cardAlpha
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
title = { Text(stringResource(R.string.more_settings)) },
|
||||||
|
colors = TopAppBarDefaults.topAppBarColors(
|
||||||
|
containerColor = cardColor.copy(alpha = cardAlphaUse),
|
||||||
|
scrolledContainerColor = cardColor.copy(alpha = cardAlphaUse)),
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onClick = { navigator.popBackStack() }) {
|
||||||
|
Icon(Icons.AutoMirrored.Filled.ArrowBack,
|
||||||
|
contentDescription = stringResource(R.string.back))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scrollBehavior = scrollBehavior,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) { paddingValues ->
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(paddingValues)
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||||
|
) {
|
||||||
|
// 外观设置部分
|
||||||
|
SectionHeader(
|
||||||
|
title = stringResource(R.string.appearance_settings),
|
||||||
|
expanded = isAppearanceExpanded,
|
||||||
|
onToggle = { isAppearanceExpanded = !isAppearanceExpanded }
|
||||||
|
)
|
||||||
|
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = isAppearanceExpanded,
|
||||||
|
enter = fadeIn() + expandVertically(),
|
||||||
|
exit = fadeOut() + shrinkVertically()
|
||||||
|
) {
|
||||||
|
Surface(
|
||||||
|
shape = RoundedCornerShape(16.dp),
|
||||||
|
tonalElevation = 1.dp,
|
||||||
|
modifier = Modifier.padding(bottom = 16.dp)
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
|
// 主题模式
|
||||||
|
ListItem(
|
||||||
|
headlineContent = { Text(stringResource(R.string.theme_mode)) },
|
||||||
|
supportingContent = { Text(themeOptions[themeMode]) },
|
||||||
|
leadingContent = {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.DarkMode,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
},
|
||||||
|
trailingContent = {
|
||||||
|
Icon(
|
||||||
|
Icons.AutoMirrored.Filled.NavigateNext,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
},
|
||||||
|
colors = ListItemDefaults.colors(
|
||||||
|
containerColor = Color.Transparent
|
||||||
|
),
|
||||||
|
modifier = Modifier.clickable { showThemeModeDialog = true }
|
||||||
|
)
|
||||||
|
|
||||||
|
HorizontalDivider(
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp),
|
||||||
|
color = MaterialTheme.colorScheme.outlineVariant
|
||||||
|
)
|
||||||
|
|
||||||
|
// 动态颜色开关
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
SwitchItem(
|
||||||
|
icon = Icons.Filled.ColorLens,
|
||||||
|
title = stringResource(R.string.dynamic_color_title),
|
||||||
|
summary = stringResource(R.string.dynamic_color_summary),
|
||||||
|
checked = useDynamicColor
|
||||||
|
) { enabled ->
|
||||||
|
useDynamicColor = enabled
|
||||||
|
context.saveDynamicColorState(enabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
HorizontalDivider(
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp),
|
||||||
|
color = MaterialTheme.colorScheme.outlineVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 只在未启用动态颜色时显示主题色选择
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = Build.VERSION.SDK_INT < Build.VERSION_CODES.S || !useDynamicColor,
|
||||||
|
enter = fadeIn() + expandVertically(),
|
||||||
|
exit = fadeOut() + shrinkVertically()
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
|
ListItem(
|
||||||
|
headlineContent = { Text(stringResource(R.string.theme_color)) },
|
||||||
|
supportingContent = {
|
||||||
|
val currentThemeName = when (ThemeConfig.currentTheme) {
|
||||||
|
is ThemeColors.Default -> stringResource(R.string.color_default)
|
||||||
|
is ThemeColors.Green -> stringResource(R.string.color_green)
|
||||||
|
is ThemeColors.Purple -> stringResource(R.string.color_purple)
|
||||||
|
is ThemeColors.Orange -> stringResource(R.string.color_orange)
|
||||||
|
is ThemeColors.Pink -> stringResource(R.string.color_pink)
|
||||||
|
is ThemeColors.Gray -> stringResource(R.string.color_gray)
|
||||||
|
is ThemeColors.Yellow -> stringResource(R.string.color_yellow)
|
||||||
|
else -> stringResource(R.string.color_default)
|
||||||
|
}
|
||||||
|
Text(currentThemeName)
|
||||||
|
},
|
||||||
|
leadingContent = {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Palette,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
},
|
||||||
|
trailingContent = {
|
||||||
|
Icon(
|
||||||
|
Icons.AutoMirrored.Filled.NavigateNext,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
},
|
||||||
|
colors = ListItemDefaults.colors(
|
||||||
|
containerColor = Color.Transparent
|
||||||
|
),
|
||||||
|
modifier = Modifier.clickable { showThemeColorDialog = true }
|
||||||
|
)
|
||||||
|
|
||||||
|
HorizontalDivider(
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp),
|
||||||
|
color = MaterialTheme.colorScheme.outlineVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 自定义背景开关
|
||||||
|
ListItem(
|
||||||
|
headlineContent = { Text(stringResource(id = R.string.settings_custom_background)) },
|
||||||
|
supportingContent = { Text(stringResource(id = R.string.settings_custom_background_summary)) },
|
||||||
|
leadingContent = {
|
||||||
|
Icon(
|
||||||
|
Icons.Filled.Wallpaper,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = if (isCustomBackgroundEnabled)
|
||||||
|
MaterialTheme.colorScheme.primary
|
||||||
|
else
|
||||||
|
MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
},
|
||||||
|
trailingContent = {
|
||||||
|
Switch(
|
||||||
|
checked = isCustomBackgroundEnabled,
|
||||||
|
onCheckedChange = { isChecked ->
|
||||||
|
if (isChecked) {
|
||||||
|
pickImageLauncher.launch("image/*")
|
||||||
|
} else {
|
||||||
|
context.saveCustomBackground(null)
|
||||||
|
isCustomBackgroundEnabled = false
|
||||||
|
CardConfig.cardElevation
|
||||||
|
CardConfig.cardAlpha = 1f
|
||||||
|
CardConfig.isCustomAlphaSet = false
|
||||||
|
CardConfig.isCustomBackgroundEnabled = false
|
||||||
|
saveCardConfig(context)
|
||||||
|
cardAlpha = 1f
|
||||||
|
|
||||||
|
// 重置其他相关设置
|
||||||
|
ThemeConfig.needsResetOnThemeChange = true
|
||||||
|
ThemeConfig.preventBackgroundRefresh = false
|
||||||
|
|
||||||
|
context.getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
||||||
|
.edit {
|
||||||
|
putBoolean(
|
||||||
|
"prevent_background_refresh",
|
||||||
|
false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Toast.makeText(
|
||||||
|
context,
|
||||||
|
context.getString(R.string.background_removed),
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
colors = ListItemDefaults.colors(
|
||||||
|
containerColor = Color.Transparent
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
// 透明度 Slider
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = ThemeConfig.customBackgroundUri != null,
|
||||||
|
enter = fadeIn() + expandVertically(),
|
||||||
|
exit = fadeOut() + shrinkVertically(),
|
||||||
|
modifier = Modifier.padding(horizontal = 32.dp)
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.padding(vertical = 8.dp)) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.padding(bottom = 4.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Filled.Opacity,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(20.dp),
|
||||||
|
tint = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.settings_card_alpha),
|
||||||
|
style = MaterialTheme.typography.titleSmall
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
Text(
|
||||||
|
text = "${(cardAlpha * 100).roundToInt()}%",
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Slider(
|
||||||
|
value = cardAlpha,
|
||||||
|
onValueChange = { newValue ->
|
||||||
|
cardAlpha = newValue
|
||||||
|
CardConfig.cardAlpha = newValue
|
||||||
|
CardConfig.isCustomAlphaSet = true
|
||||||
|
prefs.edit {
|
||||||
|
putBoolean("is_custom_alpha_set", true)
|
||||||
|
putFloat("card_alpha", newValue)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onValueChangeFinished = {
|
||||||
|
coroutineScope.launch(Dispatchers.IO) {
|
||||||
|
saveCardConfig(context)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
valueRange = 0f..1f,
|
||||||
|
steps = 20,
|
||||||
|
colors = SliderDefaults.colors(
|
||||||
|
thumbColor = MaterialTheme.colorScheme.primary,
|
||||||
|
activeTrackColor = MaterialTheme.colorScheme.primary,
|
||||||
|
inactiveTrackColor = MaterialTheme.colorScheme.surfaceVariant
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 自定义设置部分
|
||||||
|
SectionHeader(
|
||||||
|
title = stringResource(R.string.custom_settings),
|
||||||
|
expanded = isCustomizeExpanded,
|
||||||
|
onToggle = { isCustomizeExpanded = !isCustomizeExpanded }
|
||||||
|
)
|
||||||
|
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = isCustomizeExpanded,
|
||||||
|
enter = fadeIn() + expandVertically(),
|
||||||
|
exit = fadeOut() + shrinkVertically()
|
||||||
|
) {
|
||||||
|
Surface(
|
||||||
|
shape = RoundedCornerShape(16.dp),
|
||||||
|
tonalElevation = 1.dp,
|
||||||
|
modifier = Modifier.padding(bottom = 16.dp)
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
|
// 添加简洁模式开关
|
||||||
|
SwitchItem(
|
||||||
|
icon = Icons.Filled.Brush,
|
||||||
|
title = stringResource(R.string.simple_mode),
|
||||||
|
summary = stringResource(R.string.simple_mode_summary),
|
||||||
|
checked = isSimpleMode
|
||||||
|
) {
|
||||||
|
onSimpleModeChange(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
HorizontalDivider(
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp),
|
||||||
|
color = MaterialTheme.colorScheme.outlineVariant
|
||||||
|
)
|
||||||
|
|
||||||
|
// 隐藏内核部分版本号
|
||||||
|
SwitchItem(
|
||||||
|
icon = Icons.Filled.VisibilityOff,
|
||||||
|
title = stringResource(R.string.hide_kernel_kernelsu_version),
|
||||||
|
summary = stringResource(R.string.hide_kernel_kernelsu_version_summary),
|
||||||
|
checked = isHideVersion
|
||||||
|
) {
|
||||||
|
onHideVersionChange(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
HorizontalDivider(
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp),
|
||||||
|
color = MaterialTheme.colorScheme.outlineVariant
|
||||||
|
)
|
||||||
|
|
||||||
|
// 模块数量等信息
|
||||||
|
SwitchItem(
|
||||||
|
icon = Icons.Filled.VisibilityOff,
|
||||||
|
title = stringResource(R.string.hide_other_info),
|
||||||
|
summary = stringResource(R.string.hide_other_info_summary),
|
||||||
|
checked = isHideOtherInfo
|
||||||
|
) {
|
||||||
|
onHideOtherInfoChange(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
HorizontalDivider(
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp),
|
||||||
|
color = MaterialTheme.colorScheme.outlineVariant
|
||||||
|
)
|
||||||
|
|
||||||
|
// SuSFS 状态信息
|
||||||
|
SwitchItem(
|
||||||
|
icon = Icons.Filled.VisibilityOff,
|
||||||
|
title = stringResource(R.string.hide_susfs_status),
|
||||||
|
summary = stringResource(R.string.hide_susfs_status_summary),
|
||||||
|
checked = isHideSusfsStatus
|
||||||
|
) {
|
||||||
|
onHideSusfsStatusChange(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
HorizontalDivider(
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp),
|
||||||
|
color = MaterialTheme.colorScheme.outlineVariant
|
||||||
|
)
|
||||||
|
|
||||||
|
// 隐藏链接信息
|
||||||
|
SwitchItem(
|
||||||
|
icon = Icons.Filled.VisibilityOff,
|
||||||
|
title = stringResource(R.string.hide_link_card),
|
||||||
|
summary = stringResource(R.string.hide_link_card_summary),
|
||||||
|
checked = isHideLinkCard
|
||||||
|
) {
|
||||||
|
onHideLinkCardChange(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 高级设置部分
|
||||||
|
SectionHeader(
|
||||||
|
title = stringResource(R.string.advanced_settings),
|
||||||
|
expanded = isAdvancedExpanded,
|
||||||
|
onToggle = { isAdvancedExpanded = !isAdvancedExpanded }
|
||||||
|
)
|
||||||
|
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = isAdvancedExpanded,
|
||||||
|
enter = fadeIn() + expandVertically(),
|
||||||
|
exit = fadeOut() + shrinkVertically()
|
||||||
|
) {
|
||||||
|
Surface(
|
||||||
|
shape = RoundedCornerShape(16.dp),
|
||||||
|
tonalElevation = 1.dp,
|
||||||
|
modifier = Modifier.padding(bottom = 16.dp)
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
|
// SELinux 开关
|
||||||
|
KsuIsValid {
|
||||||
|
SwitchItem(
|
||||||
|
icon = Icons.Filled.Security,
|
||||||
|
title = stringResource(R.string.selinux),
|
||||||
|
summary = if (selinuxEnabled)
|
||||||
|
stringResource(R.string.selinux_enabled) else
|
||||||
|
stringResource(R.string.selinux_disabled),
|
||||||
|
checked = selinuxEnabled
|
||||||
|
) { enabled ->
|
||||||
|
val command = if (enabled) "setenforce 1" else "setenforce 0"
|
||||||
|
Shell.getShell().newJob().add(command).exec().let { result ->
|
||||||
|
if (result.isSuccess) {
|
||||||
|
selinuxEnabled = enabled
|
||||||
|
// 显示成功提示
|
||||||
|
val message = if (enabled)
|
||||||
|
context.getString(R.string.selinux_enabled_toast)
|
||||||
|
else
|
||||||
|
context.getString(R.string.selinux_disabled_toast)
|
||||||
|
|
||||||
|
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
|
||||||
|
} else {
|
||||||
|
// 显示失败提示
|
||||||
|
Toast.makeText(
|
||||||
|
context,
|
||||||
|
context.getString(R.string.selinux_change_failed),
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HorizontalDivider(
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp),
|
||||||
|
color = MaterialTheme.colorScheme.outlineVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SuSFS 配置(仅在支持时显示)
|
||||||
|
val suSFS = getSuSFS()
|
||||||
|
val isSUS_SU = getSuSFSFeatures()
|
||||||
|
if (suSFS == "Supported" && isSUS_SU == "CONFIG_KSU_SUSFS_SUS_SU") {
|
||||||
|
// 初始化时,默认启用
|
||||||
|
var isEnabled by rememberSaveable {
|
||||||
|
mutableStateOf(true) // 默认启用
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在启动时检查状态
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
// 如果当前模式不是2就强制启用
|
||||||
|
val currentMode = susfsSUS_SU_Mode()
|
||||||
|
val wasManuallyDisabled = prefs.getBoolean("enable_sus_su", true)
|
||||||
|
if (currentMode != "2" && wasManuallyDisabled) {
|
||||||
|
susfsSUS_SU_2() // 强制切换到模式2
|
||||||
|
prefs.edit { putBoolean("enable_sus_su", true) }
|
||||||
|
}
|
||||||
|
isEnabled = currentMode == "2"
|
||||||
|
}
|
||||||
|
|
||||||
|
SwitchItem(
|
||||||
|
icon = Icons.Filled.Security,
|
||||||
|
title = stringResource(id = R.string.settings_susfs_toggle),
|
||||||
|
summary = stringResource(id = R.string.settings_susfs_toggle_summary),
|
||||||
|
checked = isEnabled
|
||||||
|
) {
|
||||||
|
if (it) {
|
||||||
|
// 手动启用
|
||||||
|
susfsSUS_SU_2()
|
||||||
|
prefs.edit { putBoolean("enable_sus_su", true) }
|
||||||
|
Toast.makeText(
|
||||||
|
context,
|
||||||
|
context.getString(R.string.susfs_enabled),
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
} else {
|
||||||
|
// 手动关闭
|
||||||
|
susfsSUS_SU_0()
|
||||||
|
prefs.edit { putBoolean("enable_sus_su", false) }
|
||||||
|
Toast.makeText(
|
||||||
|
context,
|
||||||
|
context.getString(R.string.susfs_disabled),
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
isEnabled = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 主题模式选择对话框
|
||||||
|
if (showThemeModeDialog) {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = { showThemeModeDialog = false },
|
||||||
|
title = { Text(stringResource(R.string.theme_mode)) },
|
||||||
|
text = {
|
||||||
|
Column {
|
||||||
|
themeOptions.forEachIndexed { index, option ->
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable {
|
||||||
|
themeMode = index
|
||||||
|
val newThemeMode = when(index) {
|
||||||
|
0 -> null // 跟随系统
|
||||||
|
1 -> false // 浅色
|
||||||
|
2 -> true // 深色
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
context.saveThemeMode(newThemeMode)
|
||||||
|
when (index) {
|
||||||
|
2 -> { // 深色
|
||||||
|
ThemeConfig.forceDarkMode = true
|
||||||
|
CardConfig.isUserDarkModeEnabled = true
|
||||||
|
CardConfig.isUserLightModeEnabled = false
|
||||||
|
if (!CardConfig.isCustomAlphaSet) {
|
||||||
|
CardConfig.cardAlpha = 1f
|
||||||
|
}
|
||||||
|
CardConfig.save(context)
|
||||||
|
}
|
||||||
|
1 -> { // 浅色
|
||||||
|
ThemeConfig.forceDarkMode = false
|
||||||
|
CardConfig.isUserLightModeEnabled = true
|
||||||
|
CardConfig.isUserDarkModeEnabled = false
|
||||||
|
if (!CardConfig.isCustomAlphaSet) {
|
||||||
|
CardConfig.cardAlpha = 1f
|
||||||
|
}
|
||||||
|
CardConfig.save(context)
|
||||||
|
}
|
||||||
|
0 -> { // 跟随系统
|
||||||
|
ThemeConfig.forceDarkMode = null
|
||||||
|
CardConfig.isUserLightModeEnabled = false
|
||||||
|
CardConfig.isUserDarkModeEnabled = false
|
||||||
|
CardConfig.save(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
showThemeModeDialog = false
|
||||||
|
}
|
||||||
|
.padding(vertical = 12.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
RadioButton(
|
||||||
|
selected = themeMode == index,
|
||||||
|
onClick = null
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Text(option)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(
|
||||||
|
onClick = { showThemeModeDialog = false }
|
||||||
|
) {
|
||||||
|
Text(stringResource(R.string.cancel))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 主题色选择对话框
|
||||||
|
if (showThemeColorDialog) {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = { showThemeColorDialog = false },
|
||||||
|
title = { Text(stringResource(R.string.choose_theme_color)) },
|
||||||
|
text = {
|
||||||
|
Column {
|
||||||
|
themeColorOptions.forEach { (name, theme) ->
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable {
|
||||||
|
context.saveThemeColors(when (theme) {
|
||||||
|
ThemeColors.Default -> "default"
|
||||||
|
ThemeColors.Green -> "green"
|
||||||
|
ThemeColors.Purple -> "purple"
|
||||||
|
ThemeColors.Orange -> "orange"
|
||||||
|
ThemeColors.Pink -> "pink"
|
||||||
|
ThemeColors.Gray -> "gray"
|
||||||
|
ThemeColors.Yellow -> "yellow"
|
||||||
|
else -> "default"
|
||||||
|
})
|
||||||
|
showThemeColorDialog = false
|
||||||
|
}
|
||||||
|
.padding(vertical = 12.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
RadioButton(
|
||||||
|
selected = ThemeConfig.currentTheme::class == theme::class,
|
||||||
|
onClick = null
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(12.dp))
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(24.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(theme.Primary)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(12.dp))
|
||||||
|
Text(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(
|
||||||
|
onClick = { showThemeColorDialog = false }
|
||||||
|
) {
|
||||||
|
Text(stringResource(R.string.cancel))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SectionHeader(
|
||||||
|
title: String,
|
||||||
|
expanded: Boolean,
|
||||||
|
onToggle: () -> Unit
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable { onToggle() }
|
||||||
|
.padding(vertical = 12.dp, horizontal = 4.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
style = MaterialTheme.typography.titleMedium.copy(
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
),
|
||||||
|
color = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
Icon(
|
||||||
|
imageVector = if (expanded) Icons.Default.KeyboardArrowUp else Icons.Default.KeyboardArrowDown,
|
||||||
|
contentDescription = if (expanded)
|
||||||
|
stringResource(R.string.collapse)
|
||||||
|
else
|
||||||
|
stringResource(R.string.expand),
|
||||||
|
tint = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
722
manager/app/src/main/java/com/sukisu/ultra/ui/screen/Settings.kt
Normal file
722
manager/app/src/main/java/com/sukisu/ultra/ui/screen/Settings.kt
Normal file
@@ -0,0 +1,722 @@
|
|||||||
|
package com.sukisu.ultra.ui.screen
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.Undo
|
||||||
|
import androidx.compose.material.icons.filled.*
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.core.content.FileProvider
|
||||||
|
import androidx.core.content.edit
|
||||||
|
import com.maxkeppeker.sheets.core.models.base.IconSource
|
||||||
|
import com.maxkeppeler.sheets.list.models.ListOption
|
||||||
|
import com.ramcosta.composedestinations.annotation.Destination
|
||||||
|
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||||
|
import com.ramcosta.composedestinations.generated.destinations.AppProfileTemplateScreenDestination
|
||||||
|
import com.ramcosta.composedestinations.generated.destinations.FlashScreenDestination
|
||||||
|
import com.ramcosta.composedestinations.generated.destinations.MoreSettingsScreenDestination
|
||||||
|
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import com.sukisu.ultra.BuildConfig
|
||||||
|
import com.sukisu.ultra.Natives
|
||||||
|
import com.sukisu.ultra.R
|
||||||
|
import com.sukisu.ultra.*
|
||||||
|
import com.sukisu.ultra.ui.component.*
|
||||||
|
import com.sukisu.ultra.ui.theme.*
|
||||||
|
import com.sukisu.ultra.ui.theme.CardConfig.cardAlpha
|
||||||
|
import com.sukisu.ultra.ui.theme.CardConfig.cardElevation
|
||||||
|
import com.sukisu.ultra.ui.util.LocalSnackbarHost
|
||||||
|
import com.sukisu.ultra.ui.util.getBugreportFile
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
import java.time.format.DateTimeFormatter
|
||||||
|
import com.sukisu.ultra.ui.component.KsuIsValid
|
||||||
|
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Destination<RootGraph>
|
||||||
|
@Composable
|
||||||
|
fun SettingScreen(navigator: DestinationsNavigator) {
|
||||||
|
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||||
|
val snackBarHost = LocalSnackbarHost.current
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
TopBar(
|
||||||
|
scrollBehavior = scrollBehavior
|
||||||
|
)
|
||||||
|
},
|
||||||
|
snackbarHost = { SnackbarHost(snackBarHost) },
|
||||||
|
contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal)
|
||||||
|
) { paddingValues ->
|
||||||
|
val aboutDialog = rememberCustomDialog {
|
||||||
|
AboutDialog(it)
|
||||||
|
}
|
||||||
|
val loadingDialog = rememberLoadingDialog()
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(paddingValues)
|
||||||
|
.nestedScroll(scrollBehavior.nestedScrollConnection)
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
) {
|
||||||
|
// region 上下文与协程
|
||||||
|
val context = LocalContext.current
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// region 日志导出功能
|
||||||
|
val exportBugreportLauncher = rememberLauncherForActivityResult(
|
||||||
|
ActivityResultContracts.CreateDocument("application/gzip")
|
||||||
|
) { uri: Uri? ->
|
||||||
|
if (uri == null) return@rememberLauncherForActivityResult
|
||||||
|
scope.launch(Dispatchers.IO) {
|
||||||
|
loadingDialog.show()
|
||||||
|
context.contentResolver.openOutputStream(uri)?.use { output ->
|
||||||
|
getBugreportFile(context).inputStream().use {
|
||||||
|
it.copyTo(output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
loadingDialog.hide()
|
||||||
|
snackBarHost.showSnackbar(context.getString(R.string.log_saved))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 配置
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceContainerLow.copy(alpha = cardAlpha)
|
||||||
|
),
|
||||||
|
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation)
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.padding(vertical = 8.dp)) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.configuration),
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
// 配置文件模板入口
|
||||||
|
val profileTemplate = stringResource(id = R.string.settings_profile_template)
|
||||||
|
KsuIsValid {
|
||||||
|
SettingItem(
|
||||||
|
icon = Icons.Filled.Fence,
|
||||||
|
title = profileTemplate,
|
||||||
|
summary = stringResource(id = R.string.settings_profile_template_summary),
|
||||||
|
onClick = {
|
||||||
|
navigator.navigate(AppProfileTemplateScreenDestination)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 卸载模块开关
|
||||||
|
var umountChecked by rememberSaveable {
|
||||||
|
mutableStateOf(Natives.isDefaultUmountModules())
|
||||||
|
}
|
||||||
|
|
||||||
|
KsuIsValid {
|
||||||
|
SwitchSettingItem(
|
||||||
|
icon = Icons.Filled.FolderDelete,
|
||||||
|
title = stringResource(id = R.string.settings_umount_modules_default),
|
||||||
|
summary = stringResource(id = R.string.settings_umount_modules_default_summary),
|
||||||
|
checked = umountChecked,
|
||||||
|
onCheckedChange = {
|
||||||
|
if (Natives.setDefaultUmountModules(it)) {
|
||||||
|
umountChecked = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SU 禁用开关(仅在兼容版本显示)
|
||||||
|
KsuIsValid {
|
||||||
|
if (Natives.version >= Natives.MINIMAL_SUPPORTED_SU_COMPAT) {
|
||||||
|
var isSuDisabled by rememberSaveable {
|
||||||
|
mutableStateOf(!Natives.isSuEnabled())
|
||||||
|
}
|
||||||
|
SwitchSettingItem(
|
||||||
|
icon = Icons.Filled.RemoveModerator,
|
||||||
|
title = stringResource(id = R.string.settings_disable_su),
|
||||||
|
summary = stringResource(id = R.string.settings_disable_su_summary),
|
||||||
|
checked = isSuDisabled,
|
||||||
|
onCheckedChange = { checked ->
|
||||||
|
val shouldEnable = !checked
|
||||||
|
if (Natives.setSuEnabled(shouldEnable)) {
|
||||||
|
isSuDisabled = !shouldEnable
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置分组卡片 - 应用设置
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceContainerLow.copy(alpha = cardAlpha)
|
||||||
|
),
|
||||||
|
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation)
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.padding(vertical = 8.dp)) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.app_settings),
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||||
|
|
||||||
|
// 更新检查开关
|
||||||
|
var checkUpdate by rememberSaveable {
|
||||||
|
mutableStateOf(
|
||||||
|
prefs.getBoolean("check_update", true)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
SwitchSettingItem(
|
||||||
|
icon = Icons.Filled.Update,
|
||||||
|
title = stringResource(id = R.string.settings_check_update),
|
||||||
|
summary = stringResource(id = R.string.settings_check_update_summary),
|
||||||
|
checked = checkUpdate,
|
||||||
|
onCheckedChange = {
|
||||||
|
prefs.edit {putBoolean("check_update", it) }
|
||||||
|
checkUpdate = it
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Web调试开关
|
||||||
|
var enableWebDebugging by rememberSaveable {
|
||||||
|
mutableStateOf(
|
||||||
|
prefs.getBoolean("enable_web_debugging", false)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
KsuIsValid {
|
||||||
|
SwitchSettingItem(
|
||||||
|
icon = Icons.Filled.DeveloperMode,
|
||||||
|
title = stringResource(id = R.string.enable_web_debugging),
|
||||||
|
summary = stringResource(id = R.string.enable_web_debugging_summary),
|
||||||
|
checked = enableWebDebugging,
|
||||||
|
onCheckedChange = {
|
||||||
|
prefs.edit { putBoolean("enable_web_debugging", it) }
|
||||||
|
enableWebDebugging = it
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更多设置
|
||||||
|
SettingItem(
|
||||||
|
icon = Icons.Filled.Settings,
|
||||||
|
title = stringResource(id = R.string.more_settings),
|
||||||
|
summary = stringResource(id = R.string.more_settings),
|
||||||
|
onClick = {
|
||||||
|
navigator.navigate(MoreSettingsScreenDestination)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置分组卡片 - 工具
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceContainerLow.copy(alpha = cardAlpha)
|
||||||
|
),
|
||||||
|
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation)
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.padding(vertical = 8.dp)) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.tools),
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
var showBottomsheet by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
SettingItem(
|
||||||
|
icon = Icons.Filled.BugReport,
|
||||||
|
title = stringResource(id = R.string.send_log),
|
||||||
|
onClick = {
|
||||||
|
showBottomsheet = true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (showBottomsheet) {
|
||||||
|
ModalBottomSheet(
|
||||||
|
onDismissRequest = { showBottomsheet = false },
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceEvenly
|
||||||
|
) {
|
||||||
|
LogActionButton(
|
||||||
|
icon = Icons.Filled.Save,
|
||||||
|
text = stringResource(R.string.save_log),
|
||||||
|
onClick = {
|
||||||
|
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH_mm")
|
||||||
|
val current = LocalDateTime.now().format(formatter)
|
||||||
|
exportBugreportLauncher.launch("KernelSU_bugreport_${current}.tar.gz")
|
||||||
|
showBottomsheet = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
LogActionButton(
|
||||||
|
icon = Icons.Filled.Share,
|
||||||
|
text = stringResource(R.string.send_log),
|
||||||
|
onClick = {
|
||||||
|
scope.launch {
|
||||||
|
val bugreport = loadingDialog.withLoading {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
getBugreportFile(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val uri: Uri =
|
||||||
|
FileProvider.getUriForFile(
|
||||||
|
context,
|
||||||
|
"${BuildConfig.APPLICATION_ID}.fileprovider",
|
||||||
|
bugreport
|
||||||
|
)
|
||||||
|
|
||||||
|
val shareIntent = Intent(Intent.ACTION_SEND).apply {
|
||||||
|
putExtra(Intent.EXTRA_STREAM, uri)
|
||||||
|
setDataAndType(uri, "application/gzip")
|
||||||
|
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
|
}
|
||||||
|
|
||||||
|
context.startActivity(
|
||||||
|
Intent.createChooser(
|
||||||
|
shareIntent,
|
||||||
|
context.getString(R.string.send_log)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
showBottomsheet = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val lkmMode = Natives.version >= Natives.MINIMAL_SUPPORTED_KERNEL_LKM && Natives.isLkmMode
|
||||||
|
if (lkmMode) {
|
||||||
|
UninstallItem(navigator) {
|
||||||
|
loadingDialog.withLoading(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置分组卡片 - 关于
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceContainerLow.copy(alpha = cardAlpha)
|
||||||
|
),
|
||||||
|
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation)
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.padding(vertical = 8.dp)) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.about),
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
SettingItem(
|
||||||
|
icon = Icons.Filled.Info,
|
||||||
|
title = stringResource(R.string.about),
|
||||||
|
onClick = {
|
||||||
|
aboutDialog.show()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LogActionButton(
|
||||||
|
icon: ImageVector,
|
||||||
|
text: String,
|
||||||
|
onClick: () -> Unit
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
modifier = Modifier
|
||||||
|
.clickable(onClick = onClick)
|
||||||
|
.padding(8.dp)
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
modifier = Modifier
|
||||||
|
.size(56.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(MaterialTheme.colorScheme.primaryContainer)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = icon,
|
||||||
|
contentDescription = text,
|
||||||
|
tint = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SettingItem(
|
||||||
|
icon: ImageVector,
|
||||||
|
title: String,
|
||||||
|
summary: String? = null,
|
||||||
|
onClick: () -> Unit
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable(onClick = onClick)
|
||||||
|
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = icon,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(end = 16.dp)
|
||||||
|
.size(24.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
|
)
|
||||||
|
if (summary != null) {
|
||||||
|
Text(
|
||||||
|
text = summary,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.ChevronRight,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SwitchSettingItem(
|
||||||
|
icon: ImageVector,
|
||||||
|
title: String,
|
||||||
|
summary: String? = null,
|
||||||
|
checked: Boolean,
|
||||||
|
onCheckedChange: (Boolean) -> Unit
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable { onCheckedChange(!checked) }
|
||||||
|
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = icon,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(end = 16.dp)
|
||||||
|
.size(24.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
|
)
|
||||||
|
if (summary != null) {
|
||||||
|
Text(
|
||||||
|
text = summary,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Switch(
|
||||||
|
checked = checked,
|
||||||
|
onCheckedChange = onCheckedChange,
|
||||||
|
colors = SwitchDefaults.colors(
|
||||||
|
checkedThumbColor = MaterialTheme.colorScheme.onPrimary,
|
||||||
|
checkedTrackColor = MaterialTheme.colorScheme.primary,
|
||||||
|
checkedIconColor = MaterialTheme.colorScheme.primary,
|
||||||
|
uncheckedThumbColor = MaterialTheme.colorScheme.outline,
|
||||||
|
uncheckedTrackColor = MaterialTheme.colorScheme.surfaceVariant,
|
||||||
|
uncheckedIconColor = MaterialTheme.colorScheme.surfaceVariant
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun UninstallItem(
|
||||||
|
navigator: DestinationsNavigator,
|
||||||
|
withLoading: suspend (suspend () -> Unit) -> Unit
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
val uninstallConfirmDialog = rememberConfirmDialog()
|
||||||
|
val showTodo = {
|
||||||
|
Toast.makeText(context, "TODO", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
val uninstallDialog = rememberUninstallDialog { uninstallType ->
|
||||||
|
scope.launch {
|
||||||
|
val result = uninstallConfirmDialog.awaitConfirm(
|
||||||
|
title = context.getString(uninstallType.title),
|
||||||
|
content = context.getString(uninstallType.message)
|
||||||
|
)
|
||||||
|
if (result == ConfirmResult.Confirmed) {
|
||||||
|
withLoading {
|
||||||
|
when (uninstallType) {
|
||||||
|
UninstallType.TEMPORARY -> showTodo()
|
||||||
|
UninstallType.PERMANENT -> navigator.navigate(
|
||||||
|
FlashScreenDestination(FlashIt.FlashUninstall)
|
||||||
|
)
|
||||||
|
UninstallType.RESTORE_STOCK_IMAGE -> navigator.navigate(
|
||||||
|
FlashScreenDestination(FlashIt.FlashRestore)
|
||||||
|
)
|
||||||
|
UninstallType.NONE -> Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingItem(
|
||||||
|
icon = Icons.Filled.Delete,
|
||||||
|
title = stringResource(id = R.string.settings_uninstall),
|
||||||
|
onClick = {
|
||||||
|
uninstallDialog.show()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class UninstallType(val title: Int, val message: Int, val icon: ImageVector) {
|
||||||
|
TEMPORARY(
|
||||||
|
R.string.settings_uninstall_temporary,
|
||||||
|
R.string.settings_uninstall_temporary_message,
|
||||||
|
Icons.Filled.Delete
|
||||||
|
),
|
||||||
|
PERMANENT(
|
||||||
|
R.string.settings_uninstall_permanent,
|
||||||
|
R.string.settings_uninstall_permanent_message,
|
||||||
|
Icons.Filled.DeleteForever
|
||||||
|
),
|
||||||
|
RESTORE_STOCK_IMAGE(
|
||||||
|
R.string.settings_restore_stock_image,
|
||||||
|
R.string.settings_restore_stock_image_message,
|
||||||
|
Icons.AutoMirrored.Filled.Undo
|
||||||
|
),
|
||||||
|
NONE(0, 0, Icons.Filled.Delete)
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun rememberUninstallDialog(onSelected: (UninstallType) -> Unit): DialogHandle {
|
||||||
|
return rememberCustomDialog { dismiss ->
|
||||||
|
val options = listOf(
|
||||||
|
// UninstallType.TEMPORARY,
|
||||||
|
UninstallType.PERMANENT,
|
||||||
|
UninstallType.RESTORE_STOCK_IMAGE
|
||||||
|
)
|
||||||
|
val listOptions = options.map {
|
||||||
|
ListOption(
|
||||||
|
titleText = stringResource(it.title),
|
||||||
|
subtitleText = if (it.message != 0) stringResource(it.message) else null,
|
||||||
|
icon = IconSource(it.icon)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
var selection = UninstallType.NONE
|
||||||
|
val cardColor = if (!ThemeConfig.useDynamicColor) {
|
||||||
|
ThemeConfig.currentTheme.ButtonContrast
|
||||||
|
} else {
|
||||||
|
MaterialTheme.colorScheme.surfaceContainerHigh
|
||||||
|
}
|
||||||
|
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = {
|
||||||
|
dismiss()
|
||||||
|
},
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.settings_uninstall),
|
||||||
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
|
)
|
||||||
|
},
|
||||||
|
text = {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(vertical = 8.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
) {
|
||||||
|
listOptions.forEachIndexed { index, option ->
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clip(MaterialTheme.shapes.medium)
|
||||||
|
.clickable {
|
||||||
|
selection = options[index]
|
||||||
|
}
|
||||||
|
.padding(vertical = 12.dp, horizontal = 8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = options[index].icon,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(end = 16.dp)
|
||||||
|
.size(24.dp)
|
||||||
|
)
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
text = option.titleText,
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
|
)
|
||||||
|
option.subtitleText?.let {
|
||||||
|
Text(
|
||||||
|
text = it,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
if (selection != UninstallType.NONE) {
|
||||||
|
onSelected(selection)
|
||||||
|
}
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(android.R.string.ok),
|
||||||
|
color = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(android.R.string.cancel),
|
||||||
|
color = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
containerColor = cardColor,
|
||||||
|
shape = MaterialTheme.shapes.extraLarge,
|
||||||
|
tonalElevation = 4.dp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
private fun TopBar(
|
||||||
|
scrollBehavior: TopAppBarScrollBehavior? = null
|
||||||
|
) {
|
||||||
|
val systemIsDark = isSystemInDarkTheme()
|
||||||
|
val cardColor = MaterialTheme.colorScheme.surfaceVariant
|
||||||
|
val cardAlpha = if (ThemeConfig.customBackgroundUri != null) {
|
||||||
|
cardAlpha
|
||||||
|
} else {
|
||||||
|
if (systemIsDark) 0.8f else 1f
|
||||||
|
}
|
||||||
|
|
||||||
|
TopAppBar(
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.settings),
|
||||||
|
style = MaterialTheme.typography.titleLarge
|
||||||
|
)
|
||||||
|
},
|
||||||
|
colors = TopAppBarDefaults.topAppBarColors(
|
||||||
|
containerColor = cardColor.copy(alpha = cardAlpha),
|
||||||
|
scrolledContainerColor = cardColor.copy(alpha = cardAlpha)
|
||||||
|
),
|
||||||
|
windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),
|
||||||
|
scrollBehavior = scrollBehavior
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,589 @@
|
|||||||
|
package com.sukisu.ultra.ui.screen
|
||||||
|
|
||||||
|
import androidx.compose.animation.*
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.gestures.detectTapGestures
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.ExperimentalMaterialApi
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.*
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.draw.shadow
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import com.sukisu.ultra.R
|
||||||
|
import coil.compose.AsyncImage
|
||||||
|
import coil.request.ImageRequest
|
||||||
|
import com.ramcosta.composedestinations.annotation.Destination
|
||||||
|
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||||
|
import com.ramcosta.composedestinations.generated.destinations.AppProfileScreenDestination
|
||||||
|
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import com.sukisu.ultra.Natives
|
||||||
|
import com.sukisu.ultra.ui.component.SearchAppBar
|
||||||
|
import com.sukisu.ultra.ui.theme.CardConfig.cardElevation
|
||||||
|
import com.sukisu.ultra.ui.util.ModuleModify
|
||||||
|
import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterialApi::class, ExperimentalMaterial3Api::class)
|
||||||
|
@Destination<RootGraph>
|
||||||
|
@Composable
|
||||||
|
fun SuperUserScreen(navigator: DestinationsNavigator) {
|
||||||
|
val viewModel = viewModel<SuperUserViewModel>()
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||||
|
val listState = rememberLazyListState()
|
||||||
|
val context = LocalContext.current
|
||||||
|
val snackBarHostState = remember { SnackbarHostState() }
|
||||||
|
|
||||||
|
// 添加备份和还原启动器
|
||||||
|
val backupLauncher = ModuleModify.rememberAllowlistBackupLauncher(context, snackBarHostState)
|
||||||
|
val restoreLauncher = ModuleModify.rememberAllowlistRestoreLauncher(context, snackBarHostState)
|
||||||
|
|
||||||
|
LaunchedEffect(key1 = navigator) {
|
||||||
|
viewModel.search = ""
|
||||||
|
if (viewModel.appList.isEmpty()) {
|
||||||
|
viewModel.fetchAppList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(viewModel.search) {
|
||||||
|
if (viewModel.search.isEmpty()) {
|
||||||
|
listState.scrollToItem(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
SearchAppBar(
|
||||||
|
title = { Text(stringResource(R.string.superuser)) },
|
||||||
|
searchText = viewModel.search,
|
||||||
|
onSearchTextChange = { viewModel.search = it },
|
||||||
|
onClearClick = { viewModel.search = "" },
|
||||||
|
dropdownContent = {
|
||||||
|
var showDropdown by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
IconButton(
|
||||||
|
onClick = { showDropdown = true },
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.MoreVert,
|
||||||
|
contentDescription = stringResource(id = R.string.settings),
|
||||||
|
)
|
||||||
|
|
||||||
|
DropdownMenu(expanded = showDropdown, onDismissRequest = {
|
||||||
|
showDropdown = false
|
||||||
|
}) {
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text(stringResource(R.string.refresh)) },
|
||||||
|
leadingIcon = {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.Refresh,
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
scope.launch {
|
||||||
|
viewModel.fetchAppList()
|
||||||
|
}
|
||||||
|
showDropdown = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = {
|
||||||
|
Text(
|
||||||
|
if (viewModel.showSystemApps) {
|
||||||
|
stringResource(R.string.hide_system_apps)
|
||||||
|
} else {
|
||||||
|
stringResource(R.string.show_system_apps)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
leadingIcon = {
|
||||||
|
Icon(
|
||||||
|
imageVector = if (viewModel.showSystemApps)
|
||||||
|
Icons.Filled.VisibilityOff else Icons.Filled.Visibility,
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
viewModel.showSystemApps = !viewModel.showSystemApps
|
||||||
|
showDropdown = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
HorizontalDivider(thickness = 0.5.dp, modifier = Modifier.padding(vertical = 4.dp))
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text(stringResource(R.string.backup_allowlist)) },
|
||||||
|
leadingIcon = {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.Save,
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
backupLauncher.launch(ModuleModify.createAllowlistBackupIntent())
|
||||||
|
showDropdown = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text(stringResource(R.string.restore_allowlist)) },
|
||||||
|
leadingIcon = {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.RestoreFromTrash,
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
restoreLauncher.launch(ModuleModify.createAllowlistRestoreIntent())
|
||||||
|
showDropdown = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scrollBehavior = scrollBehavior
|
||||||
|
)
|
||||||
|
},
|
||||||
|
snackbarHost = { SnackbarHost(snackBarHostState) },
|
||||||
|
contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),
|
||||||
|
bottomBar = {
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = viewModel.showBatchActions && viewModel.selectedApps.isNotEmpty(),
|
||||||
|
enter = slideInVertically(initialOffsetY = { it }),
|
||||||
|
exit = slideOutVertically(targetOffsetY = { it })
|
||||||
|
) {
|
||||||
|
Surface(
|
||||||
|
color = MaterialTheme.colorScheme.surfaceContainerHighest,
|
||||||
|
tonalElevation = cardElevation,
|
||||||
|
shadowElevation = cardElevation
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceEvenly
|
||||||
|
) {
|
||||||
|
OutlinedButton(
|
||||||
|
onClick = {
|
||||||
|
viewModel.selectedApps = emptySet()
|
||||||
|
viewModel.showBatchActions = false
|
||||||
|
},
|
||||||
|
colors = ButtonDefaults.outlinedButtonColors(
|
||||||
|
contentColor = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.Close,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(16.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(6.dp))
|
||||||
|
Text(stringResource(android.R.string.cancel))
|
||||||
|
}
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
scope.launch {
|
||||||
|
viewModel.updateBatchPermissions(true)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.Check,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(16.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(6.dp))
|
||||||
|
Text(stringResource(R.string.batch_authorization))
|
||||||
|
}
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
scope.launch {
|
||||||
|
viewModel.updateBatchPermissions(false)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.error
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.Block,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(16.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(6.dp))
|
||||||
|
Text(stringResource(R.string.batch_cancel_authorization))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) { innerPadding ->
|
||||||
|
PullToRefreshBox(
|
||||||
|
modifier = Modifier.padding(innerPadding),
|
||||||
|
onRefresh = {
|
||||||
|
scope.launch { viewModel.fetchAppList() }
|
||||||
|
},
|
||||||
|
isRefreshing = viewModel.isRefreshing
|
||||||
|
) {
|
||||||
|
LazyColumn(
|
||||||
|
state = listState,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||||
|
contentPadding = PaddingValues(
|
||||||
|
top = 8.dp,
|
||||||
|
bottom = if (viewModel.showBatchActions && viewModel.selectedApps.isNotEmpty()) 88.dp else 16.dp
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
// 获取分组后的应用列表
|
||||||
|
val rootApps = viewModel.appList.filter { it.allowSu }
|
||||||
|
val customApps = viewModel.appList.filter { !it.allowSu && it.hasCustomProfile }
|
||||||
|
val otherApps = viewModel.appList.filter { !it.allowSu && !it.hasCustomProfile }
|
||||||
|
|
||||||
|
// 显示ROOT权限应用组
|
||||||
|
if (rootApps.isNotEmpty()) {
|
||||||
|
item {
|
||||||
|
GroupHeader(title = stringResource(R.string.apps_with_root))
|
||||||
|
}
|
||||||
|
|
||||||
|
items(rootApps, key = { "root_" + it.packageName + it.uid }) { app ->
|
||||||
|
AppItem(
|
||||||
|
app = app,
|
||||||
|
isSelected = viewModel.selectedApps.contains(app.packageName),
|
||||||
|
onToggleSelection = { viewModel.toggleAppSelection(app.packageName) },
|
||||||
|
onSwitchChange = { allowSu ->
|
||||||
|
scope.launch {
|
||||||
|
val profile = Natives.getAppProfile(app.packageName, app.uid)
|
||||||
|
val updatedProfile = profile.copy(allowSu = allowSu)
|
||||||
|
if (Natives.setAppProfile(updatedProfile)) {
|
||||||
|
viewModel.fetchAppList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
if (viewModel.showBatchActions) {
|
||||||
|
viewModel.toggleAppSelection(app.packageName)
|
||||||
|
} else {
|
||||||
|
navigator.navigate(AppProfileScreenDestination(app))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onLongClick = {
|
||||||
|
// 长按进入多选模式
|
||||||
|
if (!viewModel.showBatchActions) {
|
||||||
|
viewModel.toggleBatchMode()
|
||||||
|
viewModel.toggleAppSelection(app.packageName)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
viewModel = viewModel
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示自定义配置应用组
|
||||||
|
if (customApps.isNotEmpty()) {
|
||||||
|
item {
|
||||||
|
GroupHeader(title = stringResource(R.string.apps_with_custom_profile))
|
||||||
|
}
|
||||||
|
|
||||||
|
items(customApps, key = { "custom_" + it.packageName + it.uid }) { app ->
|
||||||
|
AppItem(
|
||||||
|
app = app,
|
||||||
|
isSelected = viewModel.selectedApps.contains(app.packageName),
|
||||||
|
onToggleSelection = { viewModel.toggleAppSelection(app.packageName) },
|
||||||
|
onSwitchChange = { allowSu ->
|
||||||
|
scope.launch {
|
||||||
|
val profile = Natives.getAppProfile(app.packageName, app.uid)
|
||||||
|
val updatedProfile = profile.copy(allowSu = allowSu)
|
||||||
|
if (Natives.setAppProfile(updatedProfile)) {
|
||||||
|
viewModel.fetchAppList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
if (viewModel.showBatchActions) {
|
||||||
|
viewModel.toggleAppSelection(app.packageName)
|
||||||
|
} else {
|
||||||
|
navigator.navigate(AppProfileScreenDestination(app))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onLongClick = {
|
||||||
|
// 长按进入多选模式
|
||||||
|
if (!viewModel.showBatchActions) {
|
||||||
|
viewModel.toggleBatchMode()
|
||||||
|
viewModel.toggleAppSelection(app.packageName)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
viewModel = viewModel
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示其他应用组
|
||||||
|
if (otherApps.isNotEmpty()) {
|
||||||
|
item {
|
||||||
|
GroupHeader(title = stringResource(R.string.other_apps))
|
||||||
|
}
|
||||||
|
|
||||||
|
items(otherApps, key = { "other_" + it.packageName + it.uid }) { app ->
|
||||||
|
AppItem(
|
||||||
|
app = app,
|
||||||
|
isSelected = viewModel.selectedApps.contains(app.packageName),
|
||||||
|
onToggleSelection = { viewModel.toggleAppSelection(app.packageName) },
|
||||||
|
onSwitchChange = { allowSu ->
|
||||||
|
scope.launch {
|
||||||
|
val profile = Natives.getAppProfile(app.packageName, app.uid)
|
||||||
|
val updatedProfile = profile.copy(allowSu = allowSu)
|
||||||
|
if (Natives.setAppProfile(updatedProfile)) {
|
||||||
|
viewModel.fetchAppList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
if (viewModel.showBatchActions) {
|
||||||
|
viewModel.toggleAppSelection(app.packageName)
|
||||||
|
} else {
|
||||||
|
navigator.navigate(AppProfileScreenDestination(app))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onLongClick = {
|
||||||
|
// 长按进入多选模式
|
||||||
|
if (!viewModel.showBatchActions) {
|
||||||
|
viewModel.toggleBatchMode()
|
||||||
|
viewModel.toggleAppSelection(app.packageName)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
viewModel = viewModel
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 当没有应用显示时显示空状态
|
||||||
|
if (viewModel.appList.isEmpty()) {
|
||||||
|
item {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(400.dp),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.Apps,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.6f),
|
||||||
|
modifier = Modifier
|
||||||
|
.size(96.dp)
|
||||||
|
.padding(bottom = 16.dp)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.no_apps_found),
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun GroupHeader(title: String) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(MaterialTheme.colorScheme.surfaceContainerHighest.copy(alpha = 0.7f))
|
||||||
|
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalLayoutApi::class)
|
||||||
|
@Composable
|
||||||
|
private fun AppItem(
|
||||||
|
app: SuperUserViewModel.AppInfo,
|
||||||
|
isSelected: Boolean,
|
||||||
|
onToggleSelection: () -> Unit,
|
||||||
|
onSwitchChange: (Boolean) -> Unit,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
onLongClick: () -> Unit,
|
||||||
|
viewModel: SuperUserViewModel
|
||||||
|
) {
|
||||||
|
val cardColor = if (app.allowSu)
|
||||||
|
MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f)
|
||||||
|
else if (app.hasCustomProfile)
|
||||||
|
MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.3f)
|
||||||
|
else
|
||||||
|
MaterialTheme.colorScheme.surfaceContainerLow
|
||||||
|
|
||||||
|
Card(
|
||||||
|
colors = CardDefaults.cardColors(containerColor = cardColor),
|
||||||
|
elevation = CardDefaults.cardElevation(defaultElevation = 0.dp),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 8.dp, vertical = 4.dp)
|
||||||
|
.clip(MaterialTheme.shapes.medium)
|
||||||
|
.shadow(
|
||||||
|
elevation = 0.dp,
|
||||||
|
shape = MaterialTheme.shapes.medium,
|
||||||
|
spotColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
|
||||||
|
)
|
||||||
|
.then(
|
||||||
|
if (isSelected)
|
||||||
|
Modifier.border(
|
||||||
|
width = 2.dp,
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
shape = MaterialTheme.shapes.medium
|
||||||
|
)
|
||||||
|
else
|
||||||
|
Modifier
|
||||||
|
)
|
||||||
|
.pointerInput(Unit) {
|
||||||
|
detectTapGestures(
|
||||||
|
onLongPress = { onLongClick() },
|
||||||
|
onTap = { onClick() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
AsyncImage(
|
||||||
|
model = ImageRequest.Builder(LocalContext.current)
|
||||||
|
.data(app.packageInfo)
|
||||||
|
.crossfade(true)
|
||||||
|
.build(),
|
||||||
|
contentDescription = app.label,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(end = 16.dp)
|
||||||
|
.size(48.dp)
|
||||||
|
.clip(MaterialTheme.shapes.small)
|
||||||
|
)
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.padding(end = 8.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = app.label,
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = app.packageName,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
|
||||||
|
FlowRow(
|
||||||
|
modifier = Modifier.padding(top = 4.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||||
|
) {
|
||||||
|
if (app.allowSu) {
|
||||||
|
LabelText(label = "ROOT", backgroundColor = MaterialTheme.colorScheme.primary)
|
||||||
|
}
|
||||||
|
if (Natives.uidShouldUmount(app.uid)) {
|
||||||
|
LabelText(label = "UMOUNT", backgroundColor = MaterialTheme.colorScheme.tertiary)
|
||||||
|
}
|
||||||
|
if (app.hasCustomProfile) {
|
||||||
|
LabelText(label = "CUSTOM", backgroundColor = MaterialTheme.colorScheme.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!viewModel.showBatchActions) {
|
||||||
|
Switch(
|
||||||
|
checked = app.allowSu,
|
||||||
|
onCheckedChange = onSwitchChange,
|
||||||
|
colors = SwitchDefaults.colors(
|
||||||
|
checkedThumbColor = MaterialTheme.colorScheme.onPrimary,
|
||||||
|
checkedTrackColor = MaterialTheme.colorScheme.primary,
|
||||||
|
checkedIconColor = MaterialTheme.colorScheme.primary,
|
||||||
|
uncheckedThumbColor = MaterialTheme.colorScheme.outline,
|
||||||
|
uncheckedTrackColor = MaterialTheme.colorScheme.surfaceVariant,
|
||||||
|
uncheckedIconColor = MaterialTheme.colorScheme.surfaceVariant
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Checkbox(
|
||||||
|
checked = isSelected,
|
||||||
|
onCheckedChange = { onToggleSelection() },
|
||||||
|
colors = CheckboxDefaults.colors(
|
||||||
|
checkedColor = MaterialTheme.colorScheme.primary,
|
||||||
|
uncheckedColor = MaterialTheme.colorScheme.outline
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LabelText(label: String, backgroundColor: Color) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(top = 2.dp, end = 2.dp)
|
||||||
|
.background(
|
||||||
|
backgroundColor,
|
||||||
|
shape = RoundedCornerShape(4.dp)
|
||||||
|
)
|
||||||
|
.clip(RoundedCornerShape(4.dp))
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = label,
|
||||||
|
modifier = Modifier.padding(vertical = 2.dp, horizontal = 6.dp),
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 10.sp,
|
||||||
|
color = Color.White,
|
||||||
|
fontWeight = FontWeight.Medium
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
package shirkneko.zako.sukisu.ui.screen
|
package com.sukisu.ultra.ui.screen
|
||||||
|
|
||||||
|
import android.content.ClipData
|
||||||
|
import android.content.ClipboardManager
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@@ -31,6 +33,7 @@ import androidx.compose.material3.MaterialTheme
|
|||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
|
import androidx.compose.material3.TopAppBarColors
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||||
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||||
@@ -44,11 +47,10 @@ import androidx.compose.runtime.rememberCoroutineScope
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.platform.LocalClipboardManager
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.core.content.getSystemService
|
||||||
import androidx.lifecycle.compose.dropUnlessResumed
|
import androidx.lifecycle.compose.dropUnlessResumed
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import com.ramcosta.composedestinations.annotation.Destination
|
import com.ramcosta.composedestinations.annotation.Destination
|
||||||
@@ -59,8 +61,9 @@ import com.ramcosta.composedestinations.result.ResultRecipient
|
|||||||
import com.ramcosta.composedestinations.result.getOr
|
import com.ramcosta.composedestinations.result.getOr
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import shirkneko.zako.sukisu.R
|
import com.sukisu.ultra.R
|
||||||
import shirkneko.zako.sukisu.ui.viewmodel.TemplateViewModel
|
import com.sukisu.ultra.ui.theme.CardConfig
|
||||||
|
import com.sukisu.ultra.ui.viewmodel.TemplateViewModel
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author weishu
|
* @author weishu
|
||||||
@@ -77,7 +80,6 @@ fun AppProfileTemplateScreen(
|
|||||||
val viewModel = viewModel<TemplateViewModel>()
|
val viewModel = viewModel<TemplateViewModel>()
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||||
val cardColor = MaterialTheme.colorScheme.secondaryContainer
|
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
if (viewModel.templateList.isEmpty()) {
|
if (viewModel.templateList.isEmpty()) {
|
||||||
@@ -92,10 +94,13 @@ fun AppProfileTemplateScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val cardColorUse = MaterialTheme.colorScheme.surfaceVariant
|
||||||
|
val cardAlpha = CardConfig.cardAlpha
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
val clipboardManager = LocalClipboardManager.current
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
val clipboardManager = context.getSystemService<ClipboardManager>()
|
||||||
val showToast = fun(msg: String) {
|
val showToast = fun(msg: String) {
|
||||||
scope.launch(Dispatchers.Main) {
|
scope.launch(Dispatchers.Main) {
|
||||||
Toast.makeText(context, msg, Toast.LENGTH_SHORT).show()
|
Toast.makeText(context, msg, Toast.LENGTH_SHORT).show()
|
||||||
@@ -103,25 +108,29 @@ fun AppProfileTemplateScreen(
|
|||||||
}
|
}
|
||||||
TopBar(
|
TopBar(
|
||||||
onBack = dropUnlessResumed { navigator.popBackStack() },
|
onBack = dropUnlessResumed { navigator.popBackStack() },
|
||||||
|
colors = TopAppBarDefaults.topAppBarColors(
|
||||||
|
containerColor = cardColorUse.copy(alpha = cardAlpha),
|
||||||
|
scrolledContainerColor = cardColorUse.copy(alpha = cardAlpha)
|
||||||
|
),
|
||||||
onSync = {
|
onSync = {
|
||||||
scope.launch { viewModel.fetchTemplates(true) }
|
scope.launch { viewModel.fetchTemplates(true) }
|
||||||
},
|
},
|
||||||
onImport = {
|
onImport = {
|
||||||
clipboardManager.getText()?.text?.let {
|
|
||||||
if (it.isEmpty()) {
|
|
||||||
showToast(context.getString(R.string.app_profile_template_import_empty))
|
|
||||||
return@let
|
|
||||||
}
|
|
||||||
scope.launch {
|
scope.launch {
|
||||||
|
val clipboardText = clipboardManager?.primaryClip?.getItemAt(0)?.text?.toString()
|
||||||
|
if (clipboardText.isNullOrEmpty()) {
|
||||||
|
showToast(context.getString(R.string.app_profile_template_import_empty))
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
viewModel.importTemplates(
|
viewModel.importTemplates(
|
||||||
it, {
|
clipboardText,
|
||||||
|
{
|
||||||
showToast(context.getString(R.string.app_profile_template_import_success))
|
showToast(context.getString(R.string.app_profile_template_import_success))
|
||||||
viewModel.fetchTemplates(false)
|
viewModel.fetchTemplates(false)
|
||||||
},
|
},
|
||||||
showToast
|
showToast
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
},
|
},
|
||||||
onExport = {
|
onExport = {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
@@ -129,8 +138,8 @@ fun AppProfileTemplateScreen(
|
|||||||
{
|
{
|
||||||
showToast(context.getString(R.string.app_profile_template_export_empty))
|
showToast(context.getString(R.string.app_profile_template_export_empty))
|
||||||
}
|
}
|
||||||
) {
|
) { text ->
|
||||||
clipboardManager.setText(AnnotatedString(it))
|
clipboardManager?.setPrimaryClip(ClipData.newPlainText("", text))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -149,7 +158,6 @@ fun AppProfileTemplateScreen(
|
|||||||
},
|
},
|
||||||
icon = { Icon(Icons.Filled.Add, null) },
|
icon = { Icon(Icons.Filled.Add, null) },
|
||||||
text = { Text(stringResource(id = R.string.app_profile_template_create)) },
|
text = { Text(stringResource(id = R.string.app_profile_template_create)) },
|
||||||
containerColor = cardColor.copy(alpha = 1f),
|
|
||||||
contentColor = MaterialTheme.colorScheme.onSecondaryContainer
|
contentColor = MaterialTheme.colorScheme.onSecondaryContainer
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -199,17 +207,17 @@ private fun TemplateItem(
|
|||||||
)
|
)
|
||||||
Text(template.description)
|
Text(template.description)
|
||||||
FlowRow {
|
FlowRow {
|
||||||
LabelText(label = "UID: ${template.uid}")
|
LabelText(label = "UID: ${template.uid}", backgroundColor = MaterialTheme.colorScheme.surface)
|
||||||
LabelText(label = "GID: ${template.gid}")
|
LabelText(label = "GID: ${template.gid}", backgroundColor = MaterialTheme.colorScheme.surface)
|
||||||
LabelText(label = template.context)
|
LabelText(label = template.context, backgroundColor = MaterialTheme.colorScheme.surface)
|
||||||
if (template.local) {
|
if (template.local) {
|
||||||
LabelText(label = "local")
|
LabelText(label = "local", backgroundColor = MaterialTheme.colorScheme.surface)
|
||||||
} else {
|
} else {
|
||||||
LabelText(label = "remote")
|
LabelText(label = "remote", backgroundColor = MaterialTheme.colorScheme.surface)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -220,12 +228,20 @@ private fun TopBar(
|
|||||||
onSync: () -> Unit = {},
|
onSync: () -> Unit = {},
|
||||||
onImport: () -> Unit = {},
|
onImport: () -> Unit = {},
|
||||||
onExport: () -> Unit = {},
|
onExport: () -> Unit = {},
|
||||||
|
colors: TopAppBarColors,
|
||||||
scrollBehavior: TopAppBarScrollBehavior? = null
|
scrollBehavior: TopAppBarScrollBehavior? = null
|
||||||
) {
|
) {
|
||||||
|
val cardColor = MaterialTheme.colorScheme.surfaceVariant
|
||||||
|
val cardAlpha = CardConfig.cardAlpha
|
||||||
|
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = {
|
title = {
|
||||||
Text(stringResource(R.string.settings_profile_template))
|
Text(stringResource(R.string.settings_profile_template))
|
||||||
},
|
},
|
||||||
|
colors = TopAppBarDefaults.topAppBarColors(
|
||||||
|
containerColor = cardColor.copy(alpha = cardAlpha),
|
||||||
|
scrolledContainerColor = cardColor.copy(alpha = cardAlpha)
|
||||||
|
),
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = onBack
|
onClick = onBack
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package shirkneko.zako.sukisu.ui.screen
|
package com.sukisu.ultra.ui.screen
|
||||||
|
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
@@ -47,14 +47,14 @@ import androidx.compose.ui.text.input.KeyboardType
|
|||||||
import com.ramcosta.composedestinations.annotation.Destination
|
import com.ramcosta.composedestinations.annotation.Destination
|
||||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||||
import com.ramcosta.composedestinations.result.ResultBackNavigator
|
import com.ramcosta.composedestinations.result.ResultBackNavigator
|
||||||
import shirkneko.zako.sukisu.Natives
|
import com.sukisu.ultra.Natives
|
||||||
import shirkneko.zako.sukisu.R
|
import com.sukisu.ultra.R
|
||||||
import shirkneko.zako.sukisu.ui.component.profile.RootProfileConfig
|
import com.sukisu.ultra.ui.component.profile.RootProfileConfig
|
||||||
import shirkneko.zako.sukisu.ui.util.deleteAppProfileTemplate
|
import com.sukisu.ultra.ui.util.deleteAppProfileTemplate
|
||||||
import shirkneko.zako.sukisu.ui.util.getAppProfileTemplate
|
import com.sukisu.ultra.ui.util.getAppProfileTemplate
|
||||||
import shirkneko.zako.sukisu.ui.util.setAppProfileTemplate
|
import com.sukisu.ultra.ui.util.setAppProfileTemplate
|
||||||
import shirkneko.zako.sukisu.ui.viewmodel.TemplateViewModel
|
import com.sukisu.ultra.ui.viewmodel.TemplateViewModel
|
||||||
import shirkneko.zako.sukisu.ui.viewmodel.toJSON
|
import com.sukisu.ultra.ui.viewmodel.toJSON
|
||||||
import androidx.lifecycle.compose.dropUnlessResumed
|
import androidx.lifecycle.compose.dropUnlessResumed
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package shirkneko.zako.sukisu.ui.theme
|
package com.sukisu.ultra.ui.theme
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
@@ -13,76 +13,95 @@ import androidx.compose.ui.unit.Dp
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
object CardConfig {
|
object CardConfig {
|
||||||
val defaultElevation: Dp = 0.dp
|
val settingElevation: Dp = 4.dp
|
||||||
|
val customBackgroundElevation: Dp = 0.dp
|
||||||
|
|
||||||
var cardAlpha by mutableStateOf(1f)
|
var cardAlpha by mutableStateOf(1f)
|
||||||
var cardElevation by mutableStateOf(defaultElevation)
|
var cardElevation by mutableStateOf(settingElevation)
|
||||||
var isShadowEnabled by mutableStateOf(true)
|
var isShadowEnabled by mutableStateOf(true)
|
||||||
var isCustomAlphaSet by mutableStateOf(false)
|
var isCustomAlphaSet by mutableStateOf(false)
|
||||||
var isUserDarkModeEnabled by mutableStateOf(false)
|
var isUserDarkModeEnabled by mutableStateOf(false)
|
||||||
var isUserLightModeEnabled by mutableStateOf(false)
|
var isUserLightModeEnabled by mutableStateOf(false)
|
||||||
var isCustomBackgroundEnabled by mutableStateOf(false)
|
var isCustomBackgroundEnabled by mutableStateOf(false)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存卡片配置到SharedPreferences
|
||||||
|
*/
|
||||||
fun save(context: Context) {
|
fun save(context: Context) {
|
||||||
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||||
prefs.edit().apply {
|
prefs.edit().apply {
|
||||||
putFloat("card_alpha", cardAlpha)
|
putFloat("card_alpha", cardAlpha)
|
||||||
putBoolean("custom_background_enabled", cardElevation == 0.dp)
|
putBoolean("custom_background_enabled", isCustomBackgroundEnabled)
|
||||||
|
putBoolean("is_shadow_enabled", isShadowEnabled)
|
||||||
putBoolean("is_custom_alpha_set", isCustomAlphaSet)
|
putBoolean("is_custom_alpha_set", isCustomAlphaSet)
|
||||||
putBoolean("is_user_dark_mode_enabled", isUserDarkModeEnabled)
|
putBoolean("is_user_dark_mode_enabled", isUserDarkModeEnabled)
|
||||||
putBoolean("is_user_light_mode_enabled", isUserLightModeEnabled)
|
putBoolean("is_user_light_mode_enabled", isUserLightModeEnabled)
|
||||||
putBoolean("is_custom_background_enabled", isCustomBackgroundEnabled)
|
|
||||||
apply()
|
apply()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从SharedPreferences加载卡片配置
|
||||||
|
*/
|
||||||
fun load(context: Context) {
|
fun load(context: Context) {
|
||||||
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||||
cardAlpha = prefs.getFloat("card_alpha", 1f)
|
cardAlpha = prefs.getFloat("card_alpha", 1f)
|
||||||
cardElevation = if (prefs.getBoolean("custom_background_enabled", false)) 0.dp else defaultElevation
|
isCustomBackgroundEnabled = prefs.getBoolean("custom_background_enabled", false)
|
||||||
|
isShadowEnabled = prefs.getBoolean("is_shadow_enabled", true)
|
||||||
isCustomAlphaSet = prefs.getBoolean("is_custom_alpha_set", false)
|
isCustomAlphaSet = prefs.getBoolean("is_custom_alpha_set", false)
|
||||||
isUserDarkModeEnabled = prefs.getBoolean("is_user_dark_mode_enabled", false)
|
isUserDarkModeEnabled = prefs.getBoolean("is_user_dark_mode_enabled", false)
|
||||||
isUserLightModeEnabled = prefs.getBoolean("is_user_light_mode_enabled", false)
|
isUserLightModeEnabled = prefs.getBoolean("is_user_light_mode_enabled", false)
|
||||||
isCustomBackgroundEnabled = prefs.getBoolean("is_custom_background_enabled", false)
|
updateShadowEnabled(isShadowEnabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新阴影启用状态
|
||||||
|
*/
|
||||||
fun updateShadowEnabled(enabled: Boolean) {
|
fun updateShadowEnabled(enabled: Boolean) {
|
||||||
isShadowEnabled = enabled
|
isShadowEnabled = enabled
|
||||||
cardElevation = if (enabled) defaultElevation else 0.dp
|
cardElevation = if (isCustomBackgroundEnabled && cardAlpha != 1f) {
|
||||||
|
customBackgroundElevation
|
||||||
|
} else if (enabled) {
|
||||||
|
settingElevation
|
||||||
|
} else {
|
||||||
|
customBackgroundElevation
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置深色模式默认值
|
||||||
|
*/
|
||||||
fun setDarkModeDefaults() {
|
fun setDarkModeDefaults() {
|
||||||
if (!isCustomAlphaSet) {
|
if (!isCustomAlphaSet) {
|
||||||
cardAlpha = 0.5f
|
cardAlpha = 1f
|
||||||
cardElevation = 0.dp
|
|
||||||
}
|
}
|
||||||
|
updateShadowEnabled(isShadowEnabled)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取卡片颜色配置
|
||||||
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun getCardColors(originalColor: Color) = CardDefaults.elevatedCardColors(
|
fun getCardColors(originalColor: Color) = CardDefaults.cardColors(
|
||||||
containerColor = originalColor.copy(alpha = CardConfig.cardAlpha),
|
containerColor = originalColor.copy(alpha = CardConfig.cardAlpha),
|
||||||
contentColor = when {
|
contentColor = determineContentColor(originalColor)
|
||||||
CardConfig.isUserLightModeEnabled -> {
|
|
||||||
Color.Black
|
|
||||||
}
|
|
||||||
CardConfig.isUserDarkModeEnabled -> {
|
|
||||||
Color.White
|
|
||||||
}
|
|
||||||
!isSystemInDarkTheme() && !CardConfig.isUserDarkModeEnabled -> {
|
|
||||||
Color.Black
|
|
||||||
}
|
|
||||||
!isSystemInDarkTheme() && !CardConfig.isCustomBackgroundEnabled && !CardConfig.isUserDarkModeEnabled && originalColor.luminance() > 0.3 -> {
|
|
||||||
Color.Black
|
|
||||||
}
|
|
||||||
isSystemInDarkTheme() && !CardConfig.isUserDarkModeEnabled && !CardConfig.isUserLightModeEnabled-> {
|
|
||||||
Color.White
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
Color.White
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
fun getCardElevation() = CardConfig.cardElevation
|
/**
|
||||||
|
* 根据背景颜色、主题模式和用户设置确定内容颜色
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
private fun determineContentColor(originalColor: Color): Color {
|
||||||
|
val isDarkTheme = isSystemInDarkTheme()
|
||||||
|
if (ThemeConfig.isThemeChanging) {
|
||||||
|
return if (isDarkTheme) Color.White else Color.Black
|
||||||
|
}
|
||||||
|
|
||||||
|
return when {
|
||||||
|
CardConfig.isUserLightModeEnabled -> Color.Black
|
||||||
|
!isDarkTheme && originalColor.luminance() > 0.5f -> Color.Black
|
||||||
|
isDarkTheme -> Color.White
|
||||||
|
else -> if (originalColor.luminance() > 0.5f) Color.Black else Color.White
|
||||||
|
}
|
||||||
|
}
|
||||||
273
manager/app/src/main/java/com/sukisu/ultra/ui/theme/Color.kt
Normal file
273
manager/app/src/main/java/com/sukisu/ultra/ui/theme/Color.kt
Normal file
@@ -0,0 +1,273 @@
|
|||||||
|
package com.sukisu.ultra.ui.theme
|
||||||
|
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
|
||||||
|
sealed class ThemeColors {
|
||||||
|
abstract val Primary: Color
|
||||||
|
abstract val Secondary: Color
|
||||||
|
abstract val Tertiary: Color
|
||||||
|
abstract val OnPrimary: Color
|
||||||
|
abstract val OnSecondary: Color
|
||||||
|
abstract val OnTertiary: Color
|
||||||
|
abstract val PrimaryContainer: Color
|
||||||
|
abstract val SecondaryContainer: Color
|
||||||
|
abstract val TertiaryContainer: Color
|
||||||
|
abstract val OnPrimaryContainer: Color
|
||||||
|
abstract val OnSecondaryContainer: Color
|
||||||
|
abstract val OnTertiaryContainer: Color
|
||||||
|
abstract val ButtonContrast: Color
|
||||||
|
|
||||||
|
// 表面颜色
|
||||||
|
abstract val Surface: Color
|
||||||
|
abstract val SurfaceVariant: Color
|
||||||
|
abstract val OnSurface: Color
|
||||||
|
abstract val OnSurfaceVariant: Color
|
||||||
|
|
||||||
|
// 错误状态颜色
|
||||||
|
abstract val Error: Color
|
||||||
|
abstract val OnError: Color
|
||||||
|
abstract val ErrorContainer: Color
|
||||||
|
abstract val OnErrorContainer: Color
|
||||||
|
|
||||||
|
// 边框和背景色
|
||||||
|
abstract val Outline: Color
|
||||||
|
abstract val OutlineVariant: Color
|
||||||
|
abstract val Background: Color
|
||||||
|
abstract val OnBackground: Color
|
||||||
|
|
||||||
|
// 默认主题 (蓝色)
|
||||||
|
object Default : ThemeColors() {
|
||||||
|
override val Primary = Color(0xFF2196F3)
|
||||||
|
override val Secondary = Color(0xFF64B5F6)
|
||||||
|
override val Tertiary = Color(0xFF0D47A1)
|
||||||
|
override val OnPrimary = Color(0xFFFFFFFF)
|
||||||
|
override val OnSecondary = Color(0xFFFFFFFF)
|
||||||
|
override val OnTertiary = Color(0xFFFFFFFF)
|
||||||
|
override val PrimaryContainer = Color(0xFFD6EAFF)
|
||||||
|
override val SecondaryContainer = Color(0xFFE3F2FD)
|
||||||
|
override val TertiaryContainer = Color(0xFFCFD8DC)
|
||||||
|
override val OnPrimaryContainer = Color(0xFF0A3049)
|
||||||
|
override val OnSecondaryContainer = Color(0xFF0D3C61)
|
||||||
|
override val OnTertiaryContainer = Color(0xFF071D41)
|
||||||
|
override val ButtonContrast = Color(0xFF2196F3)
|
||||||
|
|
||||||
|
override val Surface = Color(0xFFF5F9FF)
|
||||||
|
override val SurfaceVariant = Color(0xFFEDF5FE)
|
||||||
|
override val OnSurface = Color(0xFF1A1C1E)
|
||||||
|
override val OnSurfaceVariant = Color(0xFF42474E)
|
||||||
|
|
||||||
|
override val Error = Color(0xFFB00020)
|
||||||
|
override val OnError = Color(0xFFFFFFFF)
|
||||||
|
override val ErrorContainer = Color(0xFFFDE7E9)
|
||||||
|
override val OnErrorContainer = Color(0xFF410008)
|
||||||
|
|
||||||
|
override val Outline = Color(0xFFBAC3CF)
|
||||||
|
override val OutlineVariant = Color(0xFFDFE3EB)
|
||||||
|
override val Background = Color(0xFFFAFCFF)
|
||||||
|
override val OnBackground = Color(0xFF1A1C1E)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 绿色主题
|
||||||
|
object Green : ThemeColors() {
|
||||||
|
override val Primary = Color(0xFF43A047)
|
||||||
|
override val Secondary = Color(0xFF66BB6A)
|
||||||
|
override val Tertiary = Color(0xFF1B5E20)
|
||||||
|
override val OnPrimary = Color(0xFFFFFFFF)
|
||||||
|
override val OnSecondary = Color(0xFFFFFFFF)
|
||||||
|
override val OnTertiary = Color(0xFFFFFFFF)
|
||||||
|
override val PrimaryContainer = Color(0xFFD8EFDB)
|
||||||
|
override val SecondaryContainer = Color(0xFFE8F5E9)
|
||||||
|
override val TertiaryContainer = Color(0xFFB9F6CA)
|
||||||
|
override val OnPrimaryContainer = Color(0xFF0A280D)
|
||||||
|
override val OnSecondaryContainer = Color(0xFF0E2912)
|
||||||
|
override val OnTertiaryContainer = Color(0xFF051B07)
|
||||||
|
override val ButtonContrast = Color(0xFF43A047)
|
||||||
|
|
||||||
|
override val Surface = Color(0xFFF6FBF6)
|
||||||
|
override val SurfaceVariant = Color(0xFFEDF7EE)
|
||||||
|
override val OnSurface = Color(0xFF191C19)
|
||||||
|
override val OnSurfaceVariant = Color(0xFF414941)
|
||||||
|
|
||||||
|
override val Error = Color(0xFFC62828)
|
||||||
|
override val OnError = Color(0xFFFFFFFF)
|
||||||
|
override val ErrorContainer = Color(0xFFF8D7DA)
|
||||||
|
override val OnErrorContainer = Color(0xFF4A0808)
|
||||||
|
|
||||||
|
override val Outline = Color(0xFFBDC9BF)
|
||||||
|
override val OutlineVariant = Color(0xFFDDE6DE)
|
||||||
|
override val Background = Color(0xFFFBFDFB)
|
||||||
|
override val OnBackground = Color(0xFF191C19)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 紫色主题
|
||||||
|
object Purple : ThemeColors() {
|
||||||
|
override val Primary = Color(0xFF9C27B0)
|
||||||
|
override val Secondary = Color(0xFFBA68C8)
|
||||||
|
override val Tertiary = Color(0xFF6A1B9A)
|
||||||
|
override val OnPrimary = Color(0xFFFFFFFF)
|
||||||
|
override val OnSecondary = Color(0xFFFFFFFF)
|
||||||
|
override val OnTertiary = Color(0xFFFFFFFF)
|
||||||
|
override val PrimaryContainer = Color(0xFFF3D8F8)
|
||||||
|
override val SecondaryContainer = Color(0xFFF5E9F7)
|
||||||
|
override val TertiaryContainer = Color(0xFFE1BEE7)
|
||||||
|
override val OnPrimaryContainer = Color(0xFF2A0934)
|
||||||
|
override val OnSecondaryContainer = Color(0xFF3C0F50)
|
||||||
|
override val OnTertiaryContainer = Color(0xFF1D0830)
|
||||||
|
override val ButtonContrast = Color(0xFF9C27B0)
|
||||||
|
|
||||||
|
override val Surface = Color(0xFFFCF6FF)
|
||||||
|
override val SurfaceVariant = Color(0xFFF5EEFA)
|
||||||
|
override val OnSurface = Color(0xFF1D1B1E)
|
||||||
|
override val OnSurfaceVariant = Color(0xFF49454E)
|
||||||
|
|
||||||
|
override val Error = Color(0xFFD50000)
|
||||||
|
override val OnError = Color(0xFFFFFFFF)
|
||||||
|
override val ErrorContainer = Color(0xFFFFDCD5)
|
||||||
|
override val OnErrorContainer = Color(0xFF480000)
|
||||||
|
|
||||||
|
override val Outline = Color(0xFFC9B9D0)
|
||||||
|
override val OutlineVariant = Color(0xFFE8DAED)
|
||||||
|
override val Background = Color(0xFFFFFBFF)
|
||||||
|
override val OnBackground = Color(0xFF1D1B1E)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 橙色主题
|
||||||
|
object Orange : ThemeColors() {
|
||||||
|
override val Primary = Color(0xFFFF9800)
|
||||||
|
override val Secondary = Color(0xFFFFB74D)
|
||||||
|
override val Tertiary = Color(0xFFE65100)
|
||||||
|
override val OnPrimary = Color(0xFFFFFFFF)
|
||||||
|
override val OnSecondary = Color(0xFF000000)
|
||||||
|
override val OnTertiary = Color(0xFFFFFFFF)
|
||||||
|
override val PrimaryContainer = Color(0xFFFFECCC)
|
||||||
|
override val SecondaryContainer = Color(0xFFFFF0D9)
|
||||||
|
override val TertiaryContainer = Color(0xFFFFD180)
|
||||||
|
override val OnPrimaryContainer = Color(0xFF351F00)
|
||||||
|
override val OnSecondaryContainer = Color(0xFF3D2800)
|
||||||
|
override val OnTertiaryContainer = Color(0xFF2E1500)
|
||||||
|
override val ButtonContrast = Color(0xFFFF9800)
|
||||||
|
|
||||||
|
override val Surface = Color(0xFFFFF8F3)
|
||||||
|
override val SurfaceVariant = Color(0xFFFFF0E6)
|
||||||
|
override val OnSurface = Color(0xFF1F1B16)
|
||||||
|
override val OnSurfaceVariant = Color(0xFF4E4639)
|
||||||
|
|
||||||
|
override val Error = Color(0xFFD32F2F)
|
||||||
|
override val OnError = Color(0xFFFFFFFF)
|
||||||
|
override val ErrorContainer = Color(0xFFFFDBC8)
|
||||||
|
override val OnErrorContainer = Color(0xFF490700)
|
||||||
|
|
||||||
|
override val Outline = Color(0xFFD6C3AD)
|
||||||
|
override val OutlineVariant = Color(0xFFEFDFCC)
|
||||||
|
override val Background = Color(0xFFFFFBFF)
|
||||||
|
override val OnBackground = Color(0xFF1F1B16)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 粉色主题
|
||||||
|
object Pink : ThemeColors() {
|
||||||
|
override val Primary = Color(0xFFE91E63)
|
||||||
|
override val Secondary = Color(0xFFF06292)
|
||||||
|
override val Tertiary = Color(0xFF880E4F)
|
||||||
|
override val OnPrimary = Color(0xFFFFFFFF)
|
||||||
|
override val OnSecondary = Color(0xFFFFFFFF)
|
||||||
|
override val OnTertiary = Color(0xFFFFFFFF)
|
||||||
|
override val PrimaryContainer = Color(0xFFFCE4EC)
|
||||||
|
override val SecondaryContainer = Color(0xFFFCE4EC)
|
||||||
|
override val TertiaryContainer = Color(0xFFF8BBD0)
|
||||||
|
override val OnPrimaryContainer = Color(0xFF3B0819)
|
||||||
|
override val OnSecondaryContainer = Color(0xFF3B0819)
|
||||||
|
override val OnTertiaryContainer = Color(0xFF2B0516)
|
||||||
|
override val ButtonContrast = Color(0xFFE91E63)
|
||||||
|
|
||||||
|
override val Surface = Color(0xFFFFF7F9)
|
||||||
|
override val SurfaceVariant = Color(0xFFFCEEF2)
|
||||||
|
override val OnSurface = Color(0xFF201A1C)
|
||||||
|
override val OnSurfaceVariant = Color(0xFF534347)
|
||||||
|
|
||||||
|
override val Error = Color(0xFFB71C1C)
|
||||||
|
override val OnError = Color(0xFFFFFFFF)
|
||||||
|
override val ErrorContainer = Color(0xFFFFDAD6)
|
||||||
|
override val OnErrorContainer = Color(0xFF410002)
|
||||||
|
|
||||||
|
override val Outline = Color(0xFFD6BABF)
|
||||||
|
override val OutlineVariant = Color(0xFFEFDDE0)
|
||||||
|
override val Background = Color(0xFFFFFBFF)
|
||||||
|
override val OnBackground = Color(0xFF201A1C)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 灰色主题
|
||||||
|
object Gray : ThemeColors() {
|
||||||
|
override val Primary = Color(0xFF607D8B)
|
||||||
|
override val Secondary = Color(0xFF90A4AE)
|
||||||
|
override val Tertiary = Color(0xFF455A64)
|
||||||
|
override val OnPrimary = Color(0xFFFFFFFF)
|
||||||
|
override val OnSecondary = Color(0xFFFFFFFF)
|
||||||
|
override val OnTertiary = Color(0xFFFFFFFF)
|
||||||
|
override val PrimaryContainer = Color(0xFFECEFF1)
|
||||||
|
override val SecondaryContainer = Color(0xFFECEFF1)
|
||||||
|
override val TertiaryContainer = Color(0xFFCFD8DC)
|
||||||
|
override val OnPrimaryContainer = Color(0xFF1A2327)
|
||||||
|
override val OnSecondaryContainer = Color(0xFF1A2327)
|
||||||
|
override val OnTertiaryContainer = Color(0xFF121A1D)
|
||||||
|
override val ButtonContrast = Color(0xFF607D8B)
|
||||||
|
|
||||||
|
override val Surface = Color(0xFFF6F9FB)
|
||||||
|
override val SurfaceVariant = Color(0xFFEEF2F4)
|
||||||
|
override val OnSurface = Color(0xFF191C1E)
|
||||||
|
override val OnSurfaceVariant = Color(0xFF41484D)
|
||||||
|
|
||||||
|
override val Error = Color(0xFFC62828)
|
||||||
|
override val OnError = Color(0xFFFFFFFF)
|
||||||
|
override val ErrorContainer = Color(0xFFFFDAD6)
|
||||||
|
override val OnErrorContainer = Color(0xFF410002)
|
||||||
|
|
||||||
|
override val Outline = Color(0xFFBDC1C4)
|
||||||
|
override val OutlineVariant = Color(0xFFDDE1E3)
|
||||||
|
override val Background = Color(0xFFFBFCFE)
|
||||||
|
override val OnBackground = Color(0xFF191C1E)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 黄色主题
|
||||||
|
object Yellow : ThemeColors() {
|
||||||
|
override val Primary = Color(0xFFFFC107)
|
||||||
|
override val Secondary = Color(0xFFFFD54F)
|
||||||
|
override val Tertiary = Color(0xFFFF8F00)
|
||||||
|
override val OnPrimary = Color(0xFF000000)
|
||||||
|
override val OnSecondary = Color(0xFF000000)
|
||||||
|
override val OnTertiary = Color(0xFFFFFFFF)
|
||||||
|
override val PrimaryContainer = Color(0xFFFFF8E1)
|
||||||
|
override val SecondaryContainer = Color(0xFFFFF8E1)
|
||||||
|
override val TertiaryContainer = Color(0xFFFFECB3)
|
||||||
|
override val OnPrimaryContainer = Color(0xFF332A00)
|
||||||
|
override val OnSecondaryContainer = Color(0xFF332A00)
|
||||||
|
override val OnTertiaryContainer = Color(0xFF221200)
|
||||||
|
override val ButtonContrast = Color(0xFFFFC107)
|
||||||
|
|
||||||
|
override val Surface = Color(0xFFFFFAF3)
|
||||||
|
override val SurfaceVariant = Color(0xFFFFF7E6)
|
||||||
|
override val OnSurface = Color(0xFF1F1C17)
|
||||||
|
override val OnSurfaceVariant = Color(0xFF4E4A3C)
|
||||||
|
|
||||||
|
override val Error = Color(0xFFB71C1C)
|
||||||
|
override val OnError = Color(0xFFFFFFFF)
|
||||||
|
override val ErrorContainer = Color(0xFFFFDAD6)
|
||||||
|
override val OnErrorContainer = Color(0xFF410002)
|
||||||
|
|
||||||
|
override val Outline = Color(0xFFD1C8AF)
|
||||||
|
override val OutlineVariant = Color(0xFFEEE8D7)
|
||||||
|
override val Background = Color(0xFFFFFCF8)
|
||||||
|
override val OnBackground = Color(0xFF1F1C17)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromName(name: String): ThemeColors = when (name.lowercase()) {
|
||||||
|
"green" -> Green
|
||||||
|
"purple" -> Purple
|
||||||
|
"orange" -> Orange
|
||||||
|
"pink" -> Pink
|
||||||
|
"gray" -> Gray
|
||||||
|
"yellow" -> Yellow
|
||||||
|
else -> Default
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
538
manager/app/src/main/java/com/sukisu/ultra/ui/theme/Theme.kt
Normal file
538
manager/app/src/main/java/com/sukisu/ultra/ui/theme/Theme.kt
Normal file
@@ -0,0 +1,538 @@
|
|||||||
|
package com.sukisu.ultra.ui.theme
|
||||||
|
|
||||||
|
import android.content.ContentResolver
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.compose.animation.core.animateFloat
|
||||||
|
import androidx.compose.animation.core.spring
|
||||||
|
import androidx.compose.animation.core.updateTransition
|
||||||
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.darkColorScheme
|
||||||
|
import androidx.compose.material3.dynamicDarkColorScheme
|
||||||
|
import androidx.compose.material3.dynamicLightColorScheme
|
||||||
|
import androidx.compose.material3.lightColorScheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.alpha
|
||||||
|
import androidx.compose.ui.draw.paint
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.zIndex
|
||||||
|
import coil.compose.AsyncImagePainter
|
||||||
|
import coil.compose.rememberAsyncImagePainter
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import java.io.InputStream
|
||||||
|
import androidx.core.content.edit
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
import com.sukisu.ultra.ui.util.BackgroundTransformation
|
||||||
|
import com.sukisu.ultra.ui.util.saveTransformedBackground
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 主题配置对象,管理应用的主题相关状态
|
||||||
|
*/
|
||||||
|
object ThemeConfig {
|
||||||
|
var customBackgroundUri by mutableStateOf<Uri?>(null)
|
||||||
|
var forceDarkMode by mutableStateOf<Boolean?>(null)
|
||||||
|
var currentTheme by mutableStateOf<ThemeColors>(ThemeColors.Default)
|
||||||
|
var useDynamicColor by mutableStateOf(false)
|
||||||
|
var backgroundImageLoaded by mutableStateOf(false)
|
||||||
|
var needsResetOnThemeChange by mutableStateOf(false)
|
||||||
|
var isThemeChanging by mutableStateOf(false)
|
||||||
|
var preventBackgroundRefresh by mutableStateOf(false)
|
||||||
|
|
||||||
|
private var lastDarkModeState: Boolean? = null
|
||||||
|
fun detectThemeChange(currentDarkMode: Boolean): Boolean {
|
||||||
|
val isChanged = lastDarkModeState != null && lastDarkModeState != currentDarkMode
|
||||||
|
lastDarkModeState = currentDarkMode
|
||||||
|
return isChanged
|
||||||
|
}
|
||||||
|
|
||||||
|
fun resetBackgroundState() {
|
||||||
|
if (!preventBackgroundRefresh) {
|
||||||
|
backgroundImageLoaded = false
|
||||||
|
}
|
||||||
|
isThemeChanging = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应用主题
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun KernelSUTheme(
|
||||||
|
darkTheme: Boolean = when(ThemeConfig.forceDarkMode) {
|
||||||
|
true -> true
|
||||||
|
false -> false
|
||||||
|
null -> isSystemInDarkTheme()
|
||||||
|
},
|
||||||
|
dynamicColor: Boolean = ThemeConfig.useDynamicColor,
|
||||||
|
content: @Composable () -> Unit
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val systemIsDark = isSystemInDarkTheme()
|
||||||
|
|
||||||
|
// 检测系统主题变化并保存状态
|
||||||
|
val themeChanged = ThemeConfig.detectThemeChange(systemIsDark)
|
||||||
|
LaunchedEffect(systemIsDark, themeChanged) {
|
||||||
|
if (ThemeConfig.forceDarkMode == null && themeChanged) {
|
||||||
|
Log.d("ThemeSystem", "系统主题变化检测: 从 ${!systemIsDark} 变为 $systemIsDark")
|
||||||
|
ThemeConfig.resetBackgroundState()
|
||||||
|
|
||||||
|
if (!ThemeConfig.preventBackgroundRefresh) {
|
||||||
|
context.loadCustomBackground()
|
||||||
|
}
|
||||||
|
|
||||||
|
CardConfig.apply {
|
||||||
|
load(context)
|
||||||
|
if (!isCustomAlphaSet) {
|
||||||
|
cardAlpha = if (systemIsDark) 0.50f else 1f
|
||||||
|
}
|
||||||
|
save(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始加载配置
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
context.loadThemeMode()
|
||||||
|
context.loadThemeColors()
|
||||||
|
context.loadDynamicColorState()
|
||||||
|
CardConfig.load(context)
|
||||||
|
|
||||||
|
if (!ThemeConfig.backgroundImageLoaded && !ThemeConfig.preventBackgroundRefresh) {
|
||||||
|
context.loadCustomBackground()
|
||||||
|
ThemeConfig.backgroundImageLoaded = false
|
||||||
|
}
|
||||||
|
|
||||||
|
ThemeConfig.preventBackgroundRefresh = context.getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
||||||
|
.getBoolean("prevent_background_refresh", true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建颜色方案
|
||||||
|
val colorScheme = when {
|
||||||
|
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
||||||
|
if (darkTheme) createDynamicDarkColorScheme(context) else createDynamicLightColorScheme(context)
|
||||||
|
}
|
||||||
|
darkTheme -> createDarkColorScheme()
|
||||||
|
else -> createLightColorScheme()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据暗色模式和自定义背景调整卡片配置
|
||||||
|
val isDarkModeWithCustomBackground = darkTheme && ThemeConfig.customBackgroundUri != null
|
||||||
|
if (darkTheme && !dynamicColor) {
|
||||||
|
CardConfig.setDarkModeDefaults()
|
||||||
|
}
|
||||||
|
CardConfig.updateShadowEnabled(!isDarkModeWithCustomBackground)
|
||||||
|
|
||||||
|
val backgroundUri = rememberSaveable { mutableStateOf(ThemeConfig.customBackgroundUri) }
|
||||||
|
|
||||||
|
LaunchedEffect(ThemeConfig.customBackgroundUri) {
|
||||||
|
backgroundUri.value = ThemeConfig.customBackgroundUri
|
||||||
|
}
|
||||||
|
|
||||||
|
val bgImagePainter = backgroundUri.value?.let {
|
||||||
|
rememberAsyncImagePainter(
|
||||||
|
model = it,
|
||||||
|
onError = {
|
||||||
|
Log.e("ThemeSystem", "背景图加载失败: ${it.result.throwable.message}")
|
||||||
|
ThemeConfig.customBackgroundUri = null
|
||||||
|
context.saveCustomBackground(null)
|
||||||
|
},
|
||||||
|
onSuccess = {
|
||||||
|
Log.d("ThemeSystem", "背景图加载成功")
|
||||||
|
ThemeConfig.backgroundImageLoaded = true
|
||||||
|
ThemeConfig.isThemeChanging = false
|
||||||
|
|
||||||
|
ThemeConfig.preventBackgroundRefresh = true
|
||||||
|
context.getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
||||||
|
.edit { putBoolean("prevent_background_refresh", true) }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val transition = updateTransition(
|
||||||
|
targetState = ThemeConfig.backgroundImageLoaded,
|
||||||
|
label = "bgTransition"
|
||||||
|
)
|
||||||
|
val bgAlpha by transition.animateFloat(
|
||||||
|
label = "bgAlpha",
|
||||||
|
transitionSpec = {
|
||||||
|
spring(
|
||||||
|
dampingRatio = 0.8f,
|
||||||
|
stiffness = 300f
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) { loaded -> if (loaded) 1f else 0f }
|
||||||
|
|
||||||
|
DisposableEffect(systemIsDark) {
|
||||||
|
onDispose {
|
||||||
|
if (ThemeConfig.isThemeChanging) {
|
||||||
|
ThemeConfig.isThemeChanging = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MaterialTheme(
|
||||||
|
colorScheme = colorScheme,
|
||||||
|
typography = Typography
|
||||||
|
) {
|
||||||
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.zIndex(-2f)
|
||||||
|
.background(if (darkTheme) Color.Black else Color.White)
|
||||||
|
)
|
||||||
|
|
||||||
|
// 自定义背景层
|
||||||
|
backgroundUri.value?.let { uri ->
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.zIndex(-1f)
|
||||||
|
.alpha(bgAlpha)
|
||||||
|
) {
|
||||||
|
// 背景图片
|
||||||
|
bgImagePainter?.let { painter ->
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.paint(
|
||||||
|
painter = painter,
|
||||||
|
contentScale = ContentScale.Crop
|
||||||
|
)
|
||||||
|
.graphicsLayer {
|
||||||
|
alpha = (painter.state as? AsyncImagePainter.State.Success)?.let { 1f } ?: 0f
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 亮度调节层
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(
|
||||||
|
if (darkTheme) Color.Black.copy(alpha = 0.6f)
|
||||||
|
else Color.White.copy(alpha = 0.1f)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
// 边缘渐变遮罩
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(
|
||||||
|
Brush.radialGradient(
|
||||||
|
colors = listOf(
|
||||||
|
Color.Transparent,
|
||||||
|
if (darkTheme) Color.Black.copy(alpha = 0.5f)
|
||||||
|
else Color.Black.copy(alpha = 0.2f)
|
||||||
|
),
|
||||||
|
radius = 1200f
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 内容层
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.zIndex(1f)
|
||||||
|
) {
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建动态深色颜色方案
|
||||||
|
*/
|
||||||
|
@RequiresApi(Build.VERSION_CODES.S)
|
||||||
|
@Composable
|
||||||
|
private fun createDynamicDarkColorScheme(context: Context) =
|
||||||
|
dynamicDarkColorScheme(context).copy(
|
||||||
|
background = Color.Transparent,
|
||||||
|
surface = Color.Transparent,
|
||||||
|
onBackground = Color.White,
|
||||||
|
onSurface = Color.White
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建动态浅色颜色方案
|
||||||
|
*/
|
||||||
|
@RequiresApi(Build.VERSION_CODES.S)
|
||||||
|
@Composable
|
||||||
|
private fun createDynamicLightColorScheme(context: Context) =
|
||||||
|
dynamicLightColorScheme(context).copy(
|
||||||
|
background = Color.Transparent,
|
||||||
|
surface = Color.Transparent
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建深色颜色方案
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
private fun createDarkColorScheme() = darkColorScheme(
|
||||||
|
primary = ThemeConfig.currentTheme.Primary.copy(alpha = 0.8f),
|
||||||
|
onPrimary = Color.White,
|
||||||
|
primaryContainer = ThemeConfig.currentTheme.PrimaryContainer.copy(alpha = 0.15f),
|
||||||
|
onPrimaryContainer = Color.White,
|
||||||
|
secondary = ThemeConfig.currentTheme.Secondary.copy(alpha = 0.8f),
|
||||||
|
onSecondary = Color.White,
|
||||||
|
secondaryContainer = ThemeConfig.currentTheme.SecondaryContainer.copy(alpha = 0.15f),
|
||||||
|
onSecondaryContainer = Color.White,
|
||||||
|
tertiary = ThemeConfig.currentTheme.Tertiary.copy(alpha = 0.8f),
|
||||||
|
onTertiary = Color.White,
|
||||||
|
tertiaryContainer = ThemeConfig.currentTheme.TertiaryContainer.copy(alpha = 0.15f),
|
||||||
|
onTertiaryContainer = Color.White,
|
||||||
|
background = Color.Transparent,
|
||||||
|
surface = Color.Transparent,
|
||||||
|
onBackground = Color.White,
|
||||||
|
onSurface = Color.White,
|
||||||
|
surfaceVariant = Color(0xFF2F2F2F),
|
||||||
|
onSurfaceVariant = Color.White.copy(alpha = 0.7f),
|
||||||
|
outline = Color.White.copy(alpha = 0.12f),
|
||||||
|
outlineVariant = Color.White.copy(alpha = 0.12f),
|
||||||
|
error = ThemeConfig.currentTheme.Error,
|
||||||
|
onError = ThemeConfig.currentTheme.OnError,
|
||||||
|
errorContainer = ThemeConfig.currentTheme.ErrorContainer.copy(alpha = 0.15f),
|
||||||
|
onErrorContainer = Color.White
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建浅色颜色方案
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
private fun createLightColorScheme() = lightColorScheme(
|
||||||
|
primary = ThemeConfig.currentTheme.Primary,
|
||||||
|
onPrimary = ThemeConfig.currentTheme.OnPrimary,
|
||||||
|
primaryContainer = ThemeConfig.currentTheme.PrimaryContainer,
|
||||||
|
onPrimaryContainer = ThemeConfig.currentTheme.OnPrimaryContainer,
|
||||||
|
secondary = ThemeConfig.currentTheme.Secondary,
|
||||||
|
onSecondary = ThemeConfig.currentTheme.OnSecondary,
|
||||||
|
secondaryContainer = ThemeConfig.currentTheme.SecondaryContainer,
|
||||||
|
onSecondaryContainer = ThemeConfig.currentTheme.OnSecondaryContainer,
|
||||||
|
tertiary = ThemeConfig.currentTheme.Tertiary,
|
||||||
|
onTertiary = ThemeConfig.currentTheme.OnTertiary,
|
||||||
|
tertiaryContainer = ThemeConfig.currentTheme.TertiaryContainer,
|
||||||
|
onTertiaryContainer = ThemeConfig.currentTheme.OnTertiaryContainer,
|
||||||
|
background = Color.Transparent,
|
||||||
|
surface = Color.Transparent,
|
||||||
|
onBackground = Color.Black.copy(alpha = 0.87f),
|
||||||
|
onSurface = Color.Black.copy(alpha = 0.87f),
|
||||||
|
surfaceVariant = Color(0xFFF5F5F5),
|
||||||
|
onSurfaceVariant = Color.Black.copy(alpha = 0.78f),
|
||||||
|
outline = Color.Black.copy(alpha = 0.12f),
|
||||||
|
outlineVariant = Color.Black.copy(alpha = 0.12f),
|
||||||
|
error = ThemeConfig.currentTheme.Error,
|
||||||
|
onError = ThemeConfig.currentTheme.OnError,
|
||||||
|
errorContainer = ThemeConfig.currentTheme.ErrorContainer,
|
||||||
|
onErrorContainer = ThemeConfig.currentTheme.OnErrorContainer
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 复制图片到应用内部存储并提升持久性
|
||||||
|
*/
|
||||||
|
private fun Context.copyImageToInternalStorage(uri: Uri): Uri? {
|
||||||
|
return try {
|
||||||
|
val contentResolver: ContentResolver = contentResolver
|
||||||
|
val inputStream: InputStream = contentResolver.openInputStream(uri) ?: return null
|
||||||
|
|
||||||
|
val fileName = "custom_background.jpg"
|
||||||
|
val file = File(filesDir, fileName)
|
||||||
|
|
||||||
|
val backupFile = File(filesDir, "${fileName}.backup")
|
||||||
|
val outputStream = FileOutputStream(backupFile)
|
||||||
|
val buffer = ByteArray(4 * 1024)
|
||||||
|
var read: Int
|
||||||
|
|
||||||
|
while (inputStream.read(buffer).also { read = it } != -1) {
|
||||||
|
outputStream.write(buffer, 0, read)
|
||||||
|
}
|
||||||
|
|
||||||
|
outputStream.flush()
|
||||||
|
outputStream.close()
|
||||||
|
inputStream.close()
|
||||||
|
|
||||||
|
if (file.exists()) {
|
||||||
|
file.delete()
|
||||||
|
}
|
||||||
|
backupFile.renameTo(file)
|
||||||
|
|
||||||
|
Uri.fromFile(file)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("ImageCopy", "复制图片失败: ${e.message}")
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存并应用自定义背景
|
||||||
|
*/
|
||||||
|
fun Context.saveAndApplyCustomBackground(uri: Uri, transformation: BackgroundTransformation? = null) {
|
||||||
|
val finalUri = if (transformation != null) {
|
||||||
|
saveTransformedBackground(uri, transformation)
|
||||||
|
} else {
|
||||||
|
copyImageToInternalStorage(uri)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存到配置文件
|
||||||
|
getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
||||||
|
.edit {
|
||||||
|
putString("custom_background", finalUri?.toString())
|
||||||
|
// 设置阻止刷新标志为false,允许新设置的背景加载一次
|
||||||
|
putBoolean("prevent_background_refresh", false)
|
||||||
|
}
|
||||||
|
|
||||||
|
ThemeConfig.customBackgroundUri = finalUri
|
||||||
|
ThemeConfig.backgroundImageLoaded = false
|
||||||
|
ThemeConfig.preventBackgroundRefresh = false
|
||||||
|
CardConfig.cardElevation = 0.dp
|
||||||
|
CardConfig.isCustomBackgroundEnabled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存自定义背景
|
||||||
|
*/
|
||||||
|
fun Context.saveCustomBackground(uri: Uri?) {
|
||||||
|
val newUri = uri?.let { copyImageToInternalStorage(it) }
|
||||||
|
|
||||||
|
// 保存到配置文件
|
||||||
|
getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
||||||
|
.edit {
|
||||||
|
putString("custom_background", newUri?.toString())
|
||||||
|
if (uri == null) {
|
||||||
|
// 如果清除背景,也重置阻止刷新标志
|
||||||
|
putBoolean("prevent_background_refresh", false)
|
||||||
|
} else {
|
||||||
|
// 设置阻止刷新标志为false,允许新设置的背景加载一次
|
||||||
|
putBoolean("prevent_background_refresh", false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ThemeConfig.customBackgroundUri = newUri
|
||||||
|
ThemeConfig.backgroundImageLoaded = false
|
||||||
|
ThemeConfig.preventBackgroundRefresh = false
|
||||||
|
|
||||||
|
if (uri != null) {
|
||||||
|
CardConfig.cardElevation = 0.dp
|
||||||
|
CardConfig.isCustomBackgroundEnabled = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载自定义背景
|
||||||
|
*/
|
||||||
|
fun Context.loadCustomBackground() {
|
||||||
|
val uriString = getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
||||||
|
.getString("custom_background", null)
|
||||||
|
|
||||||
|
val newUri = uriString?.toUri()
|
||||||
|
val preventRefresh = getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
||||||
|
.getBoolean("prevent_background_refresh", false)
|
||||||
|
|
||||||
|
ThemeConfig.preventBackgroundRefresh = preventRefresh
|
||||||
|
|
||||||
|
if (!preventRefresh || ThemeConfig.customBackgroundUri?.toString() != newUri?.toString()) {
|
||||||
|
Log.d("ThemeSystem", "加载自定义背景: $uriString, 阻止刷新: $preventRefresh")
|
||||||
|
ThemeConfig.customBackgroundUri = newUri
|
||||||
|
ThemeConfig.backgroundImageLoaded = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存主题模式
|
||||||
|
*/
|
||||||
|
fun Context.saveThemeMode(forceDark: Boolean?) {
|
||||||
|
getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
||||||
|
.edit {
|
||||||
|
putString(
|
||||||
|
"theme_mode", when (forceDark) {
|
||||||
|
true -> "dark"
|
||||||
|
false -> "light"
|
||||||
|
null -> "system"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
ThemeConfig.forceDarkMode = forceDark
|
||||||
|
ThemeConfig.needsResetOnThemeChange = forceDark == null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载主题模式
|
||||||
|
*/
|
||||||
|
fun Context.loadThemeMode() {
|
||||||
|
val mode = getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
||||||
|
.getString("theme_mode", "system")
|
||||||
|
|
||||||
|
ThemeConfig.forceDarkMode = when(mode) {
|
||||||
|
"dark" -> true
|
||||||
|
"light" -> false
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
ThemeConfig.needsResetOnThemeChange = ThemeConfig.forceDarkMode == null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存主题颜色
|
||||||
|
*/
|
||||||
|
fun Context.saveThemeColors(themeName: String) {
|
||||||
|
getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
||||||
|
.edit {
|
||||||
|
putString("theme_colors", themeName)
|
||||||
|
}
|
||||||
|
|
||||||
|
ThemeConfig.currentTheme = ThemeColors.fromName(themeName)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载主题颜色
|
||||||
|
*/
|
||||||
|
fun Context.loadThemeColors() {
|
||||||
|
val themeName = getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
||||||
|
.getString("theme_colors", "default")
|
||||||
|
|
||||||
|
ThemeConfig.currentTheme = ThemeColors.fromName(themeName ?: "default")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存动态颜色状态
|
||||||
|
*/
|
||||||
|
fun Context.saveDynamicColorState(enabled: Boolean) {
|
||||||
|
getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
||||||
|
.edit {
|
||||||
|
putBoolean("use_dynamic_color", enabled)
|
||||||
|
}
|
||||||
|
ThemeConfig.useDynamicColor = enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载动态颜色状态
|
||||||
|
*/
|
||||||
|
fun Context.loadDynamicColorState() {
|
||||||
|
val enabled = getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
||||||
|
.getBoolean("use_dynamic_color", true)
|
||||||
|
|
||||||
|
ThemeConfig.useDynamicColor = enabled
|
||||||
|
}
|
||||||
108
manager/app/src/main/java/com/sukisu/ultra/ui/theme/Type.kt
Normal file
108
manager/app/src/main/java/com/sukisu/ultra/ui/theme/Type.kt
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
package com.sukisu.ultra.ui.theme
|
||||||
|
|
||||||
|
import androidx.compose.material3.Typography
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
|
||||||
|
val Typography = Typography(
|
||||||
|
// 大标题
|
||||||
|
displayLarge = TextStyle(
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
fontSize = 57.sp,
|
||||||
|
lineHeight = 64.sp,
|
||||||
|
letterSpacing = (-0.25).sp
|
||||||
|
),
|
||||||
|
displayMedium = TextStyle(
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
fontSize = 45.sp,
|
||||||
|
lineHeight = 52.sp,
|
||||||
|
letterSpacing = 0.sp
|
||||||
|
),
|
||||||
|
displaySmall = TextStyle(
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
fontSize = 36.sp,
|
||||||
|
lineHeight = 44.sp,
|
||||||
|
letterSpacing = 0.sp
|
||||||
|
),
|
||||||
|
|
||||||
|
// 标题
|
||||||
|
headlineLarge = TextStyle(
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
fontSize = 32.sp,
|
||||||
|
lineHeight = 40.sp,
|
||||||
|
letterSpacing = 0.sp
|
||||||
|
),
|
||||||
|
headlineMedium = TextStyle(
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
fontSize = 28.sp,
|
||||||
|
lineHeight = 36.sp,
|
||||||
|
letterSpacing = 0.sp
|
||||||
|
),
|
||||||
|
headlineSmall = TextStyle(
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
fontSize = 24.sp,
|
||||||
|
lineHeight = 32.sp,
|
||||||
|
letterSpacing = 0.sp
|
||||||
|
),
|
||||||
|
|
||||||
|
// 标题栏
|
||||||
|
titleLarge = TextStyle(
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
fontSize = 22.sp,
|
||||||
|
lineHeight = 28.sp,
|
||||||
|
letterSpacing = 0.sp
|
||||||
|
),
|
||||||
|
titleMedium = TextStyle(
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
lineHeight = 24.sp,
|
||||||
|
letterSpacing = 0.15.sp
|
||||||
|
),
|
||||||
|
titleSmall = TextStyle(
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
lineHeight = 20.sp,
|
||||||
|
letterSpacing = 0.1.sp
|
||||||
|
),
|
||||||
|
|
||||||
|
// 主体文字
|
||||||
|
bodyLarge = TextStyle(
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
lineHeight = 24.sp,
|
||||||
|
letterSpacing = 0.5.sp
|
||||||
|
),
|
||||||
|
bodyMedium = TextStyle(
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
lineHeight = 20.sp,
|
||||||
|
letterSpacing = 0.25.sp
|
||||||
|
),
|
||||||
|
bodySmall = TextStyle(
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
lineHeight = 16.sp,
|
||||||
|
letterSpacing = 0.4.sp
|
||||||
|
),
|
||||||
|
|
||||||
|
// 标签
|
||||||
|
labelLarge = TextStyle(
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
lineHeight = 20.sp,
|
||||||
|
letterSpacing = 0.1.sp
|
||||||
|
),
|
||||||
|
labelMedium = TextStyle(
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
lineHeight = 16.sp,
|
||||||
|
letterSpacing = 0.5.sp
|
||||||
|
),
|
||||||
|
labelSmall = TextStyle(
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
fontSize = 11.sp,
|
||||||
|
lineHeight = 16.sp,
|
||||||
|
letterSpacing = 0.5.sp
|
||||||
|
)
|
||||||
|
)
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
package com.sukisu.ultra.ui.util
|
||||||
|
|
||||||
|
import android.content.ContentResolver
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.Matrix
|
||||||
|
import android.net.Uri
|
||||||
|
import android.util.Log
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import java.io.InputStream
|
||||||
|
import androidx.core.graphics.createBitmap
|
||||||
|
|
||||||
|
data class BackgroundTransformation(
|
||||||
|
val scale: Float = 1f,
|
||||||
|
val offsetX: Float = 0f,
|
||||||
|
val offsetY: Float = 0f
|
||||||
|
)
|
||||||
|
|
||||||
|
fun Context.getImageBitmap(uri: Uri): Bitmap? {
|
||||||
|
return try {
|
||||||
|
val contentResolver: ContentResolver = contentResolver
|
||||||
|
val inputStream: InputStream = contentResolver.openInputStream(uri) ?: return null
|
||||||
|
val bitmap = BitmapFactory.decodeStream(inputStream)
|
||||||
|
inputStream.close()
|
||||||
|
bitmap
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("BackgroundUtils", "Failed to get image bitmap: ${e.message}")
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Context.applyTransformationToBitmap(bitmap: Bitmap, transformation: BackgroundTransformation): Bitmap {
|
||||||
|
val width = bitmap.width
|
||||||
|
val height = bitmap.height
|
||||||
|
|
||||||
|
// 创建与屏幕比例相同的目标位图
|
||||||
|
val displayMetrics = resources.displayMetrics
|
||||||
|
val screenWidth = displayMetrics.widthPixels
|
||||||
|
val screenHeight = displayMetrics.heightPixels
|
||||||
|
val screenRatio = screenHeight.toFloat() / screenWidth.toFloat()
|
||||||
|
|
||||||
|
// 计算目标宽高
|
||||||
|
val targetWidth: Int
|
||||||
|
val targetHeight: Int
|
||||||
|
if (width.toFloat() / height.toFloat() > screenRatio) {
|
||||||
|
targetHeight = height
|
||||||
|
targetWidth = (height / screenRatio).toInt()
|
||||||
|
} else {
|
||||||
|
targetWidth = width
|
||||||
|
targetHeight = (width * screenRatio).toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建与目标相同大小的位图
|
||||||
|
val scaledBitmap = createBitmap(targetWidth, targetHeight)
|
||||||
|
val canvas = Canvas(scaledBitmap)
|
||||||
|
|
||||||
|
val matrix = Matrix()
|
||||||
|
|
||||||
|
// 确保缩放值有效
|
||||||
|
val safeScale = maxOf(0.1f, transformation.scale)
|
||||||
|
matrix.postScale(safeScale, safeScale)
|
||||||
|
|
||||||
|
// 计算偏移量,确保不会出现负最大值的问题
|
||||||
|
val widthDiff = (bitmap.width * safeScale - targetWidth)
|
||||||
|
val heightDiff = (bitmap.height * safeScale - targetHeight)
|
||||||
|
|
||||||
|
// 安全计算偏移量边界
|
||||||
|
val maxOffsetX = maxOf(0f, widthDiff / 2)
|
||||||
|
val maxOffsetY = maxOf(0f, heightDiff / 2)
|
||||||
|
|
||||||
|
// 限制偏移范围
|
||||||
|
val safeOffsetX = if (maxOffsetX > 0)
|
||||||
|
transformation.offsetX.coerceIn(-maxOffsetX, maxOffsetX) else 0f
|
||||||
|
val safeOffsetY = if (maxOffsetY > 0)
|
||||||
|
transformation.offsetY.coerceIn(-maxOffsetY, maxOffsetY) else 0f
|
||||||
|
|
||||||
|
// 应用偏移量到矩阵
|
||||||
|
val translationX = -widthDiff / 2 + safeOffsetX
|
||||||
|
val translationY = -heightDiff / 2 + safeOffsetY
|
||||||
|
|
||||||
|
matrix.postTranslate(translationX, translationY)
|
||||||
|
|
||||||
|
// 将原始位图绘制到新位图上
|
||||||
|
canvas.drawBitmap(bitmap, matrix, null)
|
||||||
|
|
||||||
|
return scaledBitmap
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Context.saveTransformedBackground(uri: Uri, transformation: BackgroundTransformation): Uri? {
|
||||||
|
try {
|
||||||
|
val bitmap = getImageBitmap(uri) ?: return null
|
||||||
|
val transformedBitmap = applyTransformationToBitmap(bitmap, transformation)
|
||||||
|
|
||||||
|
val fileName = "custom_background_transformed.jpg"
|
||||||
|
val file = File(filesDir, fileName)
|
||||||
|
val outputStream = FileOutputStream(file)
|
||||||
|
|
||||||
|
transformedBitmap.compress(Bitmap.CompressFormat.JPEG, 90, outputStream)
|
||||||
|
outputStream.flush()
|
||||||
|
outputStream.close()
|
||||||
|
|
||||||
|
return Uri.fromFile(file)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("BackgroundUtils", "Failed to save transformed image: ${e.message}", e)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package shirkneko.zako.sukisu.ui.util
|
package com.sukisu.ultra.ui.util
|
||||||
|
|
||||||
import androidx.compose.material3.SnackbarHostState
|
import androidx.compose.material3.SnackbarHostState
|
||||||
import androidx.compose.runtime.compositionLocalOf
|
import androidx.compose.runtime.compositionLocalOf
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package shirkneko.zako.sukisu.ui.util
|
package com.sukisu.ultra.ui.util
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.DownloadManager
|
import android.app.DownloadManager
|
||||||
@@ -12,7 +12,8 @@ import android.util.Log
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import shirkneko.zako.sukisu.ui.util.module.LatestVersionInfo
|
import com.sukisu.ultra.ui.util.module.LatestVersionInfo
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author weishu
|
* @author weishu
|
||||||
@@ -42,14 +43,14 @@ fun download(
|
|||||||
onDownloading()
|
onDownloading()
|
||||||
return
|
return
|
||||||
} else if (status == DownloadManager.STATUS_SUCCESSFUL) {
|
} else if (status == DownloadManager.STATUS_SUCCESSFUL) {
|
||||||
onDownloaded(Uri.parse(localUri))
|
onDownloaded(localUri.toUri())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val request = DownloadManager.Request(Uri.parse(url))
|
val request = DownloadManager.Request(url.toUri())
|
||||||
.setDestinationInExternalPublicDir(
|
.setDestinationInExternalPublicDir(
|
||||||
Environment.DIRECTORY_DOWNLOADS,
|
Environment.DIRECTORY_DOWNLOADS,
|
||||||
fileName
|
fileName
|
||||||
@@ -63,7 +64,6 @@ fun download(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun checkNewVersion(): LatestVersionInfo {
|
fun checkNewVersion(): LatestVersionInfo {
|
||||||
// 改为新的 release 接口
|
|
||||||
val url = "https://api.github.com/repos/ShirkNeko/SukiSU-Ultra/releases/latest"
|
val url = "https://api.github.com/repos/ShirkNeko/SukiSU-Ultra/releases/latest"
|
||||||
val defaultValue = LatestVersionInfo()
|
val defaultValue = LatestVersionInfo()
|
||||||
return runCatching {
|
return runCatching {
|
||||||
@@ -141,7 +141,7 @@ fun DownloadListener(context: Context, onDownloaded: (Uri) -> Unit) {
|
|||||||
val uri = cursor.getString(
|
val uri = cursor.getString(
|
||||||
cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI)
|
cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI)
|
||||||
)
|
)
|
||||||
onDownloaded(Uri.parse(uri))
|
onDownloaded(uri.toUri())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package shirkneko.zako.sukisu.ui.util;
|
package com.sukisu.ultra.ui.util;
|
||||||
/*
|
/*
|
||||||
* Copyright (C) 2009 The Android Open Source Project
|
* Copyright (C) 2009 The Android Open Source Project
|
||||||
*
|
*
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package shirkneko.zako.sukisu.ui.util
|
package com.sukisu.ultra.ui.util
|
||||||
|
|
||||||
import androidx.compose.foundation.gestures.detectTapGestures
|
import androidx.compose.foundation.gestures.detectTapGestures
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package shirkneko.zako.sukisu.ui.util
|
package com.sukisu.ultra.ui.util
|
||||||
|
|
||||||
import android.content.ContentResolver
|
import android.content.ContentResolver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
@@ -16,9 +16,9 @@ import com.topjohnwu.superuser.ShellUtils
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import shirkneko.zako.sukisu.BuildConfig
|
import com.sukisu.ultra.BuildConfig
|
||||||
import shirkneko.zako.sukisu.Natives
|
import com.sukisu.ultra.Natives
|
||||||
import shirkneko.zako.sukisu.ksuApp
|
import com.sukisu.ultra.ksuApp
|
||||||
import org.json.JSONArray
|
import org.json.JSONArray
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
@@ -30,7 +30,7 @@ import java.io.File
|
|||||||
private const val TAG = "KsuCli"
|
private const val TAG = "KsuCli"
|
||||||
|
|
||||||
private fun getKsuDaemonPath(): String {
|
private fun getKsuDaemonPath(): String {
|
||||||
return ksuApp.applicationInfo.nativeLibraryDir + File.separator + "libzakomk.so"
|
return ksuApp.applicationInfo.nativeLibraryDir + File.separator + "libzakozako.so"
|
||||||
}
|
}
|
||||||
|
|
||||||
object KsuCli {
|
object KsuCli {
|
||||||
@@ -436,8 +436,8 @@ fun restartApp(packageName: String) {
|
|||||||
launchApp(packageName)
|
launchApp(packageName)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getSuSFSDaemonPath(): String {
|
fun getSuSFSDaemonPath(): String {
|
||||||
return ksuApp.applicationInfo.nativeLibraryDir + File.separator + "libzakomksd.so"
|
return ksuApp.applicationInfo.nativeLibraryDir + File.separator + "libzakozakozako.so"
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getSuSFS(): String {
|
fun getSuSFS(): String {
|
||||||
@@ -481,7 +481,7 @@ fun susfsSUS_SU_Mode(): String {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getKpmmgrPath(): String {
|
fun getKpmmgrPath(): String {
|
||||||
return ksuApp.applicationInfo.nativeLibraryDir + File.separator + "libkpmmgr.so"
|
return ksuApp.applicationInfo.nativeLibraryDir + File.separator + "libkpmmgr.so"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -489,48 +489,62 @@ private fun getKpmmgrPath(): String {
|
|||||||
fun loadKpmModule(path: String, args: String? = null): String {
|
fun loadKpmModule(path: String, args: String? = null): String {
|
||||||
val shell = getRootShell()
|
val shell = getRootShell()
|
||||||
val cmd = "${getKpmmgrPath()} load $path ${args ?: ""}"
|
val cmd = "${getKpmmgrPath()} load $path ${args ?: ""}"
|
||||||
val result = ShellUtils.fastCmd(shell, cmd)
|
return ShellUtils.fastCmd(shell, cmd)
|
||||||
return result
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun unloadKpmModule(name: String): String {
|
fun unloadKpmModule(name: String): String {
|
||||||
val shell = getRootShell()
|
val shell = getRootShell()
|
||||||
val cmd = "${getKpmmgrPath()} unload $name"
|
val cmd = "${getKpmmgrPath()} unload $name"
|
||||||
val result = ShellUtils.fastCmd(shell, cmd)
|
return ShellUtils.fastCmd(shell, cmd)
|
||||||
return result
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getKpmModuleCount(): String {
|
fun getKpmModuleCount(): Int {
|
||||||
val shell = getRootShell()
|
val shell = getRootShell()
|
||||||
val cmd = "${getKpmmgrPath()} num"
|
val cmd = "${getKpmmgrPath()} num"
|
||||||
val result = ShellUtils.fastCmd(shell, cmd)
|
val result = ShellUtils.fastCmd(shell, cmd)
|
||||||
return result
|
return result.trim().toIntOrNull() ?: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
fun runCmd(shell : Shell, cmd : String) : String {
|
||||||
|
return shell.newJob()
|
||||||
|
.add(cmd)
|
||||||
|
.to(mutableListOf<String>(), null)
|
||||||
|
.exec().out
|
||||||
|
.joinToString("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun listKpmModules(): String {
|
fun listKpmModules(): String {
|
||||||
val shell = getRootShell()
|
val shell = getRootShell()
|
||||||
val cmd = "${getKpmmgrPath()} list"
|
val cmd = "${getKpmmgrPath()} list"
|
||||||
val result = ShellUtils.fastCmd(shell, cmd)
|
return try {
|
||||||
return result
|
runCmd(shell, cmd).trim()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Failed to list KPM modules", e)
|
||||||
|
""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getKpmModuleInfo(name: String): String {
|
fun getKpmModuleInfo(name: String): String {
|
||||||
val shell = getRootShell()
|
val shell = getRootShell()
|
||||||
val cmd = "${getKpmmgrPath()} info $name"
|
val cmd = "${getKpmmgrPath()} info $name"
|
||||||
val result = ShellUtils.fastCmd(shell, cmd)
|
return try {
|
||||||
return result
|
runCmd(shell, cmd).trim()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Failed to get KPM module info: $name", e)
|
||||||
|
""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun controlKpmModule(name: String, args: String? = null): String {
|
fun controlKpmModule(name: String, args: String? = null): Int {
|
||||||
val shell = getRootShell()
|
val shell = getRootShell()
|
||||||
val cmd = "${getKpmmgrPath()} control $name ${args ?: ""}"
|
val cmd = """${getKpmmgrPath()} control $name "${args ?: ""}""""
|
||||||
val result = ShellUtils.fastCmd(shell, cmd)
|
val result = runCmd(shell, cmd)
|
||||||
return result
|
return result.trim().toIntOrNull() ?: -1
|
||||||
}
|
}
|
||||||
|
|
||||||
fun printKpmModules(): String {
|
fun getKpmVersion(): String {
|
||||||
val shell = getRootShell()
|
val shell = getRootShell()
|
||||||
val cmd = "${getKpmmgrPath()} print"
|
val cmd = "${getKpmmgrPath()} version"
|
||||||
val result = ShellUtils.fastCmd(shell, cmd)
|
val result = ShellUtils.fastCmd(shell, cmd)
|
||||||
return result
|
return result.trim()
|
||||||
}
|
}
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
package shirkneko.zako.sukisu.ui.util
|
package com.sukisu.ultra.ui.util
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.system.Os
|
import android.system.Os
|
||||||
import com.topjohnwu.superuser.ShellUtils
|
import com.topjohnwu.superuser.ShellUtils
|
||||||
import shirkneko.zako.sukisu.Natives
|
import com.sukisu.ultra.Natives
|
||||||
import shirkneko.zako.sukisu.ui.screen.getManagerVersion
|
import com.sukisu.ultra.ui.screen.getManagerVersion
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileWriter
|
import java.io.FileWriter
|
||||||
import java.io.PrintWriter
|
import java.io.PrintWriter
|
||||||
@@ -24,7 +24,7 @@ fun getBugreportFile(context: Context): File {
|
|||||||
val pstoreFile = File(bugreportDir, "pstore.tar.gz")
|
val pstoreFile = File(bugreportDir, "pstore.tar.gz")
|
||||||
// Xiaomi/Readmi devices have diag in /data/vendor/diag
|
// Xiaomi/Readmi devices have diag in /data/vendor/diag
|
||||||
val diagFile = File(bugreportDir, "diag.tar.gz")
|
val diagFile = File(bugreportDir, "diag.tar.gz")
|
||||||
val opulsFile = File(bugreportDir, "opuls.tar.gz")
|
val oplusFile = File(bugreportDir, "oplus.tar.gz")
|
||||||
val bootlogFile = File(bugreportDir, "bootlog.tar.gz")
|
val bootlogFile = File(bugreportDir, "bootlog.tar.gz")
|
||||||
val mountsFile = File(bugreportDir, "mounts.txt")
|
val mountsFile = File(bugreportDir, "mounts.txt")
|
||||||
val fileSystemsFile = File(bugreportDir, "filesystems.txt")
|
val fileSystemsFile = File(bugreportDir, "filesystems.txt")
|
||||||
@@ -46,7 +46,7 @@ fun getBugreportFile(context: Context): File {
|
|||||||
shell.newJob().add("tar -czf ${dropboxFile.absolutePath} -C /data/system/dropbox .").exec()
|
shell.newJob().add("tar -czf ${dropboxFile.absolutePath} -C /data/system/dropbox .").exec()
|
||||||
shell.newJob().add("tar -czf ${pstoreFile.absolutePath} -C /sys/fs/pstore .").exec()
|
shell.newJob().add("tar -czf ${pstoreFile.absolutePath} -C /sys/fs/pstore .").exec()
|
||||||
shell.newJob().add("tar -czf ${diagFile.absolutePath} -C /data/vendor/diag . --exclude=./minidump.gz").exec()
|
shell.newJob().add("tar -czf ${diagFile.absolutePath} -C /data/vendor/diag . --exclude=./minidump.gz").exec()
|
||||||
shell.newJob().add("tar -czf ${opulsFile.absolutePath} -C /mnt/oplus/op2/media/log/boot_log/ .").exec()
|
shell.newJob().add("tar -czf ${oplusFile.absolutePath} -C /mnt/oplus/op2/media/log/boot_log/ .").exec()
|
||||||
shell.newJob().add("tar -czf ${bootlogFile.absolutePath} -C /data/adb/ksu/log .").exec()
|
shell.newJob().add("tar -czf ${bootlogFile.absolutePath} -C /data/adb/ksu/log .").exec()
|
||||||
|
|
||||||
shell.newJob().add("cat /proc/1/mountinfo > ${mountsFile.absolutePath}").exec()
|
shell.newJob().add("cat /proc/1/mountinfo > ${mountsFile.absolutePath}").exec()
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package shirkneko.zako.sukisu.ui.util
|
package com.sukisu.ultra.ui.util
|
||||||
|
|
||||||
import android.app.AlertDialog
|
import android.app.AlertDialog
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
@@ -16,7 +16,7 @@ import kotlinx.coroutines.CompletableDeferred
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import shirkneko.zako.sukisu.R
|
import com.sukisu.ultra.R
|
||||||
import java.io.BufferedReader
|
import java.io.BufferedReader
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStreamReader
|
import java.io.InputStreamReader
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
package shirkneko.zako.sukisu.ui.util
|
package com.sukisu.ultra.ui.util
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
import shirkneko.zako.sukisu.R
|
import com.sukisu.ultra.R
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun getSELinuxStatus(): String {
|
fun getSELinuxStatus(): String {
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package shirkneko.zako.sukisu.ui.util.module
|
package com.sukisu.ultra.ui.util.module
|
||||||
|
|
||||||
data class LatestVersionInfo(
|
data class LatestVersionInfo(
|
||||||
val versionCode : Int = 0,
|
val versionCode : Int = 0,
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
package com.sukisu.ultra.ui.viewmodel
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import com.sukisu.ultra.ui.util.*
|
||||||
|
|
||||||
|
class KpmViewModel : ViewModel() {
|
||||||
|
var moduleList by mutableStateOf(emptyList<ModuleInfo>())
|
||||||
|
private set
|
||||||
|
|
||||||
|
var search by mutableStateOf("")
|
||||||
|
internal set
|
||||||
|
|
||||||
|
var isRefreshing by mutableStateOf(false)
|
||||||
|
private set
|
||||||
|
|
||||||
|
var currentModuleDetail by mutableStateOf("")
|
||||||
|
private set
|
||||||
|
|
||||||
|
fun fetchModuleList() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
isRefreshing = true
|
||||||
|
try {
|
||||||
|
val moduleCount = getKpmModuleCount()
|
||||||
|
Log.d("KsuCli", "Module count: $moduleCount")
|
||||||
|
|
||||||
|
moduleList = getAllKpmModuleInfo()
|
||||||
|
|
||||||
|
// 获取 KPM 版本信息
|
||||||
|
val kpmVersion = getKpmVersion()
|
||||||
|
Log.d("KsuCli", "KPM Version: $kpmVersion")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("KsuCli", "获取模块列表失败", e)
|
||||||
|
} finally {
|
||||||
|
isRefreshing = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getAllKpmModuleInfo(): List<ModuleInfo> {
|
||||||
|
val result = mutableListOf<ModuleInfo>()
|
||||||
|
try {
|
||||||
|
val str = listKpmModules()
|
||||||
|
val moduleNames = str
|
||||||
|
.split("\n")
|
||||||
|
.filter { it.isNotBlank() }
|
||||||
|
|
||||||
|
for (name in moduleNames) {
|
||||||
|
try {
|
||||||
|
val moduleInfo = parseModuleInfo(name)
|
||||||
|
moduleInfo?.let { result.add(it) }
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("KsuCli", "Error processing module $name", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("KsuCli", "Failed to get module list", e)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseModuleInfo(name: String): ModuleInfo? {
|
||||||
|
val info = getKpmModuleInfo(name)
|
||||||
|
if (info.isBlank()) return null
|
||||||
|
|
||||||
|
val properties = info.lineSequence()
|
||||||
|
.filter { line ->
|
||||||
|
val trimmed = line.trim()
|
||||||
|
trimmed.isNotEmpty() && !trimmed.startsWith("#")
|
||||||
|
}
|
||||||
|
.mapNotNull { line ->
|
||||||
|
line.split("=", limit = 2).let { parts ->
|
||||||
|
when (parts.size) {
|
||||||
|
2 -> parts[0].trim() to parts[1].trim()
|
||||||
|
1 -> parts[0].trim() to ""
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.toMap()
|
||||||
|
|
||||||
|
return ModuleInfo(
|
||||||
|
id = name,
|
||||||
|
name = properties["name"] ?: name,
|
||||||
|
version = properties["version"] ?: "",
|
||||||
|
author = properties["author"] ?: "",
|
||||||
|
description = properties["description"] ?: "",
|
||||||
|
args = properties["args"] ?: "",
|
||||||
|
enabled = true,
|
||||||
|
hasAction = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadModuleDetail(moduleId: String) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
currentModuleDetail = withContext(Dispatchers.IO) {
|
||||||
|
getKpmModuleInfo(moduleId)
|
||||||
|
}
|
||||||
|
Log.d("KsuCli", "Module detail loaded: $currentModuleDetail")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("KsuCli", "Failed to load module detail", e)
|
||||||
|
currentModuleDetail = "Error: ${e.message}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var showInputDialog by mutableStateOf(false)
|
||||||
|
private set
|
||||||
|
|
||||||
|
var selectedModuleId by mutableStateOf<String?>(null)
|
||||||
|
private set
|
||||||
|
|
||||||
|
var inputArgs by mutableStateOf("")
|
||||||
|
private set
|
||||||
|
|
||||||
|
fun showInputDialog(moduleId: String) {
|
||||||
|
selectedModuleId = moduleId
|
||||||
|
showInputDialog = true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun hideInputDialog() {
|
||||||
|
showInputDialog = false
|
||||||
|
selectedModuleId = null
|
||||||
|
inputArgs = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateInputArgs(args: String) {
|
||||||
|
inputArgs = args
|
||||||
|
}
|
||||||
|
|
||||||
|
fun executeControl(): Int {
|
||||||
|
val moduleId = selectedModuleId ?: return -1
|
||||||
|
val result = controlKpmModule(moduleId, inputArgs)
|
||||||
|
hideInputDialog()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
data class ModuleInfo(
|
||||||
|
val id: String,
|
||||||
|
val name: String,
|
||||||
|
val version: String,
|
||||||
|
val author: String,
|
||||||
|
val description: String,
|
||||||
|
val args: String,
|
||||||
|
val enabled: Boolean,
|
||||||
|
val hasAction: Boolean
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package shirkneko.zako.sukisu.ui.viewmodel
|
package com.sukisu.ultra.ui.viewmodel
|
||||||
|
|
||||||
import android.os.SystemClock
|
import android.os.SystemClock
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
@@ -10,8 +10,8 @@ import androidx.lifecycle.ViewModel
|
|||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import shirkneko.zako.sukisu.ui.util.HanziToPinyin
|
import com.sukisu.ultra.ui.util.HanziToPinyin
|
||||||
import shirkneko.zako.sukisu.ui.util.listModules
|
import com.sukisu.ultra.ui.util.listModules
|
||||||
import org.json.JSONArray
|
import org.json.JSONArray
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
import java.text.Collator
|
import java.text.Collator
|
||||||
@@ -40,13 +40,6 @@ class ModuleViewModel : ViewModel() {
|
|||||||
val dirId: String, // real module id (dir name)
|
val dirId: String, // real module id (dir name)
|
||||||
)
|
)
|
||||||
|
|
||||||
data class ModuleUpdateInfo(
|
|
||||||
val version: String,
|
|
||||||
val versionCode: Int,
|
|
||||||
val zipUrl: String,
|
|
||||||
val changelog: String,
|
|
||||||
)
|
|
||||||
|
|
||||||
var isRefreshing by mutableStateOf(false)
|
var isRefreshing by mutableStateOf(false)
|
||||||
private set
|
private set
|
||||||
var search by mutableStateOf("")
|
var search by mutableStateOf("")
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package shirkneko.zako.sukisu.ui.viewmodel
|
package com.sukisu.ultra.ui.viewmodel
|
||||||
|
|
||||||
import android.content.ComponentName
|
import android.content.ComponentName
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
@@ -18,12 +18,12 @@ import com.topjohnwu.superuser.Shell
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import shirkneko.zako.sukisu.IKsuInterface
|
import com.sukisu.zako.IKsuInterface
|
||||||
import shirkneko.zako.sukisu.Natives
|
import com.sukisu.ultra.Natives
|
||||||
import shirkneko.zako.sukisu.ksuApp
|
import com.sukisu.ultra.ksuApp
|
||||||
import shirkneko.zako.sukisu.ui.KsuService
|
import com.sukisu.ultra.ui.KsuService
|
||||||
import shirkneko.zako.sukisu.ui.util.HanziToPinyin
|
import com.sukisu.ultra.ui.util.HanziToPinyin
|
||||||
import shirkneko.zako.sukisu.ui.util.KsuCli
|
import com.sukisu.ultra.ui.util.KsuCli
|
||||||
import java.text.Collator
|
import java.text.Collator
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.coroutines.resume
|
import kotlin.coroutines.resume
|
||||||
@@ -68,9 +68,9 @@ class SuperUserViewModel : ViewModel() {
|
|||||||
|
|
||||||
// 批量操作相关状态
|
// 批量操作相关状态
|
||||||
var showBatchActions by mutableStateOf(false)
|
var showBatchActions by mutableStateOf(false)
|
||||||
private set
|
internal set
|
||||||
var selectedApps by mutableStateOf<Set<String>>(emptySet())
|
var selectedApps by mutableStateOf<Set<String>>(emptySet())
|
||||||
private set
|
internal set
|
||||||
|
|
||||||
private val sortedList by derivedStateOf {
|
private val sortedList by derivedStateOf {
|
||||||
val comparator = compareBy<AppInfo> {
|
val comparator = compareBy<AppInfo> {
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package shirkneko.zako.sukisu.ui.viewmodel
|
package com.sukisu.ultra.ui.viewmodel
|
||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
@@ -10,12 +10,12 @@ import androidx.lifecycle.ViewModel
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import shirkneko.zako.sukisu.Natives
|
import com.sukisu.ultra.Natives
|
||||||
import shirkneko.zako.sukisu.profile.Capabilities
|
import com.sukisu.ultra.profile.Capabilities
|
||||||
import shirkneko.zako.sukisu.profile.Groups
|
import com.sukisu.ultra.profile.Groups
|
||||||
import shirkneko.zako.sukisu.ui.util.getAppProfileTemplate
|
import com.sukisu.ultra.ui.util.getAppProfileTemplate
|
||||||
import shirkneko.zako.sukisu.ui.util.listAppProfileTemplates
|
import com.sukisu.ultra.ui.util.listAppProfileTemplates
|
||||||
import shirkneko.zako.sukisu.ui.util.setAppProfileTemplate
|
import com.sukisu.ultra.ui.util.setAppProfileTemplate
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import org.json.JSONArray
|
import org.json.JSONArray
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package shirkneko.zako.sukisu.ui.webui;
|
package com.sukisu.ultra.ui.webui;
|
||||||
|
|
||||||
import java.net.URLConnection;
|
import java.net.URLConnection;
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package shirkneko.zako.sukisu.ui.webui;
|
package com.sukisu.ultra.ui.webui;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package shirkneko.zako.sukisu.ui.webui
|
package com.sukisu.ultra.ui.webui
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.ActivityManager
|
import android.app.ActivityManager
|
||||||
@@ -16,7 +16,7 @@ import androidx.core.view.WindowInsetsCompat
|
|||||||
import androidx.core.view.updateLayoutParams
|
import androidx.core.view.updateLayoutParams
|
||||||
import androidx.webkit.WebViewAssetLoader
|
import androidx.webkit.WebViewAssetLoader
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
import shirkneko.zako.sukisu.ui.util.createRootShell
|
import com.sukisu.ultra.ui.util.createRootShell
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
@SuppressLint("SetJavaScriptEnabled")
|
@SuppressLint("SetJavaScriptEnabled")
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package shirkneko.zako.sukisu.ui.webui
|
package com.sukisu.ultra.ui.webui
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
@@ -14,11 +14,13 @@ import androidx.core.view.WindowInsetsControllerCompat
|
|||||||
import com.topjohnwu.superuser.CallbackList
|
import com.topjohnwu.superuser.CallbackList
|
||||||
import com.topjohnwu.superuser.ShellUtils
|
import com.topjohnwu.superuser.ShellUtils
|
||||||
import com.topjohnwu.superuser.internal.UiThreadHandler
|
import com.topjohnwu.superuser.internal.UiThreadHandler
|
||||||
import shirkneko.zako.sukisu.ui.util.createRootShell
|
import com.sukisu.ultra.ui.util.createRootShell
|
||||||
import shirkneko.zako.sukisu.ui.util.listModules
|
import com.sukisu.ultra.ui.util.listModules
|
||||||
import shirkneko.zako.sukisu.ui.util.withNewRootShell
|
import com.sukisu.ultra.ui.util.withNewRootShell
|
||||||
import org.json.JSONArray
|
import org.json.JSONArray
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
|
import com.sukisu.ultra.ui.util.controlKpmModule
|
||||||
|
import com.sukisu.ultra.ui.util.listKpmModules
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util.concurrent.CompletableFuture
|
import java.util.concurrent.CompletableFuture
|
||||||
|
|
||||||
@@ -197,6 +199,18 @@ class WebViewInterface(
|
|||||||
}
|
}
|
||||||
return currentModuleInfo.toString()
|
return currentModuleInfo.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =================== KPM支持 =============================
|
||||||
|
|
||||||
|
@JavascriptInterface
|
||||||
|
fun listAllKpm() : String {
|
||||||
|
return listKpmModules()
|
||||||
|
}
|
||||||
|
|
||||||
|
@JavascriptInterface
|
||||||
|
fun controlKpm(name: String, args: String) : Int {
|
||||||
|
return controlKpmModule(name, args)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun hideSystemUI(window: Window) =
|
fun hideSystemUI(window: Window) =
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package shirkneko.zako.sukisu.utils
|
package com.sukisu.ultra.utils
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package io.sukisu.ultra;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
|
||||||
|
import com.sukisu.ultra.ui.util.KsuCli;
|
||||||
|
|
||||||
|
public class UltraShellHelper {
|
||||||
|
public static String runCmd(String cmds) {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for(String str : KsuCli.INSTANCE.getGLOBAL_MNT_SHELL()
|
||||||
|
.newJob()
|
||||||
|
.add(cmds)
|
||||||
|
.to(new ArrayList<>(), null)
|
||||||
|
.exec()
|
||||||
|
.getOut()) {
|
||||||
|
sb.append(str).append("\n");
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isPathExists(String path) {
|
||||||
|
return runCmd("file " + path).contains("No such file or directory");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void CopyFileTo(String path, String target) {
|
||||||
|
runCmd("cp -f " + path + " " + target);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package io.sukisu.ultra;
|
||||||
|
|
||||||
|
import static com.sukisu.ultra.ui.util.KsuCliKt.getKpmmgrPath;
|
||||||
|
import static com.sukisu.ultra.ui.util.KsuCliKt.getSuSFSDaemonPath;
|
||||||
|
|
||||||
|
public class UltraToolInstall {
|
||||||
|
private static final String OUTSIDE_KPMMGR_PATH = "/data/adb/ksu/bin/kpmmgr";
|
||||||
|
private static final String OUTSIDE_SUSFSD_PATH = "/data/adb/ksu/bin/susfsd";
|
||||||
|
public static void tryToInstall() {
|
||||||
|
String kpmmgrPath = getKpmmgrPath();
|
||||||
|
if (UltraShellHelper.isPathExists(OUTSIDE_KPMMGR_PATH)) {
|
||||||
|
UltraShellHelper.CopyFileTo(kpmmgrPath, OUTSIDE_KPMMGR_PATH);
|
||||||
|
UltraShellHelper.runCmd("chmod a+rx " + OUTSIDE_KPMMGR_PATH);
|
||||||
|
}
|
||||||
|
String SuSFSDaemonPath = getSuSFSDaemonPath();
|
||||||
|
if (UltraShellHelper.isPathExists(OUTSIDE_SUSFSD_PATH)) {
|
||||||
|
UltraShellHelper.CopyFileTo(SuSFSDaemonPath, OUTSIDE_SUSFSD_PATH);
|
||||||
|
UltraShellHelper.runCmd("chmod a+rx " + OUTSIDE_SUSFSD_PATH);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,156 +0,0 @@
|
|||||||
package shirkneko.zako.sukisu.ui
|
|
||||||
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.Bundle
|
|
||||||
import androidx.activity.ComponentActivity
|
|
||||||
import androidx.activity.compose.setContent
|
|
||||||
import androidx.activity.enableEdgeToEdge
|
|
||||||
import androidx.compose.animation.AnimatedContentTransitionScope
|
|
||||||
import androidx.compose.animation.EnterTransition
|
|
||||||
import androidx.compose.animation.ExitTransition
|
|
||||||
import androidx.compose.animation.core.tween
|
|
||||||
import androidx.compose.animation.fadeIn
|
|
||||||
import androidx.compose.animation.fadeOut
|
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
|
||||||
import androidx.compose.foundation.layout.WindowInsetsSides
|
|
||||||
import androidx.compose.foundation.layout.displayCutout
|
|
||||||
import androidx.compose.foundation.layout.only
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.systemBars
|
|
||||||
import androidx.compose.foundation.layout.union
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.NavigationBar
|
|
||||||
import androidx.compose.material3.NavigationBarItem
|
|
||||||
import androidx.compose.material3.Scaffold
|
|
||||||
import androidx.compose.material3.SnackbarHostState
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.graphics.luminance
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.navigation.NavBackStackEntry
|
|
||||||
import androidx.navigation.NavHostController
|
|
||||||
import androidx.navigation.compose.rememberNavController
|
|
||||||
import com.ramcosta.composedestinations.DestinationsNavHost
|
|
||||||
import com.ramcosta.composedestinations.animations.NavHostAnimatedDestinationStyle
|
|
||||||
import com.ramcosta.composedestinations.generated.NavGraphs
|
|
||||||
import com.ramcosta.composedestinations.utils.isRouteOnBackStackAsState
|
|
||||||
import com.ramcosta.composedestinations.utils.rememberDestinationsNavigator
|
|
||||||
import shirkneko.zako.sukisu.Natives
|
|
||||||
import shirkneko.zako.sukisu.ksuApp
|
|
||||||
import shirkneko.zako.sukisu.ui.screen.BottomBarDestination
|
|
||||||
import shirkneko.zako.sukisu.ui.theme.CardConfig
|
|
||||||
import shirkneko.zako.sukisu.ui.theme.KernelSUTheme
|
|
||||||
import shirkneko.zako.sukisu.ui.theme.loadCustomBackground
|
|
||||||
import shirkneko.zako.sukisu.ui.theme.loadThemeMode
|
|
||||||
import shirkneko.zako.sukisu.ui.util.LocalSnackbarHost
|
|
||||||
import shirkneko.zako.sukisu.ui.util.rootAvailable
|
|
||||||
import shirkneko.zako.sukisu.ui.util.install
|
|
||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
|
|
||||||
// Enable edge to edge
|
|
||||||
enableEdgeToEdge()
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
||||||
window.isNavigationBarContrastEnforced = false
|
|
||||||
}
|
|
||||||
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
|
|
||||||
// 加载保存的背景设置
|
|
||||||
loadCustomBackground()
|
|
||||||
loadThemeMode()
|
|
||||||
CardConfig.load(applicationContext)
|
|
||||||
|
|
||||||
|
|
||||||
val isManager = Natives.becomeManager(ksuApp.packageName)
|
|
||||||
if (isManager) install()
|
|
||||||
|
|
||||||
setContent {
|
|
||||||
KernelSUTheme {
|
|
||||||
val navController = rememberNavController()
|
|
||||||
val snackBarHostState = remember { SnackbarHostState() }
|
|
||||||
Scaffold(
|
|
||||||
bottomBar = { BottomBar(navController) },
|
|
||||||
contentWindowInsets = WindowInsets(0, 0, 0, 0)
|
|
||||||
) { innerPadding ->
|
|
||||||
CompositionLocalProvider(
|
|
||||||
LocalSnackbarHost provides snackBarHostState,
|
|
||||||
) {
|
|
||||||
DestinationsNavHost(
|
|
||||||
modifier = Modifier.padding(innerPadding),
|
|
||||||
navGraph = NavGraphs.root,
|
|
||||||
navController = navController,
|
|
||||||
defaultTransitions = object : NavHostAnimatedDestinationStyle() {
|
|
||||||
override val enterTransition: AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition
|
|
||||||
get() = { fadeIn(animationSpec = tween(340)) }
|
|
||||||
override val exitTransition: AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition
|
|
||||||
get() = { fadeOut(animationSpec = tween(340)) }
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun BottomBar(navController: NavHostController) {
|
|
||||||
val navigator = navController.rememberDestinationsNavigator()
|
|
||||||
val isManager = Natives.becomeManager(ksuApp.packageName)
|
|
||||||
val fullFeatured = isManager && !Natives.requireNewKernel() && rootAvailable()
|
|
||||||
|
|
||||||
// 获取卡片颜色和透明度
|
|
||||||
val cardColor = MaterialTheme.colorScheme.secondaryContainer
|
|
||||||
val cardAlpha = CardConfig.cardAlpha
|
|
||||||
val cardElevation = CardConfig.cardElevation
|
|
||||||
|
|
||||||
NavigationBar(
|
|
||||||
tonalElevation = cardElevation, // 动态设置阴影
|
|
||||||
containerColor = cardColor.copy(alpha = cardAlpha), // 动态设置颜色和透明度
|
|
||||||
contentColor = if (cardColor.luminance() > 0.5) Color.Black else Color.White, // 根据背景亮度设置文字颜色
|
|
||||||
windowInsets = WindowInsets.systemBars.union(WindowInsets.displayCutout).only(
|
|
||||||
WindowInsetsSides.Horizontal + WindowInsetsSides.Bottom
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
BottomBarDestination.entries.forEach { destination ->
|
|
||||||
if (!fullFeatured && destination.rootRequired) return@forEach
|
|
||||||
val isCurrentDestOnBackStack by navController.isRouteOnBackStackAsState(destination.direction)
|
|
||||||
NavigationBarItem(
|
|
||||||
selected = isCurrentDestOnBackStack,
|
|
||||||
onClick = {
|
|
||||||
if (isCurrentDestOnBackStack) {
|
|
||||||
navigator.popBackStack(destination.direction, false)
|
|
||||||
}
|
|
||||||
navigator.navigate(destination.direction) {
|
|
||||||
popUpTo(NavGraphs.root) {
|
|
||||||
saveState = true
|
|
||||||
}
|
|
||||||
launchSingleTop = true
|
|
||||||
restoreState = true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
icon = {
|
|
||||||
if (isCurrentDestOnBackStack) {
|
|
||||||
Icon(destination.iconSelected, stringResource(destination.label))
|
|
||||||
} else {
|
|
||||||
Icon(destination.iconNotSelected, stringResource(destination.label))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
label = { Text(stringResource(destination.label)) },
|
|
||||||
alwaysShowLabel = false,
|
|
||||||
colors = androidx.compose.material3.NavigationBarItemDefaults.colors(
|
|
||||||
unselectedTextColor = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
package shirkneko.zako.sukisu.ui.component
|
|
||||||
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.ListItem
|
|
||||||
import androidx.compose.material3.Switch
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun SwitchItem(
|
|
||||||
icon: ImageVector,
|
|
||||||
title: String,
|
|
||||||
summary: String,
|
|
||||||
checked: Boolean,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
onCheckedChange: (Boolean) -> Unit
|
|
||||||
) {
|
|
||||||
ListItem(
|
|
||||||
modifier = modifier,
|
|
||||||
leadingContent = { Icon(icon, contentDescription = null) },
|
|
||||||
headlineContent = { Text(title) },
|
|
||||||
supportingContent = { Text(summary) },
|
|
||||||
trailingContent = {
|
|
||||||
Switch(
|
|
||||||
checked = checked,
|
|
||||||
onCheckedChange = onCheckedChange
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,393 +0,0 @@
|
|||||||
package shirkneko.zako.sukisu.ui.screen
|
|
||||||
|
|
||||||
import androidx.annotation.StringRes
|
|
||||||
import androidx.compose.animation.Crossfade
|
|
||||||
import androidx.compose.foundation.gestures.detectTapGestures
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
|
||||||
import androidx.compose.foundation.layout.WindowInsetsSides
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.only
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.safeDrawing
|
|
||||||
import androidx.compose.foundation.layout.width
|
|
||||||
import androidx.compose.foundation.rememberScrollState
|
|
||||||
import androidx.compose.foundation.verticalScroll
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
|
||||||
import androidx.compose.material.icons.filled.AccountCircle
|
|
||||||
import androidx.compose.material.icons.filled.Android
|
|
||||||
import androidx.compose.material.icons.filled.Security
|
|
||||||
import androidx.compose.material3.DropdownMenu
|
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
||||||
import androidx.compose.material3.FilterChip
|
|
||||||
import androidx.compose.material3.HorizontalDivider
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.ListItem
|
|
||||||
import androidx.compose.material3.Scaffold
|
|
||||||
import androidx.compose.material3.SnackbarHost
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.material3.TopAppBar
|
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
|
||||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.geometry.Offset
|
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import androidx.compose.ui.unit.Dp
|
|
||||||
import androidx.compose.ui.unit.DpOffset
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.lifecycle.compose.dropUnlessResumed
|
|
||||||
import coil.compose.AsyncImage
|
|
||||||
import coil.request.ImageRequest
|
|
||||||
import com.ramcosta.composedestinations.annotation.Destination
|
|
||||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
|
||||||
import com.ramcosta.composedestinations.generated.destinations.AppProfileTemplateScreenDestination
|
|
||||||
import com.ramcosta.composedestinations.generated.destinations.TemplateEditorScreenDestination
|
|
||||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import shirkneko.zako.sukisu.Natives
|
|
||||||
import shirkneko.zako.sukisu.R
|
|
||||||
import shirkneko.zako.sukisu.ui.component.SwitchItem
|
|
||||||
import shirkneko.zako.sukisu.ui.component.profile.AppProfileConfig
|
|
||||||
import shirkneko.zako.sukisu.ui.component.profile.RootProfileConfig
|
|
||||||
import shirkneko.zako.sukisu.ui.component.profile.TemplateConfig
|
|
||||||
import shirkneko.zako.sukisu.ui.util.LocalSnackbarHost
|
|
||||||
import shirkneko.zako.sukisu.ui.util.forceStopApp
|
|
||||||
import shirkneko.zako.sukisu.ui.util.getSepolicy
|
|
||||||
import shirkneko.zako.sukisu.ui.util.launchApp
|
|
||||||
import shirkneko.zako.sukisu.ui.util.restartApp
|
|
||||||
import shirkneko.zako.sukisu.ui.util.setSepolicy
|
|
||||||
import shirkneko.zako.sukisu.ui.viewmodel.SuperUserViewModel
|
|
||||||
import shirkneko.zako.sukisu.ui.viewmodel.getTemplateInfoById
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author weishu
|
|
||||||
* @date 2023/5/16.
|
|
||||||
*/
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Destination<RootGraph>
|
|
||||||
@Composable
|
|
||||||
fun AppProfileScreen(
|
|
||||||
navigator: DestinationsNavigator,
|
|
||||||
appInfo: SuperUserViewModel.AppInfo,
|
|
||||||
) {
|
|
||||||
val context = LocalContext.current
|
|
||||||
val snackBarHost = LocalSnackbarHost.current
|
|
||||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
|
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
val failToUpdateAppProfile = stringResource(R.string.failed_to_update_app_profile).format(appInfo.label)
|
|
||||||
val failToUpdateSepolicy = stringResource(R.string.failed_to_update_sepolicy).format(appInfo.label)
|
|
||||||
val suNotAllowed = stringResource(R.string.su_not_allowed).format(appInfo.label)
|
|
||||||
|
|
||||||
val packageName = appInfo.packageName
|
|
||||||
val initialProfile = Natives.getAppProfile(packageName, appInfo.uid)
|
|
||||||
if (initialProfile.allowSu) {
|
|
||||||
initialProfile.rules = getSepolicy(packageName)
|
|
||||||
}
|
|
||||||
var profile by rememberSaveable {
|
|
||||||
mutableStateOf(initialProfile)
|
|
||||||
}
|
|
||||||
|
|
||||||
Scaffold(
|
|
||||||
topBar = {
|
|
||||||
TopBar(
|
|
||||||
onBack = dropUnlessResumed { navigator.popBackStack() },
|
|
||||||
scrollBehavior = scrollBehavior
|
|
||||||
)
|
|
||||||
},
|
|
||||||
snackbarHost = { SnackbarHost(hostState = snackBarHost) },
|
|
||||||
contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal)
|
|
||||||
) { paddingValues ->
|
|
||||||
AppProfileInner(
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(paddingValues)
|
|
||||||
.nestedScroll(scrollBehavior.nestedScrollConnection)
|
|
||||||
.verticalScroll(rememberScrollState()),
|
|
||||||
packageName = appInfo.packageName,
|
|
||||||
appLabel = appInfo.label,
|
|
||||||
appIcon = {
|
|
||||||
AsyncImage(
|
|
||||||
model = ImageRequest.Builder(context).data(appInfo.packageInfo).crossfade(true).build(),
|
|
||||||
contentDescription = appInfo.label,
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(4.dp)
|
|
||||||
.width(48.dp)
|
|
||||||
.height(48.dp)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
profile = profile,
|
|
||||||
onViewTemplate = {
|
|
||||||
getTemplateInfoById(it)?.let { info ->
|
|
||||||
navigator.navigate(TemplateEditorScreenDestination(info))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onManageTemplate = {
|
|
||||||
navigator.navigate(AppProfileTemplateScreenDestination())
|
|
||||||
},
|
|
||||||
onProfileChange = {
|
|
||||||
scope.launch {
|
|
||||||
if (it.allowSu) {
|
|
||||||
// sync with allowlist.c - forbid_system_uid
|
|
||||||
if (appInfo.uid < 2000 && appInfo.uid != 1000) {
|
|
||||||
snackBarHost.showSnackbar(suNotAllowed)
|
|
||||||
return@launch
|
|
||||||
}
|
|
||||||
if (!it.rootUseDefault && it.rules.isNotEmpty() && !setSepolicy(profile.name, it.rules)) {
|
|
||||||
snackBarHost.showSnackbar(failToUpdateSepolicy)
|
|
||||||
return@launch
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!Natives.setAppProfile(it)) {
|
|
||||||
snackBarHost.showSnackbar(failToUpdateAppProfile.format(appInfo.uid))
|
|
||||||
} else {
|
|
||||||
profile = it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun AppProfileInner(
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
packageName: String,
|
|
||||||
appLabel: String,
|
|
||||||
appIcon: @Composable () -> Unit,
|
|
||||||
profile: Natives.Profile,
|
|
||||||
onViewTemplate: (id: String) -> Unit = {},
|
|
||||||
onManageTemplate: () -> Unit = {},
|
|
||||||
onProfileChange: (Natives.Profile) -> Unit,
|
|
||||||
) {
|
|
||||||
val isRootGranted = profile.allowSu
|
|
||||||
|
|
||||||
Column(modifier = modifier) {
|
|
||||||
AppMenuBox(packageName) {
|
|
||||||
ListItem(
|
|
||||||
headlineContent = { Text(appLabel) },
|
|
||||||
supportingContent = { Text(packageName) },
|
|
||||||
leadingContent = appIcon,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
SwitchItem(
|
|
||||||
icon = Icons.Filled.Security,
|
|
||||||
title = stringResource(id = R.string.superuser),
|
|
||||||
checked = isRootGranted,
|
|
||||||
onCheckedChange = { onProfileChange(profile.copy(allowSu = it)) },
|
|
||||||
)
|
|
||||||
|
|
||||||
Crossfade(targetState = isRootGranted, label = "") { current ->
|
|
||||||
Column(
|
|
||||||
modifier = Modifier.padding(bottom = 6.dp + 48.dp + 6.dp /* SnackBar height */)
|
|
||||||
) {
|
|
||||||
if (current) {
|
|
||||||
val initialMode = if (profile.rootUseDefault) {
|
|
||||||
Mode.Default
|
|
||||||
} else if (profile.rootTemplate != null) {
|
|
||||||
Mode.Template
|
|
||||||
} else {
|
|
||||||
Mode.Custom
|
|
||||||
}
|
|
||||||
var mode by rememberSaveable {
|
|
||||||
mutableStateOf(initialMode)
|
|
||||||
}
|
|
||||||
ProfileBox(mode, true) {
|
|
||||||
// template mode shouldn't change profile here!
|
|
||||||
if (it == Mode.Default || it == Mode.Custom) {
|
|
||||||
onProfileChange(profile.copy(rootUseDefault = it == Mode.Default))
|
|
||||||
}
|
|
||||||
mode = it
|
|
||||||
}
|
|
||||||
Crossfade(targetState = mode, label = "") { currentMode ->
|
|
||||||
if (currentMode == Mode.Template) {
|
|
||||||
TemplateConfig(
|
|
||||||
profile = profile,
|
|
||||||
onViewTemplate = onViewTemplate,
|
|
||||||
onManageTemplate = onManageTemplate,
|
|
||||||
onProfileChange = onProfileChange
|
|
||||||
)
|
|
||||||
} else if (mode == Mode.Custom) {
|
|
||||||
RootProfileConfig(
|
|
||||||
fixedName = true,
|
|
||||||
profile = profile,
|
|
||||||
onProfileChange = onProfileChange
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
val mode = if (profile.nonRootUseDefault) Mode.Default else Mode.Custom
|
|
||||||
ProfileBox(mode, false) {
|
|
||||||
onProfileChange(profile.copy(nonRootUseDefault = (it == Mode.Default)))
|
|
||||||
}
|
|
||||||
Crossfade(targetState = mode, label = "") { currentMode ->
|
|
||||||
val modifyEnabled = currentMode == Mode.Custom
|
|
||||||
AppProfileConfig(
|
|
||||||
fixedName = true,
|
|
||||||
profile = profile,
|
|
||||||
enabled = modifyEnabled,
|
|
||||||
onProfileChange = onProfileChange
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private enum class Mode(@StringRes private val res: Int) {
|
|
||||||
Default(R.string.profile_default), Template(R.string.profile_template), Custom(R.string.profile_custom);
|
|
||||||
|
|
||||||
val text: String
|
|
||||||
@Composable get() = stringResource(res)
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
|
||||||
private fun TopBar(
|
|
||||||
onBack: () -> Unit,
|
|
||||||
scrollBehavior: TopAppBarScrollBehavior? = null
|
|
||||||
) {
|
|
||||||
TopAppBar(
|
|
||||||
title = {
|
|
||||||
Text(stringResource(R.string.profile))
|
|
||||||
},
|
|
||||||
navigationIcon = {
|
|
||||||
IconButton(
|
|
||||||
onClick = onBack
|
|
||||||
) { Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null) }
|
|
||||||
},
|
|
||||||
windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),
|
|
||||||
scrollBehavior = scrollBehavior
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun ProfileBox(
|
|
||||||
mode: Mode,
|
|
||||||
hasTemplate: Boolean,
|
|
||||||
onModeChange: (Mode) -> Unit,
|
|
||||||
) {
|
|
||||||
ListItem(
|
|
||||||
headlineContent = { Text(stringResource(R.string.profile)) },
|
|
||||||
supportingContent = { Text(mode.text) },
|
|
||||||
leadingContent = { Icon(Icons.Filled.AccountCircle, null) },
|
|
||||||
)
|
|
||||||
HorizontalDivider(thickness = Dp.Hairline)
|
|
||||||
ListItem(headlineContent = {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly
|
|
||||||
) {
|
|
||||||
FilterChip(
|
|
||||||
selected = mode == Mode.Default,
|
|
||||||
label = { Text(stringResource(R.string.profile_default)) },
|
|
||||||
onClick = { onModeChange(Mode.Default) },
|
|
||||||
)
|
|
||||||
if (hasTemplate) {
|
|
||||||
FilterChip(
|
|
||||||
selected = mode == Mode.Template,
|
|
||||||
label = { Text(stringResource(R.string.profile_template)) },
|
|
||||||
onClick = { onModeChange(Mode.Template) },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
FilterChip(
|
|
||||||
selected = mode == Mode.Custom,
|
|
||||||
label = { Text(stringResource(R.string.profile_custom)) },
|
|
||||||
onClick = { onModeChange(Mode.Custom) },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun AppMenuBox(packageName: String, content: @Composable () -> Unit) {
|
|
||||||
|
|
||||||
var expanded by remember { mutableStateOf(false) }
|
|
||||||
var touchPoint: Offset by remember { mutableStateOf(Offset.Zero) }
|
|
||||||
val density = LocalDensity.current
|
|
||||||
|
|
||||||
BoxWithConstraints(
|
|
||||||
Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.pointerInput(Unit) {
|
|
||||||
detectTapGestures {
|
|
||||||
touchPoint = it
|
|
||||||
expanded = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
|
|
||||||
content()
|
|
||||||
|
|
||||||
val (offsetX, offsetY) = with(density) {
|
|
||||||
(touchPoint.x.toDp()) to (touchPoint.y.toDp())
|
|
||||||
}
|
|
||||||
|
|
||||||
DropdownMenu(
|
|
||||||
expanded = expanded,
|
|
||||||
offset = DpOffset(offsetX, -offsetY),
|
|
||||||
onDismissRequest = {
|
|
||||||
expanded = false
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
DropdownMenuItem(
|
|
||||||
text = { Text(stringResource(id = R.string.launch_app)) },
|
|
||||||
onClick = {
|
|
||||||
expanded = false
|
|
||||||
launchApp(packageName)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
DropdownMenuItem(
|
|
||||||
text = { Text(stringResource(id = R.string.force_stop_app)) },
|
|
||||||
onClick = {
|
|
||||||
expanded = false
|
|
||||||
forceStopApp(packageName)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
DropdownMenuItem(
|
|
||||||
text = { Text(stringResource(id = R.string.restart_app)) },
|
|
||||||
onClick = {
|
|
||||||
expanded = false
|
|
||||||
restartApp(packageName)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
|
||||||
private fun AppProfilePreview() {
|
|
||||||
var profile by remember { mutableStateOf(Natives.Profile("")) }
|
|
||||||
AppProfileInner(
|
|
||||||
packageName = "icu.nullptr.test",
|
|
||||||
appLabel = "Test",
|
|
||||||
appIcon = { Icon(Icons.Filled.Android, null) },
|
|
||||||
profile = profile,
|
|
||||||
onProfileChange = {
|
|
||||||
profile = it
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,611 +0,0 @@
|
|||||||
package shirkneko.zako.sukisu.ui.screen
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.Context
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.PowerManager
|
|
||||||
import android.system.Os
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.annotation.StringRes
|
|
||||||
import androidx.compose.animation.*
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.layout.*
|
|
||||||
import androidx.compose.foundation.rememberScrollState
|
|
||||||
import androidx.compose.foundation.verticalScroll
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.filled.*
|
|
||||||
import androidx.compose.material.icons.outlined.*
|
|
||||||
import androidx.compose.material3.*
|
|
||||||
import androidx.compose.runtime.*
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
|
||||||
import androidx.compose.ui.platform.*
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.core.content.pm.PackageInfoCompat
|
|
||||||
import com.ramcosta.composedestinations.annotation.Destination
|
|
||||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
|
||||||
import com.ramcosta.composedestinations.generated.destinations.InstallScreenDestination
|
|
||||||
import com.ramcosta.composedestinations.generated.destinations.SettingScreenDestination
|
|
||||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import shirkneko.zako.sukisu.*
|
|
||||||
import shirkneko.zako.sukisu.R
|
|
||||||
import shirkneko.zako.sukisu.ui.component.rememberConfirmDialog
|
|
||||||
import shirkneko.zako.sukisu.ui.util.*
|
|
||||||
import shirkneko.zako.sukisu.ui.util.module.LatestVersionInfo
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import shirkneko.zako.sukisu.ui.theme.getCardColors
|
|
||||||
import shirkneko.zako.sukisu.ui.theme.getCardElevation
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
|
||||||
import androidx.compose.animation.fadeOut
|
|
||||||
import androidx.compose.animation.shrinkVertically
|
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
|
||||||
import shirkneko.zako.sukisu.ui.theme.CardConfig
|
|
||||||
import androidx.core.content.edit
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Destination<RootGraph>(start = true)
|
|
||||||
@Composable
|
|
||||||
fun HomeScreen(navigator: DestinationsNavigator) {
|
|
||||||
val context = LocalContext.current
|
|
||||||
var isSimpleMode by rememberSaveable { mutableStateOf(false) }
|
|
||||||
var isHideVersion by rememberSaveable { mutableStateOf(false) }
|
|
||||||
|
|
||||||
// 从 SharedPreferences 加载简洁模式状态
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
isSimpleMode = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
|
||||||
.getBoolean("is_simple_mode", false)
|
|
||||||
}
|
|
||||||
// 从 SharedPreferences 加载隐藏 KernelSU 版本号开关状态
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
isHideVersion = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
|
||||||
.getBoolean("is_hide_version", false)
|
|
||||||
}
|
|
||||||
val kernelVersion = getKernelVersion()
|
|
||||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
|
||||||
|
|
||||||
|
|
||||||
Scaffold(
|
|
||||||
topBar = {
|
|
||||||
TopBar(
|
|
||||||
kernelVersion,
|
|
||||||
onInstallClick = { navigator.navigate(InstallScreenDestination) },
|
|
||||||
onSettingsClick = { navigator.navigate(SettingScreenDestination) },
|
|
||||||
scrollBehavior = scrollBehavior
|
|
||||||
)
|
|
||||||
},
|
|
||||||
contentWindowInsets = WindowInsets.safeDrawing.only(
|
|
||||||
WindowInsetsSides.Top + WindowInsetsSides.Horizontal
|
|
||||||
)
|
|
||||||
) { innerPadding ->
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(innerPadding)
|
|
||||||
.nestedScroll(scrollBehavior.nestedScrollConnection)
|
|
||||||
.verticalScroll(rememberScrollState())
|
|
||||||
.padding(top = 12.dp)
|
|
||||||
.padding(horizontal = 16.dp),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
|
||||||
) {
|
|
||||||
val isManager = Natives.becomeManager(ksuApp.packageName)
|
|
||||||
val ksuVersion = if (isManager) Natives.version else null
|
|
||||||
val lkmMode = ksuVersion?.let {
|
|
||||||
if (it >= Natives.MINIMAL_SUPPORTED_KERNEL_LKM && kernelVersion.isGKI()) Natives.isLkmMode else null
|
|
||||||
}
|
|
||||||
|
|
||||||
StatusCard(kernelVersion, ksuVersion, lkmMode) {
|
|
||||||
navigator.navigate(InstallScreenDestination)
|
|
||||||
}
|
|
||||||
if (isManager && Natives.requireNewKernel()) {
|
|
||||||
WarningCard(
|
|
||||||
stringResource(id = R.string.require_kernel_version).format(
|
|
||||||
ksuVersion, Natives.MINIMAL_SUPPORTED_KERNEL
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (ksuVersion != null && !rootAvailable()) {
|
|
||||||
WarningCard(
|
|
||||||
stringResource(id = R.string.grant_root_failed)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
val checkUpdate =
|
|
||||||
LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
|
||||||
.getBoolean("check_update", true)
|
|
||||||
if (checkUpdate) {
|
|
||||||
UpdateCard()
|
|
||||||
}
|
|
||||||
val prefs = remember { context.getSharedPreferences("app_prefs", Context.MODE_PRIVATE) }
|
|
||||||
var clickCount by rememberSaveable { mutableIntStateOf(prefs.getInt("click_count", 0)) }
|
|
||||||
|
|
||||||
if (!isSimpleMode && clickCount < 3) {
|
|
||||||
AnimatedVisibility(
|
|
||||||
visible = clickCount < 3,
|
|
||||||
exit = shrinkVertically() + fadeOut()
|
|
||||||
) {
|
|
||||||
ElevatedCard(
|
|
||||||
colors = getCardColors(MaterialTheme.colorScheme.secondaryContainer),
|
|
||||||
elevation = CardDefaults.cardElevation(defaultElevation = getCardElevation())
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.clickable {
|
|
||||||
clickCount++
|
|
||||||
prefs.edit { putInt("click_count", clickCount) }
|
|
||||||
}
|
|
||||||
.padding(16.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.using_mksu_manager),
|
|
||||||
style = MaterialTheme.typography.bodyMedium
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
InfoCard()
|
|
||||||
if (!isSimpleMode) {
|
|
||||||
DonateCard()
|
|
||||||
LearnMoreCard()
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(Modifier)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun UpdateCard() {
|
|
||||||
val context = LocalContext.current
|
|
||||||
val latestVersionInfo = LatestVersionInfo()
|
|
||||||
val newVersion by produceState(initialValue = latestVersionInfo) {
|
|
||||||
value = withContext(Dispatchers.IO) {
|
|
||||||
checkNewVersion()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val currentVersionCode = getManagerVersion(context).second
|
|
||||||
val newVersionCode = newVersion.versionCode
|
|
||||||
val newVersionUrl = newVersion.downloadUrl
|
|
||||||
val changelog = newVersion.changelog
|
|
||||||
|
|
||||||
Log.d("UpdateCard", "Current version code: $currentVersionCode")
|
|
||||||
Log.d("UpdateCard", "New version code: $newVersionCode")
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
val uriHandler = LocalUriHandler.current
|
|
||||||
val title = stringResource(id = R.string.module_changelog)
|
|
||||||
val updateText = stringResource(id = R.string.module_update)
|
|
||||||
|
|
||||||
AnimatedVisibility(
|
|
||||||
visible = newVersionCode > currentVersionCode,
|
|
||||||
enter = fadeIn() + expandVertically(),
|
|
||||||
exit = shrinkVertically() + fadeOut()
|
|
||||||
) {
|
|
||||||
val updateDialog = rememberConfirmDialog(onConfirm = { uriHandler.openUri(newVersionUrl) })
|
|
||||||
WarningCard(
|
|
||||||
message = stringResource(id = R.string.new_version_available).format(newVersionCode),
|
|
||||||
MaterialTheme.colorScheme.outlineVariant
|
|
||||||
) {
|
|
||||||
if (changelog.isEmpty()) {
|
|
||||||
uriHandler.openUri(newVersionUrl)
|
|
||||||
} else {
|
|
||||||
updateDialog.showConfirm(
|
|
||||||
title = title,
|
|
||||||
content = changelog,
|
|
||||||
markdown = true,
|
|
||||||
confirm = updateText
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun RebootDropdownItem(@StringRes id: Int, reason: String = "") {
|
|
||||||
DropdownMenuItem(text = {
|
|
||||||
Text(stringResource(id))
|
|
||||||
}, onClick = {
|
|
||||||
reboot(reason)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
|
||||||
private fun TopBar(
|
|
||||||
kernelVersion: KernelVersion,
|
|
||||||
onInstallClick: () -> Unit,
|
|
||||||
onSettingsClick: () -> Unit,
|
|
||||||
scrollBehavior: TopAppBarScrollBehavior? = null
|
|
||||||
) {
|
|
||||||
val cardColor = MaterialTheme.colorScheme.secondaryContainer
|
|
||||||
val cardAlpha = CardConfig.cardAlpha
|
|
||||||
|
|
||||||
TopAppBar(
|
|
||||||
title = { Text(stringResource(R.string.app_name)) },
|
|
||||||
colors = TopAppBarDefaults.topAppBarColors(
|
|
||||||
containerColor = cardColor.copy(alpha = cardAlpha),
|
|
||||||
scrolledContainerColor = cardColor.copy(alpha = cardAlpha)
|
|
||||||
),
|
|
||||||
actions = {
|
|
||||||
if (kernelVersion.isGKI()) {
|
|
||||||
IconButton(onClick = onInstallClick) {
|
|
||||||
Icon(Icons.Filled.Archive, stringResource(R.string.install))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var showDropdown by remember { mutableStateOf(false) }
|
|
||||||
IconButton(onClick = { showDropdown = true }) {
|
|
||||||
Icon(Icons.Filled.Refresh, stringResource(R.string.reboot))
|
|
||||||
DropdownMenu(expanded = showDropdown, onDismissRequest = { showDropdown = false }
|
|
||||||
) {
|
|
||||||
|
|
||||||
RebootDropdownItem(id = R.string.reboot)
|
|
||||||
|
|
||||||
val pm = LocalContext.current.getSystemService(Context.POWER_SERVICE) as PowerManager?
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && pm?.isRebootingUserspaceSupported == true) {
|
|
||||||
RebootDropdownItem(id = R.string.reboot_userspace, reason = "userspace")
|
|
||||||
}
|
|
||||||
RebootDropdownItem(id = R.string.reboot_recovery, reason = "recovery")
|
|
||||||
RebootDropdownItem(id = R.string.reboot_bootloader, reason = "bootloader")
|
|
||||||
RebootDropdownItem(id = R.string.reboot_download, reason = "download")
|
|
||||||
RebootDropdownItem(id = R.string.reboot_edl, reason = "edl")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),
|
|
||||||
scrollBehavior = scrollBehavior
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun StatusCard(
|
|
||||||
kernelVersion: KernelVersion,
|
|
||||||
ksuVersion: Int?,
|
|
||||||
lkmMode: Boolean?,
|
|
||||||
onClickInstall: () -> Unit = {}
|
|
||||||
) {
|
|
||||||
ElevatedCard(
|
|
||||||
colors = getCardColors(MaterialTheme.colorScheme.secondaryContainer),
|
|
||||||
elevation = CardDefaults.cardElevation(defaultElevation = getCardElevation())
|
|
||||||
) {
|
|
||||||
Row(modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.clickable {
|
|
||||||
if (kernelVersion.isGKI()) {
|
|
||||||
onClickInstall()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(24.dp), verticalAlignment = Alignment.CenterVertically) {
|
|
||||||
when {
|
|
||||||
ksuVersion != null -> {
|
|
||||||
val safeMode = when {
|
|
||||||
Natives.isSafeMode -> " [${stringResource(id = R.string.safe_mode)}]"
|
|
||||||
else -> ""
|
|
||||||
}
|
|
||||||
|
|
||||||
val workingMode = when (lkmMode) {
|
|
||||||
null -> " <Non-GKI>"
|
|
||||||
true -> " <LKM>"
|
|
||||||
else -> " <GKI>"
|
|
||||||
}
|
|
||||||
|
|
||||||
val workingText =
|
|
||||||
"${stringResource(id = R.string.home_working)}$workingMode$safeMode"
|
|
||||||
|
|
||||||
val isHideVersion = LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
|
||||||
.getBoolean("is_hide_version", false)
|
|
||||||
|
|
||||||
Icon(Icons.Outlined.CheckCircle, stringResource(R.string.home_working))
|
|
||||||
Column(Modifier.padding(start = 20.dp)) {
|
|
||||||
Text(
|
|
||||||
text = workingText,
|
|
||||||
style = MaterialTheme.typography.titleMedium
|
|
||||||
)
|
|
||||||
if (!isHideVersion) {
|
|
||||||
Spacer(Modifier.height(4.dp))
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.home_working_version, ksuVersion),
|
|
||||||
style = MaterialTheme.typography.bodyMedium
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Spacer(Modifier.height(4.dp))
|
|
||||||
Text(
|
|
||||||
text = stringResource(
|
|
||||||
R.string.home_superuser_count, getSuperuserCount()
|
|
||||||
), style = MaterialTheme.typography.bodyMedium
|
|
||||||
)
|
|
||||||
Spacer(Modifier.height(4.dp))
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.home_module_count, getModuleCount()),
|
|
||||||
style = MaterialTheme.typography.bodyMedium
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
|
||||||
|
|
||||||
val suSFS = getSuSFS()
|
|
||||||
val translatedStatus = when (suSFS) {
|
|
||||||
"Supported" -> stringResource(R.string.status_supported)
|
|
||||||
"Not Supported" -> stringResource(R.string.status_not_supported)
|
|
||||||
else -> stringResource(R.string.status_unknown)
|
|
||||||
}
|
|
||||||
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.home_susfs, translatedStatus),
|
|
||||||
style = MaterialTheme.typography.bodyMedium
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
kernelVersion.isGKI() -> {
|
|
||||||
Icon(Icons.Outlined.Warning, stringResource(R.string.home_not_installed))
|
|
||||||
Column(Modifier.padding(start = 20.dp)) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.home_not_installed),
|
|
||||||
style = MaterialTheme.typography.titleMedium
|
|
||||||
)
|
|
||||||
Spacer(Modifier.height(4.dp))
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.home_click_to_install),
|
|
||||||
style = MaterialTheme.typography.bodyMedium
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
Icon(Icons.Outlined.Block, stringResource(R.string.home_unsupported))
|
|
||||||
Column(Modifier.padding(start = 20.dp)) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.home_unsupported),
|
|
||||||
style = MaterialTheme.typography.titleMedium
|
|
||||||
)
|
|
||||||
Spacer(Modifier.height(4.dp))
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.home_unsupported_reason),
|
|
||||||
style = MaterialTheme.typography.bodyMedium
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun WarningCard(
|
|
||||||
message: String, color: Color = MaterialTheme.colorScheme.error, onClick: (() -> Unit)? = null
|
|
||||||
) {
|
|
||||||
ElevatedCard(
|
|
||||||
colors = getCardColors(MaterialTheme.colorScheme.secondaryContainer),
|
|
||||||
elevation = CardDefaults.cardElevation(defaultElevation = getCardElevation())
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.then(onClick?.let { Modifier.clickable { it() } } ?: Modifier)
|
|
||||||
.padding(24.dp)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = message, style = MaterialTheme.typography.bodyMedium
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun LearnMoreCard() {
|
|
||||||
val uriHandler = LocalUriHandler.current
|
|
||||||
val url = stringResource(R.string.home_learn_kernelsu_url)
|
|
||||||
|
|
||||||
ElevatedCard(
|
|
||||||
colors = getCardColors(MaterialTheme.colorScheme.secondaryContainer),
|
|
||||||
elevation = CardDefaults.cardElevation(defaultElevation = getCardElevation())
|
|
||||||
) {
|
|
||||||
|
|
||||||
Row(modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.clickable {
|
|
||||||
uriHandler.openUri(url)
|
|
||||||
}
|
|
||||||
.padding(24.dp), verticalAlignment = Alignment.CenterVertically) {
|
|
||||||
Column {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.home_learn_kernelsu),
|
|
||||||
style = MaterialTheme.typography.titleSmall
|
|
||||||
)
|
|
||||||
Spacer(Modifier.height(4.dp))
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.home_click_to_learn_kernelsu),
|
|
||||||
style = MaterialTheme.typography.bodyMedium
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun DonateCard() {
|
|
||||||
val uriHandler = LocalUriHandler.current
|
|
||||||
|
|
||||||
ElevatedCard(
|
|
||||||
colors = getCardColors(MaterialTheme.colorScheme.secondaryContainer),
|
|
||||||
elevation = CardDefaults.cardElevation(defaultElevation = getCardElevation())
|
|
||||||
) {
|
|
||||||
|
|
||||||
Row(modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.clickable {
|
|
||||||
uriHandler.openUri("https://patreon.com/weishu")
|
|
||||||
}
|
|
||||||
.padding(24.dp), verticalAlignment = Alignment.CenterVertically) {
|
|
||||||
Column {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.home_support_title),
|
|
||||||
style = MaterialTheme.typography.titleSmall
|
|
||||||
)
|
|
||||||
Spacer(Modifier.height(4.dp))
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.home_support_content),
|
|
||||||
style = MaterialTheme.typography.bodyMedium
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun InfoCard() {
|
|
||||||
val context = LocalContext.current
|
|
||||||
val isSimpleMode = LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
|
||||||
.getBoolean("is_simple_mode", false)
|
|
||||||
|
|
||||||
ElevatedCard(
|
|
||||||
colors = getCardColors(MaterialTheme.colorScheme.secondaryContainer),
|
|
||||||
elevation = CardDefaults.cardElevation(defaultElevation = getCardElevation())
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(start = 24.dp, top = 24.dp, end = 24.dp, bottom = 16.dp)
|
|
||||||
) {
|
|
||||||
val contents = StringBuilder()
|
|
||||||
val uname = Os.uname()
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun InfoCardItem(
|
|
||||||
label: String,
|
|
||||||
content: String,
|
|
||||||
) {
|
|
||||||
contents.appendLine(label).appendLine(content).appendLine()
|
|
||||||
Text(text = label, style = MaterialTheme.typography.bodyLarge)
|
|
||||||
Text(text = content, style = MaterialTheme.typography.bodyMedium)
|
|
||||||
}
|
|
||||||
|
|
||||||
InfoCardItem(stringResource(R.string.home_kernel), uname.release)
|
|
||||||
|
|
||||||
if (!isSimpleMode) {
|
|
||||||
Spacer(Modifier.height(16.dp))
|
|
||||||
val androidVersion = Build.VERSION.RELEASE
|
|
||||||
InfoCardItem(stringResource(R.string.home_android_version), androidVersion)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
Spacer(Modifier.height(16.dp))
|
|
||||||
val deviceModel = getDeviceModel(context)
|
|
||||||
InfoCardItem(stringResource(R.string.home_device_model), deviceModel)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Spacer(Modifier.height(16.dp))
|
|
||||||
val managerVersion = getManagerVersion(context)
|
|
||||||
InfoCardItem(
|
|
||||||
stringResource(R.string.home_manager_version),
|
|
||||||
"${managerVersion.first} (${managerVersion.second})"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Spacer(Modifier.height(16.dp))
|
|
||||||
InfoCardItem(stringResource(R.string.home_selinux_status), getSELinuxStatus())
|
|
||||||
|
|
||||||
|
|
||||||
if (!isSimpleMode) {
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
|
|
||||||
val suSFS = getSuSFS()
|
|
||||||
if (suSFS == "Supported") {
|
|
||||||
InfoCardItem(
|
|
||||||
stringResource(R.string.home_susfs_version),
|
|
||||||
"${getSuSFSVersion()} (${stringResource(R.string.manual_hook)})"
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
val susSUMode = try {
|
|
||||||
susfsSUS_SU_Mode()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
0
|
|
||||||
}
|
|
||||||
|
|
||||||
if (susSUMode == 2 || susSUMode == 0) {
|
|
||||||
val isSUS_SU = getSuSFSFeatures() == "CONFIG_KSU_SUSFS_SUS_SU"
|
|
||||||
val susSUModeLabel = stringResource(R.string.sus_su_mode)
|
|
||||||
val susSUModeValue = susSUMode.toString()
|
|
||||||
val susSUModeText = if (isSUS_SU) " $susSUModeLabel $susSUModeValue" else ""
|
|
||||||
|
|
||||||
InfoCardItem(
|
|
||||||
stringResource(R.string.home_susfs_version),
|
|
||||||
"${getSuSFSVersion()} (${getSuSFSVariant()})$susSUModeText"
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
InfoCardItem(
|
|
||||||
stringResource(R.string.home_susfs_version),
|
|
||||||
"${getSuSFSVersion()} (${stringResource(R.string.manual_hook)})"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getManagerVersion(context: Context): Pair<String, Long> {
|
|
||||||
val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0)!!
|
|
||||||
val versionCode = PackageInfoCompat.getLongVersionCode(packageInfo)
|
|
||||||
return Pair(packageInfo.versionName!!, versionCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
|
||||||
private fun StatusCardPreview() {
|
|
||||||
Column {
|
|
||||||
StatusCard(KernelVersion(5, 10, 101), 1, null)
|
|
||||||
StatusCard(KernelVersion(5, 10, 101), 20000, true)
|
|
||||||
StatusCard(KernelVersion(5, 10, 101), null, true)
|
|
||||||
StatusCard(KernelVersion(4, 10, 101), null, false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
|
||||||
private fun WarningCardPreview() {
|
|
||||||
Column {
|
|
||||||
WarningCard(message = "Warning message")
|
|
||||||
WarningCard(
|
|
||||||
message = "Warning message ",
|
|
||||||
MaterialTheme.colorScheme.outlineVariant,
|
|
||||||
onClick = {})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("PrivateApi")
|
|
||||||
private fun getDeviceModel(context: Context): String {
|
|
||||||
return try {
|
|
||||||
val systemProperties = Class.forName("android.os.SystemProperties")
|
|
||||||
val getMethod = systemProperties.getMethod("get", String::class.java, String::class.java)
|
|
||||||
val marketNameKeys = listOf(
|
|
||||||
"ro.product.marketname", // Xiaomi
|
|
||||||
"ro.vendor.oplus.market.name", // Oppo, OnePlus, Realme
|
|
||||||
"ro.vivo.market.name", // Vivo
|
|
||||||
"ro.config.marketing_name" // Huawei
|
|
||||||
)
|
|
||||||
for (key in marketNameKeys) {
|
|
||||||
val marketName = getMethod.invoke(null, key, "") as String
|
|
||||||
if (marketName.isNotEmpty()) {
|
|
||||||
return marketName
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Build.DEVICE
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Build.DEVICE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,575 +0,0 @@
|
|||||||
package shirkneko.zako.sukisu.ui.screen
|
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.net.Uri
|
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
|
||||||
import androidx.annotation.StringRes
|
|
||||||
import androidx.compose.foundation.LocalIndication
|
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
|
||||||
import androidx.compose.foundation.layout.*
|
|
||||||
import androidx.compose.foundation.rememberScrollState
|
|
||||||
import androidx.compose.foundation.selection.toggleable
|
|
||||||
import androidx.compose.foundation.verticalScroll
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
|
||||||
import androidx.compose.material.icons.filled.FileUpload
|
|
||||||
import androidx.compose.material3.*
|
|
||||||
import androidx.compose.runtime.*
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.semantics.Role
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.documentfile.provider.DocumentFile
|
|
||||||
import com.maxkeppeker.sheets.core.models.base.Header
|
|
||||||
import com.maxkeppeker.sheets.core.models.base.rememberUseCaseState
|
|
||||||
import com.maxkeppeler.sheets.list.ListDialog
|
|
||||||
import com.maxkeppeler.sheets.list.models.ListOption
|
|
||||||
import com.maxkeppeler.sheets.list.models.ListSelection
|
|
||||||
import com.ramcosta.composedestinations.annotation.Destination
|
|
||||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
|
||||||
import com.ramcosta.composedestinations.generated.destinations.FlashScreenDestination
|
|
||||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
|
||||||
import com.ramcosta.composedestinations.navigation.EmptyDestinationsNavigator
|
|
||||||
import shirkneko.zako.sukisu.R
|
|
||||||
import shirkneko.zako.sukisu.ui.component.DialogHandle
|
|
||||||
import shirkneko.zako.sukisu.ui.component.rememberConfirmDialog
|
|
||||||
import shirkneko.zako.sukisu.ui.component.rememberCustomDialog
|
|
||||||
import shirkneko.zako.sukisu.ui.util.*
|
|
||||||
import shirkneko.zako.sukisu.utils.AssetsUtil
|
|
||||||
import java.io.File
|
|
||||||
import java.io.FileOutputStream
|
|
||||||
import java.io.IOException
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author weishu
|
|
||||||
* @date 2024/3/12.
|
|
||||||
*/
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Destination<RootGraph>
|
|
||||||
@Composable
|
|
||||||
fun InstallScreen(navigator: DestinationsNavigator) {
|
|
||||||
var installMethod by remember { mutableStateOf<InstallMethod?>(null) }
|
|
||||||
var lkmSelection by remember { mutableStateOf<LkmSelection>(LkmSelection.KmiNone) }
|
|
||||||
val context = LocalContext.current
|
|
||||||
|
|
||||||
var showRebootDialog by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
val onFlashComplete = {
|
|
||||||
showRebootDialog = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showRebootDialog) {
|
|
||||||
RebootDialog(
|
|
||||||
show = true,
|
|
||||||
onDismiss = { showRebootDialog = false },
|
|
||||||
onConfirm = {
|
|
||||||
showRebootDialog = false
|
|
||||||
try {
|
|
||||||
val process = Runtime.getRuntime().exec("su")
|
|
||||||
process.outputStream.bufferedWriter().use { writer ->
|
|
||||||
writer.write("svc power reboot\n")
|
|
||||||
writer.write("exit\n")
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Toast.makeText(context, R.string.failed_reboot, Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val onInstall = {
|
|
||||||
installMethod?.let { method ->
|
|
||||||
when (method) {
|
|
||||||
is InstallMethod.HorizonKernel -> {
|
|
||||||
method.uri?.let { uri ->
|
|
||||||
val worker = HorizonKernelWorker(context)
|
|
||||||
worker.uri = uri
|
|
||||||
worker.setOnFlashCompleteListener(onFlashComplete)
|
|
||||||
worker.start()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
val flashIt = FlashIt.FlashBoot(
|
|
||||||
boot = if (method is InstallMethod.SelectFile) method.uri else null,
|
|
||||||
lkm = lkmSelection,
|
|
||||||
ota = method is InstallMethod.DirectInstallToInactiveSlot
|
|
||||||
)
|
|
||||||
navigator.navigate(FlashScreenDestination(flashIt))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Unit
|
|
||||||
}
|
|
||||||
|
|
||||||
val currentKmi by produceState(initialValue = "") {
|
|
||||||
value = getCurrentKmi()
|
|
||||||
}
|
|
||||||
|
|
||||||
val selectKmiDialog = rememberSelectKmiDialog { kmi ->
|
|
||||||
kmi?.let {
|
|
||||||
lkmSelection = LkmSelection.KmiString(it)
|
|
||||||
onInstall()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val onClickNext = {
|
|
||||||
if (lkmSelection == LkmSelection.KmiNone && currentKmi.isBlank()) {
|
|
||||||
selectKmiDialog.show()
|
|
||||||
} else {
|
|
||||||
onInstall()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val selectLkmLauncher = rememberLauncherForActivityResult(
|
|
||||||
contract = ActivityResultContracts.StartActivityForResult()
|
|
||||||
) {
|
|
||||||
if (it.resultCode == Activity.RESULT_OK) {
|
|
||||||
it.data?.data?.let { uri ->
|
|
||||||
lkmSelection = LkmSelection.LkmUri(uri)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val onLkmUpload = {
|
|
||||||
selectLkmLauncher.launch(Intent(Intent.ACTION_GET_CONTENT).apply {
|
|
||||||
type = "application/octet-stream"
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
|
||||||
|
|
||||||
Scaffold(
|
|
||||||
topBar = {
|
|
||||||
TopBar(
|
|
||||||
onBack = { navigator.popBackStack() },
|
|
||||||
onLkmUpload = onLkmUpload,
|
|
||||||
scrollBehavior = scrollBehavior
|
|
||||||
)
|
|
||||||
},
|
|
||||||
contentWindowInsets = WindowInsets.safeDrawing.only(
|
|
||||||
WindowInsetsSides.Top + WindowInsetsSides.Horizontal
|
|
||||||
)
|
|
||||||
) { innerPadding ->
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(innerPadding)
|
|
||||||
.nestedScroll(scrollBehavior.nestedScrollConnection)
|
|
||||||
.verticalScroll(rememberScrollState())
|
|
||||||
) {
|
|
||||||
SelectInstallMethod { method ->
|
|
||||||
installMethod = method
|
|
||||||
}
|
|
||||||
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(16.dp)
|
|
||||||
) {
|
|
||||||
(lkmSelection as? LkmSelection.LkmUri)?.let {
|
|
||||||
Text(
|
|
||||||
stringResource(
|
|
||||||
id = R.string.selected_lkm,
|
|
||||||
it.uri.lastPathSegment ?: "(file)"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Button(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
enabled = installMethod != null,
|
|
||||||
onClick = onClickNext
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
stringResource(id = R.string.install_next),
|
|
||||||
fontSize = MaterialTheme.typography.bodyMedium.fontSize
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun launchHorizonKernelFlash(context: Context, uri: Uri) {
|
|
||||||
val worker = HorizonKernelWorker(context)
|
|
||||||
worker.uri = uri
|
|
||||||
worker.setOnFlashCompleteListener {
|
|
||||||
}
|
|
||||||
worker.start()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun RebootDialog(
|
|
||||||
show: Boolean,
|
|
||||||
onDismiss: () -> Unit,
|
|
||||||
onConfirm: () -> Unit
|
|
||||||
) {
|
|
||||||
if (show) {
|
|
||||||
AlertDialog(
|
|
||||||
onDismissRequest = onDismiss,
|
|
||||||
title = { Text(stringResource(id = R.string.reboot_complete_title)) },
|
|
||||||
text = { Text(stringResource(id = R.string.reboot_complete_msg)) },
|
|
||||||
confirmButton = {
|
|
||||||
TextButton(onClick = onConfirm) {
|
|
||||||
Text(stringResource(id = R.string.yes))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
dismissButton = {
|
|
||||||
TextButton(onClick = onDismiss) {
|
|
||||||
Text(stringResource(id = R.string.no))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private class HorizonKernelWorker(private val context: Context) : Thread() {
|
|
||||||
var uri: Uri? = null
|
|
||||||
private lateinit var filePath: String
|
|
||||||
private lateinit var binaryPath: String
|
|
||||||
|
|
||||||
|
|
||||||
private var onFlashComplete: (() -> Unit)? = null
|
|
||||||
|
|
||||||
fun setOnFlashCompleteListener(listener: () -> Unit) {
|
|
||||||
onFlashComplete = listener
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun run() {
|
|
||||||
filePath = "${context.filesDir.absolutePath}/${DocumentFile.fromSingleUri(context, uri!!)?.name}"
|
|
||||||
binaryPath = "${context.filesDir.absolutePath}/META-INF/com/google/android/update-binary"
|
|
||||||
|
|
||||||
try {
|
|
||||||
cleanup()
|
|
||||||
if (!rootAvailable()) {
|
|
||||||
showError(context.getString(R.string.root_required))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
copy()
|
|
||||||
if (!File(filePath).exists()) {
|
|
||||||
showError(context.getString(R.string.copy_failed))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
getBinary()
|
|
||||||
patch()
|
|
||||||
flash()
|
|
||||||
|
|
||||||
(context as? Activity)?.runOnUiThread {
|
|
||||||
onFlashComplete?.invoke()
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
showError(e.message ?: context.getString(R.string.unknown_error))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun cleanup() {
|
|
||||||
runCommand(false, "find ${context.filesDir.absolutePath} -type f ! -name '*.jpg' ! -name '*.png' -delete")
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun copy() {
|
|
||||||
uri?.let { safeUri ->
|
|
||||||
context.contentResolver.openInputStream(safeUri)?.use { input ->
|
|
||||||
FileOutputStream(File(filePath)).use { output ->
|
|
||||||
input.copyTo(output)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getBinary() {
|
|
||||||
runCommand(false, "unzip \"$filePath\" \"*/update-binary\" -d ${context.filesDir.absolutePath}")
|
|
||||||
if (!File(binaryPath).exists()) {
|
|
||||||
throw IOException("Failed to extract update-binary")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun patch() {
|
|
||||||
val mkbootfsPath = "${context.filesDir.absolutePath}/mkbootfs"
|
|
||||||
AssetsUtil.exportFiles(context, "mkbootfs", mkbootfsPath)
|
|
||||||
runCommand(false, "sed -i '/chmod -R 755 tools bin;/i cp -f $mkbootfsPath \$AKHOME/tools;' $binaryPath")
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun flash() {
|
|
||||||
val process = ProcessBuilder("su")
|
|
||||||
.redirectErrorStream(true)
|
|
||||||
.start()
|
|
||||||
|
|
||||||
try {
|
|
||||||
process.outputStream.bufferedWriter().use { writer ->
|
|
||||||
writer.write("export POSTINSTALL=${context.filesDir.absolutePath}\n")
|
|
||||||
writer.write("sh $binaryPath 3 1 \"$filePath\" && touch ${context.filesDir.absolutePath}/done\nexit\n")
|
|
||||||
writer.flush()
|
|
||||||
}
|
|
||||||
|
|
||||||
process.inputStream.bufferedReader().use { reader ->
|
|
||||||
reader.lineSequence().forEach { line ->
|
|
||||||
if (line.startsWith("ui_print")) {
|
|
||||||
showLog(line.removePrefix("ui_print"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
process.destroy()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!File("${context.filesDir.absolutePath}/done").exists()) {
|
|
||||||
throw IOException("Flash failed")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun runCommand(su: Boolean, cmd: String): Int {
|
|
||||||
val process = ProcessBuilder(if (su) "su" else "sh")
|
|
||||||
.redirectErrorStream(true)
|
|
||||||
.start()
|
|
||||||
|
|
||||||
return try {
|
|
||||||
process.outputStream.bufferedWriter().use { writer ->
|
|
||||||
writer.write("$cmd\n")
|
|
||||||
writer.write("exit\n")
|
|
||||||
writer.flush()
|
|
||||||
}
|
|
||||||
process.waitFor()
|
|
||||||
} finally {
|
|
||||||
process.destroy()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showError(message: String) {
|
|
||||||
(context as? Activity)?.runOnUiThread {
|
|
||||||
Toast.makeText(context, message, Toast.LENGTH_LONG).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showLog(message: String) {
|
|
||||||
(context as? Activity)?.runOnUiThread {
|
|
||||||
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sealed class InstallMethod {
|
|
||||||
data class SelectFile(
|
|
||||||
val uri: Uri? = null,
|
|
||||||
@StringRes override val label: Int = R.string.select_file,
|
|
||||||
override val summary: String?
|
|
||||||
) : InstallMethod()
|
|
||||||
|
|
||||||
data object DirectInstall : InstallMethod() {
|
|
||||||
override val label: Int
|
|
||||||
get() = R.string.direct_install
|
|
||||||
}
|
|
||||||
|
|
||||||
data object DirectInstallToInactiveSlot : InstallMethod() {
|
|
||||||
override val label: Int
|
|
||||||
get() = R.string.install_inactive_slot
|
|
||||||
}
|
|
||||||
|
|
||||||
data class HorizonKernel(
|
|
||||||
val uri: Uri? = null,
|
|
||||||
@StringRes override val label: Int = R.string.horizon_kernel,
|
|
||||||
override val summary: String? = null
|
|
||||||
) : InstallMethod()
|
|
||||||
|
|
||||||
abstract val label: Int
|
|
||||||
open val summary: String? = null
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun SelectInstallMethod(onSelected: (InstallMethod) -> Unit = {}) {
|
|
||||||
val rootAvailable = rootAvailable()
|
|
||||||
val isAbDevice = isAbDevice()
|
|
||||||
val selectFileTip = stringResource(
|
|
||||||
id = R.string.select_file_tip,
|
|
||||||
if (isInitBoot()) "init_boot" else "boot"
|
|
||||||
)
|
|
||||||
|
|
||||||
val radioOptions = mutableListOf<InstallMethod>(
|
|
||||||
InstallMethod.SelectFile(summary = selectFileTip)
|
|
||||||
)
|
|
||||||
|
|
||||||
if (rootAvailable) {
|
|
||||||
radioOptions.add(InstallMethod.DirectInstall)
|
|
||||||
if (isAbDevice) {
|
|
||||||
radioOptions.add(InstallMethod.DirectInstallToInactiveSlot)
|
|
||||||
}
|
|
||||||
radioOptions.add(InstallMethod.HorizonKernel(summary = "Flashing the Anykernel3 Kernel"))
|
|
||||||
}
|
|
||||||
|
|
||||||
var selectedOption by remember { mutableStateOf<InstallMethod?>(null) }
|
|
||||||
var currentSelectingMethod by remember { mutableStateOf<InstallMethod?>(null) }
|
|
||||||
|
|
||||||
val selectImageLauncher = rememberLauncherForActivityResult(
|
|
||||||
contract = ActivityResultContracts.StartActivityForResult()
|
|
||||||
) {
|
|
||||||
if (it.resultCode == Activity.RESULT_OK) {
|
|
||||||
it.data?.data?.let { uri ->
|
|
||||||
val option = when (currentSelectingMethod) {
|
|
||||||
is InstallMethod.SelectFile -> InstallMethod.SelectFile(uri, summary = selectFileTip)
|
|
||||||
is InstallMethod.HorizonKernel -> InstallMethod.HorizonKernel(uri, summary = " Flashing the Anykernel3 Kernel")
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
option?.let {
|
|
||||||
selectedOption = it
|
|
||||||
onSelected(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val confirmDialog = rememberConfirmDialog(
|
|
||||||
onConfirm = {
|
|
||||||
selectedOption = InstallMethod.DirectInstallToInactiveSlot
|
|
||||||
onSelected(InstallMethod.DirectInstallToInactiveSlot)
|
|
||||||
},
|
|
||||||
onDismiss = null
|
|
||||||
)
|
|
||||||
|
|
||||||
val dialogTitle = stringResource(id = android.R.string.dialog_alert_title)
|
|
||||||
val dialogContent = stringResource(id = R.string.install_inactive_slot_warning)
|
|
||||||
|
|
||||||
val onClick = { option: InstallMethod ->
|
|
||||||
currentSelectingMethod = option
|
|
||||||
when (option) {
|
|
||||||
is InstallMethod.SelectFile, is InstallMethod.HorizonKernel -> {
|
|
||||||
selectImageLauncher.launch(Intent(Intent.ACTION_GET_CONTENT).apply {
|
|
||||||
type = "application/*"
|
|
||||||
putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("application/octet-stream", "application/zip"))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
is InstallMethod.DirectInstall -> {
|
|
||||||
selectedOption = option
|
|
||||||
onSelected(option)
|
|
||||||
}
|
|
||||||
is InstallMethod.DirectInstallToInactiveSlot -> {
|
|
||||||
confirmDialog.showConfirm(dialogTitle, dialogContent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
radioOptions.forEach { option ->
|
|
||||||
val interactionSource = remember { MutableInteractionSource() }
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.toggleable(
|
|
||||||
value = option.javaClass == selectedOption?.javaClass,
|
|
||||||
onValueChange = { onClick(option) },
|
|
||||||
role = Role.RadioButton,
|
|
||||||
indication = LocalIndication.current,
|
|
||||||
interactionSource = interactionSource
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
RadioButton(
|
|
||||||
selected = option.javaClass == selectedOption?.javaClass,
|
|
||||||
onClick = { onClick(option) },
|
|
||||||
interactionSource = interactionSource
|
|
||||||
)
|
|
||||||
Column(
|
|
||||||
modifier = Modifier.padding(vertical = 12.dp)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(id = option.label),
|
|
||||||
fontSize = MaterialTheme.typography.titleMedium.fontSize,
|
|
||||||
fontFamily = MaterialTheme.typography.titleMedium.fontFamily,
|
|
||||||
fontStyle = MaterialTheme.typography.titleMedium.fontStyle
|
|
||||||
)
|
|
||||||
option.summary?.let {
|
|
||||||
Text(
|
|
||||||
text = it,
|
|
||||||
fontSize = MaterialTheme.typography.bodySmall.fontSize,
|
|
||||||
fontFamily = MaterialTheme.typography.bodySmall.fontFamily,
|
|
||||||
fontStyle = MaterialTheme.typography.bodySmall.fontStyle
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
|
||||||
fun rememberSelectKmiDialog(onSelected: (String?) -> Unit): DialogHandle {
|
|
||||||
return rememberCustomDialog { dismiss ->
|
|
||||||
val supportedKmi by produceState(initialValue = emptyList<String>()) {
|
|
||||||
value = getSupportedKmis()
|
|
||||||
}
|
|
||||||
val options = supportedKmi.map { value ->
|
|
||||||
ListOption(
|
|
||||||
titleText = value
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
var selection by remember { mutableStateOf<String?>(null) }
|
|
||||||
Surface(
|
|
||||||
color = MaterialTheme.colorScheme.secondaryContainer,
|
|
||||||
contentColor = MaterialTheme.colorScheme.onSecondaryContainer,
|
|
||||||
shape = MaterialTheme.shapes.medium
|
|
||||||
) {
|
|
||||||
ListDialog(
|
|
||||||
state = rememberUseCaseState(
|
|
||||||
visible = true,
|
|
||||||
onFinishedRequest = {
|
|
||||||
onSelected(selection)
|
|
||||||
},
|
|
||||||
onCloseRequest = {
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
),
|
|
||||||
header = Header.Default(
|
|
||||||
title = stringResource(R.string.select_kmi),
|
|
||||||
),
|
|
||||||
selection = ListSelection.Single(
|
|
||||||
showRadioButtons = true,
|
|
||||||
options = options,
|
|
||||||
) { _, option ->
|
|
||||||
selection = option.titleText
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
|
||||||
private fun TopBar(
|
|
||||||
onBack: () -> Unit = {},
|
|
||||||
onLkmUpload: () -> Unit = {},
|
|
||||||
scrollBehavior: TopAppBarScrollBehavior? = null
|
|
||||||
) {
|
|
||||||
TopAppBar(
|
|
||||||
title = { Text(stringResource(R.string.install)) },
|
|
||||||
navigationIcon = {
|
|
||||||
IconButton(onClick = onBack) {
|
|
||||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
actions = {
|
|
||||||
IconButton(onClick = onLkmUpload) {
|
|
||||||
Icon(Icons.Filled.FileUpload, contentDescription = null)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
windowInsets = WindowInsets.safeDrawing.only(
|
|
||||||
WindowInsetsSides.Top + WindowInsetsSides.Horizontal
|
|
||||||
),
|
|
||||||
scrollBehavior = scrollBehavior
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
|
||||||
fun SelectInstallPreview() {
|
|
||||||
InstallScreen(EmptyDestinationsNavigator)
|
|
||||||
}
|
|
||||||
@@ -1,537 +0,0 @@
|
|||||||
package shirkneko.zako.sukisu.ui.screen
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Build
|
|
||||||
import androidx.compose.foundation.layout.*
|
|
||||||
import androidx.compose.foundation.rememberScrollState
|
|
||||||
import androidx.compose.foundation.verticalScroll
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
|
||||||
import androidx.compose.material.icons.filled.*
|
|
||||||
import androidx.compose.material3.*
|
|
||||||
import androidx.compose.runtime.*
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.unit.DpSize
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
|
||||||
import androidx.compose.material3.Slider
|
|
||||||
import androidx.compose.material3.SliderDefaults
|
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import com.ramcosta.composedestinations.annotation.Destination
|
|
||||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
|
||||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
|
||||||
import com.topjohnwu.superuser.Shell
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import shirkneko.zako.sukisu.R
|
|
||||||
import shirkneko.zako.sukisu.ui.component.SwitchItem
|
|
||||||
import shirkneko.zako.sukisu.ui.theme.CardConfig
|
|
||||||
import shirkneko.zako.sukisu.ui.theme.ThemeColors
|
|
||||||
import shirkneko.zako.sukisu.ui.theme.ThemeConfig
|
|
||||||
import shirkneko.zako.sukisu.ui.theme.saveCustomBackground
|
|
||||||
import shirkneko.zako.sukisu.ui.theme.saveThemeColors
|
|
||||||
import shirkneko.zako.sukisu.ui.theme.saveThemeMode
|
|
||||||
import shirkneko.zako.sukisu.ui.theme.saveDynamicColorState
|
|
||||||
import shirkneko.zako.sukisu.ui.util.getSuSFS
|
|
||||||
import shirkneko.zako.sukisu.ui.util.getSuSFSFeatures
|
|
||||||
import shirkneko.zako.sukisu.ui.util.susfsSUS_SU_0
|
|
||||||
import shirkneko.zako.sukisu.ui.util.susfsSUS_SU_2
|
|
||||||
import shirkneko.zako.sukisu.ui.util.susfsSUS_SU_Mode
|
|
||||||
import androidx.core.content.edit
|
|
||||||
|
|
||||||
|
|
||||||
fun saveCardConfig(context: Context) {
|
|
||||||
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
|
||||||
with(prefs.edit()) {
|
|
||||||
putFloat("card_alpha", CardConfig.cardAlpha)
|
|
||||||
putBoolean("custom_background_enabled", CardConfig.cardElevation == 0.dp)
|
|
||||||
putBoolean("is_custom_alpha_set", CardConfig.isCustomAlphaSet)
|
|
||||||
apply()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Destination<RootGraph>
|
|
||||||
@Composable
|
|
||||||
fun MoreSettingsScreen(navigator: DestinationsNavigator) {
|
|
||||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
|
||||||
val context = LocalContext.current
|
|
||||||
val prefs = remember { context.getSharedPreferences("settings", Context.MODE_PRIVATE) }
|
|
||||||
// 主题模式选择
|
|
||||||
var themeMode by remember {
|
|
||||||
mutableIntStateOf(
|
|
||||||
when(ThemeConfig.forceDarkMode) {
|
|
||||||
true -> 2 // 深色
|
|
||||||
false -> 1 // 浅色
|
|
||||||
null -> 0 // 跟随系统
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 动态颜色开关状态
|
|
||||||
var useDynamicColor by remember {
|
|
||||||
mutableStateOf(ThemeConfig.useDynamicColor)
|
|
||||||
}
|
|
||||||
|
|
||||||
var showThemeModeDialog by remember { mutableStateOf(false) }
|
|
||||||
// 主题模式选项
|
|
||||||
val themeOptions = listOf(
|
|
||||||
stringResource(R.string.theme_follow_system),
|
|
||||||
stringResource(R.string.theme_light),
|
|
||||||
stringResource(R.string.theme_dark)
|
|
||||||
)
|
|
||||||
|
|
||||||
// 简洁模块开关状态
|
|
||||||
var isSimpleMode by remember {
|
|
||||||
mutableStateOf(prefs.getBoolean("is_simple_mode", false))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新简洁模块开关状态
|
|
||||||
val onSimpleModeChange = { newValue: Boolean ->
|
|
||||||
prefs.edit { putBoolean("is_simple_mode", newValue) }
|
|
||||||
isSimpleMode = newValue
|
|
||||||
}
|
|
||||||
|
|
||||||
// 隐藏内核 KernelSU 版本号开关状态
|
|
||||||
var isHideVersion by remember {
|
|
||||||
mutableStateOf(prefs.getBoolean("is_hide_version", false))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 隐藏内核 KernelSU 版本号模块开关状态
|
|
||||||
val onHideVersionChange = { newValue: Boolean ->
|
|
||||||
prefs.edit { putBoolean("is_hide_version", newValue) }
|
|
||||||
isHideVersion = newValue
|
|
||||||
}
|
|
||||||
|
|
||||||
// SELinux 状态
|
|
||||||
var selinuxEnabled by remember {
|
|
||||||
mutableStateOf(Shell.cmd("getenforce").exec().out.firstOrNull() == "Enforcing")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 卡片配置状态
|
|
||||||
var cardAlpha by rememberSaveable { mutableFloatStateOf(CardConfig.cardAlpha) }
|
|
||||||
var showCardSettings by remember { mutableStateOf(false) }
|
|
||||||
var isCustomBackgroundEnabled by rememberSaveable {
|
|
||||||
mutableStateOf(ThemeConfig.customBackgroundUri != null)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始化卡片配置
|
|
||||||
val systemIsDark = isSystemInDarkTheme()
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
CardConfig.apply {
|
|
||||||
cardAlpha = prefs.getFloat("card_alpha", 0.65f)
|
|
||||||
cardElevation = if (prefs.getBoolean("custom_background_enabled", false)) 0.dp else defaultElevation
|
|
||||||
isCustomAlphaSet = prefs.getBoolean("is_custom_alpha_set", false)
|
|
||||||
|
|
||||||
// 如果没有手动设置透明度,且是深色模式,则使用默认值
|
|
||||||
if (!isCustomAlphaSet) {
|
|
||||||
val isDarkMode = ThemeConfig.forceDarkMode ?: systemIsDark
|
|
||||||
if (isDarkMode) {
|
|
||||||
cardAlpha = 0.5f
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
themeMode = when (ThemeConfig.forceDarkMode) {
|
|
||||||
true -> 2
|
|
||||||
false -> 1
|
|
||||||
null -> 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 主题色选项
|
|
||||||
val themeColorOptions = listOf(
|
|
||||||
stringResource(R.string.color_default) to ThemeColors.Default,
|
|
||||||
stringResource(R.string.color_blue) to ThemeColors.Blue,
|
|
||||||
stringResource(R.string.color_green) to ThemeColors.Green,
|
|
||||||
stringResource(R.string.color_purple) to ThemeColors.Purple,
|
|
||||||
stringResource(R.string.color_orange) to ThemeColors.Orange,
|
|
||||||
stringResource(R.string.color_pink) to ThemeColors.Pink,
|
|
||||||
stringResource(R.string.color_gray) to ThemeColors.Gray,
|
|
||||||
stringResource(R.string.color_yellow) to ThemeColors.Yellow
|
|
||||||
)
|
|
||||||
|
|
||||||
var showThemeColorDialog by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
// 图片选择器
|
|
||||||
val pickImageLauncher = rememberLauncherForActivityResult(
|
|
||||||
ActivityResultContracts.GetContent()
|
|
||||||
) { uri: Uri? ->
|
|
||||||
uri?.let {
|
|
||||||
context.saveCustomBackground(it)
|
|
||||||
isCustomBackgroundEnabled = true
|
|
||||||
CardConfig.cardElevation = 0.dp
|
|
||||||
saveCardConfig(context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Scaffold(
|
|
||||||
topBar = {
|
|
||||||
TopAppBar(
|
|
||||||
title = { Text(stringResource(R.string.more_settings)) },
|
|
||||||
navigationIcon = {
|
|
||||||
IconButton(onClick = { navigator.popBackStack() }) {
|
|
||||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, null)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
scrollBehavior = scrollBehavior
|
|
||||||
)
|
|
||||||
}
|
|
||||||
) { paddingValues ->
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(paddingValues)
|
|
||||||
.verticalScroll(rememberScrollState())
|
|
||||||
.padding(top = 12.dp)
|
|
||||||
) {
|
|
||||||
// SELinux 开关
|
|
||||||
SwitchItem(
|
|
||||||
icon = Icons.Filled.Security,
|
|
||||||
title = stringResource(R.string.selinux),
|
|
||||||
summary = if (selinuxEnabled)
|
|
||||||
stringResource(R.string.selinux_enabled) else
|
|
||||||
stringResource(R.string.selinux_disabled),
|
|
||||||
checked = selinuxEnabled
|
|
||||||
) { enabled ->
|
|
||||||
val command = if (enabled) "setenforce 1" else "setenforce 0"
|
|
||||||
Shell.getShell().newJob().add(command).exec().let { result ->
|
|
||||||
if (result.isSuccess) selinuxEnabled = enabled
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加简洁模块开关
|
|
||||||
SwitchItem(
|
|
||||||
icon = Icons.Filled.FormatPaint,
|
|
||||||
title = stringResource(R.string.simple_mode),
|
|
||||||
summary = stringResource(R.string.simple_mode_summary),
|
|
||||||
checked = isSimpleMode
|
|
||||||
) {
|
|
||||||
onSimpleModeChange(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 隐藏内核部分版本号
|
|
||||||
SwitchItem(
|
|
||||||
icon = Icons.Filled.FormatPaint,
|
|
||||||
title = stringResource(R.string.hide_kernel_kernelsu_version),
|
|
||||||
summary = stringResource(R.string.hide_kernel_kernelsu_version_summary),
|
|
||||||
checked = isHideVersion
|
|
||||||
) {
|
|
||||||
onHideVersionChange(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
// region SUSFS 配置(仅在支持时显示)
|
|
||||||
val suSFS = getSuSFS()
|
|
||||||
val isSUS_SU = getSuSFSFeatures()
|
|
||||||
if (suSFS == "Supported") {
|
|
||||||
if (isSUS_SU == "CONFIG_KSU_SUSFS_SUS_SU") {
|
|
||||||
// 初始化时,默认启用
|
|
||||||
var isEnabled by rememberSaveable {
|
|
||||||
mutableStateOf(true) // 默认启用
|
|
||||||
}
|
|
||||||
|
|
||||||
// 在启动时检查状态
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
// 如果当前模式不是2就强制启用
|
|
||||||
val currentMode = susfsSUS_SU_Mode()
|
|
||||||
val wasManuallyDisabled = prefs.getBoolean("enable_sus_su", true)
|
|
||||||
if (currentMode != "2" && wasManuallyDisabled) {
|
|
||||||
susfsSUS_SU_2() // 强制切换到模式2
|
|
||||||
prefs.edit { putBoolean("enable_sus_su", true) }
|
|
||||||
}
|
|
||||||
isEnabled = currentMode == "2"
|
|
||||||
}
|
|
||||||
|
|
||||||
SwitchItem(
|
|
||||||
icon = Icons.Filled.VisibilityOff,
|
|
||||||
title = stringResource(id = R.string.settings_susfs_toggle),
|
|
||||||
summary = stringResource(id = R.string.settings_susfs_toggle_summary),
|
|
||||||
checked = isEnabled
|
|
||||||
) {
|
|
||||||
if (it) {
|
|
||||||
// 手动启用
|
|
||||||
susfsSUS_SU_2()
|
|
||||||
prefs.edit { putBoolean("enable_sus_su", true) }
|
|
||||||
} else {
|
|
||||||
// 手动关闭
|
|
||||||
susfsSUS_SU_0()
|
|
||||||
prefs.edit { putBoolean("enable_sus_su", false) }
|
|
||||||
}
|
|
||||||
isEnabled = it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// endregion
|
|
||||||
// 动态颜色开关
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
||||||
SwitchItem(
|
|
||||||
icon = Icons.Filled.ColorLens,
|
|
||||||
title = stringResource(R.string.dynamic_color_title),
|
|
||||||
summary = stringResource(R.string.dynamic_color_summary),
|
|
||||||
checked = useDynamicColor
|
|
||||||
) { enabled ->
|
|
||||||
useDynamicColor = enabled
|
|
||||||
context.saveDynamicColorState(enabled)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 只在未启用动态颜色时显示主题色选择
|
|
||||||
if (!useDynamicColor) {
|
|
||||||
ListItem(
|
|
||||||
leadingContent = { Icon(Icons.Default.Palette, null) },
|
|
||||||
headlineContent = { Text("主题颜色") },
|
|
||||||
supportingContent = {
|
|
||||||
val currentThemeName = when (ThemeConfig.currentTheme) {
|
|
||||||
is ThemeColors.Default -> stringResource(R.string.color_default)
|
|
||||||
is ThemeColors.Blue -> stringResource(R.string.color_blue)
|
|
||||||
is ThemeColors.Green -> stringResource(R.string.color_green)
|
|
||||||
is ThemeColors.Purple -> stringResource(R.string.color_purple)
|
|
||||||
is ThemeColors.Orange -> stringResource(R.string.color_orange)
|
|
||||||
is ThemeColors.Pink -> stringResource(R.string.color_pink)
|
|
||||||
is ThemeColors.Gray -> stringResource(R.string.color_gray)
|
|
||||||
is ThemeColors.Yellow -> stringResource(R.string.color_yellow)
|
|
||||||
else -> stringResource(R.string.color_default)
|
|
||||||
}
|
|
||||||
Text(currentThemeName)
|
|
||||||
},
|
|
||||||
modifier = Modifier.clickable { showThemeColorDialog = true }
|
|
||||||
)
|
|
||||||
|
|
||||||
if (showThemeColorDialog) {
|
|
||||||
AlertDialog(
|
|
||||||
onDismissRequest = { showThemeColorDialog = false },
|
|
||||||
title = { Text(stringResource(R.string.choose_theme_color)) },
|
|
||||||
text = {
|
|
||||||
Column {
|
|
||||||
themeColorOptions.forEach { (name, theme) ->
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.clickable {
|
|
||||||
context.saveThemeColors(when (theme) {
|
|
||||||
ThemeColors.Default -> "default"
|
|
||||||
ThemeColors.Blue -> "blue"
|
|
||||||
ThemeColors.Green -> "green"
|
|
||||||
ThemeColors.Purple -> "purple"
|
|
||||||
ThemeColors.Orange -> "orange"
|
|
||||||
ThemeColors.Pink -> "pink"
|
|
||||||
ThemeColors.Gray -> "gray"
|
|
||||||
ThemeColors.Yellow -> "yellow"
|
|
||||||
else -> "default"
|
|
||||||
})
|
|
||||||
showThemeColorDialog = false
|
|
||||||
}
|
|
||||||
.padding(vertical = 12.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
RadioButton(
|
|
||||||
selected = ThemeConfig.currentTheme::class == theme::class,
|
|
||||||
onClick = null
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.size(24.dp)
|
|
||||||
.background(theme.Primary, shape = CircleShape)
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
|
||||||
Text(name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
confirmButton = {}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 自定义背景开关
|
|
||||||
ListItem(
|
|
||||||
leadingContent = { Icon(Icons.Filled.Wallpaper, null) },
|
|
||||||
headlineContent = { Text(stringResource(id = R.string.settings_custom_background)) },
|
|
||||||
supportingContent = { Text(stringResource(id = R.string.settings_custom_background_summary)) },
|
|
||||||
modifier = Modifier.clickable {
|
|
||||||
if (isCustomBackgroundEnabled) {
|
|
||||||
showCardSettings = !showCardSettings
|
|
||||||
}
|
|
||||||
},
|
|
||||||
trailingContent = {
|
|
||||||
Switch(
|
|
||||||
checked = isCustomBackgroundEnabled,
|
|
||||||
onCheckedChange = { isChecked ->
|
|
||||||
if (isChecked) {
|
|
||||||
pickImageLauncher.launch("image/*")
|
|
||||||
} else {
|
|
||||||
context.saveCustomBackground(null)
|
|
||||||
isCustomBackgroundEnabled = false
|
|
||||||
CardConfig.cardElevation = CardConfig.defaultElevation
|
|
||||||
CardConfig.cardAlpha = 1f
|
|
||||||
CardConfig.isCustomAlphaSet = false
|
|
||||||
saveCardConfig(context)
|
|
||||||
cardAlpha = 0.65f
|
|
||||||
themeMode = 0
|
|
||||||
context.saveThemeMode(null)
|
|
||||||
CardConfig.isUserDarkModeEnabled = false
|
|
||||||
CardConfig.isUserLightModeEnabled = false
|
|
||||||
CardConfig.save(context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
if (ThemeConfig.customBackgroundUri != null && showCardSettings) {
|
|
||||||
// 透明度 Slider
|
|
||||||
ListItem(
|
|
||||||
leadingContent = { Icon(Icons.Filled.Opacity, null) },
|
|
||||||
headlineContent = { Text(stringResource(R.string.settings_card_alpha)) },
|
|
||||||
supportingContent = {
|
|
||||||
Slider(
|
|
||||||
value = cardAlpha,
|
|
||||||
onValueChange = { newValue ->
|
|
||||||
cardAlpha = newValue
|
|
||||||
CardConfig.cardAlpha = newValue
|
|
||||||
CardConfig.isCustomAlphaSet = true
|
|
||||||
prefs.edit { putBoolean("is_custom_alpha_set", true) }
|
|
||||||
prefs.edit { putFloat("card_alpha", newValue) }
|
|
||||||
},
|
|
||||||
onValueChangeFinished = {
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
|
||||||
saveCardConfig(context)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
valueRange = 0f..1f,
|
|
||||||
colors = getSliderColors(cardAlpha, useCustomColors = true),
|
|
||||||
thumb = {
|
|
||||||
SliderDefaults.Thumb(
|
|
||||||
interactionSource = remember { MutableInteractionSource() },
|
|
||||||
thumbSize = DpSize(0.dp, 0.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
ListItem(
|
|
||||||
leadingContent = { Icon(Icons.Filled.DarkMode, null) },
|
|
||||||
headlineContent = { Text(stringResource(R.string.theme_mode)) },
|
|
||||||
supportingContent = { Text(themeOptions[themeMode]) },
|
|
||||||
modifier = Modifier.clickable {
|
|
||||||
showThemeModeDialog = true
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
// 主题模式选择对话框
|
|
||||||
if (showThemeModeDialog) {
|
|
||||||
AlertDialog(
|
|
||||||
onDismissRequest = { showThemeModeDialog = false },
|
|
||||||
title = { Text(stringResource(R.string.theme_mode)) },
|
|
||||||
text = {
|
|
||||||
Column {
|
|
||||||
themeOptions.forEachIndexed { index, option ->
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.clickable {
|
|
||||||
themeMode = index
|
|
||||||
val newThemeMode = when(index) {
|
|
||||||
0 -> null // 跟随系统
|
|
||||||
1 -> false // 浅色
|
|
||||||
2 -> true // 深色
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
context.saveThemeMode(newThemeMode)
|
|
||||||
when (index) {
|
|
||||||
2 -> {
|
|
||||||
ThemeConfig.forceDarkMode = true
|
|
||||||
CardConfig.isUserLightModeEnabled = false
|
|
||||||
CardConfig.isUserDarkModeEnabled = true
|
|
||||||
CardConfig.save(context)
|
|
||||||
}
|
|
||||||
1 -> {
|
|
||||||
ThemeConfig.forceDarkMode = false
|
|
||||||
CardConfig.isUserLightModeEnabled = true
|
|
||||||
CardConfig.isUserDarkModeEnabled = false
|
|
||||||
CardConfig.save(context)
|
|
||||||
}
|
|
||||||
0 -> {
|
|
||||||
ThemeConfig.forceDarkMode = null
|
|
||||||
CardConfig.isUserLightModeEnabled = false
|
|
||||||
CardConfig.isUserDarkModeEnabled = false
|
|
||||||
CardConfig.save(context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
showThemeModeDialog = false
|
|
||||||
}
|
|
||||||
.padding(vertical = 12.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
RadioButton(
|
|
||||||
selected = themeMode == index,
|
|
||||||
onClick = null
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
|
||||||
Text(option)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
confirmButton = {}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun getSliderColors(cardAlpha: Float, useCustomColors: Boolean = false): SliderColors {
|
|
||||||
val theme = ThemeConfig.currentTheme
|
|
||||||
val isDarkTheme = ThemeConfig.forceDarkMode ?: isSystemInDarkTheme()
|
|
||||||
val useDynamicColor = ThemeConfig.useDynamicColor
|
|
||||||
|
|
||||||
return when {
|
|
||||||
// 使用动态颜色时
|
|
||||||
useDynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
|
||||||
SliderDefaults.colors(
|
|
||||||
activeTrackColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.8f),
|
|
||||||
inactiveTrackColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.3f),
|
|
||||||
thumbColor = MaterialTheme.colorScheme.primary
|
|
||||||
)
|
|
||||||
}
|
|
||||||
// 使用自定义主题色时
|
|
||||||
useCustomColors -> {
|
|
||||||
SliderDefaults.colors(
|
|
||||||
activeTrackColor = theme.getCustomSliderActiveColor(),
|
|
||||||
inactiveTrackColor = theme.getCustomSliderInactiveColor(),
|
|
||||||
thumbColor = theme.Primary
|
|
||||||
)
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
val activeColor = if (isDarkTheme) {
|
|
||||||
theme.Primary.copy(alpha = cardAlpha)
|
|
||||||
} else {
|
|
||||||
theme.Primary.copy(alpha = cardAlpha)
|
|
||||||
}
|
|
||||||
val inactiveColor = if (isDarkTheme) {
|
|
||||||
Color.DarkGray.copy(alpha = 0.3f)
|
|
||||||
} else {
|
|
||||||
Color.LightGray.copy(alpha = 0.3f)
|
|
||||||
}
|
|
||||||
SliderDefaults.colors(
|
|
||||||
activeTrackColor = activeColor,
|
|
||||||
inactiveTrackColor = inactiveColor,
|
|
||||||
thumbColor = activeColor
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,497 +0,0 @@
|
|||||||
package shirkneko.zako.sukisu.ui.screen
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.net.Uri
|
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
|
||||||
import androidx.compose.foundation.layout.WindowInsetsSides
|
|
||||||
import androidx.compose.foundation.layout.only
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.safeDrawing
|
|
||||||
import androidx.compose.foundation.rememberScrollState
|
|
||||||
import androidx.compose.foundation.verticalScroll
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.automirrored.filled.Undo
|
|
||||||
import androidx.compose.material.icons.filled.*
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.ListItem
|
|
||||||
import androidx.compose.material3.ModalBottomSheet
|
|
||||||
import androidx.compose.material3.Scaffold
|
|
||||||
import androidx.compose.material3.SnackbarHost
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.material3.TopAppBar
|
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
|
||||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
|
||||||
import androidx.compose.material3.rememberTopAppBarState
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.text.style.LineHeightStyle
|
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.core.content.FileProvider
|
|
||||||
import com.maxkeppeker.sheets.core.models.base.Header
|
|
||||||
import com.maxkeppeker.sheets.core.models.base.IconSource
|
|
||||||
import com.maxkeppeker.sheets.core.models.base.rememberUseCaseState
|
|
||||||
import com.maxkeppeler.sheets.list.ListDialog
|
|
||||||
import com.maxkeppeler.sheets.list.models.ListOption
|
|
||||||
import com.maxkeppeler.sheets.list.models.ListSelection
|
|
||||||
import com.ramcosta.composedestinations.annotation.Destination
|
|
||||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
|
||||||
import com.ramcosta.composedestinations.generated.destinations.AppProfileTemplateScreenDestination
|
|
||||||
import com.ramcosta.composedestinations.generated.destinations.FlashScreenDestination
|
|
||||||
import com.ramcosta.composedestinations.generated.destinations.MoreSettingsScreenDestination
|
|
||||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
|
||||||
import com.ramcosta.composedestinations.navigation.EmptyDestinationsNavigator
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import shirkneko.zako.sukisu.BuildConfig
|
|
||||||
import shirkneko.zako.sukisu.Natives
|
|
||||||
import shirkneko.zako.sukisu.R
|
|
||||||
import shirkneko.zako.sukisu.ui.component.AboutDialog
|
|
||||||
import shirkneko.zako.sukisu.ui.component.ConfirmResult
|
|
||||||
import shirkneko.zako.sukisu.ui.component.DialogHandle
|
|
||||||
import shirkneko.zako.sukisu.ui.component.SwitchItem
|
|
||||||
import shirkneko.zako.sukisu.ui.component.rememberConfirmDialog
|
|
||||||
import shirkneko.zako.sukisu.ui.component.rememberCustomDialog
|
|
||||||
import shirkneko.zako.sukisu.ui.component.rememberLoadingDialog
|
|
||||||
import shirkneko.zako.sukisu.ui.util.LocalSnackbarHost
|
|
||||||
import shirkneko.zako.sukisu.ui.util.getBugreportFile
|
|
||||||
import java.time.LocalDateTime
|
|
||||||
import java.time.format.DateTimeFormatter
|
|
||||||
import androidx.compose.material.icons.filled.ExpandMore
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import shirkneko.zako.sukisu.ui.theme.CardConfig
|
|
||||||
import androidx.core.content.edit
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author weishu
|
|
||||||
* @date 2023/1/1.
|
|
||||||
*/
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Destination<RootGraph>
|
|
||||||
@Composable
|
|
||||||
fun SettingScreen(navigator: DestinationsNavigator) {
|
|
||||||
// region 界面基础设置
|
|
||||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
|
||||||
val snackBarHost = LocalSnackbarHost.current
|
|
||||||
// endregion
|
|
||||||
|
|
||||||
Scaffold(
|
|
||||||
topBar = {
|
|
||||||
TopBar(
|
|
||||||
scrollBehavior = scrollBehavior
|
|
||||||
)
|
|
||||||
},
|
|
||||||
snackbarHost = { SnackbarHost(snackBarHost) },
|
|
||||||
contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal)
|
|
||||||
) { paddingValues ->
|
|
||||||
val aboutDialog = rememberCustomDialog {
|
|
||||||
AboutDialog(it)
|
|
||||||
}
|
|
||||||
val loadingDialog = rememberLoadingDialog()
|
|
||||||
val shrinkDialog = rememberConfirmDialog()
|
|
||||||
// endregion
|
|
||||||
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(paddingValues)
|
|
||||||
.nestedScroll(scrollBehavior.nestedScrollConnection)
|
|
||||||
.verticalScroll(rememberScrollState())
|
|
||||||
) {
|
|
||||||
// region 上下文与协程
|
|
||||||
val context = LocalContext.current
|
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
// endregion
|
|
||||||
|
|
||||||
// region 日志导出功能
|
|
||||||
val exportBugreportLauncher = rememberLauncherForActivityResult(
|
|
||||||
ActivityResultContracts.CreateDocument("application/gzip")
|
|
||||||
) { uri: Uri? ->
|
|
||||||
if (uri == null) return@rememberLauncherForActivityResult
|
|
||||||
scope.launch(Dispatchers.IO) {
|
|
||||||
loadingDialog.show()
|
|
||||||
context.contentResolver.openOutputStream(uri)?.use { output ->
|
|
||||||
getBugreportFile(context).inputStream().use {
|
|
||||||
it.copyTo(output)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
loadingDialog.hide()
|
|
||||||
snackBarHost.showSnackbar(context.getString(R.string.log_saved))
|
|
||||||
}
|
|
||||||
// endregion
|
|
||||||
}
|
|
||||||
// region 配置项列表
|
|
||||||
// 配置文件模板入口
|
|
||||||
val profileTemplate = stringResource(id = R.string.settings_profile_template)
|
|
||||||
ListItem(
|
|
||||||
leadingContent = { Icon(Icons.Filled.Fence, profileTemplate) },
|
|
||||||
headlineContent = { Text(profileTemplate) },
|
|
||||||
supportingContent = { Text(stringResource(id = R.string.settings_profile_template_summary)) },
|
|
||||||
modifier = Modifier.clickable {
|
|
||||||
navigator.navigate(AppProfileTemplateScreenDestination)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
// 卸载模块开关
|
|
||||||
var umountChecked by rememberSaveable {
|
|
||||||
mutableStateOf(Natives.isDefaultUmountModules())
|
|
||||||
}
|
|
||||||
SwitchItem(
|
|
||||||
icon = Icons.Filled.FolderDelete,
|
|
||||||
title = stringResource(id = R.string.settings_umount_modules_default),
|
|
||||||
summary = stringResource(id = R.string.settings_umount_modules_default_summary),
|
|
||||||
checked = umountChecked
|
|
||||||
) {
|
|
||||||
if (Natives.setDefaultUmountModules(it)) {
|
|
||||||
umountChecked = it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// SU 禁用开关(仅在兼容版本显示)
|
|
||||||
if (Natives.version >= Natives.MINIMAL_SUPPORTED_SU_COMPAT) {
|
|
||||||
var isSuDisabled by rememberSaveable {
|
|
||||||
mutableStateOf(!Natives.isSuEnabled())
|
|
||||||
}
|
|
||||||
SwitchItem(
|
|
||||||
icon = Icons.Filled.RemoveModerator,
|
|
||||||
title = stringResource(id = R.string.settings_disable_su),
|
|
||||||
summary = stringResource(id = R.string.settings_disable_su_summary),
|
|
||||||
checked = isSuDisabled,
|
|
||||||
) { checked ->
|
|
||||||
val shouldEnable = !checked
|
|
||||||
if (Natives.setSuEnabled(shouldEnable)) {
|
|
||||||
isSuDisabled = !shouldEnable
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
|
||||||
|
|
||||||
// 更新检查开关
|
|
||||||
var checkUpdate by rememberSaveable {
|
|
||||||
mutableStateOf(
|
|
||||||
prefs.getBoolean("check_update", true)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
SwitchItem(
|
|
||||||
icon = Icons.Filled.Update,
|
|
||||||
title = stringResource(id = R.string.settings_check_update),
|
|
||||||
summary = stringResource(id = R.string.settings_check_update_summary),
|
|
||||||
checked = checkUpdate
|
|
||||||
) {
|
|
||||||
prefs.edit {putBoolean("check_update", it) }
|
|
||||||
checkUpdate = it
|
|
||||||
}
|
|
||||||
|
|
||||||
// Web调试开关
|
|
||||||
var enableWebDebugging by rememberSaveable {
|
|
||||||
mutableStateOf(
|
|
||||||
prefs.getBoolean("enable_web_debugging", false)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
SwitchItem(
|
|
||||||
icon = Icons.Filled.DeveloperMode,
|
|
||||||
title = stringResource(id = R.string.enable_web_debugging),
|
|
||||||
summary = stringResource(id = R.string.enable_web_debugging_summary),
|
|
||||||
checked = enableWebDebugging
|
|
||||||
) {
|
|
||||||
prefs.edit { putBoolean("enable_web_debugging", it) }
|
|
||||||
enableWebDebugging = it
|
|
||||||
}
|
|
||||||
// endregion
|
|
||||||
val newButtonTitle = stringResource(id = R.string.more_settings)
|
|
||||||
ListItem(
|
|
||||||
leadingContent = {
|
|
||||||
Icon(
|
|
||||||
Icons.Filled.ExpandMore,
|
|
||||||
contentDescription = newButtonTitle
|
|
||||||
)
|
|
||||||
},
|
|
||||||
headlineContent = { Text(newButtonTitle) },
|
|
||||||
supportingContent = { Text(stringResource(id = R.string.more_settings)) },
|
|
||||||
modifier = Modifier.clickable {
|
|
||||||
navigator.navigate(MoreSettingsScreenDestination)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
var showBottomsheet by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
ListItem(
|
|
||||||
leadingContent = {
|
|
||||||
Icon(
|
|
||||||
Icons.Filled.BugReport,
|
|
||||||
stringResource(id = R.string.send_log)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
headlineContent = { Text(stringResource(id = R.string.send_log)) },
|
|
||||||
modifier = Modifier.clickable {
|
|
||||||
showBottomsheet = true
|
|
||||||
}
|
|
||||||
)
|
|
||||||
if (showBottomsheet) {
|
|
||||||
ModalBottomSheet(
|
|
||||||
onDismissRequest = { showBottomsheet = false },
|
|
||||||
content = {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(10.dp)
|
|
||||||
.align(Alignment.CenterHorizontally)
|
|
||||||
|
|
||||||
) {
|
|
||||||
Box {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(16.dp)
|
|
||||||
.clickable {
|
|
||||||
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH_mm")
|
|
||||||
val current = LocalDateTime.now().format(formatter)
|
|
||||||
exportBugreportLauncher.launch("KernelSU_bugreport_${current}.tar.gz")
|
|
||||||
showBottomsheet = false
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
Icons.Filled.Save,
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = Modifier.align(Alignment.CenterHorizontally)
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = stringResource(id = R.string.save_log),
|
|
||||||
modifier = Modifier.padding(top = 16.dp),
|
|
||||||
textAlign = TextAlign.Center.also {
|
|
||||||
LineHeightStyle(
|
|
||||||
alignment = LineHeightStyle.Alignment.Center,
|
|
||||||
trim = LineHeightStyle.Trim.None
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Box {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(16.dp)
|
|
||||||
.clickable {
|
|
||||||
scope.launch {
|
|
||||||
val bugreport = loadingDialog.withLoading {
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
getBugreportFile(context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val uri: Uri =
|
|
||||||
FileProvider.getUriForFile(
|
|
||||||
context,
|
|
||||||
"${BuildConfig.APPLICATION_ID}.fileprovider",
|
|
||||||
bugreport
|
|
||||||
)
|
|
||||||
|
|
||||||
val shareIntent = Intent(Intent.ACTION_SEND).apply {
|
|
||||||
putExtra(Intent.EXTRA_STREAM, uri)
|
|
||||||
setDataAndType(uri, "application/gzip")
|
|
||||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
|
||||||
}
|
|
||||||
|
|
||||||
context.startActivity(
|
|
||||||
Intent.createChooser(
|
|
||||||
shareIntent,
|
|
||||||
context.getString(R.string.send_log)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
Icons.Filled.Share,
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = Modifier.align(Alignment.CenterHorizontally)
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = stringResource(id = R.string.send_log),
|
|
||||||
modifier = Modifier.padding(top = 16.dp),
|
|
||||||
textAlign = TextAlign.Center.also {
|
|
||||||
LineHeightStyle(
|
|
||||||
alignment = LineHeightStyle.Alignment.Center,
|
|
||||||
trim = LineHeightStyle.Trim.None
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val lkmMode = Natives.version >= Natives.MINIMAL_SUPPORTED_KERNEL_LKM && Natives.isLkmMode
|
|
||||||
if (lkmMode) {
|
|
||||||
UninstallItem(navigator) {
|
|
||||||
loadingDialog.withLoading(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val about = stringResource(id = R.string.about)
|
|
||||||
ListItem(
|
|
||||||
leadingContent = {
|
|
||||||
Icon(
|
|
||||||
Icons.Filled.ContactPage,
|
|
||||||
about
|
|
||||||
)
|
|
||||||
},
|
|
||||||
headlineContent = { Text(about) },
|
|
||||||
modifier = Modifier.clickable {
|
|
||||||
aboutDialog.show()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun UninstallItem(
|
|
||||||
navigator: DestinationsNavigator,
|
|
||||||
withLoading: suspend (suspend () -> Unit) -> Unit
|
|
||||||
) {
|
|
||||||
val context = LocalContext.current
|
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
val uninstallConfirmDialog = rememberConfirmDialog()
|
|
||||||
val showTodo = {
|
|
||||||
Toast.makeText(context, "TODO", Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
val uninstallDialog = rememberUninstallDialog { uninstallType ->
|
|
||||||
scope.launch {
|
|
||||||
val result = uninstallConfirmDialog.awaitConfirm(
|
|
||||||
title = context.getString(uninstallType.title),
|
|
||||||
content = context.getString(uninstallType.message)
|
|
||||||
)
|
|
||||||
if (result == ConfirmResult.Confirmed) {
|
|
||||||
withLoading {
|
|
||||||
when (uninstallType) {
|
|
||||||
UninstallType.TEMPORARY -> showTodo()
|
|
||||||
UninstallType.PERMANENT -> navigator.navigate(
|
|
||||||
FlashScreenDestination(FlashIt.FlashUninstall)
|
|
||||||
)
|
|
||||||
UninstallType.RESTORE_STOCK_IMAGE -> navigator.navigate(
|
|
||||||
FlashScreenDestination(FlashIt.FlashRestore)
|
|
||||||
)
|
|
||||||
UninstallType.NONE -> Unit
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val uninstall = stringResource(id = R.string.settings_uninstall)
|
|
||||||
ListItem(
|
|
||||||
leadingContent = {
|
|
||||||
Icon(
|
|
||||||
Icons.Filled.Delete,
|
|
||||||
uninstall
|
|
||||||
)
|
|
||||||
},
|
|
||||||
headlineContent = { Text(uninstall) },
|
|
||||||
modifier = Modifier.clickable {
|
|
||||||
uninstallDialog.show()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
enum class UninstallType(val title: Int, val message: Int, val icon: ImageVector) {
|
|
||||||
TEMPORARY(
|
|
||||||
R.string.settings_uninstall_temporary,
|
|
||||||
R.string.settings_uninstall_temporary_message,
|
|
||||||
Icons.Filled.Delete
|
|
||||||
),
|
|
||||||
PERMANENT(
|
|
||||||
R.string.settings_uninstall_permanent,
|
|
||||||
R.string.settings_uninstall_permanent_message,
|
|
||||||
Icons.Filled.DeleteForever
|
|
||||||
),
|
|
||||||
RESTORE_STOCK_IMAGE(
|
|
||||||
R.string.settings_restore_stock_image,
|
|
||||||
R.string.settings_restore_stock_image_message,
|
|
||||||
Icons.AutoMirrored.Filled.Undo
|
|
||||||
),
|
|
||||||
NONE(0, 0, Icons.Filled.Delete)
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
|
||||||
fun rememberUninstallDialog(onSelected: (UninstallType) -> Unit): DialogHandle {
|
|
||||||
return rememberCustomDialog { dismiss ->
|
|
||||||
val options = listOf(
|
|
||||||
// UninstallType.TEMPORARY,
|
|
||||||
UninstallType.PERMANENT,
|
|
||||||
UninstallType.RESTORE_STOCK_IMAGE
|
|
||||||
)
|
|
||||||
val listOptions = options.map {
|
|
||||||
ListOption(
|
|
||||||
titleText = stringResource(it.title),
|
|
||||||
subtitleText = if (it.message != 0) stringResource(it.message) else null,
|
|
||||||
icon = IconSource(it.icon)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
var selection = UninstallType.NONE
|
|
||||||
ListDialog(state = rememberUseCaseState(visible = true, onFinishedRequest = {
|
|
||||||
if (selection != UninstallType.NONE) {
|
|
||||||
onSelected(selection)
|
|
||||||
}
|
|
||||||
}, onCloseRequest = {
|
|
||||||
dismiss()
|
|
||||||
}), header = Header.Default(
|
|
||||||
title = stringResource(R.string.settings_uninstall),
|
|
||||||
), selection = ListSelection.Single(
|
|
||||||
showRadioButtons = false,
|
|
||||||
options = listOptions,
|
|
||||||
) { index, _ ->
|
|
||||||
selection = options[index]
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
|
||||||
private fun TopBar(
|
|
||||||
scrollBehavior: TopAppBarScrollBehavior? = null
|
|
||||||
) {
|
|
||||||
val cardColor = MaterialTheme.colorScheme.secondaryContainer
|
|
||||||
val cardAlpha = CardConfig.cardAlpha
|
|
||||||
TopAppBar(
|
|
||||||
title = { Text(stringResource(R.string.settings)) },
|
|
||||||
colors = TopAppBarDefaults.topAppBarColors(
|
|
||||||
containerColor = cardColor.copy(alpha = cardAlpha),
|
|
||||||
scrolledContainerColor = cardColor.copy(alpha = cardAlpha)
|
|
||||||
),
|
|
||||||
|
|
||||||
windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),
|
|
||||||
scrollBehavior = scrollBehavior
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
|
||||||
private fun SettingsPreview() {
|
|
||||||
SettingScreen(EmptyDestinationsNavigator)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,393 +0,0 @@
|
|||||||
package shirkneko.zako.sukisu.ui.screen
|
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.gestures.detectTapGestures
|
|
||||||
import androidx.compose.foundation.layout.*
|
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
|
||||||
import androidx.compose.foundation.lazy.items
|
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.material.ExperimentalMaterialApi
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.filled.*
|
|
||||||
import androidx.compose.material3.*
|
|
||||||
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
|
||||||
import androidx.compose.runtime.*
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.text.TextStyle
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.ui.unit.sp
|
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
|
||||||
import coil.compose.AsyncImage
|
|
||||||
import coil.request.ImageRequest
|
|
||||||
import com.ramcosta.composedestinations.annotation.Destination
|
|
||||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
|
||||||
import com.ramcosta.composedestinations.generated.destinations.AppProfileScreenDestination
|
|
||||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import shirkneko.zako.sukisu.Natives
|
|
||||||
import shirkneko.zako.sukisu.R
|
|
||||||
import shirkneko.zako.sukisu.ui.component.SearchAppBar
|
|
||||||
import shirkneko.zako.sukisu.ui.util.ModuleModify
|
|
||||||
import shirkneko.zako.sukisu.ui.viewmodel.SuperUserViewModel
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterialApi::class, ExperimentalMaterial3Api::class)
|
|
||||||
@Destination<RootGraph>
|
|
||||||
@Composable
|
|
||||||
fun SuperUserScreen(navigator: DestinationsNavigator) {
|
|
||||||
val viewModel = viewModel<SuperUserViewModel>()
|
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
|
||||||
val listState = rememberLazyListState()
|
|
||||||
val context = LocalContext.current
|
|
||||||
val snackBarHostState = remember { SnackbarHostState() }
|
|
||||||
|
|
||||||
// 添加备份和还原启动器
|
|
||||||
val backupLauncher = ModuleModify.rememberAllowlistBackupLauncher(context, snackBarHostState)
|
|
||||||
val restoreLauncher = ModuleModify.rememberAllowlistRestoreLauncher(context, snackBarHostState)
|
|
||||||
|
|
||||||
LaunchedEffect(key1 = navigator) {
|
|
||||||
viewModel.search = ""
|
|
||||||
if (viewModel.appList.isEmpty()) {
|
|
||||||
viewModel.fetchAppList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(viewModel.search) {
|
|
||||||
if (viewModel.search.isEmpty()) {
|
|
||||||
listState.scrollToItem(0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Scaffold(
|
|
||||||
topBar = {
|
|
||||||
SearchAppBar(
|
|
||||||
title = { Text(stringResource(R.string.superuser)) },
|
|
||||||
searchText = viewModel.search,
|
|
||||||
onSearchTextChange = { viewModel.search = it },
|
|
||||||
onClearClick = { viewModel.search = "" },
|
|
||||||
dropdownContent = {
|
|
||||||
var showDropdown by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
IconButton(
|
|
||||||
onClick = { showDropdown = true },
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Filled.MoreVert,
|
|
||||||
contentDescription = stringResource(id = R.string.settings)
|
|
||||||
)
|
|
||||||
|
|
||||||
DropdownMenu(expanded = showDropdown, onDismissRequest = {
|
|
||||||
showDropdown = false
|
|
||||||
}) {
|
|
||||||
DropdownMenuItem(text = {
|
|
||||||
Text(stringResource(R.string.refresh))
|
|
||||||
}, onClick = {
|
|
||||||
scope.launch {
|
|
||||||
viewModel.fetchAppList()
|
|
||||||
}
|
|
||||||
showDropdown = false
|
|
||||||
})
|
|
||||||
DropdownMenuItem(text = {
|
|
||||||
Text(
|
|
||||||
if (viewModel.showSystemApps) {
|
|
||||||
stringResource(R.string.hide_system_apps)
|
|
||||||
} else {
|
|
||||||
stringResource(R.string.show_system_apps)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}, onClick = {
|
|
||||||
viewModel.showSystemApps = !viewModel.showSystemApps
|
|
||||||
showDropdown = false
|
|
||||||
})
|
|
||||||
DropdownMenuItem(text = {
|
|
||||||
Text(stringResource(R.string.backup_allowlist))
|
|
||||||
}, onClick = {
|
|
||||||
backupLauncher.launch(ModuleModify.createAllowlistBackupIntent())
|
|
||||||
showDropdown = false
|
|
||||||
})
|
|
||||||
DropdownMenuItem(text = {
|
|
||||||
Text(stringResource(R.string.restore_allowlist))
|
|
||||||
}, onClick = {
|
|
||||||
restoreLauncher.launch(ModuleModify.createAllowlistRestoreIntent())
|
|
||||||
showDropdown = false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
scrollBehavior = scrollBehavior
|
|
||||||
)
|
|
||||||
},
|
|
||||||
snackbarHost = { SnackbarHost(snackBarHostState) },
|
|
||||||
contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),
|
|
||||||
bottomBar = {
|
|
||||||
// 批量操作按钮,直接放在底部栏
|
|
||||||
if (viewModel.showBatchActions && viewModel.selectedApps.isNotEmpty()) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.background(MaterialTheme.colorScheme.surface)
|
|
||||||
.padding(16.dp),
|
|
||||||
horizontalArrangement = Arrangement.SpaceEvenly
|
|
||||||
) {
|
|
||||||
Button(
|
|
||||||
onClick = {
|
|
||||||
scope.launch {
|
|
||||||
viewModel.updateBatchPermissions(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
Text(stringResource(R.string.batch_authorization))
|
|
||||||
}
|
|
||||||
|
|
||||||
Button(
|
|
||||||
onClick = {
|
|
||||||
scope.launch {
|
|
||||||
viewModel.updateBatchPermissions(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
Text(stringResource(R.string.batch_cancel_authorization))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
) { innerPadding ->
|
|
||||||
PullToRefreshBox(
|
|
||||||
modifier = Modifier.padding(innerPadding),
|
|
||||||
onRefresh = {
|
|
||||||
scope.launch { viewModel.fetchAppList() }
|
|
||||||
},
|
|
||||||
isRefreshing = viewModel.isRefreshing
|
|
||||||
) {
|
|
||||||
LazyColumn(
|
|
||||||
state = listState,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.nestedScroll(scrollBehavior.nestedScrollConnection)
|
|
||||||
) {
|
|
||||||
// 获取分组后的应用列表 - 修改分组逻辑,避免应用重复出现在多个分组中
|
|
||||||
val rootApps = viewModel.appList.filter { it.allowSu }
|
|
||||||
val customApps = viewModel.appList.filter { !it.allowSu && it.hasCustomProfile }
|
|
||||||
val otherApps = viewModel.appList.filter { !it.allowSu && !it.hasCustomProfile }
|
|
||||||
|
|
||||||
// 显示ROOT权限应用组
|
|
||||||
if (rootApps.isNotEmpty()) {
|
|
||||||
items(rootApps, key = { "root_" + it.packageName + it.uid }) { app ->
|
|
||||||
AppItem(
|
|
||||||
app = app,
|
|
||||||
isSelected = viewModel.selectedApps.contains(app.packageName),
|
|
||||||
onToggleSelection = { viewModel.toggleAppSelection(app.packageName) },
|
|
||||||
onSwitchChange = { allowSu ->
|
|
||||||
scope.launch {
|
|
||||||
val profile = Natives.getAppProfile(app.packageName, app.uid)
|
|
||||||
val updatedProfile = profile.copy(allowSu = allowSu)
|
|
||||||
if (Natives.setAppProfile(updatedProfile)) {
|
|
||||||
viewModel.fetchAppList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onClick = {
|
|
||||||
if (viewModel.showBatchActions) {
|
|
||||||
viewModel.toggleAppSelection(app.packageName)
|
|
||||||
} else {
|
|
||||||
navigator.navigate(AppProfileScreenDestination(app))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onLongClick = {
|
|
||||||
// 长按进入多选模式
|
|
||||||
if (!viewModel.showBatchActions) {
|
|
||||||
viewModel.toggleBatchMode()
|
|
||||||
viewModel.toggleAppSelection(app.packageName)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
viewModel = viewModel
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 显示自定义配置应用组
|
|
||||||
if (customApps.isNotEmpty()) {
|
|
||||||
items(customApps, key = { "custom_" + it.packageName + it.uid }) { app ->
|
|
||||||
AppItem(
|
|
||||||
app = app,
|
|
||||||
isSelected = viewModel.selectedApps.contains(app.packageName),
|
|
||||||
onToggleSelection = { viewModel.toggleAppSelection(app.packageName) },
|
|
||||||
onSwitchChange = { allowSu ->
|
|
||||||
scope.launch {
|
|
||||||
val profile = Natives.getAppProfile(app.packageName, app.uid)
|
|
||||||
val updatedProfile = profile.copy(allowSu = allowSu)
|
|
||||||
if (Natives.setAppProfile(updatedProfile)) {
|
|
||||||
viewModel.fetchAppList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onClick = {
|
|
||||||
if (viewModel.showBatchActions) {
|
|
||||||
viewModel.toggleAppSelection(app.packageName)
|
|
||||||
} else {
|
|
||||||
navigator.navigate(AppProfileScreenDestination(app))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onLongClick = {
|
|
||||||
// 长按进入多选模式
|
|
||||||
if (!viewModel.showBatchActions) {
|
|
||||||
viewModel.toggleBatchMode()
|
|
||||||
viewModel.toggleAppSelection(app.packageName)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
viewModel = viewModel
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 显示其他应用组
|
|
||||||
if (otherApps.isNotEmpty()) {
|
|
||||||
items(otherApps, key = { "other_" + it.packageName + it.uid }) { app ->
|
|
||||||
AppItem(
|
|
||||||
app = app,
|
|
||||||
isSelected = viewModel.selectedApps.contains(app.packageName),
|
|
||||||
onToggleSelection = { viewModel.toggleAppSelection(app.packageName) },
|
|
||||||
onSwitchChange = { allowSu ->
|
|
||||||
scope.launch {
|
|
||||||
val profile = Natives.getAppProfile(app.packageName, app.uid)
|
|
||||||
val updatedProfile = profile.copy(allowSu = allowSu)
|
|
||||||
if (Natives.setAppProfile(updatedProfile)) {
|
|
||||||
viewModel.fetchAppList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onClick = {
|
|
||||||
if (viewModel.showBatchActions) {
|
|
||||||
viewModel.toggleAppSelection(app.packageName)
|
|
||||||
} else {
|
|
||||||
navigator.navigate(AppProfileScreenDestination(app))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onLongClick = {
|
|
||||||
// 长按进入多选模式
|
|
||||||
if (!viewModel.showBatchActions) {
|
|
||||||
viewModel.toggleBatchMode()
|
|
||||||
viewModel.toggleAppSelection(app.packageName)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
viewModel = viewModel
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun GroupHeader(title: String) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.background(MaterialTheme.colorScheme.surfaceVariant)
|
|
||||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = title,
|
|
||||||
style = TextStyle(
|
|
||||||
fontSize = 14.sp,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalLayoutApi::class)
|
|
||||||
@Composable
|
|
||||||
private fun AppItem(
|
|
||||||
app: SuperUserViewModel.AppInfo,
|
|
||||||
isSelected: Boolean,
|
|
||||||
onToggleSelection: () -> Unit,
|
|
||||||
onSwitchChange: (Boolean) -> Unit,
|
|
||||||
onClick: () -> Unit,
|
|
||||||
onLongClick: () -> Unit,
|
|
||||||
viewModel: SuperUserViewModel
|
|
||||||
) {
|
|
||||||
ListItem(
|
|
||||||
modifier = Modifier
|
|
||||||
.pointerInput(Unit) {
|
|
||||||
detectTapGestures(
|
|
||||||
onLongPress = { onLongClick() },
|
|
||||||
onTap = { onClick() }
|
|
||||||
)
|
|
||||||
},
|
|
||||||
headlineContent = { Text(app.label) },
|
|
||||||
supportingContent = {
|
|
||||||
Column {
|
|
||||||
Text(app.packageName)
|
|
||||||
FlowRow {
|
|
||||||
if (app.allowSu) {
|
|
||||||
LabelText(label = "ROOT")
|
|
||||||
} else {
|
|
||||||
if (Natives.uidShouldUmount(app.uid)) {
|
|
||||||
LabelText(label = "UMOUNT")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (app.hasCustomProfile) {
|
|
||||||
LabelText(label = "CUSTOM")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
leadingContent = {
|
|
||||||
AsyncImage(
|
|
||||||
model = ImageRequest.Builder(LocalContext.current)
|
|
||||||
.data(app.packageInfo)
|
|
||||||
.crossfade(true)
|
|
||||||
.build(),
|
|
||||||
contentDescription = app.label,
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(4.dp)
|
|
||||||
.width(48.dp)
|
|
||||||
.height(48.dp)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
trailingContent = {
|
|
||||||
if (!viewModel.showBatchActions) {
|
|
||||||
Switch(
|
|
||||||
checked = app.allowSu,
|
|
||||||
onCheckedChange = onSwitchChange
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
Checkbox(
|
|
||||||
checked = isSelected,
|
|
||||||
onCheckedChange = { onToggleSelection() }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun LabelText(label: String) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(top = 4.dp, end = 4.dp)
|
|
||||||
.background(
|
|
||||||
Color.Black,
|
|
||||||
shape = RoundedCornerShape(4.dp)
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = label,
|
|
||||||
modifier = Modifier.padding(vertical = 2.dp, horizontal = 5.dp),
|
|
||||||
style = TextStyle(
|
|
||||||
fontSize = 8.sp,
|
|
||||||
color = Color.White,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,297 +0,0 @@
|
|||||||
package shirkneko.zako.sukisu.ui.screen
|
|
||||||
|
|
||||||
import android.app.Activity.RESULT_OK
|
|
||||||
import android.content.Intent
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
|
||||||
import androidx.compose.foundation.layout.*
|
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
|
||||||
import androidx.compose.foundation.lazy.items
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.outlined.*
|
|
||||||
import androidx.compose.material3.*
|
|
||||||
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
|
||||||
import androidx.compose.runtime.*
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
|
||||||
import com.ramcosta.composedestinations.annotation.Destination
|
|
||||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
|
||||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import shirkneko.zako.sukisu.R
|
|
||||||
import shirkneko.zako.sukisu.ui.component.ConfirmResult
|
|
||||||
import shirkneko.zako.sukisu.ui.component.SearchAppBar
|
|
||||||
import shirkneko.zako.sukisu.ui.component.rememberConfirmDialog
|
|
||||||
import shirkneko.zako.sukisu.ui.component.rememberLoadingDialog
|
|
||||||
import shirkneko.zako.sukisu.ui.theme.getCardColors
|
|
||||||
import shirkneko.zako.sukisu.ui.theme.getCardElevation
|
|
||||||
import shirkneko.zako.sukisu.ui.viewmodel.KpmViewModel
|
|
||||||
import shirkneko.zako.sukisu.ui.util.loadKpmModule
|
|
||||||
import shirkneko.zako.sukisu.ui.util.unloadKpmModule
|
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Destination<RootGraph>
|
|
||||||
@Composable
|
|
||||||
fun KpmScreen(
|
|
||||||
navigator: DestinationsNavigator,
|
|
||||||
viewModel: KpmViewModel = viewModel()
|
|
||||||
) {
|
|
||||||
val context = LocalContext.current
|
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
val snackBarHost = remember { SnackbarHostState() }
|
|
||||||
val confirmDialog = rememberConfirmDialog()
|
|
||||||
val loadingDialog = rememberLoadingDialog()
|
|
||||||
|
|
||||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
|
||||||
|
|
||||||
val kpmInstall = stringResource(R.string.kpm_install)
|
|
||||||
val kpmInstallConfirm = stringResource(R.string.kpm_install_confirm)
|
|
||||||
val kpmInstallSuccess = stringResource(R.string.kpm_install_success)
|
|
||||||
val kpmInstallFailed = stringResource(R.string.kpm_install_failed)
|
|
||||||
val install = stringResource(R.string.install)
|
|
||||||
val cancel = stringResource(R.string.cancel)
|
|
||||||
val kpmUninstall = stringResource(R.string.kpm_uninstall)
|
|
||||||
val kpmUninstallConfirmTemplate = stringResource(R.string.kpm_uninstall_confirm)
|
|
||||||
val uninstall = stringResource(R.string.uninstall)
|
|
||||||
val kpmUninstallSuccess = stringResource(R.string.kpm_uninstall_success)
|
|
||||||
val kpmUninstallFailed = stringResource(R.string.kpm_uninstall_failed)
|
|
||||||
|
|
||||||
val selectPatchLauncher = rememberLauncherForActivityResult(
|
|
||||||
contract = ActivityResultContracts.StartActivityForResult()
|
|
||||||
) { result ->
|
|
||||||
if (result.resultCode != RESULT_OK) return@rememberLauncherForActivityResult
|
|
||||||
|
|
||||||
val uri = result.data?.data ?: return@rememberLauncherForActivityResult
|
|
||||||
|
|
||||||
scope.launch {
|
|
||||||
// 复制文件到临时目录
|
|
||||||
val tempFile = File(context.cacheDir, "temp_patch.kpm")
|
|
||||||
context.contentResolver.openInputStream(uri)?.use { input ->
|
|
||||||
tempFile.outputStream().use { output ->
|
|
||||||
input.copyTo(output)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val confirmResult = confirmDialog.awaitConfirm(
|
|
||||||
title = kpmInstall,
|
|
||||||
content = kpmInstallConfirm,
|
|
||||||
confirm = install,
|
|
||||||
dismiss = cancel
|
|
||||||
)
|
|
||||||
|
|
||||||
if (confirmResult == ConfirmResult.Confirmed) {
|
|
||||||
val success = loadingDialog.withLoading {
|
|
||||||
loadKpmModule(tempFile.absolutePath)
|
|
||||||
}
|
|
||||||
|
|
||||||
Log.d("KsuCli", "loadKpmModule result: $success")
|
|
||||||
|
|
||||||
if (success == "success") {
|
|
||||||
viewModel.fetchModuleList()
|
|
||||||
snackBarHost.showSnackbar(
|
|
||||||
message = kpmInstallSuccess,
|
|
||||||
duration = SnackbarDuration.Long
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
// 修正为显示安装失败的消息
|
|
||||||
snackBarHost.showSnackbar(
|
|
||||||
message = kpmInstallFailed,
|
|
||||||
duration = SnackbarDuration.Long
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tempFile.delete()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
if (viewModel.moduleList.isEmpty()) {
|
|
||||||
viewModel.fetchModuleList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Scaffold(
|
|
||||||
topBar = {
|
|
||||||
SearchAppBar(
|
|
||||||
title = { Text(stringResource(R.string.kpm_title)) },
|
|
||||||
searchText = viewModel.search,
|
|
||||||
onSearchTextChange = { viewModel.search = it },
|
|
||||||
onClearClick = { viewModel.search = "" },
|
|
||||||
scrollBehavior = scrollBehavior,
|
|
||||||
dropdownContent = {
|
|
||||||
IconButton(onClick = { viewModel.fetchModuleList() }) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Outlined.Refresh,
|
|
||||||
contentDescription = stringResource(R.string.refresh)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
},
|
|
||||||
floatingActionButton = {
|
|
||||||
ExtendedFloatingActionButton(
|
|
||||||
onClick = {
|
|
||||||
selectPatchLauncher.launch(
|
|
||||||
Intent(Intent.ACTION_GET_CONTENT).apply {
|
|
||||||
type = "application/*"
|
|
||||||
}
|
|
||||||
)
|
|
||||||
},
|
|
||||||
icon = {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Outlined.Add,
|
|
||||||
contentDescription = stringResource(R.string.kpm_install)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
text = { Text(stringResource(R.string.kpm_install)) },
|
|
||||||
containerColor = MaterialTheme.colorScheme.secondaryContainer,
|
|
||||||
contentColor = MaterialTheme.colorScheme.onSecondaryContainer
|
|
||||||
)
|
|
||||||
},
|
|
||||||
snackbarHost = { SnackbarHost(snackBarHost) }
|
|
||||||
) { padding ->
|
|
||||||
PullToRefreshBox(
|
|
||||||
onRefresh = { viewModel.fetchModuleList() },
|
|
||||||
isRefreshing = viewModel.isRefreshing,
|
|
||||||
modifier = Modifier.padding(padding)
|
|
||||||
) {
|
|
||||||
if (viewModel.moduleList.isEmpty()) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier.fillMaxSize(),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
stringResource(R.string.kpm_empty),
|
|
||||||
textAlign = TextAlign.Center
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
LazyColumn(
|
|
||||||
modifier = Modifier.fillMaxSize(),
|
|
||||||
contentPadding = PaddingValues(16.dp),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
|
||||||
) {
|
|
||||||
items(viewModel.moduleList) { module ->
|
|
||||||
val kpmUninstallConfirm = String.format(kpmUninstallConfirmTemplate, module.name)
|
|
||||||
KpmModuleItem(
|
|
||||||
module = module,
|
|
||||||
onUninstall = {
|
|
||||||
scope.launch {
|
|
||||||
val confirmResult = confirmDialog.awaitConfirm(
|
|
||||||
title = kpmUninstall,
|
|
||||||
content = kpmUninstallConfirm,
|
|
||||||
confirm = uninstall,
|
|
||||||
dismiss = cancel
|
|
||||||
)
|
|
||||||
if (confirmResult == ConfirmResult.Confirmed) {
|
|
||||||
val success = loadingDialog.withLoading {
|
|
||||||
unloadKpmModule(module.id)
|
|
||||||
}
|
|
||||||
Log.d("KsuCli", "unloadKpmModule result: $success")
|
|
||||||
if (success == "success") {
|
|
||||||
viewModel.fetchModuleList()
|
|
||||||
snackBarHost.showSnackbar(
|
|
||||||
message = kpmUninstallSuccess,
|
|
||||||
duration = SnackbarDuration.Long
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
snackBarHost.showSnackbar(
|
|
||||||
message = kpmUninstallFailed,
|
|
||||||
duration = SnackbarDuration.Long
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onControl = {
|
|
||||||
viewModel.loadModuleDetail(module.id)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun KpmModuleItem(
|
|
||||||
module: KpmViewModel.ModuleInfo,
|
|
||||||
onUninstall: () -> Unit,
|
|
||||||
onControl: () -> Unit
|
|
||||||
) {
|
|
||||||
ElevatedCard(
|
|
||||||
colors = getCardColors(MaterialTheme.colorScheme.secondaryContainer),
|
|
||||||
elevation = CardDefaults.cardElevation(defaultElevation = getCardElevation())
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier.padding(16.dp)
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Column(modifier = Modifier.weight(1f)) {
|
|
||||||
Text(
|
|
||||||
text = module.name,
|
|
||||||
style = MaterialTheme.typography.titleMedium
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = "${stringResource(R.string.kpm_version)}: ${module.version}",
|
|
||||||
style = MaterialTheme.typography.bodyMedium
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = "${stringResource(R.string.kpm_author)}: ${module.author}",
|
|
||||||
style = MaterialTheme.typography.bodyMedium
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = "${stringResource(R.string.kpm_args)}: ${module.args}",
|
|
||||||
style = MaterialTheme.typography.bodyMedium
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
|
|
||||||
Text(
|
|
||||||
text = module.description,
|
|
||||||
style = MaterialTheme.typography.bodyMedium
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
|
||||||
) {
|
|
||||||
FilledTonalButton(
|
|
||||||
onClick = onControl
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Outlined.Settings,
|
|
||||||
contentDescription = null
|
|
||||||
)
|
|
||||||
Text(stringResource(R.string.kpm_control))
|
|
||||||
}
|
|
||||||
|
|
||||||
FilledTonalButton(
|
|
||||||
onClick = onUninstall
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Outlined.Delete,
|
|
||||||
contentDescription = null
|
|
||||||
)
|
|
||||||
Text(stringResource(R.string.kpm_uninstall))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,170 +0,0 @@
|
|||||||
package shirkneko.zako.sukisu.ui.theme
|
|
||||||
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
|
|
||||||
sealed class ThemeColors {
|
|
||||||
abstract val Primary: Color
|
|
||||||
abstract val Secondary: Color
|
|
||||||
abstract val Tertiary: Color
|
|
||||||
abstract val OnPrimary: Color
|
|
||||||
abstract val OnSecondary: Color
|
|
||||||
abstract val OnTertiary: Color
|
|
||||||
abstract val PrimaryContainer: Color
|
|
||||||
abstract val SecondaryContainer: Color
|
|
||||||
abstract val TertiaryContainer: Color
|
|
||||||
abstract val OnPrimaryContainer: Color
|
|
||||||
abstract val OnSecondaryContainer: Color
|
|
||||||
abstract val OnTertiaryContainer: Color
|
|
||||||
abstract val ButtonContrast: Color
|
|
||||||
|
|
||||||
open fun getCustomSliderActiveColor(): Color = Primary
|
|
||||||
open fun getCustomSliderInactiveColor(): Color = PrimaryContainer
|
|
||||||
|
|
||||||
// Default Theme (white)
|
|
||||||
object Default : ThemeColors() {
|
|
||||||
override val Primary = Color(0xFFFFFFFF)
|
|
||||||
override val Secondary = Color(0xFFF5F5F5)
|
|
||||||
override val Tertiary = Color(0xFFE0E0E0)
|
|
||||||
override val OnPrimary = Color(0xFF616161)
|
|
||||||
override val OnSecondary = Color(0xFF616161)
|
|
||||||
override val OnTertiary = Color(0xFF616161)
|
|
||||||
override val PrimaryContainer = Color(0xFFF5F5F5)
|
|
||||||
override val SecondaryContainer = Color(0xFFEEEEEE)
|
|
||||||
override val TertiaryContainer = Color(0xFFE0E0E0)
|
|
||||||
override val OnPrimaryContainer = Color(0xFF000000)
|
|
||||||
override val OnSecondaryContainer = Color(0xFF000000)
|
|
||||||
override val OnTertiaryContainer = Color(0xFF000000)
|
|
||||||
override val ButtonContrast = Color(0xFF00BFFF)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Blue Theme
|
|
||||||
object Blue : ThemeColors() {
|
|
||||||
override val Primary = Color(0xFF2196F3)
|
|
||||||
override val Secondary = Color(0xFF1E88E5)
|
|
||||||
override val Tertiary = Color(0xFF0D47A1)
|
|
||||||
override val OnPrimary = Color(0xFFFFFFFF)
|
|
||||||
override val OnSecondary = Color(0xFFFFFFFF)
|
|
||||||
override val OnTertiary = Color(0xFFFFFFFF)
|
|
||||||
override val PrimaryContainer = Color(0xFFCBE6FC)
|
|
||||||
override val SecondaryContainer = Color(0xFFBBDEFB)
|
|
||||||
override val TertiaryContainer = Color(0xFF90CAF9)
|
|
||||||
override val OnPrimaryContainer = Color(0xFF0A1A2E)
|
|
||||||
override val OnSecondaryContainer = Color(0xFF0A192D)
|
|
||||||
override val OnTertiaryContainer = Color(0xFF071B3D)
|
|
||||||
override val ButtonContrast = Color(0xFF00BFFF)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Green Theme
|
|
||||||
object Green : ThemeColors() {
|
|
||||||
override val Primary = Color(0xFF4CAF50)
|
|
||||||
override val Secondary = Color(0xFF43A047)
|
|
||||||
override val Tertiary = Color(0xFF1B5E20)
|
|
||||||
override val OnPrimary = Color(0xFFFFFFFF)
|
|
||||||
override val OnSecondary = Color(0xFFFFFFFF)
|
|
||||||
override val OnTertiary = Color(0xFFFFFFFF)
|
|
||||||
override val PrimaryContainer = Color(0xFFC8E6C9)
|
|
||||||
override val SecondaryContainer = Color(0xFFA5D6A7)
|
|
||||||
override val TertiaryContainer = Color(0xFF81C784)
|
|
||||||
override val OnPrimaryContainer = Color(0xFF0A1F0B)
|
|
||||||
override val OnSecondaryContainer = Color(0xFF0A1D0B)
|
|
||||||
override val OnTertiaryContainer = Color(0xFF071F09)
|
|
||||||
override val ButtonContrast = Color(0xFF32CD32)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Purple Theme
|
|
||||||
object Purple : ThemeColors() {
|
|
||||||
override val Primary = Color(0xFF9C27B0)
|
|
||||||
override val Secondary = Color(0xFF8E24AA)
|
|
||||||
override val Tertiary = Color(0xFF4A148C)
|
|
||||||
override val OnPrimary = Color(0xFFFFFFFF)
|
|
||||||
override val OnSecondary = Color(0xFFFFFFFF)
|
|
||||||
override val OnTertiary = Color(0xFFFFFFFF)
|
|
||||||
override val PrimaryContainer = Color(0xFFE1BEE7)
|
|
||||||
override val SecondaryContainer = Color(0xFFCE93D8)
|
|
||||||
override val TertiaryContainer = Color(0xFFB39DDB)
|
|
||||||
override val OnPrimaryContainer = Color(0xFF1F0A23)
|
|
||||||
override val OnSecondaryContainer = Color(0xFF1C0A21)
|
|
||||||
override val OnTertiaryContainer = Color(0xFF12071C)
|
|
||||||
override val ButtonContrast = Color(0xFFDA70D6)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Orange Theme
|
|
||||||
object Orange : ThemeColors() {
|
|
||||||
override val Primary = Color(0xFFFF9800)
|
|
||||||
override val Secondary = Color(0xFFFB8C00)
|
|
||||||
override val Tertiary = Color(0xFFE65100)
|
|
||||||
override val OnPrimary = Color(0xFFFFFFFF)
|
|
||||||
override val OnSecondary = Color(0xFFFFFFFF)
|
|
||||||
override val OnTertiary = Color(0xFFFFFFFF)
|
|
||||||
override val PrimaryContainer = Color(0xFFFFE0B2)
|
|
||||||
override val SecondaryContainer = Color(0xFFFFCC80)
|
|
||||||
override val TertiaryContainer = Color(0xFFFFB74D)
|
|
||||||
override val OnPrimaryContainer = Color(0xFF1A1100)
|
|
||||||
override val OnSecondaryContainer = Color(0xFF1A1000)
|
|
||||||
override val OnTertiaryContainer = Color(0xFF1A0B00)
|
|
||||||
override val ButtonContrast = Color(0xFFFF6347)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pink Theme
|
|
||||||
object Pink : ThemeColors() {
|
|
||||||
override val Primary = Color(0xFFE91E63)
|
|
||||||
override val Secondary = Color(0xFFD81B60)
|
|
||||||
override val Tertiary = Color(0xFF880E4F)
|
|
||||||
override val OnPrimary = Color(0xFFFFFFFF)
|
|
||||||
override val OnSecondary = Color(0xFFFFFFFF)
|
|
||||||
override val OnTertiary = Color(0xFFFFFFFF)
|
|
||||||
override val PrimaryContainer = Color(0xFFF8BBD0)
|
|
||||||
override val SecondaryContainer = Color(0xFFF48FB1)
|
|
||||||
override val TertiaryContainer = Color(0xFFE91E63)
|
|
||||||
override val OnPrimaryContainer = Color(0xFF2E0A14)
|
|
||||||
override val OnSecondaryContainer = Color(0xFF2B0A13)
|
|
||||||
override val OnTertiaryContainer = Color(0xFF1C0311)
|
|
||||||
override val ButtonContrast = Color(0xFFFF1493)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Gray Theme
|
|
||||||
object Gray : ThemeColors() {
|
|
||||||
override val Primary = Color(0xFF9E9E9E)
|
|
||||||
override val Secondary = Color(0xFF757575)
|
|
||||||
override val Tertiary = Color(0xFF616161)
|
|
||||||
override val OnPrimary = Color(0xFFFFFFFF)
|
|
||||||
override val OnSecondary = Color(0xFFFFFFFF)
|
|
||||||
override val OnTertiary = Color(0xFFFFFFFF)
|
|
||||||
override val PrimaryContainer = Color(0xFFEEEEEE)
|
|
||||||
override val SecondaryContainer = Color(0xFFE0E0E0)
|
|
||||||
override val TertiaryContainer = Color(0xFFBDBDBD)
|
|
||||||
override val OnPrimaryContainer = Color(0xFF1A1A1A)
|
|
||||||
override val OnSecondaryContainer = Color(0xFF171717)
|
|
||||||
override val OnTertiaryContainer = Color(0xFF141414)
|
|
||||||
override val ButtonContrast = Color(0xFF696969)
|
|
||||||
}
|
|
||||||
|
|
||||||
object Yellow : ThemeColors() {
|
|
||||||
override val Primary = Color(0xFFFFD700)
|
|
||||||
override val Secondary = Color(0xFFFFBC52)
|
|
||||||
override val Tertiary = Color(0xFF795548)
|
|
||||||
override val OnPrimary = Color(0xFFFFFFFF)
|
|
||||||
override val OnSecondary = Color(0xFFFFFFFF)
|
|
||||||
override val OnTertiary = Color(0xFFFFFFFF)
|
|
||||||
override val PrimaryContainer = Color(0xFFFFF7D6)
|
|
||||||
override val SecondaryContainer = Color(0xFFFFE6B3)
|
|
||||||
override val TertiaryContainer = Color(0xFFD7CCC8)
|
|
||||||
override val OnPrimaryContainer = Color(0xFF1A1600)
|
|
||||||
override val OnSecondaryContainer = Color(0xFF1A1100)
|
|
||||||
override val OnTertiaryContainer = Color(0xFF1A1717)
|
|
||||||
override val ButtonContrast = Color(0xFFFFD700)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun fromName(name: String): ThemeColors = when (name.lowercase()) {
|
|
||||||
"blue" -> Blue
|
|
||||||
"green" -> Green
|
|
||||||
"purple" -> Purple
|
|
||||||
"orange" -> Orange
|
|
||||||
"pink" -> Pink
|
|
||||||
"gray" -> Gray
|
|
||||||
"white" -> Yellow
|
|
||||||
else -> Default
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,342 +0,0 @@
|
|||||||
package shirkneko.zako.sukisu.ui.theme
|
|
||||||
|
|
||||||
import android.content.ContentResolver
|
|
||||||
import android.content.Context
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Build
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.darkColorScheme
|
|
||||||
import androidx.compose.material3.dynamicDarkColorScheme
|
|
||||||
import androidx.compose.material3.dynamicLightColorScheme
|
|
||||||
import androidx.compose.material3.lightColorScheme
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.draw.paint
|
|
||||||
import androidx.compose.ui.graphics.Brush
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.layout.ContentScale
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.zIndex
|
|
||||||
import coil.compose.rememberAsyncImagePainter
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.ui.graphics.luminance
|
|
||||||
import java.io.File
|
|
||||||
import java.io.FileOutputStream
|
|
||||||
import java.io.InputStream
|
|
||||||
import androidx.core.content.edit
|
|
||||||
import androidx.core.net.toUri
|
|
||||||
|
|
||||||
object ThemeConfig {
|
|
||||||
var customBackgroundUri by mutableStateOf<Uri?>(null)
|
|
||||||
var forceDarkMode by mutableStateOf<Boolean?>(null)
|
|
||||||
var currentTheme by mutableStateOf<ThemeColors>(ThemeColors.Default)
|
|
||||||
var useDynamicColor by mutableStateOf(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun getDarkColorScheme() = darkColorScheme(
|
|
||||||
primary = ThemeConfig.currentTheme.Primary.copy(alpha = 0.8f),
|
|
||||||
onPrimary = Color.White,
|
|
||||||
primaryContainer = ThemeConfig.currentTheme.PrimaryContainer.copy(alpha = 0.15f),
|
|
||||||
onPrimaryContainer = Color.White,
|
|
||||||
secondary = ThemeConfig.currentTheme.Secondary.copy(alpha = 0.8f),
|
|
||||||
onSecondary = Color.White,
|
|
||||||
secondaryContainer = ThemeConfig.currentTheme.SecondaryContainer.copy(alpha = 0.15f),
|
|
||||||
onSecondaryContainer = Color.White,
|
|
||||||
tertiary = ThemeConfig.currentTheme.Tertiary.copy(alpha = 0.8f),
|
|
||||||
onTertiary = Color.White,
|
|
||||||
tertiaryContainer = ThemeConfig.currentTheme.TertiaryContainer.copy(alpha = 0.15f),
|
|
||||||
onTertiaryContainer = Color.White,
|
|
||||||
background = Color.Transparent,
|
|
||||||
surface = Color.Transparent,
|
|
||||||
onBackground = Color.White.copy(alpha = 0.87f),
|
|
||||||
onSurface = Color.White.copy(alpha = 0.87f),
|
|
||||||
surfaceVariant = Color(0xFF2F2F2F),
|
|
||||||
onSurfaceVariant = Color.White.copy(alpha = 0.78f),
|
|
||||||
outline = Color.White.copy(alpha = 0.12f),
|
|
||||||
outlineVariant = Color.White.copy(alpha = 0.12f)
|
|
||||||
)
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun getLightColorScheme() = lightColorScheme(
|
|
||||||
primary = ThemeConfig.currentTheme.Primary,
|
|
||||||
onPrimary = ThemeConfig.currentTheme.OnPrimary,
|
|
||||||
primaryContainer = ThemeConfig.currentTheme.PrimaryContainer,
|
|
||||||
onPrimaryContainer = ThemeConfig.currentTheme.OnPrimaryContainer,
|
|
||||||
secondary = ThemeConfig.currentTheme.Secondary,
|
|
||||||
onSecondary = ThemeConfig.currentTheme.OnSecondary,
|
|
||||||
secondaryContainer = ThemeConfig.currentTheme.SecondaryContainer,
|
|
||||||
onSecondaryContainer = ThemeConfig.currentTheme.OnSecondaryContainer,
|
|
||||||
tertiary = ThemeConfig.currentTheme.Tertiary,
|
|
||||||
onTertiary = ThemeConfig.currentTheme.OnTertiary,
|
|
||||||
tertiaryContainer = ThemeConfig.currentTheme.TertiaryContainer,
|
|
||||||
onTertiaryContainer = ThemeConfig.currentTheme.OnTertiaryContainer,
|
|
||||||
background = Color.Transparent,
|
|
||||||
surface = Color.Transparent,
|
|
||||||
onBackground = Color.Black.copy(alpha = 0.87f),
|
|
||||||
onSurface = Color.Black.copy(alpha = 0.87f),
|
|
||||||
surfaceVariant = Color(0xFFF5F5F5),
|
|
||||||
onSurfaceVariant = Color.Black.copy(alpha = 0.78f),
|
|
||||||
outline = Color.Black.copy(alpha = 0.12f),
|
|
||||||
outlineVariant = Color.Black.copy(alpha = 0.12f)
|
|
||||||
)
|
|
||||||
|
|
||||||
// 复制图片到应用内部存储
|
|
||||||
fun Context.copyImageToInternalStorage(uri: Uri): Uri? {
|
|
||||||
try {
|
|
||||||
val contentResolver: ContentResolver = contentResolver
|
|
||||||
val inputStream: InputStream = contentResolver.openInputStream(uri)!!
|
|
||||||
val fileName = "custom_background.jpg"
|
|
||||||
val file = File(filesDir, fileName)
|
|
||||||
val outputStream = FileOutputStream(file)
|
|
||||||
val buffer = ByteArray(4 * 1024)
|
|
||||||
var read: Int
|
|
||||||
while (inputStream.read(buffer).also { read = it } != -1) {
|
|
||||||
outputStream.write(buffer, 0, read)
|
|
||||||
}
|
|
||||||
outputStream.flush()
|
|
||||||
outputStream.close()
|
|
||||||
inputStream.close()
|
|
||||||
return Uri.fromFile(file)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e("ImageCopy", "Failed to copy image: ${e.message}")
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun KernelSUTheme(
|
|
||||||
darkTheme: Boolean = when(ThemeConfig.forceDarkMode) {
|
|
||||||
true -> true
|
|
||||||
false -> false
|
|
||||||
null -> isSystemInDarkTheme()
|
|
||||||
},
|
|
||||||
dynamicColor: Boolean = ThemeConfig.useDynamicColor,
|
|
||||||
content: @Composable () -> Unit
|
|
||||||
) {
|
|
||||||
val context = LocalContext.current
|
|
||||||
context.loadCustomBackground()
|
|
||||||
context.loadThemeColors()
|
|
||||||
context.loadDynamicColorState()
|
|
||||||
|
|
||||||
val colorScheme = when {
|
|
||||||
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
|
||||||
if (darkTheme) {
|
|
||||||
val originalScheme = dynamicDarkColorScheme(context)
|
|
||||||
originalScheme.copy(
|
|
||||||
// 调整按钮相关颜色
|
|
||||||
primary = adjustColor(originalScheme.primary),
|
|
||||||
onPrimary = adjustColor(originalScheme.onPrimary),
|
|
||||||
primaryContainer = adjustColor(originalScheme.primaryContainer),
|
|
||||||
onPrimaryContainer = adjustColor(originalScheme.onPrimaryContainer),
|
|
||||||
background = Color.Transparent,
|
|
||||||
surface = Color.Transparent,
|
|
||||||
onBackground = Color.White,
|
|
||||||
onSurface = Color.White,
|
|
||||||
onSecondary = Color.White,
|
|
||||||
onTertiary = Color.White,
|
|
||||||
onSecondaryContainer = Color.White,
|
|
||||||
onTertiaryContainer = Color.White
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
val originalScheme = dynamicLightColorScheme(context)
|
|
||||||
originalScheme.copy(
|
|
||||||
primary = adjustColor(originalScheme.primary),
|
|
||||||
onPrimary = adjustColor(originalScheme.onPrimary),
|
|
||||||
primaryContainer = adjustColor(originalScheme.primaryContainer),
|
|
||||||
onPrimaryContainer = adjustColor(originalScheme.onPrimaryContainer),
|
|
||||||
background = Color.Transparent,
|
|
||||||
surface = Color.Transparent
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
darkTheme -> getDarkColorScheme()
|
|
||||||
else -> getLightColorScheme()
|
|
||||||
}
|
|
||||||
|
|
||||||
val isDarkModeWithCustomBackground = darkTheme && ThemeConfig.customBackgroundUri != null
|
|
||||||
|
|
||||||
if (darkTheme && !dynamicColor) {
|
|
||||||
CardConfig.setDarkModeDefaults()
|
|
||||||
}
|
|
||||||
|
|
||||||
CardConfig.updateShadowEnabled(!isDarkModeWithCustomBackground)
|
|
||||||
|
|
||||||
MaterialTheme(
|
|
||||||
colorScheme = colorScheme,
|
|
||||||
typography = Typography
|
|
||||||
) {
|
|
||||||
Box(modifier = Modifier.fillMaxSize()) {
|
|
||||||
// 背景图层
|
|
||||||
ThemeConfig.customBackgroundUri?.let { uri ->
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.zIndex(-1f)
|
|
||||||
) {
|
|
||||||
// 背景图片
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.paint(
|
|
||||||
painter = rememberAsyncImagePainter(
|
|
||||||
model = uri,
|
|
||||||
onError = {
|
|
||||||
ThemeConfig.customBackgroundUri = null
|
|
||||||
context.saveCustomBackground(null)
|
|
||||||
}
|
|
||||||
),
|
|
||||||
contentScale = ContentScale.Crop
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
// 亮度调节层
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.background(
|
|
||||||
if (darkTheme) {
|
|
||||||
Color.Black.copy(alpha = 0.4f)
|
|
||||||
} else {
|
|
||||||
Color.White.copy(alpha = 0.1f)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
// 边缘渐变遮罩层
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.background(
|
|
||||||
Brush.radialGradient(
|
|
||||||
colors = listOf(
|
|
||||||
Color.Transparent,
|
|
||||||
if (darkTheme) {
|
|
||||||
Color.Black.copy(alpha = 0.5f)
|
|
||||||
} else {
|
|
||||||
Color.Black.copy(alpha = 0.2f)
|
|
||||||
}
|
|
||||||
),
|
|
||||||
radius = 1200f
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 内容图层
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.zIndex(1f)
|
|
||||||
) {
|
|
||||||
content()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Context.saveCustomBackground(uri: Uri?) {
|
|
||||||
val newUri = uri?.let { copyImageToInternalStorage(it) }
|
|
||||||
getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
|
||||||
.edit {
|
|
||||||
putString("custom_background", newUri?.toString())
|
|
||||||
}
|
|
||||||
ThemeConfig.customBackgroundUri = newUri
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Context.loadCustomBackground() {
|
|
||||||
val uriString = getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
|
||||||
.getString("custom_background", null)
|
|
||||||
ThemeConfig.customBackgroundUri = uriString?.toUri()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Context.saveThemeMode(forceDark: Boolean?) {
|
|
||||||
getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
|
||||||
.edit {
|
|
||||||
putString(
|
|
||||||
"theme_mode", when (forceDark) {
|
|
||||||
true -> "dark"
|
|
||||||
false -> "light"
|
|
||||||
null -> "system"
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
ThemeConfig.forceDarkMode = forceDark
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Context.loadThemeMode() {
|
|
||||||
val mode = getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
|
||||||
.getString("theme_mode", "system")
|
|
||||||
ThemeConfig.forceDarkMode = when(mode) {
|
|
||||||
"dark" -> true
|
|
||||||
"light" -> false
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Context.saveThemeColors(themeName: String) {
|
|
||||||
getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
|
||||||
.edit {
|
|
||||||
putString("theme_colors", themeName)
|
|
||||||
}
|
|
||||||
|
|
||||||
ThemeConfig.currentTheme = when(themeName) {
|
|
||||||
"blue" -> ThemeColors.Blue
|
|
||||||
"green" -> ThemeColors.Green
|
|
||||||
"purple" -> ThemeColors.Purple
|
|
||||||
"orange" -> ThemeColors.Orange
|
|
||||||
"pink" -> ThemeColors.Pink
|
|
||||||
"gray" -> ThemeColors.Gray
|
|
||||||
"yellow" -> ThemeColors.Yellow
|
|
||||||
else -> ThemeColors.Default
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Context.loadThemeColors() {
|
|
||||||
val themeName = getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
|
||||||
.getString("theme_colors", "default")
|
|
||||||
|
|
||||||
ThemeConfig.currentTheme = when(themeName) {
|
|
||||||
"blue" -> ThemeColors.Blue
|
|
||||||
"green" -> ThemeColors.Green
|
|
||||||
"purple" -> ThemeColors.Purple
|
|
||||||
"orange" -> ThemeColors.Orange
|
|
||||||
"pink" -> ThemeColors.Pink
|
|
||||||
"gray" -> ThemeColors.Gray
|
|
||||||
"yellow" -> ThemeColors.Yellow
|
|
||||||
else -> ThemeColors.Default
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Context.saveDynamicColorState(enabled: Boolean) {
|
|
||||||
getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
|
||||||
.edit {
|
|
||||||
putBoolean("use_dynamic_color", enabled)
|
|
||||||
}
|
|
||||||
ThemeConfig.useDynamicColor = enabled
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Context.loadDynamicColorState() {
|
|
||||||
val enabled = getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
|
||||||
.getBoolean("use_dynamic_color", true)
|
|
||||||
ThemeConfig.useDynamicColor = enabled
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun adjustColor(color: Color): Color {
|
|
||||||
val minLuminance = 0.75f
|
|
||||||
val maxLuminance = 1f
|
|
||||||
var luminance = color.luminance()
|
|
||||||
if (luminance < minLuminance) {
|
|
||||||
luminance = minLuminance
|
|
||||||
} else if (luminance > maxLuminance) {
|
|
||||||
luminance = maxLuminance
|
|
||||||
}
|
|
||||||
return color.copy(luminance)
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
package shirkneko.zako.sukisu.ui.theme
|
|
||||||
|
|
||||||
import androidx.compose.ui.text.TextStyle
|
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.unit.sp
|
|
||||||
|
|
||||||
// Set of Material typography styles to start with
|
|
||||||
val Typography = androidx.compose.material3.Typography(
|
|
||||||
bodyLarge = TextStyle(
|
|
||||||
fontFamily = FontFamily.Default,
|
|
||||||
fontWeight = FontWeight.Normal,
|
|
||||||
fontSize = 16.sp,
|
|
||||||
lineHeight = 24.sp,
|
|
||||||
letterSpacing = 0.5.sp
|
|
||||||
)
|
|
||||||
/* Other default text styles to override
|
|
||||||
titleLarge = TextStyle(
|
|
||||||
fontFamily = FontFamily.Default,
|
|
||||||
fontWeight = FontWeight.Normal,
|
|
||||||
fontSize = 22.sp,
|
|
||||||
lineHeight = 28.sp,
|
|
||||||
letterSpacing = 0.sp
|
|
||||||
),
|
|
||||||
labelSmall = TextStyle(
|
|
||||||
fontFamily = FontFamily.Default,
|
|
||||||
fontWeight = FontWeight.Medium,
|
|
||||||
fontSize = 11.sp,
|
|
||||||
lineHeight = 16.sp,
|
|
||||||
letterSpacing = 0.5.sp
|
|
||||||
)
|
|
||||||
*/
|
|
||||||
)
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
package shirkneko.zako.sukisu.ui.viewmodel
|
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import shirkneko.zako.sukisu.ui.util.*
|
|
||||||
|
|
||||||
class KpmViewModel : ViewModel() {
|
|
||||||
var moduleList by mutableStateOf(emptyList<ModuleInfo>())
|
|
||||||
private set
|
|
||||||
|
|
||||||
|
|
||||||
var search by mutableStateOf("")
|
|
||||||
internal set
|
|
||||||
|
|
||||||
var isRefreshing by mutableStateOf(false)
|
|
||||||
private set
|
|
||||||
|
|
||||||
var currentModuleDetail by mutableStateOf("")
|
|
||||||
private set
|
|
||||||
|
|
||||||
fun loadModuleDetail(moduleId: String) {
|
|
||||||
viewModelScope.launch {
|
|
||||||
currentModuleDetail = withContext(Dispatchers.IO) {
|
|
||||||
try {
|
|
||||||
getKpmModuleInfo(moduleId)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
"无法获取模块详细信息: ${e.message}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Log.d("KsuCli", "Module detail: $currentModuleDetail")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun fetchModuleList() {
|
|
||||||
viewModelScope.launch {
|
|
||||||
isRefreshing = true
|
|
||||||
try {
|
|
||||||
val moduleCount = getKpmModuleCount()
|
|
||||||
Log.d("KsuCli", "Module count: $moduleCount")
|
|
||||||
|
|
||||||
val moduleInfo = listKpmModules()
|
|
||||||
Log.d("KsuCli", "Module info: $moduleInfo")
|
|
||||||
|
|
||||||
val modules = parseModuleList(moduleInfo)
|
|
||||||
moduleList = modules
|
|
||||||
} finally {
|
|
||||||
isRefreshing = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getInstalledKernelPatches(): List<ModuleInfo> {
|
|
||||||
return try {
|
|
||||||
val output = printKpmModules()
|
|
||||||
parseModuleList(output)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
emptyList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseModuleList(output: String): List<ModuleInfo> {
|
|
||||||
return output.split("\n").mapNotNull { line ->
|
|
||||||
if (line.isBlank()) return@mapNotNull null
|
|
||||||
val parts = line.split("|")
|
|
||||||
if (parts.size < 7) return@mapNotNull null
|
|
||||||
|
|
||||||
ModuleInfo(
|
|
||||||
id = parts[0].trim(),
|
|
||||||
name = parts[1].trim(),
|
|
||||||
version = parts[2].trim(),
|
|
||||||
author = parts[3].trim(),
|
|
||||||
description = parts[4].trim(),
|
|
||||||
args = parts[6].trim(),
|
|
||||||
enabled = true,
|
|
||||||
hasAction = controlKpmModule(parts[0].trim()).isNotBlank()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
data class ModuleInfo(
|
|
||||||
val id: String,
|
|
||||||
val name: String,
|
|
||||||
val version: String,
|
|
||||||
val author: String,
|
|
||||||
val description: String,
|
|
||||||
val args: String,
|
|
||||||
val enabled: Boolean,
|
|
||||||
val hasAction: Boolean
|
|
||||||
)
|
|
||||||
}
|
|
||||||
6
manager/app/src/main/jniLibs/.gitignore
vendored
6
manager/app/src/main/jniLibs/.gitignore
vendored
@@ -1,3 +1,5 @@
|
|||||||
libzakomk.so
|
libzakozako.so
|
||||||
libzakomksd.so
|
libzakozakozako.so
|
||||||
libkpmmgr.so
|
libkpmmgr.so
|
||||||
|
libzako.so
|
||||||
|
libandroidx.graphics.path.so
|
||||||
@@ -50,7 +50,6 @@
|
|||||||
<string name="home_click_to_learn_kernelsu">یاد بگیرید چگونه از کرنل اس یو و ماژول ها استفاده کنید</string>
|
<string name="home_click_to_learn_kernelsu">یاد بگیرید چگونه از کرنل اس یو و ماژول ها استفاده کنید</string>
|
||||||
<string name="home_support_title">از ما حمایت کنید</string>
|
<string name="home_support_title">از ما حمایت کنید</string>
|
||||||
<string name="home_support_content">KernelSU رایگان است و همیشه خواهد بود و منبع باز است. با این حال، می توانید با اهدای کمک مالی به ما نشان دهید که برایتان مهم است.</string>
|
<string name="home_support_content">KernelSU رایگان است و همیشه خواهد بود و منبع باز است. با این حال، می توانید با اهدای کمک مالی به ما نشان دهید که برایتان مهم است.</string>
|
||||||
<string name="profile">پروفایل برنامه</string>
|
|
||||||
<string name="profile_default">پیشفرض</string>
|
<string name="profile_default">پیشفرض</string>
|
||||||
<string name="profile_template">قالب</string>
|
<string name="profile_template">قالب</string>
|
||||||
<string name="profile_custom">شخصی سازی شده</string>
|
<string name="profile_custom">شخصی سازی شده</string>
|
||||||
|
|||||||
@@ -50,7 +50,6 @@
|
|||||||
<string name="home_click_to_learn_kernelsu">Pelajari cara instal KernelSU dan menggunakan modul</string>
|
<string name="home_click_to_learn_kernelsu">Pelajari cara instal KernelSU dan menggunakan modul</string>
|
||||||
<string name="home_support_title">Dukung Kami</string>
|
<string name="home_support_title">Dukung Kami</string>
|
||||||
<string name="home_support_content">KernelSU akan selalu menjadi aplikasi gratis dan terbuka. Anda dapat memberikan donasi sebagai bentuk dukungan.</string>
|
<string name="home_support_content">KernelSU akan selalu menjadi aplikasi gratis dan terbuka. Anda dapat memberikan donasi sebagai bentuk dukungan.</string>
|
||||||
<string name="profile">Profil Apl</string>
|
|
||||||
<string name="profile_default">Bawaan</string>
|
<string name="profile_default">Bawaan</string>
|
||||||
<string name="profile_template">Templat</string>
|
<string name="profile_template">Templat</string>
|
||||||
<string name="profile_custom">Khusus</string>
|
<string name="profile_custom">Khusus</string>
|
||||||
|
|||||||
@@ -8,50 +8,56 @@
|
|||||||
<string name="home_superuser_count">スーパーユーザー: %d</string>
|
<string name="home_superuser_count">スーパーユーザー: %d</string>
|
||||||
<string name="home_module_count">モジュール: %d</string>
|
<string name="home_module_count">モジュール: %d</string>
|
||||||
<string name="home_unsupported">非対応</string>
|
<string name="home_unsupported">非対応</string>
|
||||||
<string name="home_unsupported_reason">現在、 KernelSU は GKI カーネルにのみ対応しています</string>
|
<string name="home_unsupported_reason">カーネルの KernelSU ドライバが未検出です。カーネルが間違ってませんか?</string>
|
||||||
<string name="home_kernel">カーネル</string>
|
<string name="home_kernel">カーネルのバージョン</string>
|
||||||
<string name="home_manager_version">アプリのバージョン</string>
|
<string name="home_susfs">SuSFS: %s</string>
|
||||||
|
<string name="home_susfs_version">SuSFS のバージョン</string>
|
||||||
|
<string name="home_susfs_sus_su">SuS SU</string>
|
||||||
|
<string name="home_manager_version">マネージャーのバージョン</string>
|
||||||
<string name="home_fingerprint">Fingerprint</string>
|
<string name="home_fingerprint">Fingerprint</string>
|
||||||
<string name="home_selinux_status">SELinux の状態</string>
|
<string name="home_selinux_status">SELinux のステータス</string>
|
||||||
<string name="selinux_status_disabled">Disabled</string>
|
<string name="selinux_status_disabled">無効</string>
|
||||||
<string name="selinux_status_enforcing">Enforcing</string>
|
<string name="selinux_status_enforcing">Enforcing</string>
|
||||||
<string name="selinux_status_permissive">Permissive</string>
|
<string name="selinux_status_permissive">Permissive</string>
|
||||||
<string name="selinux_status_unknown">不明</string>
|
<string name="selinux_status_unknown">不明</string>
|
||||||
<string name="superuser">スーパーユーザー</string>
|
<string name="superuser">スーパーユーザー</string>
|
||||||
<string name="module_failed_to_enable">%s モジュールをオンにできませんでした</string>
|
<string name="module_failed_to_enable">%s モジュールを ON にできませんでした</string>
|
||||||
<string name="module_failed_to_disable">%s モジュールをオフにできませんでした</string>
|
<string name="module_failed_to_disable">%s モジュールを OFF にできませんでした</string>
|
||||||
<string name="module_empty">モジュールがインストールされていません</string>
|
<string name="module_empty">モジュールがインストールされていません</string>
|
||||||
<string name="module">モジュール</string>
|
<string name="module">モジュール</string>
|
||||||
|
<string name="module_sort_action_first">並べ替え (アクション優先)</string>
|
||||||
|
<string name="module_sort_enabled_first">並べ替え (最初に有効)</string>
|
||||||
<string name="uninstall">アンインストール</string>
|
<string name="uninstall">アンインストール</string>
|
||||||
|
<string name="restore">復元</string>
|
||||||
<string name="module_install">インストール</string>
|
<string name="module_install">インストール</string>
|
||||||
<string name="install">インストール</string>
|
<string name="install">インストール</string>
|
||||||
<string name="reboot">再起動</string>
|
<string name="reboot">再起動</string>
|
||||||
<string name="settings">設定</string>
|
<string name="settings">設定</string>
|
||||||
<string name="reboot_userspace">通常の再起動</string>
|
<string name="reboot_userspace">ソフトリブート</string>
|
||||||
<string name="reboot_recovery">リカバリーへ再起動</string>
|
<string name="reboot_recovery">リカバリーで再起動</string>
|
||||||
<string name="reboot_bootloader">ブートローダー へ再起動</string>
|
<string name="reboot_bootloader">ブートローダーで再起動</string>
|
||||||
<string name="reboot_download">ダウンロードモードへ再起動</string>
|
<string name="reboot_download">ダウンロードモードで再起動</string>
|
||||||
<string name="reboot_edl">EDL へ再起動</string>
|
<string name="reboot_edl">EDL で再起動</string>
|
||||||
<string name="about">アプリについて</string>
|
<string name="about">アプリについて</string>
|
||||||
<string name="module_uninstall_confirm">モジュール %s をアンインストールしますか?</string>
|
<string name="module_uninstall_confirm">モジュール %s をアンインストールしますか?</string>
|
||||||
<string name="module_uninstall_success">%s はアンインストールされました</string>
|
<string name="module_uninstall_success">%s はアンインストールされました</string>
|
||||||
<string name="module_uninstall_failed">%s をアンインストールできませんでした</string>
|
<string name="module_uninstall_failed">%s をアンインストールできませんでした</string>
|
||||||
<string name="module_version">バージョン</string>
|
<string name="module_version">バージョン</string>
|
||||||
<string name="module_author">制作者</string>
|
<string name="module_author">作者</string>
|
||||||
<string name="refresh">更新</string>
|
<string name="refresh">更新</string>
|
||||||
<string name="show_system_apps">システムアプリを表示</string>
|
<string name="show_system_apps">システムアプリを表示</string>
|
||||||
<string name="hide_system_apps">システムアプリを非表示</string>
|
<string name="hide_system_apps">システムアプリを非表示</string>
|
||||||
<string name="send_log">ログを送信</string>
|
<string name="send_log">ログを送信する</string>
|
||||||
<string name="safe_mode">セーフモード</string>
|
<string name="safe_mode">セーフモード</string>
|
||||||
<string name="reboot_to_apply">再起動すると有効化されます</string>
|
<string name="reboot_to_apply">再起動すると有効化されます</string>
|
||||||
<string name="module_magisk_conflict">モジュールが Magisk との競合により利用できません!</string>
|
<string name="module_magisk_conflict">モジュールが Magisk との競合により利用できません!</string>
|
||||||
<string name="home_learn_kernelsu">KernelSU について</string>
|
<string name="home_learn_kernelsu">KernelSU について学ぶ</string>
|
||||||
<string name="home_learn_kernelsu_url">https://kernelsu.org/ja_JP/guide/what-is-kernelsu.html</string>
|
<string name="home_learn_kernelsu_url">https://kernelsu.org/ja_JP/guide/what-is-kernelsu.html</string>
|
||||||
<string name="home_click_to_learn_kernelsu">KernelSU のインストール方法やモジュールの使い方はこちら</string>
|
<string name="home_click_to_learn_kernelsu">KernelSU のインストール方法やモジュールの使い方を学習できます。</string>
|
||||||
<string name="home_support_title">支援する</string>
|
<string name="home_support_title">支援する</string>
|
||||||
<string name="home_support_content">KernelSU はこれからもずっと無料でオープンソースです。寄付をして頂くことで、開発を支援していただけます。</string>
|
<string name="home_support_content">KernelSU は今後も無料でオープンソースです。ですが、寄付をして頂けると開発者への貢献になります。</string>
|
||||||
<string name="profile">アプリのプロファイル</string>
|
<string name="about_source_code"><![CDATA[ソースコードは %1$s で確認できます。<br/>%2$s チャンネルにご参加ください。]]></string>
|
||||||
<string name="profile_default">既定</string>
|
<string name="profile_default">デフォルト</string>
|
||||||
<string name="profile_template">テンプレート</string>
|
<string name="profile_template">テンプレート</string>
|
||||||
<string name="profile_custom">カスタム</string>
|
<string name="profile_custom">カスタム</string>
|
||||||
<string name="profile_name">プロファイル名</string>
|
<string name="profile_name">プロファイル名</string>
|
||||||
@@ -59,76 +65,264 @@
|
|||||||
<string name="profile_namespace_inherited">継承</string>
|
<string name="profile_namespace_inherited">継承</string>
|
||||||
<string name="profile_namespace_global">共通</string>
|
<string name="profile_namespace_global">共通</string>
|
||||||
<string name="profile_namespace_individual">分離</string>
|
<string name="profile_namespace_individual">分離</string>
|
||||||
<string name="profile_umount_modules">モジュールのアンマウント</string>
|
|
||||||
<string name="profile_groups">グループ</string>
|
<string name="profile_groups">グループ</string>
|
||||||
|
<string name="profile_capabilities">ケーパビリティ</string>
|
||||||
<string name="profile_selinux_context">SELinux コンテキスト</string>
|
<string name="profile_selinux_context">SELinux コンテキスト</string>
|
||||||
|
<string name="profile_umount_modules">モジュールのアンマウント</string>
|
||||||
<string name="failed_to_update_app_profile">%s のアプリのプロファイルの更新をできませでした</string>
|
<string name="failed_to_update_app_profile">%s のアプリのプロファイルの更新をできませでした</string>
|
||||||
|
<string name="require_kernel_version" formatted="false">現在の KernelSU のバージョン %d は低すぎるため、マネージャーは正常に動作しません。バージョン %d 以上に更新してください!</string>
|
||||||
|
<string name="settings_umount_modules_default">デフォルトでモジュールのマウントを解除する</string>
|
||||||
|
<string name="settings_umount_modules_default_summary">アプリプロファイルの「モジュールのアンマウント」の共通となるデフォルト値です。 有効にすると、プロファイルセットを持たないアプリのシステムに対するすべてのモジュールの変更が削除されます。</string>
|
||||||
|
<string name="settings_susfs_toggle">kprobe フックを無効化</string>
|
||||||
|
<string name="profile_umount_modules_summary">このオプションを有効にすると、KernelSU はこのアプリのモジュールによって変更されたファイルを復元できるようになります。</string>
|
||||||
<string name="profile_selinux_domain">ドメイン</string>
|
<string name="profile_selinux_domain">ドメイン</string>
|
||||||
<string name="profile_selinux_rules">ルール</string>
|
<string name="profile_selinux_rules">ルール</string>
|
||||||
<string name="new_version_available">新しいバージョン %s が利用可能です。タップしてダウンロード。</string>
|
<string name="module_update">更新</string>
|
||||||
<string name="module_update">アップデート</string>
|
<string name="module_downloading">モジュールをダウンロード中: %s</string>
|
||||||
<string name="module_start_downloading">ダウンロードを開始: %s</string>
|
<string name="module_start_downloading">ダウンロードを開始: %s</string>
|
||||||
|
<string name="new_version_available">新しいバージョン %s が利用可能です。タップしてダウンロード。</string>
|
||||||
<string name="launch_app">起動</string>
|
<string name="launch_app">起動</string>
|
||||||
<string name="force_stop_app" formatted="false">強制停止</string>
|
<string name="force_stop_app" formatted="false">強制停止</string>
|
||||||
<string name="restart_app">再起動</string>
|
<string name="restart_app">再起動</string>
|
||||||
<string name="failed_to_update_sepolicy">SELinux ルールの更新に失敗しました %s</string>
|
<string name="failed_to_update_sepolicy">SELinux ルールの更新に失敗しました %s</string>
|
||||||
<string name="profile_capabilities">ケーパビリティ</string>
|
|
||||||
<string name="module_downloading">モジュールをダウンロード中: %s</string>
|
|
||||||
<string name="profile_umount_modules_summary">このオプションを有効にすると、KernelSU はこのアプリのモジュールによって変更されたファイルを復元できるようになります。</string>
|
|
||||||
<string name="settings_umount_modules_default">既定でモジュールのマウントを解除</string>
|
|
||||||
<string name="settings_umount_modules_default_summary">アプリプロファイルの「モジュールのアンマウント」の共通のデフォルト値です。 有効にすると、プロファイルセットを持たないアプリのシステムに対するすべてのモジュールの変更が削除されます。</string>
|
|
||||||
<string name="module_changelog">変更履歴</string>
|
<string name="module_changelog">変更履歴</string>
|
||||||
<string name="app_profile_template_import_success">インポート成功</string>
|
<string name="settings_profile_template">アプリプロファイルのテンプレート</string>
|
||||||
<string name="app_profile_export_to_clipboard">クリップボードからエクスポート</string>
|
<string name="settings_profile_template_summary">アプリプロファイルのローカルおよびオンラインテンプレートを管理します</string>
|
||||||
<string name="app_profile_template_export_empty">エクスポートするローカル テンプレートが見つかりません!</string>
|
|
||||||
<string name="app_profile_template_id_exist">テンプレート ID はすでに存在します!</string>
|
|
||||||
<string name="app_profile_import_from_clipboard">クリップボードからインポート</string>
|
|
||||||
<string name="module_changelog_failed">変更ログの取得に失敗しました: %s</string>
|
|
||||||
<string name="app_profile_template_name">名前</string>
|
|
||||||
<string name="app_profile_template_id_invalid">無効なテンプレート ID</string>
|
|
||||||
<string name="app_profile_template_sync">オンラインテンプレートの同期</string>
|
|
||||||
<string name="app_profile_template_create">テンプレートの作成</string>
|
<string name="app_profile_template_create">テンプレートの作成</string>
|
||||||
<string name="app_profile_template_readonly">読み取り専用</string>
|
|
||||||
<string name="app_profile_import_export">インポート/エクスポート</string>
|
|
||||||
<string name="app_profile_template_save_failed">テンプレートの保存に失敗しました</string>
|
|
||||||
<string name="app_profile_template_edit">テンプレートの編集</string>
|
<string name="app_profile_template_edit">テンプレートの編集</string>
|
||||||
<string name="app_profile_template_id">ID</string>
|
<string name="app_profile_template_id">ID</string>
|
||||||
<string name="settings_profile_template">アプリプロファイルのテンプレート</string>
|
<string name="app_profile_template_id_invalid">無効なテンプレート ID</string>
|
||||||
|
<string name="app_profile_template_name">名前</string>
|
||||||
<string name="app_profile_template_description">説明</string>
|
<string name="app_profile_template_description">説明</string>
|
||||||
<string name="app_profile_template_save">保存</string>
|
<string name="app_profile_template_save">保存</string>
|
||||||
<string name="settings_profile_template_summary">アプリプロファイルのローカルおよびオンラインテンプレートを管理する</string>
|
|
||||||
<string name="app_profile_template_delete">消去</string>
|
<string name="app_profile_template_delete">消去</string>
|
||||||
<string name="app_profile_template_import_empty">クリップボードが空です!</string>
|
|
||||||
<string name="app_profile_template_view">テンプレートを表示</string>
|
<string name="app_profile_template_view">テンプレートを表示</string>
|
||||||
<string name="settings_check_update">アップデートを確認</string>
|
<string name="app_profile_template_readonly">読み取り専用</string>
|
||||||
<string name="settings_check_update_summary">アプリを開いたときにアップデートを自動的に確認する</string>
|
<string name="app_profile_template_id_exist">テンプレート ID はすでに存在します!</string>
|
||||||
|
<string name="app_profile_import_export">インポートとエクスポート</string>
|
||||||
|
<string name="app_profile_import_from_clipboard">クリップボードからインポート</string>
|
||||||
|
<string name="app_profile_export_to_clipboard">クリップボードからエクスポート</string>
|
||||||
|
<string name="app_profile_template_export_empty">エクスポートするローカル テンプレートが見つかりません!</string>
|
||||||
|
<string name="app_profile_template_import_success">インポートが成功しました</string>
|
||||||
|
<string name="app_profile_template_sync">オンラインテンプレートの同期</string>
|
||||||
|
<string name="app_profile_template_save_failed">テンプレートの保存に失敗しました</string>
|
||||||
|
<string name="app_profile_template_import_empty">クリップボードが空です!</string>
|
||||||
|
<string name="module_changelog_failed">変更ログの取得に失敗しました: %s</string>
|
||||||
|
<string name="settings_check_update">更新を確認する</string>
|
||||||
|
<string name="settings_check_update_summary">アプリを開いたときに更新を自動的に確認します</string>
|
||||||
<string name="grant_root_failed">root の付与に失敗しました!</string>
|
<string name="grant_root_failed">root の付与に失敗しました!</string>
|
||||||
|
<string name="action">アクション</string>
|
||||||
<string name="open">開く</string>
|
<string name="open">開く</string>
|
||||||
<string name="enable_web_debugging">WebView デバッグを有効にする</string>
|
<string name="enable_web_debugging">WebView デバッグを有効化する</string>
|
||||||
<string name="enable_web_debugging_summary">WebUI のデバッグに使用できます。必要な場合にのみ有効にしてください。</string>
|
<string name="enable_web_debugging_summary">WebUI のデバッグに使用できます。必要な場合でのみ有効化してください</string>
|
||||||
<string name="select_file_tip">%1$s パーティション イメージが推奨されます</string>
|
<string name="direct_install">直接インストール (推奨)</string>
|
||||||
<string name="select_kmi">KMI を選択してください</string>
|
<string name="select_file">パッチを適用する必要があるミラーを選択</string>
|
||||||
<string name="install_next">次に</string>
|
|
||||||
<string name="install_inactive_slot">非アクティブなスロットにインストール (OTA 後)</string>
|
<string name="install_inactive_slot">非アクティブなスロットにインストール (OTA 後)</string>
|
||||||
<string name="install_inactive_slot_warning">再起動後、デバイスは**強制的に**、現在非アクティブなスロットから起動します。
|
<string name="install_inactive_slot_warning">再起動後、デバイスは**強制的に**、現在非アクティブなスロットから起動します。
|
||||||
\nこのオプションは、OTA が完了した後にのみ使用してください。
|
\nこのオプションは、OTA が完了した後にのみ使用してください。
|
||||||
\n続く?</string>
|
\n続行しますか?</string>
|
||||||
<string name="direct_install">直接インストール (推奨)</string>
|
<string name="install_next">次へ</string>
|
||||||
<string name="select_file">ファイルを選択してください</string>
|
<string name="select_file_tip">%1$s のパーティションイメージを推奨します</string>
|
||||||
|
<string name="select_kmi">KMI を選択してください</string>
|
||||||
|
<string name="settings_uninstall">アンインストール</string>
|
||||||
|
<string name="settings_uninstall_temporary">一時的にアンインストールする</string>
|
||||||
<string name="settings_uninstall_permanent">完全にアンインストールする</string>
|
<string name="settings_uninstall_permanent">完全にアンインストールする</string>
|
||||||
<string name="settings_restore_stock_image">ストックイメージを復元</string>
|
<string name="settings_restore_stock_image">ストックイメージを復元</string>
|
||||||
<string name="settings_uninstall_temporary">一時的にアンインストールする</string>
|
|
||||||
<string name="settings_uninstall">アンインストール</string>
|
|
||||||
<string name="settings_uninstall_temporary_message">KernelSU を一時的にアンインストールし、次回の再起動後に元の状態に戻します。</string>
|
<string name="settings_uninstall_temporary_message">KernelSU を一時的にアンインストールし、次回の再起動後に元の状態に戻します。</string>
|
||||||
<string name="settings_uninstall_permanent_message">KernelSU (ルートおよびすべてのモジュール) を完全かつ永久にアンインストールします。</string>
|
<string name="settings_uninstall_permanent_message">KernelSU (root およびすべてのモジュール) を完全かつ恒久的にアンインストールします。</string>
|
||||||
<string name="settings_restore_stock_image_message">バックアップが存在する場合、工場出荷時のイメージを復元できます (OTA の前に使用してください)。KernelSU をアンインストールする必要がある場合は、「完全にアンインストールする」を使用してください。</string>
|
<string name="settings_restore_stock_image_message">バックアップが存在する場合、工場出荷時のイメージを復元できます (OTA の前に使用してください)。KernelSU をアンインストールする必要がある場合は、「完全にアンインストールする」を使用してください。</string>
|
||||||
<string name="flashing">フラッシュ</string>
|
<string name="flashing">フラッシュ</string>
|
||||||
<string name="flash_success">フラッシュ成功</string>
|
<string name="flash_success">フラッシュが成功しました</string>
|
||||||
<string name="flash_failed">フラッシュ失敗</string>
|
<string name="flash_failed">フラッシュに失敗しました</string>
|
||||||
<string name="selected_lkm">選択された LKM: %s</string>
|
<string name="selected_lkm">選択された LKM: %s</string>
|
||||||
<string name="save_log">ログを保存</string>
|
<string name="save_log">ログを保存</string>
|
||||||
<string name="action">アクション</string>
|
|
||||||
<string name="log_saved">保存されたログ</string>
|
<string name="log_saved">保存されたログ</string>
|
||||||
<string name="module_sort_enabled_first">並べ替え(最初に有効)</string>
|
<string name="status_supported">対応</string>
|
||||||
<string name="module_sort_action_first">並べ替え(アクション優先)</string>
|
<string name="status_not_supported">非対応</string>
|
||||||
|
<string name="status_unknown">不明</string>
|
||||||
|
<string name="sus_su_mode">SuS SU モード:</string>
|
||||||
|
<!-- Module related -->
|
||||||
|
<string name="module_install_confirm">%1$s のモジュールをインストールしますか?</string>
|
||||||
|
<string name="unknown_module">不明なモジュール</string>
|
||||||
|
<!-- Restore related -->
|
||||||
|
<string name="restore_confirm_title">モジュールの復元を確認</string>
|
||||||
|
<string name="restore_confirm_message">この操作によりモジュールが上書きされます。続行しますか?</string>
|
||||||
|
<string name="confirm">確認</string>
|
||||||
|
<string name="cancel">キャンセル</string>
|
||||||
|
<!-- Backup related -->
|
||||||
|
<string name="backup_success">バックアップが完了しました (tar.gz)</string>
|
||||||
|
<string name="backup_failed">バックアップに失敗: %1$s</string>
|
||||||
|
<string name="backup_modules">モジュールをバックアップ</string>
|
||||||
|
<string name="restore_modules">モジュールを復元</string>
|
||||||
|
<!-- Restore related messages -->
|
||||||
|
<string name="restore_success">モジュールは正常に復元されました、再起動が必要です</string>
|
||||||
|
<string name="restore_failed">復元に失敗: %1$s</string>
|
||||||
|
<string name="restart_now">今すぐ再起動</string>
|
||||||
|
<string name="unknown_error">不明なエラー</string>
|
||||||
|
<!-- Command related -->
|
||||||
|
<string name="command_execution_failed">コマンドの実行に失敗しました: %1$s</string>
|
||||||
|
<!-- Allowlist related -->
|
||||||
|
<string name="allowlist_backup_success">許可リストのバックアップが成功しました</string>
|
||||||
|
<string name="allowlist_backup_failed">許可リストのバックアップに失敗: %1$s</string>
|
||||||
|
<string name="allowlist_restore_confirm_title">許可リストの復元を確認</string>
|
||||||
|
<string name="allowlist_restore_confirm_message">この操作により許可リストが上書きされます。続行しますか?</string>
|
||||||
|
<string name="allowlist_restore_success">許可リストの復元が成功しました</string>
|
||||||
|
<string name="allowlist_restore_failed">許可リストの復元に失敗: %1$s</string>
|
||||||
|
<string name="backup_allowlist">許可リストをバックアップ</string>
|
||||||
|
<string name="restore_allowlist">許可リストを復元</string>
|
||||||
|
<string name="settings_custom_background">カスタムアプリ背景</string>
|
||||||
|
<string name="settings_custom_background_summary">背景にする画像を選択してください</string>
|
||||||
|
<string name="settings_card_alpha">ナビゲーションバーの透過</string>
|
||||||
|
<string name="settings_restore_default">デフォルトに復元</string>
|
||||||
|
<string name="home_android_version">Android のバージョン</string>
|
||||||
|
<string name="home_device_model">デバイスモデル</string>
|
||||||
|
<string name="su_not_allowed">%s にスーパーユーザー権限を付与することはできません</string>
|
||||||
|
<string name="settings_disable_su">su の互換性を無効化する</string>
|
||||||
|
<string name="settings_disable_su_summary">su コマンドを使用してアプリが root 権限を取得する動作を一時的に無効化します (既存の root プロセスは影響を受けません)。</string>
|
||||||
|
<string name="using_mksu_manager">SukiSU Beta Manager を使用しています。</string>
|
||||||
|
<string name="module_install_multiple_confirm">選択した %d 個のモジュールをインストールしてもよろしいですか?</string>
|
||||||
|
<string name="module_install_multiple_confirm_with_names">%1$d 個のモジュールをインストールしてもよろしいですか?\n\n%2$s</string>
|
||||||
|
<string name="more_settings">その他の設定</string>
|
||||||
|
<string name="selinux">SELinux</string>
|
||||||
|
<string name="selinux_enabled">有効</string>
|
||||||
|
<string name="selinux_disabled">無効</string>
|
||||||
|
<string name="simple_mode">シンプルモード</string>
|
||||||
|
<string name="simple_mode_summary">ON にすると不要なカードを非表示にします</string>
|
||||||
|
<string name="hide_kernel_kernelsu_version">カーネルのバージョンを非表示にする</string>
|
||||||
|
<string name="hide_kernel_kernelsu_version_summary">カーネルのバージョンを非表示にします</string>
|
||||||
|
<string name="hide_other_info">その他の情報を非表示にする</string>
|
||||||
|
<string name="hide_other_info_summary">ホームページ上のスーパーユーザー、モジュール、KPM モジュールの数に関する情報を非表示にします</string>
|
||||||
|
<string name="hide_susfs_status">SuSFS ステータスを非表示にする</string>
|
||||||
|
<string name="hide_susfs_status_summary">ホームページ上の SuSFS ステータス情報を非表示にします</string>
|
||||||
|
<string name="hide_link_card">リンクカードのステータスを隠す</string>
|
||||||
|
<string name="hide_link_card_summary">ホームページのリンクカード情報を隠す</string>
|
||||||
|
<string name="theme_mode">テーマ</string>
|
||||||
|
<string name="theme_follow_system">システムに従う</string>
|
||||||
|
<string name="theme_light">ライト</string>
|
||||||
|
<string name="theme_dark">ダーク</string>
|
||||||
|
<string name="manual_hook">手動でフック</string>
|
||||||
|
<string name="dynamic_color_title">ダイナミックカラー</string>
|
||||||
|
<string name="dynamic_color_summary">システムテーマのダイナミックカラーを使用します</string>
|
||||||
|
<string name="choose_theme_color">テーマカラーを選択</string>
|
||||||
|
<string name="color_default">ブルー</string>
|
||||||
|
<string name="color_green">グリーン</string>
|
||||||
|
<string name="color_purple">パープル</string>
|
||||||
|
<string name="color_orange">オレンジ</string>
|
||||||
|
<string name="color_pink">ピンク</string>
|
||||||
|
<string name="color_gray">グレー</string>
|
||||||
|
<string name="color_ivory">アイボリー</string>
|
||||||
|
<string name="flash_option">ブラシの設定</string>
|
||||||
|
<string name="flash_option_tip">フラッシュするファイルを選択</string>
|
||||||
|
<string name="horizon_kernel">AnyKernel3 をインストール</string>
|
||||||
|
<string name="horizon_kernel_summary">AnyKernel3 カーネルファイルをフラッシュします</string>
|
||||||
|
<string name="root_required">root 権限が必要です</string>
|
||||||
|
<string name="copy_failed">ファイルのコピーに失敗しました</string>
|
||||||
|
<string name="reboot_complete_title">スクラブが完了しました</string>
|
||||||
|
<string name="reboot_complete_msg">すぐに再起動しますか?</string>
|
||||||
|
<string name="yes">はい</string>
|
||||||
|
<string name="no">いいえ</string>
|
||||||
|
<string name="failed_reboot">再起動に失敗しました</string>
|
||||||
|
<string name="batch_authorization">Bulk ライセンス</string>
|
||||||
|
<string name="batch_cancel_authorization">認証を一括でキャンセル</string>
|
||||||
|
<string name="backup">バックアップ</string>
|
||||||
|
<string name="color_yellow">イエロー</string>
|
||||||
|
<string name="kpm">カーネルモジュール</string>
|
||||||
|
<string name="kpm_title">カーネルモジュール</string>
|
||||||
|
<string name="kpm_empty">カーネルモジュールは現在インストールされていません</string>
|
||||||
|
<string name="kpm_version">バージョン</string>
|
||||||
|
<string name="kpm_author">作者</string>
|
||||||
|
<string name="kpm_uninstall">アンインストール</string>
|
||||||
|
<string name="kpm_uninstall_success">アンインストールに失敗しました</string>
|
||||||
|
<string name="kpm_uninstall_failed">アンインストールに失敗しました</string>
|
||||||
|
<string name="kpm_install">インストール</string>
|
||||||
|
<string name="kpm_install_success">KPM モジュールの読み込みに成功しました</string>
|
||||||
|
<string name="kpm_install_failed">KPM モジュールの読み込みに失敗しました</string>
|
||||||
|
<string name="kpm_args">パラメータ</string>
|
||||||
|
<string name="kpm_control">実行</string>
|
||||||
|
<string name="home_kpm_version">KPM のバージョン</string>
|
||||||
|
<string name="close_notice">閉じる</string>
|
||||||
|
<string name="kernel_module_notice">以下のカーネルモジュール関数は KernelPatch によって開発され、SukiSU Ultra のカーネルモジュール関数を含むように変更されました</string>
|
||||||
|
<string name="home_ContributionCard_kernelsu">SukiSU Ultra の今後にご期待ください</string>
|
||||||
|
<string name="kpm_control_success">成功</string>
|
||||||
|
<string name="kpm_control_failed">失敗</string>
|
||||||
|
<string name="home_click_to_ContributionCard_kernelsu">SukiSU Ultra は将来的に KSU から比較的に独立したブランチになりますが、公式の KernelSU や MKSU などの貢献は引き続き感謝しています!</string>
|
||||||
|
<string name="not_supported">非対応</string>
|
||||||
|
<string name="supported">対応</string>
|
||||||
|
<string name="home_kpm_module">"KPM モジュールの数: %d "</string>
|
||||||
|
<string name="kpm_invalid_file">無効な KPM ファイル</string>
|
||||||
|
<string name="kernel_patched">カーネルはパッチされていません</string>
|
||||||
|
<string name="kernel_not_enabled">カーネルは未設定です</string>
|
||||||
|
<string name="custom_settings">カスタム設定</string>
|
||||||
|
<string name="kpm_install_mode">KPM をインストール</string>
|
||||||
|
<string name="kpm_install_mode_load">読み込む</string>
|
||||||
|
<string name="kpm_install_mode_embed">埋め込む</string>
|
||||||
|
<string name="kpm_install_mode_description">選択してください: %1\$s モジュールのインストールモード \n\n読み込む: モジュールを一時的に読み込みます\n埋め込む: システムで恒久的にインストールします</string>
|
||||||
|
<string name="log_failed_to_check_module_file">モジュールファイルの存在を確認できませんでした</string>
|
||||||
|
<string name="snackbar_failed_to_check_module_file">モジュールファイルが存在するか確認できません</string>
|
||||||
|
<string name="confirm_uninstall_title">アンインストールを確認</string>
|
||||||
|
<string name="confirm_uninstall_confirm">アンインストール</string>
|
||||||
|
<string name="confirm_uninstall_dismiss">キャンセル</string>
|
||||||
|
<string name="theme_color">テーマカラー</string>
|
||||||
|
<string name="invalid_file_type">ファイルの種類が間違っています!.kpm ファイルを選択してください。</string>
|
||||||
|
<string name="confirm_uninstall_title_with_filename">アンインストール</string>
|
||||||
|
<string name="confirm_uninstall_content">次の KPM がアンインストールされます: %s</string>
|
||||||
|
<string name="settings_susfs_toggle_summary">KernelSU によって作成された kprobe フックを無効化して、代替となるインラインフックを使用します。これは、非 GKI カーネルのフック方式に似た物になります。</string>
|
||||||
|
<string name="image_editor_title">背景画像を調整</string>
|
||||||
|
<string name="image_editor_hint">2 本の指で画像を拡大、1 本の指でドラッグで位置を調整します</string>
|
||||||
|
<string name="background_image_error">イメージを読み込めません</string>
|
||||||
|
<string name="reprovision">再プロビジョニング</string>
|
||||||
|
<!-- Kernel Flash Progress Related -->
|
||||||
|
<string name="horizon_flash_title">カーネルをフラッシュ</string>
|
||||||
|
<string name="horizon_logs_label">ログ:</string>
|
||||||
|
<string name="horizon_flash_complete">フラッシュが完了しました</string>
|
||||||
|
<!-- Flash Status Related -->
|
||||||
|
<string name="horizon_preparing">準備中…</string>
|
||||||
|
<string name="horizon_cleaning_files">ファイルを削除中…</string>
|
||||||
|
<string name="horizon_copying_files">ファイルをコピー中…</string>
|
||||||
|
<string name="horizon_extracting_tool">フラッシュツールを展開中…</string>
|
||||||
|
<string name="horizon_patching_script">フラッシュスクリプトをパッチ中…</string>
|
||||||
|
<string name="horizon_flashing">カーネルをフラッシュ中…</string>
|
||||||
|
<string name="horizon_flash_complete_status">フラッシュが完了しました</string>
|
||||||
|
<!-- Slot selection related strings -->
|
||||||
|
<string name="select_slot_title">フラッシュ先のスロットを選択</string>
|
||||||
|
<string name="select_slot_description">フラッシュする boot のターゲットスロットを選択</string>
|
||||||
|
<string name="slot_a">スロット A</string>
|
||||||
|
<string name="slot_b">スロット B</string>
|
||||||
|
<string name="selected_slot">選択したスロット: %1$s</string>
|
||||||
|
<string name="horizon_getting_original_slot">オリジナルのスロットを取得</string>
|
||||||
|
<string name="horizon_setting_target_slot">指定するスロットを設定</string>
|
||||||
|
<string name="horizon_restoring_original_slot">デフォルトのスロットに復元</string>
|
||||||
|
<string name="current_slot">現在のスロット: %1$s </string>
|
||||||
|
<!-- Error Messages -->
|
||||||
|
<string name="horizon_copy_failed">コピーに失敗しました</string>
|
||||||
|
<string name="horizon_unknown_error">不明なエラー</string>
|
||||||
|
<string name="flash_failed_message">フラッシュに失敗しました</string>
|
||||||
|
<!-- lkm/gki install -->
|
||||||
|
<string name="Lkm_install_methods">LKM の修復またはインストール</string>
|
||||||
|
<string name="GKI_install_methods">GKI/non-GKI のインストール</string>
|
||||||
|
<string name="kernel_version_log">カーネルのバージョン: %1$s</string>
|
||||||
|
<string name="tool_version_log">パッチ適用ツールの使用: %1$s</string>
|
||||||
|
<string name="configuration">設定</string>
|
||||||
|
<string name="app_settings">アプリの設定</string>
|
||||||
|
<string name="tools">ツール</string>
|
||||||
|
<string name="currently_selected">現在</string>
|
||||||
|
<!-- String resources used in SuperUser -->
|
||||||
|
<string name="clear">削除</string>
|
||||||
|
<string name="apps_with_root">root アプリの権限</string>
|
||||||
|
<string name="apps_with_custom_profile">カスタマイズされたアプリ構成</string>
|
||||||
|
<string name="other_apps">その他のアプリ</string>
|
||||||
|
<string name="no_apps_found">アプリがありません</string>
|
||||||
|
<string name="selinux_enabled_toast">SELinux 有効</string>
|
||||||
|
<string name="selinux_disabled_toast">SELinux 無効</string>
|
||||||
|
<string name="selinux_change_failed">SELinux ステータスの変更に失敗しました</string>
|
||||||
|
<string name="advanced_settings">高度な設定</string>
|
||||||
|
<string name="appearance_settings">ツールバーをカスタマイズ</string>
|
||||||
|
<string name="back">戻る</string>
|
||||||
|
<string name="expand">最高の状態</string>
|
||||||
|
<string name="collapse">設置</string>
|
||||||
|
<string name="susfs_enabled">SuSFS 有効</string>
|
||||||
|
<string name="susfs_disabled">SuSFS 無効</string>
|
||||||
|
<string name="background_set_success">背景の設定が成功しました</string>
|
||||||
|
<string name="background_removed">カスタム背景を削除しました</string>
|
||||||
|
<string name="root_require_for_install">root 権限が必要</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<string name="app_name" translatable="false">KernelSU</string>
|
|
||||||
<string name="home">Strona główna</string>
|
<string name="home">Strona główna</string>
|
||||||
<string name="home_not_installed">Nie zainstalowano</string>
|
<string name="home_not_installed">Nie zainstalowano</string>
|
||||||
<string name="home_click_to_install">Kliknij, aby zainstalować</string>
|
<string name="home_click_to_install">Kliknij, aby zainstalować</string>
|
||||||
@@ -51,7 +50,6 @@
|
|||||||
<string name="home_click_to_learn_kernelsu">Dowiedz się jak zainstalować KernelSU i jak korzystać z modułów</string>
|
<string name="home_click_to_learn_kernelsu">Dowiedz się jak zainstalować KernelSU i jak korzystać z modułów</string>
|
||||||
<string name="home_support_title">Wesprzyj nas</string>
|
<string name="home_support_title">Wesprzyj nas</string>
|
||||||
<string name="home_support_content">KernelSU jest i zawsze będzie darmowy oraz otwarty. Niemniej jednak możesz nam pokazać, że Ci zależy, wysyłając darowiznę.</string>
|
<string name="home_support_content">KernelSU jest i zawsze będzie darmowy oraz otwarty. Niemniej jednak możesz nam pokazać, że Ci zależy, wysyłając darowiznę.</string>
|
||||||
<string name="profile" translatable="false">Profil aplikacji</string>
|
|
||||||
<string name="profile_default">Domyślny</string>
|
<string name="profile_default">Domyślny</string>
|
||||||
<string name="profile_template">Szablon</string>
|
<string name="profile_template">Szablon</string>
|
||||||
<string name="profile_custom">Własny</string>
|
<string name="profile_custom">Własny</string>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user