Compare commits
130 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d743073309 | ||
|
|
a636911612 | ||
|
|
7a62f91752 | ||
|
|
b551a54c8f | ||
|
|
26d86aa2fe | ||
|
|
6ee9246650 | ||
|
|
1cd96fbdbf | ||
|
|
a030a026b1 | ||
|
|
8bf9cd0bee | ||
|
|
13b1aad4b8 | ||
|
|
916d956ce2 | ||
|
|
87a7650d26 | ||
|
|
3484e187da | ||
|
|
0835f330e2 | ||
|
|
8064472477 | ||
|
|
2281012e33 | ||
|
|
83eaeab1ba | ||
|
|
6405764df3 | ||
|
|
253276a27b | ||
|
|
855a71ac56 | ||
|
|
96dc53977f | ||
|
|
31111e68eb | ||
|
|
ac0de29872 | ||
|
|
9e2b722491 | ||
|
|
59627e6fe2 | ||
|
|
cd0b5fb378 | ||
|
|
48a3c64c7c | ||
|
|
62da804518 | ||
|
|
439b99cc4a | ||
|
|
64f0efc2c0 | ||
|
|
f196bf5b76 | ||
|
|
790968be6a | ||
|
|
83f0f9537f | ||
|
|
68ebfec918 | ||
|
|
8be4dea081 | ||
|
|
cfdbba45c3 | ||
|
|
d408c9f4bf | ||
|
|
8f4c58c4c3 | ||
|
|
7e88e9648f | ||
|
|
4516d136a4 | ||
|
|
1b85dfbed1 | ||
|
|
807ffb419a | ||
|
|
e826f43aed | ||
|
|
d619f5fafc | ||
|
|
b3e2f9b7ff | ||
|
|
99a39c6f52 | ||
|
|
22991e8740 | ||
|
|
7646ecb6f7 | ||
|
|
204db674bb | ||
|
|
99fe6623de | ||
|
|
f1f78d2485 | ||
|
|
b2ae20b796 | ||
|
|
83bd4e9642 | ||
|
|
767349798a | ||
|
|
ae38f4709b | ||
|
|
fc7001a11a | ||
|
|
9924809bdb | ||
|
|
58a4ff94e4 | ||
|
|
29033e9b80 | ||
|
|
ea24daf37c | ||
|
|
ebc16583fb | ||
|
|
2a10b41781 | ||
|
|
d5946047a1 | ||
|
|
4ff46a4911 | ||
|
|
b587216b5e | ||
|
|
245fce167e | ||
|
|
de9b82ffd5 | ||
|
|
e570f402e4 | ||
|
|
9c761b13fa | ||
|
|
cc4b135d20 | ||
|
|
ec5395c787 | ||
|
|
6d60e54a7d | ||
|
|
28aa34c0b6 | ||
|
|
0701967bab | ||
|
|
a76b1eece4 | ||
|
|
8e791c680e | ||
|
|
fc9f2ccf25 | ||
|
|
d4682fb06e | ||
|
|
377ea183a7 | ||
|
|
72361ab8bf | ||
|
|
f708e583c3 | ||
|
|
d753e1dc48 | ||
|
|
315a8a3805 | ||
|
|
129fed9c9f | ||
|
|
0baccb7621 | ||
|
|
842a8aa45a | ||
|
|
d17843479c | ||
|
|
0d70cc8e58 | ||
|
|
4e6cacb206 | ||
|
|
52514ba35b | ||
|
|
4d59ce435e | ||
|
|
b3b7fa6f4d | ||
|
|
c057c16391 | ||
|
|
dee7cc6f2b | ||
|
|
3d0d87cb0c | ||
|
|
6b66d9b3f8 | ||
|
|
a301d94858 | ||
|
|
01199470f2 | ||
|
|
9e7ea19567 | ||
|
|
cdc6a6cb4a | ||
|
|
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 |
74
.github/workflows/build-lkm-local.yml
vendored
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
name: Build LKM for KernelSU Local
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
inputs:
|
||||||
|
upload:
|
||||||
|
required: true
|
||||||
|
type: boolean
|
||||||
|
default: true
|
||||||
|
description: "Whether to upload to branch"
|
||||||
|
secrets:
|
||||||
|
# username:github_pat
|
||||||
|
TOKEN:
|
||||||
|
required: true
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
upload:
|
||||||
|
required: true
|
||||||
|
type: boolean
|
||||||
|
default: true
|
||||||
|
description: "Whether to upload to branch"
|
||||||
|
jobs:
|
||||||
|
build-lkm:
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- version: "android12-5.10"
|
||||||
|
sub_level: 236
|
||||||
|
os_patch_level: 2025-05
|
||||||
|
- version: "android13-5.10"
|
||||||
|
sub_level: 234
|
||||||
|
os_patch_level: 2025-03
|
||||||
|
- version: "android13-5.15"
|
||||||
|
sub_level: 178
|
||||||
|
os_patch_level: 2025-03
|
||||||
|
- version: "android14-5.15"
|
||||||
|
sub_level: 178
|
||||||
|
os_patch_level: 2025-03
|
||||||
|
- version: "android14-6.1"
|
||||||
|
sub_level: 134
|
||||||
|
os_patch_level: 2025-05
|
||||||
|
- version: "android15-6.6"
|
||||||
|
sub_level: 87
|
||||||
|
os_patch_level: 2025-05
|
||||||
|
# uses: ./.github/workflows/gki-kernel-mock.yml when debugging
|
||||||
|
uses: ./.github/workflows/gki-kernel-local.yml
|
||||||
|
with:
|
||||||
|
version: ${{ matrix.version }}
|
||||||
|
version_name: ${{ matrix.version }}.${{ matrix.sub_level }}
|
||||||
|
tag: ${{ matrix.version }}-${{ matrix.os_patch_level }}
|
||||||
|
os_patch_level: ${{ matrix.os_patch_level }}
|
||||||
|
build_lkm: true
|
||||||
|
|
||||||
|
push-to-branch:
|
||||||
|
needs: [build-lkm]
|
||||||
|
runs-on: self-hosted
|
||||||
|
if: ${{ inputs.upload }}
|
||||||
|
steps:
|
||||||
|
- name: Download all workflow run artifacts
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
path: bin/
|
||||||
|
merge-multiple: true
|
||||||
|
- name: Push to branch LKM
|
||||||
|
run: |
|
||||||
|
cd bin
|
||||||
|
git config --global init.defaultBranch lkm
|
||||||
|
git init
|
||||||
|
git remote add origin https://${{ secrets.TOKEN }}@github.com/${{ github.repository }}
|
||||||
|
git config --local user.name "github-actions[bot]"
|
||||||
|
git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||||
|
find . -type f
|
||||||
|
git add .
|
||||||
|
git commit -m "Upload LKM from ${{ github.sha }}" -m "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
|
||||||
|
git push --force --set-upstream origin lkm
|
||||||
12
.github/workflows/build-lkm.yml
vendored
@@ -24,8 +24,8 @@ jobs:
|
|||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- version: "android12-5.10"
|
- version: "android12-5.10"
|
||||||
sub_level: 233
|
sub_level: 236
|
||||||
os_patch_level: 2025-02
|
os_patch_level: 2025-05
|
||||||
- version: "android13-5.10"
|
- version: "android13-5.10"
|
||||||
sub_level: 234
|
sub_level: 234
|
||||||
os_patch_level: 2025-03
|
os_patch_level: 2025-03
|
||||||
@@ -36,11 +36,11 @@ jobs:
|
|||||||
sub_level: 178
|
sub_level: 178
|
||||||
os_patch_level: 2025-03
|
os_patch_level: 2025-03
|
||||||
- version: "android14-6.1"
|
- version: "android14-6.1"
|
||||||
sub_level: 129
|
sub_level: 134
|
||||||
os_patch_level: 2025-04
|
os_patch_level: 2025-05
|
||||||
- version: "android15-6.6"
|
- version: "android15-6.6"
|
||||||
sub_level: 82
|
sub_level: 87
|
||||||
os_patch_level: 2025-04
|
os_patch_level: 2025-05
|
||||||
# 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:
|
||||||
|
|||||||
252
.github/workflows/build-manager-manual.yml
vendored
Normal file
@@ -0,0 +1,252 @@
|
|||||||
|
name: Build Manager Manual
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
build_lkm:
|
||||||
|
required: true
|
||||||
|
type: choice
|
||||||
|
default: "auto"
|
||||||
|
options:
|
||||||
|
- "true"
|
||||||
|
- "false"
|
||||||
|
- "auto"
|
||||||
|
description: "Whether to build lkm"
|
||||||
|
upload_lkm:
|
||||||
|
required: true
|
||||||
|
type: boolean
|
||||||
|
default: true
|
||||||
|
description: "Whether to upload lkm"
|
||||||
|
jobs:
|
||||||
|
check-build-lkm:
|
||||||
|
runs-on: self-hosted
|
||||||
|
outputs:
|
||||||
|
build_lkm: ${{ steps.check-build.outputs.build_lkm }}
|
||||||
|
upload_lkm: ${{ steps.check-build.outputs.upload_lkm }}
|
||||||
|
steps:
|
||||||
|
- name: check build
|
||||||
|
id: check-build
|
||||||
|
run: |
|
||||||
|
if [ "${{ github.event_name }}" == "workflow_dispatch" ] && [ "${{ inputs.build_lkm }}" != "auto" ]; then
|
||||||
|
kernel_changed="${{ inputs.build_lkm }}"
|
||||||
|
else
|
||||||
|
kernel_changed=true
|
||||||
|
mkdir tmp
|
||||||
|
cd tmp
|
||||||
|
git config --global init.defaultBranch bot
|
||||||
|
git config --global user.name 'Bot'
|
||||||
|
git config --global user.email 'bot@github.shirkneko.io'
|
||||||
|
git init .
|
||||||
|
git remote add origin https://github.com/${{ github.repository }}
|
||||||
|
CURRENT_COMMIT="${{ github.event.head_commit.id }}"
|
||||||
|
git fetch origin $CURRENT_COMMIT --depth=1
|
||||||
|
git fetch origin lkm --depth=1
|
||||||
|
LKM_COMMIT="$(git log --format=%B -n 1 origin/lkm | head -n 1)"
|
||||||
|
LKM_COMMIT="${LKM_COMMIT#Upload LKM from }"
|
||||||
|
LKM_COMMIT=$(echo "$LKM_COMMIT" | tr -d '[:space:]')
|
||||||
|
echo "LKM_COMMIT=$LKM_COMMIT"
|
||||||
|
git fetch origin "$LKM_COMMIT" --depth=1
|
||||||
|
git diff --quiet "$LKM_COMMIT" "$CURRENT_COMMIT" -- kernel :!kernel/setup.sh .github/workflows/build-lkm-local.yml .github/workflows/build-kernel-*.yml && kernel_changed=false
|
||||||
|
cd ..
|
||||||
|
rm -rf tmp
|
||||||
|
fi
|
||||||
|
if [ "${{ github.event_name }}" == "push" ] && [ "${{ github.ref }}" == 'refs/heads/main' ]; then
|
||||||
|
need_upload=true
|
||||||
|
elif [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
|
||||||
|
need_upload="${{ inputs.upload_lkm }}"
|
||||||
|
else
|
||||||
|
need_upload=false
|
||||||
|
fi
|
||||||
|
echo "kernel changed: $kernel_changed"
|
||||||
|
echo "need upload: $need_upload"
|
||||||
|
echo "build_lkm=$kernel_changed" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "upload_lkm=$need_upload" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
build-lkm:
|
||||||
|
needs: check-build-lkm
|
||||||
|
uses: ./.github/workflows/build-lkm-local.yml
|
||||||
|
if: ${{ needs.check-build-lkm.outputs.build_lkm == 'true' }}
|
||||||
|
with:
|
||||||
|
upload: ${{ needs.check-build-lkm.outputs.upload_lkm == 'true' }}
|
||||||
|
secrets: inherit
|
||||||
|
build-susfs:
|
||||||
|
if: ${{ always() }}
|
||||||
|
needs: [ check-build-lkm, build-lkm ]
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- target: aarch64-linux-android
|
||||||
|
os: ubuntu-latest
|
||||||
|
uses: ./.github/workflows/susfs.yml
|
||||||
|
with:
|
||||||
|
target: ${{ matrix.target }}
|
||||||
|
os: ${{ matrix.os }}
|
||||||
|
|
||||||
|
build-kpmmgr:
|
||||||
|
if: ${{ always() }}
|
||||||
|
needs: [ check-build-lkm, build-lkm ]
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- target: aarch64-linux-android
|
||||||
|
os: ubuntu-latest
|
||||||
|
uses: ./.github/workflows/kpmmgr.yml
|
||||||
|
with:
|
||||||
|
target: ${{ matrix.target }}
|
||||||
|
os: ${{ matrix.os }}
|
||||||
|
|
||||||
|
build-ksud:
|
||||||
|
if: ${{ always() }}
|
||||||
|
needs: [ check-build-lkm, build-lkm ]
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- target: aarch64-linux-android
|
||||||
|
os: ubuntu-latest
|
||||||
|
- target: x86_64-linux-android
|
||||||
|
os: ubuntu-latest
|
||||||
|
- target: armv7-linux-androideabi
|
||||||
|
os: ubuntu-latest
|
||||||
|
uses: ./.github/workflows/ksud.yml
|
||||||
|
with:
|
||||||
|
target: ${{ matrix.target }}
|
||||||
|
os: ${{ matrix.os }}
|
||||||
|
pack_lkm: true
|
||||||
|
pull_lkm: ${{ needs.check-build-lkm.outputs.build_lkm != 'true' }}
|
||||||
|
|
||||||
|
build-manager:
|
||||||
|
if: ${{ always() }}
|
||||||
|
needs: build-ksud
|
||||||
|
runs-on: self-hosted
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: ./manager
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Setup need_upload
|
||||||
|
id: need_upload
|
||||||
|
run: |
|
||||||
|
if [ ! -z "${{ secrets.BOT_TOKEN }}" ]; then
|
||||||
|
echo "UPLOAD=true" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "UPLOAD=false" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Write key
|
||||||
|
if: ${{ ( github.event_name != 'pull_request' && github.ref == 'refs/heads/main' ) || github.ref == 'refs/heads/susfs' || github.ref_type == 'tag' }}
|
||||||
|
run: |
|
||||||
|
if [ ! -z "${{ secrets.KEYSTORE }}" ]; then
|
||||||
|
{
|
||||||
|
echo KEYSTORE_PASSWORD='${{ secrets.KEYSTORE_PASSWORD }}'
|
||||||
|
echo KEY_ALIAS='${{ secrets.KEY_ALIAS }}'
|
||||||
|
echo KEY_PASSWORD='${{ secrets.KEY_PASSWORD }}'
|
||||||
|
echo KEYSTORE_FILE='key.jks'
|
||||||
|
} >> gradle.properties
|
||||||
|
echo "${{ secrets.KEYSTORE }}" | base64 -d > key.jks
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Download arm64 susfs
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: susfs-aarch64-linux-android
|
||||||
|
path: .
|
||||||
|
|
||||||
|
- name: Download arm64 kpmmgr
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: kpmmgr-aarch64-linux-android
|
||||||
|
path: .
|
||||||
|
|
||||||
|
- name: Download arm64 ksud
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: ksud-aarch64-linux-android
|
||||||
|
path: .
|
||||||
|
|
||||||
|
- name: Download x86_64 ksud
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: ksud-x86_64-linux-android
|
||||||
|
path: .
|
||||||
|
|
||||||
|
- name: Download arm ksud
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: ksud-armv7-linux-androideabi
|
||||||
|
path: .
|
||||||
|
|
||||||
|
- name: Copy ksud to app jniLibs
|
||||||
|
run: |
|
||||||
|
mkdir -p app/src/main/jniLibs/arm64-v8a
|
||||||
|
mkdir -p app/src/main/jniLibs/x86_64
|
||||||
|
mkdir -p app/src/main/jniLibs/armeabi-v7a
|
||||||
|
cp -f ../aarch64-linux-android/release/zakozako ../manager/app/src/main/jniLibs/arm64-v8a/libzakozako.so
|
||||||
|
cp -f ../x86_64-linux-android/release/zakozako ../manager/app/src/main/jniLibs/x86_64/libzakozako.so
|
||||||
|
cp -f ../armv7-linux-androideabi/release/zakozako ../manager/app/src/main/jniLibs/armeabi-v7a/libzakozako.so
|
||||||
|
|
||||||
|
- name: Copy kpmmgr to app jniLibs
|
||||||
|
run: |
|
||||||
|
mkdir -p app/src/main/jniLibs/arm64-v8a
|
||||||
|
cp -f ../arm64-v8a/kpmmgr ../manager/app/src/main/jniLibs/arm64-v8a/libkpmmgr.so
|
||||||
|
|
||||||
|
- name: Copy susfs to app jniLibs
|
||||||
|
run: |
|
||||||
|
mkdir -p app/src/main/jniLibs/arm64-v8a
|
||||||
|
cp -f ../arm64-v8a/zakozakozako ../manager/app/src/main/jniLibs/arm64-v8a/libzakozakozako.so
|
||||||
|
|
||||||
|
- name: Build with Gradle
|
||||||
|
run: |
|
||||||
|
export ANDROID_HOME=/root/.android/sdk
|
||||||
|
export PATH=$ANDROID_HOME/platform-tools:$ANDROID_HOME/tools:$ANDROID_HOME/tools/bin:$PATH
|
||||||
|
{
|
||||||
|
echo 'org.gradle.parallel=true'
|
||||||
|
echo 'org.gradle.vfs.watch=true'
|
||||||
|
echo 'org.gradle.jvmargs=-Xmx2048m'
|
||||||
|
echo 'android.native.buildOutput=verbose'
|
||||||
|
} >> gradle.properties
|
||||||
|
sed -i 's/org.gradle.configuration-cache=true//g' gradle.properties
|
||||||
|
./gradlew clean assembleRelease
|
||||||
|
|
||||||
|
- name: Upload build artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
if: ${{ ( github.event_name != 'pull_request' && github.ref == 'refs/heads/main' ) || github.ref_type == 'tag' }}
|
||||||
|
with:
|
||||||
|
name: manager
|
||||||
|
path: manager/app/build/outputs/apk/release/*.apk
|
||||||
|
|
||||||
|
- name: Upload mappings
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
if: ${{ ( github.event_name != 'pull_request' && github.ref == 'refs/heads/main' ) || github.ref_type == 'tag' }}
|
||||||
|
with:
|
||||||
|
name: "mappings"
|
||||||
|
path: "manager/app/build/outputs/mapping/release/"
|
||||||
|
|
||||||
|
- name: Bot session cache
|
||||||
|
if: github.event_name != 'pull_request' && steps.need_upload.outputs.UPLOAD == 'true'
|
||||||
|
id: bot_session_cache
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: scripts/ksubot.session
|
||||||
|
key: ${{ runner.os }}-bot-session
|
||||||
|
|
||||||
|
- name: Upload to telegram
|
||||||
|
if: github.event_name != 'pull_request' && steps.need_upload.outputs.UPLOAD == 'true'
|
||||||
|
env:
|
||||||
|
CHAT_ID: ${{ vars.CHAT_ID }}
|
||||||
|
BOT_TOKEN: ${{ secrets.BOT_TOKEN }}
|
||||||
|
MESSAGE_THREAD_ID: ${{ vars.MESSAGE_THREAD_ID }}
|
||||||
|
COMMIT_MESSAGE: ${{ github.event.head_commit.message }}
|
||||||
|
COMMIT_URL: ${{ github.event.head_commit.url }}
|
||||||
|
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||||
|
TITLE: Manager
|
||||||
|
run: |
|
||||||
|
if [ ! -z "${{ secrets.BOT_TOKEN }}" ]; then
|
||||||
|
export VERSION=$(git rev-list --count HEAD)
|
||||||
|
APK=$(find ./app/build/outputs/apk/release -name "*.apk")
|
||||||
|
python3 $GITHUB_WORKSPACE/scripts/ksubot.py $APK
|
||||||
|
fi
|
||||||
10
.github/workflows/build-manager.yml
vendored
@@ -119,6 +119,8 @@ jobs:
|
|||||||
os: ubuntu-latest
|
os: ubuntu-latest
|
||||||
- target: x86_64-linux-android
|
- target: x86_64-linux-android
|
||||||
os: ubuntu-latest
|
os: ubuntu-latest
|
||||||
|
- target: armv7-linux-androideabi
|
||||||
|
os: ubuntu-latest
|
||||||
uses: ./.github/workflows/ksud.yml
|
uses: ./.github/workflows/ksud.yml
|
||||||
with:
|
with:
|
||||||
target: ${{ matrix.target }}
|
target: ${{ matrix.target }}
|
||||||
@@ -198,12 +200,20 @@ jobs:
|
|||||||
name: ksud-x86_64-linux-android
|
name: ksud-x86_64-linux-android
|
||||||
path: .
|
path: .
|
||||||
|
|
||||||
|
- name: Download arm ksud
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: ksud-armv7-linux-androideabi
|
||||||
|
path: .
|
||||||
|
|
||||||
- name: Copy ksud to app jniLibs
|
- name: Copy ksud to app jniLibs
|
||||||
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
|
||||||
|
mkdir -p app/src/main/jniLibs/armeabi-v7a
|
||||||
cp -f ../aarch64-linux-android/release/zakozako ../manager/app/src/main/jniLibs/arm64-v8a/libzakozako.so
|
cp -f ../aarch64-linux-android/release/zakozako ../manager/app/src/main/jniLibs/arm64-v8a/libzakozako.so
|
||||||
cp -f ../x86_64-linux-android/release/zakozako ../manager/app/src/main/jniLibs/x86_64/libzakozako.so
|
cp -f ../x86_64-linux-android/release/zakozako ../manager/app/src/main/jniLibs/x86_64/libzakozako.so
|
||||||
|
cp -f ../armv7-linux-androideabi/release/zakozako ../manager/app/src/main/jniLibs/armeabi-v7a/libzakozako.so
|
||||||
|
|
||||||
- name: Copy kpmmgr to app jniLibs
|
- name: Copy kpmmgr to app jniLibs
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
38
.github/workflows/crowdin.yml
vendored
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
name: Crowdin Action
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ main ]
|
||||||
|
paths:
|
||||||
|
- 'manager/app/src/main/res/**'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
synchronize-with-crowdin:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: crowdin action
|
||||||
|
uses: crowdin/github-action@master
|
||||||
|
with:
|
||||||
|
upload_sources: true
|
||||||
|
upload_translations: true
|
||||||
|
download_translations: true
|
||||||
|
localization_branch_name: "Crowdin"
|
||||||
|
crowdin_branch_name: "main"
|
||||||
|
create_pull_request: true
|
||||||
|
pull_request_title: 'New Crowdin Translations'
|
||||||
|
pull_request_body: 'New Crowdin translations by [Crowdin GH Action](https://github.com/crowdin/github-action)'
|
||||||
|
pull_request_base_branch_name: 'main'
|
||||||
|
skip_untranslated_files: true
|
||||||
|
env:
|
||||||
|
# A classic GitHub Personal Access Token with the 'repo' scope selected (the user should have write access to the repository).
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||||
|
|
||||||
|
# A numeric ID, found at https://crowdin.com/project/<projectName>/tools/api
|
||||||
|
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
|
||||||
|
|
||||||
|
# Visit https://crowdin.com/settings#api-key to create this token
|
||||||
|
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
|
||||||
252
.github/workflows/gki-kernel-local.yml
vendored
Normal file
@@ -0,0 +1,252 @@
|
|||||||
|
name: GKI Kernel Build Local
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
inputs:
|
||||||
|
version:
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
description: >
|
||||||
|
Output directory of gki,
|
||||||
|
for example: android12-5.10
|
||||||
|
version_name:
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
description: >
|
||||||
|
With SUBLEVEL of kernel,
|
||||||
|
for example: android12-5.10.66
|
||||||
|
tag:
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
description: >
|
||||||
|
Part of branch name of common kernel manifest,
|
||||||
|
for example: android12-5.10-2021-11
|
||||||
|
os_patch_level:
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
description: >
|
||||||
|
Patch level of common kernel manifest,
|
||||||
|
for example: 2021-11
|
||||||
|
default: 2022-05
|
||||||
|
patch_path:
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
description: >
|
||||||
|
Directory name of .github/patches/<patch_path>
|
||||||
|
for example: 5.10
|
||||||
|
use_cache:
|
||||||
|
required: false
|
||||||
|
type: boolean
|
||||||
|
default: true
|
||||||
|
embed_ksud:
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
default: ksud-aarch64-linux-android
|
||||||
|
description: >
|
||||||
|
Artifact name of prebuilt ksud to be embedded
|
||||||
|
for example: ksud-aarch64-linux-android
|
||||||
|
debug:
|
||||||
|
required: false
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
build_lkm:
|
||||||
|
required: false
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
secrets:
|
||||||
|
BOOT_SIGN_KEY:
|
||||||
|
required: false
|
||||||
|
CHAT_ID:
|
||||||
|
required: false
|
||||||
|
BOT_TOKEN:
|
||||||
|
required: false
|
||||||
|
MESSAGE_THREAD_ID:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build ${{ inputs.version_name }}
|
||||||
|
runs-on: self-hosted
|
||||||
|
env:
|
||||||
|
CCACHE_COMPILERCHECK: "%compiler% -dumpmachine; %compiler% -dumpversion"
|
||||||
|
CCACHE_NOHASHDIR: "true"
|
||||||
|
CCACHE_HARDLINK: "true"
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
path: KernelSU
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Setup need_upload
|
||||||
|
id: need_upload
|
||||||
|
run: |
|
||||||
|
if [ ! -z "${{ secrets.BOT_TOKEN }}" ]; then
|
||||||
|
echo "UPLOAD=true" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "UPLOAD=false" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Setup kernel source
|
||||||
|
run: |
|
||||||
|
echo "Free space:"
|
||||||
|
df -h
|
||||||
|
cd $GITHUB_WORKSPACE
|
||||||
|
sudo apt-get install repo -y
|
||||||
|
export REPO_URL='https://mirrors.tuna.tsinghua.edu.cn/git/git-repo'
|
||||||
|
mkdir android-kernel && cd android-kernel
|
||||||
|
repo init --depth=1 --u https://mirrors.tuna.tsinghua.edu.cn/git/AOSP/kernel/manifest -b common-${{ inputs.tag }} --repo-rev=v2.35
|
||||||
|
REMOTE_BRANCH=$(git ls-remote https://mirrors.tuna.tsinghua.edu.cn/git/AOSP/kernel/common ${{ inputs.tag }})
|
||||||
|
DEFAULT_MANIFEST_PATH=.repo/manifests/default.xml
|
||||||
|
if grep -q deprecated <<< $REMOTE_BRANCH; then
|
||||||
|
echo "Found deprecated branch: ${{ inputs.tag }}"
|
||||||
|
sed -i 's/"${{ inputs.tag }}"/"deprecated\/${{ inputs.tag }}"/g' $DEFAULT_MANIFEST_PATH
|
||||||
|
cat $DEFAULT_MANIFEST_PATH
|
||||||
|
fi
|
||||||
|
repo --version
|
||||||
|
repo --trace sync -c -j$(nproc --all) --no-tags
|
||||||
|
df -h
|
||||||
|
|
||||||
|
- name: Setup KernelSU
|
||||||
|
env:
|
||||||
|
PATCH_PATH: ${{ inputs.patch_path }}
|
||||||
|
IS_DEBUG_KERNEL: ${{ inputs.debug }}
|
||||||
|
run: |
|
||||||
|
cd $GITHUB_WORKSPACE/android-kernel
|
||||||
|
echo "[+] KernelSU setup"
|
||||||
|
GKI_ROOT=$(pwd)
|
||||||
|
echo "[+] GKI_ROOT: $GKI_ROOT"
|
||||||
|
echo "[+] Copy KernelSU driver to $GKI_ROOT/common/drivers"
|
||||||
|
ln -sf $GITHUB_WORKSPACE/KernelSU/kernel $GKI_ROOT/common/drivers/kernelsu
|
||||||
|
echo "[+] Add KernelSU driver to Makefile"
|
||||||
|
DRIVER_MAKEFILE=$GKI_ROOT/common/drivers/Makefile
|
||||||
|
DRIVER_KCONFIG=$GKI_ROOT/common/drivers/Kconfig
|
||||||
|
grep -q "kernelsu" "$DRIVER_MAKEFILE" || printf "\nobj-\$(CONFIG_KSU) += kernelsu/\n" >> "$DRIVER_MAKEFILE"
|
||||||
|
grep -q "kernelsu" "$DRIVER_KCONFIG" || sed -i "/endmenu/i\\source \"drivers/kernelsu/Kconfig\"" "$DRIVER_KCONFIG"
|
||||||
|
echo "[+] Apply Compilation Patches"
|
||||||
|
if [ ! -e build/build.sh ]; then
|
||||||
|
GLIBC_VERSION=$(ldd --version 2>/dev/null | head -n 1 | awk '{print $NF}')
|
||||||
|
echo "GLIBC_VERSION: $GLIBC_VERSION"
|
||||||
|
if [ "$(printf '%s\n' "2.38" "$GLIBC_VERSION" | sort -V | head -n1)" = "2.38" ]; then
|
||||||
|
echo "Patching resolve_btfids/Makefile"
|
||||||
|
cd $GKI_ROOT/common/ && sed -i '/\$(Q)\$(MAKE) -C \$(SUBCMD_SRC) OUTPUT=\$(abspath \$(dir \$@))\/ \$(abspath \$@)/s//$(Q)$(MAKE) -C $(SUBCMD_SRC) EXTRA_CFLAGS="$(CFLAGS)" OUTPUT=$(abspath $(dir $@))\/ $(abspath $@)/' tools/bpf/resolve_btfids/Makefile || echo "No patch needed."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$IS_DEBUG_KERNEL" = "true" ]; then
|
||||||
|
echo "[+] Enable debug features for kernel"
|
||||||
|
printf "\nccflags-y += -DCONFIG_KSU_DEBUG\n" >> $GITHUB_WORKSPACE/KernelSU/kernel/Makefile
|
||||||
|
fi
|
||||||
|
repo status
|
||||||
|
echo "[+] KernelSU setup done."
|
||||||
|
|
||||||
|
- name: Symbol magic
|
||||||
|
run: |
|
||||||
|
echo "[+] Export all symbol from abi_gki_aarch64.xml"
|
||||||
|
COMMON_ROOT=$GITHUB_WORKSPACE/android-kernel/common
|
||||||
|
KSU_ROOT=$GITHUB_WORKSPACE/KernelSU
|
||||||
|
ABI_XML=$COMMON_ROOT/android/abi_gki_aarch64.xml
|
||||||
|
SYMBOL_LIST=$COMMON_ROOT/android/abi_gki_aarch64
|
||||||
|
# python3 $KSU_ROOT/scripts/abi_gki_all.py $ABI_XML > $SYMBOL_LIST
|
||||||
|
echo "[+] Add KernelSU symbols"
|
||||||
|
cat $KSU_ROOT/kernel/export_symbol.txt | awk '{sub("[ \t]+","");print " "$0}' >> $SYMBOL_LIST
|
||||||
|
|
||||||
|
- name: Setup ccache
|
||||||
|
if: inputs.use_cache == true
|
||||||
|
uses: hendrikmuhs/ccache-action@v1
|
||||||
|
with:
|
||||||
|
key: gki-kernel-aarch64-${{ inputs.version_name }}
|
||||||
|
max-size: 2G
|
||||||
|
save: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
|
||||||
|
|
||||||
|
- name: Setup for LKM
|
||||||
|
if: ${{ inputs.build_lkm == true }}
|
||||||
|
working-directory: android-kernel
|
||||||
|
run: |
|
||||||
|
pip install ast-grep-cli
|
||||||
|
sudo apt-get install llvm-15 -y
|
||||||
|
ast-grep -U -p '$$$ check_exports($$$) {$$$}' -r '' common/scripts/mod/modpost.c
|
||||||
|
ast-grep -U -p 'check_exports($$$);' -r '' common/scripts/mod/modpost.c
|
||||||
|
sed -i '/config KSU/,/help/{s/default y/default m/}' common/drivers/kernelsu/Kconfig
|
||||||
|
echo "drivers/kernelsu/kernelsu.ko" >> common/android/gki_aarch64_modules
|
||||||
|
|
||||||
|
# bazel build, android14-5.15, android14-6.1 use bazel
|
||||||
|
if [ ! -e build/build.sh ]; then
|
||||||
|
sed -i 's/needs unknown symbol/Dont abort when unknown symbol/g' build/kernel/*.sh || echo "No unknown symbol scripts found"
|
||||||
|
if [ -e common/modules.bzl ]; then
|
||||||
|
sed -i 's/_COMMON_GKI_MODULES_LIST = \[/_COMMON_GKI_MODULES_LIST = \[ "drivers\/kernelsu\/kernelsu.ko",/g' common/modules.bzl
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
TARGET_FILE="build/kernel/build.sh"
|
||||||
|
if [ ! -e "$TARGET_FILE" ]; then
|
||||||
|
TARGET_FILE="build/build.sh"
|
||||||
|
fi
|
||||||
|
sed -i 's/needs unknown symbol/Dont abort when unknown symbol/g' $TARGET_FILE || echo "No unknown symbol in $TARGET_FILE"
|
||||||
|
sed -i 's/if ! diff -u "\${KERNEL_DIR}\/\${MODULES_ORDER}" "\${OUT_DIR}\/modules\.order"; then/if false; then/g' $TARGET_FILE
|
||||||
|
sed -i 's@${ROOT_DIR}/build/abi/compare_to_symbol_list@echo@g' $TARGET_FILE
|
||||||
|
sed -i 's/needs unknown symbol/Dont abort when unknown symbol/g' build/kernel/*.sh || echo "No unknown symbol scripts found"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Make working directory clean to avoid dirty
|
||||||
|
working-directory: android-kernel
|
||||||
|
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!"
|
||||||
|
git config --global user.email "bot@kernelsu.org"
|
||||||
|
git config --global user.name "KernelSUBot"
|
||||||
|
cd common/ && git add -A && git commit -a -m "Add KernelSU"
|
||||||
|
repo status
|
||||||
|
|
||||||
|
- name: Build Kernel/LKM
|
||||||
|
working-directory: android-kernel
|
||||||
|
run: |
|
||||||
|
if [ ! -z ${{ vars.EXPECTED_SIZE }} ] && [ ! -z ${{ vars.EXPECTED_HASH }} ]; then
|
||||||
|
export KSU_EXPECTED_SIZE=${{ vars.EXPECTED_SIZE }}
|
||||||
|
export KSU_EXPECTED_HASH=${{ vars.EXPECTED_HASH }}
|
||||||
|
fi
|
||||||
|
if [ -e build/build.sh ]; then
|
||||||
|
LTO=thin BUILD_CONFIG=common/build.config.gki.aarch64 build/build.sh CC="/usr/bin/ccache clang"
|
||||||
|
else
|
||||||
|
tools/bazel run --disk_cache=/home/runner/.cache/bazel --config=fast --config=stamp --lto=thin //common:kernel_aarch64_dist -- --dist_dir=dist
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Prepare artifacts
|
||||||
|
id: prepareArtifacts
|
||||||
|
run: |
|
||||||
|
OUTDIR=android-kernel/out/${{ inputs.version }}/dist
|
||||||
|
if [ ! -e $OUTDIR ]; then
|
||||||
|
OUTDIR=android-kernel/dist
|
||||||
|
fi
|
||||||
|
mkdir output
|
||||||
|
if [ "${{ inputs.build_lkm}}" = "true" ]; then
|
||||||
|
llvm-strip-15 -d $OUTDIR/kernelsu.ko
|
||||||
|
mv $OUTDIR/kernelsu.ko ./output/${{ inputs.version }}_kernelsu.ko
|
||||||
|
else
|
||||||
|
cp $OUTDIR/Image ./output/
|
||||||
|
cp $OUTDIR/Image.lz4 ./output/
|
||||||
|
git clone https://github.com/Kernel-SU/AnyKernel3
|
||||||
|
rm -rf ./AnyKernel3/.git
|
||||||
|
cp $OUTDIR/Image ./AnyKernel3/
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Upload Image and Image.gz
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
if: ${{ inputs.build_lkm == false }}
|
||||||
|
with:
|
||||||
|
name: Image-${{ inputs.version_name }}_${{ inputs.os_patch_level }}
|
||||||
|
path: ./output/*
|
||||||
|
|
||||||
|
- name: Upload AnyKernel3
|
||||||
|
if: ${{ inputs.build_lkm == false }}
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: AnyKernel3-${{ inputs.version_name }}_${{ inputs.os_patch_level }}
|
||||||
|
path: ./AnyKernel3/*
|
||||||
|
|
||||||
|
- name: Upload LKM
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
if: ${{ inputs.build_lkm == true }}
|
||||||
|
with:
|
||||||
|
name: ${{ inputs.version }}-lkm
|
||||||
|
path: ./output/*_kernelsu.ko
|
||||||
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"
|
||||||
|
|||||||
3
crowdin.yml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
files:
|
||||||
|
- source: /manager/app/src/main/res/values/strings.xml
|
||||||
|
translation: /manager/app/src/main/res/values-%two_letters_code%/strings.xml
|
||||||
@@ -1,50 +1,67 @@
|
|||||||
# SukiSU Ultra
|
# SukiSU Ultra
|
||||||
|
|
||||||
**English** | [简体中文](README.md) | [日本語](README-ja.md)
|
**English** | [简体中文](README.md) | [日本語](README-ja.md) | [Türkçe](README-tr.md)
|
||||||
|
|
||||||
|
|
||||||
Android device root solution based on [KernelSU](https://github.com/tiann/KernelSU)
|
Android device root solution based on [KernelSU](https://github.com/tiann/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](https://github.com/tiann/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 are reserved to [@tiann](https://github.com/tiann)
|
||||||
> However, we will be a separately maintained branch of KSU in the future
|
|
||||||
>
|
>
|
||||||
|
> 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)
|
|
||||||
|
|
||||||
## How to add
|
## How to add
|
||||||
|
|
||||||
Use the susfs-stable or susfs-dev branch (integrated susfs with support for non-GKI devices)
|
Using main branching (non-GKI device builds are not supported) (requires manual integration of susfs)
|
||||||
|
|
||||||
```
|
```
|
||||||
curl -LSs "https://raw.githubusercontent.com/ShirkNeko/SukiSU-Ultra/main/kernel/setup.sh" | bash -s susfs-dev
|
curl -LSs "https://raw.githubusercontent.com/SukiSU-Ultra/SukiSU-Ultra/main/kernel/setup.sh" | bash -s main
|
||||||
```
|
```
|
||||||
|
|
||||||
Use the main branch
|
Using branches that support non-GKI devices (requires manual integration of susfs)
|
||||||
```
|
```
|
||||||
curl -LSs "https://raw.githubusercontent.com/ShirkNeko/KernelSU/main/kernel/setup.sh" | bash -s main
|
curl -LSs "https://raw.githubusercontent.com/SukiSU-Ultra/SukiSU-Ultra/main/kernel/setup.sh" | bash -s nongki
|
||||||
```
|
```
|
||||||
|
|
||||||
## How to use integrated susfs
|
## How to use integrated susfs
|
||||||
|
|
||||||
1. Use the susfs-dev branch directly without any patching
|
1. Use the susfs-dev branch directly without any patching
|
||||||
|
|
||||||
|
```
|
||||||
|
curl -LSs "https://raw.githubusercontent.com/SukiSU-Ultra/SukiSU-Ultra/main/kernel/setup.sh" | bash -s susfs-dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## KPM Support
|
||||||
|
|
||||||
## KPM support
|
- Based on KernelPatch, we have removed duplicates of KSU and kept only 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.
|
- We will introduce more APatch-compatible functions to ensure the integrity of KPM functionality.
|
||||||
|
|
||||||
|
We will introduce more APatch-compatible functions to ensure the completeness of KPM functionality.
|
||||||
|
|
||||||
Open source address: https://github.com/ShirkNeko/SukiSU_KernelPatch_patch
|
KPM templates: https://github.com/udochina/KPM-Build-Anywhere
|
||||||
|
|
||||||
|
> [!Note]
|
||||||
|
> 1. `CONFIG_KPM=y` needs to be added.
|
||||||
|
> 2. Non-GKI devices need to add `CONFIG_KALLSYMS=y` and `CONFIG_KALLSYMS_ALL=y` as well.
|
||||||
|
> 3. Some kernel source code below `4.19` also needs to be backport from `4.19` to the header file `set_memory.h`.
|
||||||
|
|
||||||
|
## How to do a system update to retain ROOT
|
||||||
|
- After OTA, don't reboot first, go to the manager flashing/patching kernel interface, find `GKI/non_GKI install` and select the Anykernel3 kernel zip file that needs to be flashed, select the slot that is opposite to the current running slot of the system for flashing, and then reboot to retain the GKI mode update (This method is not supported for all non-GKI devices, so please try it yourself. It is the safest way to use TWRP for non-GKI devices.)
|
||||||
|
- Or use LKM mode to install to the unused slot (after OTA).
|
||||||
|
|
||||||
|
## Compatibility Status
|
||||||
|
- KernelSU (versions prior to v1.0.0) officially supports Android GKI 2.0 devices (kernel 5.10+)
|
||||||
|
|
||||||
|
- Older kernels (4.4+) are also compatible, but the kernel must be built manually
|
||||||
|
|
||||||
|
- KernelSU can support 3.x kernels (3.4-3.18) through additional reverse ports
|
||||||
|
|
||||||
|
- Currently supports `arm64-v8a`, `armeabi-v7a (bare)` and some `X86_64`
|
||||||
|
|
||||||
KPM template address: https://github.com/udochina/KPM-Build-Anywhere
|
|
||||||
|
|
||||||
## More links
|
## More links
|
||||||
|
|
||||||
|
**If you need to submit a translation for the manager go to** https://crowdin.com/project/SukiSU-Ultra
|
||||||
|
|
||||||
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)
|
||||||
@@ -53,32 +70,37 @@ Projects compiled based on Sukisu and susfs
|
|||||||
- This method references the hook method from (https://github.com/rsuntk/KernelSU)
|
- This method references the hook method from (https://github.com/rsuntk/KernelSU)
|
||||||
|
|
||||||
1. **KPROBES hook:**
|
1. **KPROBES hook:**
|
||||||
- This method only supports GKI (5.10 - 6.x) kernels, and 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. **Manual hooks:**
|
|
||||||
- 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 Xiaomi, Redmi, Samsung, and other devices (does not include manufacturers that modified the kernel like Meizu, OnePlus, RealMe, and OPPO)
|
### Universal GKI
|
||||||
2. Use the prebuilt GKI kernel, the ones with their name ending with AnyKernel3, mentioned in the 'More Links' section, and then flash it with recoveries like TWRP
|
|
||||||
3. Generally, packages with a plain .zip suffix are universal. However, if your device has a MediaTek processor, you should use the ones with .gz suffix, and packages with .lz4 suffix are dedicated to Google devices.
|
Please **all** refer to https://kernelsu.org/zh_CN/guide/installation.html
|
||||||
|
|
||||||
|
> [!Note]
|
||||||
|
> 1. for devices with GKI 2.0 such as Xiaomi, Redmi, Samsung, etc. (excludes kernel-modified manufacturers such as Meizu, OnePlus, Zenith, and oppo)
|
||||||
|
> 2. Find the GKI build in [more links](#%E6%9B%B4%E5%A4%9A%E9%93%BE%E6%8E%A5). Find the device kernel version. Then download it and use TWRP or kernel flashing tool to flash the zip file with AnyKernel3 suffix.
|
||||||
|
> 3. The .zip archive without suffix is uncompressed, the gz suffix is the compression used by Tenguet models.
|
||||||
|
|
||||||
|
|
||||||
### OnePlus
|
### 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.
|
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]
|
> [!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.
|
> - 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.
|
> - 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.
|
> - 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.
|
||||||
@@ -88,23 +110,22 @@ Projects compiled based on Sukisu and susfs
|
|||||||
5. More customization
|
5. More customization
|
||||||
6. Support for KPM kernel modules
|
6. Support for KPM kernel modules
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
- 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.
|
- 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 under [GPL-3.0 or later](https://www.gnu.org/licenses/gpl-3.0.html) license.
|
|
||||||
|
- The images of the files `ic_launcher*` with anime character emoticons are copyrighted by [五十根大虾仁](https://space.bilibili.com/370927), the Brand Intellectual Property in the images is owned by [明风OuO](https://space.bilibili.com/274939213), and the vectorization is done by @MiRinChan. Before using these files, in addition to complying with [Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International](https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode.txt), you also need to comply with the authorization of the two authors to use these artistic contents.
|
||||||
|
|
||||||
|
- Except for the files or directories mentioned above, all other parts 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
|
- [yspbwx2010](https://github.com/yspbwx2010) Many thanks
|
||||||
- [DARKWWEE](https://github.com/DARKWWEE) Thanks for the 100 USDT Lao
|
- [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!
|
If the above list does not have your name, I will update it as soon as possible, and thanks again for your support!
|
||||||
|
|
||||||
## Contributions
|
## Contributions
|
||||||
|
|||||||
@@ -1,77 +1,104 @@
|
|||||||
# SukiSU Ultra
|
# SukiSU Ultra
|
||||||
|
|
||||||
**日本語** | [简体中文](README.md) | [English](README-en.md)
|
**日本語** | [简体中文](README.md) | [English](README-en.md) | [Türkçe](README-tr.md)
|
||||||
|
|
||||||
[KernelSU](https://github.com/tiann/KernelSU) をベースとした Android デバイスの root ソリューション
|
[KernelSU](https://github.com/tiann/KernelSU) をベースとした Android デバイスの root ソリューション
|
||||||
|
|
||||||
**試験中なビルドです!自己責任で使用してください!**<br>
|
**試験中なビルドです!自己責任で使用してください!**<br>
|
||||||
このソリューションは [KernelSU](https://github.com/tiann/KernelSU) に基づいていますが、試験中なビルドです。
|
このソリューションは [KernelSU](https://github.com/tiann/KernelSU) に基づいていますが、試験中なビルドです。
|
||||||
|
|
||||||
>
|
|
||||||
> これは非公式なフォークです。すべての権利は [@tiann](https://github.com/tiann) に帰属します。
|
> これは非公式なフォークです。すべての権利は [@tiann](https://github.com/tiann) に帰属します。
|
||||||
>
|
>
|
||||||
>ただし、将来的には KSU とは別に管理されるブランチとなる予定です。
|
> ただし、将来的には KSU とは別に管理されるブランチとなる予定です。
|
||||||
|
|
||||||
- GKI 非対応なデバイスに完全に適応 (susfs-dev と unsusfs-patched dev ブランチのみ)
|
|
||||||
|
|
||||||
## 追加方法
|
## 追加方法
|
||||||
susfs-stable または susfs-dev ブランチ (GKI 非対応デバイスに対応する統合された susfs) 使用してください。
|
|
||||||
|
|
||||||
|
メイン分岐の使用(GKI デバイス以外のビルドはサポートされていません。) (手動によるサスフ統合が必要)
|
||||||
```
|
```
|
||||||
curl -LSs "https://raw.githubusercontent.com/ShirkNeko/SukiSU-Ultra/main/kernel/setup.sh" | bash -s susfs-dev
|
curl -LSs "https://raw.githubusercontent.com/SukiSU-Ultra/SukiSU-Ultra/main/kernel/setup.sh" | bash -s main
|
||||||
```
|
```
|
||||||
|
|
||||||
メインブランチを使用する場合
|
GKI以外のデバイスをサポートするブランチを使用する (手動によるサスフ統合が必要)
|
||||||
```
|
```
|
||||||
curl -LSs "https://raw.githubusercontent.com/ShirkNeko/KernelSU/main/kernel/setup.sh" | bash -s main
|
curl -LSs "https://raw.githubusercontent.com/SukiSU-Ultra/SukiSU-Ultra/main/kernel/setup.sh" | bash -s nongki
|
||||||
```
|
```
|
||||||
|
|
||||||
## 統合された susfs の使い方
|
## 統合された susfs の使い方
|
||||||
|
|
||||||
1. パッチを当てずに susfs-dev ブランチを直接使用してください。
|
1. パッチを当てずに susfs-dev ブランチを直接使用してください。
|
||||||
|
```
|
||||||
|
curl -LSs "https://raw.githubusercontent.com/SukiSU-Ultra/SukiSU-Ultra/main/kernel/setup.sh" | bash -s susfs-dev
|
||||||
|
```
|
||||||
|
|
||||||
## KPM に対応
|
## KPM に対応
|
||||||
|
|
||||||
- KernelPatch に基づいて重複した KSU の機能を削除、KPM の対応を維持させています。
|
- KernelPatch に基づいて重複した KSU の機能を削除、KPM の対応を維持させています。
|
||||||
- KPM 機能の整合性を確保するために、APatch の互換機能を更に向上させる予定です。
|
- KPM 機能の整合性を確保するために、APatch の互換機能を更に向上させる予定です。
|
||||||
|
|
||||||
|
|
||||||
オープンソースアドレス: https://github.com/ShirkNeko/SukiSU_KernelPatch_patch
|
オープンソースアドレス: https://github.com/ShirkNeko/SukiSU_KernelPatch_patch
|
||||||
|
|
||||||
|
|
||||||
KPM テンプレートのアドレス: https://github.com/udochina/KPM-Build-Anywhere
|
KPM テンプレートのアドレス: https://github.com/udochina/KPM-Build-Anywhere
|
||||||
|
|
||||||
|
> [!Note]
|
||||||
|
> 1. `CONFIG_KPM=y` が必要である。
|
||||||
|
> 2.非 GKI デバイスには `CONFIG_KALLSYMS=y` と `CONFIG_KALLSYMS_ALL=y` も必要です。
|
||||||
|
> 3.いくつかのカーネル `4.19` およびそれ以降のソースコードでは、 `4.19` からバックポートされた `set_memory.h` ヘッダーファイルも必要です。
|
||||||
|
|
||||||
|
|
||||||
|
## ROOT を保持するシステムアップデートの方法
|
||||||
|
- OTAの後、最初に再起動せず、マネージャのフラッシュ/パッチカーネルインターフェイスに移動し、`GKI/non_GKI 取り付け`を見つけ、フラッシュする必要があるAnykernel3カーネルzipファイルを選択し、フラッシュするためにシステムの現在の実行スロットと反対のスロットを選択し、GKIモードアップデートを保持するために再起動します(この方法は、現時点ではすべてのnon_GKIデバイスでサポートされていませんので、各自でお試しください。 (この方法は、すべての非GKIデバイスでサポートされていませんので、ご自身でお試しください)。
|
||||||
|
- または、LKMモードを使用して未使用のスロットにインストールします(OTA後)。
|
||||||
|
|
||||||
|
## 互換性ステータス
|
||||||
|
- KernelSU(v1.0.0より前のバージョン)はAndroid GKI 2.0デバイス(カーネル5.10以上)を公式にサポートしています。
|
||||||
|
|
||||||
|
- 古いカーネル(4.4+)も互換性がありますが、カーネルは手動でビルドする必要があります。
|
||||||
|
|
||||||
|
- KernelSU は追加のリバースポートを通じて 3.x カーネル (3.4-3.18) をサポートしています。
|
||||||
|
|
||||||
|
- 現在は `arm64-v8a`、`armeabi-v7a (bare)`、いくつかの `X86_64` をサポートしています。
|
||||||
|
|
||||||
## その他のリンク
|
## その他のリンク
|
||||||
SukiSU と susfs をベースにコンパイルされたプロジェクトです。
|
|
||||||
|
**監督に翻訳を提出する必要がある場合は、https://crowdin.com/project/SukiSU-Ultra。
|
||||||
|
|
||||||
- [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)
|
||||||
|
|
||||||
## フックの方式
|
## フックの方式
|
||||||
|
|
||||||
- この方式は (https://github.com/rsuntk/KernelSU) のフック方式を参照してください。
|
- この方式は (https://github.com/rsuntk/KernelSU) のフック方式を参照してください。
|
||||||
|
|
||||||
1. **KPROBES フック:**
|
1. **KPROBES フック:**
|
||||||
- この方式は GKI (5.10 - 6.x) のカーネルのみに対応しています。GKI 以外のカーネルは手動でフックを使用する必要があります。
|
|
||||||
- 読み込み可能なカーネルモジュールの場合 (LKM)
|
- 読み込み可能なカーネルモジュールの場合 (LKM)
|
||||||
- GKI カーネルのデフォルトとなるフック方式
|
- GKI カーネルのデフォルトとなるフック方式
|
||||||
- `CONFIG_KPROBES=y` が必要です。
|
- `CONFIG_KPROBES=y` が必要です
|
||||||
|
|
||||||
2. **手動でフック:**
|
2. **手動でフック:**
|
||||||
- GKI (5.10 - 6.x) のカーネルの場合、カーネルの defconfig に `CONFIG_KSU_MANUAL_HOOK=y` を追加して `#ifdef CONFIG_KSU` ではなく `#ifdef CONFIG_KSU_MANUAL_HOOK` を使用して 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 デバイスでは手動の VFS フック `new_hook.patch` パッチは不要です。
|
- 非 GKI カーネル用のデフォルトフッキングメソッド
|
||||||
|
- `CONFIG_KSU_MANUAL_HOOK=y` が必要です
|
||||||
|
|
||||||
## 使い方
|
## 使い方
|
||||||
### GKI
|
|
||||||
1. Xiaomi、Redmi、Samsung などのデバイス (Meizu、OnePlus、Realme、OPPO などのカーネルを変更したメーカー以外)
|
|
||||||
2. `その他のリンク`の項目で言及されているカーネル名が、AnyKernel3 で終わるビルド済みの GKI カーネルを TWRP などのリカバリーでフラッシュします。
|
|
||||||
3. 一般的な .zip の接頭辞を持つパッケージは汎用的になります。ただし、デバイスに MediaTek 製の SoC が搭載されている場合は、.gz の接頭辞を持つパッケージを使用する必要があります。その他に .lz4 の接頭辞を持つパッケージは Google 製デバイス専用です。
|
|
||||||
|
|
||||||
|
### ユニバーサルGKI
|
||||||
|
|
||||||
|
https://kernelsu.org/zh_CN/guide/installation.html をご参照ください。
|
||||||
|
|
||||||
|
> [!Note]
|
||||||
|
> 1.Xiaomi、Redmi、Samsung などの GKI 2.0 を搭載したデバイス用 (Meizu、Yiga、Zenith、oppo などのマジックカーネルを搭載したメーカーは除く)。
|
||||||
|
> 2. [more links](#%E6%9B%B4%E5%A4%9A%E9%93%BE%E6%8E%A5) で GKI ビルドを検索します。 デバイスのカーネルバージョンを検索します。 次に、それをダウンロードし、TWRPまたはカーネルフラッシングツールを使用して、AnyKernel3の接尾辞が付いたzipファイルをフラッシュします。
|
||||||
|
> 接尾辞なしの.zipアーカイブは非圧縮で、接尾辞gzはTenguetモデルで使用されている圧縮方法です。
|
||||||
### OnePlus
|
### OnePlus
|
||||||
|
|
||||||
1. `その他のリンク`の項目に記載されているリンクを開き、デバイス情報を使用してカスタマイズされたカーネルをビルドし、AnyKernel3 の接頭辞を持つ .zip ファイルをフラッシュします。
|
1. `その他のリンク`の項目に記載されているリンクを開き、デバイス情報を使用してカスタマイズされたカーネルをビルドし、AnyKernel3 の接頭辞を持つ .zip ファイルをフラッシュします。
|
||||||
|
|
||||||
> [!Note]
|
> [!Note]
|
||||||
> - 5.10、5.15、6.1、6.6 などのカーネルバージョンの最初の 2 文字のみを入力する必要があります。
|
> - 5.10、5.15、6.1、6.6 などのカーネルバージョンの最初の 2 文字のみを入力する必要があります。
|
||||||
> - SoC のコードネームは自分で検索してください。通常は、数字がなく英語表記のみです。
|
> - SoC のコードネームは自分で検索してください。通常は、数字がなく英語表記のみです。
|
||||||
> - ブランチと構成ファイルは、OnePlus オープンソースカーネルリポジトリから見つけることができます。
|
> - ブランチと構成ファイルは、OnePlus オープンソースカーネルリポジトリから見つけることができます。
|
||||||
|
|
||||||
|
|
||||||
## 機能
|
## 機能
|
||||||
|
|
||||||
1. カーネルベースな `su` および root アクセスの管理。
|
1. カーネルベースな `su` および root アクセスの管理。
|
||||||
@@ -81,23 +108,20 @@ SukiSU と susfs をベースにコンパイルされたプロジェクトです
|
|||||||
5. その他のカスタマイズ
|
5. その他のカスタマイズ
|
||||||
6. KPM カーネルモジュールに対応
|
6. KPM カーネルモジュールに対応
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## ライセンス
|
## ライセンス
|
||||||
|
|
||||||
- “kernel” ディレクトリ内のファイルは [GPL-2.0](https://www.gnu.org/licenses/old-licenses/gpl-2.0.ja.html) のみライセンス下にあります。
|
- `kernel` ディレクトリ以下のファイルは[GPL-2.0-only](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html)です。
|
||||||
- “kernel” ディレクトリを除くその他すべての部分は [GPL-3.0 またはそれ以降](https://www.gnu.org/licenses/gpl-3.0.html) のライセンス下にあります。
|
- アニメキャラクターの絵文字を含むファイル `ic_launcher*` の画像は[五十根大虾仁](https://space.bilibili.com/370927)が著作権を所有しており、画像内のブランド知的財産権は[明风OuO](https://space.bilibili.com/274939213)が所有しています。ベクトル化は @MiRinChan が行っています。これらのファイルを使用する前に、[Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International](https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode.txt)に準拠することに加えて、これらの芸術コンテンツを使用するには、2人の著者の許可にも従う必要があります。
|
||||||
|
- 上記のファイルまたはディレクトリを除き、その他のすべての部分は[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) どうもありがとう。
|
- [yspbwx2010](https://github.com/yspbwx2010) どうもありがとう。
|
||||||
- [DARKWWEE](https://github.com/DARKWWEE) ラオウ100USDTありがとう!
|
- [DARKWWEE](https://github.com/DARKWWEE) ラオウ100USDTありがとう!
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
上記の一覧にあなたの名前がない場合は、できるだけ早急に更新しますので再度ご支援をお願いします。
|
上記の一覧にあなたの名前がない場合は、できるだけ早急に更新しますので再度ご支援をお願いします。
|
||||||
|
|
||||||
## 貢献者
|
## 貢献者
|
||||||
|
|||||||
151
docs/README-tr.md
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
# SukiSU Ultra
|
||||||
|
|
||||||
|
**Türkçe** | [简体中文](README.md) | [English](README-en.md) | [日本語](README-ja.md)
|
||||||
|
|
||||||
|
[KernelSU](https://github.com/tiann/KernelSU) tabanlı Android cihaz root çözümü
|
||||||
|
|
||||||
|
**Deneysel! Kullanım riski size aittir!**
|
||||||
|
|
||||||
|
> Bu resmi olmayan bir daldır, tüm hakları saklıdır [@tiann](https://github.com/tiann)
|
||||||
|
>
|
||||||
|
> Ancak, gelecekte ayrı bir KSU dalı olarak devam edeceğiz
|
||||||
|
|
||||||
|
## Nasıl Eklenir
|
||||||
|
|
||||||
|
Çekirdek kaynak kodunun kök dizininde aşağıdaki komutları çalıştırın:
|
||||||
|
|
||||||
|
Ana dalı kullanın (GKI olmayan cihazlar için desteklenmez)
|
||||||
|
|
||||||
|
```
|
||||||
|
curl -LSs "https://raw.githubusercontent.com/SukiSU-Ultra/SukiSU-Ultra/main/kernel/setup.sh" | bash -s main
|
||||||
|
```
|
||||||
|
|
||||||
|
GKI olmayan cihazları destekleyen dalı kullanın
|
||||||
|
|
||||||
|
```
|
||||||
|
curl -LSs "https://raw.githubusercontent.com/SukiSU-Ultra/SukiSU-Ultra/main/kernel/setup.sh" | bash -s nongki
|
||||||
|
```
|
||||||
|
|
||||||
|
## susfs Nasıl Entegre Edilir
|
||||||
|
|
||||||
|
1. Doğrudan susfs-stable veya susfs-dev dalını kullanın, susfs entegrasyonuna gerek yok
|
||||||
|
|
||||||
|
```
|
||||||
|
curl -LSs "https://raw.githubusercontent.com/SukiSU-Ultra/SukiSU-Ultra/main/kernel/setup.sh" | bash -s susfs-dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## Kanca Yöntemleri
|
||||||
|
|
||||||
|
- Bu bölüm [rsuntk\'nin kanca yöntemlerinden](https://github.com/rsuntk/KernelSU) alıntılanmıştır
|
||||||
|
|
||||||
|
1. **KPROBES Kancası:**
|
||||||
|
|
||||||
|
- Yüklenebilir çekirdek modülleri (LKM) için kullanılır
|
||||||
|
- GKI 2.0 çekirdeğinin varsayılan kanca yöntemi
|
||||||
|
- `CONFIG_KPROBES=y` gerektirir
|
||||||
|
|
||||||
|
2. **Manuel Kanca:**
|
||||||
|
- Standart KernelSU kancası: https://kernelsu.org/guide/how-to-integrate-for-non-gki.html#manually-modify-the-kernel-source
|
||||||
|
- backslashxx\'nin syscall manuel kancası: https://github.com/backslashxx/KernelSU/issues/5
|
||||||
|
- GKI olmayan çekirdeğin varsayılan kanca yöntemi
|
||||||
|
- `CONFIG_KSU_MANUAL_HOOK=y` gerektirir
|
||||||
|
|
||||||
|
## KPM Desteği
|
||||||
|
|
||||||
|
- KernelPatch tabanlı olarak KSU ile çakışan işlevleri kaldırdık ve yalnızca KPM desteğini koruduk
|
||||||
|
- APatch ile daha fazla uyumlu fonksiyon ekleyerek KPM işlevlerinin bütünlüğünü sağlayacağız
|
||||||
|
|
||||||
|
Kaynak kodu: https://github.com/ShirkNeko/SukiSU_KernelPatch_patch
|
||||||
|
|
||||||
|
KPM şablonu: https://github.com/udochina/KPM-Build-Anywhere
|
||||||
|
|
||||||
|
> [!Note]
|
||||||
|
>
|
||||||
|
> 1. `CONFIG_KPM=y` gerektirir
|
||||||
|
> 2. GKI olmayan cihazlar ayrıca `CONFIG_KALLSYMS=y` ve `CONFIG_KALLSYMS_ALL=y` gerektirir
|
||||||
|
> 3. Bazı çekirdek `4.19` altı kaynak kodları, `4.19`dan geri taşınan başlık dosyası `set_memory.h` gerektirir
|
||||||
|
|
||||||
|
## Sistem Güncellemesini Yaparak ROOT\'u Koruma
|
||||||
|
|
||||||
|
- OTA\'dan sonra hemen yeniden başlatmayın, yöneticiye girin ve çekirdek yazma/onarma arayüzüne gidin, `GKI/non_GKI yükleme` seçeneğini bulun ve Anykernel3 çekirdek sıkıştırma dosyasını seçin, şu anda sistemin çalıştığı yuva ile zıt yuvaya yazın ve yeniden başlatın, böylece GKI modu güncellemesini koruyabilirsiniz (şu anda tüm GKI olmayan cihazlar bu yöntemi desteklemiyor, lütfen kendiniz deneyin. GKI olmayan cihazlar için TWRP kullanmak en güvenlidir)
|
||||||
|
- Veya kullanılmayan yuvaya LKM modunu kullanarak yükleyin (OTA\'dan sonra)
|
||||||
|
|
||||||
|
## Uyumluluk Durumu
|
||||||
|
|
||||||
|
- KernelSU (v1.0.0 öncesi sürümler) resmi olarak Android GKI 2.0 cihazlarını destekler (çekirdek 5.10+)
|
||||||
|
|
||||||
|
- Eski çekirdekler (4.4+) de uyumludur, ancak çekirdeği manuel olarak oluşturmanız gerekir
|
||||||
|
|
||||||
|
- Daha fazla geri taşımayla KernelSU, 3.x çekirdeğini (3.4-3.18) destekleyebilir
|
||||||
|
|
||||||
|
- Şu anda `arm64-v8a`, `armeabi-v7a (bare)` ve bazı `X86_64` desteklenmektedir
|
||||||
|
|
||||||
|
## Daha Fazla Bağlantı
|
||||||
|
|
||||||
|
SukiSU ve susfs tabanlı derlenen projeler
|
||||||
|
|
||||||
|
- [GKI](https://github.com/ShirkNeko/GKI_KernelSU_SUSFS)
|
||||||
|
- [OnePlus](https://github.com/ShirkNeko/Action_OnePlus_MKSU_SUSFS)
|
||||||
|
|
||||||
|
## Kullanım Yöntemi
|
||||||
|
|
||||||
|
### Evrensel GKI
|
||||||
|
|
||||||
|
Lütfen **tümünü** https://kernelsu.org/zh_CN/guide/installation.html adresinden inceleyin
|
||||||
|
|
||||||
|
> [!Note]
|
||||||
|
>
|
||||||
|
> 1. Xiaomi, Redmi, Samsung gibi GKI 2.0 cihazlar için uygundur (Meizu, OnePlus, Realme ve Oppo gibi değiştirilmiş çekirdekli üreticiler hariç)
|
||||||
|
> 2. [Daha fazla bağlantı](#daha-fazla-bağlantı) bölümündeki GKI tabanlı projeleri bulun. Cihaz çekirdek sürümünü bulun. Ardından indirin ve TWRP veya çekirdek yazma aracı kullanarak AnyKernel3 soneki olan sıkıştırılmış paketi yazın
|
||||||
|
> 3. Genellikle sonek olmayan .zip sıkıştırılmış paketler sıkıştırılmamıştır, gz soneki olanlar ise Dimensity modelleri için kullanılan sıkıştırma yöntemidir
|
||||||
|
|
||||||
|
### OnePlus
|
||||||
|
|
||||||
|
1. Daha fazla bağlantı bölümündeki OnePlus projesini bulun ve kendiniz doldurun, ardından bulut derleme yapın ve AnyKernel3 soneki olan sıkıştırılmış paketi yazın
|
||||||
|
|
||||||
|
> [!Note]
|
||||||
|
>
|
||||||
|
> - Çekirdek sürümü için yalnızca ilk iki haneyi doldurmanız yeterlidir, örneğin 5.10, 5.15, 6.1, 6.6
|
||||||
|
> - İşlemci kod adını kendiniz arayın, genellikle tamamen İngilizce ve sayı içermeden oluşur
|
||||||
|
> - Dal ve yapılandırma dosyasını kendiniz OnePlus çekirdek kaynak kodundan doldurun
|
||||||
|
|
||||||
|
## Özellikler
|
||||||
|
|
||||||
|
1. Çekirdek tabanlı `su` ve root erişim yönetimi
|
||||||
|
2. 5ec1cff\'nin [Magic Mount](https://github.com/5ec1cff/KernelSU) tabanlı modül sistemi
|
||||||
|
3. [App Profile](https://kernelsu.org/guide/app-profile.html): root yetkilerini kafeste kilitleyin
|
||||||
|
4. GKI 2.0 olmayan çekirdekler için desteğin geri getirilmesi
|
||||||
|
5. Daha fazla özelleştirme özelliği
|
||||||
|
6. KPM çekirdek modülleri için destek
|
||||||
|
|
||||||
|
## Lisans
|
||||||
|
|
||||||
|
- `kernel` dizinindeki dosyalar [GPL-2.0-only](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html) lisansı altındadır.
|
||||||
|
- Anime karakter ifadeleri içeren `ic_launcher*` dosyalarının görüntüleri [五十根大虾仁](https://space.bilibili.com/370927) tarafından telif hakkıyla korunmaktadır, görüntülerdeki Marka Fikri Mülkiyeti [明风 OuO](https://space.bilibili.com/274939213)'ye aittir ve vektörleştirme @MiRinChan tarafından yapılmıştır. Bu dosyaları kullanmadan önce, [Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International](https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode.txt) ile uyumlu olmanın yanı sıra, bu sanatsal içerikleri kullanmak için iki yazarın yetkilendirmesine de uymanız gerekir.
|
||||||
|
- Yukarıda belirtilen dosyalar veya dizinler hariç, diğer tüm parçalar [GPL-3.0 veya üzeri](https://www.gnu.org/licenses/gpl-3.0.html)'dir.
|
||||||
|
|
||||||
|
## Afdian Bağlantısı
|
||||||
|
|
||||||
|
- https://afdian.com/a/shirkneko
|
||||||
|
|
||||||
|
## Sponsor Listesi
|
||||||
|
|
||||||
|
- [Ktouls](https://github.com/Ktouls) Bana sağladığınız destek için çok teşekkür ederim
|
||||||
|
- [zaoqi123](https://github.com/zaoqi123) Bana sütlü çay ısmarlamanız da güzel
|
||||||
|
- [wswzgdg](https://github.com/wswzgdg) Bu projeye olan desteğiniz için çok teşekkür ederim
|
||||||
|
- [yspbwx2010](https://github.com/yspbwx2010) Çok teşekkür ederim
|
||||||
|
- [DARKWWEE](https://github.com/DARKWWEE) 100 USDT için teşekkürler
|
||||||
|
|
||||||
|
Eğer yukarıdaki listede adınız yoksa, zamanında güncelleyeceğim, herkese tekrar teşekkür ederim
|
||||||
|
|
||||||
|
## Katkıda Bulunanlar
|
||||||
|
|
||||||
|
- [KernelSU](https://github.com/tiann/KernelSU): Orijinal proje
|
||||||
|
- [MKSU](https://github.com/5ec1cff/KernelSU): Kullanılan proje
|
||||||
|
- [RKSU](https://github.com/rsuntk/KernelsU): GKI olmayan cihazlar için destek sağlayan proje
|
||||||
|
- [susfs4ksu](https://gitlab.com/simonpunk/susfs4ksu): Kullanılan susfs dosya sistemi
|
||||||
|
- [kernel-assisted-superuser](https://git.zx2c4.com/kernel-assisted-superuser/about/): KernelSU fikri
|
||||||
|
- [Magisk](https://github.com/topjohnwu/Magisk): Güçlü root aracı
|
||||||
|
- [genuine](https://github.com/brevent/genuine/): APK v2 imza doğrulama
|
||||||
|
- [Diamorphine](https://github.com/m0nad/Diamorphine): Bazı rootkit becerileri
|
||||||
|
- [KernelPatch](https://github.com/bmax121/KernelPatch): KernelPatch, APatch\'in çekirdek modüllerini uygulamak için kritik bir parçadır
|
||||||
106
docs/README.md
@@ -1,86 +1,116 @@
|
|||||||
# SukiSU Ultra
|
# SukiSU Ultra
|
||||||
|
|
||||||
**简体中文** | [English](README-en.md) | [日本語](README-ja.md)
|
**简体中文** | [English](README-en.md) | [日本語](README-ja.md) | [Türkçe](README-tr.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分支
|
|
||||||
>
|
>
|
||||||
|
> 但是,我们将会在未来成为一个单独维护的 KSU 分支
|
||||||
|
|
||||||
## 如何添加
|
## 如何添加
|
||||||
|
|
||||||
在内核源码的根目录下执行以下命令:
|
在内核源码的根目录下执行以下命令:
|
||||||
|
|
||||||
使用 susfs-dev 分支(已集成susfs,带非GKI设备的支持)
|
使用 main 分支 (不支持非 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/SukiSU-Ultra/SukiSU-Ultra/main/kernel/setup.sh" | bash -s main
|
||||||
```
|
```
|
||||||
|
|
||||||
|
使用支持非 GKI 设备的分支 (需要手动集成 susfs)
|
||||||
|
|
||||||
使用 main 分支
|
|
||||||
```
|
```
|
||||||
curl -LSs "https://raw.githubusercontent.com/ShirkNeko/SukiSU-Ultra/main/kernel/setup.sh" | bash -s main
|
curl -LSs "https://raw.githubusercontent.com/SukiSU-Ultra/SukiSU-Ultra/main/kernel/setup.sh" | bash -s nongki
|
||||||
```
|
```
|
||||||
|
|
||||||
## 如何集成 susfs
|
## 如何集成 susfs
|
||||||
|
|
||||||
1. 直接使用 susfs-stable 或者 susfs-dev 分支,不需要再集成 susfs
|
1. 直接使用 susfs-stable 或者 susfs-dev 分支,不需要再集成 susfs
|
||||||
|
|
||||||
|
```
|
||||||
|
curl -LSs "https://raw.githubusercontent.com/SukiSU-Ultra/SukiSU-Ultra/main/kernel/setup.sh" | bash -s susfs-dev
|
||||||
|
```
|
||||||
|
|
||||||
## 钩子方法
|
## 钩子方法
|
||||||
|
|
||||||
- 此部分引用自 [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
|
- 非 GKI 内核的默认挂钩方法
|
||||||
- 部分手动集成 KPROBES 的非 GKI 2.0 设备不需要手动 VFS 钩子 `new_hook.patch` 补丁
|
- 需要 `CONFIG_KSU_MANUAL_HOOK=y`
|
||||||
|
|
||||||
|
## KPM 支持
|
||||||
|
|
||||||
## KPM支持
|
- 我们基于 KernelPatch 去掉了和 KSU 重复的功能,仅保留了 KPM 支持
|
||||||
|
- 我们将会引入更多的兼容 APatch 的函数来确保 KPM 功能的完整性
|
||||||
- 我们基于KernelPatch去掉了和KSU重复的功能,保留了KPM支持
|
|
||||||
- 我们将会引入更多的兼容APatch的函数来确保KPM功能的完整性
|
|
||||||
|
|
||||||
|
|
||||||
开源地址: https://github.com/ShirkNeko/SukiSU_KernelPatch_patch
|
开源地址: https://github.com/ShirkNeko/SukiSU_KernelPatch_patch
|
||||||
|
|
||||||
|
KPM 模板地址: https://github.com/udochina/KPM-Build-Anywhere
|
||||||
|
|
||||||
KPM模板地址: https://github.com/udochina/KPM-Build-Anywhere
|
> [!Note]
|
||||||
|
>
|
||||||
|
> 1. 需要 `CONFIG_KPM=y`
|
||||||
|
> 2. 非 GKI 设备还需要 `CONFIG_KALLSYMS=y` 和 `CONFIG_KALLSYMS_ALL=y`
|
||||||
|
> 3. 部分内核 `4.19` 以下源码还需要从 `4.19` 向后移植头文件 `set_memory.h`
|
||||||
|
|
||||||
|
## 如何进行系统更新保留 ROOT
|
||||||
|
|
||||||
|
- OTA 后先不要重启,进入管理器刷写/修补内核界面,找到 `GKI/non_GKI安装` 选择需要刷写的 Anykernel3 内核压缩文件,选择与现在系统运行槽位相反的槽位进行刷写并重启即可保留 GKI 模式更新(暂不支持所有非 GKI 设备使用这种方法,请自行尝试。非 GKI 设备使用 TWRP 刷写是最稳妥的)
|
||||||
|
- 或者使用 LKM 模式的安装到未使用的槽位(OTA 后)
|
||||||
|
|
||||||
|
## 兼容状态
|
||||||
|
|
||||||
|
- KernelSU(v1.0.0 之前版本)正式支持 Android GKI 2.0 设备(内核 5.10+)
|
||||||
|
|
||||||
|
- 旧内核(4.4+)也兼容,但必须手动构建内核
|
||||||
|
|
||||||
|
- 通过更多的反向移植,KernelSU 可以支持 3.x 内核(3.4-3.18)
|
||||||
|
|
||||||
|
- 目前支持 `arm64-v8a` ,`armeabi-v7a (bare)` 和部分 `X86_64`
|
||||||
|
|
||||||
## 更多链接
|
## 更多链接
|
||||||
|
|
||||||
|
**如果你需要为管理器提交翻译请前往** https://crowdin.com/project/SukiSU-Ultra
|
||||||
|
|
||||||
基于 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]
|
> [!Note]
|
||||||
|
>
|
||||||
> - 内核版本只需要填写前两位即可,如 5.10,5.15,6.1,6.6
|
> - 内核版本只需要填写前两位即可,如 5.10,5.15,6.1,6.6
|
||||||
> - 处理器代号请自行搜索,一般为全英文不带数字的代号
|
> - 处理器代号请自行搜索,一般为全英文不带数字的代号
|
||||||
> - 分支和配置文件请自行到一加内核开源地址进行填写
|
> - 分支和配置文件请自行到一加内核开源地址进行填写
|
||||||
|
|
||||||
|
|
||||||
## 特点
|
## 特点
|
||||||
|
|
||||||
1. 基于内核的 `su` 和 root 访问管理
|
1. 基于内核的 `su` 和 root 访问管理
|
||||||
@@ -88,34 +118,36 @@ KPM模板地址: https://github.com/udochina/KPM-Build-Anywhere
|
|||||||
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 内核的支持
|
4. 恢复对非 GKI 2.0 内核的支持
|
||||||
5. 更多自定义功能
|
5. 更多自定义功能
|
||||||
6. 对KPM内核模块的支持
|
6. 对 KPM 内核模块的支持
|
||||||
|
|
||||||
|
|
||||||
## 许可证
|
## 许可证
|
||||||
|
|
||||||
- `kernel` 目录下的文件是 [GPL-2.0-only](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html)。
|
- `kernel` 目录下的文件是 [GPL-2.0-only](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html)。
|
||||||
- 除 `kernel` 目录外,所有其他部分均为 [GPL-3.0 或更高版本](https://www.gnu.org/licenses/gpl-3.0.html)。
|
- 有动漫人物图片表情包的这些文件 `ic_launcher*` 的图像版权为[五十根大虾仁](https://space.bilibili.com/370927)所有,图像中的 Brand Intellectual Property 由[明风OuO](https://space.bilibili.com/274939213)所有,矢量化由 @MiRinChan 完成,在使用这些文件之前,除了必须遵守 [Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International](https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode.txt) 以外,还需要遵守向前两者索要使用这些艺术内容的授权。
|
||||||
|
- 除了以上所述的文件或目录外,所有其他部分均为 [GPL-3.0 或更高版本](https://www.gnu.org/licenses/gpl-3.0.html)。
|
||||||
|
|
||||||
|
## 爱发电链接
|
||||||
|
|
||||||
|
- https://afdian.com/a/shirkneko
|
||||||
|
|
||||||
## 赞助名单
|
## 赞助名单
|
||||||
|
|
||||||
- [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) 非常感谢
|
- [yspbwx2010](https://github.com/yspbwx2010) 非常感谢
|
||||||
- [DARKWWEE](https://github.com/DARKWWEE) 感谢老哥的 100 USDT
|
- [DARKWWEE](https://github.com/DARKWWEE) 感谢老哥的 100 USDT
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
如果以上名单没有你的名称,我会及时更新,再次感谢大家的支持
|
如果以上名单没有你的名称,我会及时更新,再次感谢大家的支持
|
||||||
|
|
||||||
## 贡献
|
## 贡献
|
||||||
|
|
||||||
- [KernelSU](https://github.com/tiann/KernelSU):原始项目
|
- [KernelSU](https://github.com/tiann/KernelSU):原始项目
|
||||||
- [MKSU](https://github.com/5ec1cff/KernelSU):使用的项目
|
- [MKSU](https://github.com/5ec1cff/KernelSU):使用的项目
|
||||||
- [RKSU](https://github.com/rsuntk/KernelsU):使用该项目的 kernel 对非GKI设备重新进行支持
|
- [RKSU](https://github.com/rsuntk/KernelsU):使用该项目的 kernel 对非 GKI 设备重新进行支持
|
||||||
- [susfs4ksu](https://gitlab.com/simonpunk/susfs4ksu):使用的 susfs 文件系统
|
- [susfs4ksu](https://gitlab.com/simonpunk/susfs4ksu):使用的 susfs 文件系统
|
||||||
- [kernel-assisted-superuser](https://git.zx2c4.com/kernel-assisted-superuser/about/):KernelSU 的构想
|
- [kernel-assisted-superuser](https://git.zx2c4.com/kernel-assisted-superuser/about/):KernelSU 的构想
|
||||||
- [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实现内核模块的关键部分
|
- [KernelPatch](https://github.com/bmax121/KernelPatch): KernelPatch 是 APatch 实现内核模块的关键部分
|
||||||
|
|||||||
@@ -16,20 +16,13 @@ config KSU_DEBUG
|
|||||||
help
|
help
|
||||||
Enable KernelSU debug mode.
|
Enable KernelSU debug mode.
|
||||||
|
|
||||||
config KSU_HOOK
|
|
||||||
bool "Enable KernelSU Hook"
|
|
||||||
default n
|
|
||||||
help
|
|
||||||
This option enables the KernelSU Hook feature. If enabled, it will
|
|
||||||
override the kernel version check and enable the hook functionality.
|
|
||||||
|
|
||||||
config KPM
|
config KPM
|
||||||
bool "Enable SukiSU KPM"
|
bool "Enable SukiSU KPM"
|
||||||
|
depends on KSU && 64BIT
|
||||||
default n
|
default n
|
||||||
help
|
help
|
||||||
Enabling this option will activate the KPM feature of SukiSU.
|
Enabling this option will activate the KPM feature of SukiSU.
|
||||||
This option is suitable for scenarios where you need to force KPM to be enabled.
|
This option is suitable for scenarios where you need to force KPM to be enabled.
|
||||||
but it may affect system stability.
|
but it may affect system stability.
|
||||||
|
|
||||||
|
|
||||||
endmenu
|
endmenu
|
||||||
|
|||||||
@@ -226,6 +226,7 @@ int ksu_handle_rename(struct dentry *old_dentry, struct dentry *new_dentry)
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef CONFIG_EXT4_FS
|
||||||
static void nuke_ext4_sysfs() {
|
static void nuke_ext4_sysfs() {
|
||||||
struct path path;
|
struct path path;
|
||||||
int err = kern_path("/data/adb/modules", 0, &path);
|
int err = kern_path("/data/adb/modules", 0, &path);
|
||||||
@@ -243,6 +244,9 @@ static void nuke_ext4_sysfs() {
|
|||||||
|
|
||||||
ext4_unregister_sysfs(sb);
|
ext4_unregister_sysfs(sb);
|
||||||
}
|
}
|
||||||
|
#else
|
||||||
|
static inline void nuke_ext4_sysfs() { }
|
||||||
|
#endif
|
||||||
|
|
||||||
int ksu_handle_prctl(int option, unsigned long arg2, unsigned long arg3,
|
int ksu_handle_prctl(int option, unsigned long arg2, unsigned long arg3,
|
||||||
unsigned long arg4, unsigned long arg5)
|
unsigned long arg4, unsigned long arg5)
|
||||||
@@ -425,6 +429,13 @@ int ksu_handle_prctl(int option, unsigned long arg2, unsigned long arg3,
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
if (arg2 == CMD_ENABLE_KPM) {
|
||||||
|
bool KPM_Enabled = IS_ENABLED(CONFIG_KPM);
|
||||||
|
if (copy_to_user((void __user *)arg3, &KPM_Enabled, sizeof(KPM_Enabled)))
|
||||||
|
pr_info("KPM: copy_to_user() failed\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
// all other cmds are for 'root manager'
|
// all other cmds are for 'root manager'
|
||||||
if (!from_manager) {
|
if (!from_manager) {
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
@@ -167,7 +167,11 @@ DYNAMIC_STRUCT_BEGIN(task_struct)
|
|||||||
DEFINE_MEMBER(task_struct, group_leader)
|
DEFINE_MEMBER(task_struct, group_leader)
|
||||||
DEFINE_MEMBER(task_struct, mm)
|
DEFINE_MEMBER(task_struct, mm)
|
||||||
DEFINE_MEMBER(task_struct, active_mm)
|
DEFINE_MEMBER(task_struct, active_mm)
|
||||||
|
#if LINUX_VERSION_CODE < KERNEL_VERSION(4, 19, 0)
|
||||||
|
DEFINE_MEMBER(task_struct, pids[PIDTYPE_PID].pid)
|
||||||
|
#else
|
||||||
DEFINE_MEMBER(task_struct, thread_pid)
|
DEFINE_MEMBER(task_struct, thread_pid)
|
||||||
|
#endif
|
||||||
DEFINE_MEMBER(task_struct, files)
|
DEFINE_MEMBER(task_struct, files)
|
||||||
DEFINE_MEMBER(task_struct, seccomp)
|
DEFINE_MEMBER(task_struct, seccomp)
|
||||||
#ifdef CONFIG_THREAD_INFO_IN_TASK
|
#ifdef CONFIG_THREAD_INFO_IN_TASK
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
#define CMD_UID_SHOULD_UMOUNT 13
|
#define CMD_UID_SHOULD_UMOUNT 13
|
||||||
#define CMD_IS_SU_ENABLED 14
|
#define CMD_IS_SU_ENABLED 14
|
||||||
#define CMD_ENABLE_SU 15
|
#define CMD_ENABLE_SU 15
|
||||||
|
#define CMD_ENABLE_KPM 100
|
||||||
|
|
||||||
#define EVENT_POST_FS_DATA 1
|
#define EVENT_POST_FS_DATA 1
|
||||||
#define EVENT_BOOT_COMPLETED 2
|
#define EVENT_BOOT_COMPLETED 2
|
||||||
|
|||||||
@@ -63,6 +63,10 @@ u32 ksu_devpts_sid;
|
|||||||
// Detect whether it is on or not
|
// Detect whether it is on or not
|
||||||
static bool is_boot_phase = true;
|
static bool is_boot_phase = true;
|
||||||
|
|
||||||
|
#ifdef CONFIG_COMPAT
|
||||||
|
bool ksu_is_compat __read_mostly = false;
|
||||||
|
#endif
|
||||||
|
|
||||||
void on_post_fs_data(void)
|
void on_post_fs_data(void)
|
||||||
{
|
{
|
||||||
static bool done = false;
|
static bool done = false;
|
||||||
@@ -107,6 +111,7 @@ static const char __user *get_user_arg_ptr(struct user_arg_ptr argv, int nr)
|
|||||||
if (get_user(compat, argv.ptr.compat + nr))
|
if (get_user(compat, argv.ptr.compat + nr))
|
||||||
return ERR_PTR(-EFAULT);
|
return ERR_PTR(-EFAULT);
|
||||||
|
|
||||||
|
ksu_is_compat = true;
|
||||||
return compat_ptr(compat);
|
return compat_ptr(compat);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -137,17 +137,45 @@ void apply_kernelsu_rules()
|
|||||||
#define CMD_TYPE_CHANGE 8
|
#define CMD_TYPE_CHANGE 8
|
||||||
#define CMD_GENFSCON 9
|
#define CMD_GENFSCON 9
|
||||||
|
|
||||||
|
#ifdef CONFIG_64BIT
|
||||||
struct sepol_data {
|
struct sepol_data {
|
||||||
u32 cmd;
|
u32 cmd;
|
||||||
u32 subcmd;
|
u32 subcmd;
|
||||||
char __user *sepol1;
|
u64 field_sepol1;
|
||||||
char __user *sepol2;
|
u64 field_sepol2;
|
||||||
char __user *sepol3;
|
u64 field_sepol3;
|
||||||
char __user *sepol4;
|
u64 field_sepol4;
|
||||||
char __user *sepol5;
|
u64 field_sepol5;
|
||||||
char __user *sepol6;
|
u64 field_sepol6;
|
||||||
char __user *sepol7;
|
u64 field_sepol7;
|
||||||
};
|
};
|
||||||
|
#ifdef CONFIG_COMPAT
|
||||||
|
extern bool ksu_is_compat __read_mostly;
|
||||||
|
struct sepol_compat_data {
|
||||||
|
u32 cmd;
|
||||||
|
u32 subcmd;
|
||||||
|
u32 field_sepol1;
|
||||||
|
u32 field_sepol2;
|
||||||
|
u32 field_sepol3;
|
||||||
|
u32 field_sepol4;
|
||||||
|
u32 field_sepol5;
|
||||||
|
u32 field_sepol6;
|
||||||
|
u32 field_sepol7;
|
||||||
|
};
|
||||||
|
#endif // CONFIG_COMPAT
|
||||||
|
#else
|
||||||
|
struct sepol_data {
|
||||||
|
u32 cmd;
|
||||||
|
u32 subcmd;
|
||||||
|
u32 field_sepol1;
|
||||||
|
u32 field_sepol2;
|
||||||
|
u32 field_sepol3;
|
||||||
|
u32 field_sepol4;
|
||||||
|
u32 field_sepol5;
|
||||||
|
u32 field_sepol6;
|
||||||
|
u32 field_sepol7;
|
||||||
|
};
|
||||||
|
#endif // CONFIG_64BIT
|
||||||
|
|
||||||
static int get_object(char *buf, char __user *user_object, size_t buf_sz,
|
static int get_object(char *buf, char __user *user_object, size_t buf_sz,
|
||||||
char **object)
|
char **object)
|
||||||
@@ -192,14 +220,58 @@ int handle_sepolicy(unsigned long arg3, void __user *arg4)
|
|||||||
pr_info("SELinux permissive or disabled when handle policy!\n");
|
pr_info("SELinux permissive or disabled when handle policy!\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
u32 cmd, subcmd;
|
||||||
|
char __user *sepol1, *sepol2, *sepol3, *sepol4, *sepol5, *sepol6, *sepol7;
|
||||||
|
|
||||||
|
#if defined(CONFIG_64BIT) && defined(CONFIG_COMPAT)
|
||||||
|
if (unlikely(ksu_is_compat)) {
|
||||||
|
struct sepol_compat_data compat_data;
|
||||||
|
if (copy_from_user(&compat_data, arg4, sizeof(struct sepol_compat_data))) {
|
||||||
|
pr_err("sepol: copy sepol_data failed.\n");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
sepol1 = compat_ptr(compat_data.field_sepol1);
|
||||||
|
sepol2 = compat_ptr(compat_data.field_sepol2);
|
||||||
|
sepol3 = compat_ptr(compat_data.field_sepol3);
|
||||||
|
sepol4 = compat_ptr(compat_data.field_sepol4);
|
||||||
|
sepol5 = compat_ptr(compat_data.field_sepol5);
|
||||||
|
sepol6 = compat_ptr(compat_data.field_sepol6);
|
||||||
|
sepol7 = compat_ptr(compat_data.field_sepol7);
|
||||||
|
cmd = compat_data.cmd;
|
||||||
|
subcmd = compat_data.subcmd;
|
||||||
|
} else {
|
||||||
|
struct sepol_data data;
|
||||||
|
if (copy_from_user(&data, arg4, sizeof(struct sepol_data))) {
|
||||||
|
pr_err("sepol: copy sepol_data failed.\n");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
sepol1 = data.field_sepol1;
|
||||||
|
sepol2 = data.field_sepol2;
|
||||||
|
sepol3 = data.field_sepol3;
|
||||||
|
sepol4 = data.field_sepol4;
|
||||||
|
sepol5 = data.field_sepol5;
|
||||||
|
sepol6 = data.field_sepol6;
|
||||||
|
sepol7 = data.field_sepol7;
|
||||||
|
cmd = data.cmd;
|
||||||
|
subcmd = data.subcmd;
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
// basically for full native, say (64BIT=y COMPAT=n) || (64BIT=n)
|
||||||
struct sepol_data data;
|
struct sepol_data data;
|
||||||
if (copy_from_user(&data, arg4, sizeof(struct sepol_data))) {
|
if (copy_from_user(&data, arg4, sizeof(struct sepol_data))) {
|
||||||
pr_err("sepol: copy sepol_data failed.\n");
|
pr_err("sepol: copy sepol_data failed.\n");
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
sepol1 = data.field_sepol1;
|
||||||
u32 cmd = data.cmd;
|
sepol2 = data.field_sepol2;
|
||||||
u32 subcmd = data.subcmd;
|
sepol3 = data.field_sepol3;
|
||||||
|
sepol4 = data.field_sepol4;
|
||||||
|
sepol5 = data.field_sepol5;
|
||||||
|
sepol6 = data.field_sepol6;
|
||||||
|
sepol7 = data.field_sepol7;
|
||||||
|
cmd = data.cmd;
|
||||||
|
subcmd = data.subcmd;
|
||||||
|
#endif
|
||||||
|
|
||||||
rcu_read_lock();
|
rcu_read_lock();
|
||||||
|
|
||||||
@@ -213,22 +285,22 @@ int handle_sepolicy(unsigned long arg3, void __user *arg4)
|
|||||||
char perm_buf[MAX_SEPOL_LEN];
|
char perm_buf[MAX_SEPOL_LEN];
|
||||||
|
|
||||||
char *s, *t, *c, *p;
|
char *s, *t, *c, *p;
|
||||||
if (get_object(src_buf, data.sepol1, sizeof(src_buf), &s) < 0) {
|
if (get_object(src_buf, sepol1, sizeof(src_buf), &s) < 0) {
|
||||||
pr_err("sepol: copy src failed.\n");
|
pr_err("sepol: copy src failed.\n");
|
||||||
goto exit;
|
goto exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (get_object(tgt_buf, data.sepol2, sizeof(tgt_buf), &t) < 0) {
|
if (get_object(tgt_buf, sepol2, sizeof(tgt_buf), &t) < 0) {
|
||||||
pr_err("sepol: copy tgt failed.\n");
|
pr_err("sepol: copy tgt failed.\n");
|
||||||
goto exit;
|
goto exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (get_object(cls_buf, data.sepol3, sizeof(cls_buf), &c) < 0) {
|
if (get_object(cls_buf, sepol3, sizeof(cls_buf), &c) < 0) {
|
||||||
pr_err("sepol: copy cls failed.\n");
|
pr_err("sepol: copy cls failed.\n");
|
||||||
goto exit;
|
goto exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (get_object(perm_buf, data.sepol4, sizeof(perm_buf), &p) <
|
if (get_object(perm_buf, sepol4, sizeof(perm_buf), &p) <
|
||||||
0) {
|
0) {
|
||||||
pr_err("sepol: copy perm failed.\n");
|
pr_err("sepol: copy perm failed.\n");
|
||||||
goto exit;
|
goto exit;
|
||||||
@@ -258,24 +330,24 @@ int handle_sepolicy(unsigned long arg3, void __user *arg4)
|
|||||||
char perm_set[MAX_SEPOL_LEN];
|
char perm_set[MAX_SEPOL_LEN];
|
||||||
|
|
||||||
char *s, *t, *c;
|
char *s, *t, *c;
|
||||||
if (get_object(src_buf, data.sepol1, sizeof(src_buf), &s) < 0) {
|
if (get_object(src_buf, sepol1, sizeof(src_buf), &s) < 0) {
|
||||||
pr_err("sepol: copy src failed.\n");
|
pr_err("sepol: copy src failed.\n");
|
||||||
goto exit;
|
goto exit;
|
||||||
}
|
}
|
||||||
if (get_object(tgt_buf, data.sepol2, sizeof(tgt_buf), &t) < 0) {
|
if (get_object(tgt_buf, sepol2, sizeof(tgt_buf), &t) < 0) {
|
||||||
pr_err("sepol: copy tgt failed.\n");
|
pr_err("sepol: copy tgt failed.\n");
|
||||||
goto exit;
|
goto exit;
|
||||||
}
|
}
|
||||||
if (get_object(cls_buf, data.sepol3, sizeof(cls_buf), &c) < 0) {
|
if (get_object(cls_buf, sepol3, sizeof(cls_buf), &c) < 0) {
|
||||||
pr_err("sepol: copy cls failed.\n");
|
pr_err("sepol: copy cls failed.\n");
|
||||||
goto exit;
|
goto exit;
|
||||||
}
|
}
|
||||||
if (strncpy_from_user(operation, data.sepol4,
|
if (strncpy_from_user(operation, sepol4,
|
||||||
sizeof(operation)) < 0) {
|
sizeof(operation)) < 0) {
|
||||||
pr_err("sepol: copy operation failed.\n");
|
pr_err("sepol: copy operation failed.\n");
|
||||||
goto exit;
|
goto exit;
|
||||||
}
|
}
|
||||||
if (strncpy_from_user(perm_set, data.sepol5, sizeof(perm_set)) <
|
if (strncpy_from_user(perm_set, sepol5, sizeof(perm_set)) <
|
||||||
0) {
|
0) {
|
||||||
pr_err("sepol: copy perm_set failed.\n");
|
pr_err("sepol: copy perm_set failed.\n");
|
||||||
goto exit;
|
goto exit;
|
||||||
@@ -295,7 +367,7 @@ int handle_sepolicy(unsigned long arg3, void __user *arg4)
|
|||||||
} else if (cmd == CMD_TYPE_STATE) {
|
} else if (cmd == CMD_TYPE_STATE) {
|
||||||
char src[MAX_SEPOL_LEN];
|
char src[MAX_SEPOL_LEN];
|
||||||
|
|
||||||
if (strncpy_from_user(src, data.sepol1, sizeof(src)) < 0) {
|
if (strncpy_from_user(src, sepol1, sizeof(src)) < 0) {
|
||||||
pr_err("sepol: copy src failed.\n");
|
pr_err("sepol: copy src failed.\n");
|
||||||
goto exit;
|
goto exit;
|
||||||
}
|
}
|
||||||
@@ -315,11 +387,11 @@ int handle_sepolicy(unsigned long arg3, void __user *arg4)
|
|||||||
char type[MAX_SEPOL_LEN];
|
char type[MAX_SEPOL_LEN];
|
||||||
char attr[MAX_SEPOL_LEN];
|
char attr[MAX_SEPOL_LEN];
|
||||||
|
|
||||||
if (strncpy_from_user(type, data.sepol1, sizeof(type)) < 0) {
|
if (strncpy_from_user(type, sepol1, sizeof(type)) < 0) {
|
||||||
pr_err("sepol: copy type failed.\n");
|
pr_err("sepol: copy type failed.\n");
|
||||||
goto exit;
|
goto exit;
|
||||||
}
|
}
|
||||||
if (strncpy_from_user(attr, data.sepol2, sizeof(attr)) < 0) {
|
if (strncpy_from_user(attr, sepol2, sizeof(attr)) < 0) {
|
||||||
pr_err("sepol: copy attr failed.\n");
|
pr_err("sepol: copy attr failed.\n");
|
||||||
goto exit;
|
goto exit;
|
||||||
}
|
}
|
||||||
@@ -339,7 +411,7 @@ int handle_sepolicy(unsigned long arg3, void __user *arg4)
|
|||||||
} else if (cmd == CMD_ATTR) {
|
} else if (cmd == CMD_ATTR) {
|
||||||
char attr[MAX_SEPOL_LEN];
|
char attr[MAX_SEPOL_LEN];
|
||||||
|
|
||||||
if (strncpy_from_user(attr, data.sepol1, sizeof(attr)) < 0) {
|
if (strncpy_from_user(attr, sepol1, sizeof(attr)) < 0) {
|
||||||
pr_err("sepol: copy attr failed.\n");
|
pr_err("sepol: copy attr failed.\n");
|
||||||
goto exit;
|
goto exit;
|
||||||
}
|
}
|
||||||
@@ -356,28 +428,28 @@ int handle_sepolicy(unsigned long arg3, void __user *arg4)
|
|||||||
char default_type[MAX_SEPOL_LEN];
|
char default_type[MAX_SEPOL_LEN];
|
||||||
char object[MAX_SEPOL_LEN];
|
char object[MAX_SEPOL_LEN];
|
||||||
|
|
||||||
if (strncpy_from_user(src, data.sepol1, sizeof(src)) < 0) {
|
if (strncpy_from_user(src, sepol1, sizeof(src)) < 0) {
|
||||||
pr_err("sepol: copy src failed.\n");
|
pr_err("sepol: copy src failed.\n");
|
||||||
goto exit;
|
goto exit;
|
||||||
}
|
}
|
||||||
if (strncpy_from_user(tgt, data.sepol2, sizeof(tgt)) < 0) {
|
if (strncpy_from_user(tgt, sepol2, sizeof(tgt)) < 0) {
|
||||||
pr_err("sepol: copy tgt failed.\n");
|
pr_err("sepol: copy tgt failed.\n");
|
||||||
goto exit;
|
goto exit;
|
||||||
}
|
}
|
||||||
if (strncpy_from_user(cls, data.sepol3, sizeof(cls)) < 0) {
|
if (strncpy_from_user(cls, sepol3, sizeof(cls)) < 0) {
|
||||||
pr_err("sepol: copy cls failed.\n");
|
pr_err("sepol: copy cls failed.\n");
|
||||||
goto exit;
|
goto exit;
|
||||||
}
|
}
|
||||||
if (strncpy_from_user(default_type, data.sepol4,
|
if (strncpy_from_user(default_type, sepol4,
|
||||||
sizeof(default_type)) < 0) {
|
sizeof(default_type)) < 0) {
|
||||||
pr_err("sepol: copy default_type failed.\n");
|
pr_err("sepol: copy default_type failed.\n");
|
||||||
goto exit;
|
goto exit;
|
||||||
}
|
}
|
||||||
char *real_object;
|
char *real_object;
|
||||||
if (data.sepol5 == NULL) {
|
if (sepol5 == NULL) {
|
||||||
real_object = NULL;
|
real_object = NULL;
|
||||||
} else {
|
} else {
|
||||||
if (strncpy_from_user(object, data.sepol5,
|
if (strncpy_from_user(object, sepol5,
|
||||||
sizeof(object)) < 0) {
|
sizeof(object)) < 0) {
|
||||||
pr_err("sepol: copy object failed.\n");
|
pr_err("sepol: copy object failed.\n");
|
||||||
goto exit;
|
goto exit;
|
||||||
@@ -396,19 +468,19 @@ int handle_sepolicy(unsigned long arg3, void __user *arg4)
|
|||||||
char cls[MAX_SEPOL_LEN];
|
char cls[MAX_SEPOL_LEN];
|
||||||
char default_type[MAX_SEPOL_LEN];
|
char default_type[MAX_SEPOL_LEN];
|
||||||
|
|
||||||
if (strncpy_from_user(src, data.sepol1, sizeof(src)) < 0) {
|
if (strncpy_from_user(src, sepol1, sizeof(src)) < 0) {
|
||||||
pr_err("sepol: copy src failed.\n");
|
pr_err("sepol: copy src failed.\n");
|
||||||
goto exit;
|
goto exit;
|
||||||
}
|
}
|
||||||
if (strncpy_from_user(tgt, data.sepol2, sizeof(tgt)) < 0) {
|
if (strncpy_from_user(tgt, sepol2, sizeof(tgt)) < 0) {
|
||||||
pr_err("sepol: copy tgt failed.\n");
|
pr_err("sepol: copy tgt failed.\n");
|
||||||
goto exit;
|
goto exit;
|
||||||
}
|
}
|
||||||
if (strncpy_from_user(cls, data.sepol3, sizeof(cls)) < 0) {
|
if (strncpy_from_user(cls, sepol3, sizeof(cls)) < 0) {
|
||||||
pr_err("sepol: copy cls failed.\n");
|
pr_err("sepol: copy cls failed.\n");
|
||||||
goto exit;
|
goto exit;
|
||||||
}
|
}
|
||||||
if (strncpy_from_user(default_type, data.sepol4,
|
if (strncpy_from_user(default_type, sepol4,
|
||||||
sizeof(default_type)) < 0) {
|
sizeof(default_type)) < 0) {
|
||||||
pr_err("sepol: copy default_type failed.\n");
|
pr_err("sepol: copy default_type failed.\n");
|
||||||
goto exit;
|
goto exit;
|
||||||
@@ -429,15 +501,15 @@ int handle_sepolicy(unsigned long arg3, void __user *arg4)
|
|||||||
char name[MAX_SEPOL_LEN];
|
char name[MAX_SEPOL_LEN];
|
||||||
char path[MAX_SEPOL_LEN];
|
char path[MAX_SEPOL_LEN];
|
||||||
char context[MAX_SEPOL_LEN];
|
char context[MAX_SEPOL_LEN];
|
||||||
if (strncpy_from_user(name, data.sepol1, sizeof(name)) < 0) {
|
if (strncpy_from_user(name, sepol1, sizeof(name)) < 0) {
|
||||||
pr_err("sepol: copy name failed.\n");
|
pr_err("sepol: copy name failed.\n");
|
||||||
goto exit;
|
goto exit;
|
||||||
}
|
}
|
||||||
if (strncpy_from_user(path, data.sepol2, sizeof(path)) < 0) {
|
if (strncpy_from_user(path, sepol2, sizeof(path)) < 0) {
|
||||||
pr_err("sepol: copy path failed.\n");
|
pr_err("sepol: copy path failed.\n");
|
||||||
goto exit;
|
goto exit;
|
||||||
}
|
}
|
||||||
if (strncpy_from_user(context, data.sepol3, sizeof(context)) <
|
if (strncpy_from_user(context, sepol3, sizeof(context)) <
|
||||||
0) {
|
0) {
|
||||||
pr_err("sepol: copy context failed.\n");
|
pr_err("sepol: copy context failed.\n");
|
||||||
goto exit;
|
goto exit;
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ setup_kernelsu() {
|
|||||||
echo "[+] Setting up KernelSU..."
|
echo "[+] Setting up KernelSU..."
|
||||||
# Clone the repository and rename it to KernelSU
|
# Clone the repository and rename it to KernelSU
|
||||||
if [ ! -d "$GKI_ROOT/KernelSU" ]; then
|
if [ ! -d "$GKI_ROOT/KernelSU" ]; then
|
||||||
git clone https://github.com/ShirkNeko/SukiSU-Ultra SukiSU-Ultra
|
git clone https://github.com/SukiSU-Ultra/SukiSU-Ultra SukiSU-Ultra
|
||||||
mv SukiSU-Ultra KernelSU
|
mv SukiSU-Ultra KernelSU
|
||||||
echo "[+] Repository cloned and renamed to KernelSU."
|
echo "[+] Repository cloned and renamed to KernelSU."
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -22,6 +22,10 @@
|
|||||||
|
|
||||||
extern void escape_to_root();
|
extern void escape_to_root();
|
||||||
|
|
||||||
|
#ifndef CONFIG_KPROBES
|
||||||
|
static bool ksu_sucompat_non_kp __read_mostly = true;
|
||||||
|
#endif
|
||||||
|
|
||||||
static void __user *userspace_stack_buffer(const void *d, size_t len)
|
static void __user *userspace_stack_buffer(const void *d, size_t len)
|
||||||
{
|
{
|
||||||
/* To avoid having to mmap a page in userspace, just write below the stack
|
/* To avoid having to mmap a page in userspace, just write below the stack
|
||||||
@@ -50,6 +54,12 @@ int ksu_handle_faccessat(int *dfd, const char __user **filename_user, int *mode,
|
|||||||
{
|
{
|
||||||
const char su[] = SU_PATH;
|
const char su[] = SU_PATH;
|
||||||
|
|
||||||
|
#ifndef CONFIG_KPROBES
|
||||||
|
if (!ksu_sucompat_non_kp) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
if (!ksu_is_allow_uid(current_uid().val)) {
|
if (!ksu_is_allow_uid(current_uid().val)) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -71,6 +81,11 @@ int ksu_handle_stat(int *dfd, const char __user **filename_user, int *flags)
|
|||||||
// const char sh[] = SH_PATH;
|
// const char sh[] = SH_PATH;
|
||||||
const char su[] = SU_PATH;
|
const char su[] = SU_PATH;
|
||||||
|
|
||||||
|
#ifndef CONFIG_KPROBES
|
||||||
|
if (!ksu_sucompat_non_kp) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
if (!ksu_is_allow_uid(current_uid().val)) {
|
if (!ksu_is_allow_uid(current_uid().val)) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -115,6 +130,11 @@ int ksu_handle_execveat_sucompat(int *fd, struct filename **filename_ptr,
|
|||||||
const char sh[] = KSUD_PATH;
|
const char sh[] = KSUD_PATH;
|
||||||
const char su[] = SU_PATH;
|
const char su[] = SU_PATH;
|
||||||
|
|
||||||
|
#ifndef CONFIG_KPROBES
|
||||||
|
if (!ksu_sucompat_non_kp) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
if (unlikely(!filename_ptr))
|
if (unlikely(!filename_ptr))
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
@@ -144,6 +164,11 @@ int ksu_handle_execve_sucompat(int *fd, const char __user **filename_user,
|
|||||||
const char su[] = SU_PATH;
|
const char su[] = SU_PATH;
|
||||||
char path[sizeof(su) + 1];
|
char path[sizeof(su) + 1];
|
||||||
|
|
||||||
|
#ifndef CONFIG_KPROBES
|
||||||
|
if (!ksu_sucompat_non_kp){
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
if (unlikely(!filename_user))
|
if (unlikely(!filename_user))
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
@@ -237,6 +262,9 @@ void ksu_sucompat_init()
|
|||||||
su_kps[0] = init_kprobe(SYS_EXECVE_SYMBOL, execve_handler_pre);
|
su_kps[0] = init_kprobe(SYS_EXECVE_SYMBOL, execve_handler_pre);
|
||||||
su_kps[1] = init_kprobe(SYS_FACCESSAT_SYMBOL, faccessat_handler_pre);
|
su_kps[1] = init_kprobe(SYS_FACCESSAT_SYMBOL, faccessat_handler_pre);
|
||||||
su_kps[2] = init_kprobe(SYS_NEWFSTATAT_SYMBOL, newfstatat_handler_pre);
|
su_kps[2] = init_kprobe(SYS_NEWFSTATAT_SYMBOL, newfstatat_handler_pre);
|
||||||
|
#else
|
||||||
|
ksu_sucompat_non_kp = true;
|
||||||
|
pr_info("ksu_sucompat_init: hooks enabled: execve/execveat_su, faccessat, stat\n");
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -246,5 +274,8 @@ void ksu_sucompat_exit()
|
|||||||
for (int i = 0; i < ARRAY_SIZE(su_kps); i++) {
|
for (int i = 0; i < ARRAY_SIZE(su_kps); i++) {
|
||||||
destroy_kprobe(&su_kps[i]);
|
destroy_kprobe(&su_kps[i]);
|
||||||
}
|
}
|
||||||
|
#else
|
||||||
|
ksu_sucompat_non_kp = false;
|
||||||
|
pr_info("ksu_sucompat_exit: hooks disabled: execve/execveat_su, faccessat, stat\n");
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -358,12 +358,14 @@ void track_throne()
|
|||||||
if (ksu_is_manager_uid_valid()) {
|
if (ksu_is_manager_uid_valid()) {
|
||||||
pr_info("manager is uninstalled, invalidate it!\n");
|
pr_info("manager is uninstalled, invalidate it!\n");
|
||||||
ksu_invalidate_manager_uid();
|
ksu_invalidate_manager_uid();
|
||||||
|
goto prune;
|
||||||
}
|
}
|
||||||
pr_info("Searching manager...\n");
|
pr_info("Searching manager...\n");
|
||||||
search_manager("/data/app", 2, &uid_list);
|
search_manager("/data/app", 2, &uid_list);
|
||||||
pr_info("Search manager finished\n");
|
pr_info("Search manager finished\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
prune:
|
||||||
// then prune the allowlist
|
// then prune the allowlist
|
||||||
ksu_prune_allowlist(is_uid_exist, &uid_list);
|
ksu_prune_allowlist(is_uid_exist, &uid_list);
|
||||||
out:
|
out:
|
||||||
|
|||||||
@@ -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,7 +25,17 @@ apksign {
|
|||||||
keyPasswordProperty = "KEY_PASSWORD"
|
keyPasswordProperty = "KEY_PASSWORD"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
|
||||||
|
/**signingConfigs {
|
||||||
|
create("Debug") {
|
||||||
|
storeFile = file("D:\\other\\AndroidTool\\android_key\\keystore\\release-key.keystore")
|
||||||
|
storePassword = ""
|
||||||
|
keyAlias = ""
|
||||||
|
keyPassword = ""
|
||||||
|
}
|
||||||
|
}**/
|
||||||
namespace = "com.sukisu.ultra"
|
namespace = "com.sukisu.ultra"
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
@@ -33,10 +44,12 @@ 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 {
|
||||||
aidl = true
|
|
||||||
buildConfig = true
|
buildConfig = true
|
||||||
compose = true
|
compose = true
|
||||||
prefab = true
|
prefab = true
|
||||||
@@ -95,6 +108,7 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
implementation(libs.gson)
|
||||||
implementation(libs.androidx.activity.compose)
|
implementation(libs.androidx.activity.compose)
|
||||||
implementation(libs.androidx.navigation.compose)
|
implementation(libs.androidx.navigation.compose)
|
||||||
|
|
||||||
@@ -140,4 +154,9 @@ dependencies {
|
|||||||
|
|
||||||
implementation(libs.com.github.topjohnwu.libsu.core)
|
implementation(libs.com.github.topjohnwu.libsu.core)
|
||||||
|
|
||||||
|
implementation(libs.mmrl.platform)
|
||||||
|
compileOnly(libs.mmrl.hidden.api)
|
||||||
|
implementation(libs.mmrl.webui)
|
||||||
|
implementation(libs.mmrl.ui)
|
||||||
|
|
||||||
}
|
}
|
||||||
47
manager/app/proguard-rules.pro
vendored
@@ -0,0 +1,47 @@
|
|||||||
|
-verbose
|
||||||
|
-optimizationpasses 5
|
||||||
|
|
||||||
|
-dontwarn org.conscrypt.**
|
||||||
|
-dontwarn kotlinx.serialization.**
|
||||||
|
|
||||||
|
# Please add these rules to your existing keep rules in order to suppress warnings.
|
||||||
|
# This is generated automatically by the Android Gradle plugin.
|
||||||
|
-dontwarn com.google.auto.service.AutoService
|
||||||
|
-dontwarn com.google.j2objc.annotations.RetainedWith
|
||||||
|
-dontwarn javax.lang.model.SourceVersion
|
||||||
|
-dontwarn javax.lang.model.element.AnnotationMirror
|
||||||
|
-dontwarn javax.lang.model.element.AnnotationValue
|
||||||
|
-dontwarn javax.lang.model.element.Element
|
||||||
|
-dontwarn javax.lang.model.element.ElementKind
|
||||||
|
-dontwarn javax.lang.model.element.ElementVisitor
|
||||||
|
-dontwarn javax.lang.model.element.ExecutableElement
|
||||||
|
-dontwarn javax.lang.model.element.Modifier
|
||||||
|
-dontwarn javax.lang.model.element.Name
|
||||||
|
-dontwarn javax.lang.model.element.PackageElement
|
||||||
|
-dontwarn javax.lang.model.element.TypeElement
|
||||||
|
-dontwarn javax.lang.model.element.TypeParameterElement
|
||||||
|
-dontwarn javax.lang.model.element.VariableElement
|
||||||
|
-dontwarn javax.lang.model.type.ArrayType
|
||||||
|
-dontwarn javax.lang.model.type.DeclaredType
|
||||||
|
-dontwarn javax.lang.model.type.ExecutableType
|
||||||
|
-dontwarn javax.lang.model.type.TypeKind
|
||||||
|
-dontwarn javax.lang.model.type.TypeMirror
|
||||||
|
-dontwarn javax.lang.model.type.TypeVariable
|
||||||
|
-dontwarn javax.lang.model.type.TypeVisitor
|
||||||
|
-dontwarn javax.lang.model.util.AbstractAnnotationValueVisitor8
|
||||||
|
-dontwarn javax.lang.model.util.AbstractTypeVisitor8
|
||||||
|
-dontwarn javax.lang.model.util.ElementFilter
|
||||||
|
-dontwarn javax.lang.model.util.Elements
|
||||||
|
-dontwarn javax.lang.model.util.SimpleElementVisitor8
|
||||||
|
-dontwarn javax.lang.model.util.SimpleTypeVisitor7
|
||||||
|
-dontwarn javax.lang.model.util.SimpleTypeVisitor8
|
||||||
|
-dontwarn javax.lang.model.util.Types
|
||||||
|
-dontwarn javax.tools.Diagnostic$Kind
|
||||||
|
|
||||||
|
|
||||||
|
# MMRL:webui reflection
|
||||||
|
-keep class com.dergoogler.mmrl.webui.model.ModId { *; }
|
||||||
|
-keep class com.dergoogler.mmrl.webui.interfaces.** { *; }
|
||||||
|
-keep class com.sukisu.ultra.ui.webui.WebViewInterface { *; }
|
||||||
|
|
||||||
|
-keep,allowobfuscation class * extends com.dergoogler.mmrl.platform.content.IService { *; }
|
||||||
@@ -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
|
||||||
@@ -37,6 +39,13 @@
|
|||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:theme="@style/Theme.KernelSU.WebUI" />
|
android:theme="@style/Theme.KernelSU.WebUI" />
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".ui.webui.WebUIXActivity"
|
||||||
|
android:autoRemoveFromRecents="true"
|
||||||
|
android:documentLaunchMode="intoExisting"
|
||||||
|
android:exported="false"
|
||||||
|
android:theme="@style/Theme.KernelSU.WebUI" />
|
||||||
|
|
||||||
<provider
|
<provider
|
||||||
android:name="androidx.core.content.FileProvider"
|
android:name="androidx.core.content.FileProvider"
|
||||||
android:authorities="${applicationId}.fileprovider"
|
android:authorities="${applicationId}.fileprovider"
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
package com.sukisu.zako;
|
|
||||||
|
|
||||||
import android.content.pm.PackageInfo;
|
|
||||||
import rikka.parcelablelist.ParcelableListSlice;
|
|
||||||
|
|
||||||
interface IKsuInterface {
|
|
||||||
ParcelableListSlice<PackageInfo> getPackages(int flags);
|
|
||||||
}
|
|
||||||
BIN
manager/app/src/main/assets/5_10-mkbootfs
Normal file
@@ -306,3 +306,8 @@ JNIEXPORT jboolean JNICALL
|
|||||||
Java_com_sukisu_ultra_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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extern "C" JNIEXPORT jboolean JNICALL
|
||||||
|
Java_com_sukisu_ultra_Natives_isKPMEnabled(JNIEnv *env, jobject) {
|
||||||
|
return is_KPM_enable();
|
||||||
|
}
|
||||||
@@ -29,6 +29,7 @@
|
|||||||
#define CMD_IS_UID_SHOULD_UMOUNT 13
|
#define CMD_IS_UID_SHOULD_UMOUNT 13
|
||||||
#define CMD_IS_SU_ENABLED 14
|
#define CMD_IS_SU_ENABLED 14
|
||||||
#define CMD_ENABLE_SU 15
|
#define CMD_ENABLE_SU 15
|
||||||
|
#define CMD_ENABLE_KPM 100
|
||||||
|
|
||||||
static bool ksuctl(int cmd, void* arg1, void* arg2) {
|
static bool ksuctl(int cmd, void* arg1, void* arg2) {
|
||||||
int32_t result = 0;
|
int32_t result = 0;
|
||||||
@@ -97,3 +98,8 @@ bool is_su_enabled() {
|
|||||||
ksuctl(CMD_IS_SU_ENABLED, &enabled, nullptr);
|
ksuctl(CMD_IS_SU_ENABLED, &enabled, nullptr);
|
||||||
return enabled;
|
return enabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool is_KPM_enable() {
|
||||||
|
bool enabled = false;
|
||||||
|
return ksuctl(CMD_ENABLE_KPM, &enabled, nullptr), enabled;
|
||||||
|
}
|
||||||
@@ -83,4 +83,6 @@ bool set_su_enabled(bool enabled);
|
|||||||
|
|
||||||
bool is_su_enabled();
|
bool is_su_enabled();
|
||||||
|
|
||||||
|
bool is_KPM_enable();
|
||||||
|
|
||||||
#endif //KERNELSU_KSU_H
|
#endif //KERNELSU_KSU_H
|
||||||
|
|||||||
BIN
manager/app/src/main/ic_launcher-playstore.png
Normal file
|
After Width: | Height: | Size: 87 KiB |
@@ -1,20 +1,69 @@
|
|||||||
package com.sukisu.ultra
|
package com.sukisu.ultra
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.res.Configuration
|
||||||
|
import android.content.res.Resources
|
||||||
|
import android.os.Build
|
||||||
import coil.Coil
|
import coil.Coil
|
||||||
import coil.ImageLoader
|
import coil.ImageLoader
|
||||||
|
import com.dergoogler.mmrl.platform.Platform
|
||||||
import me.zhanghai.android.appiconloader.coil.AppIconFetcher
|
import me.zhanghai.android.appiconloader.coil.AppIconFetcher
|
||||||
import me.zhanghai.android.appiconloader.coil.AppIconKeyer
|
import me.zhanghai.android.appiconloader.coil.AppIconKeyer
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
lateinit var ksuApp: KernelSUApplication
|
lateinit var ksuApp: KernelSUApplication
|
||||||
|
|
||||||
class KernelSUApplication : Application() {
|
class KernelSUApplication : Application() {
|
||||||
|
|
||||||
|
override fun attachBaseContext(base: Context) {
|
||||||
|
val prefs = base.getSharedPreferences("settings", MODE_PRIVATE)
|
||||||
|
val languageCode = prefs.getString("app_language", "") ?: ""
|
||||||
|
|
||||||
|
var context = base
|
||||||
|
if (languageCode.isNotEmpty()) {
|
||||||
|
val locale = Locale.forLanguageTag(languageCode)
|
||||||
|
Locale.setDefault(locale)
|
||||||
|
|
||||||
|
val config = Configuration(base.resources.configuration)
|
||||||
|
config.setLocale(locale)
|
||||||
|
|
||||||
|
context = base.createConfigurationContext(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
super.attachBaseContext(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("ObsoleteSdkInt")
|
||||||
|
override fun getResources(): Resources {
|
||||||
|
val resources = super.getResources()
|
||||||
|
val prefs = getSharedPreferences("settings", MODE_PRIVATE)
|
||||||
|
val languageCode = prefs.getString("app_language", "") ?: ""
|
||||||
|
|
||||||
|
if (languageCode.isNotEmpty()) {
|
||||||
|
val locale = Locale.forLanguageTag(languageCode)
|
||||||
|
val config = Configuration(resources.configuration)
|
||||||
|
config.setLocale(locale)
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
|
return createConfigurationContext(config).resources
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
resources.updateConfiguration(config, resources.displayMetrics)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return resources
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
ksuApp = this
|
ksuApp = this
|
||||||
|
|
||||||
|
Platform.setHiddenApiExemptions()
|
||||||
|
|
||||||
val context = this
|
val context = this
|
||||||
val iconSize = resources.getDimensionPixelSize(android.R.dimen.app_icon_size)
|
val iconSize = resources.getDimensionPixelSize(android.R.dimen.app_icon_size)
|
||||||
Coil.setImageLoader(
|
Coil.setImageLoader(
|
||||||
@@ -32,5 +81,30 @@ class KernelSUApplication : Application() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||||
|
super.onConfigurationChanged(newConfig)
|
||||||
|
applyLanguageSetting()
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("ObsoleteSdkInt")
|
||||||
|
private fun applyLanguageSetting() {
|
||||||
|
val prefs = getSharedPreferences("settings", MODE_PRIVATE)
|
||||||
|
val languageCode = prefs.getString("app_language", "") ?: ""
|
||||||
|
|
||||||
|
if (languageCode.isNotEmpty()) {
|
||||||
|
val locale = Locale.forLanguageTag(languageCode)
|
||||||
|
Locale.setDefault(locale)
|
||||||
|
|
||||||
|
val resources = resources
|
||||||
|
val config = Configuration(resources.configuration)
|
||||||
|
config.setLocale(locale)
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
|
createConfigurationContext(config)
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
resources.updateConfiguration(config, resources.displayMetrics)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -8,24 +8,13 @@ import android.system.Os
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
data class KernelVersion(val major: Int, val patchLevel: Int, val subLevel: Int) {
|
data class KernelVersion(val major: Int, val patchLevel: Int, val subLevel: Int) {
|
||||||
override fun toString(): String {
|
override fun toString(): String = "$major.$patchLevel.$subLevel"
|
||||||
return "$major.$patchLevel.$subLevel"
|
fun isGKI(): Boolean = when {
|
||||||
}
|
major > 5 -> true
|
||||||
|
major == 5 && patchLevel >= 10 -> true
|
||||||
fun isGKI(): Boolean {
|
else -> false
|
||||||
|
|
||||||
// kernel 6.x
|
|
||||||
if (major > 5) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// kernel 5.10.x
|
|
||||||
if (major == 5) {
|
|
||||||
return patchLevel >= 10
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
fun isGKI1(): Boolean = (major == 4 && patchLevel >= 19) || (major == 5 && patchLevel < 10)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun parseKernelVersion(version: String): KernelVersion {
|
fun parseKernelVersion(version: String): KernelVersion {
|
||||||
|
|||||||
@@ -16,16 +16,18 @@ 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 = 12800
|
const val MINIMAL_SUPPORTED_KERNEL = 11071
|
||||||
|
|
||||||
// 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 = 12800
|
const val MINIMAL_SUPPORTED_KERNEL_LKM = 11648
|
||||||
|
|
||||||
// 12040: Support disable sucompat mode
|
// 12040: Support disable sucompat mode
|
||||||
const val MINIMAL_SUPPORTED_SU_COMPAT = 12800
|
const val MINIMAL_SUPPORTED_SU_COMPAT = 12040
|
||||||
const val KERNEL_SU_DOMAIN = "u:r:su:s0"
|
const val KERNEL_SU_DOMAIN = "u:r:su:s0"
|
||||||
|
|
||||||
|
const val MINIMAL_SUPPORTED_KPM = 12800
|
||||||
|
|
||||||
const val ROOT_UID = 0
|
const val ROOT_UID = 0
|
||||||
const val ROOT_GID = 0
|
const val ROOT_GID = 0
|
||||||
|
|
||||||
@@ -66,6 +68,7 @@ object Natives {
|
|||||||
*/
|
*/
|
||||||
external fun isSuEnabled(): Boolean
|
external fun isSuEnabled(): Boolean
|
||||||
external fun setSuEnabled(enabled: Boolean): Boolean
|
external fun setSuEnabled(enabled: Boolean): Boolean
|
||||||
|
external fun isKPMEnabled(): Boolean
|
||||||
|
|
||||||
private const val NON_ROOT_DEFAULT_PROFILE_KEY = "$"
|
private const val NON_ROOT_DEFAULT_PROFILE_KEY = "$"
|
||||||
private const val NOBODY_UID = 9999
|
private const val NOBODY_UID = 9999
|
||||||
@@ -91,14 +94,6 @@ object Natives {
|
|||||||
return version < MINIMAL_SUPPORTED_KERNEL
|
return version < MINIMAL_SUPPORTED_KERNEL
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isKsuValid(pkgName: String?): Boolean {
|
|
||||||
if (becomeManager(pkgName)) {
|
|
||||||
return true
|
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
@Parcelize
|
@Parcelize
|
||||||
@Keep
|
@Keep
|
||||||
|
|||||||
@@ -27,6 +27,11 @@ import java.io.File
|
|||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author ShirkNeko
|
||||||
|
* @date 2025/5/31.
|
||||||
|
*/
|
||||||
data class FlashState(
|
data class FlashState(
|
||||||
val isFlashing: Boolean = false,
|
val isFlashing: Boolean = false,
|
||||||
val isCompleted: Boolean = false,
|
val isCompleted: Boolean = false,
|
||||||
@@ -133,15 +138,13 @@ class HorizonKernelWorker(
|
|||||||
state.updateStep(context.getString(R.string.horizon_flashing))
|
state.updateStep(context.getString(R.string.horizon_flashing))
|
||||||
state.updateProgress(0.7f)
|
state.updateProgress(0.7f)
|
||||||
|
|
||||||
// 获取原始槽位信息
|
val isAbDevice = isAbDevice()
|
||||||
if (slot != null) {
|
|
||||||
|
if (isAbDevice && slot != null) {
|
||||||
state.updateStep(context.getString(R.string.horizon_getting_original_slot))
|
state.updateStep(context.getString(R.string.horizon_getting_original_slot))
|
||||||
state.updateProgress(0.72f)
|
state.updateProgress(0.72f)
|
||||||
originalSlot = runCommandGetOutput(true, "getprop ro.boot.slot_suffix")
|
originalSlot = runCommandGetOutput(true, "getprop ro.boot.slot_suffix")
|
||||||
}
|
|
||||||
|
|
||||||
// 设置目标槽位
|
|
||||||
if (!slot.isNullOrEmpty()) {
|
|
||||||
state.updateStep(context.getString(R.string.horizon_setting_target_slot))
|
state.updateStep(context.getString(R.string.horizon_setting_target_slot))
|
||||||
state.updateProgress(0.74f)
|
state.updateProgress(0.74f)
|
||||||
runCommand(true, "resetprop -n ro.boot.slot_suffix _$slot")
|
runCommand(true, "resetprop -n ro.boot.slot_suffix _$slot")
|
||||||
@@ -149,8 +152,7 @@ class HorizonKernelWorker(
|
|||||||
|
|
||||||
flash()
|
flash()
|
||||||
|
|
||||||
// 恢复原始槽位
|
if (isAbDevice && !originalSlot.isNullOrEmpty()) {
|
||||||
if (!originalSlot.isNullOrEmpty()) {
|
|
||||||
state.updateStep(context.getString(R.string.horizon_restoring_original_slot))
|
state.updateStep(context.getString(R.string.horizon_restoring_original_slot))
|
||||||
state.updateProgress(0.8f)
|
state.updateProgress(0.8f)
|
||||||
runCommand(true, "resetprop ro.boot.slot_suffix $originalSlot")
|
runCommand(true, "resetprop ro.boot.slot_suffix $originalSlot")
|
||||||
@@ -165,8 +167,7 @@ class HorizonKernelWorker(
|
|||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
state.setError(e.message ?: context.getString(R.string.horizon_unknown_error))
|
state.setError(e.message ?: context.getString(R.string.horizon_unknown_error))
|
||||||
|
|
||||||
// 恢复原始槽位
|
if (isAbDevice() && !originalSlot.isNullOrEmpty()) {
|
||||||
if (!originalSlot.isNullOrEmpty()) {
|
|
||||||
state.updateStep(context.getString(R.string.horizon_restoring_original_slot))
|
state.updateStep(context.getString(R.string.horizon_restoring_original_slot))
|
||||||
state.updateProgress(0.8f)
|
state.updateProgress(0.8f)
|
||||||
runCommand(true, "resetprop ro.boot.slot_suffix $originalSlot")
|
runCommand(true, "resetprop ro.boot.slot_suffix $originalSlot")
|
||||||
@@ -174,6 +175,17 @@ class HorizonKernelWorker(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检查设备是否为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() {
|
private fun cleanup() {
|
||||||
runCommand(false, "find ${context.filesDir.absolutePath} -type f ! -name '*.jpg' ! -name '*.png' -delete")
|
runCommand(false, "find ${context.filesDir.absolutePath} -type f ! -name '*.jpg' ! -name '*.png' -delete")
|
||||||
}
|
}
|
||||||
@@ -196,9 +208,25 @@ class HorizonKernelWorker(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun patch() {
|
private fun patch() {
|
||||||
val mkbootfsPath = "${context.filesDir.absolutePath}/mkbootfs"
|
val kernelVersion = runCommandGetOutput(true, "cat /proc/version")
|
||||||
AssetsUtil.exportFiles(context, "mkbootfs", mkbootfsPath)
|
val versionRegex = """\d+\.\d+\.\d+""".toRegex()
|
||||||
runCommand(false, "sed -i '/chmod -R 755 tools bin;/i cp -f $mkbootfsPath \$AKHOME/tools;' $binaryPath")
|
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() {
|
private fun flash() {
|
||||||
@@ -275,7 +303,7 @@ class HorizonKernelWorker(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun runCommandGetOutput(su: Boolean, cmd: String): String {
|
private fun runCommandGetOutput(su: Boolean, cmd: String): String? {
|
||||||
val process = ProcessBuilder(if (su) "su" else "sh")
|
val process = ProcessBuilder(if (su) "su" else "sh")
|
||||||
.redirectErrorStream(true)
|
.redirectErrorStream(true)
|
||||||
.start()
|
.start()
|
||||||
@@ -354,7 +382,7 @@ fun HorizonKernelFlashProgress(state: FlashState) {
|
|||||||
Surface(
|
Surface(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.heightIn(max = 150.dp)
|
.heightIn(max = 230.dp)
|
||||||
.padding(vertical = 4.dp),
|
.padding(vertical = 4.dp),
|
||||||
color = MaterialTheme.colorScheme.surface,
|
color = MaterialTheme.colorScheme.surface,
|
||||||
tonalElevation = 1.dp,
|
tonalElevation = 1.dp,
|
||||||
|
|||||||
@@ -1,77 +0,0 @@
|
|||||||
package com.sukisu.ultra.ui;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.pm.PackageInfo;
|
|
||||||
import android.content.pm.PackageManager;
|
|
||||||
import android.os.IBinder;
|
|
||||||
import android.os.UserHandle;
|
|
||||||
import android.os.UserManager;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
|
|
||||||
import com.topjohnwu.superuser.ipc.RootService;
|
|
||||||
|
|
||||||
import java.lang.reflect.Method;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import com.sukisu.zako.IKsuInterface;
|
|
||||||
import rikka.parcelablelist.ParcelableListSlice;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author weishu
|
|
||||||
* @date 2023/4/18.
|
|
||||||
*/
|
|
||||||
|
|
||||||
public class KsuService extends RootService {
|
|
||||||
|
|
||||||
private static final String TAG = "KsuService";
|
|
||||||
|
|
||||||
class Stub extends IKsuInterface.Stub {
|
|
||||||
@Override
|
|
||||||
public ParcelableListSlice<PackageInfo> getPackages(int flags) {
|
|
||||||
List<PackageInfo> list = getInstalledPackagesAll(flags);
|
|
||||||
Log.i(TAG, "getPackages: " + list.size());
|
|
||||||
return new ParcelableListSlice<>(list);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public IBinder onBind(@NonNull Intent intent) {
|
|
||||||
return new Stub();
|
|
||||||
}
|
|
||||||
|
|
||||||
List<Integer> getUserIds() {
|
|
||||||
List<Integer> result = new ArrayList<>();
|
|
||||||
UserManager um = (UserManager) getSystemService(Context.USER_SERVICE);
|
|
||||||
List<UserHandle> userProfiles = um.getUserProfiles();
|
|
||||||
for (UserHandle userProfile : userProfiles) {
|
|
||||||
int userId = userProfile.hashCode();
|
|
||||||
result.add(userProfile.hashCode());
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
ArrayList<PackageInfo> getInstalledPackagesAll(int flags) {
|
|
||||||
ArrayList<PackageInfo> packages = new ArrayList<>();
|
|
||||||
for (Integer userId : getUserIds()) {
|
|
||||||
Log.i(TAG, "getInstalledPackagesAll: " + userId);
|
|
||||||
packages.addAll(getInstalledPackagesAsUser(flags, userId));
|
|
||||||
}
|
|
||||||
return packages;
|
|
||||||
}
|
|
||||||
|
|
||||||
List<PackageInfo> getInstalledPackagesAsUser(int flags, int userId) {
|
|
||||||
try {
|
|
||||||
PackageManager pm = getPackageManager();
|
|
||||||
Method getInstalledPackagesAsUser = pm.getClass().getDeclaredMethod("getInstalledPackagesAsUser", int.class, int.class);
|
|
||||||
return (List<PackageInfo>) getInstalledPackagesAsUser.invoke(pm, flags, userId);
|
|
||||||
} catch (Throwable e) {
|
|
||||||
Log.e(TAG, "err", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new ArrayList<>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,18 +1,22 @@
|
|||||||
package com.sukisu.ultra.ui
|
package com.sukisu.ultra.ui
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.res.Configuration
|
||||||
|
import android.database.ContentObserver
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.os.Handler
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
import androidx.compose.animation.*
|
import androidx.compose.animation.*
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.animation.fadeIn
|
|
||||||
import androidx.compose.animation.fadeOut
|
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.navigation.NavBackStackEntry
|
import androidx.navigation.NavBackStackEntry
|
||||||
import androidx.navigation.NavHostController
|
import androidx.navigation.NavHostController
|
||||||
@@ -20,6 +24,7 @@ import androidx.navigation.compose.rememberNavController
|
|||||||
import com.ramcosta.composedestinations.DestinationsNavHost
|
import com.ramcosta.composedestinations.DestinationsNavHost
|
||||||
import com.ramcosta.composedestinations.animations.NavHostAnimatedDestinationStyle
|
import com.ramcosta.composedestinations.animations.NavHostAnimatedDestinationStyle
|
||||||
import com.ramcosta.composedestinations.generated.NavGraphs
|
import com.ramcosta.composedestinations.generated.NavGraphs
|
||||||
|
import com.ramcosta.composedestinations.generated.destinations.ExecuteModuleActionScreenDestination
|
||||||
import com.ramcosta.composedestinations.spec.NavHostGraphSpec
|
import com.ramcosta.composedestinations.spec.NavHostGraphSpec
|
||||||
import com.ramcosta.composedestinations.spec.RouteOrDirection
|
import com.ramcosta.composedestinations.spec.RouteOrDirection
|
||||||
import com.ramcosta.composedestinations.utils.isRouteOnBackStackAsState
|
import com.ramcosta.composedestinations.utils.isRouteOnBackStackAsState
|
||||||
@@ -29,24 +34,142 @@ import com.sukisu.ultra.Natives
|
|||||||
import com.sukisu.ultra.ksuApp
|
import com.sukisu.ultra.ksuApp
|
||||||
import com.sukisu.ultra.ui.screen.BottomBarDestination
|
import com.sukisu.ultra.ui.screen.BottomBarDestination
|
||||||
import com.sukisu.ultra.ui.theme.*
|
import com.sukisu.ultra.ui.theme.*
|
||||||
|
import com.sukisu.ultra.ui.theme.CardConfig.cardAlpha
|
||||||
import com.sukisu.ultra.ui.util.*
|
import com.sukisu.ultra.ui.util.*
|
||||||
class MainActivity : ComponentActivity() {
|
import androidx.core.content.edit
|
||||||
|
import com.sukisu.ultra.ui.theme.CardConfig.cardElevation
|
||||||
|
import com.sukisu.ultra.ui.webui.initPlatform
|
||||||
|
import java.util.Locale
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.slideInVertically
|
||||||
|
import androidx.compose.animation.slideOutVertically
|
||||||
|
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import com.sukisu.ultra.ui.viewmodel.HomeViewModel
|
||||||
|
import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
class MainActivity : ComponentActivity() {
|
||||||
|
private lateinit var superUserViewModel: SuperUserViewModel
|
||||||
|
private lateinit var homeViewModel: HomeViewModel
|
||||||
|
|
||||||
|
private inner class ThemeChangeContentObserver(
|
||||||
|
handler: Handler,
|
||||||
|
private val onThemeChanged: () -> Unit
|
||||||
|
) : ContentObserver(handler) {
|
||||||
|
override fun onChange(selfChange: Boolean) {
|
||||||
|
super.onChange(selfChange)
|
||||||
|
onThemeChanged()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 应用保存的语言设置
|
||||||
|
@SuppressLint("ObsoleteSdkInt")
|
||||||
|
private fun applyLanguageSetting() {
|
||||||
|
val prefs = getSharedPreferences("settings", MODE_PRIVATE)
|
||||||
|
val languageCode = prefs.getString("app_language", "") ?: ""
|
||||||
|
|
||||||
|
if (languageCode.isNotEmpty()) {
|
||||||
|
val locale = Locale.forLanguageTag(languageCode)
|
||||||
|
Locale.setDefault(locale)
|
||||||
|
|
||||||
|
val resources = resources
|
||||||
|
val config = Configuration(resources.configuration)
|
||||||
|
config.setLocale(locale)
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
|
createConfigurationContext(config)
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
resources.updateConfiguration(config, resources.displayMetrics)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun attachBaseContext(newBase: Context) {
|
||||||
|
val prefs = newBase.getSharedPreferences("settings", MODE_PRIVATE)
|
||||||
|
val languageCode = prefs.getString("app_language", "") ?: ""
|
||||||
|
|
||||||
|
var context = newBase
|
||||||
|
if (languageCode.isNotEmpty()) {
|
||||||
|
val locale = Locale.forLanguageTag(languageCode)
|
||||||
|
Locale.setDefault(locale)
|
||||||
|
|
||||||
|
val config = Configuration(newBase.resources.configuration)
|
||||||
|
config.setLocale(locale)
|
||||||
|
context = newBase.createConfigurationContext(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
super.attachBaseContext(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("RestrictedApi")
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
// 确保应用正确的语言设置
|
||||||
|
applyLanguageSetting()
|
||||||
|
|
||||||
|
// 应用自定义 DPI
|
||||||
|
applyCustomDpi()
|
||||||
|
|
||||||
// Enable edge to edge
|
// Enable edge to edge
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
window.isNavigationBarContrastEnforced = false
|
window.isNavigationBarContrastEnforced = false
|
||||||
}
|
}
|
||||||
|
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
// 初始化 SuperUserViewModel
|
||||||
|
superUserViewModel = SuperUserViewModel()
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
superUserViewModel.fetchAppList()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化 HomeViewModel
|
||||||
|
homeViewModel = HomeViewModel()
|
||||||
|
|
||||||
|
// 预加载数据
|
||||||
|
lifecycleScope.launch {
|
||||||
|
homeViewModel.initializeData()
|
||||||
|
}
|
||||||
|
|
||||||
|
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) }
|
||||||
|
}
|
||||||
|
|
||||||
// 加载保存的背景设置
|
// 加载保存的背景设置
|
||||||
loadCustomBackground()
|
|
||||||
loadThemeMode()
|
loadThemeMode()
|
||||||
|
loadThemeColors()
|
||||||
|
loadDynamicColorState()
|
||||||
CardConfig.load(applicationContext)
|
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)
|
val isManager = Natives.becomeManager(ksuApp.packageName)
|
||||||
if (isManager) {
|
if (isManager) {
|
||||||
@@ -58,12 +181,32 @@ class MainActivity : ComponentActivity() {
|
|||||||
KernelSUTheme {
|
KernelSUTheme {
|
||||||
val navController = rememberNavController()
|
val navController = rememberNavController()
|
||||||
val snackBarHostState = remember { SnackbarHostState() }
|
val snackBarHostState = remember { SnackbarHostState() }
|
||||||
|
val currentDestination = navController.currentBackStackEntryAsState().value?.destination
|
||||||
|
|
||||||
|
val showBottomBar = when (currentDestination?.route) {
|
||||||
|
ExecuteModuleActionScreenDestination.route -> false // Hide for ExecuteModuleActionScreen
|
||||||
|
else -> true
|
||||||
|
}
|
||||||
|
|
||||||
|
// pre-init platform to faster start WebUI X activities
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
initPlatform()
|
||||||
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
bottomBar = { BottomBar(navController) },
|
bottomBar = {
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = showBottomBar,
|
||||||
|
enter = slideInVertically(initialOffsetY = { it }) + fadeIn(),
|
||||||
|
exit = slideOutVertically(targetOffsetY = { it }) + fadeOut()
|
||||||
|
) {
|
||||||
|
BottomBar(navController)
|
||||||
|
}
|
||||||
|
},
|
||||||
contentWindowInsets = WindowInsets(0, 0, 0, 0)
|
contentWindowInsets = WindowInsets(0, 0, 0, 0)
|
||||||
) { innerPadding ->
|
) { innerPadding ->
|
||||||
CompositionLocalProvider(
|
CompositionLocalProvider(
|
||||||
LocalSnackbarHost provides snackBarHostState,
|
LocalSnackbarHost provides snackBarHostState
|
||||||
) {
|
) {
|
||||||
DestinationsNavHost(
|
DestinationsNavHost(
|
||||||
modifier = Modifier.padding(innerPadding),
|
modifier = Modifier.padding(innerPadding),
|
||||||
@@ -81,36 +224,97 @@ class MainActivity : ComponentActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 应用自定义DPI设置
|
||||||
|
private fun applyCustomDpi() {
|
||||||
|
val prefs = getSharedPreferences("settings", MODE_PRIVATE)
|
||||||
|
val customDpi = prefs.getInt("app_dpi", 0)
|
||||||
|
|
||||||
|
if (customDpi > 0) {
|
||||||
|
try {
|
||||||
|
val resources = resources
|
||||||
|
val metrics = resources.displayMetrics
|
||||||
|
metrics.density = customDpi / 160f
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
metrics.scaledDensity = customDpi / 160f
|
||||||
|
metrics.densityDpi = customDpi
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
applyLanguageSetting()
|
||||||
|
|
||||||
|
if (!ThemeConfig.backgroundImageLoaded && !ThemeConfig.preventBackgroundRefresh) {
|
||||||
|
loadCustomBackground()
|
||||||
|
}
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
superUserViewModel.fetchAppList()
|
||||||
|
}
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
homeViewModel.initializeData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val destroyListeners = mutableListOf<() -> Unit>()
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
destroyListeners.forEach { it() }
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||||
|
super.onConfigurationChanged(newConfig)
|
||||||
|
applyLanguageSetting()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
private fun BottomBar(navController: NavHostController) {
|
private fun BottomBar(navController: NavHostController) {
|
||||||
val navigator = navController.rememberDestinationsNavigator()
|
val navigator = navController.rememberDestinationsNavigator()
|
||||||
val isManager = Natives.becomeManager(ksuApp.packageName)
|
val isManager = Natives.becomeManager(ksuApp.packageName)
|
||||||
val fullFeatured = isManager && !Natives.requireNewKernel() && rootAvailable()
|
val fullFeatured = isManager && !Natives.requireNewKernel() && rootAvailable()
|
||||||
val kpmVersion = getKpmVersion()
|
val kpmVersion = getKpmVersion()
|
||||||
|
val cardColor = MaterialTheme.colorScheme.surfaceContainer
|
||||||
|
|
||||||
// 获取卡片颜色和透明度
|
// 检查是否显示KPM
|
||||||
val cardColor = MaterialTheme.colorScheme.secondaryContainer
|
val showKpmInfo = LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||||
val cardAlpha = CardConfig.cardAlpha
|
.getBoolean("show_kpm_info", true)
|
||||||
val cardElevation = CardConfig.cardElevation
|
|
||||||
|
|
||||||
NavigationBar(
|
NavigationBar(
|
||||||
tonalElevation = cardElevation, // 动态设置阴影
|
modifier = Modifier.windowInsetsPadding(
|
||||||
containerColor = cardColor.copy(alpha = cardAlpha),
|
WindowInsets.navigationBars.only(WindowInsetsSides.Horizontal)
|
||||||
windowInsets = WindowInsets.systemBars.union(WindowInsets.displayCutout).only(
|
),
|
||||||
WindowInsetsSides.Horizontal + WindowInsetsSides.Bottom
|
containerColor = TopAppBarDefaults.topAppBarColors(
|
||||||
)
|
containerColor = cardColor.copy(alpha = cardAlpha),
|
||||||
|
scrolledContainerColor = cardColor.copy(alpha = cardAlpha)
|
||||||
|
).containerColor,
|
||||||
|
tonalElevation = cardElevation
|
||||||
) {
|
) {
|
||||||
BottomBarDestination.entries.forEach { destination ->
|
BottomBarDestination.entries.forEach { destination ->
|
||||||
if (destination == BottomBarDestination.Kpm) {
|
if (destination == BottomBarDestination.Kpm) {
|
||||||
if (kpmVersion.isNotEmpty() && !kpmVersion.startsWith("Error")) {
|
if (kpmVersion.isNotEmpty() && !kpmVersion.startsWith("Error") && showKpmInfo && Natives.version >= Natives.MINIMAL_SUPPORTED_KPM) {
|
||||||
if (!fullFeatured && destination.rootRequired) return@forEach
|
if (!fullFeatured && destination.rootRequired) return@forEach
|
||||||
val isCurrentDestOnBackStack by navController.isRouteOnBackStackAsState(destination.direction)
|
val isCurrentDestOnBackStack by navController.isRouteOnBackStackAsState(destination.direction)
|
||||||
NavigationBarItem(
|
NavigationBarItem(
|
||||||
selected = isCurrentDestOnBackStack,
|
selected = isCurrentDestOnBackStack,
|
||||||
onClick = {
|
onClick = {
|
||||||
if (isCurrentDestOnBackStack) {
|
if (!isCurrentDestOnBackStack) {
|
||||||
navigator.popBackStack(destination.direction, false)
|
navigator.popBackStack(destination.direction, false)
|
||||||
}
|
}
|
||||||
navigator.navigate(destination.direction) {
|
navigator.navigate(destination.direction) {
|
||||||
@@ -128,11 +332,8 @@ private fun BottomBar(navController: NavHostController) {
|
|||||||
Icon(destination.iconNotSelected, stringResource(destination.label))
|
Icon(destination.iconNotSelected, stringResource(destination.label))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
label = { Text(stringResource(destination.label)) },
|
label = { Text(stringResource(destination.label),style = MaterialTheme.typography.labelMedium) },
|
||||||
alwaysShowLabel = false,
|
alwaysShowLabel = false
|
||||||
colors = NavigationBarItemDefaults.colors(
|
|
||||||
unselectedTextColor = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -145,7 +346,7 @@ private fun BottomBar(navController: NavHostController) {
|
|||||||
navigator.popBackStack(destination.direction, false)
|
navigator.popBackStack(destination.direction, false)
|
||||||
}
|
}
|
||||||
navigator.navigate(destination.direction) {
|
navigator.navigate(destination.direction) {
|
||||||
popUpTo(NavGraphs.root as RouteOrDirection) {
|
popUpTo(NavGraphs.root) {
|
||||||
saveState = true
|
saveState = true
|
||||||
}
|
}
|
||||||
launchSingleTop = true
|
launchSingleTop = true
|
||||||
@@ -159,8 +360,8 @@ private fun BottomBar(navController: NavHostController) {
|
|||||||
Icon(destination.iconNotSelected, stringResource(destination.label))
|
Icon(destination.iconNotSelected, stringResource(destination.label))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
label = { Text(stringResource(destination.label)) },
|
label = { Text(stringResource(destination.label),style = MaterialTheme.typography.labelMedium) },
|
||||||
alwaysShowLabel = false,
|
alwaysShowLabel = false
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,7 +48,6 @@ fun ImageEditorDialog(
|
|||||||
var offsetY by remember { mutableFloatStateOf(0f) }
|
var offsetY by remember { mutableFloatStateOf(0f) }
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val density = LocalDensity.current
|
|
||||||
var lastScale by remember { mutableFloatStateOf(1f) }
|
var lastScale by remember { mutableFloatStateOf(1f) }
|
||||||
var lastOffsetX by remember { mutableFloatStateOf(0f) }
|
var lastOffsetX by remember { mutableFloatStateOf(0f) }
|
||||||
var lastOffsetY by remember { mutableFloatStateOf(0f) }
|
var lastOffsetY by remember { mutableFloatStateOf(0f) }
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.surfaceContainerLow
|
||||||
val cardAlpha = CardConfig.cardAlpha
|
val cardAlpha = CardConfig.cardAlpha
|
||||||
|
|
||||||
if (onSearch) {
|
if (onSearch) {
|
||||||
|
|||||||
@@ -5,14 +5,17 @@ import androidx.compose.foundation.interaction.MutableInteractionSource
|
|||||||
import androidx.compose.foundation.selection.toggleable
|
import androidx.compose.foundation.selection.toggleable
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.ListItem
|
import androidx.compose.material3.ListItem
|
||||||
import androidx.compose.material3.RadioButton
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Switch
|
import androidx.compose.material3.Switch
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.alpha
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.semantics.Role
|
import androidx.compose.ui.semantics.Role
|
||||||
|
import com.dergoogler.mmrl.ui.component.LabelItem
|
||||||
|
import com.dergoogler.mmrl.ui.component.text.TextRow
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SwitchItem(
|
fun SwitchItem(
|
||||||
@@ -21,9 +24,11 @@ fun SwitchItem(
|
|||||||
summary: String? = null,
|
summary: String? = null,
|
||||||
checked: Boolean,
|
checked: Boolean,
|
||||||
enabled: Boolean = true,
|
enabled: Boolean = true,
|
||||||
onCheckedChange: (Boolean) -> Unit
|
beta: Boolean = false,
|
||||||
|
onCheckedChange: (Boolean) -> Unit,
|
||||||
) {
|
) {
|
||||||
val interactionSource = remember { MutableInteractionSource() }
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
|
val stateAlpha = remember(checked, enabled) { Modifier.alpha(if (enabled) 1f else 0.5f) }
|
||||||
|
|
||||||
ListItem(
|
ListItem(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -36,10 +41,31 @@ fun SwitchItem(
|
|||||||
onValueChange = onCheckedChange
|
onValueChange = onCheckedChange
|
||||||
),
|
),
|
||||||
headlineContent = {
|
headlineContent = {
|
||||||
Text(title)
|
TextRow(
|
||||||
|
leadingContent = if (beta) {
|
||||||
|
{
|
||||||
|
LabelItem(
|
||||||
|
modifier = Modifier.then(stateAlpha),
|
||||||
|
text = "Beta"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else null
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.then(stateAlpha),
|
||||||
|
text = title,
|
||||||
|
)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
leadingContent = icon?.let {
|
leadingContent = icon?.let {
|
||||||
{ Icon(icon, title) }
|
{
|
||||||
|
Icon(
|
||||||
|
modifier = Modifier.then(stateAlpha),
|
||||||
|
imageVector = icon,
|
||||||
|
contentDescription = title,
|
||||||
|
tint = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
trailingContent = {
|
trailingContent = {
|
||||||
Switch(
|
Switch(
|
||||||
@@ -51,24 +77,11 @@ fun SwitchItem(
|
|||||||
},
|
},
|
||||||
supportingContent = {
|
supportingContent = {
|
||||||
if (summary != null) {
|
if (summary != null) {
|
||||||
Text(summary)
|
Text(
|
||||||
|
modifier = Modifier.then(stateAlpha),
|
||||||
|
text = summary
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun RadioItem(
|
|
||||||
title: String,
|
|
||||||
selected: Boolean,
|
|
||||||
onClick: () -> Unit,
|
|
||||||
) {
|
|
||||||
ListItem(
|
|
||||||
headlineContent = {
|
|
||||||
Text(title)
|
|
||||||
},
|
|
||||||
leadingContent = {
|
|
||||||
RadioButton(selected = selected, onClick = onClick)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
package com.sukisu.ultra.ui.component
|
package com.sukisu.ultra.ui.component
|
||||||
|
|
||||||
import android.content.Context
|
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.layout.*
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.material3.HorizontalDivider
|
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
@@ -11,15 +14,16 @@ import androidx.compose.ui.platform.LocalContext
|
|||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.window.Dialog
|
|
||||||
import com.sukisu.ultra.R
|
import com.sukisu.ultra.R
|
||||||
import com.sukisu.ultra.ui.theme.ThemeConfig
|
import com.sukisu.ultra.ui.theme.ThemeConfig
|
||||||
import com.sukisu.ultra.ui.theme.getCardElevation
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.foundation.shape.CornerSize
|
import androidx.compose.material.icons.filled.SdStorage
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 槽位选择对话框组件
|
* 槽位选择对话框组件
|
||||||
* 用于HorizonKernel刷写时选择目标槽位
|
* 用于Kernel刷写时选择目标槽位
|
||||||
*/
|
*/
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -43,38 +47,22 @@ fun SlotSelectionDialog(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (show) {
|
if (show) {
|
||||||
val backgroundColor = if (!ThemeConfig.useDynamicColor) {
|
val cardColor = MaterialTheme.colorScheme.surfaceContainerHighest
|
||||||
ThemeConfig.currentTheme.ButtonContrast.copy(alpha = 1.0f)
|
|
||||||
} else {
|
|
||||||
MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 1.0f)
|
|
||||||
}
|
|
||||||
|
|
||||||
Dialog(onDismissRequest = onDismiss) {
|
AlertDialog(
|
||||||
Card(
|
onDismissRequest = onDismiss,
|
||||||
shape = MaterialTheme.shapes.medium.copy(
|
title = {
|
||||||
topStart = CornerSize(16.dp),
|
Text(
|
||||||
topEnd = CornerSize(16.dp),
|
text = stringResource(id = R.string.select_slot_title),
|
||||||
bottomEnd = CornerSize(16.dp),
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
bottomStart = CornerSize(16.dp)
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
),
|
)
|
||||||
colors = CardDefaults.cardColors(
|
},
|
||||||
containerColor = backgroundColor
|
text = {
|
||||||
),
|
|
||||||
elevation = CardDefaults.cardElevation(defaultElevation = getCardElevation())
|
|
||||||
) {
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier.padding(vertical = 8.dp),
|
||||||
.padding(24.dp)
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
.fillMaxWidth(),
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
|
||||||
) {
|
) {
|
||||||
Text(
|
|
||||||
text = stringResource(id = R.string.select_slot_title),
|
|
||||||
style = MaterialTheme.typography.headlineSmall,
|
|
||||||
textAlign = TextAlign.Center,
|
|
||||||
modifier = Modifier.padding(bottom = 16.dp)
|
|
||||||
)
|
|
||||||
|
|
||||||
if (errorMessage != null) {
|
if (errorMessage != null) {
|
||||||
Text(
|
Text(
|
||||||
text = "Error: $errorMessage",
|
text = "Error: $errorMessage",
|
||||||
@@ -105,88 +93,134 @@ fun SlotSelectionDialog(
|
|||||||
|
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
|
// Horizontal arrangement for slot options with highlighted current slot
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier
|
||||||
horizontalArrangement = Arrangement.spacedBy(6.dp)
|
.fillMaxWidth()
|
||||||
) {
|
.horizontalScroll(rememberScrollState()),
|
||||||
val isDefaultSlotA = currentSlot == "_a" || currentSlot == "a"
|
|
||||||
Button(
|
|
||||||
onClick = { onSlotSelected("a") },
|
|
||||||
modifier = Modifier.weight(1f),
|
|
||||||
colors = ButtonDefaults.buttonColors(
|
|
||||||
containerColor = if (isDefaultSlotA)
|
|
||||||
MaterialTheme.colorScheme.primary
|
|
||||||
else
|
|
||||||
MaterialTheme.colorScheme.primaryContainer,
|
|
||||||
contentColor = if (isDefaultSlotA)
|
|
||||||
MaterialTheme.colorScheme.onPrimary
|
|
||||||
else
|
|
||||||
MaterialTheme.colorScheme.onPrimaryContainer
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(id = R.string.slot_a),
|
|
||||||
style = MaterialTheme.typography.labelLarge
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val isDefaultSlotB = currentSlot == "_b" || currentSlot == "b"
|
|
||||||
Button(
|
|
||||||
onClick = { onSlotSelected("b") },
|
|
||||||
modifier = Modifier.weight(1f),
|
|
||||||
colors = ButtonDefaults.buttonColors(
|
|
||||||
containerColor = if (isDefaultSlotB)
|
|
||||||
MaterialTheme.colorScheme.secondary
|
|
||||||
else
|
|
||||||
MaterialTheme.colorScheme.secondaryContainer,
|
|
||||||
contentColor = if (isDefaultSlotB)
|
|
||||||
MaterialTheme.colorScheme.onSecondary
|
|
||||||
else
|
|
||||||
MaterialTheme.colorScheme.onSecondaryContainer
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(id = R.string.slot_b),
|
|
||||||
style = MaterialTheme.typography.labelLarge
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
|
||||||
|
|
||||||
HorizontalDivider(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
thickness = 1.dp,
|
|
||||||
color = MaterialTheme.colorScheme.outline
|
|
||||||
)
|
|
||||||
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
) {
|
) {
|
||||||
TextButton(
|
val slotOptions = listOf(
|
||||||
onClick = onDismiss,
|
ListOption(
|
||||||
modifier = Modifier.weight(1f)
|
titleText = stringResource(id = R.string.slot_a),
|
||||||
) {
|
subtitleText = if (currentSlot == "a" || currentSlot == "_a") stringResource(id = R.string.currently_selected) else null,
|
||||||
Text(text = stringResource(id = android.R.string.cancel))
|
icon = Icons.Filled.SdStorage
|
||||||
}
|
),
|
||||||
TextButton(
|
ListOption(
|
||||||
onClick = {
|
titleText = stringResource(id = R.string.slot_b),
|
||||||
currentSlot?.let { onSlotSelected(it) }
|
subtitleText = if (currentSlot == "b" || currentSlot == "_b") stringResource(id = R.string.currently_selected) else null,
|
||||||
onDismiss()
|
icon = Icons.Filled.SdStorage
|
||||||
},
|
)
|
||||||
modifier = Modifier.weight(1f)
|
)
|
||||||
) {
|
|
||||||
Text(text = stringResource(id = android.R.string.ok))
|
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? {
|
private fun getCurrentSlot(context: Context): String? {
|
||||||
return runCommandGetOutput(true, "getprop ro.boot.slot_suffix")?.let {
|
return runCommandGetOutput(true, "getprop ro.boot.slot_suffix")?.let {
|
||||||
if (it.startsWith("_")) it.substring(1) else it
|
if (it.startsWith("_")) it.substring(1) else it
|
||||||
|
|||||||
@@ -1,32 +1,101 @@
|
|||||||
package com.sukisu.ultra.ui.component
|
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.Icon
|
||||||
import androidx.compose.material3.ListItem
|
import androidx.compose.material3.ListItem
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Switch
|
import androidx.compose.material3.Switch
|
||||||
|
import androidx.compose.material3.SwitchDefaults
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SwitchItem(
|
fun SwitchItem(
|
||||||
icon: ImageVector,
|
icon: ImageVector,
|
||||||
title: String,
|
title: String,
|
||||||
summary: String,
|
summary: String? = null,
|
||||||
checked: Boolean,
|
checked: Boolean,
|
||||||
modifier: Modifier = Modifier,
|
enabled: Boolean = true,
|
||||||
onCheckedChange: (Boolean) -> Unit
|
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(
|
ListItem(
|
||||||
modifier = modifier,
|
headlineContent = {
|
||||||
leadingContent = { Icon(icon, contentDescription = null) },
|
Text(
|
||||||
headlineContent = { Text(title) },
|
text = title,
|
||||||
supportingContent = { Text(summary) },
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
maxLines = Int.MAX_VALUE,
|
||||||
|
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 = {
|
trailingContent = {
|
||||||
Switch(
|
Switch(
|
||||||
checked = checked,
|
checked = checked,
|
||||||
onCheckedChange = onCheckedChange
|
onCheckedChange = null,
|
||||||
|
enabled = enabled,
|
||||||
|
colors = switchColors
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable(enabled = enabled) {
|
||||||
|
onCheckedChange(!checked)
|
||||||
|
}
|
||||||
|
.padding(vertical = 4.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -206,28 +206,36 @@ fun GroupsPanel(selected: List<Groups>, closeSelection: (selection: Set<Groups>)
|
|||||||
}
|
}
|
||||||
|
|
||||||
val selection = HashSet(selected)
|
val selection = HashSet(selected)
|
||||||
ListDialog(
|
val backgroundColor = MaterialTheme.colorScheme.surfaceContainerHighest
|
||||||
state = rememberUseCaseState(visible = true, onFinishedRequest = {
|
|
||||||
closeSelection(selection)
|
MaterialTheme(
|
||||||
}, onCloseRequest = {
|
colorScheme = MaterialTheme.colorScheme.copy(
|
||||||
dismiss()
|
surface = backgroundColor
|
||||||
}),
|
)
|
||||||
header = Header.Default(
|
) {
|
||||||
title = stringResource(R.string.profile_groups),
|
ListDialog(
|
||||||
),
|
state = rememberUseCaseState(visible = true, onFinishedRequest = {
|
||||||
selection = ListSelection.Multiple(
|
closeSelection(selection)
|
||||||
showCheckBoxes = true,
|
}, onCloseRequest = {
|
||||||
options = options,
|
dismiss()
|
||||||
maxChoices = 32, // Kernel only supports 32 groups at most
|
}),
|
||||||
) { indecies, _ ->
|
header = Header.Default(
|
||||||
// Handle selection
|
title = stringResource(R.string.profile_groups),
|
||||||
selection.clear()
|
),
|
||||||
indecies.forEach { index ->
|
selection = ListSelection.Multiple(
|
||||||
val group = groups[index]
|
showCheckBoxes = true,
|
||||||
selection.add(group)
|
options = options,
|
||||||
|
maxChoices = 32, // Kernel only supports 32 groups at most
|
||||||
|
) { indecies, _ ->
|
||||||
|
// Handle selection
|
||||||
|
selection.clear()
|
||||||
|
indecies.forEach { index ->
|
||||||
|
val group = groups[index]
|
||||||
|
selection.add(group)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
)
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
OutlinedCard(
|
OutlinedCard(
|
||||||
@@ -278,27 +286,35 @@ fun CapsPanel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val selection = HashSet(selected)
|
val selection = HashSet(selected)
|
||||||
ListDialog(
|
val backgroundColor = MaterialTheme.colorScheme.surfaceContainerHighest
|
||||||
state = rememberUseCaseState(visible = true, onFinishedRequest = {
|
|
||||||
closeSelection(selection)
|
MaterialTheme(
|
||||||
}, onCloseRequest = {
|
colorScheme = MaterialTheme.colorScheme.copy(
|
||||||
dismiss()
|
surface = backgroundColor
|
||||||
}),
|
)
|
||||||
header = Header.Default(
|
) {
|
||||||
title = stringResource(R.string.profile_capabilities),
|
ListDialog(
|
||||||
),
|
state = rememberUseCaseState(visible = true, onFinishedRequest = {
|
||||||
selection = ListSelection.Multiple(
|
closeSelection(selection)
|
||||||
showCheckBoxes = true,
|
}, onCloseRequest = {
|
||||||
options = options
|
dismiss()
|
||||||
) { indecies, _ ->
|
}),
|
||||||
// Handle selection
|
header = Header.Default(
|
||||||
selection.clear()
|
title = stringResource(R.string.profile_capabilities),
|
||||||
indecies.forEach { index ->
|
),
|
||||||
val group = caps[index]
|
selection = ListSelection.Multiple(
|
||||||
selection.add(group)
|
showCheckBoxes = true,
|
||||||
|
options = options
|
||||||
|
) { indecies, _ ->
|
||||||
|
// Handle selection
|
||||||
|
selection.clear()
|
||||||
|
indecies.forEach { index ->
|
||||||
|
val group = caps[index]
|
||||||
|
selection.add(group)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
)
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
OutlinedCard(
|
OutlinedCard(
|
||||||
@@ -425,24 +441,33 @@ private fun SELinuxPanel(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
InputDialog(
|
val backgroundColor = MaterialTheme.colorScheme.surfaceContainerHighest
|
||||||
state = rememberUseCaseState(visible = true,
|
|
||||||
onFinishedRequest = {
|
MaterialTheme(
|
||||||
onSELinuxChange(domain, rules)
|
colorScheme = MaterialTheme.colorScheme.copy(
|
||||||
},
|
surface = backgroundColor
|
||||||
onCloseRequest = {
|
|
||||||
dismiss()
|
|
||||||
}),
|
|
||||||
header = Header.Default(
|
|
||||||
title = stringResource(R.string.profile_selinux_context),
|
|
||||||
),
|
|
||||||
selection = InputSelection(
|
|
||||||
input = inputOptions,
|
|
||||||
onPositiveClick = { result ->
|
|
||||||
// Handle selection
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
)
|
) {
|
||||||
|
InputDialog(
|
||||||
|
state = rememberUseCaseState(
|
||||||
|
visible = true,
|
||||||
|
onFinishedRequest = {
|
||||||
|
onSELinuxChange(domain, rules)
|
||||||
|
},
|
||||||
|
onCloseRequest = {
|
||||||
|
dismiss()
|
||||||
|
}),
|
||||||
|
header = Header.Default(
|
||||||
|
title = stringResource(R.string.profile_selinux_context),
|
||||||
|
),
|
||||||
|
selection = InputSelection(
|
||||||
|
input = inputOptions,
|
||||||
|
onPositiveClick = { result ->
|
||||||
|
// Handle selection
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ListItem(headlineContent = {
|
ListItem(headlineContent = {
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
package com.sukisu.ultra.ui.screen
|
package com.sukisu.ultra.ui.screen
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.Crossfade
|
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.gestures.detectTapGestures
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||||
@@ -25,16 +31,21 @@ import androidx.compose.material.icons.filled.Android
|
|||||||
import androidx.compose.material.icons.filled.Security
|
import androidx.compose.material.icons.filled.Security
|
||||||
import androidx.compose.material3.DropdownMenu
|
import androidx.compose.material3.DropdownMenu
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
|
import androidx.compose.material3.ElevatedCard
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.FilterChip
|
import androidx.compose.material3.FilterChip
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.IconButtonDefaults
|
||||||
import androidx.compose.material3.ListItem
|
import androidx.compose.material3.ListItem
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.SnackbarHost
|
import androidx.compose.material3.SnackbarHost
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
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.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -44,7 +55,10 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
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.geometry.Offset
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
@@ -63,13 +77,13 @@ import com.ramcosta.composedestinations.annotation.RootGraph
|
|||||||
import com.ramcosta.composedestinations.generated.destinations.AppProfileTemplateScreenDestination
|
import com.ramcosta.composedestinations.generated.destinations.AppProfileTemplateScreenDestination
|
||||||
import com.ramcosta.composedestinations.generated.destinations.TemplateEditorScreenDestination
|
import com.ramcosta.composedestinations.generated.destinations.TemplateEditorScreenDestination
|
||||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import com.sukisu.ultra.Natives
|
import com.sukisu.ultra.Natives
|
||||||
import com.sukisu.ultra.R
|
import com.sukisu.ultra.R
|
||||||
import com.sukisu.ultra.ui.component.SwitchItem
|
import com.sukisu.ultra.ui.component.SwitchItem
|
||||||
import com.sukisu.ultra.ui.component.profile.AppProfileConfig
|
import com.sukisu.ultra.ui.component.profile.AppProfileConfig
|
||||||
import com.sukisu.ultra.ui.component.profile.RootProfileConfig
|
import com.sukisu.ultra.ui.component.profile.RootProfileConfig
|
||||||
import com.sukisu.ultra.ui.component.profile.TemplateConfig
|
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.LocalSnackbarHost
|
||||||
import com.sukisu.ultra.ui.util.forceStopApp
|
import com.sukisu.ultra.ui.util.forceStopApp
|
||||||
import com.sukisu.ultra.ui.util.getSepolicy
|
import com.sukisu.ultra.ui.util.getSepolicy
|
||||||
@@ -78,6 +92,7 @@ import com.sukisu.ultra.ui.util.restartApp
|
|||||||
import com.sukisu.ultra.ui.util.setSepolicy
|
import com.sukisu.ultra.ui.util.setSepolicy
|
||||||
import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel
|
import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel
|
||||||
import com.sukisu.ultra.ui.viewmodel.getTemplateInfoById
|
import com.sukisu.ultra.ui.viewmodel.getTemplateInfoById
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author weishu
|
* @author weishu
|
||||||
@@ -107,9 +122,18 @@ fun AppProfileScreen(
|
|||||||
mutableStateOf(initialProfile)
|
mutableStateOf(initialProfile)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val cardColor = MaterialTheme.colorScheme.surfaceContainerLow
|
||||||
|
val cardAlpha = CardConfig.cardAlpha
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
TopBar(
|
TopBar(
|
||||||
|
title = appInfo.label,
|
||||||
|
packageName = packageName,
|
||||||
|
colors = TopAppBarDefaults.topAppBarColors(
|
||||||
|
containerColor = cardColor.copy(alpha = cardAlpha),
|
||||||
|
scrolledContainerColor = cardColor.copy(alpha = cardAlpha)
|
||||||
|
),
|
||||||
onBack = dropUnlessResumed { navigator.popBackStack() },
|
onBack = dropUnlessResumed { navigator.popBackStack() },
|
||||||
scrollBehavior = scrollBehavior
|
scrollBehavior = scrollBehavior
|
||||||
)
|
)
|
||||||
@@ -181,22 +205,50 @@ private fun AppProfileInner(
|
|||||||
val isRootGranted = profile.allowSu
|
val isRootGranted = profile.allowSu
|
||||||
|
|
||||||
Column(modifier = modifier) {
|
Column(modifier = modifier) {
|
||||||
AppMenuBox(packageName) {
|
ElevatedCard(
|
||||||
ListItem(
|
modifier = Modifier
|
||||||
headlineContent = { Text(appLabel) },
|
.fillMaxWidth()
|
||||||
supportingContent = { Text(packageName) },
|
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||||
leadingContent = appIcon,
|
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)) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
SwitchItem(
|
Crossfade(
|
||||||
icon = Icons.Filled.Security,
|
targetState = isRootGranted,
|
||||||
title = stringResource(id = R.string.superuser),
|
label = "RootAccess"
|
||||||
checked = isRootGranted,
|
) { current ->
|
||||||
onCheckedChange = { onProfileChange(profile.copy(allowSu = it)) },
|
|
||||||
)
|
|
||||||
|
|
||||||
Crossfade(targetState = isRootGranted, label = "") { current ->
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.padding(bottom = 6.dp + 48.dp + 6.dp /* SnackBar height */)
|
modifier = Modifier.padding(bottom = 6.dp + 48.dp + 6.dp /* SnackBar height */)
|
||||||
) {
|
) {
|
||||||
@@ -211,42 +263,91 @@ private fun AppProfileInner(
|
|||||||
var mode by rememberSaveable {
|
var mode by rememberSaveable {
|
||||||
mutableStateOf(initialMode)
|
mutableStateOf(initialMode)
|
||||||
}
|
}
|
||||||
ProfileBox(mode, true) {
|
|
||||||
// template mode shouldn't change profile here!
|
ElevatedCard(
|
||||||
if (it == Mode.Default || it == Mode.Custom) {
|
modifier = Modifier
|
||||||
onProfileChange(profile.copy(rootUseDefault = it == Mode.Default))
|
.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
|
||||||
}
|
}
|
||||||
mode = it
|
|
||||||
}
|
}
|
||||||
Crossfade(targetState = mode, label = "") { currentMode ->
|
|
||||||
if (currentMode == Mode.Template) {
|
AnimatedVisibility(
|
||||||
TemplateConfig(
|
visible = mode != Mode.Default,
|
||||||
profile = profile,
|
enter = fadeIn() + expandVertically(),
|
||||||
onViewTemplate = onViewTemplate,
|
exit = fadeOut() + shrinkVertically()
|
||||||
onManageTemplate = onManageTemplate,
|
) {
|
||||||
onProfileChange = onProfileChange
|
ElevatedCard(
|
||||||
)
|
modifier = Modifier
|
||||||
} else if (mode == Mode.Custom) {
|
.fillMaxWidth()
|
||||||
RootProfileConfig(
|
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||||
fixedName = true,
|
shape = MaterialTheme.shapes.medium
|
||||||
profile = profile,
|
) {
|
||||||
onProfileChange = onProfileChange
|
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 {
|
} else {
|
||||||
val mode = if (profile.nonRootUseDefault) Mode.Default else Mode.Custom
|
val mode = if (profile.nonRootUseDefault) Mode.Default else Mode.Custom
|
||||||
ProfileBox(mode, false) {
|
|
||||||
onProfileChange(profile.copy(nonRootUseDefault = (it == Mode.Default)))
|
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)))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Crossfade(targetState = mode, label = "") { currentMode ->
|
|
||||||
val modifyEnabled = currentMode == Mode.Custom
|
AnimatedVisibility(
|
||||||
AppProfileConfig(
|
visible = mode == Mode.Custom,
|
||||||
fixedName = true,
|
enter = fadeIn() + expandVertically(),
|
||||||
profile = profile,
|
exit = fadeOut() + shrinkVertically()
|
||||||
enabled = modifyEnabled,
|
) {
|
||||||
onProfileChange = onProfileChange
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -264,20 +365,51 @@ private enum class Mode(@StringRes private val res: Int) {
|
|||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
private fun TopBar(
|
private fun TopBar(
|
||||||
|
title: String,
|
||||||
|
packageName: String,
|
||||||
onBack: () -> Unit,
|
onBack: () -> Unit,
|
||||||
|
colors: TopAppBarColors,
|
||||||
scrollBehavior: TopAppBarScrollBehavior? = null
|
scrollBehavior: TopAppBarScrollBehavior? = null
|
||||||
) {
|
) {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = {
|
title = {
|
||||||
Text(stringResource(R.string.profile))
|
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 = {
|
navigationIcon = {
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = onBack
|
onClick = onBack,
|
||||||
) { Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null) }
|
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),
|
windowInsets = WindowInsets.safeDrawing.only(
|
||||||
scrollBehavior = scrollBehavior
|
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)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -287,40 +419,88 @@ private fun ProfileBox(
|
|||||||
hasTemplate: Boolean,
|
hasTemplate: Boolean,
|
||||||
onModeChange: (Mode) -> Unit,
|
onModeChange: (Mode) -> Unit,
|
||||||
) {
|
) {
|
||||||
ListItem(
|
Column(modifier = Modifier.padding(vertical = 8.dp)) {
|
||||||
headlineContent = { Text(stringResource(R.string.profile)) },
|
ListItem(
|
||||||
supportingContent = { Text(mode.text) },
|
headlineContent = {
|
||||||
leadingContent = { Icon(Icons.Filled.AccountCircle, null) },
|
Text(
|
||||||
)
|
text = stringResource(R.string.profile),
|
||||||
HorizontalDivider(thickness = Dp.Hairline)
|
style = MaterialTheme.typography.titleMedium
|
||||||
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) },
|
|
||||||
)
|
)
|
||||||
|
},
|
||||||
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
FilterChip(
|
)
|
||||||
selected = mode == Mode.Custom,
|
}
|
||||||
label = { Text(stringResource(R.string.profile_custom)) },
|
|
||||||
onClick = { onModeChange(Mode.Custom) },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("UnusedBoxWithConstraintsScope")
|
||||||
@Composable
|
@Composable
|
||||||
private fun AppMenuBox(packageName: String, content: @Composable () -> Unit) {
|
private fun AppMenuBox(packageName: String, content: @Composable () -> Unit) {
|
||||||
|
|
||||||
var expanded by remember { mutableStateOf(false) }
|
var expanded by remember { mutableStateOf(false) }
|
||||||
var touchPoint: Offset by remember { mutableStateOf(Offset.Zero) }
|
var touchPoint: Offset by remember { mutableStateOf(Offset.Zero) }
|
||||||
val density = LocalDensity.current
|
val density = LocalDensity.current
|
||||||
@@ -329,13 +509,14 @@ private fun AppMenuBox(packageName: String, content: @Composable () -> Unit) {
|
|||||||
Modifier
|
Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.pointerInput(Unit) {
|
.pointerInput(Unit) {
|
||||||
detectTapGestures {
|
detectTapGestures(
|
||||||
touchPoint = it
|
onLongPress = {
|
||||||
expanded = true
|
touchPoint = it
|
||||||
}
|
expanded = true
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
|
|
||||||
content()
|
content()
|
||||||
|
|
||||||
val (offsetX, offsetY) = with(density) {
|
val (offsetX, offsetY) = with(density) {
|
||||||
@@ -349,45 +530,64 @@ private fun AppMenuBox(packageName: String, content: @Composable () -> Unit) {
|
|||||||
expanded = false
|
expanded = false
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
DropdownMenuItem(
|
AppMenuOption(
|
||||||
text = { Text(stringResource(id = R.string.launch_app)) },
|
text = stringResource(id = R.string.launch_app),
|
||||||
onClick = {
|
onClick = {
|
||||||
expanded = false
|
expanded = false
|
||||||
launchApp(packageName)
|
launchApp(packageName)
|
||||||
},
|
}
|
||||||
)
|
)
|
||||||
DropdownMenuItem(
|
|
||||||
text = { Text(stringResource(id = R.string.force_stop_app)) },
|
AppMenuOption(
|
||||||
|
text = stringResource(id = R.string.force_stop_app),
|
||||||
onClick = {
|
onClick = {
|
||||||
expanded = false
|
expanded = false
|
||||||
forceStopApp(packageName)
|
forceStopApp(packageName)
|
||||||
},
|
}
|
||||||
)
|
)
|
||||||
DropdownMenuItem(
|
|
||||||
text = { Text(stringResource(id = R.string.restart_app)) },
|
AppMenuOption(
|
||||||
|
text = stringResource(id = R.string.restart_app),
|
||||||
onClick = {
|
onClick = {
|
||||||
expanded = false
|
expanded = false
|
||||||
restartApp(packageName)
|
restartApp(packageName)
|
||||||
},
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun AppMenuOption(text: String, onClick: () -> Unit) {
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = {
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
style = MaterialTheme.typography.bodyMedium
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onClick = onClick
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Preview
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
private fun AppProfilePreview() {
|
private fun AppProfilePreview() {
|
||||||
var profile by remember { mutableStateOf(Natives.Profile("")) }
|
var profile by remember { mutableStateOf(Natives.Profile("")) }
|
||||||
AppProfileInner(
|
Surface {
|
||||||
packageName = "icu.nullptr.test",
|
AppProfileInner(
|
||||||
appLabel = "Test",
|
packageName = "icu.nullptr.test",
|
||||||
appIcon = { Icon(Icons.Filled.Android, null) },
|
appLabel = "Test",
|
||||||
profile = profile,
|
appIcon = {
|
||||||
onProfileChange = {
|
Icon(
|
||||||
profile = it
|
imageVector = Icons.Filled.Android,
|
||||||
},
|
contentDescription = null,
|
||||||
)
|
)
|
||||||
|
},
|
||||||
|
profile = profile,
|
||||||
|
onProfileChange = {
|
||||||
|
profile = it
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ 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
|
||||||
import androidx.compose.material.icons.filled.*
|
import androidx.compose.material.icons.filled.*
|
||||||
|
import androidx.compose.material.icons.filled.Archive
|
||||||
import androidx.compose.material.icons.outlined.*
|
import androidx.compose.material.icons.outlined.*
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import com.ramcosta.composedestinations.generated.destinations.HomeScreenDestination
|
import com.ramcosta.composedestinations.generated.destinations.HomeScreenDestination
|
||||||
@@ -21,8 +22,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),
|
Kpm(KpmScreenDestination, R.string.kpm_title, Icons.Filled.Archive, Icons.Outlined.Archive, true),
|
||||||
SuperUser(SuperUserScreenDestination, R.string.superuser, Icons.Filled.Security, Icons.Outlined.Security, true),
|
SuperUser(SuperUserScreenDestination, R.string.superuser, Icons.Filled.AdminPanelSettings, Icons.Outlined.AdminPanelSettings, true),
|
||||||
Module(ModuleScreenDestination, R.string.module, Icons.Filled.Apps, Icons.Outlined.Apps, true),
|
Module(ModuleScreenDestination, R.string.module, Icons.Filled.Extension, Icons.Outlined.Extension, 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,15 +1,22 @@
|
|||||||
package com.sukisu.ultra.ui.screen
|
package com.sukisu.ultra.ui.screen
|
||||||
|
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
|
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||||
|
import androidx.compose.foundation.layout.defaultMinSize
|
||||||
|
import androidx.compose.foundation.layout.safeDrawing
|
||||||
|
import androidx.compose.foundation.layout.only
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
import androidx.compose.material.icons.filled.Close
|
||||||
import androidx.compose.material.icons.filled.Save
|
import androidx.compose.material.icons.filled.Save
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.ExtendedFloatingActionButton
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
@@ -30,7 +37,6 @@ import androidx.compose.ui.input.key.key
|
|||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.compose.dropUnlessResumed
|
|
||||||
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.navigation.DestinationsNavigator
|
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||||
@@ -55,7 +61,11 @@ fun ExecuteModuleActionScreen(navigator: DestinationsNavigator, moduleId: String
|
|||||||
val snackBarHost = LocalSnackbarHost.current
|
val snackBarHost = LocalSnackbarHost.current
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val scrollState = rememberScrollState()
|
val scrollState = rememberScrollState()
|
||||||
var actionResult: Boolean
|
var isActionRunning by rememberSaveable { mutableStateOf(true) }
|
||||||
|
|
||||||
|
BackHandler(enabled = isActionRunning) {
|
||||||
|
// Disable back button if action is running
|
||||||
|
}
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
if (text.isNotEmpty()) {
|
if (text.isNotEmpty()) {
|
||||||
@@ -76,33 +86,43 @@ fun ExecuteModuleActionScreen(navigator: DestinationsNavigator, moduleId: String
|
|||||||
onStderr = {
|
onStderr = {
|
||||||
logContent.append(it).append("\n")
|
logContent.append(it).append("\n")
|
||||||
}
|
}
|
||||||
).let {
|
)
|
||||||
actionResult = it
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (actionResult) navigator.popBackStack()
|
isActionRunning = false
|
||||||
}
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
TopBar(
|
TopBar(
|
||||||
onBack = dropUnlessResumed {
|
isActionRunning = isActionRunning,
|
||||||
navigator.popBackStack()
|
|
||||||
},
|
|
||||||
onSave = {
|
onSave = {
|
||||||
scope.launch {
|
if (!isActionRunning) {
|
||||||
val format = SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.getDefault())
|
scope.launch {
|
||||||
val date = format.format(Date())
|
val format = SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.getDefault())
|
||||||
val file = File(
|
val date = format.format(Date())
|
||||||
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
|
val file = File(
|
||||||
"KernelSU_module_action_log_${date}.log"
|
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
|
||||||
)
|
"KernelSU_module_action_log_${date}.log"
|
||||||
file.writeText(logContent.toString())
|
)
|
||||||
snackBarHost.showSnackbar("Log saved to ${file.absolutePath}")
|
file.writeText(logContent.toString())
|
||||||
|
snackBarHost.showSnackbar("Log saved to ${file.absolutePath}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
floatingActionButton = {
|
||||||
|
if (!isActionRunning) {
|
||||||
|
ExtendedFloatingActionButton(
|
||||||
|
text = { Text(text = stringResource(R.string.close)) },
|
||||||
|
icon = { Icon(Icons.Filled.Close, contentDescription = null) },
|
||||||
|
onClick = {
|
||||||
|
navigator.popBackStack()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
contentWindowInsets = WindowInsets.safeDrawing,
|
||||||
snackbarHost = { SnackbarHost(snackBarHost) }
|
snackbarHost = { SnackbarHost(snackBarHost) }
|
||||||
) { innerPadding ->
|
) { innerPadding ->
|
||||||
KeyEventBlocker {
|
KeyEventBlocker {
|
||||||
@@ -130,16 +150,14 @@ fun ExecuteModuleActionScreen(navigator: DestinationsNavigator, moduleId: String
|
|||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
private fun TopBar(onBack: () -> Unit = {}, onSave: () -> Unit = {}) {
|
private fun TopBar(isActionRunning: Boolean, onSave: () -> Unit = {}) {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = { Text(stringResource(R.string.action)) },
|
title = { Text(stringResource(R.string.action)) },
|
||||||
navigationIcon = {
|
|
||||||
IconButton(
|
|
||||||
onClick = onBack
|
|
||||||
) { Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null) }
|
|
||||||
},
|
|
||||||
actions = {
|
actions = {
|
||||||
IconButton(onClick = onSave) {
|
IconButton(
|
||||||
|
onClick = onSave,
|
||||||
|
enabled = !isActionRunning
|
||||||
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Filled.Save,
|
imageVector = Icons.Filled.Save,
|
||||||
contentDescription = stringResource(id = R.string.save_log),
|
contentDescription = stringResource(id = R.string.save_log),
|
||||||
|
|||||||
@@ -3,27 +3,40 @@ package com.sukisu.ultra.ui.screen
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
|
import androidx.activity.compose.BackHandler
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
|
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.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
|
import androidx.compose.material.icons.filled.Error
|
||||||
import androidx.compose.material.icons.filled.Refresh
|
import androidx.compose.material.icons.filled.Refresh
|
||||||
import androidx.compose.material.icons.filled.Save
|
import androidx.compose.material.icons.filled.Save
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.input.key.Key
|
import androidx.compose.ui.input.key.Key
|
||||||
import androidx.compose.ui.input.key.key
|
import androidx.compose.ui.input.key.key
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
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 androidx.lifecycle.compose.dropUnlessResumed
|
|
||||||
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.generated.destinations.FlashScreenDestination
|
||||||
|
import com.ramcosta.composedestinations.generated.destinations.ModuleScreenDestination
|
||||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||||
import com.ramcosta.composedestinations.navigation.EmptyDestinationsNavigator
|
import com.ramcosta.composedestinations.navigation.EmptyDestinationsNavigator
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -33,10 +46,17 @@ import kotlinx.parcelize.Parcelize
|
|||||||
import com.sukisu.ultra.ui.component.KeyEventBlocker
|
import com.sukisu.ultra.ui.component.KeyEventBlocker
|
||||||
import com.sukisu.ultra.ui.util.*
|
import com.sukisu.ultra.ui.util.*
|
||||||
import com.sukisu.ultra.R
|
import com.sukisu.ultra.R
|
||||||
|
import com.sukisu.ultra.ui.theme.CardConfig
|
||||||
|
import com.sukisu.ultra.ui.viewmodel.ModuleViewModel
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author ShirkNeko
|
||||||
|
* @date 2025/5/31.
|
||||||
|
*/
|
||||||
enum class FlashingStatus {
|
enum class FlashingStatus {
|
||||||
FLASHING,
|
FLASHING,
|
||||||
SUCCESS,
|
SUCCESS,
|
||||||
@@ -45,14 +65,47 @@ enum class FlashingStatus {
|
|||||||
|
|
||||||
private var currentFlashingStatus = mutableStateOf(FlashingStatus.FLASHING)
|
private var currentFlashingStatus = mutableStateOf(FlashingStatus.FLASHING)
|
||||||
|
|
||||||
|
// 添加模块安装状态跟踪
|
||||||
|
data class ModuleInstallStatus(
|
||||||
|
val totalModules: Int = 0,
|
||||||
|
val currentModule: Int = 0,
|
||||||
|
val currentModuleName: String = "",
|
||||||
|
val failedModules: MutableList<String> = mutableListOf()
|
||||||
|
)
|
||||||
|
|
||||||
|
private var moduleInstallStatus = mutableStateOf(ModuleInstallStatus())
|
||||||
|
|
||||||
fun setFlashingStatus(status: FlashingStatus) {
|
fun setFlashingStatus(status: FlashingStatus) {
|
||||||
currentFlashingStatus.value = status
|
currentFlashingStatus.value = status
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun updateModuleInstallStatus(
|
||||||
|
totalModules: Int? = null,
|
||||||
|
currentModule: Int? = null,
|
||||||
|
currentModuleName: String? = null,
|
||||||
|
failedModule: String? = null
|
||||||
|
) {
|
||||||
|
val current = moduleInstallStatus.value
|
||||||
|
moduleInstallStatus.value = current.copy(
|
||||||
|
totalModules = totalModules ?: current.totalModules,
|
||||||
|
currentModule = currentModule ?: current.currentModule,
|
||||||
|
currentModuleName = currentModuleName ?: current.currentModuleName
|
||||||
|
)
|
||||||
|
|
||||||
|
if (failedModule != null) {
|
||||||
|
val updatedFailedModules = current.failedModules.toMutableList()
|
||||||
|
updatedFailedModules.add(failedModule)
|
||||||
|
moduleInstallStatus.value = moduleInstallStatus.value.copy(
|
||||||
|
failedModules = updatedFailedModules
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
@Destination<RootGraph>
|
@Destination<RootGraph>
|
||||||
fun FlashScreen(navigator: DestinationsNavigator, flashIt: FlashIt) {
|
fun FlashScreen(navigator: DestinationsNavigator, flashIt: FlashIt) {
|
||||||
|
val context = LocalContext.current
|
||||||
var text by rememberSaveable { mutableStateOf("") }
|
var text by rememberSaveable { mutableStateOf("") }
|
||||||
var tempText: String
|
var tempText: String
|
||||||
val logContent = rememberSaveable { StringBuilder() }
|
val logContent = rememberSaveable { StringBuilder() }
|
||||||
@@ -62,6 +115,25 @@ fun FlashScreen(navigator: DestinationsNavigator, flashIt: FlashIt) {
|
|||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val scrollState = rememberScrollState()
|
val scrollState = rememberScrollState()
|
||||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||||
|
val viewModel: ModuleViewModel = viewModel()
|
||||||
|
|
||||||
|
val errorCodeString = stringResource(R.string.error_code)
|
||||||
|
val checkLogString = stringResource(R.string.check_log)
|
||||||
|
val logSavedString = stringResource(R.string.log_saved)
|
||||||
|
val installingModuleString = stringResource(R.string.installing_module)
|
||||||
|
|
||||||
|
// 当前模块安装状态
|
||||||
|
val currentStatus = moduleInstallStatus.value
|
||||||
|
|
||||||
|
// 重置状态
|
||||||
|
LaunchedEffect(flashIt) {
|
||||||
|
if (flashIt is FlashIt.FlashModules && flashIt.currentIndex == 0) {
|
||||||
|
moduleInstallStatus.value = ModuleInstallStatus(
|
||||||
|
totalModules = flashIt.uris.size,
|
||||||
|
currentModule = 1
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
if (text.isNotEmpty()) {
|
if (text.isNotEmpty()) {
|
||||||
@@ -69,17 +141,50 @@ fun FlashScreen(navigator: DestinationsNavigator, flashIt: FlashIt) {
|
|||||||
}
|
}
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
setFlashingStatus(FlashingStatus.FLASHING)
|
setFlashingStatus(FlashingStatus.FLASHING)
|
||||||
flashIt(flashIt, onFinish = { showReboot, code ->
|
|
||||||
|
if (flashIt is FlashIt.FlashModules) {
|
||||||
|
try {
|
||||||
|
val currentUri = flashIt.uris[flashIt.currentIndex]
|
||||||
|
val moduleName = getModuleNameFromUri(context, currentUri)
|
||||||
|
updateModuleInstallStatus(
|
||||||
|
currentModuleName = moduleName
|
||||||
|
)
|
||||||
|
text = installingModuleString.format(flashIt.currentIndex + 1, flashIt.uris.size, moduleName)
|
||||||
|
logContent.append(text).append("\n")
|
||||||
|
} catch (_: Exception) {
|
||||||
|
text = installingModuleString.format(flashIt.currentIndex + 1, flashIt.uris.size, "Module")
|
||||||
|
logContent.append(text).append("\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
flashIt(context, flashIt, onFinish = { showReboot, code ->
|
||||||
if (code != 0) {
|
if (code != 0) {
|
||||||
text += "Error: exit code = $code.\nPlease save and check the log.\n"
|
text += "$errorCodeString $code.\n$checkLogString\n"
|
||||||
setFlashingStatus(FlashingStatus.FAILED)
|
setFlashingStatus(FlashingStatus.FAILED)
|
||||||
|
|
||||||
|
if (flashIt is FlashIt.FlashModules) {
|
||||||
|
updateModuleInstallStatus(
|
||||||
|
failedModule = moduleInstallStatus.value.currentModuleName
|
||||||
|
)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
setFlashingStatus(FlashingStatus.SUCCESS)
|
setFlashingStatus(FlashingStatus.SUCCESS)
|
||||||
|
viewModel.markNeedRefresh()
|
||||||
}
|
}
|
||||||
if (showReboot) {
|
if (showReboot) {
|
||||||
text += "\n\n\n"
|
text += "\n\n\n"
|
||||||
showFloatAction = true
|
showFloatAction = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (flashIt is FlashIt.FlashModules && flashIt.currentIndex < flashIt.uris.size - 1) {
|
||||||
|
val nextFlashIt = flashIt.copy(
|
||||||
|
currentIndex = flashIt.currentIndex + 1
|
||||||
|
)
|
||||||
|
scope.launch {
|
||||||
|
kotlinx.coroutines.delay(500)
|
||||||
|
navigator.navigate(FlashScreenDestination(nextFlashIt))
|
||||||
|
}
|
||||||
|
}
|
||||||
}, onStdout = {
|
}, onStdout = {
|
||||||
tempText = "$it\n"
|
tempText = "$it\n"
|
||||||
if (tempText.startsWith("[H[J")) { // clear command
|
if (tempText.startsWith("[H[J")) { // clear command
|
||||||
@@ -94,13 +199,31 @@ fun FlashScreen(navigator: DestinationsNavigator, flashIt: FlashIt) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val onBack: () -> Unit = {
|
||||||
|
if (currentFlashingStatus.value != FlashingStatus.FLASHING) {
|
||||||
|
if (flashIt is FlashIt.FlashBoot) {
|
||||||
|
navigator.popBackStack()
|
||||||
|
} else {
|
||||||
|
viewModel.markNeedRefresh()
|
||||||
|
viewModel.fetchModuleList()
|
||||||
|
navigator.navigate(ModuleScreenDestination) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BackHandler(enabled = true) {
|
||||||
|
onBack()
|
||||||
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
TopBar(
|
TopBar(
|
||||||
currentFlashingStatus.value,
|
currentFlashingStatus.value,
|
||||||
onBack = dropUnlessResumed {
|
currentStatus,
|
||||||
navigator.popBackStack()
|
navigator = navigator,
|
||||||
},
|
flashIt = flashIt,
|
||||||
|
onBack = onBack,
|
||||||
onSave = {
|
onSave = {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
val format = SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.getDefault())
|
val format = SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.getDefault())
|
||||||
@@ -110,7 +233,7 @@ fun FlashScreen(navigator: DestinationsNavigator, flashIt: FlashIt) {
|
|||||||
"KernelSU_install_log_${date}.log"
|
"KernelSU_install_log_${date}.log"
|
||||||
)
|
)
|
||||||
file.writeText(logContent.toString())
|
file.writeText(logContent.toString())
|
||||||
snackBarHost.showSnackbar("Log saved to ${file.absolutePath}")
|
snackBarHost.showSnackbar(logSavedString.format(file.absolutePath))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
scrollBehavior = scrollBehavior
|
scrollBehavior = scrollBehavior
|
||||||
@@ -118,8 +241,6 @@ fun FlashScreen(navigator: DestinationsNavigator, flashIt: FlashIt) {
|
|||||||
},
|
},
|
||||||
floatingActionButton = {
|
floatingActionButton = {
|
||||||
if (showFloatAction) {
|
if (showFloatAction) {
|
||||||
val cardColor = MaterialTheme.colorScheme.secondaryContainer
|
|
||||||
val reboot = stringResource(id = R.string.reboot)
|
|
||||||
ExtendedFloatingActionButton(
|
ExtendedFloatingActionButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
@@ -128,36 +249,268 @@ fun FlashScreen(navigator: DestinationsNavigator, flashIt: FlashIt) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
icon = { Icon(Icons.Filled.Refresh, reboot) },
|
icon = {
|
||||||
text = { Text(text = reboot) },
|
Icon(
|
||||||
containerColor = cardColor.copy(alpha = 1f),
|
Icons.Filled.Refresh,
|
||||||
contentColor = MaterialTheme.colorScheme.onSecondaryContainer
|
contentDescription = stringResource(id = R.string.reboot)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
text = {
|
||||||
|
Text(text = stringResource(id = R.string.reboot))
|
||||||
|
},
|
||||||
|
containerColor = MaterialTheme.colorScheme.secondaryContainer,
|
||||||
|
contentColor = MaterialTheme.colorScheme.onSecondaryContainer,
|
||||||
|
expanded = true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
snackbarHost = { SnackbarHost(hostState = snackBarHost) },
|
snackbarHost = { SnackbarHost(hostState = snackBarHost) },
|
||||||
contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal)
|
contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),
|
||||||
|
containerColor = MaterialTheme.colorScheme.background
|
||||||
) { innerPadding ->
|
) { innerPadding ->
|
||||||
KeyEventBlocker {
|
KeyEventBlocker {
|
||||||
it.key == Key.VolumeDown || it.key == Key.VolumeUp
|
it.key == Key.VolumeDown || it.key == Key.VolumeUp
|
||||||
}
|
}
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize(1f)
|
.fillMaxSize(1f)
|
||||||
.padding(innerPadding)
|
.padding(innerPadding)
|
||||||
.nestedScroll(scrollBehavior.nestedScrollConnection)
|
.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||||
.verticalScroll(scrollState),
|
|
||||||
) {
|
) {
|
||||||
LaunchedEffect(text) {
|
if (flashIt is FlashIt.FlashModules) {
|
||||||
scrollState.animateScrollTo(scrollState.maxValue)
|
ModuleInstallProgressBar(
|
||||||
|
currentIndex = flashIt.currentIndex + 1,
|
||||||
|
totalCount = flashIt.uris.size,
|
||||||
|
currentModuleName = currentStatus.currentModuleName,
|
||||||
|
status = currentFlashingStatus.value,
|
||||||
|
failedModules = currentStatus.failedModules
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
}
|
}
|
||||||
Text(
|
|
||||||
modifier = Modifier.padding(8.dp),
|
Box(
|
||||||
text = text,
|
modifier = Modifier
|
||||||
fontSize = MaterialTheme.typography.bodySmall.fontSize,
|
.fillMaxWidth()
|
||||||
fontFamily = FontFamily.Monospace,
|
.weight(1f)
|
||||||
lineHeight = MaterialTheme.typography.bodySmall.lineHeight,
|
.verticalScroll(scrollState)
|
||||||
|
) {
|
||||||
|
LaunchedEffect(text) {
|
||||||
|
scrollState.animateScrollTo(scrollState.maxValue)
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.padding(16.dp),
|
||||||
|
text = text,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
fontFamily = FontFamily.Monospace,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示模块安装进度条和状态
|
||||||
|
@Composable
|
||||||
|
fun ModuleInstallProgressBar(
|
||||||
|
currentIndex: Int,
|
||||||
|
totalCount: Int,
|
||||||
|
currentModuleName: String,
|
||||||
|
status: FlashingStatus,
|
||||||
|
failedModules: List<String>
|
||||||
|
) {
|
||||||
|
val progressColor = when(status) {
|
||||||
|
FlashingStatus.FLASHING -> MaterialTheme.colorScheme.primary
|
||||||
|
FlashingStatus.SUCCESS -> MaterialTheme.colorScheme.tertiary
|
||||||
|
FlashingStatus.FAILED -> MaterialTheme.colorScheme.error
|
||||||
|
}
|
||||||
|
|
||||||
|
val progress = animateFloatAsState(
|
||||||
|
targetValue = currentIndex.toFloat() / totalCount.toFloat(),
|
||||||
|
label = "InstallProgress"
|
||||||
|
)
|
||||||
|
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceVariant
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp)
|
||||||
|
) {
|
||||||
|
// 模块名称和进度
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = if (currentModuleName.isNotEmpty()) currentModuleName else stringResource(R.string.module),
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "$currentIndex/$totalCount",
|
||||||
|
style = MaterialTheme.typography.titleMedium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
// 进度条
|
||||||
|
LinearProgressIndicator(
|
||||||
|
progress = { progress.value },
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(8.dp),
|
||||||
|
color = progressColor,
|
||||||
|
trackColor = MaterialTheme.colorScheme.surfaceVariant
|
||||||
)
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
// 失败模块列表
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = failedModules.isNotEmpty(),
|
||||||
|
enter = fadeIn() + expandVertically(),
|
||||||
|
exit = fadeOut() + shrinkVertically()
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Error,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.error,
|
||||||
|
modifier = Modifier.size(16.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.module_failed_count, failedModules.size),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.error
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
|
||||||
|
// 失败模块列表
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(
|
||||||
|
MaterialTheme.colorScheme.errorContainer.copy(alpha = 0.3f),
|
||||||
|
shape = MaterialTheme.shapes.small
|
||||||
|
)
|
||||||
|
.padding(8.dp)
|
||||||
|
) {
|
||||||
|
failedModules.forEach { moduleName ->
|
||||||
|
Text(
|
||||||
|
text = "• $moduleName",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onErrorContainer
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
private fun TopBar(
|
||||||
|
status: FlashingStatus,
|
||||||
|
moduleStatus: ModuleInstallStatus = ModuleInstallStatus(),
|
||||||
|
navigator: DestinationsNavigator,
|
||||||
|
flashIt: FlashIt,
|
||||||
|
onBack: () -> Unit,
|
||||||
|
onSave: () -> Unit = {},
|
||||||
|
scrollBehavior: TopAppBarScrollBehavior? = null
|
||||||
|
) {
|
||||||
|
val cardColor = MaterialTheme.colorScheme.surfaceContainerLow
|
||||||
|
val cardAlpha = CardConfig.cardAlpha
|
||||||
|
|
||||||
|
val statusColor = when(status) {
|
||||||
|
FlashingStatus.FLASHING -> MaterialTheme.colorScheme.primary
|
||||||
|
FlashingStatus.SUCCESS -> MaterialTheme.colorScheme.tertiary
|
||||||
|
FlashingStatus.FAILED -> MaterialTheme.colorScheme.error
|
||||||
|
}
|
||||||
|
|
||||||
|
TopAppBar(
|
||||||
|
title = {
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
text = stringResource(
|
||||||
|
when (status) {
|
||||||
|
FlashingStatus.FLASHING -> R.string.flashing
|
||||||
|
FlashingStatus.SUCCESS -> R.string.flash_success
|
||||||
|
FlashingStatus.FAILED -> R.string.flash_failed
|
||||||
|
}
|
||||||
|
),
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
color = statusColor
|
||||||
|
)
|
||||||
|
|
||||||
|
if (moduleStatus.failedModules.isNotEmpty()) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.module_failed_count, moduleStatus.failedModules.size),
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.error
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onClick = onBack) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.onSurface
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
colors = TopAppBarDefaults.topAppBarColors(
|
||||||
|
containerColor = cardColor.copy(alpha = cardAlpha),
|
||||||
|
scrolledContainerColor = cardColor.copy(alpha = cardAlpha)
|
||||||
|
),
|
||||||
|
actions = {
|
||||||
|
IconButton(onClick = onSave) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.Save,
|
||||||
|
contentDescription = stringResource(id = R.string.save_log),
|
||||||
|
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),
|
||||||
|
scrollBehavior = scrollBehavior
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getModuleNameFromUri(context: android.content.Context, uri: Uri): String {
|
||||||
|
return withContext(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
if (uri == Uri.EMPTY) {
|
||||||
|
return@withContext context.getString(R.string.unknown_module)
|
||||||
|
}
|
||||||
|
if (!ModuleUtils.isUriAccessible(context, uri)) {
|
||||||
|
return@withContext context.getString(R.string.unknown_module)
|
||||||
|
}
|
||||||
|
ModuleUtils.extractModuleName(context, uri)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
context.getString(R.string.unknown_module)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -166,11 +519,13 @@ fun FlashScreen(navigator: DestinationsNavigator, flashIt: FlashIt) {
|
|||||||
sealed class FlashIt : Parcelable {
|
sealed class FlashIt : Parcelable {
|
||||||
data class FlashBoot(val boot: Uri? = null, val lkm: LkmSelection, val ota: Boolean) : FlashIt()
|
data class FlashBoot(val boot: Uri? = null, val lkm: LkmSelection, val ota: Boolean) : FlashIt()
|
||||||
data class FlashModule(val uri: Uri) : FlashIt()
|
data class FlashModule(val uri: Uri) : FlashIt()
|
||||||
|
data class FlashModules(val uris: List<Uri>, val currentIndex: Int = 0) : FlashIt()
|
||||||
data object FlashRestore : FlashIt()
|
data object FlashRestore : FlashIt()
|
||||||
data object FlashUninstall : FlashIt()
|
data object FlashUninstall : FlashIt()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun flashIt(
|
fun flashIt(
|
||||||
|
context: android.content.Context,
|
||||||
flashIt: FlashIt,
|
flashIt: FlashIt,
|
||||||
onFinish: (Boolean, Int) -> Unit,
|
onFinish: (Boolean, Int) -> Unit,
|
||||||
onStdout: (String) -> Unit,
|
onStdout: (String) -> Unit,
|
||||||
@@ -186,49 +541,22 @@ fun flashIt(
|
|||||||
onStderr
|
onStderr
|
||||||
)
|
)
|
||||||
is FlashIt.FlashModule -> flashModule(flashIt.uri, onFinish, onStdout, onStderr)
|
is FlashIt.FlashModule -> flashModule(flashIt.uri, onFinish, onStdout, onStderr)
|
||||||
|
is FlashIt.FlashModules -> {
|
||||||
|
if (flashIt.uris.isEmpty() || flashIt.currentIndex >= flashIt.uris.size) {
|
||||||
|
onFinish(false, 0)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val currentUri = flashIt.uris[flashIt.currentIndex]
|
||||||
|
onStdout("\n")
|
||||||
|
|
||||||
|
flashModule(currentUri, onFinish, onStdout, onStderr)
|
||||||
|
}
|
||||||
FlashIt.FlashRestore -> restoreBoot(onFinish, onStdout, onStderr)
|
FlashIt.FlashRestore -> restoreBoot(onFinish, onStdout, onStderr)
|
||||||
FlashIt.FlashUninstall -> uninstallPermanently(onFinish, onStdout, onStderr)
|
FlashIt.FlashUninstall -> uninstallPermanently(onFinish, onStdout, onStderr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
|
||||||
private fun TopBar(
|
|
||||||
status: FlashingStatus,
|
|
||||||
onBack: () -> Unit = {},
|
|
||||||
onSave: () -> Unit = {},
|
|
||||||
scrollBehavior: TopAppBarScrollBehavior? = null
|
|
||||||
) {
|
|
||||||
TopAppBar(
|
|
||||||
title = {
|
|
||||||
Text(
|
|
||||||
stringResource(
|
|
||||||
when (status) {
|
|
||||||
FlashingStatus.FLASHING -> R.string.flashing
|
|
||||||
FlashingStatus.SUCCESS -> R.string.flash_success
|
|
||||||
FlashingStatus.FAILED -> R.string.flash_failed
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
navigationIcon = {
|
|
||||||
IconButton(
|
|
||||||
onClick = onBack
|
|
||||||
) { Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null) }
|
|
||||||
},
|
|
||||||
actions = {
|
|
||||||
IconButton(onClick = onSave) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Filled.Save,
|
|
||||||
contentDescription = "Localized description"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),
|
|
||||||
scrollBehavior = scrollBehavior
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Preview
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
fun FlashScreenPreview() {
|
fun FlashScreenPreview() {
|
||||||
|
|||||||
@@ -15,27 +15,66 @@ import androidx.compose.animation.shrinkVertically
|
|||||||
import androidx.compose.foundation.LocalIndication
|
import androidx.compose.foundation.LocalIndication
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.*
|
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.rememberScrollState
|
||||||
import androidx.compose.foundation.selection.toggleable
|
import androidx.compose.foundation.selection.toggleable
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
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.material.icons.filled.FileUpload
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.runtime.*
|
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.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.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
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.input.nestedscroll.nestedScroll
|
||||||
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.semantics.Role
|
import androidx.compose.ui.semantics.Role
|
||||||
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 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.ListOption
|
||||||
|
import com.maxkeppeler.sheets.list.models.ListSelection
|
||||||
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.generated.destinations.FlashScreenDestination
|
import com.ramcosta.composedestinations.generated.destinations.FlashScreenDestination
|
||||||
|
import com.ramcosta.composedestinations.generated.destinations.KernelFlashScreenDestination
|
||||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||||
import com.ramcosta.composedestinations.navigation.EmptyDestinationsNavigator
|
import com.ramcosta.composedestinations.navigation.EmptyDestinationsNavigator
|
||||||
import com.sukisu.ultra.R
|
import com.sukisu.ultra.R
|
||||||
@@ -43,18 +82,20 @@ import com.sukisu.ultra.ui.component.DialogHandle
|
|||||||
import com.sukisu.ultra.ui.component.SlotSelectionDialog
|
import com.sukisu.ultra.ui.component.SlotSelectionDialog
|
||||||
import com.sukisu.ultra.ui.component.rememberConfirmDialog
|
import com.sukisu.ultra.ui.component.rememberConfirmDialog
|
||||||
import com.sukisu.ultra.ui.component.rememberCustomDialog
|
import com.sukisu.ultra.ui.component.rememberCustomDialog
|
||||||
import com.sukisu.ultra.flash.HorizonKernelFlashProgress
|
import com.sukisu.ultra.ui.theme.CardConfig.cardAlpha
|
||||||
import com.sukisu.ultra.flash.HorizonKernelState
|
import com.sukisu.ultra.ui.theme.CardConfig.cardElevation
|
||||||
import com.sukisu.ultra.flash.HorizonKernelWorker
|
|
||||||
import com.sukisu.ultra.ui.theme.CardConfig
|
|
||||||
import com.sukisu.ultra.ui.theme.ThemeConfig
|
|
||||||
import com.sukisu.ultra.ui.theme.getCardColors
|
import com.sukisu.ultra.ui.theme.getCardColors
|
||||||
import com.sukisu.ultra.ui.theme.getCardElevation
|
import com.sukisu.ultra.ui.util.LkmSelection
|
||||||
import com.sukisu.ultra.ui.util.*
|
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
|
* @author ShirkNeko
|
||||||
* @date 2024/3/12.
|
* @date 2025/5/31.
|
||||||
*/
|
*/
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Destination<RootGraph>
|
@Destination<RootGraph>
|
||||||
@@ -66,14 +107,11 @@ fun InstallScreen(navigator: DestinationsNavigator) {
|
|||||||
var showRebootDialog by remember { mutableStateOf(false) }
|
var showRebootDialog by remember { mutableStateOf(false) }
|
||||||
var showSlotSelectionDialog by remember { mutableStateOf(false) }
|
var showSlotSelectionDialog by remember { mutableStateOf(false) }
|
||||||
var tempKernelUri by remember { mutableStateOf<Uri?>(null) }
|
var tempKernelUri by remember { mutableStateOf<Uri?>(null) }
|
||||||
val horizonKernelState = remember { HorizonKernelState() }
|
val kernelVersion = getKernelVersion()
|
||||||
val flashState by horizonKernelState.state.collectAsState()
|
val isGKI = kernelVersion.isGKI()
|
||||||
|
val isAbDevice = isAbDevice()
|
||||||
val summary = stringResource(R.string.horizon_kernel_summary)
|
val summary = stringResource(R.string.horizon_kernel_summary)
|
||||||
|
|
||||||
val onFlashComplete = {
|
|
||||||
showRebootDialog = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showRebootDialog) {
|
if (showRebootDialog) {
|
||||||
RebootDialog(
|
RebootDialog(
|
||||||
show = true,
|
show = true,
|
||||||
@@ -98,14 +136,12 @@ fun InstallScreen(navigator: DestinationsNavigator) {
|
|||||||
when (method) {
|
when (method) {
|
||||||
is InstallMethod.HorizonKernel -> {
|
is InstallMethod.HorizonKernel -> {
|
||||||
method.uri?.let { uri ->
|
method.uri?.let { uri ->
|
||||||
val worker = HorizonKernelWorker(
|
navigator.navigate(
|
||||||
context = context,
|
KernelFlashScreenDestination(
|
||||||
state = horizonKernelState,
|
kernelUri = uri,
|
||||||
slot = method.slot
|
selectedSlot = method.slot
|
||||||
|
)
|
||||||
)
|
)
|
||||||
worker.uri = uri
|
|
||||||
worker.setOnFlashCompleteListener(onFlashComplete)
|
|
||||||
worker.start()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
@@ -123,7 +159,7 @@ fun InstallScreen(navigator: DestinationsNavigator) {
|
|||||||
|
|
||||||
// 槽位选择
|
// 槽位选择
|
||||||
SlotSelectionDialog(
|
SlotSelectionDialog(
|
||||||
show = showSlotSelectionDialog,
|
show = showSlotSelectionDialog && isAbDevice,
|
||||||
onDismiss = { showSlotSelectionDialog = false },
|
onDismiss = { showSlotSelectionDialog = false },
|
||||||
onSlotSelected = { slot ->
|
onSlotSelected = { slot ->
|
||||||
showSlotSelectionDialog = false
|
showSlotSelectionDialog = false
|
||||||
@@ -133,7 +169,6 @@ fun InstallScreen(navigator: DestinationsNavigator) {
|
|||||||
summary = summary
|
summary = summary
|
||||||
)
|
)
|
||||||
installMethod = horizonMethod
|
installMethod = horizonMethod
|
||||||
onInstall()
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -149,7 +184,7 @@ fun InstallScreen(navigator: DestinationsNavigator) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val onClickNext = {
|
val onClickNext = {
|
||||||
if (lkmSelection == LkmSelection.KmiNone && currentKmi.isBlank()) {
|
if (isGKI && lkmSelection == LkmSelection.KmiNone && currentKmi.isBlank()) {
|
||||||
selectKmiDialog.show()
|
selectKmiDialog.show()
|
||||||
} else {
|
} else {
|
||||||
onInstall()
|
onInstall()
|
||||||
@@ -191,59 +226,98 @@ fun InstallScreen(navigator: DestinationsNavigator) {
|
|||||||
.padding(innerPadding)
|
.padding(innerPadding)
|
||||||
.nestedScroll(scrollBehavior.nestedScrollConnection)
|
.nestedScroll(scrollBehavior.nestedScrollConnection)
|
||||||
.verticalScroll(rememberScrollState())
|
.verticalScroll(rememberScrollState())
|
||||||
|
.padding(top = 12.dp)
|
||||||
) {
|
) {
|
||||||
SelectInstallMethod(
|
SelectInstallMethod(
|
||||||
|
isGKI = isGKI,
|
||||||
|
isAbDevice = isAbDevice,
|
||||||
onSelected = { method ->
|
onSelected = { method ->
|
||||||
if (method is InstallMethod.HorizonKernel && method.uri != null && method.slot == null) {
|
if (method is InstallMethod.HorizonKernel && method.uri != null) {
|
||||||
tempKernelUri = method.uri
|
if (isAbDevice) {
|
||||||
showSlotSelectionDialog = true
|
tempKernelUri = method.uri
|
||||||
|
showSlotSelectionDialog = true
|
||||||
|
} else {
|
||||||
|
installMethod = method
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
installMethod = method
|
installMethod = method
|
||||||
}
|
}
|
||||||
horizonKernelState.reset()
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
AnimatedVisibility(
|
|
||||||
visible = flashState.isFlashing && installMethod is InstallMethod.HorizonKernel,
|
|
||||||
enter = fadeIn() + expandVertically(),
|
|
||||||
exit = fadeOut() + shrinkVertically()
|
|
||||||
) {
|
|
||||||
HorizonKernelFlashProgress(flashState)
|
|
||||||
}
|
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(16.dp)
|
.padding(16.dp)
|
||||||
) {
|
) {
|
||||||
(lkmSelection as? LkmSelection.LkmUri)?.let {
|
(lkmSelection as? LkmSelection.LkmUri)?.let {
|
||||||
Text(
|
ElevatedCard(
|
||||||
stringResource(
|
colors = getCardColors(MaterialTheme.colorScheme.surfaceVariant),
|
||||||
id = R.string.selected_lkm,
|
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation),
|
||||||
it.uri.lastPathSegment ?: "(file)"
|
modifier = Modifier
|
||||||
)
|
.fillMaxWidth()
|
||||||
)
|
.padding(bottom = 12.dp)
|
||||||
}
|
.clip(MaterialTheme.shapes.medium)
|
||||||
(installMethod as? InstallMethod.HorizonKernel)?.let { method ->
|
.shadow(
|
||||||
if (method.slot != null) {
|
elevation = cardElevation,
|
||||||
Text(
|
shape = MaterialTheme.shapes.medium,
|
||||||
stringResource(
|
spotColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
|
||||||
id = R.string.selected_slot,
|
|
||||||
if (method.slot == "a") stringResource(id = R.string.slot_a)
|
|
||||||
else stringResource(id = R.string.slot_b)
|
|
||||||
)
|
)
|
||||||
|
) {
|
||||||
|
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(
|
Button(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
enabled = installMethod != null && !flashState.isFlashing,
|
enabled = installMethod != null,
|
||||||
onClick = onClickNext
|
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(
|
Text(
|
||||||
stringResource(id = R.string.install_next),
|
stringResource(id = R.string.install_next),
|
||||||
fontSize = MaterialTheme.typography.bodyMedium.fontSize
|
style = MaterialTheme.typography.bodyMedium
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -305,7 +379,11 @@ sealed class InstallMethod {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun SelectInstallMethod(onSelected: (InstallMethod) -> Unit = {}) {
|
private fun SelectInstallMethod(
|
||||||
|
isGKI: Boolean = false,
|
||||||
|
isAbDevice: Boolean = false,
|
||||||
|
onSelected: (InstallMethod) -> Unit = {}
|
||||||
|
) {
|
||||||
val rootAvailable = rootAvailable()
|
val rootAvailable = rootAvailable()
|
||||||
val isAbDevice = isAbDevice()
|
val isAbDevice = isAbDevice()
|
||||||
val horizonKernelSummary = stringResource(R.string.horizon_kernel_summary)
|
val horizonKernelSummary = stringResource(R.string.horizon_kernel_summary)
|
||||||
@@ -335,8 +413,16 @@ private fun SelectInstallMethod(onSelected: (InstallMethod) -> Unit = {}) {
|
|||||||
if (it.resultCode == Activity.RESULT_OK) {
|
if (it.resultCode == Activity.RESULT_OK) {
|
||||||
it.data?.data?.let { uri ->
|
it.data?.data?.let { uri ->
|
||||||
val option = when (currentSelectingMethod) {
|
val option = when (currentSelectingMethod) {
|
||||||
is InstallMethod.SelectFile -> InstallMethod.SelectFile(uri, summary = selectFileTip)
|
is InstallMethod.SelectFile -> InstallMethod.SelectFile(
|
||||||
is InstallMethod.HorizonKernel -> InstallMethod.HorizonKernel(uri, summary = horizonKernelSummary)
|
uri,
|
||||||
|
summary = selectFileTip
|
||||||
|
)
|
||||||
|
|
||||||
|
is InstallMethod.HorizonKernel -> InstallMethod.HorizonKernel(
|
||||||
|
uri,
|
||||||
|
summary = horizonKernelSummary
|
||||||
|
)
|
||||||
|
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
option?.let {
|
option?.let {
|
||||||
@@ -364,55 +450,237 @@ private fun SelectInstallMethod(onSelected: (InstallMethod) -> Unit = {}) {
|
|||||||
is InstallMethod.SelectFile, is InstallMethod.HorizonKernel -> {
|
is InstallMethod.SelectFile, is InstallMethod.HorizonKernel -> {
|
||||||
selectImageLauncher.launch(Intent(Intent.ACTION_GET_CONTENT).apply {
|
selectImageLauncher.launch(Intent(Intent.ACTION_GET_CONTENT).apply {
|
||||||
type = "application/*"
|
type = "application/*"
|
||||||
putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("application/octet-stream", "application/zip"))
|
putExtra(
|
||||||
|
Intent.EXTRA_MIME_TYPES,
|
||||||
|
arrayOf("application/octet-stream", "application/zip")
|
||||||
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
is InstallMethod.DirectInstall -> {
|
is InstallMethod.DirectInstall -> {
|
||||||
selectedOption = option
|
selectedOption = option
|
||||||
onSelected(option)
|
onSelected(option)
|
||||||
}
|
}
|
||||||
|
|
||||||
is InstallMethod.DirectInstallToInactiveSlot -> {
|
is InstallMethod.DirectInstallToInactiveSlot -> {
|
||||||
confirmDialog.showConfirm(dialogTitle, dialogContent)
|
confirmDialog.showConfirm(dialogTitle, dialogContent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Column {
|
var LKMExpanded by remember { mutableStateOf(false) }
|
||||||
radioOptions.forEach { option ->
|
var GKIExpanded by remember { mutableStateOf(false) }
|
||||||
val interactionSource = remember { MutableInteractionSource() }
|
|
||||||
Row(
|
Column(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
modifier = Modifier.padding(horizontal = 16.dp)
|
||||||
|
) {
|
||||||
|
// LKM 安装/修补
|
||||||
|
if (isGKI) {
|
||||||
|
ElevatedCard(
|
||||||
|
colors = getCardColors(MaterialTheme.colorScheme.surfaceVariant),
|
||||||
|
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.toggleable(
|
.padding(bottom = 12.dp)
|
||||||
value = option.javaClass == selectedOption?.javaClass,
|
.clip(MaterialTheme.shapes.large)
|
||||||
onValueChange = { onClick(option) },
|
.shadow(
|
||||||
role = Role.RadioButton,
|
elevation = cardElevation,
|
||||||
indication = LocalIndication.current,
|
shape = MaterialTheme.shapes.large,
|
||||||
interactionSource = interactionSource
|
spotColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
RadioButton(
|
ListItem(
|
||||||
selected = option.javaClass == selectedOption?.javaClass,
|
leadingContent = {
|
||||||
onClick = { onClick(option) },
|
Icon(
|
||||||
interactionSource = interactionSource
|
Icons.Filled.AutoFixHigh,
|
||||||
)
|
contentDescription = null,
|
||||||
Column(
|
tint = MaterialTheme.colorScheme.primary
|
||||||
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
|
|
||||||
)
|
)
|
||||||
|
},
|
||||||
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -427,77 +695,33 @@ fun rememberSelectKmiDialog(onSelected: (String?) -> Unit): DialogHandle {
|
|||||||
val supportedKmi by produceState(initialValue = emptyList<String>()) {
|
val supportedKmi by produceState(initialValue = emptyList<String>()) {
|
||||||
value = getSupportedKmis()
|
value = getSupportedKmis()
|
||||||
}
|
}
|
||||||
val listOptions = supportedKmi.map { value ->
|
val options = supportedKmi.map { value ->
|
||||||
ListOption(
|
ListOption(
|
||||||
titleText = value,
|
titleText = value
|
||||||
subtitleText = null,
|
|
||||||
icon = null
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
var selection: String? = null
|
var selection by remember { mutableStateOf<String?>(null) }
|
||||||
val cardColor = if (!ThemeConfig.useDynamicColor) {
|
val backgroundColor = MaterialTheme.colorScheme.surfaceContainerHighest
|
||||||
ThemeConfig.currentTheme.ButtonContrast
|
|
||||||
} else {
|
|
||||||
MaterialTheme.colorScheme.secondaryContainer
|
|
||||||
}
|
|
||||||
|
|
||||||
AlertDialog(
|
MaterialTheme(
|
||||||
onDismissRequest = {
|
colorScheme = MaterialTheme.colorScheme.copy(
|
||||||
|
surface = backgroundColor
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
ListDialog(state = rememberUseCaseState(visible = true, onFinishedRequest = {
|
||||||
|
onSelected(selection)
|
||||||
|
}, onCloseRequest = {
|
||||||
dismiss()
|
dismiss()
|
||||||
},
|
}), header = Header.Default(
|
||||||
title = {
|
title = stringResource(R.string.select_kmi),
|
||||||
Text(text = stringResource(R.string.select_kmi))
|
), selection = ListSelection.Single(
|
||||||
},
|
showRadioButtons = true,
|
||||||
text = {
|
options = options,
|
||||||
Column {
|
) { _, option ->
|
||||||
listOptions.forEachIndexed { index, option ->
|
selection = option.titleText
|
||||||
Row(
|
})
|
||||||
modifier = Modifier
|
}
|
||||||
.clickable {
|
|
||||||
selection = supportedKmi[index]
|
|
||||||
}
|
|
||||||
.padding(vertical = 8.dp)
|
|
||||||
) {
|
|
||||||
Column {
|
|
||||||
Text(text = option.titleText)
|
|
||||||
option.subtitleText?.let {
|
|
||||||
Text(
|
|
||||||
text = it,
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
confirmButton = {
|
|
||||||
TextButton(
|
|
||||||
onClick = {
|
|
||||||
if (selection != null) {
|
|
||||||
onSelected(selection)
|
|
||||||
}
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
Text(text = stringResource(android.R.string.ok))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
dismissButton = {
|
|
||||||
TextButton(
|
|
||||||
onClick = {
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
Text(text = stringResource(android.R.string.cancel))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
containerColor = getCardColors(cardColor.copy(alpha = 0.9f)).containerColor.copy(alpha = 0.9f),
|
|
||||||
shape = MaterialTheme.shapes.medium,
|
|
||||||
tonalElevation = getCardElevation()
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -508,23 +732,26 @@ private fun TopBar(
|
|||||||
onLkmUpload: () -> Unit = {},
|
onLkmUpload: () -> Unit = {},
|
||||||
scrollBehavior: TopAppBarScrollBehavior? = null
|
scrollBehavior: TopAppBarScrollBehavior? = null
|
||||||
) {
|
) {
|
||||||
val cardColor = MaterialTheme.colorScheme.secondaryContainer
|
val cardColor = MaterialTheme.colorScheme.surfaceContainerLow
|
||||||
val cardAlpha = CardConfig.cardAlpha
|
val cardAlpha = cardAlpha
|
||||||
|
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = { Text(stringResource(R.string.install)) },
|
title = {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.install),
|
||||||
|
style = MaterialTheme.typography.titleLarge
|
||||||
|
)
|
||||||
|
},
|
||||||
colors = TopAppBarDefaults.topAppBarColors(
|
colors = TopAppBarDefaults.topAppBarColors(
|
||||||
containerColor = cardColor.copy(alpha = cardAlpha),
|
containerColor = cardColor.copy(alpha = cardAlpha),
|
||||||
scrolledContainerColor = cardColor.copy(alpha = cardAlpha)
|
scrolledContainerColor = cardColor.copy(alpha = cardAlpha)
|
||||||
),
|
),
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
IconButton(onClick = onBack) {
|
IconButton(onClick = onBack) {
|
||||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null)
|
Icon(
|
||||||
}
|
Icons.AutoMirrored.Filled.ArrowBack,
|
||||||
},
|
contentDescription = stringResource(R.string.back)
|
||||||
actions = {
|
)
|
||||||
IconButton(onClick = onLkmUpload) {
|
|
||||||
Icon(Icons.Filled.FileUpload, contentDescription = null)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
windowInsets = WindowInsets.safeDrawing.only(
|
windowInsets = WindowInsets.safeDrawing.only(
|
||||||
|
|||||||
@@ -0,0 +1,411 @@
|
|||||||
|
package com.sukisu.ultra.ui.screen
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Environment
|
||||||
|
import androidx.activity.compose.BackHandler
|
||||||
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
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.CheckCircle
|
||||||
|
import androidx.compose.material.icons.filled.Error
|
||||||
|
import androidx.compose.material.icons.filled.Refresh
|
||||||
|
import androidx.compose.material.icons.filled.Save
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
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.text.font.FontFamily
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.ramcosta.composedestinations.annotation.Destination
|
||||||
|
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||||
|
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||||
|
import com.sukisu.ultra.R
|
||||||
|
import com.sukisu.ultra.flash.HorizonKernelState
|
||||||
|
import com.sukisu.ultra.flash.HorizonKernelWorker
|
||||||
|
import com.sukisu.ultra.ui.component.KeyEventBlocker
|
||||||
|
import com.sukisu.ultra.ui.util.*
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.io.File
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.*
|
||||||
|
import androidx.compose.ui.input.key.Key
|
||||||
|
import androidx.compose.ui.input.key.key
|
||||||
|
import com.sukisu.ultra.ui.theme.CardConfig
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author ShirkNeko
|
||||||
|
* @date 2025/5/31.
|
||||||
|
*/
|
||||||
|
private object KernelFlashStateHolder {
|
||||||
|
var currentState: HorizonKernelState? = null
|
||||||
|
var currentUri: Uri? = null
|
||||||
|
var currentSlot: String? = null
|
||||||
|
var isFlashing = false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kernel刷写界面
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Destination<RootGraph>
|
||||||
|
@Composable
|
||||||
|
fun KernelFlashScreen(
|
||||||
|
navigator: DestinationsNavigator,
|
||||||
|
kernelUri: Uri,
|
||||||
|
selectedSlot: String? = null
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val scrollState = rememberScrollState()
|
||||||
|
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||||
|
val snackBarHost = LocalSnackbarHost.current
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
var logText by rememberSaveable { mutableStateOf("") }
|
||||||
|
var showFloatAction by rememberSaveable { mutableStateOf(false) }
|
||||||
|
val logContent = rememberSaveable { StringBuilder() }
|
||||||
|
val horizonKernelState = remember {
|
||||||
|
if (KernelFlashStateHolder.currentState != null &&
|
||||||
|
KernelFlashStateHolder.currentUri == kernelUri &&
|
||||||
|
KernelFlashStateHolder.currentSlot == selectedSlot) {
|
||||||
|
KernelFlashStateHolder.currentState!!
|
||||||
|
} else {
|
||||||
|
HorizonKernelState().also {
|
||||||
|
KernelFlashStateHolder.currentState = it
|
||||||
|
KernelFlashStateHolder.currentUri = kernelUri
|
||||||
|
KernelFlashStateHolder.currentSlot = selectedSlot
|
||||||
|
KernelFlashStateHolder.isFlashing = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val flashState by horizonKernelState.state.collectAsState()
|
||||||
|
val logSavedString = stringResource(R.string.log_saved)
|
||||||
|
|
||||||
|
val onFlashComplete = {
|
||||||
|
showFloatAction = true
|
||||||
|
KernelFlashStateHolder.isFlashing = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开始刷写
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
if (!KernelFlashStateHolder.isFlashing && !flashState.isCompleted && flashState.error.isEmpty()) {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
KernelFlashStateHolder.isFlashing = true
|
||||||
|
val worker = HorizonKernelWorker(
|
||||||
|
context = context,
|
||||||
|
state = horizonKernelState,
|
||||||
|
slot = selectedSlot
|
||||||
|
)
|
||||||
|
worker.uri = kernelUri
|
||||||
|
worker.setOnFlashCompleteListener(onFlashComplete)
|
||||||
|
worker.start()
|
||||||
|
|
||||||
|
// 监听日志更新
|
||||||
|
while (!flashState.isCompleted && flashState.error.isEmpty()) {
|
||||||
|
if (flashState.logs.isNotEmpty()) {
|
||||||
|
logText = flashState.logs.joinToString("\n")
|
||||||
|
logContent.clear()
|
||||||
|
logContent.append(logText)
|
||||||
|
}
|
||||||
|
kotlinx.coroutines.delay(100)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (flashState.error.isNotEmpty()) {
|
||||||
|
logText += "\n${flashState.error}\n"
|
||||||
|
logContent.append("\n${flashState.error}\n")
|
||||||
|
KernelFlashStateHolder.isFlashing = false
|
||||||
|
} else if (flashState.isCompleted) {
|
||||||
|
logText += "\n${context.getString(R.string.horizon_flash_complete)}\n\n\n"
|
||||||
|
logContent.append("\n${context.getString(R.string.horizon_flash_complete)}\n\n\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logText = flashState.logs.joinToString("\n")
|
||||||
|
if (flashState.error.isNotEmpty()) {
|
||||||
|
logText += "\n${flashState.error}\n"
|
||||||
|
} else if (flashState.isCompleted) {
|
||||||
|
logText += "\n${context.getString(R.string.horizon_flash_complete)}\n\n\n"
|
||||||
|
showFloatAction = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val onBack: () -> Unit = {
|
||||||
|
if (!flashState.isFlashing || flashState.isCompleted || flashState.error.isNotEmpty()) {
|
||||||
|
// 清理全局状态
|
||||||
|
if (flashState.isCompleted || flashState.error.isNotEmpty()) {
|
||||||
|
KernelFlashStateHolder.currentState = null
|
||||||
|
KernelFlashStateHolder.currentUri = null
|
||||||
|
KernelFlashStateHolder.currentSlot = null
|
||||||
|
KernelFlashStateHolder.isFlashing = false
|
||||||
|
}
|
||||||
|
navigator.popBackStack()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BackHandler(enabled = true) {
|
||||||
|
onBack()
|
||||||
|
}
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
TopBar(
|
||||||
|
flashState = flashState,
|
||||||
|
onBack = onBack,
|
||||||
|
onSave = {
|
||||||
|
scope.launch {
|
||||||
|
val format = SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.getDefault())
|
||||||
|
val date = format.format(Date())
|
||||||
|
val file = File(
|
||||||
|
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
|
||||||
|
"KernelSU_kernel_flash_log_${date}.log"
|
||||||
|
)
|
||||||
|
file.writeText(logContent.toString())
|
||||||
|
snackBarHost.showSnackbar(logSavedString.format(file.absolutePath))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scrollBehavior = scrollBehavior
|
||||||
|
)
|
||||||
|
},
|
||||||
|
floatingActionButton = {
|
||||||
|
if (showFloatAction) {
|
||||||
|
ExtendedFloatingActionButton(
|
||||||
|
onClick = {
|
||||||
|
scope.launch {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
reboot()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
icon = {
|
||||||
|
Icon(
|
||||||
|
Icons.Filled.Refresh,
|
||||||
|
contentDescription = stringResource(id = R.string.reboot)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
text = {
|
||||||
|
Text(text = stringResource(id = R.string.reboot))
|
||||||
|
},
|
||||||
|
containerColor = MaterialTheme.colorScheme.secondaryContainer,
|
||||||
|
contentColor = MaterialTheme.colorScheme.onSecondaryContainer,
|
||||||
|
expanded = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
snackbarHost = { SnackbarHost(hostState = snackBarHost) },
|
||||||
|
contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),
|
||||||
|
containerColor = MaterialTheme.colorScheme.background
|
||||||
|
) { innerPadding ->
|
||||||
|
KeyEventBlocker {
|
||||||
|
it.key == Key.VolumeDown || it.key == Key.VolumeUp
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(innerPadding)
|
||||||
|
.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||||
|
) {
|
||||||
|
FlashProgressIndicator(flashState)
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.weight(1f)
|
||||||
|
.verticalScroll(scrollState)
|
||||||
|
) {
|
||||||
|
LaunchedEffect(logText) {
|
||||||
|
scrollState.animateScrollTo(scrollState.maxValue)
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.padding(16.dp),
|
||||||
|
text = logText,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
fontFamily = FontFamily.Monospace,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun FlashProgressIndicator(flashState: com.sukisu.ultra.flash.FlashState) {
|
||||||
|
val progressColor = when {
|
||||||
|
flashState.error.isNotEmpty() -> MaterialTheme.colorScheme.error
|
||||||
|
flashState.isCompleted -> MaterialTheme.colorScheme.tertiary
|
||||||
|
else -> MaterialTheme.colorScheme.primary
|
||||||
|
}
|
||||||
|
|
||||||
|
val progress = animateFloatAsState(
|
||||||
|
targetValue = flashState.progress,
|
||||||
|
label = "FlashProgress"
|
||||||
|
)
|
||||||
|
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceVariant
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = when {
|
||||||
|
flashState.error.isNotEmpty() -> stringResource(R.string.flash_failed)
|
||||||
|
flashState.isCompleted -> stringResource(R.string.flash_success)
|
||||||
|
else -> stringResource(R.string.flashing)
|
||||||
|
},
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = progressColor
|
||||||
|
)
|
||||||
|
|
||||||
|
when {
|
||||||
|
flashState.error.isNotEmpty() -> {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Error,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.error
|
||||||
|
)
|
||||||
|
}
|
||||||
|
flashState.isCompleted -> {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.CheckCircle,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.tertiary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
if (flashState.currentStep.isNotEmpty()) {
|
||||||
|
Text(
|
||||||
|
text = flashState.currentStep,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
}
|
||||||
|
|
||||||
|
LinearProgressIndicator(
|
||||||
|
progress = { progress.value },
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(8.dp),
|
||||||
|
color = progressColor,
|
||||||
|
trackColor = MaterialTheme.colorScheme.surfaceVariant
|
||||||
|
)
|
||||||
|
|
||||||
|
if (flashState.error.isNotEmpty()) {
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Error,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.error,
|
||||||
|
modifier = Modifier.size(16.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = flashState.error,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.error,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(
|
||||||
|
MaterialTheme.colorScheme.errorContainer.copy(alpha = 0.3f),
|
||||||
|
shape = MaterialTheme.shapes.small
|
||||||
|
)
|
||||||
|
.padding(8.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
private fun TopBar(
|
||||||
|
flashState: com.sukisu.ultra.flash.FlashState,
|
||||||
|
onBack: () -> Unit,
|
||||||
|
onSave: () -> Unit = {},
|
||||||
|
scrollBehavior: TopAppBarScrollBehavior? = null
|
||||||
|
) {
|
||||||
|
val statusColor = when {
|
||||||
|
flashState.error.isNotEmpty() -> MaterialTheme.colorScheme.error
|
||||||
|
flashState.isCompleted -> MaterialTheme.colorScheme.tertiary
|
||||||
|
else -> MaterialTheme.colorScheme.primary
|
||||||
|
}
|
||||||
|
|
||||||
|
val cardColor = MaterialTheme.colorScheme.surfaceContainerLow
|
||||||
|
val cardAlpha = CardConfig.cardAlpha
|
||||||
|
|
||||||
|
TopAppBar(
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(
|
||||||
|
when {
|
||||||
|
flashState.error.isNotEmpty() -> R.string.flash_failed
|
||||||
|
flashState.isCompleted -> R.string.flash_success
|
||||||
|
else -> R.string.kernel_flashing
|
||||||
|
}
|
||||||
|
),
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
color = statusColor
|
||||||
|
)
|
||||||
|
},
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onClick = onBack) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.onSurface
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
colors = TopAppBarDefaults.topAppBarColors(
|
||||||
|
containerColor = cardColor.copy(alpha = cardAlpha),
|
||||||
|
scrolledContainerColor = cardColor.copy(alpha = cardAlpha)
|
||||||
|
),
|
||||||
|
actions = {
|
||||||
|
IconButton(onClick = onSave) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.Save,
|
||||||
|
contentDescription = stringResource(id = R.string.save_log),
|
||||||
|
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),
|
||||||
|
scrollBehavior = scrollBehavior
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
package com.sukisu.ultra.ui.screen
|
package com.sukisu.ultra.ui.screen
|
||||||
|
|
||||||
import android.app.Activity.RESULT_OK
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
@@ -10,11 +9,13 @@ import androidx.compose.foundation.layout.*
|
|||||||
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.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.outlined.*
|
import androidx.compose.material.icons.filled.*
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
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.shadow
|
||||||
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.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
@@ -36,6 +37,8 @@ import java.io.BufferedReader
|
|||||||
import java.io.FileInputStream
|
import java.io.FileInputStream
|
||||||
import java.io.InputStreamReader
|
import java.io.InputStreamReader
|
||||||
import java.net.*
|
import java.net.*
|
||||||
|
import android.app.Activity
|
||||||
|
import com.sukisu.ultra.ui.theme.CardConfig.cardElevation
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* KPM 管理界面
|
* KPM 管理界面
|
||||||
@@ -53,11 +56,6 @@ fun KpmScreen(
|
|||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val snackBarHost = remember { SnackbarHostState() }
|
val snackBarHost = remember { SnackbarHostState() }
|
||||||
val confirmDialog = rememberConfirmDialog()
|
val confirmDialog = rememberConfirmDialog()
|
||||||
val cardColor = if (!ThemeConfig.useDynamicColor) {
|
|
||||||
ThemeConfig.currentTheme.ButtonContrast
|
|
||||||
} else {
|
|
||||||
MaterialTheme.colorScheme.secondaryContainer
|
|
||||||
}
|
|
||||||
|
|
||||||
val moduleConfirmContentMap = viewModel.moduleList.associate { module ->
|
val moduleConfirmContentMap = viewModel.moduleList.associate { module ->
|
||||||
val moduleFileName = module.id
|
val moduleFileName = module.id
|
||||||
@@ -103,77 +101,123 @@ fun KpmScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = {
|
onDismissRequest = {
|
||||||
dismiss()
|
dismiss()
|
||||||
tempFileForInstall?.delete()
|
tempFileForInstall?.delete()
|
||||||
tempFileForInstall = null
|
tempFileForInstall = null
|
||||||
},
|
},
|
||||||
title = { Text(kpmInstallMode) },
|
title = {
|
||||||
text = { moduleName?.let { Text(stringResource(R.string.kpm_install_mode_description, it)) } },
|
Text(
|
||||||
confirmButton = {
|
text = kpmInstallMode,
|
||||||
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
|
)
|
||||||
|
},
|
||||||
|
text = {
|
||||||
Column {
|
Column {
|
||||||
Button(
|
moduleName?.let {
|
||||||
onClick = {
|
Text(
|
||||||
scope.launch {
|
text = stringResource(R.string.kpm_install_mode_description, it),
|
||||||
dismiss()
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
tempFileForInstall?.let { tempFile ->
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
handleModuleInstall(
|
)
|
||||||
tempFile = tempFile,
|
|
||||||
isEmbed = false,
|
|
||||||
viewModel = viewModel,
|
|
||||||
snackBarHost = snackBarHost,
|
|
||||||
kpmInstallSuccess = kpmInstallSuccess,
|
|
||||||
kpmInstallFailed = kpmInstallFailed
|
|
||||||
)
|
|
||||||
}
|
|
||||||
tempFileForInstall = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
Text(kpmInstallModeLoad)
|
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
Button(
|
Column(
|
||||||
onClick = {
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
scope.launch {
|
|
||||||
dismiss()
|
|
||||||
tempFileForInstall?.let { tempFile ->
|
|
||||||
handleModuleInstall(
|
|
||||||
tempFile = tempFile,
|
|
||||||
isEmbed = true,
|
|
||||||
viewModel = viewModel,
|
|
||||||
snackBarHost = snackBarHost,
|
|
||||||
kpmInstallSuccess = kpmInstallSuccess,
|
|
||||||
kpmInstallFailed = kpmInstallFailed
|
|
||||||
)
|
|
||||||
}
|
|
||||||
tempFileForInstall = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
) {
|
) {
|
||||||
Text(kpmInstallModeEmbed)
|
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 = {
|
dismissButton = {
|
||||||
TextButton(
|
Column(
|
||||||
onClick = {
|
modifier = Modifier.fillMaxWidth(),
|
||||||
dismiss()
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
tempFileForInstall?.delete()
|
|
||||||
tempFileForInstall = null
|
|
||||||
}
|
|
||||||
) {
|
) {
|
||||||
Text(cancel)
|
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(
|
val selectPatchLauncher = rememberLauncherForActivityResult(
|
||||||
contract = ActivityResultContracts.StartActivityForResult()
|
contract = ActivityResultContracts.StartActivityForResult()
|
||||||
) { result ->
|
) { result ->
|
||||||
if (result.resultCode != RESULT_OK) return@rememberLauncherForActivityResult
|
if (result.resultCode != Activity.RESULT_OK) return@rememberLauncherForActivityResult
|
||||||
|
|
||||||
val uri = result.data?.data ?: return@rememberLauncherForActivityResult
|
val uri = result.data?.data ?: return@rememberLauncherForActivityResult
|
||||||
|
|
||||||
@@ -236,10 +280,12 @@ fun KpmScreen(
|
|||||||
onClearClick = { viewModel.search = "" },
|
onClearClick = { viewModel.search = "" },
|
||||||
scrollBehavior = scrollBehavior,
|
scrollBehavior = scrollBehavior,
|
||||||
dropdownContent = {
|
dropdownContent = {
|
||||||
IconButton(onClick = { viewModel.fetchModuleList() }) {
|
IconButton(
|
||||||
|
onClick = { viewModel.fetchModuleList() }
|
||||||
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Outlined.Refresh,
|
imageVector = Icons.Filled.Refresh,
|
||||||
contentDescription = stringResource(R.string.refresh)
|
contentDescription = stringResource(R.string.refresh),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -256,39 +302,70 @@ fun KpmScreen(
|
|||||||
},
|
},
|
||||||
icon = {
|
icon = {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Outlined.Add,
|
imageVector = Icons.Filled.Add,
|
||||||
contentDescription = stringResource(R.string.kpm_install)
|
contentDescription = stringResource(R.string.kpm_install),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
text = { Text(stringResource(R.string.kpm_install)) },
|
text = {
|
||||||
containerColor = cardColor.copy(alpha = 1f),
|
Text(
|
||||||
contentColor = MaterialTheme.colorScheme.onSecondaryContainer
|
text = stringResource(R.string.kpm_install),
|
||||||
|
color = MaterialTheme.colorScheme.onPrimaryContainer
|
||||||
|
)
|
||||||
|
},
|
||||||
|
contentColor = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||||
|
expanded = true,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
snackbarHost = { SnackbarHost(snackBarHost) }
|
snackbarHost = { SnackbarHost(snackBarHost) }
|
||||||
) { padding ->
|
) { padding ->
|
||||||
Column(modifier = Modifier.padding(padding)) {
|
Column(modifier = Modifier.padding(padding)) {
|
||||||
if (!isNoticeClosed) {
|
if (!isNoticeClosed) {
|
||||||
Row(
|
Card(
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.secondaryContainer
|
||||||
|
),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(16.dp),
|
.padding(16.dp)
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
.clip(MaterialTheme.shapes.medium)
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
) {
|
||||||
Text(
|
Row(
|
||||||
text = stringResource(R.string.kernel_module_notice),
|
modifier = Modifier
|
||||||
modifier = Modifier.weight(1f),
|
.fillMaxWidth()
|
||||||
textAlign = TextAlign.Center
|
.padding(16.dp),
|
||||||
)
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
IconButton(onClick = {
|
verticalAlignment = Alignment.CenterVertically
|
||||||
isNoticeClosed = true
|
) {
|
||||||
sharedPreferences.edit { putBoolean("is_notice_closed", true) }
|
|
||||||
}) {
|
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Outlined.Close,
|
imageVector = Icons.Filled.Info,
|
||||||
contentDescription = stringResource(R.string.close_notice)
|
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)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -298,15 +375,30 @@ fun KpmScreen(
|
|||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
Text(
|
Column(
|
||||||
stringResource(R.string.kpm_empty),
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
textAlign = TextAlign.Center
|
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 {
|
} else {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
contentPadding = PaddingValues(16.dp),
|
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 16.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
) {
|
) {
|
||||||
items(viewModel.moduleList) { module ->
|
items(viewModel.moduleList) { module ->
|
||||||
@@ -489,13 +581,34 @@ private fun KpmModuleItem(
|
|||||||
if (viewModel.showInputDialog && viewModel.selectedModuleId == module.id) {
|
if (viewModel.showInputDialog && viewModel.selectedModuleId == module.id) {
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = { viewModel.hideInputDialog() },
|
onDismissRequest = { viewModel.hideInputDialog() },
|
||||||
title = { Text(stringResource(R.string.kpm_control)) },
|
title = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.kpm_control),
|
||||||
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
|
)
|
||||||
|
},
|
||||||
text = {
|
text = {
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = viewModel.inputArgs,
|
value = viewModel.inputArgs,
|
||||||
onValueChange = { viewModel.updateInputArgs(it) },
|
onValueChange = { viewModel.updateInputArgs(it) },
|
||||||
label = { Text(stringResource(R.string.kpm_args)) },
|
label = {
|
||||||
placeholder = { Text(module.args) }
|
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 = {
|
confirmButton = {
|
||||||
@@ -512,23 +625,39 @@ private fun KpmModuleItem(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
Text(stringResource(R.string.confirm))
|
Text(
|
||||||
|
text = stringResource(R.string.confirm),
|
||||||
|
color = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
dismissButton = {
|
dismissButton = {
|
||||||
TextButton(onClick = { viewModel.hideInputDialog() }) {
|
TextButton(onClick = { viewModel.hideInputDialog() }) {
|
||||||
Text(stringResource(R.string.cancel))
|
Text(
|
||||||
|
text = stringResource(R.string.cancel),
|
||||||
|
color = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
|
||||||
|
shape = MaterialTheme.shapes.extraLarge
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
ElevatedCard(
|
Card(
|
||||||
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)
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.padding(16.dp)
|
modifier = Modifier.padding(20.dp)
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
@@ -538,54 +667,77 @@ private fun KpmModuleItem(
|
|||||||
Column(modifier = Modifier.weight(1f)) {
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
Text(
|
Text(
|
||||||
text = module.name,
|
text = module.name,
|
||||||
style = MaterialTheme.typography.titleMedium
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
)
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = "${stringResource(R.string.kpm_version)}: ${module.version}",
|
text = "${stringResource(R.string.kpm_version)}: ${module.version}",
|
||||||
style = MaterialTheme.typography.bodyMedium
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = "${stringResource(R.string.kpm_author)}: ${module.author}",
|
text = "${stringResource(R.string.kpm_author)}: ${module.author}",
|
||||||
style = MaterialTheme.typography.bodyMedium
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = "${stringResource(R.string.kpm_args)}: ${module.args}",
|
text = "${stringResource(R.string.kpm_args)}: ${module.args}",
|
||||||
style = MaterialTheme.typography.bodyMedium
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = module.description,
|
text = module.description,
|
||||||
style = MaterialTheme.typography.bodyMedium
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(20.dp))
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
) {
|
) {
|
||||||
FilledTonalButton(
|
Button(
|
||||||
onClick = { viewModel.showInputDialog(module.id) },
|
onClick = { viewModel.showInputDialog(module.id) },
|
||||||
enabled = module.hasAction
|
enabled = module.hasAction,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.primary,
|
||||||
|
disabledContainerColor = MaterialTheme.colorScheme.surfaceVariant
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Outlined.Settings,
|
imageVector = Icons.Filled.Settings,
|
||||||
contentDescription = null
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(20.dp)
|
||||||
)
|
)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
Text(stringResource(R.string.kpm_control))
|
Text(stringResource(R.string.kpm_control))
|
||||||
}
|
}
|
||||||
|
|
||||||
FilledTonalButton(
|
Button(
|
||||||
onClick = onUninstall
|
onClick = onUninstall,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.error
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Outlined.Delete,
|
imageVector = Icons.Filled.Delete,
|
||||||
contentDescription = null
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(20.dp)
|
||||||
)
|
)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
Text(stringResource(R.string.kpm_uninstall))
|
Text(stringResource(R.string.kpm_uninstall))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,27 +4,13 @@ import android.app.Activity.*
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.util.Log
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
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 +18,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
|
||||||
@@ -105,17 +64,21 @@ import com.sukisu.ultra.ui.webui.WebUIActivity
|
|||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import com.sukisu.ultra.ui.util.ModuleModify
|
import com.sukisu.ultra.ui.util.ModuleModify
|
||||||
import com.sukisu.ultra.ui.theme.getCardColors
|
import com.sukisu.ultra.ui.theme.getCardColors
|
||||||
import com.sukisu.ultra.ui.theme.getCardElevation
|
|
||||||
import com.sukisu.ultra.ui.viewmodel.ModuleViewModel
|
import com.sukisu.ultra.ui.viewmodel.ModuleViewModel
|
||||||
import java.io.BufferedReader
|
import java.util.concurrent.TimeUnit
|
||||||
import java.io.InputStreamReader
|
|
||||||
import java.util.zip.ZipInputStream
|
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
import androidx.core.net.toUri
|
|
||||||
import com.sukisu.ultra.ui.theme.ThemeConfig
|
|
||||||
import com.sukisu.ultra.R
|
import com.sukisu.ultra.R
|
||||||
|
import com.sukisu.ultra.ui.theme.CardConfig.cardElevation
|
||||||
|
import com.sukisu.ultra.ui.webui.WebUIXActivity
|
||||||
|
import com.dergoogler.mmrl.platform.Platform
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
import com.dergoogler.mmrl.platform.model.ModuleConfig
|
||||||
|
import com.dergoogler.mmrl.platform.model.ModuleConfig.Companion.asModuleConfig
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author ShirkNeko
|
||||||
|
* @date 2025/5/31.
|
||||||
|
*/
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Destination<RootGraph>
|
@Destination<RootGraph>
|
||||||
@Composable
|
@Composable
|
||||||
@@ -125,6 +88,7 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
|
|||||||
val snackBarHost = LocalSnackbarHost.current
|
val snackBarHost = LocalSnackbarHost.current
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val confirmDialog = rememberConfirmDialog()
|
val confirmDialog = rememberConfirmDialog()
|
||||||
|
var lastClickTime by remember { mutableStateOf(0L) }
|
||||||
|
|
||||||
val selectZipLauncher = rememberLauncherForActivityResult(
|
val selectZipLauncher = rememberLauncherForActivityResult(
|
||||||
contract = ActivityResultContracts.StartActivityForResult()
|
contract = ActivityResultContracts.StartActivityForResult()
|
||||||
@@ -137,38 +101,21 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
|
|||||||
scope.launch {
|
scope.launch {
|
||||||
val clipData = data.clipData
|
val clipData = data.clipData
|
||||||
if (clipData != null) {
|
if (clipData != null) {
|
||||||
// 处理多选结果
|
val selectedModules = mutableListOf<Uri>()
|
||||||
val selectedModules = mutableSetOf<Uri>()
|
|
||||||
val selectedModuleNames = mutableMapOf<Uri, String>()
|
val selectedModuleNames = mutableMapOf<Uri, String>()
|
||||||
|
|
||||||
suspend fun processUri(uri: Uri) {
|
fun processUri(uri: Uri) {
|
||||||
val moduleName = withContext(Dispatchers.IO) {
|
try {
|
||||||
try {
|
if (!ModuleUtils.isUriAccessible(context, uri)) {
|
||||||
val zipInputStream = ZipInputStream(context.contentResolver.openInputStream(uri))
|
return
|
||||||
var entry = zipInputStream.nextEntry
|
|
||||||
var name = context.getString(R.string.unknown_module)
|
|
||||||
|
|
||||||
while (entry != null) {
|
|
||||||
if (entry.name == "module.prop") {
|
|
||||||
val reader = BufferedReader(InputStreamReader(zipInputStream))
|
|
||||||
var line: String?
|
|
||||||
while (reader.readLine().also { line = it } != null) {
|
|
||||||
if (line?.startsWith("name=") == true) {
|
|
||||||
name = line.substringAfter("=")
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
entry = zipInputStream.nextEntry
|
|
||||||
}
|
|
||||||
name
|
|
||||||
} catch (e: Exception) {
|
|
||||||
context.getString(R.string.unknown_module)
|
|
||||||
}
|
}
|
||||||
|
ModuleUtils.takePersistableUriPermission(context, uri)
|
||||||
|
val moduleName = ModuleUtils.extractModuleName(context, uri)
|
||||||
|
selectedModules.add(uri)
|
||||||
|
selectedModuleNames[uri] = moduleName
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("ModuleScreen", "Error while processing URI: $uri, Error: ${e.message}")
|
||||||
}
|
}
|
||||||
selectedModules.add(uri)
|
|
||||||
selectedModuleNames[uri] = moduleName
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (i in 0 until clipData.itemCount) {
|
for (i in 0 until clipData.itemCount) {
|
||||||
@@ -176,7 +123,11 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
|
|||||||
processUri(uri)
|
processUri(uri)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 显示确认对话框
|
if (selectedModules.isEmpty()) {
|
||||||
|
snackBarHost.showSnackbar("Unable to access selected module files")
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
val modulesList = selectedModuleNames.values.joinToString("\n• ", "• ")
|
val modulesList = selectedModuleNames.values.joinToString("\n• ", "• ")
|
||||||
val confirmResult = confirmDialog.awaitConfirm(
|
val confirmResult = confirmDialog.awaitConfirm(
|
||||||
title = context.getString(R.string.module_install),
|
title = context.getString(R.string.module_install),
|
||||||
@@ -186,51 +137,42 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (confirmResult == ConfirmResult.Confirmed) {
|
if (confirmResult == ConfirmResult.Confirmed) {
|
||||||
// 批量安装模块
|
try {
|
||||||
selectedModules.forEach { uri ->
|
// 批量安装模块
|
||||||
navigator.navigate(FlashScreenDestination(FlashIt.FlashModule(uri)))
|
navigator.navigate(FlashScreenDestination(FlashIt.FlashModules(selectedModules)))
|
||||||
|
viewModel.markNeedRefresh()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("ModuleScreen", "Error navigating to FlashScreen: ${e.message}")
|
||||||
|
snackBarHost.showSnackbar("Error while installing module: ${e.message}")
|
||||||
}
|
}
|
||||||
viewModel.markNeedRefresh()
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 单个文件安装逻辑
|
|
||||||
val uri = data.data ?: return@launch
|
val uri = data.data ?: return@launch
|
||||||
val moduleName = withContext(Dispatchers.IO) {
|
// 单个安装模块
|
||||||
try {
|
try {
|
||||||
val zipInputStream = ZipInputStream(context.contentResolver.openInputStream(uri))
|
if (!ModuleUtils.isUriAccessible(context, uri)) {
|
||||||
var entry = zipInputStream.nextEntry
|
snackBarHost.showSnackbar("Unable to access selected module files")
|
||||||
var name = context.getString(R.string.unknown_module)
|
return@launch
|
||||||
|
|
||||||
while (entry != null) {
|
|
||||||
if (entry.name == "module.prop") {
|
|
||||||
val reader = BufferedReader(InputStreamReader(zipInputStream))
|
|
||||||
var line: String?
|
|
||||||
while (reader.readLine().also { line = it } != null) {
|
|
||||||
if (line?.startsWith("name=") == true) {
|
|
||||||
name = line.substringAfter("=")
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
entry = zipInputStream.nextEntry
|
|
||||||
}
|
|
||||||
name
|
|
||||||
} catch (e: Exception) {
|
|
||||||
context.getString(R.string.unknown_module)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
val confirmResult = confirmDialog.awaitConfirm(
|
ModuleUtils.takePersistableUriPermission(context, uri)
|
||||||
title = context.getString(R.string.module_install),
|
|
||||||
content = context.getString(R.string.module_install_confirm, moduleName),
|
|
||||||
confirm = context.getString(R.string.install),
|
|
||||||
dismiss = context.getString(R.string.cancel)
|
|
||||||
)
|
|
||||||
|
|
||||||
if (confirmResult == ConfirmResult.Confirmed) {
|
val moduleName = ModuleUtils.extractModuleName(context, uri)
|
||||||
navigator.navigate(FlashScreenDestination(FlashIt.FlashModule(uri)))
|
|
||||||
viewModel.markNeedRefresh()
|
val confirmResult = confirmDialog.awaitConfirm(
|
||||||
|
title = context.getString(R.string.module_install),
|
||||||
|
content = context.getString(R.string.module_install_confirm, moduleName),
|
||||||
|
confirm = context.getString(R.string.install),
|
||||||
|
dismiss = context.getString(R.string.cancel)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (confirmResult == ConfirmResult.Confirmed) {
|
||||||
|
navigator.navigate(FlashScreenDestination(FlashIt.FlashModule(uri)))
|
||||||
|
viewModel.markNeedRefresh()
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("ModuleScreen", "Error processing a single URI: $uri, Error: ${e.message}")
|
||||||
|
snackBarHost.showSnackbar("Error processing module file: ${e.message}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -239,7 +181,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 +217,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 +226,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,23 +251,33 @@ 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 {
|
||||||
putBoolean("module_sort_enabled_first", viewModel.sortEnabledFirst)
|
putBoolean("module_sort_enabled_first", viewModel.sortEnabledFirst)
|
||||||
}
|
}
|
||||||
scope.launch {
|
scope.launch {
|
||||||
viewModel.fetchModuleList()
|
viewModel.fetchModuleList()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
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 +290,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,11 +307,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 = if (!ThemeConfig.useDynamicColor) {
|
|
||||||
ThemeConfig.currentTheme.ButtonContrast
|
|
||||||
} else {
|
|
||||||
MaterialTheme.colorScheme.secondaryContainer
|
|
||||||
}
|
|
||||||
ExtendedFloatingActionButton(
|
ExtendedFloatingActionButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
selectZipLauncher.launch(
|
selectZipLauncher.launch(
|
||||||
@@ -363,16 +319,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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -389,10 +346,25 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
|
|||||||
.padding(24.dp),
|
.padding(24.dp),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
Text(
|
Column(
|
||||||
stringResource(R.string.module_magisk_conflict),
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
textAlign = TextAlign.Center,
|
verticalArrangement = Arrangement.Center
|
||||||
)
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.Warning,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.error,
|
||||||
|
modifier = Modifier
|
||||||
|
.size(64.dp)
|
||||||
|
.padding(bottom = 16.dp)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.module_magisk_conflict),
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
@@ -405,13 +377,60 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
|
|||||||
navigator.navigate(FlashScreenDestination(FlashIt.FlashModule(it)))
|
navigator.navigate(FlashScreenDestination(FlashIt.FlashModule(it)))
|
||||||
},
|
},
|
||||||
onClickModule = { id, name, hasWebUi ->
|
onClickModule = { id, name, hasWebUi ->
|
||||||
|
val currentTime = System.currentTimeMillis()
|
||||||
|
if (currentTime - lastClickTime < 600) {
|
||||||
|
Log.d("ModuleScreen", "Click too fast, ignoring")
|
||||||
|
return@ModuleList
|
||||||
|
}
|
||||||
|
lastClickTime = currentTime
|
||||||
|
|
||||||
if (hasWebUi) {
|
if (hasWebUi) {
|
||||||
webUILauncher.launch(
|
try {
|
||||||
Intent(context, WebUIActivity::class.java)
|
val wxEngine = Intent(context, WebUIXActivity::class.java)
|
||||||
|
.setData("kernelsu://webuix/$id".toUri())
|
||||||
|
.putExtra("id", id)
|
||||||
|
.putExtra("name", name)
|
||||||
|
|
||||||
|
val ksuEngine = Intent(context, WebUIActivity::class.java)
|
||||||
.setData("kernelsu://webui/$id".toUri())
|
.setData("kernelsu://webui/$id".toUri())
|
||||||
.putExtra("id", id)
|
.putExtra("id", id)
|
||||||
.putExtra("name", name)
|
.putExtra("name", name)
|
||||||
)
|
|
||||||
|
val config = try {
|
||||||
|
id.asModuleConfig
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("ModuleScreen", "Failed to get config from id: $id", e)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
val globalEngine = prefs.getString("webui_engine", "default") ?: "default"
|
||||||
|
val moduleEngine = config?.getWebuiEngine(context)
|
||||||
|
val selectedEngine = when (globalEngine) {
|
||||||
|
"wx" -> wxEngine
|
||||||
|
"ksu" -> ksuEngine
|
||||||
|
"default" -> {
|
||||||
|
when (moduleEngine) {
|
||||||
|
"wx" -> wxEngine
|
||||||
|
"ksu" -> ksuEngine
|
||||||
|
else -> {
|
||||||
|
if (Platform.isAlive) {
|
||||||
|
wxEngine
|
||||||
|
} else {
|
||||||
|
ksuEngine
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> ksuEngine
|
||||||
|
}
|
||||||
|
webUILauncher.launch(selectedEngine)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("ModuleScreen", "Error launching WebUI: ${e.message}", e)
|
||||||
|
scope.launch {
|
||||||
|
snackBarHost.showSnackbar("Error launching WebUI: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return@ModuleList
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
context = context,
|
context = context,
|
||||||
@@ -449,6 +468,7 @@ private fun ModuleList(
|
|||||||
val downloadingText = stringResource(R.string.module_downloading)
|
val downloadingText = stringResource(R.string.module_downloading)
|
||||||
val startDownloadingText = stringResource(R.string.module_start_downloading)
|
val startDownloadingText = stringResource(R.string.module_start_downloading)
|
||||||
val fetchChangeLogFailed = stringResource(R.string.module_changelog_failed)
|
val fetchChangeLogFailed = stringResource(R.string.module_changelog_failed)
|
||||||
|
val downloadErrorText = stringResource(R.string.module_download_error)
|
||||||
|
|
||||||
val loadingDialog = rememberLoadingDialog()
|
val loadingDialog = rememberLoadingDialog()
|
||||||
val confirmDialog = rememberConfirmDialog()
|
val confirmDialog = rememberConfirmDialog()
|
||||||
@@ -459,12 +479,20 @@ private fun ModuleList(
|
|||||||
downloadUrl: String,
|
downloadUrl: String,
|
||||||
fileName: String
|
fileName: String
|
||||||
) {
|
) {
|
||||||
|
val client = OkHttpClient.Builder()
|
||||||
|
.connectTimeout(15, TimeUnit.SECONDS)
|
||||||
|
.readTimeout(30, TimeUnit.SECONDS)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val request = okhttp3.Request.Builder()
|
||||||
|
.url(changelogUrl)
|
||||||
|
.header("User-Agent", "SukiSU-Ultra/2.0")
|
||||||
|
.build()
|
||||||
|
|
||||||
val changelogResult = loadingDialog.withLoading {
|
val changelogResult = loadingDialog.withLoading {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
runCatching {
|
runCatching {
|
||||||
OkHttpClient().newCall(
|
client.newCall(request).execute().body!!.string()
|
||||||
okhttp3.Request.Builder().url(changelogUrl).build()
|
|
||||||
).execute().body!!.string()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -513,6 +541,11 @@ private fun ModuleList(
|
|||||||
launch(Dispatchers.Main) {
|
launch(Dispatchers.Main) {
|
||||||
Toast.makeText(context, downloading, Toast.LENGTH_SHORT).show()
|
Toast.makeText(context, downloading, Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
onError = { errorMsg ->
|
||||||
|
launch(Dispatchers.Main) {
|
||||||
|
Toast.makeText(context, "$downloadErrorText: $errorMsg", Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -591,10 +624,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
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -677,8 +725,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() }
|
||||||
@@ -706,6 +762,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)
|
||||||
@@ -720,6 +777,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(
|
||||||
@@ -727,7 +785,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(
|
||||||
@@ -735,7 +794,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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -749,7 +809,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
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -764,83 +832,54 @@ 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()) {
|
|
||||||
Text(
|
|
||||||
modifier = Modifier.padding(start = 7.dp),
|
|
||||||
text = stringResource(R.string.action),
|
|
||||||
fontFamily = MaterialTheme.typography.labelMedium.fontFamily,
|
|
||||||
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()) {
|
|
||||||
Text(
|
|
||||||
modifier = Modifier.padding(start = 7.dp),
|
|
||||||
fontFamily = MaterialTheme.typography.labelMedium.fontFamily,
|
|
||||||
fontSize = MaterialTheme.typography.labelMedium.fontSize,
|
|
||||||
text = stringResource(R.string.open)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -848,41 +887,27 @@ 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,
|
||||||
contentPadding = ButtonDefaults.TextButtonContentPadding,
|
contentPadding = ButtonDefaults.TextButtonContentPadding,
|
||||||
|
colors = ButtonDefaults.filledTonalButtonColors()
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
modifier = Modifier.size(20.dp),
|
modifier = Modifier.size(20.dp),
|
||||||
imageVector = Icons.Outlined.Download,
|
imageVector = Icons.Outlined.Download,
|
||||||
contentDescription = null
|
contentDescription = null
|
||||||
)
|
)
|
||||||
if (!module.hasActionScript || !module.hasWebUi) {
|
|
||||||
Text(
|
|
||||||
modifier = Modifier.padding(start = 7.dp),
|
|
||||||
fontFamily = MaterialTheme.typography.labelMedium.fontFamily,
|
|
||||||
fontSize = MaterialTheme.typography.labelMedium.fontSize,
|
|
||||||
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(
|
||||||
@@ -894,16 +919,7 @@ 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)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -928,8 +944,8 @@ fun ModuleItemPreview() {
|
|||||||
updateJson = "",
|
updateJson = "",
|
||||||
hasWebUi = false,
|
hasWebUi = false,
|
||||||
hasActionScript = false,
|
hasActionScript = false,
|
||||||
dirId = "dirId"
|
dirId = "dirId",
|
||||||
|
config = ModuleConfig(),
|
||||||
)
|
)
|
||||||
ModuleItem(EmptyDestinationsNavigator, module, "", {}, {}, {}, {})
|
ModuleItem(EmptyDestinationsNavigator, module, "", {}, {}, {}, {})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,14 +6,22 @@ import android.net.Uri
|
|||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
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.border
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.Undo
|
import androidx.compose.material.icons.automirrored.filled.Undo
|
||||||
import androidx.compose.material.icons.filled.*
|
import androidx.compose.material.icons.filled.*
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
@@ -24,13 +32,12 @@ import androidx.compose.runtime.saveable.rememberSaveable
|
|||||||
import androidx.compose.runtime.setValue
|
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.graphics.Color
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
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.style.LineHeightStyle
|
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.core.content.FileProvider
|
import androidx.core.content.FileProvider
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
@@ -42,7 +49,6 @@ import com.ramcosta.composedestinations.generated.destinations.AppProfileTemplat
|
|||||||
import com.ramcosta.composedestinations.generated.destinations.FlashScreenDestination
|
import com.ramcosta.composedestinations.generated.destinations.FlashScreenDestination
|
||||||
import com.ramcosta.composedestinations.generated.destinations.MoreSettingsScreenDestination
|
import com.ramcosta.composedestinations.generated.destinations.MoreSettingsScreenDestination
|
||||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||||
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
|
||||||
@@ -52,25 +58,30 @@ import com.sukisu.ultra.R
|
|||||||
import com.sukisu.ultra.*
|
import com.sukisu.ultra.*
|
||||||
import com.sukisu.ultra.ui.component.*
|
import com.sukisu.ultra.ui.component.*
|
||||||
import com.sukisu.ultra.ui.theme.*
|
import com.sukisu.ultra.ui.theme.*
|
||||||
|
import com.sukisu.ultra.ui.theme.CardConfig.cardAlpha
|
||||||
import com.sukisu.ultra.ui.util.LocalSnackbarHost
|
import com.sukisu.ultra.ui.util.LocalSnackbarHost
|
||||||
import com.sukisu.ultra.ui.util.getBugreportFile
|
import com.sukisu.ultra.ui.util.getBugreportFile
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
import java.time.format.DateTimeFormatter
|
import java.time.format.DateTimeFormatter
|
||||||
|
import com.sukisu.ultra.ui.component.KsuIsValid
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author weishu
|
* @author ShirkNeko
|
||||||
* @date 2023/1/1.
|
* @date 2025/5/31.
|
||||||
*/
|
*/
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Destination<RootGraph>
|
@Destination<RootGraph>
|
||||||
@Composable
|
@Composable
|
||||||
fun SettingScreen(navigator: DestinationsNavigator) {
|
fun SettingScreen(navigator: DestinationsNavigator) {
|
||||||
// region 界面基础设置
|
|
||||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||||
val snackBarHost = LocalSnackbarHost.current
|
val snackBarHost = LocalSnackbarHost.current
|
||||||
val ksuIsValid = Natives.isKsuValid(ksuApp.packageName)
|
val context = LocalContext.current
|
||||||
// endregion
|
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||||
|
var selectedEngine by rememberSaveable {
|
||||||
|
mutableStateOf(
|
||||||
|
prefs.getString("webui_engine", "default") ?: "default"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
@@ -85,7 +96,6 @@ fun SettingScreen(navigator: DestinationsNavigator) {
|
|||||||
AboutDialog(it)
|
AboutDialog(it)
|
||||||
}
|
}
|
||||||
val loadingDialog = rememberLoadingDialog()
|
val loadingDialog = rememberLoadingDialog()
|
||||||
// endregion
|
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -93,12 +103,8 @@ fun SettingScreen(navigator: DestinationsNavigator) {
|
|||||||
.nestedScroll(scrollBehavior.nestedScrollConnection)
|
.nestedScroll(scrollBehavior.nestedScrollConnection)
|
||||||
.verticalScroll(rememberScrollState())
|
.verticalScroll(rememberScrollState())
|
||||||
) {
|
) {
|
||||||
// region 上下文与协程
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
// endregion
|
|
||||||
|
|
||||||
// region 日志导出功能
|
|
||||||
val exportBugreportLauncher = rememberLauncherForActivityResult(
|
val exportBugreportLauncher = rememberLauncherForActivityResult(
|
||||||
ActivityResultContracts.CreateDocument("application/gzip")
|
ActivityResultContracts.CreateDocument("application/gzip")
|
||||||
) { uri: Uri? ->
|
) { uri: Uri? ->
|
||||||
@@ -114,237 +120,436 @@ fun SettingScreen(navigator: DestinationsNavigator) {
|
|||||||
snackBarHost.showSnackbar(context.getString(R.string.log_saved))
|
snackBarHost.showSnackbar(context.getString(R.string.log_saved))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// region 配置项列表
|
|
||||||
// 配置文件模板入口
|
// 配置
|
||||||
val profileTemplate = stringResource(id = R.string.settings_profile_template)
|
Card(
|
||||||
if (ksuIsValid) {
|
modifier = Modifier
|
||||||
ListItem(
|
.fillMaxWidth()
|
||||||
leadingContent = { Icon(Icons.Filled.Fence, profileTemplate) },
|
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||||
headlineContent = { Text(profileTemplate) },
|
colors = CardDefaults.cardColors(
|
||||||
supportingContent = { Text(stringResource(id = R.string.settings_profile_template_summary)) },
|
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh.copy(alpha = cardAlpha)
|
||||||
modifier = Modifier.clickable {
|
),
|
||||||
navigator.navigate(AppProfileTemplateScreenDestination)
|
elevation = CardDefaults.cardElevation(defaultElevation = 0.dp)
|
||||||
|
) {
|
||||||
|
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 {
|
||||||
|
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
|
||||||
|
) { enabled ->
|
||||||
|
if (Natives.setDefaultUmountModules(enabled)) {
|
||||||
|
umountChecked = enabled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SU 禁用开关(仅在兼容版本显示)
|
||||||
|
KsuIsValid {
|
||||||
|
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
|
||||||
|
) { enabled ->
|
||||||
|
val shouldEnable = !enabled
|
||||||
|
if (Natives.setSuEnabled(shouldEnable)) {
|
||||||
|
isSuDisabled = enabled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
}
|
|
||||||
// 卸载模块开关
|
|
||||||
var umountChecked by rememberSaveable {
|
|
||||||
mutableStateOf(Natives.isDefaultUmountModules())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ksuIsValid) {
|
// 应用设置
|
||||||
SwitchItem(
|
Card(
|
||||||
icon = Icons.Filled.FolderDelete,
|
modifier = Modifier
|
||||||
title = stringResource(id = R.string.settings_umount_modules_default),
|
.fillMaxWidth()
|
||||||
summary = stringResource(id = R.string.settings_umount_modules_default_summary),
|
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||||
checked = umountChecked
|
colors = CardDefaults.cardColors(
|
||||||
) {
|
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh.copy(alpha = cardAlpha)
|
||||||
if (Natives.setDefaultUmountModules(it)) {
|
),
|
||||||
umountChecked = it
|
elevation = CardDefaults.cardElevation(defaultElevation = 0.dp)
|
||||||
}
|
) {
|
||||||
}
|
Column(modifier = Modifier.padding(vertical = 8.dp)) {
|
||||||
}
|
Text(
|
||||||
// SU 禁用开关(仅在兼容版本显示)
|
text = stringResource(R.string.app_settings),
|
||||||
if (ksuIsValid) {
|
style = MaterialTheme.typography.titleMedium,
|
||||||
if (Natives.version >= Natives.MINIMAL_SUPPORTED_SU_COMPAT) {
|
color = MaterialTheme.colorScheme.primary,
|
||||||
var isSuDisabled by rememberSaveable {
|
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||||
mutableStateOf(!Natives.isSuEnabled())
|
)
|
||||||
|
|
||||||
|
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||||
|
|
||||||
|
// 更新检查开关
|
||||||
|
var checkUpdate by rememberSaveable {
|
||||||
|
mutableStateOf(
|
||||||
|
prefs.getBoolean("check_update", true)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
SwitchItem(
|
SwitchItem(
|
||||||
icon = Icons.Filled.RemoveModerator,
|
icon = Icons.Filled.Update,
|
||||||
title = stringResource(id = R.string.settings_disable_su),
|
title = stringResource(id = R.string.settings_check_update),
|
||||||
summary = stringResource(id = R.string.settings_disable_su_summary),
|
summary = stringResource(id = R.string.settings_check_update_summary),
|
||||||
checked = isSuDisabled,
|
checked = checkUpdate
|
||||||
) { checked ->
|
) { enabled ->
|
||||||
val shouldEnable = !checked
|
prefs.edit { putBoolean("check_update", enabled) }
|
||||||
if (Natives.setSuEnabled(shouldEnable)) {
|
checkUpdate = enabled
|
||||||
isSuDisabled = !shouldEnable
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
// WebUI引擎选择
|
||||||
|
KsuIsValid {
|
||||||
|
val engineOptions = listOf(
|
||||||
|
"default" to stringResource(id = R.string.engine_auto_select),
|
||||||
|
"wx" to stringResource(id = R.string.engine_force_webuix),
|
||||||
|
"ksu" to stringResource(id = R.string.engine_force_ksu)
|
||||||
|
)
|
||||||
|
|
||||||
// 更新检查开关
|
var showEngineDialog by remember { mutableStateOf(false) }
|
||||||
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调试开关
|
SettingItem(
|
||||||
var enableWebDebugging by rememberSaveable {
|
icon = Icons.Filled.WebAsset,
|
||||||
mutableStateOf(
|
title = stringResource(id = R.string.use_webuix),
|
||||||
prefs.getBoolean("enable_web_debugging", false)
|
summary = engineOptions.find { it.first == selectedEngine }?.second
|
||||||
)
|
?: stringResource(id = R.string.engine_auto_select),
|
||||||
}
|
onClick = {
|
||||||
if (Natives.isKsuValid(ksuApp.packageName)) {
|
showEngineDialog = true
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 更多设置
|
|
||||||
val newButtonTitle = stringResource(id = R.string.more_settings)
|
|
||||||
ListItem(
|
|
||||||
leadingContent = {
|
|
||||||
Icon(
|
|
||||||
Icons.Filled.Settings,
|
|
||||||
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
|
if (showEngineDialog) {
|
||||||
.padding(16.dp)
|
AlertDialog(
|
||||||
.clickable {
|
onDismissRequest = { showEngineDialog = false },
|
||||||
scope.launch {
|
title = { Text(stringResource(id = R.string.use_webuix)) },
|
||||||
val bugreport = loadingDialog.withLoading {
|
text = {
|
||||||
withContext(Dispatchers.IO) {
|
Column {
|
||||||
getBugreportFile(context)
|
engineOptions.forEach { (value, label) ->
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable {
|
||||||
|
selectedEngine = value
|
||||||
|
prefs.edit {
|
||||||
|
putString("webui_engine", value)
|
||||||
|
}
|
||||||
|
showEngineDialog = false
|
||||||
}
|
}
|
||||||
}
|
.padding(vertical = 12.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
val uri: Uri =
|
) {
|
||||||
FileProvider.getUriForFile(
|
RadioButton(
|
||||||
context,
|
selected = selectedEngine == value,
|
||||||
"${BuildConfig.APPLICATION_ID}.fileprovider",
|
onClick = null
|
||||||
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)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Text(text = label)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
) {
|
}
|
||||||
Icon(
|
},
|
||||||
Icons.Filled.Share,
|
confirmButton = {
|
||||||
contentDescription = null,
|
TextButton(
|
||||||
modifier = Modifier.align(Alignment.CenterHorizontally)
|
onClick = { showEngineDialog = false }
|
||||||
)
|
) {
|
||||||
Text(
|
Text(stringResource(id = R.string.cancel))
|
||||||
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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Web调试和Web X Eruda 开关
|
||||||
|
var enableWebDebugging by rememberSaveable {
|
||||||
|
mutableStateOf(
|
||||||
|
prefs.getBoolean("enable_web_debugging", false)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
var useWebUIXEruda by rememberSaveable {
|
||||||
|
mutableStateOf(
|
||||||
|
prefs.getBoolean("use_webuix_eruda", false)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
KsuIsValid {
|
||||||
|
SwitchItem(
|
||||||
|
icon = Icons.Filled.DeveloperMode,
|
||||||
|
title = stringResource(id = R.string.enable_web_debugging),
|
||||||
|
summary = stringResource(id = R.string.enable_web_debugging_summary),
|
||||||
|
checked = enableWebDebugging
|
||||||
|
) { enabled ->
|
||||||
|
prefs.edit { putBoolean("enable_web_debugging", enabled) }
|
||||||
|
enableWebDebugging = enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = enableWebDebugging && selectedEngine == "wx",
|
||||||
|
enter = fadeIn() + expandVertically(),
|
||||||
|
exit = fadeOut() + shrinkVertically()
|
||||||
|
) {
|
||||||
|
SwitchItem(
|
||||||
|
icon = Icons.Filled.FormatListNumbered,
|
||||||
|
title = stringResource(id = R.string.use_webuix_eruda),
|
||||||
|
summary = stringResource(id = R.string.use_webuix_eruda_summary),
|
||||||
|
checked = useWebUIXEruda
|
||||||
|
) { enabled ->
|
||||||
|
prefs.edit { putBoolean("use_webuix_eruda", enabled) }
|
||||||
|
useWebUIXEruda = enabled
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val lkmMode = Natives.version >= Natives.MINIMAL_SUPPORTED_KERNEL_LKM && Natives.isLkmMode
|
// 更多设置
|
||||||
if (lkmMode) {
|
SettingItem(
|
||||||
UninstallItem(navigator) {
|
icon = Icons.Filled.Settings,
|
||||||
loadingDialog.withLoading(it)
|
title = stringResource(id = R.string.more_settings),
|
||||||
}
|
summary = stringResource(id = R.string.more_settings),
|
||||||
}
|
onClick = {
|
||||||
|
navigator.navigate(MoreSettingsScreenDestination)
|
||||||
val about = stringResource(id = R.string.about)
|
}
|
||||||
ListItem(
|
|
||||||
leadingContent = {
|
|
||||||
Icon(
|
|
||||||
Icons.Filled.ContactPage,
|
|
||||||
about
|
|
||||||
)
|
)
|
||||||
},
|
|
||||||
headlineContent = { Text(about) },
|
|
||||||
modifier = Modifier.clickable {
|
|
||||||
aboutDialog.show()
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 工具
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh.copy(alpha = cardAlpha)
|
||||||
|
),
|
||||||
|
elevation = CardDefaults.cardElevation(defaultElevation = 0.dp)
|
||||||
|
) {
|
||||||
|
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.surfaceContainerHigh.copy(alpha = cardAlpha)
|
||||||
|
),
|
||||||
|
elevation = CardDefaults.cardElevation(defaultElevation = 0.dp)
|
||||||
|
) {
|
||||||
|
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)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -381,16 +586,11 @@ fun UninstallItem(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val uninstall = stringResource(id = R.string.settings_uninstall)
|
|
||||||
ListItem(
|
SettingItem(
|
||||||
leadingContent = {
|
icon = Icons.Filled.Delete,
|
||||||
Icon(
|
title = stringResource(id = R.string.settings_uninstall),
|
||||||
Icons.Filled.Delete,
|
onClick = {
|
||||||
uninstall
|
|
||||||
)
|
|
||||||
},
|
|
||||||
headlineContent = { Text(uninstall) },
|
|
||||||
modifier = Modifier.clickable {
|
|
||||||
uninstallDialog.show()
|
uninstallDialog.show()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -432,73 +632,138 @@ fun rememberUninstallDialog(onSelected: (UninstallType) -> Unit): DialogHandle {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
var selection = UninstallType.NONE
|
var selectedOption by remember { mutableStateOf<UninstallType?>(null) }
|
||||||
val cardColor = if (!ThemeConfig.useDynamicColor) {
|
|
||||||
ThemeConfig.currentTheme.ButtonContrast
|
|
||||||
} else {
|
|
||||||
MaterialTheme.colorScheme.secondaryContainer
|
|
||||||
}
|
|
||||||
|
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = {
|
onDismissRequest = {
|
||||||
dismiss()
|
dismiss()
|
||||||
},
|
},
|
||||||
title = {
|
title = {
|
||||||
Text(text = stringResource(R.string.settings_uninstall))
|
Text(
|
||||||
|
text = stringResource(R.string.settings_uninstall),
|
||||||
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
|
)
|
||||||
},
|
},
|
||||||
text = {
|
text = {
|
||||||
Column {
|
Column(
|
||||||
listOptions.forEachIndexed { index, option ->
|
modifier = Modifier.padding(vertical = 8.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
) {
|
||||||
|
options.forEachIndexed { index, option ->
|
||||||
|
val isSelected = selectedOption == option
|
||||||
|
val backgroundColor = if (isSelected)
|
||||||
|
MaterialTheme.colorScheme.primaryContainer
|
||||||
|
else
|
||||||
|
Color.Transparent
|
||||||
|
val borderColor = if (isSelected)
|
||||||
|
MaterialTheme.colorScheme.primary
|
||||||
|
else
|
||||||
|
Color.Transparent
|
||||||
|
val contentColor = if (isSelected)
|
||||||
|
MaterialTheme.colorScheme.onPrimaryContainer
|
||||||
|
else
|
||||||
|
MaterialTheme.colorScheme.onSurface
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clip(MaterialTheme.shapes.medium)
|
||||||
|
.background(backgroundColor)
|
||||||
|
.border(
|
||||||
|
width = 1.dp,
|
||||||
|
color = borderColor,
|
||||||
|
shape = MaterialTheme.shapes.medium
|
||||||
|
)
|
||||||
.clickable {
|
.clickable {
|
||||||
selection = options[index]
|
selectedOption = option
|
||||||
}
|
}
|
||||||
.padding(vertical = 8.dp)
|
.padding(vertical = 12.dp, horizontal = 8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = options[index].icon,
|
imageVector = option.icon,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier.padding(end = 8.dp)
|
tint = if (isSelected)
|
||||||
|
MaterialTheme.colorScheme.primary
|
||||||
|
else
|
||||||
|
MaterialTheme.colorScheme.primary,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(end = 16.dp)
|
||||||
|
.size(24.dp)
|
||||||
)
|
)
|
||||||
Column {
|
Column(
|
||||||
Text(text = option.titleText)
|
modifier = Modifier.weight(1f)
|
||||||
option.subtitleText?.let {
|
) {
|
||||||
|
Text(
|
||||||
|
text = listOptions[index].titleText,
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
color = contentColor
|
||||||
|
)
|
||||||
|
listOptions[index].subtitleText?.let {
|
||||||
Text(
|
Text(
|
||||||
text = it,
|
text = it,
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
color = if (isSelected)
|
||||||
|
contentColor.copy(alpha = 0.8f)
|
||||||
|
else
|
||||||
|
MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (isSelected) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.RadioButtonChecked,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.RadioButtonUnchecked,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
androidx.compose.material3.TextButton(
|
Button(
|
||||||
onClick = {
|
onClick = {
|
||||||
if (selection != UninstallType.NONE) {
|
selectedOption?.let { onSelected(it) }
|
||||||
onSelected(selection)
|
|
||||||
}
|
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
},
|
||||||
|
enabled = selectedOption != null,
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.primary,
|
||||||
|
contentColor = MaterialTheme.colorScheme.onPrimary,
|
||||||
|
disabledContainerColor = MaterialTheme.colorScheme.surfaceVariant,
|
||||||
|
disabledContentColor = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
Text(text = stringResource(android.R.string.ok))
|
Text(
|
||||||
|
text = stringResource(android.R.string.ok)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
dismissButton = {
|
dismissButton = {
|
||||||
androidx.compose.material3.TextButton(
|
TextButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
Text(text = stringResource(android.R.string.cancel))
|
Text(
|
||||||
|
text = stringResource(android.R.string.cancel),
|
||||||
|
color = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
containerColor = getCardColors(cardColor.copy(alpha = 0.9f)).containerColor.copy(alpha = 0.9f),
|
containerColor = MaterialTheme.colorScheme.secondaryContainer,
|
||||||
shape = MaterialTheme.shapes.medium,
|
shape = MaterialTheme.shapes.extraLarge,
|
||||||
tonalElevation = getCardElevation()
|
tonalElevation = 4.dp
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -508,24 +773,26 @@ fun rememberUninstallDialog(onSelected: (UninstallType) -> Unit): DialogHandle {
|
|||||||
private fun TopBar(
|
private fun TopBar(
|
||||||
scrollBehavior: TopAppBarScrollBehavior? = null
|
scrollBehavior: TopAppBarScrollBehavior? = null
|
||||||
) {
|
) {
|
||||||
val cardColor = MaterialTheme.colorScheme.secondaryContainer
|
val systemIsDark = isSystemInDarkTheme()
|
||||||
val cardAlpha = CardConfig.cardAlpha
|
val cardColor = MaterialTheme.colorScheme.surfaceContainerLow
|
||||||
|
val cardAlpha = if (ThemeConfig.customBackgroundUri != null) {
|
||||||
|
cardAlpha
|
||||||
|
} else {
|
||||||
|
if (systemIsDark) 0.8f else 1f
|
||||||
|
}
|
||||||
|
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = { Text(stringResource(R.string.settings)) },
|
title = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.settings),
|
||||||
|
style = MaterialTheme.typography.titleLarge
|
||||||
|
)
|
||||||
|
},
|
||||||
colors = TopAppBarDefaults.topAppBarColors(
|
colors = TopAppBarDefaults.topAppBarColors(
|
||||||
containerColor = cardColor.copy(alpha = cardAlpha),
|
containerColor = cardColor.copy(alpha = cardAlpha),
|
||||||
scrolledContainerColor = cardColor.copy(alpha = cardAlpha)
|
scrolledContainerColor = cardColor.copy(alpha = cardAlpha)
|
||||||
),
|
),
|
||||||
|
|
||||||
windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),
|
windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),
|
||||||
scrollBehavior = scrollBehavior
|
scrollBehavior = scrollBehavior
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
|
||||||
private fun SettingsPreview() {
|
|
||||||
SettingScreen(EmptyDestinationsNavigator)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,17 @@
|
|||||||
package com.sukisu.ultra.ui.screen
|
package com.sukisu.ultra.ui.screen
|
||||||
|
|
||||||
|
import androidx.compose.animation.*
|
||||||
|
import androidx.compose.animation.core.*
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
import androidx.compose.foundation.gestures.detectTapGestures
|
import androidx.compose.foundation.gestures.detectTapGestures
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.interaction.collectIsPressedAsState
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
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.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.ExperimentalMaterialApi
|
import androidx.compose.material.ExperimentalMaterialApi
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
@@ -13,7 +19,10 @@ import androidx.compose.material.icons.filled.*
|
|||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
|
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.shadow
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
@@ -21,6 +30,7 @@ import androidx.compose.ui.platform.LocalContext
|
|||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
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.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
@@ -34,9 +44,16 @@ import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import com.sukisu.ultra.Natives
|
import com.sukisu.ultra.Natives
|
||||||
import com.sukisu.ultra.ui.component.SearchAppBar
|
import com.sukisu.ultra.ui.component.SearchAppBar
|
||||||
|
import com.sukisu.ultra.ui.theme.CardConfig
|
||||||
import com.sukisu.ultra.ui.util.ModuleModify
|
import com.sukisu.ultra.ui.util.ModuleModify
|
||||||
import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel
|
import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel
|
||||||
|
import com.dergoogler.mmrl.ui.component.LabelItem
|
||||||
|
import com.dergoogler.mmrl.ui.component.LabelItemDefaults
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author ShirkNeko
|
||||||
|
* @date 2025/5/31.
|
||||||
|
*/
|
||||||
@OptIn(ExperimentalMaterialApi::class, ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterialApi::class, ExperimentalMaterial3Api::class)
|
||||||
@Destination<RootGraph>
|
@Destination<RootGraph>
|
||||||
@Composable
|
@Composable
|
||||||
@@ -55,13 +72,14 @@ fun SuperUserScreen(navigator: DestinationsNavigator) {
|
|||||||
LaunchedEffect(key1 = navigator) {
|
LaunchedEffect(key1 = navigator) {
|
||||||
viewModel.search = ""
|
viewModel.search = ""
|
||||||
if (viewModel.appList.isEmpty()) {
|
if (viewModel.appList.isEmpty()) {
|
||||||
viewModel.fetchAppList()
|
// viewModel.fetchAppList()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(viewModel.search) {
|
LaunchedEffect(viewModel.search) {
|
||||||
if (viewModel.search.isEmpty()) {
|
if (viewModel.search.isEmpty()) {
|
||||||
listState.scrollToItem(0)
|
// 取消自动滚动到顶部的行为
|
||||||
|
// listState.scrollToItem(0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,44 +98,76 @@ fun SuperUserScreen(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(expanded = showDropdown, onDismissRequest = {
|
DropdownMenu(expanded = showDropdown, onDismissRequest = {
|
||||||
showDropdown = false
|
showDropdown = false
|
||||||
}) {
|
}) {
|
||||||
DropdownMenuItem(text = {
|
DropdownMenuItem(
|
||||||
Text(stringResource(R.string.refresh))
|
text = { Text(stringResource(R.string.refresh)) },
|
||||||
}, onClick = {
|
leadingIcon = {
|
||||||
scope.launch {
|
Icon(
|
||||||
viewModel.fetchAppList()
|
imageVector = Icons.Filled.Refresh,
|
||||||
}
|
contentDescription = null,
|
||||||
showDropdown = false
|
)
|
||||||
})
|
},
|
||||||
DropdownMenuItem(text = {
|
onClick = {
|
||||||
Text(
|
scope.launch {
|
||||||
if (viewModel.showSystemApps) {
|
viewModel.fetchAppList()
|
||||||
stringResource(R.string.hide_system_apps)
|
|
||||||
} else {
|
|
||||||
stringResource(R.string.show_system_apps)
|
|
||||||
}
|
}
|
||||||
)
|
showDropdown = false
|
||||||
}, onClick = {
|
}
|
||||||
viewModel.showSystemApps = !viewModel.showSystemApps
|
)
|
||||||
showDropdown = false
|
DropdownMenuItem(
|
||||||
})
|
text = {
|
||||||
DropdownMenuItem(text = {
|
Text(
|
||||||
Text(stringResource(R.string.backup_allowlist))
|
if (viewModel.showSystemApps) {
|
||||||
}, onClick = {
|
stringResource(R.string.hide_system_apps)
|
||||||
backupLauncher.launch(ModuleModify.createAllowlistBackupIntent())
|
} else {
|
||||||
showDropdown = false
|
stringResource(R.string.show_system_apps)
|
||||||
})
|
}
|
||||||
DropdownMenuItem(text = {
|
)
|
||||||
Text(stringResource(R.string.restore_allowlist))
|
},
|
||||||
}, onClick = {
|
leadingIcon = {
|
||||||
restoreLauncher.launch(ModuleModify.createAllowlistRestoreIntent())
|
Icon(
|
||||||
showDropdown = false
|
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
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -126,34 +176,310 @@ fun SuperUserScreen(navigator: DestinationsNavigator) {
|
|||||||
},
|
},
|
||||||
snackbarHost = { SnackbarHost(snackBarHostState) },
|
snackbarHost = { SnackbarHost(snackBarHostState) },
|
||||||
contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),
|
contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),
|
||||||
bottomBar = {
|
floatingActionButton = {
|
||||||
// 批量操作按钮,直接放在底部栏
|
// 侧边悬浮按钮集合
|
||||||
if (viewModel.showBatchActions && viewModel.selectedApps.isNotEmpty()) {
|
Column(
|
||||||
Row(
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
modifier = Modifier
|
horizontalAlignment = Alignment.End
|
||||||
.fillMaxWidth()
|
) {
|
||||||
.background(MaterialTheme.colorScheme.surface)
|
// 批量操作相关按钮
|
||||||
.padding(16.dp),
|
// 只有在批量模式且有选中应用时才显示批量操作按钮
|
||||||
horizontalArrangement = Arrangement.SpaceEvenly
|
if (viewModel.showBatchActions && viewModel.selectedApps.isNotEmpty()) {
|
||||||
) {
|
// 取消按钮
|
||||||
Button(
|
val cancelInteractionSource = remember { MutableInteractionSource() }
|
||||||
|
val isCancelPressed by cancelInteractionSource.collectIsPressedAsState()
|
||||||
|
|
||||||
|
FloatingActionButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
scope.launch {
|
viewModel.selectedApps = emptySet()
|
||||||
viewModel.updateBatchPermissions(true)
|
viewModel.showBatchActions = false
|
||||||
|
},
|
||||||
|
modifier = Modifier.size(if (isCancelPressed) 56.dp else 40.dp),
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
|
||||||
|
contentColor = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
shape = CircleShape,
|
||||||
|
interactionSource = cancelInteractionSource,
|
||||||
|
elevation = FloatingActionButtonDefaults.elevation(4.dp, 6.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.Close,
|
||||||
|
contentDescription = stringResource(android.R.string.cancel),
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
|
)
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = isCancelPressed,
|
||||||
|
enter = expandHorizontally() + fadeIn(),
|
||||||
|
exit = shrinkHorizontally() + fadeOut()
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
stringResource(android.R.string.cancel),
|
||||||
|
modifier = Modifier.padding(end = 4.dp),
|
||||||
|
style = MaterialTheme.typography.labelMedium
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
) {
|
|
||||||
Text(stringResource(R.string.batch_authorization))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Button(
|
// 取消授权按钮
|
||||||
|
val unauthorizeInteractionSource = remember { MutableInteractionSource() }
|
||||||
|
val isUnauthorizePressed by unauthorizeInteractionSource.collectIsPressedAsState()
|
||||||
|
|
||||||
|
FloatingActionButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
viewModel.updateBatchPermissions(false)
|
viewModel.updateBatchPermissions(false)
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
modifier = Modifier.size(if (isUnauthorizePressed) 56.dp else 40.dp),
|
||||||
|
containerColor = MaterialTheme.colorScheme.errorContainer,
|
||||||
|
contentColor = MaterialTheme.colorScheme.onErrorContainer,
|
||||||
|
shape = CircleShape,
|
||||||
|
interactionSource = unauthorizeInteractionSource,
|
||||||
|
elevation = FloatingActionButtonDefaults.elevation(4.dp, 6.dp)
|
||||||
) {
|
) {
|
||||||
Text(stringResource(R.string.batch_cancel_authorization))
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.Block,
|
||||||
|
contentDescription = stringResource(R.string.batch_cancel_authorization),
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
|
)
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = isUnauthorizePressed,
|
||||||
|
enter = expandHorizontally() + fadeIn(),
|
||||||
|
exit = shrinkHorizontally() + fadeOut()
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.batch_cancel_authorization),
|
||||||
|
modifier = Modifier.padding(end = 4.dp),
|
||||||
|
style = MaterialTheme.typography.labelMedium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 授权按钮
|
||||||
|
val authorizeInteractionSource = remember { MutableInteractionSource() }
|
||||||
|
val isAuthorizePressed by authorizeInteractionSource.collectIsPressedAsState()
|
||||||
|
|
||||||
|
FloatingActionButton(
|
||||||
|
onClick = {
|
||||||
|
scope.launch {
|
||||||
|
viewModel.updateBatchPermissions(true)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = Modifier.size(if (isAuthorizePressed) 56.dp else 40.dp),
|
||||||
|
containerColor = MaterialTheme.colorScheme.primaryContainer,
|
||||||
|
contentColor = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||||
|
shape = CircleShape,
|
||||||
|
interactionSource = authorizeInteractionSource,
|
||||||
|
elevation = FloatingActionButtonDefaults.elevation(4.dp, 6.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.Check,
|
||||||
|
contentDescription = stringResource(R.string.batch_authorization),
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
|
)
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = isAuthorizePressed,
|
||||||
|
enter = expandHorizontally() + fadeIn(),
|
||||||
|
exit = shrinkHorizontally() + fadeOut()
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.batch_authorization),
|
||||||
|
modifier = Modifier.padding(end = 4.dp),
|
||||||
|
style = MaterialTheme.typography.labelMedium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加分隔
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (viewModel.showBatchActions && viewModel.selectedApps.isNotEmpty()) {
|
||||||
|
|
||||||
|
// 在批量操作按钮组中添加卸载模块的按钮
|
||||||
|
// 卸载模块启用按钮
|
||||||
|
val umountEnableInteractionSource = remember { MutableInteractionSource() }
|
||||||
|
val isUmountEnablePressed by umountEnableInteractionSource.collectIsPressedAsState()
|
||||||
|
|
||||||
|
FloatingActionButton(
|
||||||
|
onClick = {
|
||||||
|
scope.launch {
|
||||||
|
viewModel.updateBatchPermissions(
|
||||||
|
allowSu = false, // 不改变ROOT权限状态
|
||||||
|
umountModules = true // 启用卸载模块
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = Modifier.size(if (isUmountEnablePressed) 56.dp else 40.dp),
|
||||||
|
containerColor = MaterialTheme.colorScheme.tertiaryContainer,
|
||||||
|
contentColor = MaterialTheme.colorScheme.onTertiaryContainer,
|
||||||
|
shape = CircleShape,
|
||||||
|
interactionSource = umountEnableInteractionSource,
|
||||||
|
elevation = FloatingActionButtonDefaults.elevation(4.dp, 6.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.FolderOff,
|
||||||
|
contentDescription = stringResource(R.string.profile_umount_modules),
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
|
)
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = isUmountEnablePressed,
|
||||||
|
enter = expandHorizontally() + fadeIn(),
|
||||||
|
exit = shrinkHorizontally() + fadeOut()
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.profile_umount_modules),
|
||||||
|
modifier = Modifier.padding(end = 4.dp),
|
||||||
|
style = MaterialTheme.typography.labelMedium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 卸载模块禁用按钮
|
||||||
|
val umountDisableInteractionSource = remember { MutableInteractionSource() }
|
||||||
|
val isUmountDisablePressed by umountDisableInteractionSource.collectIsPressedAsState()
|
||||||
|
|
||||||
|
FloatingActionButton(
|
||||||
|
onClick = {
|
||||||
|
scope.launch {
|
||||||
|
viewModel.updateBatchPermissions(
|
||||||
|
allowSu = false, // 不改变ROOT权限状态
|
||||||
|
umountModules = false // 禁用卸载模块
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = Modifier.size(if (isUmountDisablePressed) 56.dp else 40.dp),
|
||||||
|
containerColor = MaterialTheme.colorScheme.tertiaryContainer,
|
||||||
|
contentColor = MaterialTheme.colorScheme.onTertiaryContainer,
|
||||||
|
shape = CircleShape,
|
||||||
|
interactionSource = umountDisableInteractionSource,
|
||||||
|
elevation = FloatingActionButtonDefaults.elevation(4.dp, 6.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.Folder,
|
||||||
|
contentDescription = stringResource(R.string.profile_umount_modules_disable),
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
|
)
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = isUmountDisablePressed,
|
||||||
|
enter = expandHorizontally() + fadeIn(),
|
||||||
|
exit = shrinkHorizontally() + fadeOut()
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.profile_umount_modules_disable),
|
||||||
|
modifier = Modifier.padding(end = 4.dp),
|
||||||
|
style = MaterialTheme.typography.labelMedium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 添加分隔
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 向上导航按钮
|
||||||
|
val topBtnInteractionSource = remember { MutableInteractionSource() }
|
||||||
|
val isTopBtnPressed by topBtnInteractionSource.collectIsPressedAsState()
|
||||||
|
|
||||||
|
FloatingActionButton(
|
||||||
|
onClick = {
|
||||||
|
scope.launch {
|
||||||
|
listState.animateScrollToItem(0)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = Modifier.size(if (isTopBtnPressed) 56.dp else 40.dp),
|
||||||
|
containerColor = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 1f),
|
||||||
|
contentColor = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||||
|
shape = CircleShape,
|
||||||
|
interactionSource = topBtnInteractionSource,
|
||||||
|
elevation = FloatingActionButtonDefaults.elevation(4.dp, 6.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.KeyboardArrowUp,
|
||||||
|
contentDescription = stringResource(R.string.scroll_to_top_description),
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
|
)
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = isTopBtnPressed,
|
||||||
|
enter = expandHorizontally() + fadeIn(),
|
||||||
|
exit = shrinkHorizontally() + fadeOut()
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.scroll_to_top),
|
||||||
|
modifier = Modifier.padding(end = 4.dp),
|
||||||
|
style = MaterialTheme.typography.labelMedium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 向下导航按钮
|
||||||
|
val bottomBtnInteractionSource = remember { MutableInteractionSource() }
|
||||||
|
val isBottomBtnPressed by bottomBtnInteractionSource.collectIsPressedAsState()
|
||||||
|
|
||||||
|
FloatingActionButton(
|
||||||
|
onClick = {
|
||||||
|
scope.launch {
|
||||||
|
val lastIndex = viewModel.appList.size - 1
|
||||||
|
if (lastIndex >= 0) {
|
||||||
|
listState.animateScrollToItem(lastIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = Modifier.size(if (isBottomBtnPressed) 56.dp else 40.dp),
|
||||||
|
containerColor = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 1f),
|
||||||
|
contentColor = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||||
|
shape = CircleShape,
|
||||||
|
interactionSource = bottomBtnInteractionSource,
|
||||||
|
elevation = FloatingActionButtonDefaults.elevation(4.dp, 6.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.KeyboardArrowDown,
|
||||||
|
contentDescription = stringResource(R.string.scroll_to_bottom_description),
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
|
)
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = isBottomBtnPressed,
|
||||||
|
enter = expandHorizontally() + fadeIn(),
|
||||||
|
exit = shrinkHorizontally() + fadeOut()
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.scroll_to_bottom),
|
||||||
|
modifier = Modifier.padding(end = 4.dp),
|
||||||
|
style = MaterialTheme.typography.labelMedium
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -170,15 +496,23 @@ fun SuperUserScreen(navigator: DestinationsNavigator) {
|
|||||||
state = listState,
|
state = listState,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.nestedScroll(scrollBehavior.nestedScrollConnection)
|
.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||||
|
contentPadding = PaddingValues(
|
||||||
|
top = 8.dp,
|
||||||
|
bottom = 16.dp
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
// 获取分组后的应用列表 - 修改分组逻辑,避免应用重复出现在多个分组中
|
// 获取分组后的应用列表
|
||||||
val rootApps = viewModel.appList.filter { it.allowSu }
|
val rootApps = viewModel.appList.filter { it.allowSu }
|
||||||
val customApps = viewModel.appList.filter { !it.allowSu && it.hasCustomProfile }
|
val customApps = viewModel.appList.filter { !it.allowSu && it.hasCustomProfile }
|
||||||
val otherApps = viewModel.appList.filter { !it.allowSu && !it.hasCustomProfile }
|
val otherApps = viewModel.appList.filter { !it.allowSu && !it.hasCustomProfile }
|
||||||
|
|
||||||
// 显示ROOT权限应用组
|
// 显示ROOT权限应用组
|
||||||
if (rootApps.isNotEmpty()) {
|
if (rootApps.isNotEmpty()) {
|
||||||
|
item {
|
||||||
|
GroupHeader(title = stringResource(R.string.apps_with_root))
|
||||||
|
}
|
||||||
|
|
||||||
items(rootApps, key = { "root_" + it.packageName + it.uid }) { app ->
|
items(rootApps, key = { "root_" + it.packageName + it.uid }) { app ->
|
||||||
AppItem(
|
AppItem(
|
||||||
app = app,
|
app = app,
|
||||||
@@ -189,7 +523,7 @@ fun SuperUserScreen(navigator: DestinationsNavigator) {
|
|||||||
val profile = Natives.getAppProfile(app.packageName, app.uid)
|
val profile = Natives.getAppProfile(app.packageName, app.uid)
|
||||||
val updatedProfile = profile.copy(allowSu = allowSu)
|
val updatedProfile = profile.copy(allowSu = allowSu)
|
||||||
if (Natives.setAppProfile(updatedProfile)) {
|
if (Natives.setAppProfile(updatedProfile)) {
|
||||||
viewModel.fetchAppList()
|
viewModel.updateAppProfileLocally(app.packageName, updatedProfile)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -214,6 +548,10 @@ fun SuperUserScreen(navigator: DestinationsNavigator) {
|
|||||||
|
|
||||||
// 显示自定义配置应用组
|
// 显示自定义配置应用组
|
||||||
if (customApps.isNotEmpty()) {
|
if (customApps.isNotEmpty()) {
|
||||||
|
item {
|
||||||
|
GroupHeader(title = stringResource(R.string.apps_with_custom_profile))
|
||||||
|
}
|
||||||
|
|
||||||
items(customApps, key = { "custom_" + it.packageName + it.uid }) { app ->
|
items(customApps, key = { "custom_" + it.packageName + it.uid }) { app ->
|
||||||
AppItem(
|
AppItem(
|
||||||
app = app,
|
app = app,
|
||||||
@@ -224,7 +562,7 @@ fun SuperUserScreen(navigator: DestinationsNavigator) {
|
|||||||
val profile = Natives.getAppProfile(app.packageName, app.uid)
|
val profile = Natives.getAppProfile(app.packageName, app.uid)
|
||||||
val updatedProfile = profile.copy(allowSu = allowSu)
|
val updatedProfile = profile.copy(allowSu = allowSu)
|
||||||
if (Natives.setAppProfile(updatedProfile)) {
|
if (Natives.setAppProfile(updatedProfile)) {
|
||||||
viewModel.fetchAppList()
|
viewModel.updateAppProfileLocally(app.packageName, updatedProfile)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -249,6 +587,10 @@ fun SuperUserScreen(navigator: DestinationsNavigator) {
|
|||||||
|
|
||||||
// 显示其他应用组
|
// 显示其他应用组
|
||||||
if (otherApps.isNotEmpty()) {
|
if (otherApps.isNotEmpty()) {
|
||||||
|
item {
|
||||||
|
GroupHeader(title = stringResource(R.string.other_apps))
|
||||||
|
}
|
||||||
|
|
||||||
items(otherApps, key = { "other_" + it.packageName + it.uid }) { app ->
|
items(otherApps, key = { "other_" + it.packageName + it.uid }) { app ->
|
||||||
AppItem(
|
AppItem(
|
||||||
app = app,
|
app = app,
|
||||||
@@ -259,7 +601,7 @@ fun SuperUserScreen(navigator: DestinationsNavigator) {
|
|||||||
val profile = Natives.getAppProfile(app.packageName, app.uid)
|
val profile = Natives.getAppProfile(app.packageName, app.uid)
|
||||||
val updatedProfile = profile.copy(allowSu = allowSu)
|
val updatedProfile = profile.copy(allowSu = allowSu)
|
||||||
if (Natives.setAppProfile(updatedProfile)) {
|
if (Natives.setAppProfile(updatedProfile)) {
|
||||||
viewModel.fetchAppList()
|
viewModel.updateAppProfileLocally(app.packageName, updatedProfile)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -281,6 +623,38 @@ fun SuperUserScreen(navigator: DestinationsNavigator) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 当没有应用显示时显示空状态
|
||||||
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -291,7 +665,7 @@ fun GroupHeader(title: String) {
|
|||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.background(MaterialTheme.colorScheme.surfaceVariant)
|
.background(MaterialTheme.colorScheme.surfaceContainerHighest.copy(alpha = 0.7f))
|
||||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
@@ -299,7 +673,7 @@ fun GroupHeader(title: String) {
|
|||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
color = MaterialTheme.colorScheme.primary
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -316,33 +690,50 @@ private fun AppItem(
|
|||||||
onLongClick: () -> Unit,
|
onLongClick: () -> Unit,
|
||||||
viewModel: SuperUserViewModel
|
viewModel: SuperUserViewModel
|
||||||
) {
|
) {
|
||||||
ListItem(
|
val cardAlpha = CardConfig.cardAlpha
|
||||||
|
|
||||||
|
val cardColor = if (app.allowSu)
|
||||||
|
MaterialTheme.colorScheme.surfaceVariant.copy(alpha = cardAlpha)
|
||||||
|
else if (app.hasCustomProfile)
|
||||||
|
MaterialTheme.colorScheme.secondaryContainer.copy(alpha = cardAlpha)
|
||||||
|
else
|
||||||
|
MaterialTheme.colorScheme.tertiaryContainer.copy(alpha = cardAlpha)
|
||||||
|
|
||||||
|
Card(
|
||||||
|
colors = CardDefaults.cardColors(containerColor = cardColor),
|
||||||
|
elevation = CardDefaults.cardElevation(defaultElevation = 0.dp),
|
||||||
modifier = Modifier
|
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) {
|
.pointerInput(Unit) {
|
||||||
detectTapGestures(
|
detectTapGestures(
|
||||||
onLongPress = { onLongClick() },
|
onLongPress = { onLongClick() },
|
||||||
onTap = { onClick() }
|
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 = {
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
AsyncImage(
|
AsyncImage(
|
||||||
model = ImageRequest.Builder(LocalContext.current)
|
model = ImageRequest.Builder(LocalContext.current)
|
||||||
.data(app.packageInfo)
|
.data(app.packageInfo)
|
||||||
@@ -350,43 +741,147 @@ private fun AppItem(
|
|||||||
.build(),
|
.build(),
|
||||||
contentDescription = app.label,
|
contentDescription = app.label,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(4.dp)
|
.padding(end = 16.dp)
|
||||||
.width(48.dp)
|
.size(48.dp)
|
||||||
.height(48.dp)
|
.clip(MaterialTheme.shapes.small)
|
||||||
)
|
)
|
||||||
},
|
|
||||||
trailingContent = {
|
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) {
|
||||||
|
LabelItem(text = "ROOT",)
|
||||||
|
}
|
||||||
|
if (Natives.uidShouldUmount(app.uid)) {
|
||||||
|
LabelItem(text = "UNMOUNT", style = LabelItemDefaults.style.copy(
|
||||||
|
containerColor = MaterialTheme.colorScheme.secondaryContainer,
|
||||||
|
contentColor = MaterialTheme.colorScheme.onSecondaryContainer
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (app.hasCustomProfile) {
|
||||||
|
LabelItem(text = "CUSTOM", style = LabelItemDefaults.style.copy(
|
||||||
|
containerColor = MaterialTheme.colorScheme.onTertiary,
|
||||||
|
contentColor = MaterialTheme.colorScheme.onTertiaryContainer,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!viewModel.showBatchActions) {
|
if (!viewModel.showBatchActions) {
|
||||||
Switch(
|
// 开关交互源
|
||||||
checked = app.allowSu,
|
val switchInteractionSource = remember { MutableInteractionSource() }
|
||||||
onCheckedChange = onSwitchChange
|
val isSwitchPressed by switchInteractionSource.collectIsPressedAsState()
|
||||||
)
|
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.End
|
||||||
|
) {
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = isSwitchPressed,
|
||||||
|
enter = expandHorizontally() + fadeIn(),
|
||||||
|
exit = shrinkHorizontally() + fadeOut()
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = if (app.allowSu) stringResource(R.string.authorized) else stringResource(R.string.unauthorized),
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
color = if (app.allowSu) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.outline,
|
||||||
|
modifier = Modifier.padding(end = 4.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Switch(
|
||||||
|
checked = app.allowSu,
|
||||||
|
onCheckedChange = onSwitchChange,
|
||||||
|
interactionSource = switchInteractionSource,
|
||||||
|
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 {
|
} else {
|
||||||
Checkbox(
|
// 复选框交互源
|
||||||
checked = isSelected,
|
val checkboxInteractionSource = remember { MutableInteractionSource() }
|
||||||
onCheckedChange = { onToggleSelection() }
|
val isCheckboxPressed by checkboxInteractionSource.collectIsPressedAsState()
|
||||||
)
|
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.End
|
||||||
|
) {
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = isCheckboxPressed,
|
||||||
|
enter = expandHorizontally() + fadeIn(),
|
||||||
|
exit = shrinkHorizontally() + fadeOut()
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = if (isSelected) stringResource(R.string.selected) else stringResource(R.string.select),
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
color = if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.outline,
|
||||||
|
modifier = Modifier.padding(end = 4.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Checkbox(
|
||||||
|
checked = isSelected,
|
||||||
|
onCheckedChange = { onToggleSelection() },
|
||||||
|
interactionSource = checkboxInteractionSource,
|
||||||
|
colors = CheckboxDefaults.colors(
|
||||||
|
checkedColor = MaterialTheme.colorScheme.primary,
|
||||||
|
uncheckedColor = MaterialTheme.colorScheme.outline
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun LabelText(label: String) {
|
fun LabelText(label: String, backgroundColor: Color) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(top = 4.dp, end = 4.dp)
|
.padding(top = 2.dp, end = 2.dp)
|
||||||
.background(
|
.background(
|
||||||
Color.Black,
|
backgroundColor,
|
||||||
shape = RoundedCornerShape(4.dp)
|
shape = RoundedCornerShape(4.dp)
|
||||||
)
|
)
|
||||||
|
.clip(RoundedCornerShape(4.dp))
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = label,
|
text = label,
|
||||||
modifier = Modifier.padding(vertical = 2.dp, horizontal = 5.dp),
|
modifier = Modifier.padding(vertical = 2.dp, horizontal = 6.dp),
|
||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
fontSize = 8.sp,
|
fontSize = 10.sp,
|
||||||
color = Color.White,
|
color = Color.White,
|
||||||
|
fontWeight = FontWeight.Medium
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,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
|
||||||
@@ -61,7 +62,7 @@ import com.ramcosta.composedestinations.result.getOr
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import com.sukisu.ultra.R
|
import com.sukisu.ultra.R
|
||||||
import com.sukisu.ultra.ui.theme.ThemeConfig
|
import com.sukisu.ultra.ui.theme.CardConfig
|
||||||
import com.sukisu.ultra.ui.viewmodel.TemplateViewModel
|
import com.sukisu.ultra.ui.viewmodel.TemplateViewModel
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -79,11 +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 = if (!ThemeConfig.useDynamicColor) {
|
|
||||||
ThemeConfig.currentTheme.ButtonContrast
|
|
||||||
} else {
|
|
||||||
MaterialTheme.colorScheme.secondaryContainer
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
if (viewModel.templateList.isEmpty()) {
|
if (viewModel.templateList.isEmpty()) {
|
||||||
@@ -98,6 +94,9 @@ fun AppProfileTemplateScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val cardColorUse = MaterialTheme.colorScheme.surfaceVariant
|
||||||
|
val cardAlpha = CardConfig.cardAlpha
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
@@ -109,6 +108,10 @@ 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) }
|
||||||
},
|
},
|
||||||
@@ -155,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
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -205,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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -226,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.surfaceContainerLow
|
||||||
|
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
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import androidx.compose.foundation.isSystemInDarkTheme
|
|||||||
import androidx.compose.material3.CardDefaults
|
import androidx.compose.material3.CardDefaults
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
@@ -13,76 +14,120 @@ 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(0.45f)
|
// 卡片透明度
|
||||||
var cardElevation by mutableStateOf(defaultElevation)
|
var cardAlpha by mutableFloatStateOf(1f)
|
||||||
|
// 卡片亮度
|
||||||
|
var cardDim by mutableFloatStateOf(0f)
|
||||||
|
// 卡片阴影
|
||||||
|
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 isCustomDimSet 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)
|
putFloat("card_dim", cardDim)
|
||||||
|
putBoolean("custom_background_enabled", isCustomBackgroundEnabled)
|
||||||
|
putBoolean("is_shadow_enabled", isShadowEnabled)
|
||||||
putBoolean("is_custom_alpha_set", isCustomAlphaSet)
|
putBoolean("is_custom_alpha_set", isCustomAlphaSet)
|
||||||
|
putBoolean("is_custom_dim_set", isCustomDimSet)
|
||||||
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", 0.45f)
|
cardAlpha = prefs.getFloat("card_alpha", 1f)
|
||||||
cardElevation = if (prefs.getBoolean("custom_background_enabled", false)) 0.dp else defaultElevation
|
cardDim = prefs.getFloat("card_dim", 0f)
|
||||||
|
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)
|
||||||
|
isCustomDimSet = prefs.getBoolean("is_custom_dim_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.35f
|
cardAlpha = 1f
|
||||||
cardElevation = 0.dp
|
|
||||||
}
|
}
|
||||||
|
if (!isCustomDimSet) {
|
||||||
|
cardDim = 0.5f
|
||||||
|
}
|
||||||
|
updateShadowEnabled(isShadowEnabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置浅色模式默认值
|
||||||
|
*/
|
||||||
|
fun setLightModeDefaults() {
|
||||||
|
if (!isCustomAlphaSet) {
|
||||||
|
cardAlpha = 1f
|
||||||
|
}
|
||||||
|
if (!isCustomDimSet) {
|
||||||
|
cardDim = 0f
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,167 +3,612 @@ package com.sukisu.ultra.ui.theme
|
|||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
|
||||||
sealed class ThemeColors {
|
sealed class ThemeColors {
|
||||||
abstract val Primary: Color
|
// 浅色
|
||||||
abstract val Secondary: Color
|
abstract val primaryLight: Color
|
||||||
abstract val Tertiary: Color
|
abstract val onPrimaryLight: Color
|
||||||
abstract val OnPrimary: Color
|
abstract val primaryContainerLight: Color
|
||||||
abstract val OnSecondary: Color
|
abstract val onPrimaryContainerLight: Color
|
||||||
abstract val OnTertiary: Color
|
abstract val secondaryLight: Color
|
||||||
abstract val PrimaryContainer: Color
|
abstract val onSecondaryLight: Color
|
||||||
abstract val SecondaryContainer: Color
|
abstract val secondaryContainerLight: Color
|
||||||
abstract val TertiaryContainer: Color
|
abstract val onSecondaryContainerLight: Color
|
||||||
abstract val OnPrimaryContainer: Color
|
abstract val tertiaryLight: Color
|
||||||
abstract val OnSecondaryContainer: Color
|
abstract val onTertiaryLight: Color
|
||||||
abstract val OnTertiaryContainer: Color
|
abstract val tertiaryContainerLight: Color
|
||||||
abstract val ButtonContrast: Color
|
abstract val onTertiaryContainerLight: Color
|
||||||
|
abstract val errorLight: Color
|
||||||
|
abstract val onErrorLight: Color
|
||||||
|
abstract val errorContainerLight: Color
|
||||||
|
abstract val onErrorContainerLight: Color
|
||||||
|
abstract val backgroundLight: Color
|
||||||
|
abstract val onBackgroundLight: Color
|
||||||
|
abstract val surfaceLight: Color
|
||||||
|
abstract val onSurfaceLight: Color
|
||||||
|
abstract val surfaceVariantLight: Color
|
||||||
|
abstract val onSurfaceVariantLight: Color
|
||||||
|
abstract val outlineLight: Color
|
||||||
|
abstract val outlineVariantLight: Color
|
||||||
|
abstract val scrimLight: Color
|
||||||
|
abstract val inverseSurfaceLight: Color
|
||||||
|
abstract val inverseOnSurfaceLight: Color
|
||||||
|
abstract val inversePrimaryLight: Color
|
||||||
|
abstract val surfaceDimLight: Color
|
||||||
|
abstract val surfaceBrightLight: Color
|
||||||
|
abstract val surfaceContainerLowestLight: Color
|
||||||
|
abstract val surfaceContainerLowLight: Color
|
||||||
|
abstract val surfaceContainerLight: Color
|
||||||
|
abstract val surfaceContainerHighLight: Color
|
||||||
|
abstract val surfaceContainerHighestLight: Color
|
||||||
|
// 深色
|
||||||
|
abstract val primaryDark: Color
|
||||||
|
abstract val onPrimaryDark: Color
|
||||||
|
abstract val primaryContainerDark: Color
|
||||||
|
abstract val onPrimaryContainerDark: Color
|
||||||
|
abstract val secondaryDark: Color
|
||||||
|
abstract val onSecondaryDark: Color
|
||||||
|
abstract val secondaryContainerDark: Color
|
||||||
|
abstract val onSecondaryContainerDark: Color
|
||||||
|
abstract val tertiaryDark: Color
|
||||||
|
abstract val onTertiaryDark: Color
|
||||||
|
abstract val tertiaryContainerDark: Color
|
||||||
|
abstract val onTertiaryContainerDark: Color
|
||||||
|
abstract val errorDark: Color
|
||||||
|
abstract val onErrorDark: Color
|
||||||
|
abstract val errorContainerDark: Color
|
||||||
|
abstract val onErrorContainerDark: Color
|
||||||
|
abstract val backgroundDark: Color
|
||||||
|
abstract val onBackgroundDark: Color
|
||||||
|
abstract val surfaceDark: Color
|
||||||
|
abstract val onSurfaceDark: Color
|
||||||
|
abstract val surfaceVariantDark: Color
|
||||||
|
abstract val onSurfaceVariantDark: Color
|
||||||
|
abstract val outlineDark: Color
|
||||||
|
abstract val outlineVariantDark: Color
|
||||||
|
abstract val scrimDark: Color
|
||||||
|
abstract val inverseSurfaceDark: Color
|
||||||
|
abstract val inverseOnSurfaceDark: Color
|
||||||
|
abstract val inversePrimaryDark: Color
|
||||||
|
abstract val surfaceDimDark: Color
|
||||||
|
abstract val surfaceBrightDark: Color
|
||||||
|
abstract val surfaceContainerLowestDark: Color
|
||||||
|
abstract val surfaceContainerLowDark: Color
|
||||||
|
abstract val surfaceContainerDark: Color
|
||||||
|
abstract val surfaceContainerHighDark: Color
|
||||||
|
abstract val surfaceContainerHighestDark: Color
|
||||||
|
|
||||||
open fun getCustomSliderActiveColor(): Color = Primary
|
// 默认主题 (蓝色)
|
||||||
open fun getCustomSliderInactiveColor(): Color = PrimaryContainer
|
|
||||||
|
|
||||||
// Default Theme (white)
|
|
||||||
object Default : ThemeColors() {
|
object Default : ThemeColors() {
|
||||||
override val Primary = Color(0xFFFFFFFF)
|
override val primaryLight = Color(0xFF415F91)
|
||||||
override val Secondary = Color(0xFFF5F5F5)
|
override val onPrimaryLight = Color(0xFFFFFFFF)
|
||||||
override val Tertiary = Color(0xFFE0E0E0)
|
override val primaryContainerLight = Color(0xFFD6E3FF)
|
||||||
override val OnPrimary = Color(0xFF616161)
|
override val onPrimaryContainerLight = Color(0xFF284777)
|
||||||
override val OnSecondary = Color(0xFF616161)
|
override val secondaryLight = Color(0xFF565F71)
|
||||||
override val OnTertiary = Color(0xFF616161)
|
override val onSecondaryLight = Color(0xFFFFFFFF)
|
||||||
override val PrimaryContainer = Color(0xFFF5F5F5)
|
override val secondaryContainerLight = Color(0xFFDAE2F9)
|
||||||
override val SecondaryContainer = Color(0xFFEEEEEE)
|
override val onSecondaryContainerLight = Color(0xFF3E4759)
|
||||||
override val TertiaryContainer = Color(0xFFE0E0E0)
|
override val tertiaryLight = Color(0xFF705575)
|
||||||
override val OnPrimaryContainer = Color(0xFF000000)
|
override val onTertiaryLight = Color(0xFFFFFFFF)
|
||||||
override val OnSecondaryContainer = Color(0xFF000000)
|
override val tertiaryContainerLight = Color(0xFFFAD8FD)
|
||||||
override val OnTertiaryContainer = Color(0xFF000000)
|
override val onTertiaryContainerLight = Color(0xFF573E5C)
|
||||||
override val ButtonContrast = Color(0xFF00BFFF)
|
override val errorLight = Color(0xFFBA1A1A)
|
||||||
|
override val onErrorLight = Color(0xFFFFFFFF)
|
||||||
|
override val errorContainerLight = Color(0xFFFFDAD6)
|
||||||
|
override val onErrorContainerLight = Color(0xFF93000A)
|
||||||
|
override val backgroundLight = Color(0xFFF9F9FF)
|
||||||
|
override val onBackgroundLight = Color(0xFF191C20)
|
||||||
|
override val surfaceLight = Color(0xFFF9F9FF)
|
||||||
|
override val onSurfaceLight = Color(0xFF191C20)
|
||||||
|
override val surfaceVariantLight = Color(0xFFE0E2EC)
|
||||||
|
override val onSurfaceVariantLight = Color(0xFF44474E)
|
||||||
|
override val outlineLight = Color(0xFF74777F)
|
||||||
|
override val outlineVariantLight = Color(0xFFC4C6D0)
|
||||||
|
override val scrimLight = Color(0xFF000000)
|
||||||
|
override val inverseSurfaceLight = Color(0xFF2E3036)
|
||||||
|
override val inverseOnSurfaceLight = Color(0xFFF0F0F7)
|
||||||
|
override val inversePrimaryLight = Color(0xFFAAC7FF)
|
||||||
|
override val surfaceDimLight = Color(0xFFD9D9E0)
|
||||||
|
override val surfaceBrightLight = Color(0xFFF9F9FF)
|
||||||
|
override val surfaceContainerLowestLight = Color(0xFFFFFFFF)
|
||||||
|
override val surfaceContainerLowLight = Color(0xFFF3F3FA)
|
||||||
|
override val surfaceContainerLight = Color(0xFFEDEDF4)
|
||||||
|
override val surfaceContainerHighLight = Color(0xFFE7E8EE)
|
||||||
|
override val surfaceContainerHighestLight = Color(0xFFE2E2E9)
|
||||||
|
|
||||||
|
override val primaryDark = Color(0xFFAAC7FF)
|
||||||
|
override val onPrimaryDark = Color(0xFF0A305F)
|
||||||
|
override val primaryContainerDark = Color(0xFF284777)
|
||||||
|
override val onPrimaryContainerDark = Color(0xFFD6E3FF)
|
||||||
|
override val secondaryDark = Color(0xFFBEC6DC)
|
||||||
|
override val onSecondaryDark = Color(0xFF283141)
|
||||||
|
override val secondaryContainerDark = Color(0xFF3E4759)
|
||||||
|
override val onSecondaryContainerDark = Color(0xFFDAE2F9)
|
||||||
|
override val tertiaryDark = Color(0xFFDDBCE0)
|
||||||
|
override val onTertiaryDark = Color(0xFF3F2844)
|
||||||
|
override val tertiaryContainerDark = Color(0xFF573E5C)
|
||||||
|
override val onTertiaryContainerDark = Color(0xFFFAD8FD)
|
||||||
|
override val errorDark = Color(0xFFFFB4AB)
|
||||||
|
override val onErrorDark = Color(0xFF690005)
|
||||||
|
override val errorContainerDark = Color(0xFF93000A)
|
||||||
|
override val onErrorContainerDark = Color(0xFFFFDAD6)
|
||||||
|
override val backgroundDark = Color(0xFF111318)
|
||||||
|
override val onBackgroundDark = Color(0xFFE2E2E9)
|
||||||
|
override val surfaceDark = Color(0xFF111318)
|
||||||
|
override val onSurfaceDark = Color(0xFFE2E2E9)
|
||||||
|
override val surfaceVariantDark = Color(0xFF44474E)
|
||||||
|
override val onSurfaceVariantDark = Color(0xFFC4C6D0)
|
||||||
|
override val outlineDark = Color(0xFF8E9099)
|
||||||
|
override val outlineVariantDark = Color(0xFF44474E)
|
||||||
|
override val scrimDark = Color(0xFF000000)
|
||||||
|
override val inverseSurfaceDark = Color(0xFFE2E2E9)
|
||||||
|
override val inverseOnSurfaceDark = Color(0xFF2E3036)
|
||||||
|
override val inversePrimaryDark = Color(0xFF415F91)
|
||||||
|
override val surfaceDimDark = Color(0xFF111318)
|
||||||
|
override val surfaceBrightDark = Color(0xFF37393E)
|
||||||
|
override val surfaceContainerLowestDark = Color(0xFF0C0E13)
|
||||||
|
override val surfaceContainerLowDark = Color(0xFF191C20)
|
||||||
|
override val surfaceContainerDark = Color(0xFF1D2024)
|
||||||
|
override val surfaceContainerHighDark = Color(0xFF282A2F)
|
||||||
|
override val surfaceContainerHighestDark = Color(0xFF33353A)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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() {
|
object Green : ThemeColors() {
|
||||||
override val Primary = Color(0xFF4CAF50)
|
override val primaryLight = Color(0xFF4C662B)
|
||||||
override val Secondary = Color(0xFF43A047)
|
override val onPrimaryLight = Color(0xFFFFFFFF)
|
||||||
override val Tertiary = Color(0xFF1B5E20)
|
override val primaryContainerLight = Color(0xFFCDEDA3)
|
||||||
override val OnPrimary = Color(0xFFFFFFFF)
|
override val onPrimaryContainerLight = Color(0xFF354E16)
|
||||||
override val OnSecondary = Color(0xFFFFFFFF)
|
override val secondaryLight = Color(0xFF586249)
|
||||||
override val OnTertiary = Color(0xFFFFFFFF)
|
override val onSecondaryLight = Color(0xFFFFFFFF)
|
||||||
override val PrimaryContainer = Color(0xFFC8E6C9)
|
override val secondaryContainerLight = Color(0xFFDCE7C8)
|
||||||
override val SecondaryContainer = Color(0xFFA5D6A7)
|
override val onSecondaryContainerLight = Color(0xFF404A33)
|
||||||
override val TertiaryContainer = Color(0xFF81C784)
|
override val tertiaryLight = Color(0xFF386663)
|
||||||
override val OnPrimaryContainer = Color(0xFF0A1F0B)
|
override val onTertiaryLight = Color(0xFFFFFFFF)
|
||||||
override val OnSecondaryContainer = Color(0xFF0A1D0B)
|
override val tertiaryContainerLight = Color(0xFFBCECE7)
|
||||||
override val OnTertiaryContainer = Color(0xFF071F09)
|
override val onTertiaryContainerLight = Color(0xFF1F4E4B)
|
||||||
override val ButtonContrast = Color(0xFF32CD32)
|
override val errorLight = Color(0xFFBA1A1A)
|
||||||
|
override val onErrorLight = Color(0xFFFFFFFF)
|
||||||
|
override val errorContainerLight = Color(0xFFFFDAD6)
|
||||||
|
override val onErrorContainerLight = Color(0xFF93000A)
|
||||||
|
override val backgroundLight = Color(0xFFF9FAEF)
|
||||||
|
override val onBackgroundLight = Color(0xFF1A1C16)
|
||||||
|
override val surfaceLight = Color(0xFFF9FAEF)
|
||||||
|
override val onSurfaceLight = Color(0xFF1A1C16)
|
||||||
|
override val surfaceVariantLight = Color(0xFFE1E4D5)
|
||||||
|
override val onSurfaceVariantLight = Color(0xFF44483D)
|
||||||
|
override val outlineLight = Color(0xFF75796C)
|
||||||
|
override val outlineVariantLight = Color(0xFFC5C8BA)
|
||||||
|
override val scrimLight = Color(0xFF000000)
|
||||||
|
override val inverseSurfaceLight = Color(0xFF2F312A)
|
||||||
|
override val inverseOnSurfaceLight = Color(0xFFF1F2E6)
|
||||||
|
override val inversePrimaryLight = Color(0xFFB1D18A)
|
||||||
|
override val surfaceDimLight = Color(0xFFDADBD0)
|
||||||
|
override val surfaceBrightLight = Color(0xFFF9FAEF)
|
||||||
|
override val surfaceContainerLowestLight = Color(0xFFFFFFFF)
|
||||||
|
override val surfaceContainerLowLight = Color(0xFFF3F4E9)
|
||||||
|
override val surfaceContainerLight = Color(0xFFEEEFE3)
|
||||||
|
override val surfaceContainerHighLight = Color(0xFFE8E9DE)
|
||||||
|
override val surfaceContainerHighestLight = Color(0xFFE2E3D8)
|
||||||
|
|
||||||
|
override val primaryDark = Color(0xFFB1D18A)
|
||||||
|
override val onPrimaryDark = Color(0xFF1F3701)
|
||||||
|
override val primaryContainerDark = Color(0xFF354E16)
|
||||||
|
override val onPrimaryContainerDark = Color(0xFFCDEDA3)
|
||||||
|
override val secondaryDark = Color(0xFFBFCBAD)
|
||||||
|
override val onSecondaryDark = Color(0xFF2A331E)
|
||||||
|
override val secondaryContainerDark = Color(0xFF404A33)
|
||||||
|
override val onSecondaryContainerDark = Color(0xFFDCE7C8)
|
||||||
|
override val tertiaryDark = Color(0xFFA0D0CB)
|
||||||
|
override val onTertiaryDark = Color(0xFF003735)
|
||||||
|
override val tertiaryContainerDark = Color(0xFF1F4E4B)
|
||||||
|
override val onTertiaryContainerDark = Color(0xFFBCECE7)
|
||||||
|
override val errorDark = Color(0xFFFFB4AB)
|
||||||
|
override val onErrorDark = Color(0xFF690005)
|
||||||
|
override val errorContainerDark = Color(0xFF93000A)
|
||||||
|
override val onErrorContainerDark = Color(0xFFFFDAD6)
|
||||||
|
override val backgroundDark = Color(0xFF12140E)
|
||||||
|
override val onBackgroundDark = Color(0xFFE2E3D8)
|
||||||
|
override val surfaceDark = Color(0xFF12140E)
|
||||||
|
override val onSurfaceDark = Color(0xFFE2E3D8)
|
||||||
|
override val surfaceVariantDark = Color(0xFF44483D)
|
||||||
|
override val onSurfaceVariantDark = Color(0xFFC5C8BA)
|
||||||
|
override val outlineDark = Color(0xFF8F9285)
|
||||||
|
override val outlineVariantDark = Color(0xFF44483D)
|
||||||
|
override val scrimDark = Color(0xFF000000)
|
||||||
|
override val inverseSurfaceDark = Color(0xFFE2E3D8)
|
||||||
|
override val inverseOnSurfaceDark = Color(0xFF2F312A)
|
||||||
|
override val inversePrimaryDark = Color(0xFF4C662B)
|
||||||
|
override val surfaceDimDark = Color(0xFF12140E)
|
||||||
|
override val surfaceBrightDark = Color(0xFF383A32)
|
||||||
|
override val surfaceContainerLowestDark = Color(0xFF0C0F09)
|
||||||
|
override val surfaceContainerLowDark = Color(0xFF1A1C16)
|
||||||
|
override val surfaceContainerDark = Color(0xFF1E201A)
|
||||||
|
override val surfaceContainerHighDark = Color(0xFF282B24)
|
||||||
|
override val surfaceContainerHighestDark = Color(0xFF33362E)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Purple Theme
|
// 紫色主题
|
||||||
object Purple : ThemeColors() {
|
object Purple : ThemeColors() {
|
||||||
override val Primary = Color(0xFF9C27B0)
|
override val primaryLight = Color(0xFF7C4E7E)
|
||||||
override val Secondary = Color(0xFF8E24AA)
|
override val onPrimaryLight = Color(0xFFFFFFFF)
|
||||||
override val Tertiary = Color(0xFF4A148C)
|
override val primaryContainerLight = Color(0xFFFFD6FC)
|
||||||
override val OnPrimary = Color(0xFFFFFFFF)
|
override val onPrimaryContainerLight = Color(0xFF623765)
|
||||||
override val OnSecondary = Color(0xFFFFFFFF)
|
override val secondaryLight = Color(0xFF6C586B)
|
||||||
override val OnTertiary = Color(0xFFFFFFFF)
|
override val onSecondaryLight = Color(0xFFFFFFFF)
|
||||||
override val PrimaryContainer = Color(0xFFE1BEE7)
|
override val secondaryContainerLight = Color(0xFFF5DBF1)
|
||||||
override val SecondaryContainer = Color(0xFFCE93D8)
|
override val onSecondaryContainerLight = Color(0xFF534152)
|
||||||
override val TertiaryContainer = Color(0xFFB39DDB)
|
override val tertiaryLight = Color(0xFF825249)
|
||||||
override val OnPrimaryContainer = Color(0xFF1F0A23)
|
override val onTertiaryLight = Color(0xFFFFFFFF)
|
||||||
override val OnSecondaryContainer = Color(0xFF1C0A21)
|
override val tertiaryContainerLight = Color(0xFFFFDAD4)
|
||||||
override val OnTertiaryContainer = Color(0xFF12071C)
|
override val onTertiaryContainerLight = Color(0xFF673B33)
|
||||||
override val ButtonContrast = Color(0xFFDA70D6)
|
override val errorLight = Color(0xFFBA1A1A)
|
||||||
|
override val onErrorLight = Color(0xFFFFFFFF)
|
||||||
|
override val errorContainerLight = Color(0xFFFFDAD6)
|
||||||
|
override val onErrorContainerLight = Color(0xFF93000A)
|
||||||
|
override val backgroundLight = Color(0xFFFFF7FA)
|
||||||
|
override val onBackgroundLight = Color(0xFF1F1A1F)
|
||||||
|
override val surfaceLight = Color(0xFFFFF7FA)
|
||||||
|
override val onSurfaceLight = Color(0xFF1F1A1F)
|
||||||
|
override val surfaceVariantLight = Color(0xFFEDDFE8)
|
||||||
|
override val onSurfaceVariantLight = Color(0xFF4D444C)
|
||||||
|
override val outlineLight = Color(0xFF7F747C)
|
||||||
|
override val outlineVariantLight = Color(0xFFD0C3CC)
|
||||||
|
override val scrimLight = Color(0xFF000000)
|
||||||
|
override val inverseSurfaceLight = Color(0xFF352F34)
|
||||||
|
override val inverseOnSurfaceLight = Color(0xFFF9EEF4)
|
||||||
|
override val inversePrimaryLight = Color(0xFFECB4EC)
|
||||||
|
override val surfaceDimLight = Color(0xFFE2D7DE)
|
||||||
|
override val surfaceBrightLight = Color(0xFFFFF7FA)
|
||||||
|
override val surfaceContainerLowestLight = Color(0xFFFFFFFF)
|
||||||
|
override val surfaceContainerLowLight = Color(0xFFFCF0F7)
|
||||||
|
override val surfaceContainerLight = Color(0xFFF6EBF2)
|
||||||
|
override val surfaceContainerHighLight = Color(0xFFF0E5EC)
|
||||||
|
override val surfaceContainerHighestLight = Color(0xFFEBDFE6)
|
||||||
|
|
||||||
|
override val primaryDark = Color(0xFFECB4EC)
|
||||||
|
override val onPrimaryDark = Color(0xFF49204D)
|
||||||
|
override val primaryContainerDark = Color(0xFF623765)
|
||||||
|
override val onPrimaryContainerDark = Color(0xFFFFD6FC)
|
||||||
|
override val secondaryDark = Color(0xFFD8BFD5)
|
||||||
|
override val onSecondaryDark = Color(0xFF3B2B3B)
|
||||||
|
override val secondaryContainerDark = Color(0xFF534152)
|
||||||
|
override val onSecondaryContainerDark = Color(0xFFF5DBF1)
|
||||||
|
override val tertiaryDark = Color(0xFFF6B8AD)
|
||||||
|
override val onTertiaryDark = Color(0xFF4C251F)
|
||||||
|
override val tertiaryContainerDark = Color(0xFF673B33)
|
||||||
|
override val onTertiaryContainerDark = Color(0xFFFFDAD4)
|
||||||
|
override val errorDark = Color(0xFFFFB4AB)
|
||||||
|
override val onErrorDark = Color(0xFF690005)
|
||||||
|
override val errorContainerDark = Color(0xFF93000A)
|
||||||
|
override val onErrorContainerDark = Color(0xFFFFDAD6)
|
||||||
|
override val backgroundDark = Color(0xFF171216)
|
||||||
|
override val onBackgroundDark = Color(0xFFEBDFE6)
|
||||||
|
override val surfaceDark = Color(0xFF171216)
|
||||||
|
override val onSurfaceDark = Color(0xFFEBDFE6)
|
||||||
|
override val surfaceVariantDark = Color(0xFF4D444C)
|
||||||
|
override val onSurfaceVariantDark = Color(0xFFD0C3CC)
|
||||||
|
override val outlineDark = Color(0xFF998D96)
|
||||||
|
override val outlineVariantDark = Color(0xFF4D444C)
|
||||||
|
override val scrimDark = Color(0xFF000000)
|
||||||
|
override val inverseSurfaceDark = Color(0xFFEBDFE6)
|
||||||
|
override val inverseOnSurfaceDark = Color(0xFF352F34)
|
||||||
|
override val inversePrimaryDark = Color(0xFF7C4E7E)
|
||||||
|
override val surfaceDimDark = Color(0xFF171216)
|
||||||
|
override val surfaceBrightDark = Color(0xFF3E373D)
|
||||||
|
override val surfaceContainerLowestDark = Color(0xFF110D11)
|
||||||
|
override val surfaceContainerLowDark = Color(0xFF1F1A1F)
|
||||||
|
override val surfaceContainerDark = Color(0xFF231E23)
|
||||||
|
override val surfaceContainerHighDark = Color(0xFF2E282D)
|
||||||
|
override val surfaceContainerHighestDark = Color(0xFF393338)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Orange Theme
|
// 橙色主题
|
||||||
object Orange : ThemeColors() {
|
object Orange : ThemeColors() {
|
||||||
override val Primary = Color(0xFFFF9800)
|
override val primaryLight = Color(0xFF8B4F24)
|
||||||
override val Secondary = Color(0xFFFB8C00)
|
override val onPrimaryLight = Color(0xFFFFFFFF)
|
||||||
override val Tertiary = Color(0xFFE65100)
|
override val primaryContainerLight = Color(0xFFFFDCC7)
|
||||||
override val OnPrimary = Color(0xFFFFFFFF)
|
override val onPrimaryContainerLight = Color(0xFF6E390E)
|
||||||
override val OnSecondary = Color(0xFFFFFFFF)
|
override val secondaryLight = Color(0xFF755846)
|
||||||
override val OnTertiary = Color(0xFFFFFFFF)
|
override val onSecondaryLight = Color(0xFFFFFFFF)
|
||||||
override val PrimaryContainer = Color(0xFFFFE0B2)
|
override val secondaryContainerLight = Color(0xFFFFDCC7)
|
||||||
override val SecondaryContainer = Color(0xFFFFCC80)
|
override val onSecondaryContainerLight = Color(0xFF5B4130)
|
||||||
override val TertiaryContainer = Color(0xFFFFB74D)
|
override val tertiaryLight = Color(0xFF865219)
|
||||||
override val OnPrimaryContainer = Color(0xFF1A1100)
|
override val onTertiaryLight = Color(0xFFFFFFFF)
|
||||||
override val OnSecondaryContainer = Color(0xFF1A1000)
|
override val tertiaryContainerLight = Color(0xFFFFDCBF)
|
||||||
override val OnTertiaryContainer = Color(0xFF1A0B00)
|
override val onTertiaryContainerLight = Color(0xFF6A3B01)
|
||||||
override val ButtonContrast = Color(0xFFFF6347)
|
override val errorLight = Color(0xFFBA1A1A)
|
||||||
|
override val onErrorLight = Color(0xFFFFFFFF)
|
||||||
|
override val errorContainerLight = Color(0xFFFFDAD6)
|
||||||
|
override val onErrorContainerLight = Color(0xFF93000A)
|
||||||
|
override val backgroundLight = Color(0xFFFFF8F5)
|
||||||
|
override val onBackgroundLight = Color(0xFF221A15)
|
||||||
|
override val surfaceLight = Color(0xFFFFF8F5)
|
||||||
|
override val onSurfaceLight = Color(0xFF221A15)
|
||||||
|
override val surfaceVariantLight = Color(0xFFF4DED3)
|
||||||
|
override val onSurfaceVariantLight = Color(0xFF52443C)
|
||||||
|
override val outlineLight = Color(0xFF84746A)
|
||||||
|
override val outlineVariantLight = Color(0xFFD7C3B8)
|
||||||
|
override val scrimLight = Color(0xFF000000)
|
||||||
|
override val inverseSurfaceLight = Color(0xFF382E29)
|
||||||
|
override val inverseOnSurfaceLight = Color(0xFFFFEDE5)
|
||||||
|
override val inversePrimaryLight = Color(0xFFFFB787)
|
||||||
|
override val surfaceDimLight = Color(0xFFE7D7CE)
|
||||||
|
override val surfaceBrightLight = Color(0xFFFFF8F5)
|
||||||
|
override val surfaceContainerLowestLight = Color(0xFFFFFFFF)
|
||||||
|
override val surfaceContainerLowLight = Color(0xFFFFF1EA)
|
||||||
|
override val surfaceContainerLight = Color(0xFFFCEBE2)
|
||||||
|
override val surfaceContainerHighLight = Color(0xFFF6E5DC)
|
||||||
|
override val surfaceContainerHighestLight = Color(0xFFF0DFD7)
|
||||||
|
|
||||||
|
override val primaryDark = Color(0xFFFFB787)
|
||||||
|
override val onPrimaryDark = Color(0xFF502400)
|
||||||
|
override val primaryContainerDark = Color(0xFF6E390E)
|
||||||
|
override val onPrimaryContainerDark = Color(0xFFFFDCC7)
|
||||||
|
override val secondaryDark = Color(0xFFE5BFA8)
|
||||||
|
override val onSecondaryDark = Color(0xFF422B1B)
|
||||||
|
override val secondaryContainerDark = Color(0xFF5B4130)
|
||||||
|
override val onSecondaryContainerDark = Color(0xFFFFDCC7)
|
||||||
|
override val tertiaryDark = Color(0xFFFDB876)
|
||||||
|
override val onTertiaryDark = Color(0xFF4B2800)
|
||||||
|
override val tertiaryContainerDark = Color(0xFF6A3B01)
|
||||||
|
override val onTertiaryContainerDark = Color(0xFFFFDCBF)
|
||||||
|
override val errorDark = Color(0xFFFFB4AB)
|
||||||
|
override val onErrorDark = Color(0xFF690005)
|
||||||
|
override val errorContainerDark = Color(0xFF93000A)
|
||||||
|
override val onErrorContainerDark = Color(0xFFFFDAD6)
|
||||||
|
override val backgroundDark = Color(0xFF19120D)
|
||||||
|
override val onBackgroundDark = Color(0xFFF0DFD7)
|
||||||
|
override val surfaceDark = Color(0xFF19120D)
|
||||||
|
override val onSurfaceDark = Color(0xFFF0DFD7)
|
||||||
|
override val surfaceVariantDark = Color(0xFF52443C)
|
||||||
|
override val onSurfaceVariantDark = Color(0xFFD7C3B8)
|
||||||
|
override val outlineDark = Color(0xFF9F8D83)
|
||||||
|
override val outlineVariantDark = Color(0xFF52443C)
|
||||||
|
override val scrimDark = Color(0xFF000000)
|
||||||
|
override val inverseSurfaceDark = Color(0xFFF0DFD7)
|
||||||
|
override val inverseOnSurfaceDark = Color(0xFF382E29)
|
||||||
|
override val inversePrimaryDark = Color(0xFF8B4F24)
|
||||||
|
override val surfaceDimDark = Color(0xFF19120D)
|
||||||
|
override val surfaceBrightDark = Color(0xFF413731)
|
||||||
|
override val surfaceContainerLowestDark = Color(0xFF140D08)
|
||||||
|
override val surfaceContainerLowDark = Color(0xFF221A15)
|
||||||
|
override val surfaceContainerDark = Color(0xFF261E19)
|
||||||
|
override val surfaceContainerHighDark = Color(0xFF312823)
|
||||||
|
override val surfaceContainerHighestDark = Color(0xFF3D332D)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pink Theme
|
// 粉色主题
|
||||||
object Pink : ThemeColors() {
|
object Pink : ThemeColors() {
|
||||||
override val Primary = Color(0xFFE91E63)
|
override val primaryLight = Color(0xFF8C4A60)
|
||||||
override val Secondary = Color(0xFFD81B60)
|
override val onPrimaryLight = Color(0xFFFFFFFF)
|
||||||
override val Tertiary = Color(0xFF880E4F)
|
override val primaryContainerLight = Color(0xFFFFD9E2)
|
||||||
override val OnPrimary = Color(0xFFFFFFFF)
|
override val onPrimaryContainerLight = Color(0xFF703348)
|
||||||
override val OnSecondary = Color(0xFFFFFFFF)
|
override val secondaryLight = Color(0xFF8B4A62)
|
||||||
override val OnTertiary = Color(0xFFFFFFFF)
|
override val onSecondaryLight = Color(0xFFFFFFFF)
|
||||||
override val PrimaryContainer = Color(0xFFF8BBD0)
|
override val secondaryContainerLight = Color(0xFFFFD9E3)
|
||||||
override val SecondaryContainer = Color(0xFFF48FB1)
|
override val onSecondaryContainerLight = Color(0xFF6F334B)
|
||||||
override val TertiaryContainer = Color(0xFFE91E63)
|
override val tertiaryLight = Color(0xFF8B4A62)
|
||||||
override val OnPrimaryContainer = Color(0xFF2E0A14)
|
override val onTertiaryLight = Color(0xFFFFFFFF)
|
||||||
override val OnSecondaryContainer = Color(0xFF2B0A13)
|
override val tertiaryContainerLight = Color(0xFFFFD9E3)
|
||||||
override val OnTertiaryContainer = Color(0xFF1C0311)
|
override val onTertiaryContainerLight = Color(0xFF6F334B)
|
||||||
override val ButtonContrast = Color(0xFFFF1493)
|
override val errorLight = Color(0xFFBA1A1A)
|
||||||
|
override val onErrorLight = Color(0xFFFFFFFF)
|
||||||
|
override val errorContainerLight = Color(0xFFFFDAD6)
|
||||||
|
override val onErrorContainerLight = Color(0xFF93000A)
|
||||||
|
override val backgroundLight = Color(0xFFFFF8F8)
|
||||||
|
override val onBackgroundLight = Color(0xFF22191B)
|
||||||
|
override val surfaceLight = Color(0xFFFFF8F8)
|
||||||
|
override val onSurfaceLight = Color(0xFF22191B)
|
||||||
|
override val surfaceVariantLight = Color(0xFFF2DDE1)
|
||||||
|
override val onSurfaceVariantLight = Color(0xFF514346)
|
||||||
|
override val outlineLight = Color(0xFF837377)
|
||||||
|
override val outlineVariantLight = Color(0xFFD5C2C5)
|
||||||
|
override val scrimLight = Color(0xFF000000)
|
||||||
|
override val inverseSurfaceLight = Color(0xFF372E30)
|
||||||
|
override val inverseOnSurfaceLight = Color(0xFFFDEDEF)
|
||||||
|
override val inversePrimaryLight = Color(0xFFFFB1C7)
|
||||||
|
override val surfaceDimLight = Color(0xFFE6D6D9)
|
||||||
|
override val surfaceBrightLight = Color(0xFFFFF8F8)
|
||||||
|
override val surfaceContainerLowestLight = Color(0xFFFFFFFF)
|
||||||
|
override val surfaceContainerLowLight = Color(0xFFFFF0F2)
|
||||||
|
override val surfaceContainerLight = Color(0xFFFBEAED)
|
||||||
|
override val surfaceContainerHighLight = Color(0xFFF5E4E7)
|
||||||
|
override val surfaceContainerHighestLight = Color(0xFFEFDFE1)
|
||||||
|
|
||||||
|
override val primaryDark = Color(0xFFFFB1C7)
|
||||||
|
override val onPrimaryDark = Color(0xFF541D32)
|
||||||
|
override val primaryContainerDark = Color(0xFF703348)
|
||||||
|
override val onPrimaryContainerDark = Color(0xFFFFD9E2)
|
||||||
|
override val secondaryDark = Color(0xFFFFB0CB)
|
||||||
|
override val onSecondaryDark = Color(0xFF541D34)
|
||||||
|
override val secondaryContainerDark = Color(0xFF6F334B)
|
||||||
|
override val onSecondaryContainerDark = Color(0xFFFFD9E3)
|
||||||
|
override val tertiaryDark = Color(0xFFFFB0CB)
|
||||||
|
override val onTertiaryDark = Color(0xFF541D34)
|
||||||
|
override val tertiaryContainerDark = Color(0xFF6F334B)
|
||||||
|
override val onTertiaryContainerDark = Color(0xFFFFD9E3)
|
||||||
|
override val errorDark = Color(0xFFFFB4AB)
|
||||||
|
override val onErrorDark = Color(0xFF690005)
|
||||||
|
override val errorContainerDark = Color(0xFF93000A)
|
||||||
|
override val onErrorContainerDark = Color(0xFFFFDAD6)
|
||||||
|
override val backgroundDark = Color(0xFF191113)
|
||||||
|
override val onBackgroundDark = Color(0xFFEFDFE1)
|
||||||
|
override val surfaceDark = Color(0xFF191113)
|
||||||
|
override val onSurfaceDark = Color(0xFFEFDFE1)
|
||||||
|
override val surfaceVariantDark = Color(0xFF514346)
|
||||||
|
override val onSurfaceVariantDark = Color(0xFFD5C2C5)
|
||||||
|
override val outlineDark = Color(0xFF9E8C90)
|
||||||
|
override val outlineVariantDark = Color(0xFF514346)
|
||||||
|
override val scrimDark = Color(0xFF000000)
|
||||||
|
override val inverseSurfaceDark = Color(0xFFEFDFE1)
|
||||||
|
override val inverseOnSurfaceDark = Color(0xFF372E30)
|
||||||
|
override val inversePrimaryDark = Color(0xFF8C4A60)
|
||||||
|
override val surfaceDimDark = Color(0xFF191113)
|
||||||
|
override val surfaceBrightDark = Color(0xFF413739)
|
||||||
|
override val surfaceContainerLowestDark = Color(0xFF140C0E)
|
||||||
|
override val surfaceContainerLowDark = Color(0xFF22191B)
|
||||||
|
override val surfaceContainerDark = Color(0xFF261D1F)
|
||||||
|
override val surfaceContainerHighDark = Color(0xFF31282A)
|
||||||
|
override val surfaceContainerHighestDark = Color(0xFF3C3234)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gray Theme
|
// 灰色主题
|
||||||
object Gray : ThemeColors() {
|
object Gray : ThemeColors() {
|
||||||
override val Primary = Color(0xFF9E9E9E)
|
override val primaryLight = Color(0xFF5B5C5C)
|
||||||
override val Secondary = Color(0xFF757575)
|
override val onPrimaryLight = Color(0xFFFFFFFF)
|
||||||
override val Tertiary = Color(0xFF616161)
|
override val primaryContainerLight = Color(0xFF747474)
|
||||||
override val OnPrimary = Color(0xFFFFFFFF)
|
override val onPrimaryContainerLight = Color(0xFFFEFCFC)
|
||||||
override val OnSecondary = Color(0xFFFFFFFF)
|
override val secondaryLight = Color(0xFF5F5E5E)
|
||||||
override val OnTertiary = Color(0xFFFFFFFF)
|
override val onSecondaryLight = Color(0xFFFFFFFF)
|
||||||
override val PrimaryContainer = Color(0xFFEEEEEE)
|
override val secondaryContainerLight = Color(0xFFE4E2E1)
|
||||||
override val SecondaryContainer = Color(0xFFE0E0E0)
|
override val onSecondaryContainerLight = Color(0xFF656464)
|
||||||
override val TertiaryContainer = Color(0xFFBDBDBD)
|
override val tertiaryLight = Color(0xFF5E5B5D)
|
||||||
override val OnPrimaryContainer = Color(0xFF1A1A1A)
|
override val onTertiaryLight = Color(0xFFFFFFFF)
|
||||||
override val OnSecondaryContainer = Color(0xFF171717)
|
override val tertiaryContainerLight = Color(0xFF777375)
|
||||||
override val OnTertiaryContainer = Color(0xFF141414)
|
override val onTertiaryContainerLight = Color(0xFFFFFBFF)
|
||||||
override val ButtonContrast = Color(0xFF696969)
|
override val errorLight = Color(0xFFBA1A1A)
|
||||||
|
override val onErrorLight = Color(0xFFFFFFFF)
|
||||||
|
override val errorContainerLight = Color(0xFFFFDAD6)
|
||||||
|
override val onErrorContainerLight = Color(0xFF93000A)
|
||||||
|
override val backgroundLight = Color(0xFFFCF8F8)
|
||||||
|
override val onBackgroundLight = Color(0xFF1C1B1B)
|
||||||
|
override val surfaceLight = Color(0xFFFCF8F8)
|
||||||
|
override val onSurfaceLight = Color(0xFF1C1B1B)
|
||||||
|
override val surfaceVariantLight = Color(0xFFE0E3E3)
|
||||||
|
override val onSurfaceVariantLight = Color(0xFF444748)
|
||||||
|
override val outlineLight = Color(0xFF747878)
|
||||||
|
override val outlineVariantLight = Color(0xFFC4C7C7)
|
||||||
|
override val scrimLight = Color(0xFF000000)
|
||||||
|
override val inverseSurfaceLight = Color(0xFF313030)
|
||||||
|
override val inverseOnSurfaceLight = Color(0xFFF4F0EF)
|
||||||
|
override val inversePrimaryLight = Color(0xFFC7C6C6)
|
||||||
|
override val surfaceDimLight = Color(0xFFDDD9D8)
|
||||||
|
override val surfaceBrightLight = Color(0xFFFCF8F8)
|
||||||
|
override val surfaceContainerLowestLight = Color(0xFFFFFFFF)
|
||||||
|
override val surfaceContainerLowLight = Color(0xFFF7F3F2)
|
||||||
|
override val surfaceContainerLight = Color(0xFFF1EDEC)
|
||||||
|
override val surfaceContainerHighLight = Color(0xFFEBE7E7)
|
||||||
|
override val surfaceContainerHighestLight = Color(0xFFE5E2E1)
|
||||||
|
|
||||||
|
override val primaryDark = Color(0xFFC7C6C6)
|
||||||
|
override val onPrimaryDark = Color(0xFF303031)
|
||||||
|
override val primaryContainerDark = Color(0xFF919190)
|
||||||
|
override val onPrimaryContainerDark = Color(0xFF161718)
|
||||||
|
override val secondaryDark = Color(0xFFC8C6C5)
|
||||||
|
override val onSecondaryDark = Color(0xFF303030)
|
||||||
|
override val secondaryContainerDark = Color(0xFF474746)
|
||||||
|
override val onSecondaryContainerDark = Color(0xFFB7B5B4)
|
||||||
|
override val tertiaryDark = Color(0xFFCAC5C7)
|
||||||
|
override val onTertiaryDark = Color(0xFF323031)
|
||||||
|
override val tertiaryContainerDark = Color(0xFF948F91)
|
||||||
|
override val onTertiaryContainerDark = Color(0xFF181718)
|
||||||
|
override val errorDark = Color(0xFFFFB4AB)
|
||||||
|
override val onErrorDark = Color(0xFF690005)
|
||||||
|
override val errorContainerDark = Color(0xFF93000A)
|
||||||
|
override val onErrorContainerDark = Color(0xFFFFDAD6)
|
||||||
|
override val backgroundDark = Color(0xFF141313)
|
||||||
|
override val onBackgroundDark = Color(0xFFE5E2E1)
|
||||||
|
override val surfaceDark = Color(0xFF141313)
|
||||||
|
override val onSurfaceDark = Color(0xFFE5E2E1)
|
||||||
|
override val surfaceVariantDark = Color(0xFF444748)
|
||||||
|
override val onSurfaceVariantDark = Color(0xFFC4C7C7)
|
||||||
|
override val outlineDark = Color(0xFF8E9192)
|
||||||
|
override val outlineVariantDark = Color(0xFF444748)
|
||||||
|
override val scrimDark = Color(0xFF000000)
|
||||||
|
override val inverseSurfaceDark = Color(0xFFE5E2E1)
|
||||||
|
override val inverseOnSurfaceDark = Color(0xFF313030)
|
||||||
|
override val inversePrimaryDark = Color(0xFF5E5E5E)
|
||||||
|
override val surfaceDimDark = Color(0xFF141313)
|
||||||
|
override val surfaceBrightDark = Color(0xFF3A3939)
|
||||||
|
override val surfaceContainerLowestDark = Color(0xFF0E0E0E)
|
||||||
|
override val surfaceContainerLowDark = Color(0xFF1C1B1B)
|
||||||
|
override val surfaceContainerDark = Color(0xFF201F1F)
|
||||||
|
override val surfaceContainerHighDark = Color(0xFF2A2A2A)
|
||||||
|
override val surfaceContainerHighestDark = Color(0xFF353434)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 黄色主题
|
||||||
object Yellow : ThemeColors() {
|
object Yellow : ThemeColors() {
|
||||||
override val Primary = Color(0xFFFFD700)
|
override val primaryLight = Color(0xFF6D5E0F)
|
||||||
override val Secondary = Color(0xFFFFBC52)
|
override val onPrimaryLight = Color(0xFFFFFFFF)
|
||||||
override val Tertiary = Color(0xFF795548)
|
override val primaryContainerLight = Color(0xFFF8E288)
|
||||||
override val OnPrimary = Color(0xFFFFFFFF)
|
override val onPrimaryContainerLight = Color(0xFF534600)
|
||||||
override val OnSecondary = Color(0xFFFFFFFF)
|
override val secondaryLight = Color(0xFF6D5E0F)
|
||||||
override val OnTertiary = Color(0xFFFFFFFF)
|
override val onSecondaryLight = Color(0xFFFFFFFF)
|
||||||
override val PrimaryContainer = Color(0xFFFFF7D6)
|
override val secondaryContainerLight = Color(0xFFF7E388)
|
||||||
override val SecondaryContainer = Color(0xFFFFE6B3)
|
override val onSecondaryContainerLight = Color(0xFF534600)
|
||||||
override val TertiaryContainer = Color(0xFFD7CCC8)
|
override val tertiaryLight = Color(0xFF685F13)
|
||||||
override val OnPrimaryContainer = Color(0xFF1A1600)
|
override val onTertiaryLight = Color(0xFFFFFFFF)
|
||||||
override val OnSecondaryContainer = Color(0xFF1A1100)
|
override val tertiaryContainerLight = Color(0xFFF1E58A)
|
||||||
override val OnTertiaryContainer = Color(0xFF1A1717)
|
override val onTertiaryContainerLight = Color(0xFF4F4800)
|
||||||
override val ButtonContrast = Color(0xFFFFD700)
|
override val errorLight = Color(0xFFBA1A1A)
|
||||||
|
override val onErrorLight = Color(0xFFFFFFFF)
|
||||||
|
override val errorContainerLight = Color(0xFFFFDAD6)
|
||||||
|
override val onErrorContainerLight = Color(0xFF93000A)
|
||||||
|
override val backgroundLight = Color(0xFFFFF9ED)
|
||||||
|
override val onBackgroundLight = Color(0xFF1E1C13)
|
||||||
|
override val surfaceLight = Color(0xFFFFF9ED)
|
||||||
|
override val onSurfaceLight = Color(0xFF1E1C13)
|
||||||
|
override val surfaceVariantLight = Color(0xFFE9E2D0)
|
||||||
|
override val onSurfaceVariantLight = Color(0xFF4B4739)
|
||||||
|
override val outlineLight = Color(0xFF7C7768)
|
||||||
|
override val outlineVariantLight = Color(0xFFCDC6B4)
|
||||||
|
override val scrimLight = Color(0xFF000000)
|
||||||
|
override val inverseSurfaceLight = Color(0xFF333027)
|
||||||
|
override val inverseOnSurfaceLight = Color(0xFFF7F0E2)
|
||||||
|
override val inversePrimaryLight = Color(0xFFDAC66F)
|
||||||
|
override val surfaceDimLight = Color(0xFFE0D9CC)
|
||||||
|
override val surfaceBrightLight = Color(0xFFFFF9ED)
|
||||||
|
override val surfaceContainerLowestLight = Color(0xFFFFFFFF)
|
||||||
|
override val surfaceContainerLowLight = Color(0xFFFAF3E5)
|
||||||
|
override val surfaceContainerLight = Color(0xFFF4EDDF)
|
||||||
|
override val surfaceContainerHighLight = Color(0xFFEEE8DA)
|
||||||
|
override val surfaceContainerHighestLight = Color(0xFFE8E2D4)
|
||||||
|
|
||||||
|
override val primaryDark = Color(0xFFDAC66F)
|
||||||
|
override val onPrimaryDark = Color(0xFF393000)
|
||||||
|
override val primaryContainerDark = Color(0xFF534600)
|
||||||
|
override val onPrimaryContainerDark = Color(0xFFF8E288)
|
||||||
|
override val secondaryDark = Color(0xFFDAC76F)
|
||||||
|
override val onSecondaryDark = Color(0xFF393000)
|
||||||
|
override val secondaryContainerDark = Color(0xFF534600)
|
||||||
|
override val onSecondaryContainerDark = Color(0xFFF7E388)
|
||||||
|
override val tertiaryDark = Color(0xFFD4C871)
|
||||||
|
override val onTertiaryDark = Color(0xFF363100)
|
||||||
|
override val tertiaryContainerDark = Color(0xFF4F4800)
|
||||||
|
override val onTertiaryContainerDark = Color(0xFFF1E58A)
|
||||||
|
override val errorDark = Color(0xFFFFB4AB)
|
||||||
|
override val onErrorDark = Color(0xFF690005)
|
||||||
|
override val errorContainerDark = Color(0xFF93000A)
|
||||||
|
override val onErrorContainerDark = Color(0xFFFFDAD6)
|
||||||
|
override val backgroundDark = Color(0xFF15130B)
|
||||||
|
override val onBackgroundDark = Color(0xFFE8E2D4)
|
||||||
|
override val surfaceDark = Color(0xFF15130B)
|
||||||
|
override val onSurfaceDark = Color(0xFFE8E2D4)
|
||||||
|
override val surfaceVariantDark = Color(0xFF4B4739)
|
||||||
|
override val onSurfaceVariantDark = Color(0xFFCDC6B4)
|
||||||
|
override val outlineDark = Color(0xFF969080)
|
||||||
|
override val outlineVariantDark = Color(0xFF4B4739)
|
||||||
|
override val scrimDark = Color(0xFF000000)
|
||||||
|
override val inverseSurfaceDark = Color(0xFFE8E2D4)
|
||||||
|
override val inverseOnSurfaceDark = Color(0xFF333027)
|
||||||
|
override val inversePrimaryDark = Color(0xFF6D5E0F)
|
||||||
|
override val surfaceDimDark = Color(0xFF15130B)
|
||||||
|
override val surfaceBrightDark = Color(0xFF3C3930)
|
||||||
|
override val surfaceContainerLowestDark = Color(0xFF100E07)
|
||||||
|
override val surfaceContainerLowDark = Color(0xFF1E1C13)
|
||||||
|
override val surfaceContainerDark = Color(0xFF222017)
|
||||||
|
override val surfaceContainerHighDark = Color(0xFF2C2A21)
|
||||||
|
override val surfaceContainerHighestDark = Color(0xFF37352B)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromName(name: String): ThemeColors = when (name.lowercase()) {
|
fun fromName(name: String): ThemeColors = when (name.lowercase()) {
|
||||||
"blue" -> Blue
|
|
||||||
"green" -> Green
|
"green" -> Green
|
||||||
"purple" -> Purple
|
"purple" -> Purple
|
||||||
"orange" -> Orange
|
"orange" -> Orange
|
||||||
"pink" -> Pink
|
"pink" -> Pink
|
||||||
"gray" -> Gray
|
"gray" -> Gray
|
||||||
"white" -> Yellow
|
"yellow" -> Yellow
|
||||||
else -> Default
|
else -> Default
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,10 @@ import android.content.Context
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.util.Log
|
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.isSystemInDarkTheme
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
@@ -14,19 +18,24 @@ import androidx.compose.material3.dynamicDarkColorScheme
|
|||||||
import androidx.compose.material3.dynamicLightColorScheme
|
import androidx.compose.material3.dynamicLightColorScheme
|
||||||
import androidx.compose.material3.lightColorScheme
|
import androidx.compose.material3.lightColorScheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.alpha
|
||||||
import androidx.compose.ui.draw.paint
|
import androidx.compose.ui.draw.paint
|
||||||
import androidx.compose.ui.graphics.Brush
|
import androidx.compose.ui.graphics.Brush
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.zIndex
|
import androidx.compose.ui.zIndex
|
||||||
|
import coil.compose.AsyncImagePainter
|
||||||
import coil.compose.rememberAsyncImagePainter
|
import coil.compose.rememberAsyncImagePainter
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.ui.graphics.luminance
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
@@ -35,62 +44,43 @@ import androidx.core.content.edit
|
|||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import com.sukisu.ultra.ui.util.BackgroundTransformation
|
import com.sukisu.ultra.ui.util.BackgroundTransformation
|
||||||
import com.sukisu.ultra.ui.util.saveTransformedBackground
|
import com.sukisu.ultra.ui.util.saveTransformedBackground
|
||||||
|
import androidx.activity.SystemBarStyle
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.activity.enableEdgeToEdge
|
||||||
|
import androidx.compose.runtime.SideEffect
|
||||||
|
import androidx.compose.ui.graphics.toArgb
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 主题配置对象,管理应用的主题相关状态
|
||||||
|
*/
|
||||||
object ThemeConfig {
|
object ThemeConfig {
|
||||||
var customBackgroundUri by mutableStateOf<Uri?>(null)
|
var customBackgroundUri by mutableStateOf<Uri?>(null)
|
||||||
var forceDarkMode by mutableStateOf<Boolean?>(null)
|
var forceDarkMode by mutableStateOf<Boolean?>(null)
|
||||||
var currentTheme by mutableStateOf<ThemeColors>(ThemeColors.Default)
|
var currentTheme by mutableStateOf<ThemeColors>(ThemeColors.Default)
|
||||||
var useDynamicColor by mutableStateOf(false)
|
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
|
/**
|
||||||
private fun getDarkColorScheme() = darkColorScheme(
|
* 应用主题
|
||||||
primary = ThemeConfig.currentTheme.Primary.copy(alpha = 0.8f),
|
*/
|
||||||
onPrimary = mixColors(ThemeConfig.currentTheme.Primary, Color.White, 0.2f),
|
|
||||||
primaryContainer = ThemeConfig.currentTheme.PrimaryContainer.copy(alpha = 0.15f),
|
|
||||||
onPrimaryContainer = mixColors(ThemeConfig.currentTheme.Primary, Color.White, 0.2f),
|
|
||||||
secondary = ThemeConfig.currentTheme.Secondary.copy(alpha = 0.8f),
|
|
||||||
onSecondary = mixColors(ThemeConfig.currentTheme.Secondary, Color.White, 0.2f),
|
|
||||||
secondaryContainer = ThemeConfig.currentTheme.SecondaryContainer.copy(alpha = 0.15f),
|
|
||||||
onSecondaryContainer = mixColors(ThemeConfig.currentTheme.Secondary, Color.White, 0.2f),
|
|
||||||
tertiary = ThemeConfig.currentTheme.Tertiary.copy(alpha = 0.8f),
|
|
||||||
onTertiary = mixColors(ThemeConfig.currentTheme.Tertiary, Color.White, 0.2f),
|
|
||||||
tertiaryContainer = ThemeConfig.currentTheme.TertiaryContainer.copy(alpha = 0.15f),
|
|
||||||
onTertiaryContainer = mixColors(ThemeConfig.currentTheme.Tertiary, Color.White, 0.2f),
|
|
||||||
background = Color.Transparent,
|
|
||||||
surface = Color.Transparent,
|
|
||||||
onBackground = mixColors(ThemeConfig.currentTheme.Primary, Color.White, 0.1f),
|
|
||||||
onSurface = mixColors(ThemeConfig.currentTheme.Primary, Color.White, 0.1f),
|
|
||||||
surfaceVariant = Color(0xFF2F2F2F),
|
|
||||||
onSurfaceVariant = mixColors(ThemeConfig.currentTheme.Primary, Color.White, 0.2f),
|
|
||||||
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)
|
|
||||||
)
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun KernelSUTheme(
|
fun KernelSUTheme(
|
||||||
darkTheme: Boolean = when(ThemeConfig.forceDarkMode) {
|
darkTheme: Boolean = when(ThemeConfig.forceDarkMode) {
|
||||||
@@ -102,94 +92,167 @@ fun KernelSUTheme(
|
|||||||
content: @Composable () -> Unit
|
content: @Composable () -> Unit
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
context.loadCustomBackground()
|
val systemIsDark = isSystemInDarkTheme()
|
||||||
context.loadThemeColors()
|
|
||||||
context.loadDynamicColorState()
|
|
||||||
|
|
||||||
val colorScheme = when {
|
// 检测系统主题变化并保存状态
|
||||||
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
val themeChanged = ThemeConfig.detectThemeChange(systemIsDark)
|
||||||
if (darkTheme) {
|
LaunchedEffect(systemIsDark, themeChanged) {
|
||||||
val originalScheme = dynamicDarkColorScheme(context)
|
if (ThemeConfig.forceDarkMode == null && themeChanged) {
|
||||||
originalScheme.copy(
|
Log.d("ThemeSystem", "系统主题变化检测: 从 ${!systemIsDark} 变为 $systemIsDark")
|
||||||
primary = adjustColor(originalScheme.primary),
|
ThemeConfig.resetBackgroundState()
|
||||||
onPrimary = adjustColor(originalScheme.onPrimary),
|
|
||||||
primaryContainer = adjustColor(originalScheme.primaryContainer),
|
if (!ThemeConfig.preventBackgroundRefresh) {
|
||||||
onPrimaryContainer = adjustColor(originalScheme.onPrimaryContainer),
|
context.loadCustomBackground()
|
||||||
background = Color.Transparent,
|
}
|
||||||
surface = Color.Transparent,
|
|
||||||
onBackground = Color.White,
|
CardConfig.apply {
|
||||||
onSurface = Color.White,
|
load(context)
|
||||||
onSecondary = Color.White,
|
if (!isCustomAlphaSet) {
|
||||||
onTertiary = Color.White,
|
cardAlpha = if (systemIsDark) 0.50f else 1f
|
||||||
onSecondaryContainer = Color.White,
|
}
|
||||||
onTertiaryContainer = Color.White
|
if (!isCustomDimSet) {
|
||||||
)
|
cardDim = if (systemIsDark) 0.5f else 0f
|
||||||
} else {
|
}
|
||||||
val originalScheme = dynamicLightColorScheme(context)
|
save(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
|
SystemBarStyle(
|
||||||
|
darkMode = darkTheme
|
||||||
|
)
|
||||||
|
|
||||||
|
// 初始加载配置
|
||||||
|
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) {
|
if (darkTheme && !dynamicColor) {
|
||||||
CardConfig.setDarkModeDefaults()
|
CardConfig.setDarkModeDefaults()
|
||||||
|
} else if (!darkTheme && !dynamicColor) {
|
||||||
|
CardConfig.setLightModeDefaults()
|
||||||
|
}
|
||||||
|
CardConfig.updateShadowEnabled(!isDarkModeWithCustomBackground)
|
||||||
|
|
||||||
|
val backgroundUri = rememberSaveable { mutableStateOf(ThemeConfig.customBackgroundUri) }
|
||||||
|
|
||||||
|
LaunchedEffect(ThemeConfig.customBackgroundUri) {
|
||||||
|
backgroundUri.value = ThemeConfig.customBackgroundUri
|
||||||
}
|
}
|
||||||
|
|
||||||
CardConfig.updateShadowEnabled(!isDarkModeWithCustomBackground)
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算适用的暗化值
|
||||||
|
val dimFactor = CardConfig.cardDim
|
||||||
|
|
||||||
MaterialTheme(
|
MaterialTheme(
|
||||||
colorScheme = colorScheme,
|
colorScheme = colorScheme,
|
||||||
typography = Typography
|
typography = Typography
|
||||||
) {
|
) {
|
||||||
Box(modifier = Modifier.fillMaxSize()) {
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
// 背景图层
|
Box(
|
||||||
ThemeConfig.customBackgroundUri?.let { uri ->
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.zIndex(-2f)
|
||||||
|
.background(if (darkTheme) MaterialTheme.colorScheme.surfaceContainerLow else MaterialTheme.colorScheme.surfaceContainerLow)
|
||||||
|
)
|
||||||
|
|
||||||
|
// 自定义背景层
|
||||||
|
backgroundUri.value?.let { uri ->
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.zIndex(-1f)
|
.zIndex(-1f)
|
||||||
|
.alpha(bgAlpha)
|
||||||
) {
|
) {
|
||||||
// 背景图片
|
// 背景图片
|
||||||
Box(
|
bgImagePainter?.let { painter ->
|
||||||
modifier = Modifier
|
Box(
|
||||||
.fillMaxSize()
|
modifier = Modifier
|
||||||
.paint(
|
.fillMaxSize()
|
||||||
painter = rememberAsyncImagePainter(
|
.paint(
|
||||||
model = uri,
|
painter = painter,
|
||||||
onError = {
|
contentScale = ContentScale.Crop
|
||||||
ThemeConfig.customBackgroundUri = null
|
)
|
||||||
context.saveCustomBackground(null)
|
.graphicsLayer {
|
||||||
}
|
alpha = (painter.state as? AsyncImagePainter.State.Success)?.let { 1f } ?: 0f
|
||||||
),
|
}
|
||||||
contentScale = ContentScale.Crop
|
)
|
||||||
)
|
}
|
||||||
)
|
|
||||||
|
|
||||||
// 亮度调节层
|
// 亮度调节层 (根据cardDim调整)
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.background(
|
.background(
|
||||||
if (darkTheme) {
|
if (darkTheme) Color.Black.copy(alpha = 0.6f + dimFactor * 0.3f)
|
||||||
Color.Black.copy(alpha = 0.4f)
|
else Color.White.copy(alpha = 0.1f + dimFactor * 0.2f)
|
||||||
} else {
|
|
||||||
Color.White.copy(alpha = 0.1f)
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
// 边缘渐变遮罩层
|
// 边缘渐变遮罩
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
@@ -197,11 +260,8 @@ fun KernelSUTheme(
|
|||||||
Brush.radialGradient(
|
Brush.radialGradient(
|
||||||
colors = listOf(
|
colors = listOf(
|
||||||
Color.Transparent,
|
Color.Transparent,
|
||||||
if (darkTheme) {
|
if (darkTheme) Color.Black.copy(alpha = 0.5f + dimFactor * 0.2f)
|
||||||
Color.Black.copy(alpha = 0.5f)
|
else Color.Black.copy(alpha = 0.2f + dimFactor * 0.1f)
|
||||||
} else {
|
|
||||||
Color.Black.copy(alpha = 0.2f)
|
|
||||||
}
|
|
||||||
),
|
),
|
||||||
radius = 1200f
|
radius = 1200f
|
||||||
)
|
)
|
||||||
@@ -209,7 +269,8 @@ fun KernelSUTheme(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 内容图层
|
|
||||||
|
// 内容层
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
@@ -221,30 +282,153 @@ fun KernelSUTheme(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 复制图片到应用内部存储
|
/**
|
||||||
|
* 创建动态深色颜色方案
|
||||||
|
*/
|
||||||
|
@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.primaryDark,
|
||||||
|
onPrimary = ThemeConfig.currentTheme.onPrimaryDark,
|
||||||
|
primaryContainer = ThemeConfig.currentTheme.primaryContainerDark,
|
||||||
|
onPrimaryContainer = ThemeConfig.currentTheme.onPrimaryContainerDark,
|
||||||
|
secondary = ThemeConfig.currentTheme.secondaryDark,
|
||||||
|
onSecondary = ThemeConfig.currentTheme.onSecondaryDark,
|
||||||
|
secondaryContainer = ThemeConfig.currentTheme.secondaryContainerDark,
|
||||||
|
onSecondaryContainer = ThemeConfig.currentTheme.onSecondaryContainerDark,
|
||||||
|
tertiary = ThemeConfig.currentTheme.tertiaryDark,
|
||||||
|
onTertiary = ThemeConfig.currentTheme.onTertiaryDark,
|
||||||
|
tertiaryContainer = ThemeConfig.currentTheme.tertiaryContainerDark,
|
||||||
|
onTertiaryContainer = ThemeConfig.currentTheme.onTertiaryContainerDark,
|
||||||
|
error = ThemeConfig.currentTheme.errorDark,
|
||||||
|
onError = ThemeConfig.currentTheme.onErrorDark,
|
||||||
|
errorContainer = ThemeConfig.currentTheme.errorContainerDark,
|
||||||
|
onErrorContainer = ThemeConfig.currentTheme.onErrorContainerDark,
|
||||||
|
background = Color.Transparent,
|
||||||
|
onBackground = ThemeConfig.currentTheme.onBackgroundDark,
|
||||||
|
surface = Color.Transparent,
|
||||||
|
onSurface = ThemeConfig.currentTheme.onSurfaceDark,
|
||||||
|
surfaceVariant = ThemeConfig.currentTheme.surfaceVariantDark,
|
||||||
|
onSurfaceVariant = ThemeConfig.currentTheme.onSurfaceVariantDark,
|
||||||
|
outline = ThemeConfig.currentTheme.outlineDark,
|
||||||
|
outlineVariant = ThemeConfig.currentTheme.outlineVariantDark,
|
||||||
|
scrim = ThemeConfig.currentTheme.scrimDark,
|
||||||
|
inverseSurface = ThemeConfig.currentTheme.inverseSurfaceDark,
|
||||||
|
inverseOnSurface = ThemeConfig.currentTheme.inverseOnSurfaceDark,
|
||||||
|
inversePrimary = ThemeConfig.currentTheme.inversePrimaryDark,
|
||||||
|
surfaceDim = ThemeConfig.currentTheme.surfaceDimDark,
|
||||||
|
surfaceBright = ThemeConfig.currentTheme.surfaceBrightDark,
|
||||||
|
surfaceContainerLowest = ThemeConfig.currentTheme.surfaceContainerLowestDark,
|
||||||
|
surfaceContainerLow = ThemeConfig.currentTheme.surfaceContainerLowDark,
|
||||||
|
surfaceContainer = ThemeConfig.currentTheme.surfaceContainerDark,
|
||||||
|
surfaceContainerHigh = ThemeConfig.currentTheme.surfaceContainerHighDark,
|
||||||
|
surfaceContainerHighest = ThemeConfig.currentTheme.surfaceContainerHighestDark,
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建浅色颜色方案
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
private fun createLightColorScheme() = lightColorScheme(
|
||||||
|
primary = ThemeConfig.currentTheme.primaryLight,
|
||||||
|
onPrimary = ThemeConfig.currentTheme.onPrimaryLight,
|
||||||
|
primaryContainer = ThemeConfig.currentTheme.primaryContainerLight,
|
||||||
|
onPrimaryContainer = ThemeConfig.currentTheme.onPrimaryContainerLight,
|
||||||
|
secondary = ThemeConfig.currentTheme.secondaryLight,
|
||||||
|
onSecondary = ThemeConfig.currentTheme.onSecondaryLight,
|
||||||
|
secondaryContainer = ThemeConfig.currentTheme.secondaryContainerLight,
|
||||||
|
onSecondaryContainer = ThemeConfig.currentTheme.onSecondaryContainerLight,
|
||||||
|
tertiary = ThemeConfig.currentTheme.tertiaryLight,
|
||||||
|
onTertiary = ThemeConfig.currentTheme.onTertiaryLight,
|
||||||
|
tertiaryContainer = ThemeConfig.currentTheme.tertiaryContainerLight,
|
||||||
|
onTertiaryContainer = ThemeConfig.currentTheme.onTertiaryContainerLight,
|
||||||
|
error = ThemeConfig.currentTheme.errorLight,
|
||||||
|
onError = ThemeConfig.currentTheme.onErrorLight,
|
||||||
|
errorContainer = ThemeConfig.currentTheme.errorContainerLight,
|
||||||
|
onErrorContainer = ThemeConfig.currentTheme.onErrorContainerLight,
|
||||||
|
background = Color.Transparent,
|
||||||
|
onBackground = ThemeConfig.currentTheme.onBackgroundLight,
|
||||||
|
surface = Color.Transparent,
|
||||||
|
onSurface = ThemeConfig.currentTheme.onSurfaceLight,
|
||||||
|
surfaceVariant = ThemeConfig.currentTheme.surfaceVariantLight,
|
||||||
|
onSurfaceVariant = ThemeConfig.currentTheme.onSurfaceVariantLight,
|
||||||
|
outline = ThemeConfig.currentTheme.outlineLight,
|
||||||
|
outlineVariant = ThemeConfig.currentTheme.outlineVariantLight,
|
||||||
|
scrim = ThemeConfig.currentTheme.scrimLight,
|
||||||
|
inverseSurface = ThemeConfig.currentTheme.inverseSurfaceLight,
|
||||||
|
inverseOnSurface = ThemeConfig.currentTheme.inverseOnSurfaceLight,
|
||||||
|
inversePrimary = ThemeConfig.currentTheme.inversePrimaryLight,
|
||||||
|
surfaceDim = ThemeConfig.currentTheme.surfaceDimLight,
|
||||||
|
surfaceBright = ThemeConfig.currentTheme.surfaceBrightLight,
|
||||||
|
surfaceContainerLowest = ThemeConfig.currentTheme.surfaceContainerLowestLight,
|
||||||
|
surfaceContainerLow = ThemeConfig.currentTheme.surfaceContainerLowLight,
|
||||||
|
surfaceContainer = ThemeConfig.currentTheme.surfaceContainerLight,
|
||||||
|
surfaceContainerHigh = ThemeConfig.currentTheme.surfaceContainerHighLight,
|
||||||
|
surfaceContainerHighest = ThemeConfig.currentTheme.surfaceContainerHighestLight,
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 复制图片到应用内部存储并提升持久性
|
||||||
|
*/
|
||||||
private fun Context.copyImageToInternalStorage(uri: Uri): Uri? {
|
private fun Context.copyImageToInternalStorage(uri: Uri): Uri? {
|
||||||
try {
|
return try {
|
||||||
val contentResolver: ContentResolver = contentResolver
|
val contentResolver: ContentResolver = contentResolver
|
||||||
val inputStream: InputStream = contentResolver.openInputStream(uri)!!
|
val inputStream: InputStream = contentResolver.openInputStream(uri) ?: return null
|
||||||
|
|
||||||
val fileName = "custom_background.jpg"
|
val fileName = "custom_background.jpg"
|
||||||
val file = File(filesDir, fileName)
|
val file = File(filesDir, fileName)
|
||||||
val outputStream = FileOutputStream(file)
|
|
||||||
|
val backupFile = File(filesDir, "${fileName}.backup")
|
||||||
|
val outputStream = FileOutputStream(backupFile)
|
||||||
val buffer = ByteArray(4 * 1024)
|
val buffer = ByteArray(4 * 1024)
|
||||||
var read: Int
|
var read: Int
|
||||||
|
|
||||||
while (inputStream.read(buffer).also { read = it } != -1) {
|
while (inputStream.read(buffer).also { read = it } != -1) {
|
||||||
outputStream.write(buffer, 0, read)
|
outputStream.write(buffer, 0, read)
|
||||||
}
|
}
|
||||||
|
|
||||||
outputStream.flush()
|
outputStream.flush()
|
||||||
outputStream.close()
|
outputStream.close()
|
||||||
inputStream.close()
|
inputStream.close()
|
||||||
return Uri.fromFile(file)
|
|
||||||
|
if (file.exists()) {
|
||||||
|
file.delete()
|
||||||
|
}
|
||||||
|
backupFile.renameTo(file)
|
||||||
|
|
||||||
|
Uri.fromFile(file)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("ImageCopy", "Failed to copy image: ${e.message}")
|
Log.e("ImageCopy", "复制图片失败: ${e.message}")
|
||||||
return null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 保存变换后的背景图片到应用内部存储并更新配置
|
/**
|
||||||
|
* 保存并应用自定义背景
|
||||||
|
*/
|
||||||
fun Context.saveAndApplyCustomBackground(uri: Uri, transformation: BackgroundTransformation? = null) {
|
fun Context.saveAndApplyCustomBackground(uri: Uri, transformation: BackgroundTransformation? = null) {
|
||||||
val finalUri = if (transformation != null) {
|
val finalUri = if (transformation != null) {
|
||||||
saveTransformedBackground(uri, transformation)
|
saveTransformedBackground(uri, transformation)
|
||||||
@@ -252,36 +436,73 @@ fun Context.saveAndApplyCustomBackground(uri: Uri, transformation: BackgroundTra
|
|||||||
copyImageToInternalStorage(uri)
|
copyImageToInternalStorage(uri)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 保存到配置文件
|
||||||
getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
||||||
.edit {
|
.edit {
|
||||||
putString("custom_background", finalUri?.toString())
|
putString("custom_background", finalUri?.toString())
|
||||||
|
// 设置阻止刷新标志为false,允许新设置的背景加载一次
|
||||||
|
putBoolean("prevent_background_refresh", false)
|
||||||
}
|
}
|
||||||
|
|
||||||
ThemeConfig.customBackgroundUri = finalUri
|
ThemeConfig.customBackgroundUri = finalUri
|
||||||
|
ThemeConfig.backgroundImageLoaded = false
|
||||||
|
ThemeConfig.preventBackgroundRefresh = false
|
||||||
CardConfig.cardElevation = 0.dp
|
CardConfig.cardElevation = 0.dp
|
||||||
CardConfig.isCustomBackgroundEnabled = true
|
CardConfig.isCustomBackgroundEnabled = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// 保存背景图片到应用内部存储并更新配置
|
/**
|
||||||
|
* 保存自定义背景
|
||||||
|
*/
|
||||||
fun Context.saveCustomBackground(uri: Uri?) {
|
fun Context.saveCustomBackground(uri: Uri?) {
|
||||||
val newUri = uri?.let { copyImageToInternalStorage(it) }
|
val newUri = uri?.let { copyImageToInternalStorage(it) }
|
||||||
|
|
||||||
|
// 保存到配置文件
|
||||||
getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
||||||
.edit {
|
.edit {
|
||||||
putString("custom_background", newUri?.toString())
|
putString("custom_background", newUri?.toString())
|
||||||
|
if (uri == null) {
|
||||||
|
// 如果清除背景,也重置阻止刷新标志
|
||||||
|
putBoolean("prevent_background_refresh", false)
|
||||||
|
} else {
|
||||||
|
// 设置阻止刷新标志为false,允许新设置的背景加载一次
|
||||||
|
putBoolean("prevent_background_refresh", false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ThemeConfig.customBackgroundUri = newUri
|
ThemeConfig.customBackgroundUri = newUri
|
||||||
|
ThemeConfig.backgroundImageLoaded = false
|
||||||
|
ThemeConfig.preventBackgroundRefresh = false
|
||||||
|
|
||||||
if (uri != null) {
|
if (uri != null) {
|
||||||
CardConfig.cardElevation = 0.dp
|
CardConfig.cardElevation = 0.dp
|
||||||
CardConfig.isCustomBackgroundEnabled = true
|
CardConfig.isCustomBackgroundEnabled = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载自定义背景
|
||||||
|
*/
|
||||||
fun Context.loadCustomBackground() {
|
fun Context.loadCustomBackground() {
|
||||||
val uriString = getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
val uriString = getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
||||||
.getString("custom_background", null)
|
.getString("custom_background", null)
|
||||||
ThemeConfig.customBackgroundUri = uriString?.toUri()
|
|
||||||
|
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?) {
|
fun Context.saveThemeMode(forceDark: Boolean?) {
|
||||||
getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
||||||
.edit {
|
.edit {
|
||||||
@@ -294,52 +515,49 @@ fun Context.saveThemeMode(forceDark: Boolean?) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
ThemeConfig.forceDarkMode = forceDark
|
ThemeConfig.forceDarkMode = forceDark
|
||||||
|
ThemeConfig.needsResetOnThemeChange = forceDark == null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载主题模式
|
||||||
|
*/
|
||||||
fun Context.loadThemeMode() {
|
fun Context.loadThemeMode() {
|
||||||
val mode = getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
val mode = getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
||||||
.getString("theme_mode", "system")
|
.getString("theme_mode", "system")
|
||||||
|
|
||||||
ThemeConfig.forceDarkMode = when(mode) {
|
ThemeConfig.forceDarkMode = when(mode) {
|
||||||
"dark" -> true
|
"dark" -> true
|
||||||
"light" -> false
|
"light" -> false
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
|
ThemeConfig.needsResetOnThemeChange = ThemeConfig.forceDarkMode == null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存主题颜色
|
||||||
|
*/
|
||||||
fun Context.saveThemeColors(themeName: String) {
|
fun Context.saveThemeColors(themeName: String) {
|
||||||
getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
||||||
.edit {
|
.edit {
|
||||||
putString("theme_colors", themeName)
|
putString("theme_colors", themeName)
|
||||||
}
|
}
|
||||||
|
|
||||||
ThemeConfig.currentTheme = when(themeName) {
|
ThemeConfig.currentTheme = ThemeColors.fromName(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() {
|
fun Context.loadThemeColors() {
|
||||||
val themeName = getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
val themeName = getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
||||||
.getString("theme_colors", "default")
|
.getString("theme_colors", "default")
|
||||||
|
|
||||||
ThemeConfig.currentTheme = when(themeName) {
|
ThemeConfig.currentTheme = ThemeColors.fromName(themeName ?: "default")
|
||||||
"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) {
|
fun Context.saveDynamicColorState(enabled: Boolean) {
|
||||||
getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
||||||
.edit {
|
.edit {
|
||||||
@@ -348,28 +566,41 @@ fun Context.saveDynamicColorState(enabled: Boolean) {
|
|||||||
ThemeConfig.useDynamicColor = enabled
|
ThemeConfig.useDynamicColor = enabled
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载动态颜色状态
|
||||||
|
*/
|
||||||
fun Context.loadDynamicColorState() {
|
fun Context.loadDynamicColorState() {
|
||||||
val enabled = getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
val enabled = getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
||||||
.getBoolean("use_dynamic_color", true)
|
.getBoolean("use_dynamic_color", true)
|
||||||
|
|
||||||
ThemeConfig.useDynamicColor = enabled
|
ThemeConfig.useDynamicColor = enabled
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun adjustColor(color: Color): Color {
|
@Composable
|
||||||
val minLuminance = 0.75f
|
private fun SystemBarStyle(
|
||||||
val maxLuminance = 1f
|
darkMode: Boolean,
|
||||||
var luminance = color.luminance()
|
statusBarScrim: Color = Color.Transparent,
|
||||||
if (luminance < minLuminance) {
|
navigationBarScrim: Color = Color.Transparent,
|
||||||
luminance = minLuminance
|
) {
|
||||||
} else if (luminance > maxLuminance) {
|
val context = LocalContext.current
|
||||||
luminance = maxLuminance
|
val activity = context as ComponentActivity
|
||||||
}
|
|
||||||
return color.copy(luminance)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun mixColors(color1: Color, color2: Color, ratio: Float): Color {
|
SideEffect {
|
||||||
val r = (color1.red * ratio + color2.red * (1 - ratio))
|
activity.enableEdgeToEdge(
|
||||||
val g = (color1.green * ratio + color2.green * (1 - ratio))
|
statusBarStyle = SystemBarStyle.auto(
|
||||||
val b = (color1.blue * ratio + color2.blue * (1 - ratio))
|
statusBarScrim.toArgb(),
|
||||||
val a = (color1.alpha * ratio + color2.alpha * (1 - ratio))
|
statusBarScrim.toArgb(),
|
||||||
return Color(r, g, b, a)
|
) { darkMode },
|
||||||
|
navigationBarStyle = when {
|
||||||
|
darkMode -> SystemBarStyle.dark(
|
||||||
|
navigationBarScrim.toArgb()
|
||||||
|
)
|
||||||
|
|
||||||
|
else -> SystemBarStyle.light(
|
||||||
|
navigationBarScrim.toArgb(),
|
||||||
|
navigationBarScrim.toArgb(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,33 +1,108 @@
|
|||||||
package com.sukisu.ultra.ui.theme
|
package com.sukisu.ultra.ui.theme
|
||||||
|
|
||||||
|
import androidx.compose.material3.Typography
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
|
||||||
// Set of Material typography styles to start with
|
val Typography = Typography(
|
||||||
val Typography = androidx.compose.material3.Typography(
|
// 大标题
|
||||||
bodyLarge = TextStyle(
|
displayLarge = TextStyle(
|
||||||
fontFamily = FontFamily.Default,
|
|
||||||
fontWeight = FontWeight.Normal,
|
fontWeight = FontWeight.Normal,
|
||||||
fontSize = 16.sp,
|
fontSize = 57.sp,
|
||||||
lineHeight = 24.sp,
|
lineHeight = 64.sp,
|
||||||
letterSpacing = 0.5.sp
|
letterSpacing = (-0.25).sp
|
||||||
)
|
),
|
||||||
/* Other default text styles to override
|
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(
|
titleLarge = TextStyle(
|
||||||
fontFamily = FontFamily.Default,
|
fontWeight = FontWeight.SemiBold,
|
||||||
fontWeight = FontWeight.Normal,
|
|
||||||
fontSize = 22.sp,
|
fontSize = 22.sp,
|
||||||
lineHeight = 28.sp,
|
lineHeight = 28.sp,
|
||||||
letterSpacing = 0.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(
|
labelSmall = TextStyle(
|
||||||
fontFamily = FontFamily.Default,
|
|
||||||
fontWeight = FontWeight.Medium,
|
fontWeight = FontWeight.Medium,
|
||||||
fontSize = 11.sp,
|
fontSize = 11.sp,
|
||||||
lineHeight = 16.sp,
|
lineHeight = 16.sp,
|
||||||
letterSpacing = 0.5.sp
|
letterSpacing = 0.5.sp
|
||||||
)
|
)
|
||||||
*/
|
|
||||||
)
|
)
|
||||||
@@ -63,10 +63,6 @@ fun Context.applyTransformationToBitmap(bitmap: Bitmap, transformation: Backgrou
|
|||||||
val safeScale = maxOf(0.1f, transformation.scale)
|
val safeScale = maxOf(0.1f, transformation.scale)
|
||||||
matrix.postScale(safeScale, safeScale)
|
matrix.postScale(safeScale, safeScale)
|
||||||
|
|
||||||
// 计算中心点
|
|
||||||
val centerX = targetWidth / 2f
|
|
||||||
val centerY = targetHeight / 2f
|
|
||||||
|
|
||||||
// 计算偏移量,确保不会出现负最大值的问题
|
// 计算偏移量,确保不会出现负最大值的问题
|
||||||
val widthDiff = (bitmap.width * safeScale - targetWidth)
|
val widthDiff = (bitmap.width * safeScale - targetWidth)
|
||||||
val heightDiff = (bitmap.height * safeScale - targetHeight)
|
val heightDiff = (bitmap.height * safeScale - targetHeight)
|
||||||
|
|||||||
@@ -7,13 +7,23 @@ import android.content.Context
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.IntentFilter
|
import android.content.IntentFilter
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
import android.util.Log
|
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 com.sukisu.ultra.ui.util.module.LatestVersionInfo
|
import com.sukisu.ultra.ui.util.module.LatestVersionInfo
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
|
import java.io.File
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
private const val TAG = "DownloadUtil"
|
||||||
|
private val CUSTOM_USER_AGENT = "SukiSU-Ultra/2.0 (Linux; Android ${Build.VERSION.RELEASE}; ${Build.MODEL})"
|
||||||
|
private const val MAX_RETRY_COUNT = 3
|
||||||
|
private const val RETRY_DELAY_MS = 3000L
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author weishu
|
* @author weishu
|
||||||
@@ -26,8 +36,10 @@ fun download(
|
|||||||
fileName: String,
|
fileName: String,
|
||||||
description: String,
|
description: String,
|
||||||
onDownloaded: (Uri) -> Unit = {},
|
onDownloaded: (Uri) -> Unit = {},
|
||||||
onDownloading: () -> Unit = {}
|
onDownloading: () -> Unit = {},
|
||||||
|
onError: (String) -> Unit = {}
|
||||||
) {
|
) {
|
||||||
|
Log.d(TAG, "Start Download: $url")
|
||||||
val downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
val downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
||||||
|
|
||||||
val query = DownloadManager.Query()
|
val query = DownloadManager.Query()
|
||||||
@@ -49,6 +61,13 @@ fun download(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
val downloadFile = File(
|
||||||
|
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
|
||||||
|
fileName
|
||||||
|
)
|
||||||
|
if (downloadFile.exists()) {
|
||||||
|
downloadFile.delete()
|
||||||
|
}
|
||||||
|
|
||||||
val request = DownloadManager.Request(url.toUri())
|
val request = DownloadManager.Request(url.toUri())
|
||||||
.setDestinationInExternalPublicDir(
|
.setDestinationInExternalPublicDir(
|
||||||
@@ -59,66 +78,206 @@ fun download(
|
|||||||
.setMimeType("application/zip")
|
.setMimeType("application/zip")
|
||||||
.setTitle(fileName)
|
.setTitle(fileName)
|
||||||
.setDescription(description)
|
.setDescription(description)
|
||||||
|
.addRequestHeader("User-Agent", CUSTOM_USER_AGENT)
|
||||||
|
.setAllowedOverMetered(true)
|
||||||
|
.setAllowedOverRoaming(true)
|
||||||
|
.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI or DownloadManager.Request.NETWORK_MOBILE)
|
||||||
|
|
||||||
downloadManager.enqueue(request)
|
try {
|
||||||
|
val downloadId = downloadManager.enqueue(request)
|
||||||
|
Log.d(TAG, "Successful launch of the download,ID: $downloadId")
|
||||||
|
monitorDownload(context, downloadManager, downloadId, url, fileName, description, onDownloaded, onDownloading, onError)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Download startup failure", e)
|
||||||
|
onError("Download startup failure: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun monitorDownload(
|
||||||
|
context: Context,
|
||||||
|
downloadManager: DownloadManager,
|
||||||
|
downloadId: Long,
|
||||||
|
url: String,
|
||||||
|
fileName: String,
|
||||||
|
description: String,
|
||||||
|
onDownloaded: (Uri) -> Unit,
|
||||||
|
onDownloading: () -> Unit,
|
||||||
|
onError: (String) -> Unit,
|
||||||
|
retryCount: Int = 0
|
||||||
|
) {
|
||||||
|
val handler = Handler(Looper.getMainLooper())
|
||||||
|
val query = DownloadManager.Query().setFilterById(downloadId)
|
||||||
|
|
||||||
|
var lastProgress = -1
|
||||||
|
var stuckCounter = 0
|
||||||
|
|
||||||
|
val runnable = object : Runnable {
|
||||||
|
override fun run() {
|
||||||
|
downloadManager.query(query).use { cursor ->
|
||||||
|
if (cursor != null && cursor.moveToFirst()) {
|
||||||
|
@SuppressLint("Range")
|
||||||
|
val status = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS))
|
||||||
|
|
||||||
|
when (status) {
|
||||||
|
DownloadManager.STATUS_SUCCESSFUL -> {
|
||||||
|
@SuppressLint("Range")
|
||||||
|
val localUri = cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI))
|
||||||
|
Log.d(TAG, "Download Successfully: $localUri")
|
||||||
|
onDownloaded(localUri.toUri())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
DownloadManager.STATUS_FAILED -> {
|
||||||
|
@SuppressLint("Range")
|
||||||
|
val reason = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_REASON))
|
||||||
|
Log.d(TAG, "Download failed with reason code: $reason")
|
||||||
|
|
||||||
|
if (retryCount < MAX_RETRY_COUNT) {
|
||||||
|
Log.d(TAG, "Attempts to re download, number of retries: ${retryCount + 1}")
|
||||||
|
handler.postDelayed({
|
||||||
|
downloadManager.remove(downloadId)
|
||||||
|
download(context, url, fileName, description, onDownloaded, onDownloading, onError)
|
||||||
|
}, RETRY_DELAY_MS)
|
||||||
|
} else {
|
||||||
|
onError("Download failed, please check network connection or storage space")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
DownloadManager.STATUS_RUNNING, DownloadManager.STATUS_PENDING, DownloadManager.STATUS_PAUSED -> {
|
||||||
|
@SuppressLint("Range")
|
||||||
|
val totalBytes = cursor.getLong(cursor.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES))
|
||||||
|
@SuppressLint("Range")
|
||||||
|
val downloadedBytes = cursor.getLong(cursor.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR))
|
||||||
|
|
||||||
|
if (totalBytes > 0) {
|
||||||
|
val progress = (downloadedBytes * 100 / totalBytes).toInt()
|
||||||
|
if (progress == lastProgress) {
|
||||||
|
stuckCounter++
|
||||||
|
if (stuckCounter > 30) {
|
||||||
|
if (retryCount < MAX_RETRY_COUNT) {
|
||||||
|
Log.d(TAG, "Download stalled and restarted")
|
||||||
|
downloadManager.remove(downloadId)
|
||||||
|
download(context, url, fileName, description, onDownloaded, onDownloading, onError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
lastProgress = progress
|
||||||
|
stuckCounter = 0
|
||||||
|
Log.d(TAG, "Download progress: $progress% ($downloadedBytes/$totalBytes)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
handler.postDelayed(this, 1000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
handler.post(runnable)
|
||||||
|
|
||||||
|
val receiver = object : BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context?, intent: Intent?) {
|
||||||
|
val id = intent?.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1) ?: -1
|
||||||
|
if (id == downloadId) {
|
||||||
|
handler.removeCallbacks(runnable)
|
||||||
|
|
||||||
|
val query = DownloadManager.Query().setFilterById(downloadId)
|
||||||
|
downloadManager.query(query).use { cursor ->
|
||||||
|
if (cursor != null && cursor.moveToFirst()) {
|
||||||
|
@SuppressLint("Range")
|
||||||
|
val status = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS))
|
||||||
|
|
||||||
|
if (status == DownloadManager.STATUS_SUCCESSFUL) {
|
||||||
|
@SuppressLint("Range")
|
||||||
|
val localUri = cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI))
|
||||||
|
onDownloaded(localUri.toUri())
|
||||||
|
} else {
|
||||||
|
if (retryCount < MAX_RETRY_COUNT) {
|
||||||
|
download(context!!, url, fileName, description, onDownloaded, onDownloading, onError)
|
||||||
|
} else {
|
||||||
|
onError("Download failed, please try again later")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
context?.unregisterReceiver(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ContextCompat.registerReceiver(
|
||||||
|
context,
|
||||||
|
receiver,
|
||||||
|
IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE),
|
||||||
|
ContextCompat.RECEIVER_EXPORTED
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun checkNewVersion(): LatestVersionInfo {
|
fun checkNewVersion(): LatestVersionInfo {
|
||||||
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 {
|
||||||
okhttp3.OkHttpClient().newCall(okhttp3.Request.Builder().url(url).build()).execute()
|
val client = okhttp3.OkHttpClient.Builder()
|
||||||
.use { response ->
|
.connectTimeout(15, TimeUnit.SECONDS)
|
||||||
if (!response.isSuccessful) {
|
.readTimeout(30, TimeUnit.SECONDS)
|
||||||
Log.d("CheckUpdate", "Network request failed: ${response.message}")
|
.writeTimeout(15, TimeUnit.SECONDS)
|
||||||
return defaultValue
|
.build()
|
||||||
}
|
|
||||||
val body = response.body?.string()
|
|
||||||
if (body == null) {
|
|
||||||
Log.d("CheckUpdate", "Response body is null")
|
|
||||||
return defaultValue
|
|
||||||
}
|
|
||||||
Log.d("CheckUpdate", "Response body: $body")
|
|
||||||
val json = org.json.JSONObject(body)
|
|
||||||
|
|
||||||
// 直接从 tag_name 提取版本号(如 v1.1)
|
val request = okhttp3.Request.Builder()
|
||||||
val tagName = json.optString("tag_name", "")
|
.url(url)
|
||||||
val versionName = tagName.removePrefix("v") // 移除前缀 "v"
|
.header("User-Agent", CUSTOM_USER_AGENT)
|
||||||
|
.build()
|
||||||
|
|
||||||
// 从 body 字段获取更新日志(保留换行符)
|
client.newCall(request).execute().use { response ->
|
||||||
val changelog = json.optString("body")
|
if (!response.isSuccessful) {
|
||||||
.replace("\\r\\n", "\n") // 转换换行符
|
Log.d("CheckUpdate", "Network request failed: ${response.message}")
|
||||||
|
return defaultValue
|
||||||
val assets = json.getJSONArray("assets")
|
|
||||||
for (i in 0 until assets.length()) {
|
|
||||||
val asset = assets.getJSONObject(i)
|
|
||||||
val name = asset.getString("name")
|
|
||||||
if (!name.endsWith(".apk")) continue
|
|
||||||
|
|
||||||
// 修改正则表达式,只匹配 SukiSU 和版本号
|
|
||||||
val regex = Regex("SukiSU.*_(\\d+)-release")
|
|
||||||
val matchResult = regex.find(name)
|
|
||||||
if (matchResult == null) {
|
|
||||||
Log.d("CheckUpdate", "No match found in $name, skipping")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
val versionCode = matchResult.groupValues[1].toInt()
|
|
||||||
|
|
||||||
val downloadUrl = asset.getString("browser_download_url")
|
|
||||||
return LatestVersionInfo(
|
|
||||||
versionCode,
|
|
||||||
downloadUrl,
|
|
||||||
changelog,
|
|
||||||
versionName
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Log.d("CheckUpdate", "No valid apk asset found, returning default value")
|
|
||||||
defaultValue
|
|
||||||
}
|
}
|
||||||
|
val body = response.body?.string()
|
||||||
|
if (body == null) {
|
||||||
|
Log.d("CheckUpdate", "Return data is null")
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
Log.d("CheckUpdate", "Return data: $body")
|
||||||
|
val json = org.json.JSONObject(body)
|
||||||
|
|
||||||
|
// 直接从 tag_name 提取版本号(如 v1.1)
|
||||||
|
val tagName = json.optString("tag_name", "")
|
||||||
|
val versionName = tagName.removePrefix("v") // 移除前缀 "v"
|
||||||
|
|
||||||
|
// 从 body 字段获取更新日志(保留换行符)
|
||||||
|
val changelog = json.optString("body")
|
||||||
|
.replace("\\r\\n", "\n") // 转换换行符
|
||||||
|
|
||||||
|
val assets = json.getJSONArray("assets")
|
||||||
|
for (i in 0 until assets.length()) {
|
||||||
|
val asset = assets.getJSONObject(i)
|
||||||
|
val name = asset.getString("name")
|
||||||
|
if (!name.endsWith(".apk")) continue
|
||||||
|
|
||||||
|
val regex = Regex("SukiSU.*_(\\d+)-release")
|
||||||
|
val matchResult = regex.find(name)
|
||||||
|
if (matchResult == null) {
|
||||||
|
Log.d("CheckUpdate", "No matches found: $name, skip over")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
val versionCode = matchResult.groupValues[1].toInt()
|
||||||
|
|
||||||
|
val downloadUrl = asset.getString("browser_download_url")
|
||||||
|
return LatestVersionInfo(
|
||||||
|
versionCode,
|
||||||
|
downloadUrl,
|
||||||
|
changelog,
|
||||||
|
versionName
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Log.d("CheckUpdate", "No valid APK resource found, return default value")
|
||||||
|
defaultValue
|
||||||
|
}
|
||||||
}.getOrDefault(defaultValue)
|
}.getOrDefault(defaultValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DownloadListener(context: Context, onDownloaded: (Uri) -> Unit) {
|
fun DownloadListener(context: Context, onDownloaded: (Uri) -> Unit) {
|
||||||
DisposableEffect(context) {
|
DisposableEffect(context) {
|
||||||
@@ -158,4 +317,3 @@ fun DownloadListener(context: Context, onDownloaded: (Uri) -> Unit) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -76,9 +76,9 @@ fun createRootShell(globalMnt: Boolean = false): Shell {
|
|||||||
Log.w(TAG, "ksu failed: ", e)
|
Log.w(TAG, "ksu failed: ", e)
|
||||||
try {
|
try {
|
||||||
if (globalMnt) {
|
if (globalMnt) {
|
||||||
builder.build("su")
|
|
||||||
} else {
|
|
||||||
builder.build("su", "-mm")
|
builder.build("su", "-mm")
|
||||||
|
} else {
|
||||||
|
builder.build("su")
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Log.e(TAG, "su failed: ", e)
|
Log.e(TAG, "su failed: ", e)
|
||||||
@@ -537,7 +537,7 @@ fun getKpmModuleInfo(name: String): String {
|
|||||||
|
|
||||||
fun controlKpmModule(name: String, args: String? = null): Int {
|
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 = runCmd(shell, cmd)
|
val result = runCmd(shell, cmd)
|
||||||
return result.trim().toIntOrNull() ?: -1
|
return result.trim().toIntOrNull() ?: -1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
package com.sukisu.ultra.ui.util
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import java.io.BufferedReader
|
||||||
|
import java.io.InputStreamReader
|
||||||
|
import java.nio.charset.StandardCharsets
|
||||||
|
import java.util.zip.ZipInputStream
|
||||||
|
import com.sukisu.ultra.R
|
||||||
|
import android.util.Log
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
object ModuleUtils {
|
||||||
|
private const val TAG = "ModuleUtils"
|
||||||
|
|
||||||
|
fun extractModuleName(context: Context, uri: Uri): String {
|
||||||
|
if (uri == Uri.EMPTY) {
|
||||||
|
Log.e(TAG, "The supplied URI is empty")
|
||||||
|
return context.getString(R.string.unknown_module)
|
||||||
|
}
|
||||||
|
|
||||||
|
return try {
|
||||||
|
Log.d(TAG, "Start extracting module names from URIs: $uri")
|
||||||
|
|
||||||
|
// 从URI路径中提取文件名
|
||||||
|
val fileName = uri.lastPathSegment?.let { path ->
|
||||||
|
val lastSlash = path.lastIndexOf('/')
|
||||||
|
if (lastSlash != -1 && lastSlash < path.length - 1) {
|
||||||
|
path.substring(lastSlash + 1)
|
||||||
|
} else {
|
||||||
|
path
|
||||||
|
}
|
||||||
|
}?.removeSuffix(".zip") ?: context.getString(R.string.unknown_module)
|
||||||
|
|
||||||
|
var formattedFileName = fileName.replace(Regex("[^a-zA-Z0-9\\s\\-_.@()\\u4e00-\\u9fa5]"), "").trim()
|
||||||
|
var moduleName = formattedFileName
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 打开ZIP文件输入流
|
||||||
|
val inputStream = context.contentResolver.openInputStream(uri)
|
||||||
|
if (inputStream == null) {
|
||||||
|
Log.e(TAG, "Unable to get input stream from URI: $uri")
|
||||||
|
return formattedFileName
|
||||||
|
}
|
||||||
|
|
||||||
|
val zipInputStream = ZipInputStream(inputStream)
|
||||||
|
var entry = zipInputStream.nextEntry
|
||||||
|
|
||||||
|
// 遍历ZIP文件中的条目,查找module.prop文件
|
||||||
|
while (entry != null) {
|
||||||
|
if (entry.name == "module.prop") {
|
||||||
|
val reader = BufferedReader(InputStreamReader(zipInputStream, StandardCharsets.UTF_8))
|
||||||
|
var line: String?
|
||||||
|
var nameFound = false
|
||||||
|
while (reader.readLine().also { line = it } != null) {
|
||||||
|
if (line?.startsWith("name=") == true) {
|
||||||
|
moduleName = line.substringAfter("=")
|
||||||
|
moduleName = moduleName.replace(Regex("[^a-zA-Z0-9\\s\\-_.@()\\u4e00-\\u9fa5]"), "").trim()
|
||||||
|
nameFound = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
entry = zipInputStream.nextEntry
|
||||||
|
}
|
||||||
|
zipInputStream.close()
|
||||||
|
Log.d(TAG, "Successfully extracted module name: $moduleName")
|
||||||
|
moduleName
|
||||||
|
} catch (e: IOException) {
|
||||||
|
Log.e(TAG, "Error reading ZIP file: ${e.message}")
|
||||||
|
formattedFileName
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Exception when extracting module name: ${e.message}")
|
||||||
|
context.getString(R.string.unknown_module)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证URI是否有效并可访问
|
||||||
|
fun isUriAccessible(context: Context, uri: Uri): Boolean {
|
||||||
|
if (uri == Uri.EMPTY) return false
|
||||||
|
|
||||||
|
return try {
|
||||||
|
val inputStream = context.contentResolver.openInputStream(uri)
|
||||||
|
inputStream?.close()
|
||||||
|
inputStream != null
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "The URI is inaccessible: $uri, Error: ${e.message}")
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取URI的持久权限
|
||||||
|
fun takePersistableUriPermission(context: Context, uri: Uri) {
|
||||||
|
try {
|
||||||
|
val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||||
|
context.contentResolver.takePersistableUriPermission(uri, flags)
|
||||||
|
Log.d(TAG, "Persistent permissions for URIs have been obtained: $uri")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to get persistent permissions on URIs: $uri, Error: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +1,10 @@
|
|||||||
package com.sukisu.ultra.ui.util
|
package com.sukisu.ultra.ui.util
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import android.content.Context
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
import com.sukisu.ultra.R
|
import com.sukisu.ultra.R
|
||||||
|
|
||||||
@Composable
|
fun getSELinuxStatus(context: Context): String {
|
||||||
fun getSELinuxStatus(): String {
|
|
||||||
val shell = Shell.Builder.create().build("sh")
|
val shell = Shell.Builder.create().build("sh")
|
||||||
val list = ArrayList<String>()
|
val list = ArrayList<String>()
|
||||||
|
|
||||||
@@ -18,16 +16,16 @@ fun getSELinuxStatus(): String {
|
|||||||
|
|
||||||
return if (result.isSuccess) {
|
return if (result.isSuccess) {
|
||||||
when (output) {
|
when (output) {
|
||||||
"Enforcing" -> stringResource(R.string.selinux_status_enforcing)
|
"Enforcing" -> context.getString(R.string.selinux_status_enforcing)
|
||||||
"Permissive" -> stringResource(R.string.selinux_status_permissive)
|
"Permissive" -> context.getString(R.string.selinux_status_permissive)
|
||||||
"Disabled" -> stringResource(R.string.selinux_status_disabled)
|
"Disabled" -> context.getString(R.string.selinux_status_disabled)
|
||||||
else -> stringResource(R.string.selinux_status_unknown)
|
else -> context.getString(R.string.selinux_status_unknown)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (output.contains("Permission denied")) {
|
if (output.contains("Permission denied")) {
|
||||||
stringResource(R.string.selinux_status_enforcing)
|
context.getString(R.string.selinux_status_enforcing)
|
||||||
} else {
|
} else {
|
||||||
stringResource(R.string.selinux_status_unknown)
|
context.getString(R.string.selinux_status_unknown)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,290 @@
|
|||||||
|
package com.sukisu.ultra.ui.viewmodel
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.SystemClock
|
||||||
|
import android.system.Os
|
||||||
|
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 com.dergoogler.mmrl.platform.Platform.Companion.context
|
||||||
|
import com.google.gson.Gson
|
||||||
|
import com.sukisu.ultra.KernelVersion
|
||||||
|
import com.sukisu.ultra.Natives
|
||||||
|
import com.sukisu.ultra.getKernelVersion
|
||||||
|
import com.sukisu.ultra.ksuApp
|
||||||
|
import com.sukisu.ultra.ui.util.*
|
||||||
|
import com.sukisu.ultra.ui.util.module.LatestVersionInfo
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import androidx.core.content.edit
|
||||||
|
|
||||||
|
class HomeViewModel : ViewModel() {
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "HomeViewModel"
|
||||||
|
private const val CACHE_DURATION = 5 * 60 * 1000L
|
||||||
|
private const val PREFS_NAME = "home_cache"
|
||||||
|
private const val KEY_SYSTEM_STATUS = "system_status"
|
||||||
|
private const val KEY_SYSTEM_INFO = "system_info"
|
||||||
|
private const val KEY_VERSION_INFO = "version_info"
|
||||||
|
private const val KEY_LAST_UPDATE = "last_update_time"
|
||||||
|
}
|
||||||
|
|
||||||
|
// 系统状态
|
||||||
|
data class SystemStatus(
|
||||||
|
val isManager: Boolean = false,
|
||||||
|
val ksuVersion: Int? = null,
|
||||||
|
val lkmMode: Boolean? = null,
|
||||||
|
val kernelVersion: KernelVersion = getKernelVersion(),
|
||||||
|
val isRootAvailable: Boolean = false,
|
||||||
|
val isKpmConfigured: Boolean = false,
|
||||||
|
val requireNewKernel: Boolean = false
|
||||||
|
)
|
||||||
|
|
||||||
|
// 系统信息
|
||||||
|
data class SystemInfo(
|
||||||
|
val kernelRelease: String = "",
|
||||||
|
val androidVersion: String = "",
|
||||||
|
val deviceModel: String = "",
|
||||||
|
val managerVersion: Pair<String, Long> = Pair("", 0L),
|
||||||
|
val seLinuxStatus: String = "",
|
||||||
|
val kpmVersion: String = "",
|
||||||
|
val suSFSStatus: String = "",
|
||||||
|
val suSFSVersion: String = "",
|
||||||
|
val suSFSVariant: String = "",
|
||||||
|
val suSFSFeatures: String = "",
|
||||||
|
val susSUMode: String = "",
|
||||||
|
val superuserCount: Int = 0,
|
||||||
|
val moduleCount: Int = 0,
|
||||||
|
val kpmModuleCount: Int = 0
|
||||||
|
)
|
||||||
|
|
||||||
|
private val gson = Gson()
|
||||||
|
private val prefs by lazy { ksuApp.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) }
|
||||||
|
|
||||||
|
var systemStatus by mutableStateOf(SystemStatus())
|
||||||
|
private set
|
||||||
|
|
||||||
|
var systemInfo by mutableStateOf(SystemInfo())
|
||||||
|
private set
|
||||||
|
|
||||||
|
var latestVersionInfo by mutableStateOf(LatestVersionInfo())
|
||||||
|
private set
|
||||||
|
|
||||||
|
var isSimpleMode by mutableStateOf(false)
|
||||||
|
private set
|
||||||
|
var isHideVersion by mutableStateOf(false)
|
||||||
|
private set
|
||||||
|
var isHideOtherInfo by mutableStateOf(false)
|
||||||
|
private set
|
||||||
|
var isHideSusfsStatus by mutableStateOf(false)
|
||||||
|
private set
|
||||||
|
var isHideLinkCard by mutableStateOf(false)
|
||||||
|
private set
|
||||||
|
var showKpmInfo by mutableStateOf(true)
|
||||||
|
private set
|
||||||
|
|
||||||
|
fun loadUserSettings(context: Context) {
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||||
|
isSimpleMode = prefs.getBoolean("is_simple_mode", false)
|
||||||
|
isHideVersion = prefs.getBoolean("is_hide_version", false)
|
||||||
|
isHideOtherInfo = prefs.getBoolean("is_hide_other_info", false)
|
||||||
|
isHideSusfsStatus = prefs.getBoolean("is_hide_susfs_status", false)
|
||||||
|
isHideLinkCard = prefs.getBoolean("is_hide_link_card", false)
|
||||||
|
showKpmInfo = prefs.getBoolean("show_kpm_info", true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun initializeData() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
val currentTime = System.currentTimeMillis()
|
||||||
|
val lastUpdateTime = prefs.getLong(KEY_LAST_UPDATE, 0)
|
||||||
|
val shouldRefresh = currentTime - lastUpdateTime > CACHE_DURATION
|
||||||
|
|
||||||
|
if (!shouldRefresh) {
|
||||||
|
loadCachedData()
|
||||||
|
} else {
|
||||||
|
fetchAndSaveData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadCachedData() {
|
||||||
|
prefs.getString(KEY_SYSTEM_STATUS, null)?.let {
|
||||||
|
systemStatus = gson.fromJson(it, SystemStatus::class.java)
|
||||||
|
}
|
||||||
|
prefs.getString(KEY_SYSTEM_INFO, null)?.let {
|
||||||
|
systemInfo = gson.fromJson(it, SystemInfo::class.java)
|
||||||
|
}
|
||||||
|
prefs.getString(KEY_VERSION_INFO, null)?.let {
|
||||||
|
latestVersionInfo = gson.fromJson(it, LatestVersionInfo::class.java)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun fetchAndSaveData() {
|
||||||
|
fetchSystemStatus()
|
||||||
|
fetchSystemInfo()
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
prefs.edit {
|
||||||
|
putString(KEY_SYSTEM_STATUS, gson.toJson(systemStatus))
|
||||||
|
putString(KEY_SYSTEM_INFO, gson.toJson(systemInfo))
|
||||||
|
putString(KEY_VERSION_INFO, gson.toJson(latestVersionInfo))
|
||||||
|
putLong(KEY_LAST_UPDATE, System.currentTimeMillis())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun checkForUpdates(context: Context) {
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
val checkUpdate = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||||
|
.getBoolean("check_update", true)
|
||||||
|
|
||||||
|
if (checkUpdate) {
|
||||||
|
val currentTime = System.currentTimeMillis()
|
||||||
|
val lastUpdateTime = prefs.getLong(KEY_LAST_UPDATE, 0)
|
||||||
|
val shouldRefresh = currentTime - lastUpdateTime > CACHE_DURATION
|
||||||
|
|
||||||
|
if (shouldRefresh) {
|
||||||
|
val start = SystemClock.elapsedRealtime()
|
||||||
|
val newVersionInfo = checkNewVersion()
|
||||||
|
latestVersionInfo = newVersionInfo
|
||||||
|
prefs.edit { putString(KEY_VERSION_INFO, gson.toJson(newVersionInfo)) }
|
||||||
|
Log.i(TAG, "Update check completed in ${SystemClock.elapsedRealtime() - start}ms")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Error checking for updates", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun refreshAllData(context: Context) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
fetchAndSaveData()
|
||||||
|
checkForUpdates(context)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Error refreshing data", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun fetchSystemStatus() {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
val kernelVersion = getKernelVersion()
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
systemStatus = SystemStatus(
|
||||||
|
isManager = isManager,
|
||||||
|
ksuVersion = ksuVersion,
|
||||||
|
lkmMode = lkmMode,
|
||||||
|
kernelVersion = kernelVersion,
|
||||||
|
isRootAvailable = rootAvailable(),
|
||||||
|
isKpmConfigured = Natives.isKPMEnabled(),
|
||||||
|
requireNewKernel = isManager && Natives.requireNewKernel()
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Error fetching system status", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("RestrictedApi")
|
||||||
|
private suspend fun fetchSystemInfo() {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
val uname = Os.uname()
|
||||||
|
val kpmVersion = getKpmVersion()
|
||||||
|
val suSFS = getSuSFS()
|
||||||
|
var suSFSVersion = ""
|
||||||
|
var suSFSVariant = ""
|
||||||
|
var suSFSFeatures = ""
|
||||||
|
var susSUMode = ""
|
||||||
|
|
||||||
|
if (suSFS == "Supported") {
|
||||||
|
suSFSVersion = getSuSFSVersion()
|
||||||
|
if (suSFSVersion.isNotEmpty()) {
|
||||||
|
suSFSVariant = getSuSFSVariant()
|
||||||
|
suSFSFeatures = getSuSFSFeatures()
|
||||||
|
val isSUS_SU = suSFSFeatures == "CONFIG_KSU_SUSFS_SUS_SU"
|
||||||
|
if (isSUS_SU) {
|
||||||
|
susSUMode = try {
|
||||||
|
susfsSUS_SU_Mode().toString()
|
||||||
|
} catch (_: Exception) {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
systemInfo = SystemInfo(
|
||||||
|
kernelRelease = uname.release,
|
||||||
|
androidVersion = Build.VERSION.RELEASE,
|
||||||
|
deviceModel = getDeviceModel(),
|
||||||
|
managerVersion = getManagerVersion(ksuApp.applicationContext),
|
||||||
|
seLinuxStatus = getSELinuxStatus(context),
|
||||||
|
kpmVersion = kpmVersion,
|
||||||
|
suSFSStatus = suSFS,
|
||||||
|
suSFSVersion = suSFSVersion,
|
||||||
|
suSFSVariant = suSFSVariant,
|
||||||
|
suSFSFeatures = suSFSFeatures,
|
||||||
|
susSUMode = susSUMode,
|
||||||
|
superuserCount = getSuperuserCount(),
|
||||||
|
moduleCount = getModuleCount(),
|
||||||
|
kpmModuleCount = getKpmModuleCount()
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Error fetching system info", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("PrivateApi")
|
||||||
|
private fun getDeviceModel(): 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
|
||||||
|
)
|
||||||
|
var result = Build.DEVICE
|
||||||
|
for (key in marketNameKeys) {
|
||||||
|
val marketName = getMethod.invoke(null, key, "") as String
|
||||||
|
if (marketName.isNotEmpty()) {
|
||||||
|
result = marketName
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Error getting device model", e)
|
||||||
|
Build.DEVICE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getManagerVersion(context: Context): Pair<String, Long> {
|
||||||
|
return try {
|
||||||
|
val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0)!!
|
||||||
|
val versionCode = androidx.core.content.pm.PackageInfoCompat.getLongVersionCode(packageInfo)
|
||||||
|
Pair(packageInfo.versionName!!, versionCode)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Error getting manager version", e)
|
||||||
|
Pair("", 0L)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,10 @@ import kotlinx.coroutines.launch
|
|||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import com.sukisu.ultra.ui.util.*
|
import com.sukisu.ultra.ui.util.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author ShirkNeko
|
||||||
|
* @date 2025/5/31.
|
||||||
|
*/
|
||||||
class KpmViewModel : ViewModel() {
|
class KpmViewModel : ViewModel() {
|
||||||
var moduleList by mutableStateOf(emptyList<ModuleInfo>())
|
var moduleList by mutableStateOf(emptyList<ModuleInfo>())
|
||||||
private set
|
private set
|
||||||
|
|||||||
@@ -8,20 +8,29 @@ import androidx.compose.runtime.mutableStateOf
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.dergoogler.mmrl.platform.model.ModuleConfig
|
||||||
|
import com.dergoogler.mmrl.platform.model.ModuleConfig.Companion.asModuleConfig
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import com.sukisu.ultra.ui.util.HanziToPinyin
|
import com.sukisu.ultra.ui.util.HanziToPinyin
|
||||||
import com.sukisu.ultra.ui.util.listModules
|
import com.sukisu.ultra.ui.util.listModules
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import org.json.JSONArray
|
import org.json.JSONArray
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
import java.text.Collator
|
import java.text.Collator
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author ShirkNeko
|
||||||
|
* @date 2025/5/31.
|
||||||
|
*/
|
||||||
class ModuleViewModel : ViewModel() {
|
class ModuleViewModel : ViewModel() {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "ModuleViewModel"
|
private const val TAG = "ModuleViewModel"
|
||||||
private var modules by mutableStateOf<List<ModuleInfo>>(emptyList())
|
private var modules by mutableStateOf<List<ModuleInfo>>(emptyList())
|
||||||
|
private const val CUSTOM_USER_AGENT = "SukiSU-Ultra/2.0"
|
||||||
}
|
}
|
||||||
|
|
||||||
class ModuleInfo(
|
class ModuleInfo(
|
||||||
@@ -38,6 +47,7 @@ class ModuleViewModel : ViewModel() {
|
|||||||
val hasWebUi: Boolean,
|
val hasWebUi: Boolean,
|
||||||
val hasActionScript: Boolean,
|
val hasActionScript: Boolean,
|
||||||
val dirId: String, // real module id (dir name)
|
val dirId: String, // real module id (dir name)
|
||||||
|
var config: ModuleConfig? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
var isRefreshing by mutableStateOf(false)
|
var isRefreshing by mutableStateOf(false)
|
||||||
@@ -98,9 +108,42 @@ class ModuleViewModel : ViewModel() {
|
|||||||
obj.optString("updateJson"),
|
obj.optString("updateJson"),
|
||||||
obj.optBoolean("web"),
|
obj.optBoolean("web"),
|
||||||
obj.optBoolean("action"),
|
obj.optBoolean("action"),
|
||||||
obj.getString("dir_id"),
|
obj.getString("dir_id")
|
||||||
)
|
)
|
||||||
}.toList()
|
}.toList()
|
||||||
|
launch {
|
||||||
|
modules.forEach { module ->
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
runCatching {
|
||||||
|
module.config = module.id.asModuleConfig
|
||||||
|
}.onFailure { e ->
|
||||||
|
Log.e(TAG, "Failed to load config from id for module ${module.id}", e)
|
||||||
|
}
|
||||||
|
if (module.config == null) {
|
||||||
|
runCatching {
|
||||||
|
module.config = module.name.asModuleConfig
|
||||||
|
}.onFailure { e ->
|
||||||
|
Log.e(TAG, "Failed to load config from name for module ${module.id}", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (module.config == null) {
|
||||||
|
runCatching {
|
||||||
|
module.config = module.description.asModuleConfig
|
||||||
|
}.onFailure { e ->
|
||||||
|
Log.e(TAG, "Failed to load config from description for module ${module.id}", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (module.config == null) {
|
||||||
|
module.config = ModuleConfig()
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Failed to load any config for module ${module.id}", e)
|
||||||
|
module.config = ModuleConfig()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
isNeedRefresh = false
|
isNeedRefresh = false
|
||||||
}.onFailure { e ->
|
}.onFailure { e ->
|
||||||
Log.e(TAG, "fetchModuleList: ", e)
|
Log.e(TAG, "fetchModuleList: ", e)
|
||||||
@@ -117,6 +160,10 @@ class ModuleViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun sanitizeVersionString(version: String): String {
|
||||||
|
return version.replace(Regex("[^a-zA-Z0-9.\\-_]"), "_")
|
||||||
|
}
|
||||||
|
|
||||||
fun checkUpdate(m: ModuleInfo): Triple<String, String, String> {
|
fun checkUpdate(m: ModuleInfo): Triple<String, String, String> {
|
||||||
val empty = Triple("", "", "")
|
val empty = Triple("", "", "")
|
||||||
if (m.updateJson.isEmpty() || m.remove || m.update || !m.enabled) {
|
if (m.updateJson.isEmpty() || m.remove || m.update || !m.enabled) {
|
||||||
@@ -126,19 +173,32 @@ class ModuleViewModel : ViewModel() {
|
|||||||
val result = kotlin.runCatching {
|
val result = kotlin.runCatching {
|
||||||
val url = m.updateJson
|
val url = m.updateJson
|
||||||
Log.i(TAG, "checkUpdate url: $url")
|
Log.i(TAG, "checkUpdate url: $url")
|
||||||
val response = okhttp3.OkHttpClient()
|
|
||||||
.newCall(
|
val client = okhttp3.OkHttpClient.Builder()
|
||||||
okhttp3.Request.Builder()
|
.connectTimeout(15, TimeUnit.SECONDS)
|
||||||
.url(url)
|
.readTimeout(30, TimeUnit.SECONDS)
|
||||||
.build()
|
.writeTimeout(15, TimeUnit.SECONDS)
|
||||||
).execute()
|
.build()
|
||||||
|
|
||||||
|
val request = okhttp3.Request.Builder()
|
||||||
|
.url(url)
|
||||||
|
.header("User-Agent", CUSTOM_USER_AGENT)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val response = client.newCall(request).execute()
|
||||||
|
|
||||||
Log.d(TAG, "checkUpdate code: ${response.code}")
|
Log.d(TAG, "checkUpdate code: ${response.code}")
|
||||||
if (response.isSuccessful) {
|
if (response.isSuccessful) {
|
||||||
response.body?.string() ?: ""
|
response.body?.string() ?: ""
|
||||||
} else {
|
} else {
|
||||||
|
Log.d(TAG, "checkUpdate failed: ${response.message}")
|
||||||
""
|
""
|
||||||
}
|
}
|
||||||
}.getOrDefault("")
|
}.getOrElse { e ->
|
||||||
|
Log.e(TAG, "checkUpdate exception", e)
|
||||||
|
""
|
||||||
|
}
|
||||||
|
|
||||||
Log.i(TAG, "checkUpdate result: $result")
|
Log.i(TAG, "checkUpdate result: $result")
|
||||||
|
|
||||||
if (result.isEmpty()) {
|
if (result.isEmpty()) {
|
||||||
@@ -149,7 +209,8 @@ class ModuleViewModel : ViewModel() {
|
|||||||
JSONObject(result)
|
JSONObject(result)
|
||||||
}.getOrNull() ?: return empty
|
}.getOrNull() ?: return empty
|
||||||
|
|
||||||
val version = updateJson.optString("version", "")
|
var version = updateJson.optString("version", "")
|
||||||
|
version = sanitizeVersionString(version)
|
||||||
val versionCode = updateJson.optInt("versionCode", 0)
|
val versionCode = updateJson.optInt("versionCode", 0)
|
||||||
val zipUrl = updateJson.optString("zipUrl", "")
|
val zipUrl = updateJson.optString("zipUrl", "")
|
||||||
val changelog = updateJson.optString("changelog", "")
|
val changelog = updateJson.optString("changelog", "")
|
||||||
|
|||||||
@@ -1,38 +1,40 @@
|
|||||||
package com.sukisu.ultra.ui.viewmodel
|
package com.sukisu.ultra.ui.viewmodel
|
||||||
|
|
||||||
import android.content.ComponentName
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.ServiceConnection
|
|
||||||
import android.content.pm.ApplicationInfo
|
import android.content.pm.ApplicationInfo
|
||||||
import android.content.pm.PackageInfo
|
import android.content.pm.PackageInfo
|
||||||
import android.os.IBinder
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import android.os.SystemClock
|
import android.os.SystemClock
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
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 com.sukisu.zako.IKsuInterface
|
|
||||||
import com.sukisu.ultra.Natives
|
import com.sukisu.ultra.Natives
|
||||||
import com.sukisu.ultra.ksuApp
|
import com.sukisu.ultra.ksuApp
|
||||||
import com.sukisu.ultra.ui.KsuService
|
|
||||||
import com.sukisu.ultra.ui.util.HanziToPinyin
|
import com.sukisu.ultra.ui.util.HanziToPinyin
|
||||||
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 com.dergoogler.mmrl.platform.Platform
|
||||||
import kotlin.coroutines.suspendCoroutine
|
import com.dergoogler.mmrl.platform.TIMEOUT_MILLIS
|
||||||
|
import com.sukisu.ultra.ui.webui.getInstalledPackagesAll
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.withTimeoutOrNull
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author ShirkNeko
|
||||||
|
* @date 2025/5/31.
|
||||||
|
*/
|
||||||
class SuperUserViewModel : ViewModel() {
|
class SuperUserViewModel : ViewModel() {
|
||||||
|
val isPlatformAlive get() = Platform.isAlive
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "SuperUserViewModel"
|
private const val TAG = "SuperUserViewModel"
|
||||||
private var apps by mutableStateOf<List<AppInfo>>(emptyList())
|
var apps by mutableStateOf<List<AppInfo>>(emptyList())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
@@ -68,9 +70,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> {
|
||||||
@@ -142,55 +144,60 @@ class SuperUserViewModel : ViewModel() {
|
|||||||
fetchAppList() // 刷新列表以显示最新状态
|
fetchAppList() // 刷新列表以显示最新状态
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun connectKsuService(
|
// 批量更新权限和umount模块设置
|
||||||
onDisconnect: () -> Unit = {}
|
suspend fun updateBatchPermissions(allowSu: Boolean, umountModules: Boolean? = null) {
|
||||||
): Pair<IBinder, ServiceConnection> = suspendCoroutine { continuation ->
|
selectedApps.forEach { packageName ->
|
||||||
val connection = object : ServiceConnection {
|
val app = apps.find { it.packageName == packageName }
|
||||||
override fun onServiceDisconnected(name: ComponentName?) {
|
app?.let {
|
||||||
onDisconnect()
|
val profile = Natives.getAppProfile(packageName, it.uid)
|
||||||
}
|
val updatedProfile = profile.copy(
|
||||||
|
allowSu = allowSu,
|
||||||
override fun onServiceConnected(name: ComponentName?, binder: IBinder?) {
|
umountModules = umountModules ?: profile.umountModules,
|
||||||
continuation.resume(binder as IBinder to this)
|
nonRootUseDefault = false
|
||||||
|
)
|
||||||
|
if (Natives.setAppProfile(updatedProfile)) {
|
||||||
|
apps = apps.map { app ->
|
||||||
|
if (app.packageName == packageName) {
|
||||||
|
app.copy(profile = updatedProfile)
|
||||||
|
} else {
|
||||||
|
app
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
clearSelection()
|
||||||
val intent = Intent(ksuApp, KsuService::class.java)
|
showBatchActions = false // 批量操作完成后退出批量模式
|
||||||
|
fetchAppList() // 刷新列表以显示最新状态
|
||||||
val task = KsuService.bindOrTask(
|
|
||||||
intent,
|
|
||||||
Shell.EXECUTOR,
|
|
||||||
connection,
|
|
||||||
)
|
|
||||||
val shell = KsuCli.SHELL
|
|
||||||
task?.let { it1 -> shell.execTask(it1) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun stopKsuService() {
|
// 更新本地应用配置
|
||||||
val intent = Intent(ksuApp, KsuService::class.java)
|
fun updateAppProfileLocally(packageName: String, updatedProfile: Natives.Profile) {
|
||||||
KsuService.stop(intent)
|
apps = apps.map { app ->
|
||||||
|
if (app.packageName == packageName) {
|
||||||
|
app.copy(profile = updatedProfile)
|
||||||
|
} else {
|
||||||
|
app
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun fetchAppList() {
|
suspend fun fetchAppList() {
|
||||||
isRefreshing = true
|
isRefreshing = true
|
||||||
|
|
||||||
val result = connectKsuService {
|
|
||||||
Log.w(TAG, "KsuService disconnected")
|
|
||||||
}
|
|
||||||
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
|
withTimeoutOrNull(TIMEOUT_MILLIS) {
|
||||||
|
while (!isPlatformAlive) {
|
||||||
|
delay(500)
|
||||||
|
}
|
||||||
|
} ?: return@withContext // Exit early if timeout
|
||||||
val pm = ksuApp.packageManager
|
val pm = ksuApp.packageManager
|
||||||
val start = SystemClock.elapsedRealtime()
|
val start = SystemClock.elapsedRealtime()
|
||||||
|
|
||||||
val binder = result.first
|
val packages = Platform.getInstalledPackagesAll {
|
||||||
val allPackages = IKsuInterface.Stub.asInterface(binder).getPackages(0)
|
Log.e(TAG, "getInstalledPackagesAll:", it)
|
||||||
|
Toast.makeText(ksuApp, "Something went wrong, check logs", Toast.LENGTH_SHORT).show()
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
stopKsuService()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val packages = allPackages.list
|
|
||||||
|
|
||||||
apps = packages.map {
|
apps = packages.map {
|
||||||
val appInfo = it.applicationInfo
|
val appInfo = it.applicationInfo
|
||||||
val uid = appInfo!!.uid
|
val uid = appInfo!!.uid
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
package com.sukisu.ultra.ui.webui
|
||||||
|
|
||||||
|
import android.content.ServiceConnection
|
||||||
|
import android.util.Log
|
||||||
|
import android.content.pm.PackageInfo
|
||||||
|
import com.dergoogler.mmrl.platform.Platform
|
||||||
|
import com.dergoogler.mmrl.platform.model.IProvider
|
||||||
|
import com.dergoogler.mmrl.platform.model.PlatformIntent
|
||||||
|
import com.sukisu.ultra.ksuApp
|
||||||
|
import com.sukisu.ultra.Natives
|
||||||
|
import com.topjohnwu.superuser.ipc.RootService
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
class KsuLibSuProvider : IProvider {
|
||||||
|
override val name = "KsuLibSu"
|
||||||
|
|
||||||
|
override fun isAvailable() = true
|
||||||
|
|
||||||
|
override suspend fun isAuthorized() = Natives.becomeManager(ksuApp.packageName)
|
||||||
|
|
||||||
|
private val serviceIntent
|
||||||
|
get() = PlatformIntent(
|
||||||
|
ksuApp,
|
||||||
|
Platform.KsuNext,
|
||||||
|
SuService::class.java
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun bind(connection: ServiceConnection) {
|
||||||
|
RootService.bind(serviceIntent.intent, connection)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun unbind(connection: ServiceConnection) {
|
||||||
|
RootService.stop(serviceIntent.intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// webui x
|
||||||
|
suspend fun initPlatform() = withContext(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
val active = Platform.init {
|
||||||
|
this.context = ksuApp
|
||||||
|
this.platform = Platform.KsuNext
|
||||||
|
this.provider = from(KsuLibSuProvider())
|
||||||
|
}
|
||||||
|
|
||||||
|
while (!active) {
|
||||||
|
delay(1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
return@withContext active
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("KsuLibSu", "Failed to initialize platform", e)
|
||||||
|
return@withContext false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Platform.Companion.getInstalledPackagesAll(catch: (Exception) -> Unit = {}): List<PackageInfo> =
|
||||||
|
try {
|
||||||
|
val packages = mutableListOf<PackageInfo>()
|
||||||
|
val userInfos = userManager.getUsers()
|
||||||
|
|
||||||
|
for (userInfo in userInfos) {
|
||||||
|
packages.addAll(packageManager.getInstalledPackages(0, userInfo.id))
|
||||||
|
}
|
||||||
|
|
||||||
|
packages
|
||||||
|
} catch (e: Exception) {
|
||||||
|
catch(e)
|
||||||
|
packageManager.getInstalledPackages(0, userManager.myUserId)
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package com.sukisu.ultra.ui.webui
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.IBinder
|
||||||
|
import com.dergoogler.mmrl.platform.model.PlatformIntent.Companion.getPlatform
|
||||||
|
import com.dergoogler.mmrl.platform.service.ServiceManager
|
||||||
|
import com.topjohnwu.superuser.ipc.RootService
|
||||||
|
|
||||||
|
class SuService : RootService() {
|
||||||
|
override fun onBind(intent: Intent): IBinder {
|
||||||
|
val mode = intent.getPlatform()
|
||||||
|
return ServiceManager(mode)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,9 +15,11 @@ import androidx.core.view.ViewCompat
|
|||||||
import androidx.core.view.WindowInsetsCompat
|
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.dergoogler.mmrl.platform.model.ModId
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
import com.sukisu.ultra.ui.util.createRootShell
|
import com.sukisu.ultra.ui.util.createRootShell
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import com.dergoogler.mmrl.webui.interfaces.WXOptions
|
||||||
|
|
||||||
@SuppressLint("SetJavaScriptEnabled")
|
@SuppressLint("SetJavaScriptEnabled")
|
||||||
class WebUIActivity : ComponentActivity() {
|
class WebUIActivity : ComponentActivity() {
|
||||||
@@ -41,7 +43,8 @@ class WebUIActivity : ComponentActivity() {
|
|||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
setTaskDescription(ActivityManager.TaskDescription("KernelSU - $name"))
|
setTaskDescription(ActivityManager.TaskDescription("KernelSU - $name"))
|
||||||
} else {
|
} else {
|
||||||
val taskDescription = ActivityManager.TaskDescription.Builder().setLabel("KernelSU - $name").build()
|
val taskDescription =
|
||||||
|
ActivityManager.TaskDescription.Builder().setLabel("KernelSU - $name").build()
|
||||||
setTaskDescription(taskDescription)
|
setTaskDescription(taskDescription)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,7 +85,7 @@ class WebUIActivity : ComponentActivity() {
|
|||||||
settings.javaScriptEnabled = true
|
settings.javaScriptEnabled = true
|
||||||
settings.domStorageEnabled = true
|
settings.domStorageEnabled = true
|
||||||
settings.allowFileAccess = false
|
settings.allowFileAccess = false
|
||||||
webviewInterface = WebViewInterface(this@WebUIActivity, this, moduleDir)
|
webviewInterface = WebViewInterface(WXOptions(this@WebUIActivity, this, ModId(moduleId)))
|
||||||
addJavascriptInterface(webviewInterface, "ksu")
|
addJavascriptInterface(webviewInterface, "ksu")
|
||||||
setWebViewClient(webViewClient)
|
setWebViewClient(webViewClient)
|
||||||
loadUrl("https://mui.kernelsu.org/index.html")
|
loadUrl("https://mui.kernelsu.org/index.html")
|
||||||
|
|||||||
@@ -0,0 +1,112 @@
|
|||||||
|
package com.sukisu.ultra.ui.webui
|
||||||
|
|
||||||
|
import android.app.ActivityManager
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.webkit.WebView
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.activity.compose.setContent
|
||||||
|
import androidx.activity.enableEdgeToEdge
|
||||||
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import com.dergoogler.mmrl.platform.Platform
|
||||||
|
import com.dergoogler.mmrl.platform.model.ModId
|
||||||
|
import com.dergoogler.mmrl.ui.component.Loading
|
||||||
|
import com.dergoogler.mmrl.webui.screen.WebUIScreen
|
||||||
|
import com.dergoogler.mmrl.webui.util.rememberWebUIOptions
|
||||||
|
import com.sukisu.ultra.BuildConfig
|
||||||
|
import com.sukisu.ultra.ui.theme.KernelSUTheme
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
class WebUIXActivity : ComponentActivity() {
|
||||||
|
private lateinit var webView: WebView
|
||||||
|
|
||||||
|
private val userAgent
|
||||||
|
get(): String {
|
||||||
|
val ksuVersion = BuildConfig.VERSION_CODE
|
||||||
|
|
||||||
|
val platform = Platform.get("Unknown") {
|
||||||
|
platform.name
|
||||||
|
}
|
||||||
|
|
||||||
|
val platformVersion = Platform.get(-1) {
|
||||||
|
moduleManager.versionCode
|
||||||
|
}
|
||||||
|
|
||||||
|
val osVersion = Build.VERSION.RELEASE
|
||||||
|
val deviceModel = Build.MODEL
|
||||||
|
|
||||||
|
return "SukiSU /$ksuVersion (Linux; Android $osVersion; $deviceModel; $platform/$platformVersion)"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
enableEdgeToEdge()
|
||||||
|
|
||||||
|
webView = WebView(this)
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
initPlatform()
|
||||||
|
}
|
||||||
|
|
||||||
|
val moduleId = intent.getStringExtra("id")!!
|
||||||
|
val name = intent.getStringExtra("name")!!
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
setTaskDescription(ActivityManager.TaskDescription("KernelSU - $name"))
|
||||||
|
} else {
|
||||||
|
val taskDescription =
|
||||||
|
ActivityManager.TaskDescription.Builder().setLabel("KernelSU - $name").build()
|
||||||
|
setTaskDescription(taskDescription)
|
||||||
|
}
|
||||||
|
|
||||||
|
val prefs = getSharedPreferences("settings", MODE_PRIVATE)
|
||||||
|
|
||||||
|
setContent {
|
||||||
|
KernelSUTheme {
|
||||||
|
var isLoading by remember { mutableStateOf(true) }
|
||||||
|
|
||||||
|
LaunchedEffect(Platform.isAlive) {
|
||||||
|
while (!Platform.isAlive) {
|
||||||
|
delay(1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoading = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
Loading()
|
||||||
|
return@KernelSUTheme
|
||||||
|
}
|
||||||
|
|
||||||
|
val webDebugging = prefs.getBoolean("enable_web_debugging", false)
|
||||||
|
val erudaInject = prefs.getBoolean("use_webuix_eruda", false)
|
||||||
|
val dark = isSystemInDarkTheme()
|
||||||
|
|
||||||
|
val options = rememberWebUIOptions(
|
||||||
|
modId = ModId(moduleId),
|
||||||
|
debug = webDebugging,
|
||||||
|
appVersionCode = BuildConfig.VERSION_CODE,
|
||||||
|
isDarkMode = dark,
|
||||||
|
enableEruda = erudaInject,
|
||||||
|
cls = WebUIXActivity::class.java,
|
||||||
|
userAgentString = userAgent
|
||||||
|
)
|
||||||
|
|
||||||
|
WebUIScreen(
|
||||||
|
webView = webView,
|
||||||
|
options = options,
|
||||||
|
interfaces = listOf(
|
||||||
|
WebViewInterface.factory()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,16 +1,17 @@
|
|||||||
package com.sukisu.ultra.ui.webui
|
package com.sukisu.ultra.ui.webui
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Context
|
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
import android.view.Window
|
import android.view.Window
|
||||||
import android.webkit.JavascriptInterface
|
import android.webkit.JavascriptInterface
|
||||||
import android.webkit.WebView
|
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.core.view.WindowInsetsCompat
|
import androidx.core.view.WindowInsetsCompat
|
||||||
import androidx.core.view.WindowInsetsControllerCompat
|
import androidx.core.view.WindowInsetsControllerCompat
|
||||||
|
import com.dergoogler.mmrl.webui.interfaces.WXInterface
|
||||||
|
import com.dergoogler.mmrl.webui.interfaces.WXOptions
|
||||||
|
import com.dergoogler.mmrl.webui.model.JavaScriptInterface
|
||||||
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
|
||||||
@@ -19,16 +20,20 @@ import com.sukisu.ultra.ui.util.listModules
|
|||||||
import com.sukisu.ultra.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.*
|
||||||
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
|
||||||
|
|
||||||
class WebViewInterface(
|
class WebViewInterface(
|
||||||
val context: Context,
|
wxOptions: WXOptions,
|
||||||
private val webView: WebView,
|
) : WXInterface(wxOptions) {
|
||||||
private val modDir: String
|
override var name: String = "ksu"
|
||||||
) {
|
|
||||||
|
companion object {
|
||||||
|
fun factory() = JavaScriptInterface(WebViewInterface::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val modDir get() = "/data/adb/modules/${modId.id}"
|
||||||
|
|
||||||
@JavascriptInterface
|
@JavascriptInterface
|
||||||
fun exec(cmd: String): String {
|
fun exec(cmd: String): String {
|
||||||
@@ -170,9 +175,9 @@ class WebViewInterface(
|
|||||||
if (context is Activity) {
|
if (context is Activity) {
|
||||||
Handler(Looper.getMainLooper()).post {
|
Handler(Looper.getMainLooper()).post {
|
||||||
if (enable) {
|
if (enable) {
|
||||||
hideSystemUI(context.window)
|
hideSystemUI(activity.window)
|
||||||
} else {
|
} else {
|
||||||
showSystemUI(context.window)
|
showSystemUI(activity.window)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -181,7 +186,7 @@ class WebViewInterface(
|
|||||||
@JavascriptInterface
|
@JavascriptInterface
|
||||||
fun moduleInfo(): String {
|
fun moduleInfo(): String {
|
||||||
val moduleInfos = JSONArray(listModules())
|
val moduleInfos = JSONArray(listModules())
|
||||||
var currentModuleInfo = JSONObject()
|
val currentModuleInfo = JSONObject()
|
||||||
currentModuleInfo.put("moduleDir", modDir)
|
currentModuleInfo.put("moduleDir", modDir)
|
||||||
val moduleId = File(modDir).getName()
|
val moduleId = File(modDir).getName()
|
||||||
for (i in 0 until moduleInfos.length()) {
|
for (i in 0 until moduleInfos.length()) {
|
||||||
@@ -191,7 +196,7 @@ class WebViewInterface(
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
var keys = currentInfo.keys()
|
val keys = currentInfo.keys()
|
||||||
for (key in keys) {
|
for (key in keys) {
|
||||||
currentModuleInfo.put(key, currentInfo.get(key))
|
currentModuleInfo.put(key, currentInfo.get(key))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,21 +2,772 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:width="108dp"
|
android:width="108dp"
|
||||||
android:height="108dp"
|
android:height="108dp"
|
||||||
android:viewportWidth="108"
|
android:viewportWidth="512"
|
||||||
android:viewportHeight="108">
|
android:viewportHeight="512">
|
||||||
|
<group android:scaleX="0.45"
|
||||||
|
android:scaleY="0.45"
|
||||||
|
android:translateX="140.8"
|
||||||
|
android:translateY="140.8">
|
||||||
|
|
||||||
<group
|
<path
|
||||||
android:scaleX="0.135"
|
android:fillColor="#fff9f6"
|
||||||
android:scaleY="0.135">
|
android:strokeColor="#4c4f59"
|
||||||
<path
|
android:strokeWidth="6.5"
|
||||||
android:pathData="M 259 259 H 541 V 541 H 259 V 259 Z"
|
android:strokeMiterLimit="1.3"
|
||||||
android:strokeWidth="18"
|
android:strokeLineCap="round"
|
||||||
android:strokeColor="#1e110d" />
|
android:pathData="M164.239,424.641 L133.168,450.694 C133.168,450.694,127.975,468.87,159.134,479.627 C190.293,490.384,372.425,481.111,372.425,481.111 C372.425,481.111,424.728,492.981,453.29,444.017 C452.919,444.759,444.387,463.306,444.387,463.306 L445.871,482.595 C445.871,482.595,503.606,473.181,476.327,333.64 C480.524,324.197,484.72,318.427,484.72,318.427 L481.572,296.657 C481.572,296.657,487.23,256.322,489.455,251.128 C491.681,245.935,499.099,240,499.099,240 C499.099,240,496.317,219.598,492.422,213.849 C491.68,210.511,499.284,178.795,494.462,155.797 C487.414,143.927,489.64,143.927,475.544,130.202 C475.544,130.573,484.447,85.3181,474.802,56.7556 C471.834,56.7556,442.53,45.9983,377.986,80.4958 C377.615,81.6086,351.649,50.4496,224.416,64.5453 C223.674,64.5453,181.016,26.7093,157.647,24.1127 C156.534,23.7418,135.02,20.0323,132.423,73.0769 C132.423,74.1897,88.281,89.0274,76.4109,137.992 C64.5408,186.956,65.2827,218.857,65.2827,218.857 C65.2827,218.857,46.3778,246.52,55.4846,243.89 C60.8314,242.346,60.4605,267.45,60.4605,267.45 L58.2349,306.77 C58.2349,306.77,43.7682,342.009,43.3972,366.492 C43.0263,390.974,42.6553,400.619,48.2194,415.085 C53.7835,429.552,68.9921,461.824,90.8776,478.145 C99.7802,478.516,109.796,480,109.796,480 L113.505,459.227 C113.505,459.227,132.423,475.177,145.777,474.065 C159.131,472.952,145.035,473.323,145.035,473.323 L131.31,455.518 Z" />
|
||||||
<path
|
<path
|
||||||
android:fillColor="#1e110d"
|
android:fillColor="#93d4fa"
|
||||||
android:pathData="M 257 257 H 407 V 407 H 257 V 257 Z" />
|
android:strokeColor="#4c4f59"
|
||||||
<path
|
android:strokeWidth="6.9"
|
||||||
android:fillColor="#1e110d"
|
android:strokeLineJoin="round"
|
||||||
android:pathData="M 393 393 H 543 V 543 H 393 V 393 Z" />
|
android:strokeMiterLimit="1.3"
|
||||||
</group>
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M133.91,110.17 L64.1717,157.65 L82.4357,188.404 L50.8182,244.079 L79.3807,261.142 L94.2186,259.658 L96.4452,204.017 L99.023,199.626 L117.217,177.681 L140.957,127.974 Z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#ace0fe"
|
||||||
|
android:strokeColor="#4c4f59"
|
||||||
|
android:strokeWidth="6.54357"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeMiterLimit="1.3"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M217.014,405.828 C217.014,405.828,224.431,379.883,258.18,378.755 C291.929,377.627,311.955,382.891,311.955,382.891 C311.955,382.891,323.081,389.283,329.756,397.555 C336.432,405.827,358.683,438.164,371.293,446.436 C383.902,454.708,386.498,461.852,386.498,461.852 L384.644,473.132 C384.644,473.132,369.809,483.284,353.862,484.788 C337.915,486.292,206.258,484.788,206.258,484.788 L176.218,481.78" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#ffffff"
|
||||||
|
android:strokeWidth="1.2"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeMiterLimit="1.3"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M363.541,444.682 C363.541,444.682,356.647,448.748,358.68,451.488 C360.713,454.228,364.602,454.935,364.602,454.935 C364.602,454.935,369.198,455.2,369.729,451.399 C370.259,447.598,366.724,444.593,366.724,444.593" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#ffffff"
|
||||||
|
android:strokeWidth="1.2"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeMiterLimit="1.3"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M349.677,459.826 C349.677,459.826,354.177,452.576,356.677,454.326 C359.177,456.076,352.427,461.576,352.427,461.576" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#ffffff"
|
||||||
|
android:strokeWidth="8.7"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeMiterLimit="1.3"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M228.97,465.109 A8.0341015,5.6995392,0,0,1,220.936,470.809 A8.0341015,5.6995392,0,0,1,212.902,465.109 A8.0341015,5.6995392,0,0,1,220.936,459.409 A8.0341015,5.6995392,0,0,1,228.97,465.109 Z" />
|
||||||
|
<path
|
||||||
|
android:strokeColor="#6c9cb2"
|
||||||
|
android:strokeWidth="3"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeMiterLimit="1.3"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M220.852,408.918 C220.625,409.655,221.622,410.397,221.054,409.289 C220.495,408.199,221.513,410.375,221.635,410.685 C223.736,416.022,223.599,421.847,225.242,427.328 C225.544,428.335,225.83,429.367,225.836,430.427" />
|
||||||
|
<path
|
||||||
|
android:strokeColor="#6c9cb2"
|
||||||
|
android:strokeWidth="6.9"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeMiterLimit="1.3"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M225.574,436.984 C224.352,441.357,225.651,446.212,227.992,449.836 C230.334,453.462,228.851,458.145,230.558,461.902 C232.274,465.678,232.769,470.295,230.257,474.062 C228.277,477.032,225.018,479.198,223.87,482.677 C223.87,482.677,223.796,483.338,223.796,483.338" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#fbf3ef"
|
||||||
|
android:strokeWidth="5.3"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeMiterLimit="1.3"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M259.148,381.902 C259.148,381.902,268.066,397.377,268.328,402.886 C268.59,408.394,300.59,401.05,300.59,401.05 C300.59,401.05,296.131,384.263,293.246,381.64 C290.361,379.017,259.148,381.902,259.148,381.902 Z" />
|
||||||
|
<path
|
||||||
|
android:strokeColor="#6c9cb2"
|
||||||
|
android:strokeWidth="4.6"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeMiterLimit="1.3"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M247.869,386.361 C248.493,387.123,249.181,387.683,248.222,386.467 C247.237,385.218,247.587,385.904,248.316,386.709 C249.718,388.257,250.625,390.115,251.419,392.051 C252.907,395.677,253.575,399.582,255.44,403.076 C256.689,405.416,258.189,407.614,259.411,409.968" />
|
||||||
|
<path
|
||||||
|
android:strokeColor="#6c9cb2"
|
||||||
|
android:strokeWidth="4.6"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeMiterLimit="1.3"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M306.623,386.885 C308.253,392.964,310.665,398.821,313.443,404.459" />
|
||||||
|
<path
|
||||||
|
android:strokeColor="#6c9cb2"
|
||||||
|
android:strokeWidth="5.3"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeMiterLimit="1.3"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M335.738,431.213 C336.508,434.977,338.987,438.481,341.225,442.105 C343.323,445.501,344.561,449.237,346.104,452.901 C348.174,457.813,352.855,461.643,353.803,466.855 C354.723,471.917,359.045,475.752,359.082,481.05" />
|
||||||
|
<path
|
||||||
|
android:strokeColor="#494d55"
|
||||||
|
android:strokeWidth="5.3"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeMiterLimit="1.3"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M258.623,384.262 C258.623,384.262,266.754,391.082,269.902,411.278" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#fbf3ef"
|
||||||
|
android:strokeColor="#4c4f59"
|
||||||
|
android:strokeWidth="6.9"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeMiterLimit="1.3"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M127.233,329.025 C127.233,329.025,124.636,389.488,140.958,404.326 C149.119,396.165,150.973,393.94,150.973,393.94 L163.214,402.472 L168.407,398.021 L170.633,378.361 L175.826,368.346 L181.761,356.105 L173.229,347.573 L157.279,328.284 L153.941,323.462 L149.49,332.736 C149.49,332.736,137.249,332.736,133.54,330.139 C129.831,327.542,127.234,329.026,127.234,329.026 Z" />
|
||||||
|
<path
|
||||||
|
android:strokeColor="#494d55"
|
||||||
|
android:strokeWidth="5.3"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeMiterLimit="1.3"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M291.672,381.377 C291.672,381.377,298.754,387.934,301.902,407.869" />
|
||||||
|
<path
|
||||||
|
android:strokeColor="#494d55"
|
||||||
|
android:strokeWidth="5.3"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeMiterLimit="1.3"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M270.164,404.197 C270.164,404.197,286.164,400.263,297.967,401.574" />
|
||||||
|
<path
|
||||||
|
android:strokeColor="#494d55"
|
||||||
|
android:strokeWidth="5.3"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeMiterLimit="1.3"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M253.902,417.836 C253.902,417.836,291.148,404.721,323.672,408.656 C325.77,412.328,329.442,425.443,317.902,429.64 C313.181,429.64,298.754,428.066,295.345,429.115 C291.935,430.164,282.755,429.64,276.984,433.574 C271.214,437.508,262.295,442.492,258.623,436.722 C254.951,430.952,253.902,424.394,253.902,424.394 Z" />
|
||||||
|
<path
|
||||||
|
android:strokeColor="#494d55"
|
||||||
|
android:strokeWidth="5.3"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeMiterLimit="1.3"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M276.459,482.623 C276.459,482.623,266.754,481.049,277.508,436.197" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#fcf6fa"
|
||||||
|
android:strokeColor="#494d55"
|
||||||
|
android:strokeWidth="5.3"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeMiterLimit="1.3"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M287.213,432.787 L293.508,483.672 L307.41,482.885 L300.59,428.852 Z" />
|
||||||
|
<path
|
||||||
|
android:strokeColor="#494d55"
|
||||||
|
android:strokeWidth="5.3"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeMiterLimit="1.3"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M287.738,430.951 L293.508,481.836" />
|
||||||
|
<path
|
||||||
|
android:strokeColor="#494d55"
|
||||||
|
android:strokeWidth="5.3"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeMiterLimit="1.3"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M300.328,428.066 C300.328,428.066,302.426,465.837,307.672,481.05" />
|
||||||
|
<path
|
||||||
|
android:strokeColor="#494d55"
|
||||||
|
android:strokeWidth="5.3"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeMiterLimit="1.3"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M313.18,431.213 C313.18,431.213,320.787,458.754,323.41,467.147 C326.033,475.54,315.279,482.885,315.279,482.885" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#bce4fd"
|
||||||
|
android:strokeWidth="8.7"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeMiterLimit="1.3"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M164.239,424.641 L133.168,450.694 L138.176,464.419 L159.134,479.628 L176.219,481.782 L165.09,461.23 L163.544,438.446 Z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#ffffff"
|
||||||
|
android:strokeWidth="8.7"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeMiterLimit="1.3"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M163.818,451.101 A8.0341015,5.6995392,0,0,1,155.784,456.801 A8.0341015,5.6995392,0,0,1,147.75,451.101 A8.0341015,5.6995392,0,0,1,155.784,445.401 A8.0341015,5.6995392,0,0,1,163.818,451.101 Z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#fff9f6"
|
||||||
|
android:strokeWidth="8.7"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeMiterLimit="1.3"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M338.361,376.131 C338.361,376.131,340.197,407.082,332.066,427.541 C334.951,428.59,346.547,427.807,358.979,420.199 C371.411,412.591,370.361,398.164,370.361,398.164 L372.459,427.016 C372.459,427.016,396.778,398.917,396.684,374.409 C396.59,349.901,395.016,348.327,395.016,348.327 L383.182,347.2 Z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#fde9e7"
|
||||||
|
android:strokeWidth="2.845"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeMiterLimit="1.3"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M339.782,389.859 C339.782,389.859,377.989,382.069,396.165,363.893 C401.729,372.425,404.326,396.536,404.326,396.536 L406.923,409.519 L423.244,346.83 L416.567,341.637 L410.632,324.203 C410.632,324.203,395.423,339.412,383.182,347.201 C370.941,354.991,349.055,366.49,349.055,366.49 L338.298,370.199 Z" />
|
||||||
|
<path
|
||||||
|
android:strokeColor="#c3b4b0"
|
||||||
|
android:strokeWidth="2.845"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeMiterLimit="1.3"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M347.016,369.643 C347.016,369.643,349.242,379.658,345.161,389.488 C341.081,399.318,340.153,398.947,340.153,398.947" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#fde9e7"
|
||||||
|
android:strokeWidth="6.945"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeMiterLimit="1.3"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M380.852,480 C380.852,480,425.442,462.689,440.131,392.918 C446.951,390.295,474.229,378.229,479.475,367.213 C482.098,357.246,475.803,335.213,475.803,335.213 L451.147,335.738 L424.393,340.459 L419.672,352 L411.279,404.459 L398.164,432.787 L382.426,468.984 Z" />
|
||||||
|
<path
|
||||||
|
android:strokeColor="#4c4f59"
|
||||||
|
android:strokeWidth="8.74203"
|
||||||
|
android:strokeMiterLimit="1.3"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M147.656,277.034 C147.656,277.034,136.529,381.17,275.24,380.421 C413.951,379.672,425.078,280.031,425.078,280.031" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#fff9f6"
|
||||||
|
android:strokeWidth="6.5"
|
||||||
|
android:strokeMiterLimit="1.3"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M123.279,147.934 L428.066,169.442 L403.41,129.049 C403.41,129.049,447.476,70.2949,454.295,68.7211 C461.115,67.1473,470.033,58.7539,470.033,58.7539 L456.918,55.0818 L379.803,77.1146 C379.803,77.1146,257.049,43.5408,211.41,70.8195 C165.771,98.0982,141.639,116.983,141.639,116.983 Z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#fde9e7"
|
||||||
|
android:strokeWidth="6.5"
|
||||||
|
android:strokeMiterLimit="1.3"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M331.445,66.6518 L347.674,97.4945 L395.504,128.862 L401.81,129.94 L428.693,94.6046 L396.59,72.3936 L377.705,77.6395 L347.279,66.0985 Z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#ace0fe"
|
||||||
|
android:strokeWidth="14.5"
|
||||||
|
android:strokeMiterLimit="1.3"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M431.034,175.826 L448.097,158.763 L498.545,230.726 L492.61,248.531 L459.225,267.078 L446.613,250.757 L464.418,239.629 Z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#ace0fe"
|
||||||
|
android:strokeColor="#4c4f59"
|
||||||
|
android:strokeWidth="6.945"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeMiterLimit="1.3"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M465.902,261.885 C465.902,261.885,501.512,316.413,468.87,335.331 C443.646,339.411,420.648,337.557,420.648,337.557 L431.034,311.22 L452.549,269.304 Z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#febdc7"
|
||||||
|
android:strokeWidth="4.6"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeMiterLimit="1.3"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M471.869,57.7049 C471.869,57.7049,488.918,126.689,460.853,168.131 C453.509,168.393,451.673,157.639,451.673,157.639 L453.771,151.082 L452.722,141.902 L455.083,131.148 L456.919,125.64 L450.624,119.607 L444.067,114.361 L441.182,109.115 L444.067,103.869 L441.444,98.6231 L437.247,95.7379 L434.099,94.1641 L439.345,82.6231 L451.148,75.2788 Z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#ff1c1c"
|
||||||
|
android:fillAlpha="0.0509804"
|
||||||
|
android:strokeWidth="3.92397"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeMiterLimit="1.3"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M244.267,326.814 A35.587568,21.480417,0,0,1,208.679,348.294 A35.587568,21.480417,0,0,1,173.091,326.814 A35.587568,21.480417,0,0,1,208.679,305.334 A35.587568,21.480417,0,0,1,244.267,326.814 Z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#ff1c1c"
|
||||||
|
android:fillAlpha="0.05044398"
|
||||||
|
android:strokeWidth="3.65169"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeMiterLimit="1.3"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M355.204,336.752 A31.611719,20.942554,0,0,1,323.592,357.695 A31.611719,20.942554,0,0,1,291.98,336.752 A31.611719,20.942554,0,0,1,323.592,315.809 A31.611719,20.942554,0,0,1,355.204,336.752 Z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#fbf3ef"
|
||||||
|
android:strokeColor="#4c4f59"
|
||||||
|
android:strokeWidth="6.5"
|
||||||
|
android:strokeMiterLimit="1.3"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M188.328,343.607 C188.328,343.607,200.394,315.279,204.066,349.902 C209.312,340.984,214.033,332.591,221.377,341.509 C228.197,343.607,236.59,348.853,226.098,363.542 C215.606,378.231,242.436,345.171,241.543,361.909 C240.822,375.421,217.021,408.72,217.021,408.72 C217.021,408.72,210.683,472.905,183.606,478.951 C147.058,487.112,171.54,375.082,171.54,375.082 Z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#fbe7e5"
|
||||||
|
android:fillAlpha="0.40581462"
|
||||||
|
android:strokeWidth="10.4"
|
||||||
|
android:strokeMiterLimit="1.3"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M193.631,383.924 A12.612,12.241061,0,0,1,181.019,396.165 A12.612,12.241061,0,0,1,168.407,383.924 A12.612,12.241061,0,0,1,181.019,371.683 A12.612,12.241061,0,0,1,193.631,383.924 Z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#fee4e0"
|
||||||
|
android:fillAlpha="0.42411327"
|
||||||
|
android:strokeWidth="10.4"
|
||||||
|
android:strokeMiterLimit="1.3"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M179.097,356.526 L193.193,335.753 L200.241,337.237 L200.983,350.962 L212.482,339.463 L221.756,343.543 L230.288,349.478 L224.724,364.316 L238.449,359.494 L235.111,377.299 L187.26,349.849 Z" />
|
||||||
|
<path
|
||||||
|
android:strokeColor="#4c4f59"
|
||||||
|
android:strokeWidth="6.5"
|
||||||
|
android:strokeMiterLimit="1.3"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M188.328,343.607 C188.328,343.607,200.394,315.279,204.066,349.902 C209.312,340.984,214.033,332.591,221.377,341.509 C228.197,343.607,236.59,348.853,226.098,363.542 C215.606,378.231,242.436,345.171,241.543,361.909 C240.822,375.421,217.021,408.72,217.021,408.72 C217.021,408.72,210.683,472.905,183.606,478.951 C147.058,487.112,171.54,375.082,171.54,375.082 Z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#fff9f6"
|
||||||
|
android:strokeColor="#4c4f59"
|
||||||
|
android:strokeWidth="6.5"
|
||||||
|
android:strokeMiterLimit="1.3"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M137.443,123.279 C137.443,123.279,186.782,74.6292,224.42,64.5441 C224.525,65.3118,104.253,-57.6139,137.443,123.279 Z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#fde9e7"
|
||||||
|
android:strokeWidth="1.18016"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeMiterLimit="1.3"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M198.852,42 L135.218,81.0166 L139.059,121.956 L184.828,87 L224,64.5 Z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#1a1a1a"
|
||||||
|
android:fillAlpha="0.465732"
|
||||||
|
android:strokeColor="#a18f90"
|
||||||
|
android:strokeWidth="6.5"
|
||||||
|
android:strokeMiterLimit="1.3"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M404.964,131.773 C405.489,131.004,406,130.224,406.539,129.465 C409.363,125.492,412.359,121.645,415.217,117.696 C417.61,114.388,419.157,112.153,421.508,108.796 C426.834,101.216,432.688,94.0352,438.717,87.009 C441.739,83.4584,444.784,79.9101,448.194,76.7199 C450.156,74.8844,452.413,73.0471,454.468,71.3278 C459.576,66.9703,465.137,63.2182,470.69,59.4657 C470.69,59.4657,467.765,57.6015,467.765,57.6015 L467.765,57.6015 C462.349,61.5016,456.862,65.3066,451.773,69.6416 C449.774,71.287,447.329,73.2528,445.41,75.0108 C441.958,78.1737,438.921,81.7424,435.922,85.3274 C429.967,92.4153,424.289,99.7266,418.922,107.272 C413.451,115.001,407.959,122.727,401.809,129.941 Z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#1a1a1a"
|
||||||
|
android:fillAlpha="0.465732"
|
||||||
|
android:strokeColor="#a18f90"
|
||||||
|
android:strokeWidth="6.5"
|
||||||
|
android:strokeMiterLimit="1.3"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M431.705,96.8851 C432.388,96.8683,433.075,96.8931,433.759,96.9175 C435.025,96.9602,436.284,97.0933,437.535,97.2918 C438.884,97.5112,440.212,97.8397,441.533,98.1893 C442.837,98.5317,444.12,98.9495,445.392,99.3955 C445.74,99.5276,446.281,99.7279,446.624,99.8789 C446.813,99.9624,447.362,100.253,447.184,100.147 C442.264,97.2348,444.346,98.2776,445.168,99.2354 C445.635,100.178,444.914,101.046,444.335,101.751 C443.282,102.866,441.955,103.674,440.684,104.516 C439.979,105.042,438.841,105.512,438.657,106.495 C438.521,107.219,438.804,107.502,439.115,108.14 C439.373,108.455,439.611,108.787,439.889,109.085 C440.52,109.761,441.438,110.538,442.141,111.121 C443.425,112.185,444.754,113.196,446.067,114.223 C448.295,115.955,450.534,117.67,452.508,119.693 C453.753,121.058,454.844,122.554,455.616,124.235 C456.19,125.547,456.235,126.917,456.081,128.315 C455.929,129.857,455.069,131.063,454.111,132.22 C452.86,133.613,451.408,134.8,450.128,136.163 C449.51,136.928,448.934,137.773,448.792,138.77 C448.716,139.301,448.777,139.643,448.823,140.171 C449.005,141.517,449.421,142.813,449.857,144.095 C450.319,145.456,450.958,146.746,451.568,148.044 C452.147,149.17,452.449,150.372,452.547,151.627 C452.576,152.64,452.406,153.643,452.197,154.63 C452.014,155.489,451.806,156.25,451.273,156.96 C450.579,157.686,449.748,158.26,448.962,158.879 C448.962,158.879,452.094,160.848,452.094,160.848 L452.094,160.848 C452.901,160.205,453.758,159.61,454.468,158.856 C455.052,158.078,455.277,157.331,455.473,156.387 C455.691,155.367,455.861,154.331,455.857,153.286 C455.781,151.991,455.511,150.736,454.918,149.57 C454.311,148.282,453.67,147.005,453.196,145.659 C452.758,144.401,452.34,143.13,452.125,141.811 C452.072,141.379,451.995,140.958,452.039,140.519 C452.134,139.578,452.689,138.785,453.257,138.067 C454.534,136.692,455.986,135.496,457.243,134.102 C458.25,132.898,459.168,131.637,459.347,130.028 C459.522,128.576,459.514,127.143,458.951,125.764 C458.207,124.025,457.101,122.494,455.846,121.083 C455.078,120.279,454.934,120.098,454.085,119.342 C452.574,117.998,450.931,116.813,449.361,115.541 C448.052,114.517,446.728,113.509,445.442,112.456 C444.732,111.875,443.869,111.153,443.218,110.494 C442.948,110.22,442.706,109.919,442.45,109.632 C442.2,109.235,441.825,108.834,441.875,108.312 C441.954,107.485,443.212,106.929,443.774,106.494 C445.073,105.627,446.429,104.795,447.505,103.65 C448.192,102.813,448.909,101.854,448.58,100.714 C448.498,100.578,448.435,100.429,448.335,100.306 C448.225,100.172,448.104,100.04,447.956,99.9498 C446.131,98.8402,444.47,97.783,442.482,97.127 C441.204,96.6881,439.915,96.2798,438.606,95.9437 C437.278,95.6019,435.942,95.2832,434.585,95.0766 C433.319,94.8915,432.044,94.7705,430.765,94.7225 C430.077,94.6977,429.373,94.7137,428.694,94.6018 Z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#1a1a1a"
|
||||||
|
android:fillAlpha="0.465732"
|
||||||
|
android:strokeColor="#4c4f59"
|
||||||
|
android:strokeWidth="6.5"
|
||||||
|
android:strokeMiterLimit="1.3"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M472.565,132.892 C472.58,133.665,472.464,134.438,472.369,135.204 C472.142,136.869,471.719,138.496,471.276,140.113 C470.664,142.278,469.96,144.416,469.202,146.534 C468.343,148.936,467.316,151.271,466.158,153.543 C465.033,155.731,463.751,157.831,462.385,159.876 C461.185,161.66,459.873,163.363,458.525,165.037 C457.749,165.984,456.962,166.921,456.176,167.859 C456.176,167.859,459.27,169.717,459.27,169.717 L459.27,169.717 C460.051,168.773,460.833,167.83,461.601,166.875 C462.941,165.183,464.252,163.468,465.446,161.67 C466.811,159.608,468.088,157.491,469.22,155.29 C470.384,153.005,471.417,150.659,472.298,148.249 C473.071,146.13,473.791,143.992,474.423,141.827 C474.89,140.207,475.334,138.577,475.608,136.911 C475.723,136.144,475.823,135.375,475.929,134.607 Z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#fff9f6"
|
||||||
|
android:strokeColor="#4c4f59"
|
||||||
|
android:strokeWidth="6.5"
|
||||||
|
android:strokeMiterLimit="1.3"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M322.623,86.0328 C322.623,86.0328,398.056,112.153,432.787,175.213 C466.23,235.934,465.836,237.639,465.836,237.639 C465.836,237.639,466.361,246.032,445.902,249.18 C443.279,249.18,460.591,280.131,465.836,286.426 C451.147,293.77,449.049,295.344,449.049,295.344 C449.049,295.344,455.344,303.213,453.77,311.082 C436.459,310.557,431.213,310.033,431.213,310.033 C431.213,310.033,427.016,327.344,415.475,343.082 C410.23,333.115,410.23,320,410.23,320 C410.23,320,386.098,356.721,337.836,361.443 C336.262,358.82,350.426,347.279,355.672,333.115 C344.131,334.689,340.983,335.213,340.983,335.213 L351.475,318.426 C351.475,318.426,362.491,294.295,360.918,290.098 C359.869,287.475,328.918,232.393,328.918,232.393 C328.918,232.393,304.262,265.442,285.377,282.229 C278.557,278.032,271.738,267.54,271.738,267.54 L259.148,279.081 C259.148,279.081,210.71,228.317,217.054,188.898 C213.906,200.964,217.181,245.507,211.411,251.278 C206.165,249.18,195.149,236.589,197.247,210.36 C196.722,209.835,167.345,251.803,167.345,251.803 C167.345,251.803,163.148,300.59,183.607,325.246 C185.181,327.869,182.033,341.508,152.132,322.623 C153.181,321.049,151.083,332.59,151.083,332.59 C151.083,332.59,104.394,334.688,105.444,271.738 C102.821,269.115,91.8047,305.836,91.8047,305.836 C91.8047,305.836,67.6735,273.836,91.2801,212.984 C103.083,182.558,114.624,160.132,123.215,145.312 C131.805,130.492,137.445,123.279,137.445,123.279" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#fffdfc"
|
||||||
|
android:strokeWidth="7.4278"
|
||||||
|
android:strokeMiterLimit="1.3"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M355.672,146.885 A91.803276,41.967213,0,0,1,263.869,188.852 A91.803276,41.967213,0,0,1,172.066,146.885 A91.803276,41.967213,0,0,1,263.869,104.918 A91.803276,41.967213,0,0,1,355.672,146.885 Z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#fde9e7"
|
||||||
|
android:strokeWidth="6.5"
|
||||||
|
android:strokeMiterLimit="1.3"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M349.377,329.443 C349.377,329.443,420.721,287.476,428.066,238.164 C428.066,239.213,454.296,266.492,454.296,266.492 L465.837,290.623 L450.099,297.967 L452.197,311.606 L430.164,310.557 L417.574,340.983 L404.984,323.147 L381.902,349.377 L336.787,364.066 Z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#fde9e7"
|
||||||
|
android:strokeWidth="6.5"
|
||||||
|
android:strokeMiterLimit="1.3"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M325.246,144.787 C325.246,144.787,335.738,188.853,327.344,223.476 C327.344,233.968,360.918,284.328,360.918,284.328 C360.918,284.328,375.607,212.984,335.738,152.131 C327.345,146.885,325.246,144.787,325.246,144.787 Z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#fde9e7"
|
||||||
|
android:strokeWidth="6.5"
|
||||||
|
android:strokeMiterLimit="1.3"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M186.754,166.82 L209.836,167.869 C209.836,167.869,195.147,194.099,196.197,214.033 C194.099,210.885,174.164,245.508,174.164,245.508 C174.164,245.508,164.721,230.819,171.016,195.147 C180.459,178.36,186.754,166.819,186.754,166.819 Z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#fffdfc"
|
||||||
|
android:strokeWidth="6.94619"
|
||||||
|
android:strokeMiterLimit="1.3"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M340.28,112.237 A80.289719,41.967213,0,0,1,259.99,154.204 A80.289719,41.967213,0,0,1,179.7,112.237 A80.289719,41.967213,0,0,1,259.99,70.2698 A80.289719,41.967213,0,0,1,340.28,112.237 Z" />
|
||||||
|
<path
|
||||||
|
android:strokeColor="#a18f90"
|
||||||
|
android:strokeWidth="7.7"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeMiterLimit="1.3"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M138.623,122.492 C138.623,122.492,192.262,69.5084,231.607,64.2625" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#1a1a1a"
|
||||||
|
android:fillAlpha="0"
|
||||||
|
android:strokeColor="#c3b4b0"
|
||||||
|
android:strokeWidth="6.5"
|
||||||
|
android:strokeMiterLimit="1.3"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M436.459,268.066 C436.459,268.066,440.131,286.427,433.836,310.558" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#1a1a1a"
|
||||||
|
android:fillAlpha="0"
|
||||||
|
android:strokeColor="#c3b4b0"
|
||||||
|
android:strokeWidth="6.5"
|
||||||
|
android:strokeMiterLimit="1.3"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M418.098,305.836 L409.18,323.672" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#1a1a1a"
|
||||||
|
android:fillAlpha="0"
|
||||||
|
android:strokeColor="#c3b4b0"
|
||||||
|
android:strokeWidth="6.5"
|
||||||
|
android:strokeMiterLimit="1.3"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M377.705,300.59 C377.705,300.59,380.328,312.656,355.672,332.59" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#1a1a1a"
|
||||||
|
android:fillAlpha="0"
|
||||||
|
android:strokeColor="#c3b4b0"
|
||||||
|
android:strokeWidth="4.2"
|
||||||
|
android:strokeMiterLimit="1.3"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M284.328,97.5738 C284.328,97.5738,296.394,136.918,256.525,216.656 C257.05,216.656,310.033,177.836,312.656,127.476" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#1a1a1a"
|
||||||
|
android:fillAlpha="0"
|
||||||
|
android:strokeColor="#c3b4b0"
|
||||||
|
android:strokeWidth="5.1"
|
||||||
|
android:strokeMiterLimit="1.3"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M272.787,87.082 C272.787,87.082,286.426,78.6886,291.672,86.5574 C308.845,97.3968,304.705,116.794,315.869,130.951 C317.662,132.804,323.041,130.17,323.672,132.721 C328.553,152.446,338.488,183.527,327.996,232.839" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#1a1a1a"
|
||||||
|
android:fillAlpha="0"
|
||||||
|
android:strokeColor="#c3b4b0"
|
||||||
|
android:strokeWidth="4.5"
|
||||||
|
android:strokeMiterLimit="1.3"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M171.004,241.854 C171.004,241.854,142.442,181.762,263.368,94.219 C265.594,94.9609,204.017,148.377,197.34,205.502" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#1a1a1a"
|
||||||
|
android:fillAlpha="0"
|
||||||
|
android:strokeColor="#c3b4b0"
|
||||||
|
android:strokeWidth="3"
|
||||||
|
android:strokeMiterLimit="1.3"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M327.344,143.738 C327.344,143.738,352.524,164.197,359.344,209.312" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#1a1a1a"
|
||||||
|
android:fillAlpha="0"
|
||||||
|
android:strokeColor="#4c4f59"
|
||||||
|
android:strokeWidth="6.5"
|
||||||
|
android:strokeMiterLimit="1.3"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M362.492,268.59 L359.344,288.524" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#1a1a1a"
|
||||||
|
android:fillAlpha="0"
|
||||||
|
android:strokeColor="#4c4f59"
|
||||||
|
android:strokeWidth="6.5"
|
||||||
|
android:strokeMiterLimit="1.3"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M421.77,344.131 C421.77,344.131,419.147,402.361,388.196,455.869 C357.245,509.377,426.491,471.607,426.491,471.607" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#1a1a1a"
|
||||||
|
android:fillAlpha="0"
|
||||||
|
android:strokeColor="#4c4f59"
|
||||||
|
android:strokeWidth="6.5"
|
||||||
|
android:strokeMiterLimit="1.3"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M395.016,348.328 L407.082,408.656" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#1a1a1a"
|
||||||
|
android:fillAlpha="0"
|
||||||
|
android:strokeColor="#4c4f59"
|
||||||
|
android:strokeWidth="6.5"
|
||||||
|
android:strokeMiterLimit="1.3"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M371.934,423.869 C371.934,423.869,402.885,398.164,396.59,349.902" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#1a1a1a"
|
||||||
|
android:fillAlpha="0"
|
||||||
|
android:strokeColor="#4c4f59"
|
||||||
|
android:strokeWidth="6.5"
|
||||||
|
android:strokeMiterLimit="1.3"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M332.066,427.541 C332.066,427.541,368.787,429.115,370.886,398.164" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#1a1a1a"
|
||||||
|
android:fillAlpha="0"
|
||||||
|
android:strokeColor="#4c4f59"
|
||||||
|
android:strokeWidth="6.5"
|
||||||
|
android:strokeMiterLimit="1.3"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M372.459,427.016 L370.361,398.164" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#93cefc"
|
||||||
|
android:strokeColor="#4d4e59"
|
||||||
|
android:strokeWidth="6.5"
|
||||||
|
android:strokeMiterLimit="1.3"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M377.509,228.61 C377.509,228.61,401.64,230.184,401.64,239.102 C401.115,242.774,390.099,256.938,378.033,257.987 C375.935,256.413,377.508,228.61,377.508,228.61 Z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#fffdfe"
|
||||||
|
android:strokeColor="#4c4f59"
|
||||||
|
android:strokeWidth="6.5"
|
||||||
|
android:strokeMiterLimit="1.3"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M350.951,235.541 C350.951,235.541,344.394,213.508,352,213.508 C359.607,213.508,365.377,227.41,365.377,227.41 C365.377,227.41,370.436,209.683,375.344,212.197 C387.142,218.241,378.754,233.967,378.754,233.967 C378.754,233.967,397.377,259.934,366.688,261.246 C336.521,262.535,350.95,235.541,350.95,235.541 Z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#1a1a1a"
|
||||||
|
android:fillAlpha="0"
|
||||||
|
android:strokeColor="#4c4f59"
|
||||||
|
android:strokeWidth="6.5"
|
||||||
|
android:strokeMiterLimit="1.3"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M332.066,425.967 C332.066,425.967,342.558,400.787,338.361,376.131" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#e85240"
|
||||||
|
android:strokeColor="#4c4f59"
|
||||||
|
android:strokeWidth="6.5"
|
||||||
|
android:strokeMiterLimit="1.3"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M192.262,268.852 C192.262,268.852,176.787,290.885,189.639,304.262 C232.393,304.524,232.918,304.787,232.918,304.787 C232.918,304.787,244.197,296.394,238.426,272.787 C212.983,262.82,192.262,268.853,192.262,268.853 Z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#fbb579"
|
||||||
|
android:strokeWidth="6.5"
|
||||||
|
android:strokeMiterLimit="1.3"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M188.066,301.115 C187.624,292.333,201.519,287.099,210.361,286.689 C219.033,286.287,233.601,290.074,233.443,298.755 C233.366,303.017,221.902,304.263,221.902,304.263 C221.902,304.263,188.635,312.429,188.066,301.115 Z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#ffffff"
|
||||||
|
android:strokeWidth="6.5"
|
||||||
|
android:strokeMiterLimit="1.3"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M217.967,273.049 A7.8688526,7.0819674,0,0,1,210.098,280.131 A7.8688526,7.0819674,0,0,1,202.229,273.049 A7.8688526,7.0819674,0,0,1,210.098,265.967 A7.8688526,7.0819674,0,0,1,217.967,273.049 Z" />
|
||||||
|
<path
|
||||||
|
android:strokeColor="#ebc2bf"
|
||||||
|
android:strokeWidth="2.5"
|
||||||
|
android:strokeMiterLimit="1.3"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M174.164,245.508 C189.25,240.742,205.703,238.055,221.258,241.993 C224.556,242.828,227.776,243.986,230.82,245.508" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#fde9e7"
|
||||||
|
android:strokeWidth="6.945"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeMiterLimit="1.3"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M477.772,132.797 C477.772,132.797,491.126,142.812,492.61,150.231 C494.094,157.65,494.094,207.727,494.094,207.727 L489.643,208.84 L458.855,171.375 C458.855,171.375,467.758,156.908,469.983,148.748 C472.209,140.587,477.773,132.798,477.773,132.798 Z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#e85240"
|
||||||
|
android:fillAlpha="0"
|
||||||
|
android:strokeColor="#4c4f59"
|
||||||
|
android:strokeWidth="6.5"
|
||||||
|
android:strokeMiterLimit="1.3"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M192.262,268.852 C192.262,268.852,176.787,290.885,189.639,304.262 C232.393,304.524,232.918,304.787,232.918,304.787 C232.918,304.787,244.197,296.394,238.426,272.787 C212.983,262.82,192.262,268.853,192.262,268.853 Z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#e85240"
|
||||||
|
android:strokeColor="#4c4f59"
|
||||||
|
android:strokeWidth="6.5"
|
||||||
|
android:strokeMiterLimit="1.3"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M325.521,272.819 C342.546,273.885,364.341,295.901,343.357,321.081 C341.783,321.606,294.372,315.775,293.783,311.901 C292.977,306.601,285.826,270.333,325.521,272.819 Z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#fbb579"
|
||||||
|
android:strokeWidth="7.14054"
|
||||||
|
android:strokeMiterLimit="1.3"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M293.046,306.321 C295.599,297.786,313.477,296.791,323.835,298.955 C333.994,301.077,349.503,308.913,346.293,317.177 C344.717,321.235,331.038,319.114,331.038,319.114 C331.038,319.114,289.756,317.315,293.045,306.321 Z" />
|
||||||
|
<path
|
||||||
|
android:strokeColor="#4c4f59"
|
||||||
|
android:strokeWidth="6.5"
|
||||||
|
android:strokeMiterLimit="1.3"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M325.521,272.819 C342.546,273.885,364.341,295.901,343.357,321.081 C341.783,321.606,294.372,315.775,293.783,311.901 C292.977,306.601,285.826,270.333,325.521,272.819 Z" />
|
||||||
|
<path
|
||||||
|
android:strokeColor="#ebc2bf"
|
||||||
|
android:strokeWidth="2.5"
|
||||||
|
android:strokeMiterLimit="1.3"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M309.328,258.435 C309.328,258.435,335.558,253.189,346.049,262.632" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#ffffff"
|
||||||
|
android:strokeWidth="6.5"
|
||||||
|
android:strokeMiterLimit="1.3"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M329.634,284.993 A7.8688526,7.0819674,0,0,1,321.765,292.075 A7.8688526,7.0819674,0,0,1,313.896,284.993 A7.8688526,7.0819674,0,0,1,321.765,277.911 A7.8688526,7.0819674,0,0,1,329.634,284.993 Z" />
|
||||||
|
<path
|
||||||
|
android:strokeColor="#4c4f59"
|
||||||
|
android:strokeWidth="14.5"
|
||||||
|
android:strokeMiterLimit="1.3"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M170.492,266.492 C170.492,266.492,202.492,246.558,240.787,264.394" />
|
||||||
|
<path
|
||||||
|
android:strokeColor="#4c4f59"
|
||||||
|
android:strokeWidth="14.5"
|
||||||
|
android:strokeMiterLimit="1.3"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M292.997,274.177 C292.997,274.177,328.548,261.627,362.085,287.315" />
|
||||||
|
<path
|
||||||
|
android:fillAlpha="0"
|
||||||
|
android:strokeColor="#4c4f59"
|
||||||
|
android:strokeWidth="6.5"
|
||||||
|
android:strokeMiterLimit="1.3"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M431.034,176.197 L449.21,158.763 L490.755,212.92" />
|
||||||
|
<path
|
||||||
|
android:fillAlpha="0"
|
||||||
|
android:strokeColor="#4c4f59"
|
||||||
|
android:strokeWidth="6.5"
|
||||||
|
android:strokeMiterLimit="1.3"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M454.032,267.449 L497.247,243.523" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#fff9f6"
|
||||||
|
android:fillAlpha="0"
|
||||||
|
android:strokeColor="#4c4f59"
|
||||||
|
android:strokeWidth="6.5"
|
||||||
|
android:strokeMiterLimit="1.3"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M164.239,424.641 L133.168,450.694 C133.168,450.694,127.975,468.87,159.134,479.627 C190.293,490.384,372.425,481.111,372.425,481.111 C372.425,481.111,424.728,492.981,453.29,444.017 C452.919,444.759,444.387,463.306,444.387,463.306 L445.871,482.595 C445.871,482.595,503.606,473.181,476.327,333.64 C480.524,324.197,484.72,318.427,484.72,318.427 L481.572,296.657 C481.572,296.657,487.23,256.322,489.455,251.128 C491.681,245.935,497.971,244.777,499.099,240 C501.168,231.244,496.317,219.598,492.422,213.849 C491.68,210.511,499.284,178.795,494.462,155.797 C487.414,143.927,489.64,143.927,475.544,130.202 C475.544,130.573,484.447,85.3181,474.802,56.7556 C471.834,56.7556,442.53,45.9983,377.986,80.4958 C377.615,81.6086,351.649,50.4496,224.416,64.5453 C223.674,64.5453,181.016,26.7093,157.647,24.1127 C156.534,23.7418,135.02,20.0323,132.423,73.0769 C132.423,74.1897,88.281,89.0274,76.4109,137.992 C64.5408,186.956,65.2827,218.857,65.2827,218.857 C65.2827,218.857,46.3778,246.52,55.4846,243.89 C60.8314,242.346,60.4605,267.45,60.4605,267.45 L58.2349,306.77 C58.2349,306.77,43.7682,342.009,43.3972,366.492 C43.0263,390.974,42.6553,400.619,48.2194,415.085 C53.7835,429.552,68.9921,461.824,90.8776,478.145 C99.7802,478.516,109.796,480,109.796,480 L113.505,459.227 C113.505,459.227,132.423,475.177,145.777,474.065 C159.131,472.952,145.035,473.323,145.035,473.323 L131.31,455.518 Z" />
|
||||||
|
<path
|
||||||
|
android:strokeColor="#c3b4b0"
|
||||||
|
android:strokeWidth="6.5"
|
||||||
|
android:strokeMiterLimit="1.3"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M197.508,220.066 L215.869,207.738" />
|
||||||
|
<path
|
||||||
|
android:strokeColor="#c3b4b0"
|
||||||
|
android:strokeWidth="6.5"
|
||||||
|
android:strokeMiterLimit="1.3"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M327.541,234.249 C327.541,234.249,321.269,239.154,311.683,233.258" />
|
||||||
|
<path
|
||||||
|
android:strokeColor="#4c4f59"
|
||||||
|
android:strokeWidth="6.5"
|
||||||
|
android:strokeMiterLimit="1.3"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M322.623,86.0328 C322.623,86.0328,398.056,112.153,432.787,175.213 C466.23,235.934,465.836,237.639,465.836,237.639 C465.836,237.639,466.361,246.032,445.902,249.18 C443.279,249.18,460.591,280.131,465.836,286.426 C451.147,293.77,449.049,295.344,449.049,295.344 C449.049,295.344,455.344,303.213,453.77,311.082 C436.459,310.557,431.213,310.033,431.213,310.033 C431.213,310.033,427.016,327.344,415.475,343.082 C410.23,333.115,410.23,320,410.23,320 C410.23,320,386.098,356.721,337.836,361.443 C336.262,358.82,350.426,347.279,355.672,333.115 C344.131,334.689,340.983,335.213,340.983,335.213 L351.475,318.426 C351.475,318.426,362.491,294.295,360.918,290.098 C359.869,287.475,328.918,232.393,328.918,232.393 C328.918,232.393,304.262,265.442,285.377,282.229 C278.557,278.032,271.738,267.54,271.738,267.54 L259.148,279.081 C259.148,279.081,210.71,228.317,217.054,188.898 C213.906,200.964,217.181,245.507,211.411,251.278 C206.165,249.18,195.149,236.589,197.247,210.36 C196.722,209.835,167.345,251.803,167.345,251.803 C167.345,251.803,163.148,300.59,183.607,325.246 C185.181,327.869,182.033,341.508,152.132,322.623 C153.181,321.049,151.083,332.59,151.083,332.59 C151.083,332.59,104.394,334.688,105.444,271.738 C102.821,269.115,91.8047,305.836,91.8047,305.836 C91.8047,305.836,67.6735,273.836,91.2801,212.984 C103.083,182.558,114.624,160.132,123.215,145.312 C131.805,130.492,137.445,123.279,137.445,123.279" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#a88a8f"
|
||||||
|
android:strokeColor="#4c4f59"
|
||||||
|
android:strokeWidth="2.8623"
|
||||||
|
android:strokeMiterLimit="1.3"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M216.292,255.539 A8.8904896,7.4928694,0,0,1,207.402,263.032 A8.8904896,7.4928694,0,0,1,198.512,255.539 A8.8904896,7.4928694,0,0,1,207.402,248.046 A8.8904896,7.4928694,0,0,1,216.292,255.539 Z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#a88a8f"
|
||||||
|
android:strokeColor="#4c4f59"
|
||||||
|
android:strokeWidth="2.8623"
|
||||||
|
android:strokeMiterLimit="1.3"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M336.169,270.547 A8.8904896,7.4928694,0,0,1,327.279,278.04 A8.8904896,7.4928694,0,0,1,318.389,270.547 A8.8904896,7.4928694,0,0,1,327.279,263.054 A8.8904896,7.4928694,0,0,1,336.169,270.547 Z" />
|
||||||
|
<path
|
||||||
|
android:fillAlpha="0"
|
||||||
|
android:strokeColor="#a18f90"
|
||||||
|
android:strokeWidth="10.4"
|
||||||
|
android:strokeMiterLimit="1.3"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M318.719,85.4618 C318.719,85.4618,369.167,102.525,395.504,128.862 C421.841,155.199,428.147,168.182,428.147,168.182" />
|
||||||
|
<path
|
||||||
|
android:strokeColor="#4c4f59"
|
||||||
|
android:strokeWidth="6.5"
|
||||||
|
android:strokeMiterLimit="1.3"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M186.754,224.525 L197.246,225.574" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#fcbeb5"
|
||||||
|
android:strokeColor="#e99c9b"
|
||||||
|
android:strokeWidth="3.4"
|
||||||
|
android:strokeMiterLimit="1.3"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M250.942,349.983 C250.942,349.983,237.032,341.08,240.927,333.291 C244.822,325.501,258.732,328.84,258.732,328.84 C258.732,328.84,271.715,327.542,278.763,324.945 C285.811,322.348,288.407,329.953,288.407,329.953 C288.407,329.953,293.172,336.018,284.141,346.46 C278.206,353.322,262.997,353.508,262.997,353.508" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#ffffff"
|
||||||
|
android:strokeColor="#e99c9b"
|
||||||
|
android:strokeWidth="1.8"
|
||||||
|
android:strokeMiterLimit="1.3"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M276.209,325.765 C276.209,325.765,280.559,328.784,281.584,333.717 C282.155,333.823,284.918,330.63,284.56,326.844 C283.88,325.359,276.209,325.764,276.209,325.764 Z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#ffffff"
|
||||||
|
android:strokeColor="#e99c9b"
|
||||||
|
android:strokeWidth="1.8"
|
||||||
|
android:strokeMiterLimit="1.3"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M245.007,329.396 L240.834,332.178 L240.741,338.206 L240.645,338.171 L247.718,329.354 Z" />
|
||||||
|
<path
|
||||||
|
android:strokeColor="#e99c9b"
|
||||||
|
android:strokeWidth="3.4"
|
||||||
|
android:strokeMiterLimit="1.3"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M250.942,349.983 C250.942,349.983,237.032,341.08,240.927,333.291 C244.822,325.501,258.732,328.84,258.732,328.84 C258.732,328.84,271.715,327.542,278.763,324.945 C285.811,322.348,288.407,329.953,288.407,329.953 C288.407,329.953,293.172,336.018,284.141,346.46 C278.206,353.322,262.997,353.508,262.997,353.508" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#ffffff"
|
||||||
|
android:strokeColor="#e89493"
|
||||||
|
android:strokeWidth="2.8"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeMiterLimit="1.3"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M190.849,314.929 L196.97,329.952" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#ffffff"
|
||||||
|
android:strokeColor="#e89493"
|
||||||
|
android:strokeWidth="2.8"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeMiterLimit="1.3"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M204.945,316.969 L210.509,331.25" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#ffffff"
|
||||||
|
android:strokeColor="#e89493"
|
||||||
|
android:strokeWidth="2.8"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeMiterLimit="1.3"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M217.928,317.34 L225.347,333.847" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#ffffff"
|
||||||
|
android:strokeColor="#e89493"
|
||||||
|
android:strokeWidth="2.8"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeMiterLimit="1.3"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M300.648,328.468 L307.51,346.644" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#ffffff"
|
||||||
|
android:strokeColor="#e89493"
|
||||||
|
android:strokeWidth="2.8"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeMiterLimit="1.3"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M312.703,329.21 L319.38,346.273" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#ffffff"
|
||||||
|
android:strokeColor="#e89493"
|
||||||
|
android:strokeWidth="2.8"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeMiterLimit="1.3"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M326.243,328.654 L333.291,346.645" />
|
||||||
|
<path
|
||||||
|
android:strokeColor="#e49589"
|
||||||
|
android:strokeWidth="4.6"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeMiterLimit="1.3"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M218.492,348.984 C216.619,350.289,215.156,352.098,213.789,353.9 C212.764,355.251,211.787,356.65,211.017,358.164" />
|
||||||
|
<path
|
||||||
|
android:strokeColor="#4c4f59"
|
||||||
|
android:strokeWidth="3.2"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeMiterLimit="1.3"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M127.475,375.082 C127.475,375.082,113.049,355.934,115.409,315.541" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#ffffff"
|
||||||
|
android:fillAlpha="0.734285"
|
||||||
|
android:strokeColor="#c1545a"
|
||||||
|
android:strokeWidth="7.8"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeMiterLimit="1.3"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M136.393,322.098 C136.393,322.098,145.836,303.213,132.196,282.229 C129.048,281.704,159.999,254.426,137.442,205.114 C135.868,205.639,161.049,149.507,113.311,122.229 C109.114,113.836,67.6717,52.9831,24.1307,135.344 C24.1307,146.885,23.0815,171.541,23.0815,171.541 C23.0815,171.541,8.393,210.361,16.7864,243.41 C18.8848,246.558,-4.1972,293.246,27.8028,349.902 C28.3274,350.427,11.0159,407.607,40.393,432.787 C41.4422,437.508,54.0324,466.361,82.8848,472.131 C95.475,472.656,102.295,471.606,102.295,471.606 C102.295,471.606,133.77,466.885,150.033,404.458 C147.935,376.655,145.836,354.097,139.017,333.638" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#ffffff"
|
||||||
|
android:fillAlpha="0.734285"
|
||||||
|
android:strokeColor="#000000"
|
||||||
|
android:strokeWidth="9"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeMiterLimit="1.3"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M109.698,219.489 C109.698,219.489,94.4894,207.619,88.5543,199.829 M51.4602,219.86 L73.7167,197.974 M74.0876,183.878 L80.0227,225.794 M44.7833,201.683 L100.795,196.861 M111.923,179.427 L92.634,179.798 M92.2631,179.056 L90.7793,149.381 M51.8305,152.719 L89.6665,149.751 M64.8134,138.252 L65.5553,177.943" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#ffffff"
|
||||||
|
android:fillAlpha="0.734285"
|
||||||
|
android:strokeColor="#000000"
|
||||||
|
android:strokeWidth="9"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeMiterLimit="1.3"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M58.5078,352.286 L112.665,348.948 M66.2973,335.223 L101.908,332.255 M80.3934,299.983 L82.99,333.368 M64.072,317.047 L101.166,313.709 M102.65,332.256 L103.392,298.5 M103.021,298.871 L62.2175,301.097 M62.2175,300.726 L66.6688,334.111 M94.1184,285.889 L97.4569,271.422 M97.4569,269.938 L70.0072,271.422 M68.5234,286.26 L69.2653,266.229" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#d96477"
|
||||||
|
android:strokeWidth="0.00475452"
|
||||||
|
android:pathData="M69.454,376.47 C65.2333,376.691,61.3746,378.322,58.6708,381.019 C55.6903,384.001,54.4288,388.12,55.1367,392.567 C56.0415,398.241,59.9003,404.306,66.5213,410.469 C67.9264,411.773,68.9164,412.618,70.9123,414.211 C74.4198,417.006,77.5919,419.223,81.9616,421.92 C83.5211,422.888,86.5974,424.685,87.7577,425.309 L88.093,425.492 L88.5827,425.224 C90.3657,424.243,93.5432,422.361,95.4273,421.172 C102.554,416.674,108.26,412.159,112.491,407.666 C119.352,400.378,122.268,393.281,120.922,387.173 C119.991,382.968,116.877,379.49,112.459,377.723 C110.559,376.963,108.712,376.568,106.62,376.474 C103.911,376.351,101.244,376.814,98.7003,377.846 C94.3679,379.6,90.7486,383.019,88.4706,387.513 C88.3109,387.827,88.1619,388.103,88.1406,388.12 C88.0714,388.175,87.9969,388.069,87.7095,387.491 C87.0282,386.132,85.6923,384.157,84.6438,382.968 C83.5687,381.741,81.8389,380.245,80.5509,379.426 C77.2244,377.307,73.3284,376.27,69.4537,376.47 Z" />
|
||||||
|
</group>
|
||||||
</vector>
|
</vector>
|
||||||
@@ -1,22 +1,27 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:width="108dp"
|
android:width="512dp"
|
||||||
android:height="108dp"
|
android:height="512dp"
|
||||||
android:viewportWidth="108"
|
android:viewportWidth="512"
|
||||||
android:viewportHeight="108">
|
android:viewportHeight="512">
|
||||||
|
<path
|
||||||
<group
|
android:pathData="m212.06,292.39c0,0 -14.77,-11.53 -20.53,-19.09m-36.03,19.45 l21.62,-21.26m0.36,-13.69 l5.76,40.71m-34.22,-23.42 l54.4,-4.68m10.81,-16.93 l-18.73,0.36m-0.36,-0.72 l-1.44,-28.82m-37.83,3.24 l36.75,-2.88m-24.14,-11.17 l0.72,38.55"
|
||||||
android:scaleX="0.135"
|
android:strokeLineJoin="round"
|
||||||
android:scaleY="0.135">
|
android:strokeWidth="8.74077"
|
||||||
<path
|
android:fillColor="#ffffff"
|
||||||
android:pathData="M 259 259 H 541 V 541 H 259 V 259 Z"
|
android:strokeColor="#000000"
|
||||||
android:strokeWidth="18"
|
android:fillAlpha="0.734285"
|
||||||
android:strokeColor="#000000" />
|
android:strokeLineCap="round"/>
|
||||||
<path
|
<path
|
||||||
android:fillColor="#000000"
|
android:pathData="m239.47,294.6 l48.58,-2.99m-41.59,-12.31 l31.94,-2.66m-19.3,-28.95 l2.33,29.94m-16.97,-14.64 l33.27,-2.99m1.33,16.64 l0.67,-30.28m-0.33,0.33 l-36.6,2m0,-0.33 l3.99,29.94m24.62,-43.25 l2.99,-12.98m0,-1.33 l-24.62,1.33m-1.33,13.31 l0.67,-17.97"
|
||||||
android:pathData="M 257 257 H 407 V 407 H 257 V 257 Z" />
|
android:strokeLineJoin="round"
|
||||||
<path
|
android:strokeWidth="8.07261"
|
||||||
android:fillColor="#000000"
|
android:fillColor="#ffffff"
|
||||||
android:pathData="M 393 393 H 543 V 543 H 393 V 393 Z" />
|
android:strokeColor="#000000"
|
||||||
</group>
|
android:fillAlpha="0.734285"
|
||||||
|
android:strokeLineCap="round"/>
|
||||||
|
<path
|
||||||
|
android:pathData="m319.56,229.67c-3.9,0.24 -7.46,1.99 -9.95,4.89 -2.75,3.2 -3.91,7.63 -3.26,12.41 0.84,6.1 4.4,12.62 10.51,19.24 1.3,1.4 2.21,2.31 4.05,4.02 3.24,3 6.16,5.39 10.2,8.29 1.44,1.04 4.28,2.97 5.35,3.64l0.31,0.2 0.45,-0.29c1.65,-1.05 4.58,-3.08 6.32,-4.36 6.58,-4.83 11.84,-9.69 15.75,-14.52 6.33,-7.83 9.02,-15.46 7.78,-22.03 -0.86,-4.52 -3.73,-8.26 -7.81,-10.16 -1.75,-0.82 -3.46,-1.24 -5.39,-1.34 -2.5,-0.13 -4.96,0.37 -7.31,1.47 -4,1.89 -7.34,5.56 -9.44,10.39 -0.15,0.34 -0.28,0.63 -0.3,0.65 -0.06,0.06 -0.13,-0.05 -0.4,-0.68 -0.63,-1.46 -1.86,-3.58 -2.83,-4.86 -0.99,-1.32 -2.59,-2.93 -3.78,-3.81 -3.07,-2.28 -6.67,-3.39 -10.24,-3.18z"
|
||||||
|
android:strokeWidth="0.00473542"
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:strokeColor="#00000000"/>
|
||||||
</vector>
|
</vector>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<background android:drawable="@color/ic_launcher_background" />
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||||
<monochrome android:drawable="@drawable/ic_launcher_monochrome" />
|
<monochrome android:drawable="@drawable/ic_launcher_monochrome" />
|
||||||
</adaptive-icon>
|
</adaptive-icon>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
|
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||||
|
</adaptive-icon>
|
||||||
|
Before Width: | Height: | Size: 235 B |
BIN
manager/app/src/main/res/mipmap-hdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
manager/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 313 B |
|
Before Width: | Height: | Size: 380 B |
BIN
manager/app/src/main/res/mipmap-mdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
manager/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 284 B |
BIN
manager/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
manager/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 7.5 KiB |
|
Before Width: | Height: | Size: 304 B |
BIN
manager/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 8.4 KiB |
BIN
manager/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 342 B |
BIN
manager/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
manager/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 18 KiB |
@@ -10,6 +10,9 @@
|
|||||||
<string name="home_unsupported">غير مدعوم</string>
|
<string name="home_unsupported">غير مدعوم</string>
|
||||||
<string name="home_unsupported_reason">KernelSU يدعم GKI kernels فقط</string>
|
<string name="home_unsupported_reason">KernelSU يدعم GKI kernels فقط</string>
|
||||||
<string name="home_kernel">إصدار النواة</string>
|
<string name="home_kernel">إصدار النواة</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_manager_version">إصدار المدير</string>
|
||||||
<string name="home_fingerprint">البصمة</string>
|
<string name="home_fingerprint">البصمة</string>
|
||||||
<string name="home_selinux_status">وضع SELinux</string>
|
<string name="home_selinux_status">وضع SELinux</string>
|
||||||
@@ -22,7 +25,10 @@
|
|||||||
<string name="module_failed_to_disable">فشل تعطيل الإضافة : %s</string>
|
<string name="module_failed_to_disable">فشل تعطيل الإضافة : %s</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>
|
||||||
@@ -50,84 +56,318 @@
|
|||||||
<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="about_source_code"><![CDATA[أنظر إلى مصدر البرمجة في %1$s<br/>إنضم إلى قناتنا في %2$s ]]></string>
|
||||||
|
<string name="profile_default">الإفتراضي</string>
|
||||||
|
<string name="profile_template">نموذج</string>
|
||||||
|
<string name="profile_custom">مُخصّص</string>
|
||||||
|
<string name="profile_name">اسم الملف الشخصي</string>
|
||||||
|
<string name="profile_namespace">تركيب مساحة الاسم</string>
|
||||||
|
<string name="profile_namespace_inherited">موروث</string>
|
||||||
|
<string name="profile_namespace_global">عالمي</string>
|
||||||
|
<string name="profile_namespace_individual">فردي</string>
|
||||||
|
<string name="profile_groups">مجموعات</string>
|
||||||
<string name="profile_capabilities">القدرات</string>
|
<string name="profile_capabilities">القدرات</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="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_rules">القواعد</string>
|
||||||
<string name="module_update">تحديث</string>
|
<string name="module_update">تحديث</string>
|
||||||
<string name="module_downloading">تحميل الإضافة: %s</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="new_version_available">الإصدار الجديد: %s متاح ، انقر للتحديث.</string>
|
||||||
<string name="launch_app">تشغيل</string>
|
<string name="launch_app">تشغيل</string>
|
||||||
<string name="profile_default">الإفتراضي</string>
|
<string name="force_stop_app" formatted="false">ايقاف إجباري</string>
|
||||||
<string name="profile_template">نموذج</string>
|
|
||||||
<string name="profile_namespace_inherited">موروث</string>
|
|
||||||
<string name="profile_namespace_global">عالمي</string>
|
|
||||||
<string name="profile_namespace_individual">فردي</string>
|
|
||||||
<string name="profile_groups">مجموعات</string>
|
|
||||||
<string name="profile_custom">مُخصّص</string>
|
|
||||||
<string name="profile_namespace">تركيب مساحة الاسم</string>
|
|
||||||
<string name="profile_umount_modules">الغاء تحميل الإضافات</string>
|
|
||||||
<string name="failed_to_update_app_profile">فشل تحديث ملف تعريف التطبيق لـ %s</string>
|
|
||||||
<string name="profile_selinux_context">سياق SELinux</string>
|
|
||||||
<string name="force_stop_app">ايقاف إجباري</string>
|
|
||||||
<string name="settings_umount_modules_default">الغاء تحميل الإضافات بشكل افتراضي</string>
|
|
||||||
<string name="settings_umount_modules_default_summary">القيمة الافتراضية العامة لـ\"إلغاء تحميل الإضافات\" في ملفات تعريف التطبيقات. إذا تم تمكينه، إزالة جميع تعديلات الإضافات على النظام للتطبيقات التي لا تحتوي على مجموعة ملف تعريف.</string>
|
|
||||||
<string name="profile_umount_modules_summary">سيسمح تمكين هذا الخيار لـKernelSU باستعادة أي ملفات معدلة بواسطة الإضافات لهذا التطبيق.</string>
|
|
||||||
<string name="profile_selinux_domain">المجال</string>
|
|
||||||
<string name="profile_selinux_rules" 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_name">اسم الملف الشخصي</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">معرف القالب موجود بالفعل!</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">معرف القالب غير صالح</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">المعرف</string>
|
<string name="app_profile_template_id">المعرف</string>
|
||||||
<string name="settings_profile_template">قالب ملف تعريف التطبيق</string>
|
<string name="app_profile_template_id_invalid">معرف القالب غير صالح</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="grant_root_failed">فشل في منح صلاحية الجذر!</string>
|
<string name="app_profile_template_readonly">للقراءة فقط</string>
|
||||||
<string name="open">فتح</string>
|
<string name="app_profile_template_id_exist">معرف القالب موجود بالفعل!</string>
|
||||||
<string name="settings_check_update_summary">التحقق تلقائيًا من وجود تحديثات عند فتح التطبيق</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">التحقق من التحديث</string>
|
||||||
|
<string name="settings_check_update_summary">التحقق تلقائيًا من وجود تحديثات عند فتح التطبيق</string>
|
||||||
|
<string name="grant_root_failed">فشل في منح صلاحية الجذر!</string>
|
||||||
|
<string name="action">إجراء</string>
|
||||||
|
<string name="open">فتح</string>
|
||||||
|
<string name="close">إغلاق</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="install_next">التالي</string>
|
|
||||||
<string name="select_file">اختيار ملف</string>
|
|
||||||
<string name="direct_install">تثبيت مباشر (موصى به)</string>
|
<string name="direct_install">تثبيت مباشر (موصى به)</string>
|
||||||
|
<string name="select_file">اختيار ملف</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استخدم هذا الخيار فقط بعد انتهاء التحديث.
|
\nاستخدم هذا الخيار فقط بعد انتهاء التحديث.
|
||||||
\nأستمرار؟</string>
|
\nأستمرار؟</string>
|
||||||
<string name="select_kmi">اختر KMI</string>
|
<string name="install_next">التالي</string>
|
||||||
<string name="select_file_tip">يوصى باستخدام صورة القسم %1$s</string>
|
<string name="select_file_tip">يوصى باستخدام صورة القسم %1$s</string>
|
||||||
|
<string name="select_kmi">اختر KMI</string>
|
||||||
<string name="settings_uninstall">إلغاء التثبيت</string>
|
<string name="settings_uninstall">إلغاء التثبيت</string>
|
||||||
<string name="settings_uninstall_temporary">إلغاء التثبيت مؤقتًا</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_message">قم بإلغاء تثبيت KernelSU مؤقتًا، واستعد إلى حالته الأصلية بعد إعادة التشغيل التالية.</string>
|
||||||
<string name="settings_uninstall_permanent_message">إلغاء تثبيت KernelSU .(الجذر وجميع الوحدات) بشكل كامل ودائم.</string>
|
<string name="settings_uninstall_permanent_message">إلغاء تثبيت 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="settings_restore_stock_image_message">استعادة صورة المصنع المخزنة (في حالة وجود نسخة احتياطية)، والتي تُستخدم عادة قبل OTA؛ إذا كنت بحاجة إلى إلغاء تثبيت KernelSU، فيرجى استخدام \"إلغاء التثبيت الدائم\".</string>
|
|
||||||
<string name="settings_uninstall_temporary_message">قم بإلغاء تثبيت KernelSU مؤقتًا، واستعد إلى حالته الأصلية بعد إعادة التشغيل التالية.</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">تعطيل أي تطبيقات مؤقتًا من الحصول على امتيازات الجذر عن طريق الأمر <unk> su (لن تتأثر عمليات الجذر الحالية).</string>
|
||||||
|
<string name="using_mksu_manager">أنت تستخدم مدير Beta SukiSU</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">إخفاء البطاقات غير الضرورية عند تشغيلها</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_yellow">الأصفر</string>
|
||||||
|
<string name="flash_option">خيارات الفرشاة</string>
|
||||||
|
<string name="flash_option_tip">حدد الملف المراد إلفاؤه</string>
|
||||||
|
<string name="horizon_kernel">Anykernel3 yükle</string>
|
||||||
|
<string name="horizon_kernel_summary">فلاش AnyKernel3 ملف kernel</string>
|
||||||
|
<string name="root_required">يتطلب امتيازات الجذر</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">التمكين</string>
|
||||||
|
<string name="batch_cancel_authorization">السحب</string>
|
||||||
|
<string name="backup">النسخ الاحتياطية</string>
|
||||||
|
<string name="kpm_title">KPM</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">تم تحميل وحدة كيلو جزء بنجاح</string>
|
||||||
|
<string name="kpm_install_failed">فشل تحميل وحدة كيلو بايم</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">سوكيسو أولترا تتطلع إلى الأمام</string>
|
||||||
|
<string name="kpm_control_success">نجحت</string>
|
||||||
|
<string name="kpm_control_failed">فشل</string>
|
||||||
|
<string name="home_click_to_ContributionCard_kernelsu">وستشكل سوكيسو أولترا في المستقبل فرعا مستقلا نسبيا من فروع الوحدة، ولكننا لا نزال نقدر كيرنيل سو وموكسو الرسميين وما إلى ذلك. لإسهاماتهم!</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 Install</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">تعطيل روابط kprobe التي أنشأتها KernelSU، باستخدام الروابط الواردة بدلاً من ذلك، والتي تشبه طريقة الربط غير GKI غير GKI.</string>
|
||||||
|
<string name="image_editor_title">ضبط صورة الخلفية</string>
|
||||||
|
<string name="image_editor_hint">استخدم إصبعين لتكبير الصورة، وأصبع واحد لسحبها لضبط الموضع</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">الرجاء تحديد الخانة المستهدفة للتشغيل المبطن</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">Flashing AnyKernel3</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">التطبيقات مع امتيازات الجذر</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">يتطلب امتيازات الجذر</string>
|
||||||
|
<!-- KPM display settings -->
|
||||||
|
<string name="show_kpm_info">عرض وظيفة KPM</string>
|
||||||
|
<string name="show_kpm_info_summary">عرض معلومات KPM ووظيفتها في الشريط المنزلي والأسفل (تحتاج إلى إعادة فتح التطبيق)</string>
|
||||||
|
<!-- Webui X settings -->
|
||||||
|
<string name="use_webuix">Select the WebUI engine to use</string>
|
||||||
|
<string name="engine_auto_select">Automatic Selection</string>
|
||||||
|
<string name="engine_force_webuix">Force the use of WebUI X</string>
|
||||||
|
<string name="engine_force_ksu">Mandatory use of KSU WebUI</string>
|
||||||
|
<string name="use_webuix_eruda">حقن Eruda في WebUI X</string>
|
||||||
|
<string name="use_webuix_eruda_summary">حقن وحدة التصحيح في WebUI X لجعل تصحيح الأخطاء أسهل. يتطلب تصحيح أخطاء الويب لتكون قيد التشغيل.</string>
|
||||||
|
<!-- DPI setting related strings -->
|
||||||
|
<string name="dpi_settings">إعداد DPI</string>
|
||||||
|
<string name="app_dpi_title">تم تطبيق DPI</string>
|
||||||
|
<string name="app_dpi_summary">ضبط كثافة عرض الشاشة للتطبيق الحالي فقط</string>
|
||||||
|
<string name="dpi_size_small">صغير </string>
|
||||||
|
<string name="dpi_size_medium">متوسط </string>
|
||||||
|
<string name="dpi_size_large">كبير</string>
|
||||||
|
<string name="dpi_size_extra_large">حجم كبير</string>
|
||||||
|
<string name="dpi_size_custom">قابلة للتعديل</string>
|
||||||
|
<string name="dpi_apply_settings">تطبيق إعدادات DPI</string>
|
||||||
|
<string name="dpi_confirm_title">تأكيد تغيير إدارة شؤون الإعلام</string>
|
||||||
|
<string name="dpi_confirm_message">هل أنت متأكد من أنك تريد تغيير تطبيق DPI من %1$d إلى %2$d؟</string>
|
||||||
|
<string name="dpi_confirm_summary">يحتاج التطبيق إلى إعادة تشغيل لتطبيق الإعدادات الجديدة لإدارة شؤون الإعلام، ولا يؤثر على شريط حالة النظام أو التطبيقات الأخرى</string>
|
||||||
|
<string name="dpi_applied_success">تم تعيين DPI إلى %1$d، فعلي بعد إعادة تشغيل التطبيق</string>
|
||||||
|
<!-- Language settings related strings -->
|
||||||
|
<string name="language_setting">لغة التطبيق</string>
|
||||||
|
<string name="language_follow_system">اتبع النظام</string>
|
||||||
|
<string name="language_changed">تم تغيير اللغة، إعادة التشغيل لتطبيق التغييرات</string>
|
||||||
|
<string name="settings_card_dim">تعديل ظلام البطاقة</string>
|
||||||
|
<!-- Super User Related -->
|
||||||
|
<string name="scroll_to_top">في الأعلى</string>
|
||||||
|
<string name="scroll_to_bottom">أسفل</string>
|
||||||
|
<string name="scroll_to_top_description">التمرير لأعلى</string>
|
||||||
|
<string name="scroll_to_bottom_description">التمرير إلى الأسفل</string>
|
||||||
|
<string name="authorized">مصرح</string>
|
||||||
|
<string name="unauthorized">غير مصرح</string>
|
||||||
|
<string name="selected">محدد</string>
|
||||||
|
<string name="select">خيار</string>
|
||||||
|
<string name="profile_umount_modules_disable">تعطيل وحدة إلغاء التثبيت المخصصة</string>
|
||||||
|
<!-- Flash related -->
|
||||||
|
<string name="error_code">رمز الخطأ</string>
|
||||||
|
<string name="check_log">يرجى التحقق من السجل</string>
|
||||||
|
<string name="installing_module">تم تثبيت الوحدة %1$d/%2$d</string>
|
||||||
|
<string name="module_failed_count">أخفق %d في تثبيت وحدة جديدة</string>
|
||||||
|
<string name="module_download_error">فشل تحميل الوحدة</string>
|
||||||
|
<string name="kernel_flashing">Kernel Flashing</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1,80 +1,371 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<string name="home">Ana səhifə</string>
|
<string name="home">Ana səhifə</string>
|
||||||
<string name="home_superuser_count">Super istifadəçilər: %d</string>
|
|
||||||
<string name="home_kernel">Nüvə</string>
|
|
||||||
<string name="home_not_installed">Yüklənmədi</string>
|
<string name="home_not_installed">Yüklənmədi</string>
|
||||||
<string name="home_click_to_install">Yükləmək üçün toxunun</string>
|
<string name="home_click_to_install">Yükləmək üçün toxunun</string>
|
||||||
<string name="home_working">İşləyir</string>
|
<string name="home_working">İşləyir</string>
|
||||||
<string name="home_working_version">Versiya: %d</string>
|
<string name="home_working_version">Versiya: %d</string>
|
||||||
|
<string name="home_superuser_count">Super istifadəçilər: %d</string>
|
||||||
<string name="home_module_count">Modullar: %d</string>
|
<string name="home_module_count">Modullar: %d</string>
|
||||||
<string name="home_unsupported_reason">Hal-hazırda KernelSU yalnız GKI nüvələrini dəstəkləyir</string>
|
|
||||||
<string name="home_unsupported">Dəstəklənmir</string>
|
<string name="home_unsupported">Dəstəklənmir</string>
|
||||||
<string name="module_install">Yüklə</string>
|
<string name="home_unsupported_reason">Hal-hazırda KernelSU yalnız GKI nüvələrini dəstəkləyir</string>
|
||||||
<string name="install">Yüklə</string>
|
<string name="home_kernel">Nüvə</string>
|
||||||
<string name="selinux_status_unknown">Naməlum</string>
|
<string name="home_susfs">SuSFS: %s</string>
|
||||||
<string name="home_fingerprint">Barmaq izi</string>
|
<string name="home_susfs_version">SuSFS Version</string>
|
||||||
|
<string name="home_susfs_sus_su">SuS SU</string>
|
||||||
<string name="home_manager_version">Menecer versiyası</string>
|
<string name="home_manager_version">Menecer versiyası</string>
|
||||||
<string name="selinux_status_disabled">Qeyri-aktiv</string>
|
<string name="home_fingerprint">Barmaq izi</string>
|
||||||
<string name="home_selinux_status">SELinux vəziyyəti</string>
|
<string name="home_selinux_status">SELinux vəziyyəti</string>
|
||||||
<string name="selinux_status_permissive">Sərbəst</string>
|
<string name="selinux_status_disabled">Qeyri-aktiv</string>
|
||||||
<string name="selinux_status_enforcing">Məcburi</string>
|
<string name="selinux_status_enforcing">Məcburi</string>
|
||||||
|
<string name="selinux_status_permissive">Sərbəst</string>
|
||||||
|
<string name="selinux_status_unknown">Naməlum</string>
|
||||||
<string name="superuser">Super istifadəçi</string>
|
<string name="superuser">Super istifadəçi</string>
|
||||||
<string name="uninstall">Sil</string>
|
|
||||||
<string name="module_failed_to_enable">Modulu aktiv etmək mümkün olmadı: %s</string>
|
<string name="module_failed_to_enable">Modulu aktiv etmək mümkün olmadı: %s</string>
|
||||||
<string name="module_failed_to_disable">Modulu deaktiv etmək mümkün olmadı: %s</string>
|
<string name="module_failed_to_disable">Modulu deaktiv etmək mümkün olmadı: %s</string>
|
||||||
<string name="module_empty">Heç bir modul quraşdırılmayıb</string>
|
<string name="module_empty">Heç bir modul quraşdırılmayıb</string>
|
||||||
<string name="module">Modul</string>
|
<string name="module">Modul</string>
|
||||||
|
<string name="module_sort_action_first">Sort (Action first)</string>
|
||||||
|
<string name="module_sort_enabled_first">Sort (Enabled first)</string>
|
||||||
|
<string name="uninstall">Sil</string>
|
||||||
|
<string name="restore">Restore</string>
|
||||||
|
<string name="module_install">Yüklə</string>
|
||||||
|
<string name="install">Yüklə</string>
|
||||||
<string name="reboot">Yenidən başlat</string>
|
<string name="reboot">Yenidən başlat</string>
|
||||||
<string name="settings">Parametrlər</string>
|
<string name="settings">Parametrlər</string>
|
||||||
<string name="reboot_recovery">Bərpa rejimində yenidən başlat</string>
|
|
||||||
<string name="reboot_userspace">Yüngül vəziyyətdə yenodən başlat</string>
|
<string name="reboot_userspace">Yüngül vəziyyətdə yenodən başlat</string>
|
||||||
|
<string name="reboot_recovery">Bərpa rejimində yenidən başlat</string>
|
||||||
<string name="reboot_bootloader">Bootloader rejimində yenidən başlat</string>
|
<string name="reboot_bootloader">Bootloader rejimində yenidən başlat</string>
|
||||||
<string name="reboot_download">Yükləmə rejimində yenidən başlat</string>
|
<string name="reboot_download">Yükləmə rejimində yenidən başlat</string>
|
||||||
|
<string name="reboot_edl">EDL rejimində yenidən başlat</string>
|
||||||
|
<string name="about">Haqqında</string>
|
||||||
|
<string name="module_uninstall_confirm">Modulu silmək istədiyinizdən əminsiniz %s\?</string>
|
||||||
|
<string name="module_uninstall_success">%s silindi</string>
|
||||||
|
<string name="module_uninstall_failed">Silmək mümkün olmadı: %s</string>
|
||||||
<string name="module_version">Versiya</string>
|
<string name="module_version">Versiya</string>
|
||||||
<string name="module_author">Sahib</string>
|
<string name="module_author">Sahib</string>
|
||||||
<string name="module_uninstall_confirm">Modulu silmək istədiyinizdən əminsiniz %s\?</string>
|
<string name="refresh">Yenilə</string>
|
||||||
<string name="show_system_apps">Sistem proqramlarını göstər</string>
|
<string name="show_system_apps">Sistem proqramlarını göstər</string>
|
||||||
<string name="about">Haqqında</string>
|
|
||||||
<string name="reboot_edl">EDL rejimində yenidən başlat</string>
|
|
||||||
<string name="module_uninstall_failed">Silmək mümkün olmadı: %s</string>
|
|
||||||
<string name="module_uninstall_success">%s silindi</string>
|
|
||||||
<string name="hide_system_apps">Sistem proqramlarını gizlət</string>
|
<string name="hide_system_apps">Sistem proqramlarını gizlət</string>
|
||||||
<string name="send_log">Log-u göndər</string>
|
<string name="send_log">Log-u göndər</string>
|
||||||
<string name="refresh">Yenilə</string>
|
|
||||||
<string name="safe_mode">Təhlükəsiz rejimi</string>
|
<string name="safe_mode">Təhlükəsiz rejimi</string>
|
||||||
<string name="reboot_to_apply">Qüvvəyə minməsi üçün yenidən başlat</string>
|
<string name="reboot_to_apply">Qüvvəyə minməsi üçün yenidən başlat</string>
|
||||||
<string name="module_magisk_conflict">Modular deaktiv edilir,çünki o Magisk-in modulları ilə toqquşur!</string>
|
<string name="module_magisk_conflict">Modular deaktiv edilir,çünki o Magisk-in modulları ilə toqquşur!</string>
|
||||||
<string name="home_learn_kernelsu">KernelSU-yu öyrən</string>
|
<string name="home_learn_kernelsu">KernelSU-yu öyrən</string>
|
||||||
<string name="home_learn_kernelsu_url">https://kernelsu.org/guide/what-is-kernelsu.html</string>
|
<string name="home_learn_kernelsu_url">https://kernelsu.org/guide/what-is-kernelsu.html</string>
|
||||||
<string name="home_support_title">Bizi dəstəkləyin</string>
|
|
||||||
<string name="home_click_to_learn_kernelsu">KernelSU-yu necə quraşdırılacağını və modulların necə istifadə ediləcəyini öyrən</string>
|
<string name="home_click_to_learn_kernelsu">KernelSU-yu necə quraşdırılacağını və modulların necə istifadə ediləcəyini öyrən</string>
|
||||||
<string name="profile_template">Şablon</string>
|
<string name="home_support_title">Bizi dəstəkləyin</string>
|
||||||
<string name="profile_default">Defolt</string>
|
|
||||||
<string name="profile_custom">Özəl</string>
|
|
||||||
<string name="home_support_content">KernelSU pulsuz və açıq mənbəlidir,həmişə belə olacaqdır. Bununla belə, ianə etməklə bizə qayğı göstərdiyinizi göstərə bilərsiniz.</string>
|
<string name="home_support_content">KernelSU pulsuz və açıq mənbəlidir,həmişə belə olacaqdır. Bununla belə, ianə etməklə bizə qayğı göstərdiyinizi göstərə bilərsiniz.</string>
|
||||||
|
<string name="about_source_code"><![CDATA[View source code at %1$s<br/>Join our %2$s channel]]></string>
|
||||||
|
<string name="profile_default">Defolt</string>
|
||||||
|
<string name="profile_template">Şablon</string>
|
||||||
|
<string name="profile_custom">Özəl</string>
|
||||||
<string name="profile_name">Profil adı</string>
|
<string name="profile_name">Profil adı</string>
|
||||||
<string name="profile_capabilities">Bacarıqlar</string>
|
<string name="profile_namespace">Bölmənin ad sahəsi</string>
|
||||||
<string name="profile_umount_modules">Modulları umount et</string>
|
|
||||||
<string name="profile_namespace_inherited">Miras qalmış</string>
|
<string name="profile_namespace_inherited">Miras qalmış</string>
|
||||||
<string name="profile_namespace_global">Qlobal</string>
|
<string name="profile_namespace_global">Qlobal</string>
|
||||||
<string name="profile_namespace">Bölmənin ad sahəsi</string>
|
|
||||||
<string name="profile_namespace_individual">Fərdi</string>
|
<string name="profile_namespace_individual">Fərdi</string>
|
||||||
<string name="profile_groups">Qruplar</string>
|
<string name="profile_groups">Qruplar</string>
|
||||||
<string name="settings_umount_modules_default">Defolt olaraq modulları umount et</string>
|
<string name="profile_capabilities">Bacarıqlar</string>
|
||||||
<string name="profile_selinux_context">SELinux konteksi</string>
|
<string name="profile_selinux_context">SELinux konteksi</string>
|
||||||
|
<string name="profile_umount_modules">Modulları umount et</string>
|
||||||
<string name="failed_to_update_app_profile">%s görə tətbiq profillərini güncəlləmək mümkün olmadı</string>
|
<string name="failed_to_update_app_profile">%s görə tətbiq profillərini güncəlləmək mümkün olmadı</string>
|
||||||
|
<string name="require_kernel_version" formatted="false">The current KernelSU version %d is too low for the manager to work properly. Please upgrade to version %d or higher!</string>
|
||||||
|
<string name="settings_umount_modules_default">Defolt olaraq modulları umount et</string>
|
||||||
<string name="settings_umount_modules_default_summary">Tətbiq Profillərində \"Umount modulları\" üçün qlobal standart dəyər. Aktivləşdirilərsə, o, Profil dəsti olmayan proqramlar üçün sistemdəki bütün modul dəyişikliklərini siləcək.</string>
|
<string name="settings_umount_modules_default_summary">Tətbiq Profillərində \"Umount modulları\" üçün qlobal standart dəyər. Aktivləşdirilərsə, o, Profil dəsti olmayan proqramlar üçün sistemdəki bütün modul dəyişikliklərini siləcək.</string>
|
||||||
|
<string name="settings_susfs_toggle">Disable kprobe hooks</string>
|
||||||
|
<string name="profile_umount_modules_summary">Bu seçimi aktivləşdirmək KernelSU-ya bu proqram üçün modullar tərəfindən hər hansı dəyişdirilmiş faylları bərpa etməyə imkan verəcək.</string>
|
||||||
<string name="profile_selinux_domain">Domen</string>
|
<string name="profile_selinux_domain">Domen</string>
|
||||||
<string name="profile_selinux_rules">Qaydalar</string>
|
<string name="profile_selinux_rules">Qaydalar</string>
|
||||||
<string name="module_update">Güncəllə</string>
|
<string name="module_update">Güncəllə</string>
|
||||||
|
<string name="module_downloading">Modul yüklənir: %s</string>
|
||||||
<string name="module_start_downloading">Endirməni başlat: %s</string>
|
<string name="module_start_downloading">Endirməni başlat: %s</string>
|
||||||
<string name="new_version_available">Yeni versiya: %s əlçatandır, endirmək üçün toxunun</string>
|
<string name="new_version_available">Yeni versiya: %s əlçatandır, endirmək üçün toxunun</string>
|
||||||
<string name="module_downloading">Modul yüklənir: %s</string>
|
|
||||||
<string name="profile_umount_modules_summary">Bu seçimi aktivləşdirmək KernelSU-ya bu proqram üçün modullar tərəfindən hər hansı dəyişdirilmiş faylları bərpa etməyə imkan verəcək.</string>
|
|
||||||
<string name="launch_app">Aç</string>
|
<string name="launch_app">Aç</string>
|
||||||
<string name="force_stop_app">Məcburi dayandır</string>
|
<string name="force_stop_app" formatted="false">Məcburi dayandır</string>
|
||||||
<string name="restart_app">Yenidən başlat</string>
|
<string name="restart_app">Yenidən başlat</string>
|
||||||
<string name="failed_to_update_sepolicy">%s görə SELinux qaydalarını güncəlləmək mümkün olmadı</string>
|
<string name="failed_to_update_sepolicy">%s görə SELinux qaydalarını güncəlləmək mümkün olmadı</string>
|
||||||
|
<string name="module_changelog">Changelog</string>
|
||||||
|
<string name="settings_profile_template">App Profile Template</string>
|
||||||
|
<string name="settings_profile_template_summary">Manage local and online template of App Profile</string>
|
||||||
|
<string name="app_profile_template_create">Create template</string>
|
||||||
|
<string name="app_profile_template_edit">Edit template</string>
|
||||||
|
<string name="app_profile_template_id">ID</string>
|
||||||
|
<string name="app_profile_template_id_invalid">Invalid template ID</string>
|
||||||
|
<string name="app_profile_template_name">Name</string>
|
||||||
|
<string name="app_profile_template_description">Description</string>
|
||||||
|
<string name="app_profile_template_save">Save</string>
|
||||||
|
<string name="app_profile_template_delete">Delete</string>
|
||||||
|
<string name="app_profile_template_view">View template</string>
|
||||||
|
<string name="app_profile_template_readonly">Read only</string>
|
||||||
|
<string name="app_profile_template_id_exist">Template ID already exists!</string>
|
||||||
|
<string name="app_profile_import_export">Import/Export</string>
|
||||||
|
<string name="app_profile_import_from_clipboard">Import from clipboard</string>
|
||||||
|
<string name="app_profile_export_to_clipboard">Export to clipboard</string>
|
||||||
|
<string name="app_profile_template_export_empty">Cannot find local template to export!</string>
|
||||||
|
<string name="app_profile_template_import_success">Imported successfully</string>
|
||||||
|
<string name="app_profile_template_sync">Sync online templates</string>
|
||||||
|
<string name="app_profile_template_save_failed">Failed to save template</string>
|
||||||
|
<string name="app_profile_template_import_empty">Clipboard is empty!</string>
|
||||||
|
<string name="module_changelog_failed">Fetch changelog failed: %s</string>
|
||||||
|
<string name="settings_check_update">Check update</string>
|
||||||
|
<string name="settings_check_update_summary">Automatically check for updates when opening the app</string>
|
||||||
|
<string name="grant_root_failed">Failed to grant root!</string>
|
||||||
|
<string name="action">Action</string>
|
||||||
|
<string name="open">Open</string>
|
||||||
|
<string name="close">Close</string>
|
||||||
|
<string name="enable_web_debugging">Enable WebView debugging</string>
|
||||||
|
<string name="enable_web_debugging_summary">Can be used to debug WebUI. Please enable only when needed.</string>
|
||||||
|
<string name="direct_install">Direct install (Recommended)</string>
|
||||||
|
<string name="select_file">Select a image that needs to be patched</string>
|
||||||
|
<string name="install_inactive_slot">Install to inactive slot (After OTA)</string>
|
||||||
|
<string name="install_inactive_slot_warning">Your device will be **FORCED** to boot to the current inactive slot after a reboot!\nOnly use this option after OTA is done.\nContinue?</string>
|
||||||
|
<string name="install_next">Next</string>
|
||||||
|
<string name="select_file_tip">%1$s partition image is recommended</string>
|
||||||
|
<string name="select_kmi">Select KMI</string>
|
||||||
|
<string name="settings_uninstall">Uninstall</string>
|
||||||
|
<string name="settings_uninstall_temporary">Uninstall temporarily</string>
|
||||||
|
<string name="settings_uninstall_permanent">Uninstall permanently</string>
|
||||||
|
<string name="settings_restore_stock_image">Restore stock image</string>
|
||||||
|
<string name="settings_uninstall_temporary_message">Temporarily uninstall KernelSU, restore to original state after next reboot.</string>
|
||||||
|
<string name="settings_uninstall_permanent_message">Uninstalling KernelSU (Root and all modules) completely and permanently.</string>
|
||||||
|
<string name="settings_restore_stock_image_message">Restore the stock factory image (If a backup exists), usually used before OTA; if you need to uninstall KernelSU, please use \"Uninstall permanently\".</string>
|
||||||
|
<string name="flashing">Flashing</string>
|
||||||
|
<string name="flash_success">Flash success</string>
|
||||||
|
<string name="flash_failed">Flash failed</string>
|
||||||
|
<string name="selected_lkm">Selected LKM: %s</string>
|
||||||
<string name="save_log">Girişləri Saxla</string>
|
<string name="save_log">Girişləri Saxla</string>
|
||||||
|
<string name="log_saved">Logs saved</string>
|
||||||
|
<string name="status_supported">Supported</string>
|
||||||
|
<string name="status_not_supported">Not Supported</string>
|
||||||
|
<string name="status_unknown">Unknown</string>
|
||||||
|
<string name="sus_su_mode">SuS SU mode:</string>
|
||||||
|
<!-- Module related -->
|
||||||
|
<string name="module_install_confirm">confirm install module %1$s?</string>
|
||||||
|
<string name="unknown_module">unknown module</string>
|
||||||
|
<!-- Restore related -->
|
||||||
|
<string name="restore_confirm_title">Confirm Module Restoration</string>
|
||||||
|
<string name="restore_confirm_message">This operation will overwrite all existing modules. Continue?</string>
|
||||||
|
<string name="confirm">Confirm</string>
|
||||||
|
<string name="cancel">Cancel</string>
|
||||||
|
<!-- Backup related -->
|
||||||
|
<string name="backup_success">Backup successful (tar.gz)</string>
|
||||||
|
<string name="backup_failed">Backup failed: %1$s</string>
|
||||||
|
<string name="backup_modules">backup modules</string>
|
||||||
|
<string name="restore_modules">restore modules</string>
|
||||||
|
<!-- Restore related messages -->
|
||||||
|
<string name="restore_success">Modules restored successfully, restart required</string>
|
||||||
|
<string name="restore_failed">Restore failed: %1$s</string>
|
||||||
|
<string name="restart_now">Restart Now</string>
|
||||||
|
<string name="unknown_error">Unknown error</string>
|
||||||
|
<!-- Command related -->
|
||||||
|
<string name="command_execution_failed">Command execution failed: %1$s</string>
|
||||||
|
<!-- Allowlist related -->
|
||||||
|
<string name="allowlist_backup_success">Allowlist backup successful</string>
|
||||||
|
<string name="allowlist_backup_failed">Allowlist backup failed: %1$s</string>
|
||||||
|
<string name="allowlist_restore_confirm_title">Confirm Allowlist Restoration</string>
|
||||||
|
<string name="allowlist_restore_confirm_message">This operation will overwrite the current allowlist. Continue?</string>
|
||||||
|
<string name="allowlist_restore_success">Allowlist restored successfully</string>
|
||||||
|
<string name="allowlist_restore_failed">Allowlist restore failed: %1$s</string>
|
||||||
|
<string name="backup_allowlist">Backup Allowlist</string>
|
||||||
|
<string name="restore_allowlist">Restore Allowlist</string>
|
||||||
|
<string name="settings_custom_background">Custom App Background</string>
|
||||||
|
<string name="settings_custom_background_summary">Select an image as background</string>
|
||||||
|
<string name="settings_card_alpha">Navigation bar transparency</string>
|
||||||
|
<string name="settings_restore_default">Restore default</string>
|
||||||
|
<string name="home_android_version">Android version</string>
|
||||||
|
<string name="home_device_model">Device model</string>
|
||||||
|
<string name="su_not_allowed">Granting superuser to %s is not allowed</string>
|
||||||
|
<string name="settings_disable_su">Disable su compatibility</string>
|
||||||
|
<string name="settings_disable_su_summary">Temporarily disable any applications from obtaining root privileges via the su command (existing root processes will not be affected).</string>
|
||||||
|
<string name="using_mksu_manager">You are using the SukiSU Beta manager</string>
|
||||||
|
<string name="module_install_multiple_confirm">Are you sure you want to install the selected %d modules?</string>
|
||||||
|
<string name="module_install_multiple_confirm_with_names">Sure you want to install the following %1$d modules? \n\n%2$s</string>
|
||||||
|
<string name="more_settings">More settings</string>
|
||||||
|
<string name="selinux">SELinux</string>
|
||||||
|
<string name="selinux_enabled">Enabled</string>
|
||||||
|
<string name="selinux_disabled">Disabled</string>
|
||||||
|
<string name="simple_mode">Simplicity mode</string>
|
||||||
|
<string name="simple_mode_summary">Hides unnecessary cards when turned on</string>
|
||||||
|
<string name="hide_kernel_kernelsu_version">Hide kernel version</string>
|
||||||
|
<string name="hide_kernel_kernelsu_version_summary">Hide kernel version</string>
|
||||||
|
<string name="hide_other_info">Hide other info</string>
|
||||||
|
<string name="hide_other_info_summary">Hides information about the number of super users, modules and KPM modules on the home page</string>
|
||||||
|
<string name="hide_susfs_status">Hide SuSFS status</string>
|
||||||
|
<string name="hide_susfs_status_summary">Hide SuSFS status information on the home page</string>
|
||||||
|
<string name="hide_link_card">Hide Link Card Status</string>
|
||||||
|
<string name="hide_link_card_summary">Hide link card information on the home page</string>
|
||||||
|
<string name="theme_mode">Theme</string>
|
||||||
|
<string name="theme_follow_system">Follow system</string>
|
||||||
|
<string name="theme_light">Light</string>
|
||||||
|
<string name="theme_dark">Dark</string>
|
||||||
|
<string name="manual_hook">Manual Hook</string>
|
||||||
|
<string name="dynamic_color_title">Dynamic colours</string>
|
||||||
|
<string name="dynamic_color_summary">Dynamic colours using system themes</string>
|
||||||
|
<string name="choose_theme_color">Choose a theme colour</string>
|
||||||
|
<string name="color_default">Blue</string>
|
||||||
|
<string name="color_green">Green</string>
|
||||||
|
<string name="color_purple">Purple</string>
|
||||||
|
<string name="color_orange">Orange</string>
|
||||||
|
<string name="color_pink">Pink</string>
|
||||||
|
<string name="color_gray">Gray</string>
|
||||||
|
<string name="color_yellow">Yellow</string>
|
||||||
|
<string name="flash_option">Brush Options</string>
|
||||||
|
<string name="flash_option_tip">Select the file to be flashed</string>
|
||||||
|
<string name="horizon_kernel">Install Anykernel3</string>
|
||||||
|
<string name="horizon_kernel_summary">Flash AnyKernel3 kernel file</string>
|
||||||
|
<string name="root_required">Requires root privileges</string>
|
||||||
|
<string name="copy_failed">File Copy Failure</string>
|
||||||
|
<string name="reboot_complete_title">Scrubbing complete</string>
|
||||||
|
<string name="reboot_complete_msg">Whether to reboot immediately?</string>
|
||||||
|
<string name="yes">Yes</string>
|
||||||
|
<string name="no">No</string>
|
||||||
|
<string name="failed_reboot">Reboot Failed</string>
|
||||||
|
<string name="batch_authorization">empower</string>
|
||||||
|
<string name="batch_cancel_authorization">withdraw</string>
|
||||||
|
<string name="backup">Backup</string>
|
||||||
|
<string name="kpm_title">KPM</string>
|
||||||
|
<string name="kpm_empty">No installed kernel modules at this time</string>
|
||||||
|
<string name="kpm_version">Version</string>
|
||||||
|
<string name="kpm_author">Author</string>
|
||||||
|
<string name="kpm_uninstall">Uninstall</string>
|
||||||
|
<string name="kpm_uninstall_success">Uninstalled successfully</string>
|
||||||
|
<string name="kpm_uninstall_failed">Failed to uninstall</string>
|
||||||
|
<string name="kpm_install">Install</string>
|
||||||
|
<string name="kpm_install_success">Load of kpm module successful</string>
|
||||||
|
<string name="kpm_install_failed">Load of kpm module failed</string>
|
||||||
|
<string name="kpm_args">Parameters</string>
|
||||||
|
<string name="kpm_control">Execute</string>
|
||||||
|
<string name="home_kpm_version">KPM Version</string>
|
||||||
|
<string name="close_notice">Close</string>
|
||||||
|
<string name="kernel_module_notice">The following kernel module functions were developed by KernelPatch and modified to include the kernel module functions of SukiSU Ultra</string>
|
||||||
|
<string name="home_ContributionCard_kernelsu">SukiSU Ultra Look forward to</string>
|
||||||
|
<string name="kpm_control_success">Success</string>
|
||||||
|
<string name="kpm_control_failed">Failed</string>
|
||||||
|
<string name="home_click_to_ContributionCard_kernelsu">SukiSU Ultra will be a relatively independent branch of KSU in the future, but we still appreciate the official KernelSU and MKSU etc. for their contributions!</string>
|
||||||
|
<string name="not_supported">Unsupported</string>
|
||||||
|
<string name="supported">Supported</string>
|
||||||
|
<string name="home_kpm_module">"Number of KPM modules: %d "</string>
|
||||||
|
<string name="kpm_invalid_file">Invalid KPM file</string>
|
||||||
|
<string name="kernel_patched">Kernel not patched</string>
|
||||||
|
<string name="kernel_not_enabled">Kernel not configured</string>
|
||||||
|
<string name="custom_settings">Custom settings</string>
|
||||||
|
<string name="kpm_install_mode">KPM Install</string>
|
||||||
|
<string name="kpm_install_mode_load">Load</string>
|
||||||
|
<string name="kpm_install_mode_embed">Embed</string>
|
||||||
|
<string name="kpm_install_mode_description">Please select: %1\$s Module Installation Mode \n\nLoad: Temporarily load the module \nEmbedded: Permanently install into the system</string>
|
||||||
|
<string name="log_failed_to_check_module_file">Failed to check module file existence</string>
|
||||||
|
<string name="snackbar_failed_to_check_module_file">Unable to check if module file exists</string>
|
||||||
|
<string name="confirm_uninstall_title">Confirm uninstallation</string>
|
||||||
|
<string name="confirm_uninstall_confirm">Uninstall</string>
|
||||||
|
<string name="confirm_uninstall_dismiss">Cancel</string>
|
||||||
|
<string name="theme_color">Theme Color</string>
|
||||||
|
<string name="invalid_file_type">Incorrect file type! Please select .kpm file.</string>
|
||||||
|
<string name="confirm_uninstall_title_with_filename">Uninstall</string>
|
||||||
|
<string name="confirm_uninstall_content">The following KPM will be uninstalled: %s</string>
|
||||||
|
<string name="settings_susfs_toggle_summary">Disable kprobe hooks created by KernelSU, using inline hooks instead, which is similar to non-GKI kernel hooking method.</string>
|
||||||
|
<string name="image_editor_title">Adjust background image</string>
|
||||||
|
<string name="image_editor_hint">Use two fingers to zoom the image, and one finger to drag it to adjust the position</string>
|
||||||
|
<string name="background_image_error">Could not load image</string>
|
||||||
|
<string name="reprovision">Reprovision</string>
|
||||||
|
<!-- Kernel Flash Progress Related -->
|
||||||
|
<string name="horizon_flash_title">Kernel Flashing</string>
|
||||||
|
<string name="horizon_logs_label">Logs:</string>
|
||||||
|
<string name="horizon_flash_complete">Flash Complete</string>
|
||||||
|
<!-- Flash Status Related -->
|
||||||
|
<string name="horizon_preparing">Preparing…</string>
|
||||||
|
<string name="horizon_cleaning_files">Cleaning files…</string>
|
||||||
|
<string name="horizon_copying_files">Copying files…</string>
|
||||||
|
<string name="horizon_extracting_tool">Extracting flash tool…</string>
|
||||||
|
<string name="horizon_patching_script">Patching flash script…</string>
|
||||||
|
<string name="horizon_flashing">Flashing kernel…</string>
|
||||||
|
<string name="horizon_flash_complete_status">Flash completed</string>
|
||||||
|
<!-- Slot selection related strings -->
|
||||||
|
<string name="select_slot_title">Select Flash Slot</string>
|
||||||
|
<string name="select_slot_description">Please select the target slot for flashing boot</string>
|
||||||
|
<string name="slot_a">Slot A</string>
|
||||||
|
<string name="slot_b">Slot B</string>
|
||||||
|
<string name="selected_slot">Selected slot: %1$s</string>
|
||||||
|
<string name="horizon_getting_original_slot">Getting the original slot</string>
|
||||||
|
<string name="horizon_setting_target_slot">Setting the specified slot</string>
|
||||||
|
<string name="horizon_restoring_original_slot">Restore Default Slot</string>
|
||||||
|
<string name="current_slot">Current Slot:%1$s </string>
|
||||||
|
<!-- Error Messages -->
|
||||||
|
<string name="horizon_copy_failed">Copy failed</string>
|
||||||
|
<string name="horizon_unknown_error">Unknown error</string>
|
||||||
|
<string name="flash_failed_message">Flash failed</string>
|
||||||
|
<!-- lkm/gki install -->
|
||||||
|
<string name="Lkm_install_methods">LKM repair/installation</string>
|
||||||
|
<string name="GKI_install_methods">Flashing AnyKernel3</string>
|
||||||
|
<string name="kernel_version_log">Kernel version:%1$s</string>
|
||||||
|
<string name="tool_version_log">Using the patching tool:%1$s</string>
|
||||||
|
<string name="configuration">Configure</string>
|
||||||
|
<string name="app_settings">Application Settings</string>
|
||||||
|
<string name="tools">Tools</string>
|
||||||
|
<string name="currently_selected">Currently</string>
|
||||||
|
<!-- String resources used in SuperUser -->
|
||||||
|
<string name="clear">Removals</string>
|
||||||
|
<string name="apps_with_root">Applications with root privileges</string>
|
||||||
|
<string name="apps_with_custom_profile">Applications with customized configurations</string>
|
||||||
|
<string name="other_apps">Applications with unchanged defaults</string>
|
||||||
|
<string name="no_apps_found">Application not found</string>
|
||||||
|
<string name="selinux_enabled_toast">SELinux Enabled</string>
|
||||||
|
<string name="selinux_disabled_toast">SELinux Disabled</string>
|
||||||
|
<string name="selinux_change_failed">SELinux Status change failed</string>
|
||||||
|
<string name="advanced_settings">Advanced Settings</string>
|
||||||
|
<string name="appearance_settings">Customize the toolbar</string>
|
||||||
|
<string name="back">Comeback</string>
|
||||||
|
<string name="expand">Be in full swing</string>
|
||||||
|
<string name="collapse">put away</string>
|
||||||
|
<string name="susfs_enabled">SuSFS enabled</string>
|
||||||
|
<string name="susfs_disabled">SuSFS disabled</string>
|
||||||
|
<string name="background_set_success">Background set successfully</string>
|
||||||
|
<string name="background_removed">Removed custom backgrounds</string>
|
||||||
|
<string name="root_require_for_install">Requires root privileges</string>
|
||||||
|
<!-- KPM display settings -->
|
||||||
|
<string name="show_kpm_info">Display KPM Function</string>
|
||||||
|
<string name="show_kpm_info_summary">Display KPM information and Function in home and bottom bar (Need to reopen the app)</string>
|
||||||
|
<!-- Webui X settings -->
|
||||||
|
<string name="use_webuix">Select the WebUI engine to use</string>
|
||||||
|
<string name="engine_auto_select">Automatic Selection</string>
|
||||||
|
<string name="engine_force_webuix">Force the use of WebUI X</string>
|
||||||
|
<string name="engine_force_ksu">Mandatory use of KSU WebUI</string>
|
||||||
|
<string name="use_webuix_eruda">Inject Eruda into WebUI X</string>
|
||||||
|
<string name="use_webuix_eruda_summary">Inject a debug console into WebUI X to make debugging easier. Requires web debugging to be on.</string>
|
||||||
|
<!-- DPI setting related strings -->
|
||||||
|
<string name="dpi_settings">DPI setting</string>
|
||||||
|
<string name="app_dpi_title">Applied DPI</string>
|
||||||
|
<string name="app_dpi_summary">Adjust the screen display density for the current application only</string>
|
||||||
|
<string name="dpi_size_small">Small </string>
|
||||||
|
<string name="dpi_size_medium">Medium </string>
|
||||||
|
<string name="dpi_size_large">Big</string>
|
||||||
|
<string name="dpi_size_extra_large">oversize</string>
|
||||||
|
<string name="dpi_size_custom">customizable</string>
|
||||||
|
<string name="dpi_apply_settings">Applying DPI settings</string>
|
||||||
|
<string name="dpi_confirm_title">Confirm DPI change</string>
|
||||||
|
<string name="dpi_confirm_message">Are you sure you want to change the application DPI from %1$d to %2$d?</string>
|
||||||
|
<string name="dpi_confirm_summary">Application needs to be restarted to apply the new DPI settings, does not affect the system status bar or other applications</string>
|
||||||
|
<string name="dpi_applied_success">DPI has been set to %1$d, effective after restarting the application</string>
|
||||||
|
<!-- Language settings related strings -->
|
||||||
|
<string name="language_setting">App Language</string>
|
||||||
|
<string name="language_follow_system">Follow System</string>
|
||||||
|
<string name="language_changed">Language changed, restarting to apply changes</string>
|
||||||
|
<string name="settings_card_dim">Card Darkness Adjustment</string>
|
||||||
|
<!-- Super User Related -->
|
||||||
|
<string name="scroll_to_top">Top</string>
|
||||||
|
<string name="scroll_to_bottom">Bottom</string>
|
||||||
|
<string name="scroll_to_top_description">Scroll to top</string>
|
||||||
|
<string name="scroll_to_bottom_description">Scroll to the bottom</string>
|
||||||
|
<string name="authorized">authorized</string>
|
||||||
|
<string name="unauthorized">unauthorized</string>
|
||||||
|
<string name="selected">Selected</string>
|
||||||
|
<string name="select">option</string>
|
||||||
|
<string name="profile_umount_modules_disable">Disable custom uninstallation module</string>
|
||||||
|
<!-- Flash related -->
|
||||||
|
<string name="error_code">error code</string>
|
||||||
|
<string name="check_log">Please check the log</string>
|
||||||
|
<string name="installing_module">Module being installed %1$d/%2$d</string>
|
||||||
|
<string name="module_failed_count">%d Failed to install a new module</string>
|
||||||
|
<string name="module_download_error">Module download failed</string>
|
||||||
|
<string name="kernel_flashing">Kernel Flashing</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1,5 +1,66 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
|
<string name="home">Početna</string>
|
||||||
|
<string name="home_not_installed">Nije instalirano</string>
|
||||||
|
<string name="home_click_to_install">Kliknite da instalirate</string>
|
||||||
|
<string name="home_working">Radi</string>
|
||||||
|
<string name="home_working_version">Verzija: %d</string>
|
||||||
|
<string name="home_superuser_count">Superkorisnici: %d</string>
|
||||||
|
<string name="home_module_count">Module: %d</string>
|
||||||
|
<string name="home_unsupported">Nepodržano</string>
|
||||||
|
<string name="home_unsupported_reason">KernelSU samo podržava GKI kernele sad</string>
|
||||||
|
<string name="home_kernel">Kernel</string>
|
||||||
|
<string name="home_susfs">SuSFS: %s</string>
|
||||||
|
<string name="home_susfs_version">SuSFS Version</string>
|
||||||
|
<string name="home_susfs_sus_su">SuS SU</string>
|
||||||
|
<string name="home_manager_version">Verzija Upravitelja</string>
|
||||||
|
<string name="home_fingerprint">Otisak prsta</string>
|
||||||
|
<string name="home_selinux_status">SELinux stanje</string>
|
||||||
|
<string name="selinux_status_disabled">Isključeno</string>
|
||||||
|
<string name="selinux_status_enforcing">U Provođenju</string>
|
||||||
|
<string name="selinux_status_permissive">Permisivno</string>
|
||||||
|
<string name="selinux_status_unknown">Nepoznato</string>
|
||||||
|
<string name="superuser">Superkorisnik</string>
|
||||||
|
<string name="module_failed_to_enable">Neuspješno uključivanje module: %s</string>
|
||||||
|
<string name="module_failed_to_disable">Neuspješno isključivanje module: %s</string>
|
||||||
|
<string name="module_empty">Nema instaliranih modula</string>
|
||||||
|
<string name="module">Modula</string>
|
||||||
|
<string name="module_sort_action_first">Sort (Action first)</string>
|
||||||
|
<string name="module_sort_enabled_first">Sort (Enabled first)</string>
|
||||||
|
<string name="uninstall">Deinstalirajte</string>
|
||||||
|
<string name="restore">Restore</string>
|
||||||
|
<string name="module_install">Instalirajte</string>
|
||||||
|
<string name="install">Instalirajte</string>
|
||||||
|
<string name="reboot">Ponovo pokrenite</string>
|
||||||
|
<string name="settings">Podešavanja</string>
|
||||||
|
<string name="reboot_userspace">Lagano Ponovo pokretanje</string>
|
||||||
|
<string name="reboot_recovery">Ponovo pokrenite u Oporavu</string>
|
||||||
|
<string name="reboot_bootloader">Ponovo pokrenite u Pogonski Učitavatelj</string>
|
||||||
|
<string name="reboot_download">Ponovo pokrenite u Preuzimanje</string>
|
||||||
|
<string name="reboot_edl">Ponovo pokrenite u EDL</string>
|
||||||
|
<string name="about">O</string>
|
||||||
|
<string name="module_uninstall_confirm">Jeste li sigurni da želite deinstalirati modulu %s\?</string>
|
||||||
|
<string name="module_uninstall_success">%s deinstalirana</string>
|
||||||
|
<string name="module_uninstall_failed">Neuspješna deinstalacija: %s</string>
|
||||||
|
<string name="module_version">Verzija</string>
|
||||||
|
<string name="module_author">Autor</string>
|
||||||
|
<string name="refresh">Osvježi</string>
|
||||||
|
<string name="show_system_apps">Prikažite sistemske aplikacije</string>
|
||||||
|
<string name="hide_system_apps">Sakrijte sistemske aplikacije</string>
|
||||||
|
<string name="send_log">Pošaljite Izvještaj</string>
|
||||||
|
<string name="safe_mode">Sigurnosni mod</string>
|
||||||
|
<string name="reboot_to_apply">Ponovo pokrenite da bi proradilo</string>
|
||||||
|
<string name="module_magisk_conflict">Module su isključene jer je u sukobu sa Magisk-om!</string>
|
||||||
|
<string name="home_learn_kernelsu">Naučite KernelSU</string>
|
||||||
|
<string name="home_learn_kernelsu_url">https://kernelsu.org/guide/what-is-kernelsu.html</string>
|
||||||
|
<string name="home_click_to_learn_kernelsu">Naučite kako da instalirate KernelSU i da koristite module</string>
|
||||||
|
<string name="home_support_title">Podržite Nas</string>
|
||||||
|
<string name="home_support_content">KernelSU je, i uvijek če biti, besplatan, i otvorenog izvora. Možete nam međutim pokazati da vas je briga s time da napravite donaciju.</string>
|
||||||
|
<string name="about_source_code"><![CDATA[View source code at %1$s<br/>Join our %2$s channel]]></string>
|
||||||
|
<string name="profile_default">Zadano</string>
|
||||||
|
<string name="profile_template">Šablon</string>
|
||||||
|
<string name="profile_custom">Prilagođeno</string>
|
||||||
|
<string name="profile_name">Naziv profila</string>
|
||||||
<string name="profile_namespace">Imenski prostor nosača</string>
|
<string name="profile_namespace">Imenski prostor nosača</string>
|
||||||
<string name="profile_namespace_inherited">Naslijeđen</string>
|
<string name="profile_namespace_inherited">Naslijeđen</string>
|
||||||
<string name="profile_namespace_global">Globalan</string>
|
<string name="profile_namespace_global">Globalan</string>
|
||||||
@@ -9,9 +70,13 @@
|
|||||||
<string name="profile_selinux_context">SELinux kontekst</string>
|
<string name="profile_selinux_context">SELinux kontekst</string>
|
||||||
<string name="profile_umount_modules">Umount module</string>
|
<string name="profile_umount_modules">Umount module</string>
|
||||||
<string name="failed_to_update_app_profile">Ažuriranje Profila Aplikacije za %s nije uspjelo</string>
|
<string name="failed_to_update_app_profile">Ažuriranje Profila Aplikacije za %s nije uspjelo</string>
|
||||||
|
<string name="require_kernel_version" formatted="false">The current KernelSU version %d is too low for the manager to work properly. Please upgrade to version %d or higher!</string>
|
||||||
<string name="settings_umount_modules_default">Umount module po zadanom</string>
|
<string name="settings_umount_modules_default">Umount module po zadanom</string>
|
||||||
<string name="settings_umount_modules_default_summary">Globalna zadana vrijednost za \"Umount module\" u Profilima Aplikacije. Ako je omogućeno, uklonit će sve izmjene modula na sistemu za aplikacije koje nemaju postavljen Profil.</string>
|
<string name="settings_umount_modules_default_summary">Globalna zadana vrijednost za \"Umount module\" u Profilima Aplikacije. Ako je omogućeno, uklonit će sve izmjene modula na sistemu za aplikacije koje nemaju postavljen Profil.</string>
|
||||||
|
<string name="settings_susfs_toggle">Disable kprobe hooks</string>
|
||||||
<string name="profile_umount_modules_summary">Uključivanjem ove opcije omogućit će KernelSU-u da vrati sve izmjenute datoteke od strane modula za ovu aplikaciju.</string>
|
<string name="profile_umount_modules_summary">Uključivanjem ove opcije omogućit će KernelSU-u da vrati sve izmjenute datoteke od strane modula za ovu aplikaciju.</string>
|
||||||
|
<string name="profile_selinux_domain">Domena</string>
|
||||||
|
<string name="profile_selinux_rules">Pravila</string>
|
||||||
<string name="module_update">Ažuriranje</string>
|
<string name="module_update">Ažuriranje</string>
|
||||||
<string name="module_downloading">Skidanje module: %s</string>
|
<string name="module_downloading">Skidanje module: %s</string>
|
||||||
<string name="module_start_downloading">Započnite sa skidanjem: %s</string>
|
<string name="module_start_downloading">Započnite sa skidanjem: %s</string>
|
||||||
@@ -19,62 +84,288 @@
|
|||||||
<string name="launch_app">Pokrenite</string>
|
<string name="launch_app">Pokrenite</string>
|
||||||
<string name="force_stop_app" formatted="false">Prisilno Zaustavite</string>
|
<string name="force_stop_app" formatted="false">Prisilno Zaustavite</string>
|
||||||
<string name="restart_app">Resetujte</string>
|
<string name="restart_app">Resetujte</string>
|
||||||
<string name="selinux_status_enforcing">U Provođenju</string>
|
|
||||||
<string name="home">Početna</string>
|
|
||||||
<string name="home_not_installed">Nije instalirano</string>
|
|
||||||
<string name="home_click_to_install">Kliknite da instalirate</string>
|
|
||||||
<string name="home_superuser_count">Superkorisnici: %d</string>
|
|
||||||
<string name="home_module_count">Module: %d</string>
|
|
||||||
<string name="home_unsupported">Nepodržano</string>
|
|
||||||
<string name="home_unsupported_reason">KernelSU samo podržava GKI kernele sad</string>
|
|
||||||
<string name="home_manager_version">Verzija Upravitelja</string>
|
|
||||||
<string name="home_fingerprint">Otisak prsta</string>
|
|
||||||
<string name="home_selinux_status">SELinux stanje</string>
|
|
||||||
<string name="module_install">Instalirajte</string>
|
|
||||||
<string name="install">Instalirajte</string>
|
|
||||||
<string name="reboot">Ponovo pokrenite</string>
|
|
||||||
<string name="settings">Podešavanja</string>
|
|
||||||
<string name="module_version">Verzija</string>
|
|
||||||
<string name="module_author">Autor</string>
|
|
||||||
<string name="refresh">Osvježi</string>
|
|
||||||
<string name="show_system_apps">Prikažite sistemske aplikacije</string>
|
|
||||||
<string name="hide_system_apps">Sakrijte sistemske aplikacije</string>
|
|
||||||
<string name="safe_mode">Sigurnosni mod</string>
|
|
||||||
<string name="reboot_to_apply">Ponovo pokrenite da bi proradilo</string>
|
|
||||||
<string name="module_magisk_conflict">Module su isključene jer je u sukobu sa Magisk-om!</string>
|
|
||||||
<string name="home_learn_kernelsu_url">https://kernelsu.org/guide/what-is-kernelsu.html</string>
|
|
||||||
<string name="home_click_to_learn_kernelsu">Naučite kako da instalirate KernelSU i da koristite module</string>
|
|
||||||
<string name="home_support_title">Podržite Nas</string>
|
|
||||||
<string name="send_log">Pošaljite Izvještaj</string>
|
|
||||||
<string name="home_learn_kernelsu">Naučite KernelSU</string>
|
|
||||||
<string name="profile_selinux_domain">Domena</string>
|
|
||||||
<string name="profile_selinux_rules">Pravila</string>
|
|
||||||
<string name="failed_to_update_sepolicy">Neuspješno ažuriranje SELinux pravila za: %s</string>
|
<string name="failed_to_update_sepolicy">Neuspješno ažuriranje SELinux pravila za: %s</string>
|
||||||
<string name="home_working">Radi</string>
|
<string name="module_changelog">Changelog</string>
|
||||||
<string name="home_working_version">Verzija: %d</string>
|
<string name="settings_profile_template">App Profile Template</string>
|
||||||
<string name="home_kernel">Kernel</string>
|
<string name="settings_profile_template_summary">Manage local and online template of App Profile</string>
|
||||||
<string name="selinux_status_permissive">Permisivno</string>
|
<string name="app_profile_template_create">Create template</string>
|
||||||
<string name="uninstall">Deinstalirajte</string>
|
<string name="app_profile_template_edit">Edit template</string>
|
||||||
<string name="selinux_status_unknown">Nepoznato</string>
|
<string name="app_profile_template_id">ID</string>
|
||||||
<string name="module_empty">Nema instaliranih modula</string>
|
<string name="app_profile_template_id_invalid">Invalid template ID</string>
|
||||||
<string name="superuser">Superkorisnik</string>
|
<string name="app_profile_template_name">Name</string>
|
||||||
<string name="module">Modula</string>
|
<string name="app_profile_template_description">Description</string>
|
||||||
<string name="reboot_bootloader">Ponovo pokrenite u Pogonski Učitavatelj</string>
|
<string name="app_profile_template_save">Save</string>
|
||||||
<string name="reboot_recovery">Ponovo pokrenite u Oporavu</string>
|
<string name="app_profile_template_delete">Delete</string>
|
||||||
<string name="module_uninstall_success">%s deinstalirana</string>
|
<string name="app_profile_template_view">View template</string>
|
||||||
<string name="reboot_userspace">Lagano Ponovo pokretanje</string>
|
<string name="app_profile_template_readonly">Read only</string>
|
||||||
<string name="module_failed_to_enable">Neuspješno uključivanje module: %s</string>
|
<string name="app_profile_template_id_exist">Template ID already exists!</string>
|
||||||
<string name="reboot_download">Ponovo pokrenite u Preuzimanje</string>
|
<string name="app_profile_import_export">Import/Export</string>
|
||||||
<string name="module_failed_to_disable">Neuspješno isključivanje module: %s</string>
|
<string name="app_profile_import_from_clipboard">Import from clipboard</string>
|
||||||
<string name="reboot_edl">Ponovo pokrenite u EDL</string>
|
<string name="app_profile_export_to_clipboard">Export to clipboard</string>
|
||||||
<string name="module_uninstall_failed">Neuspješna deinstalacija: %s</string>
|
<string name="app_profile_template_export_empty">Cannot find local template to export!</string>
|
||||||
<string name="selinux_status_disabled">Isključeno</string>
|
<string name="app_profile_template_import_success">Imported successfully</string>
|
||||||
<string name="about">O</string>
|
<string name="app_profile_template_sync">Sync online templates</string>
|
||||||
<string name="module_uninstall_confirm">Jeste li sigurni da želite deinstalirati modulu %s\?</string>
|
<string name="app_profile_template_save_failed">Failed to save template</string>
|
||||||
<string name="home_support_content">KernelSU je, i uvijek če biti, besplatan, i otvorenog izvora. Možete nam međutim pokazati da vas je briga s time da napravite donaciju.</string>
|
<string name="app_profile_template_import_empty">Clipboard is empty!</string>
|
||||||
<string name="profile_default">Zadano</string>
|
<string name="module_changelog_failed">Fetch changelog failed: %s</string>
|
||||||
<string name="profile_template">Šablon</string>
|
<string name="settings_check_update">Check update</string>
|
||||||
<string name="profile_custom">Prilagođeno</string>
|
<string name="settings_check_update_summary">Automatically check for updates when opening the app</string>
|
||||||
<string name="profile_name">Naziv profila</string>
|
<string name="grant_root_failed">Failed to grant root!</string>
|
||||||
|
<string name="action">Action</string>
|
||||||
|
<string name="open">Open</string>
|
||||||
|
<string name="close">Close</string>
|
||||||
|
<string name="enable_web_debugging">Enable WebView debugging</string>
|
||||||
|
<string name="enable_web_debugging_summary">Can be used to debug WebUI. Please enable only when needed.</string>
|
||||||
|
<string name="direct_install">Direct install (Recommended)</string>
|
||||||
|
<string name="select_file">Select a image that needs to be patched</string>
|
||||||
|
<string name="install_inactive_slot">Install to inactive slot (After OTA)</string>
|
||||||
|
<string name="install_inactive_slot_warning">Your device will be **FORCED** to boot to the current inactive slot after a reboot!\nOnly use this option after OTA is done.\nContinue?</string>
|
||||||
|
<string name="install_next">Next</string>
|
||||||
|
<string name="select_file_tip">%1$s partition image is recommended</string>
|
||||||
|
<string name="select_kmi">Select KMI</string>
|
||||||
|
<string name="settings_uninstall">Uninstall</string>
|
||||||
|
<string name="settings_uninstall_temporary">Uninstall temporarily</string>
|
||||||
|
<string name="settings_uninstall_permanent">Uninstall permanently</string>
|
||||||
|
<string name="settings_restore_stock_image">Restore stock image</string>
|
||||||
|
<string name="settings_uninstall_temporary_message">Temporarily uninstall KernelSU, restore to original state after next reboot.</string>
|
||||||
|
<string name="settings_uninstall_permanent_message">Uninstalling KernelSU (Root and all modules) completely and permanently.</string>
|
||||||
|
<string name="settings_restore_stock_image_message">Restore the stock factory image (If a backup exists), usually used before OTA; if you need to uninstall KernelSU, please use \"Uninstall permanently\".</string>
|
||||||
|
<string name="flashing">Flashing</string>
|
||||||
|
<string name="flash_success">Flash success</string>
|
||||||
|
<string name="flash_failed">Flash failed</string>
|
||||||
|
<string name="selected_lkm">Selected LKM: %s</string>
|
||||||
<string name="save_log">Sačuvaj Dnevnike</string>
|
<string name="save_log">Sačuvaj Dnevnike</string>
|
||||||
|
<string name="log_saved">Logs saved</string>
|
||||||
|
<string name="status_supported">Supported</string>
|
||||||
|
<string name="status_not_supported">Not Supported</string>
|
||||||
|
<string name="status_unknown">Unknown</string>
|
||||||
|
<string name="sus_su_mode">SuS SU mode:</string>
|
||||||
|
<!-- Module related -->
|
||||||
|
<string name="module_install_confirm">confirm install module %1$s?</string>
|
||||||
|
<string name="unknown_module">unknown module</string>
|
||||||
|
<!-- Restore related -->
|
||||||
|
<string name="restore_confirm_title">Confirm Module Restoration</string>
|
||||||
|
<string name="restore_confirm_message">This operation will overwrite all existing modules. Continue?</string>
|
||||||
|
<string name="confirm">Confirm</string>
|
||||||
|
<string name="cancel">Cancel</string>
|
||||||
|
<!-- Backup related -->
|
||||||
|
<string name="backup_success">Backup successful (tar.gz)</string>
|
||||||
|
<string name="backup_failed">Backup failed: %1$s</string>
|
||||||
|
<string name="backup_modules">backup modules</string>
|
||||||
|
<string name="restore_modules">restore modules</string>
|
||||||
|
<!-- Restore related messages -->
|
||||||
|
<string name="restore_success">Modules restored successfully, restart required</string>
|
||||||
|
<string name="restore_failed">Restore failed: %1$s</string>
|
||||||
|
<string name="restart_now">Restart Now</string>
|
||||||
|
<string name="unknown_error">Unknown error</string>
|
||||||
|
<!-- Command related -->
|
||||||
|
<string name="command_execution_failed">Command execution failed: %1$s</string>
|
||||||
|
<!-- Allowlist related -->
|
||||||
|
<string name="allowlist_backup_success">Allowlist backup successful</string>
|
||||||
|
<string name="allowlist_backup_failed">Allowlist backup failed: %1$s</string>
|
||||||
|
<string name="allowlist_restore_confirm_title">Confirm Allowlist Restoration</string>
|
||||||
|
<string name="allowlist_restore_confirm_message">This operation will overwrite the current allowlist. Continue?</string>
|
||||||
|
<string name="allowlist_restore_success">Allowlist restored successfully</string>
|
||||||
|
<string name="allowlist_restore_failed">Allowlist restore failed: %1$s</string>
|
||||||
|
<string name="backup_allowlist">Backup Allowlist</string>
|
||||||
|
<string name="restore_allowlist">Restore Allowlist</string>
|
||||||
|
<string name="settings_custom_background">Custom App Background</string>
|
||||||
|
<string name="settings_custom_background_summary">Select an image as background</string>
|
||||||
|
<string name="settings_card_alpha">Navigation bar transparency</string>
|
||||||
|
<string name="settings_restore_default">Restore default</string>
|
||||||
|
<string name="home_android_version">Android version</string>
|
||||||
|
<string name="home_device_model">Device model</string>
|
||||||
|
<string name="su_not_allowed">Granting superuser to %s is not allowed</string>
|
||||||
|
<string name="settings_disable_su">Disable su compatibility</string>
|
||||||
|
<string name="settings_disable_su_summary">Temporarily disable any applications from obtaining root privileges via the su command (existing root processes will not be affected).</string>
|
||||||
|
<string name="using_mksu_manager">You are using the SukiSU Beta manager</string>
|
||||||
|
<string name="module_install_multiple_confirm">Are you sure you want to install the selected %d modules?</string>
|
||||||
|
<string name="module_install_multiple_confirm_with_names">Sure you want to install the following %1$d modules? \n\n%2$s</string>
|
||||||
|
<string name="more_settings">More settings</string>
|
||||||
|
<string name="selinux">SELinux</string>
|
||||||
|
<string name="selinux_enabled">Enabled</string>
|
||||||
|
<string name="selinux_disabled">Disabled</string>
|
||||||
|
<string name="simple_mode">Simplicity mode</string>
|
||||||
|
<string name="simple_mode_summary">Hides unnecessary cards when turned on</string>
|
||||||
|
<string name="hide_kernel_kernelsu_version">Hide kernel version</string>
|
||||||
|
<string name="hide_kernel_kernelsu_version_summary">Hide kernel version</string>
|
||||||
|
<string name="hide_other_info">Hide other info</string>
|
||||||
|
<string name="hide_other_info_summary">Hides information about the number of super users, modules and KPM modules on the home page</string>
|
||||||
|
<string name="hide_susfs_status">Hide SuSFS status</string>
|
||||||
|
<string name="hide_susfs_status_summary">Hide SuSFS status information on the home page</string>
|
||||||
|
<string name="hide_link_card">Hide Link Card Status</string>
|
||||||
|
<string name="hide_link_card_summary">Hide link card information on the home page</string>
|
||||||
|
<string name="theme_mode">Theme</string>
|
||||||
|
<string name="theme_follow_system">Follow system</string>
|
||||||
|
<string name="theme_light">Light</string>
|
||||||
|
<string name="theme_dark">Dark</string>
|
||||||
|
<string name="manual_hook">Manual Hook</string>
|
||||||
|
<string name="dynamic_color_title">Dynamic colours</string>
|
||||||
|
<string name="dynamic_color_summary">Dynamic colours using system themes</string>
|
||||||
|
<string name="choose_theme_color">Choose a theme colour</string>
|
||||||
|
<string name="color_default">Blue</string>
|
||||||
|
<string name="color_green">Green</string>
|
||||||
|
<string name="color_purple">Purple</string>
|
||||||
|
<string name="color_orange">Orange</string>
|
||||||
|
<string name="color_pink">Pink</string>
|
||||||
|
<string name="color_gray">Gray</string>
|
||||||
|
<string name="color_yellow">Yellow</string>
|
||||||
|
<string name="flash_option">Brush Options</string>
|
||||||
|
<string name="flash_option_tip">Select the file to be flashed</string>
|
||||||
|
<string name="horizon_kernel">Install Anykernel3</string>
|
||||||
|
<string name="horizon_kernel_summary">Flash AnyKernel3 kernel file</string>
|
||||||
|
<string name="root_required">Requires root privileges</string>
|
||||||
|
<string name="copy_failed">File Copy Failure</string>
|
||||||
|
<string name="reboot_complete_title">Scrubbing complete</string>
|
||||||
|
<string name="reboot_complete_msg">Whether to reboot immediately?</string>
|
||||||
|
<string name="yes">Yes</string>
|
||||||
|
<string name="no">No</string>
|
||||||
|
<string name="failed_reboot">Reboot Failed</string>
|
||||||
|
<string name="batch_authorization">empower</string>
|
||||||
|
<string name="batch_cancel_authorization">withdraw</string>
|
||||||
|
<string name="backup">Backup</string>
|
||||||
|
<string name="kpm_title">KPM</string>
|
||||||
|
<string name="kpm_empty">No installed kernel modules at this time</string>
|
||||||
|
<string name="kpm_version">Version</string>
|
||||||
|
<string name="kpm_author">Author</string>
|
||||||
|
<string name="kpm_uninstall">Uninstall</string>
|
||||||
|
<string name="kpm_uninstall_success">Uninstalled successfully</string>
|
||||||
|
<string name="kpm_uninstall_failed">Failed to uninstall</string>
|
||||||
|
<string name="kpm_install">Install</string>
|
||||||
|
<string name="kpm_install_success">Load of kpm module successful</string>
|
||||||
|
<string name="kpm_install_failed">Load of kpm module failed</string>
|
||||||
|
<string name="kpm_args">Parameters</string>
|
||||||
|
<string name="kpm_control">Execute</string>
|
||||||
|
<string name="home_kpm_version">KPM Version</string>
|
||||||
|
<string name="close_notice">Close</string>
|
||||||
|
<string name="kernel_module_notice">The following kernel module functions were developed by KernelPatch and modified to include the kernel module functions of SukiSU Ultra</string>
|
||||||
|
<string name="home_ContributionCard_kernelsu">SukiSU Ultra Look forward to</string>
|
||||||
|
<string name="kpm_control_success">Success</string>
|
||||||
|
<string name="kpm_control_failed">Failed</string>
|
||||||
|
<string name="home_click_to_ContributionCard_kernelsu">SukiSU Ultra will be a relatively independent branch of KSU in the future, but we still appreciate the official KernelSU and MKSU etc. for their contributions!</string>
|
||||||
|
<string name="not_supported">Unsupported</string>
|
||||||
|
<string name="supported">Supported</string>
|
||||||
|
<string name="home_kpm_module">"Number of KPM modules: %d "</string>
|
||||||
|
<string name="kpm_invalid_file">Invalid KPM file</string>
|
||||||
|
<string name="kernel_patched">Kernel not patched</string>
|
||||||
|
<string name="kernel_not_enabled">Kernel not configured</string>
|
||||||
|
<string name="custom_settings">Custom settings</string>
|
||||||
|
<string name="kpm_install_mode">KPM Install</string>
|
||||||
|
<string name="kpm_install_mode_load">Load</string>
|
||||||
|
<string name="kpm_install_mode_embed">Embed</string>
|
||||||
|
<string name="kpm_install_mode_description">Please select: %1\$s Module Installation Mode \n\nLoad: Temporarily load the module \nEmbedded: Permanently install into the system</string>
|
||||||
|
<string name="log_failed_to_check_module_file">Failed to check module file existence</string>
|
||||||
|
<string name="snackbar_failed_to_check_module_file">Unable to check if module file exists</string>
|
||||||
|
<string name="confirm_uninstall_title">Confirm uninstallation</string>
|
||||||
|
<string name="confirm_uninstall_confirm">Uninstall</string>
|
||||||
|
<string name="confirm_uninstall_dismiss">Cancel</string>
|
||||||
|
<string name="theme_color">Theme Color</string>
|
||||||
|
<string name="invalid_file_type">Incorrect file type! Please select .kpm file.</string>
|
||||||
|
<string name="confirm_uninstall_title_with_filename">Uninstall</string>
|
||||||
|
<string name="confirm_uninstall_content">The following KPM will be uninstalled: %s</string>
|
||||||
|
<string name="settings_susfs_toggle_summary">Disable kprobe hooks created by KernelSU, using inline hooks instead, which is similar to non-GKI kernel hooking method.</string>
|
||||||
|
<string name="image_editor_title">Adjust background image</string>
|
||||||
|
<string name="image_editor_hint">Use two fingers to zoom the image, and one finger to drag it to adjust the position</string>
|
||||||
|
<string name="background_image_error">Could not load image</string>
|
||||||
|
<string name="reprovision">Reprovision</string>
|
||||||
|
<!-- Kernel Flash Progress Related -->
|
||||||
|
<string name="horizon_flash_title">Kernel Flashing</string>
|
||||||
|
<string name="horizon_logs_label">Logs:</string>
|
||||||
|
<string name="horizon_flash_complete">Flash Complete</string>
|
||||||
|
<!-- Flash Status Related -->
|
||||||
|
<string name="horizon_preparing">Preparing…</string>
|
||||||
|
<string name="horizon_cleaning_files">Cleaning files…</string>
|
||||||
|
<string name="horizon_copying_files">Copying files…</string>
|
||||||
|
<string name="horizon_extracting_tool">Extracting flash tool…</string>
|
||||||
|
<string name="horizon_patching_script">Patching flash script…</string>
|
||||||
|
<string name="horizon_flashing">Flashing kernel…</string>
|
||||||
|
<string name="horizon_flash_complete_status">Flash completed</string>
|
||||||
|
<!-- Slot selection related strings -->
|
||||||
|
<string name="select_slot_title">Select Flash Slot</string>
|
||||||
|
<string name="select_slot_description">Please select the target slot for flashing boot</string>
|
||||||
|
<string name="slot_a">Slot A</string>
|
||||||
|
<string name="slot_b">Slot B</string>
|
||||||
|
<string name="selected_slot">Selected slot: %1$s</string>
|
||||||
|
<string name="horizon_getting_original_slot">Getting the original slot</string>
|
||||||
|
<string name="horizon_setting_target_slot">Setting the specified slot</string>
|
||||||
|
<string name="horizon_restoring_original_slot">Restore Default Slot</string>
|
||||||
|
<string name="current_slot">Current Slot:%1$s </string>
|
||||||
|
<!-- Error Messages -->
|
||||||
|
<string name="horizon_copy_failed">Copy failed</string>
|
||||||
|
<string name="horizon_unknown_error">Unknown error</string>
|
||||||
|
<string name="flash_failed_message">Flash failed</string>
|
||||||
|
<!-- lkm/gki install -->
|
||||||
|
<string name="Lkm_install_methods">LKM repair/installation</string>
|
||||||
|
<string name="GKI_install_methods">Flashing AnyKernel3</string>
|
||||||
|
<string name="kernel_version_log">Kernel version:%1$s</string>
|
||||||
|
<string name="tool_version_log">Using the patching tool:%1$s</string>
|
||||||
|
<string name="configuration">Configure</string>
|
||||||
|
<string name="app_settings">Application Settings</string>
|
||||||
|
<string name="tools">Tools</string>
|
||||||
|
<string name="currently_selected">Currently</string>
|
||||||
|
<!-- String resources used in SuperUser -->
|
||||||
|
<string name="clear">Removals</string>
|
||||||
|
<string name="apps_with_root">Applications with root privileges</string>
|
||||||
|
<string name="apps_with_custom_profile">Applications with customized configurations</string>
|
||||||
|
<string name="other_apps">Applications with unchanged defaults</string>
|
||||||
|
<string name="no_apps_found">Application not found</string>
|
||||||
|
<string name="selinux_enabled_toast">SELinux Enabled</string>
|
||||||
|
<string name="selinux_disabled_toast">SELinux Disabled</string>
|
||||||
|
<string name="selinux_change_failed">SELinux Status change failed</string>
|
||||||
|
<string name="advanced_settings">Advanced Settings</string>
|
||||||
|
<string name="appearance_settings">Customize the toolbar</string>
|
||||||
|
<string name="back">Comeback</string>
|
||||||
|
<string name="expand">Be in full swing</string>
|
||||||
|
<string name="collapse">put away</string>
|
||||||
|
<string name="susfs_enabled">SuSFS enabled</string>
|
||||||
|
<string name="susfs_disabled">SuSFS disabled</string>
|
||||||
|
<string name="background_set_success">Background set successfully</string>
|
||||||
|
<string name="background_removed">Removed custom backgrounds</string>
|
||||||
|
<string name="root_require_for_install">Requires root privileges</string>
|
||||||
|
<!-- KPM display settings -->
|
||||||
|
<string name="show_kpm_info">Display KPM Function</string>
|
||||||
|
<string name="show_kpm_info_summary">Display KPM information and Function in home and bottom bar (Need to reopen the app)</string>
|
||||||
|
<!-- Webui X settings -->
|
||||||
|
<string name="use_webuix">Select the WebUI engine to use</string>
|
||||||
|
<string name="engine_auto_select">Automatic Selection</string>
|
||||||
|
<string name="engine_force_webuix">Force the use of WebUI X</string>
|
||||||
|
<string name="engine_force_ksu">Mandatory use of KSU WebUI</string>
|
||||||
|
<string name="use_webuix_eruda">Inject Eruda into WebUI X</string>
|
||||||
|
<string name="use_webuix_eruda_summary">Inject a debug console into WebUI X to make debugging easier. Requires web debugging to be on.</string>
|
||||||
|
<!-- DPI setting related strings -->
|
||||||
|
<string name="dpi_settings">DPI setting</string>
|
||||||
|
<string name="app_dpi_title">Applied DPI</string>
|
||||||
|
<string name="app_dpi_summary">Adjust the screen display density for the current application only</string>
|
||||||
|
<string name="dpi_size_small">Small </string>
|
||||||
|
<string name="dpi_size_medium">Medium </string>
|
||||||
|
<string name="dpi_size_large">Big</string>
|
||||||
|
<string name="dpi_size_extra_large">oversize</string>
|
||||||
|
<string name="dpi_size_custom">customizable</string>
|
||||||
|
<string name="dpi_apply_settings">Applying DPI settings</string>
|
||||||
|
<string name="dpi_confirm_title">Confirm DPI change</string>
|
||||||
|
<string name="dpi_confirm_message">Are you sure you want to change the application DPI from %1$d to %2$d?</string>
|
||||||
|
<string name="dpi_confirm_summary">Application needs to be restarted to apply the new DPI settings, does not affect the system status bar or other applications</string>
|
||||||
|
<string name="dpi_applied_success">DPI has been set to %1$d, effective after restarting the application</string>
|
||||||
|
<!-- Language settings related strings -->
|
||||||
|
<string name="language_setting">App Language</string>
|
||||||
|
<string name="language_follow_system">Follow System</string>
|
||||||
|
<string name="language_changed">Language changed, restarting to apply changes</string>
|
||||||
|
<string name="settings_card_dim">Card Darkness Adjustment</string>
|
||||||
|
<!-- Super User Related -->
|
||||||
|
<string name="scroll_to_top">Top</string>
|
||||||
|
<string name="scroll_to_bottom">Bottom</string>
|
||||||
|
<string name="scroll_to_top_description">Scroll to top</string>
|
||||||
|
<string name="scroll_to_bottom_description">Scroll to the bottom</string>
|
||||||
|
<string name="authorized">authorized</string>
|
||||||
|
<string name="unauthorized">unauthorized</string>
|
||||||
|
<string name="selected">Selected</string>
|
||||||
|
<string name="select">option</string>
|
||||||
|
<string name="profile_umount_modules_disable">Disable custom uninstallation module</string>
|
||||||
|
<!-- Flash related -->
|
||||||
|
<string name="error_code">error code</string>
|
||||||
|
<string name="check_log">Please check the log</string>
|
||||||
|
<string name="installing_module">Module being installed %1$d/%2$d</string>
|
||||||
|
<string name="module_failed_count">%d Failed to install a new module</string>
|
||||||
|
<string name="module_download_error">Module download failed</string>
|
||||||
|
<string name="kernel_flashing">Kernel Flashing</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1,80 +1,371 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
|
<string name="home">Hjem</string>
|
||||||
|
<string name="home_not_installed">Ikke installeret</string>
|
||||||
|
<string name="home_click_to_install">Klik for at installere</string>
|
||||||
<string name="home_working">Arbejder</string>
|
<string name="home_working">Arbejder</string>
|
||||||
|
<string name="home_working_version">Version: %d</string>
|
||||||
|
<string name="home_superuser_count">Superbrugere: %d</string>
|
||||||
<string name="home_module_count">Moduler: %d</string>
|
<string name="home_module_count">Moduler: %d</string>
|
||||||
<string name="home_unsupported">Ikke understøttet</string>
|
<string name="home_unsupported">Ikke understøttet</string>
|
||||||
<string name="home_kernel">Kernel</string>
|
|
||||||
<string name="home_unsupported_reason">KernelSU understøtter kun GKI kernels</string>
|
<string name="home_unsupported_reason">KernelSU understøtter kun GKI kernels</string>
|
||||||
|
<string name="home_kernel">Kernel</string>
|
||||||
|
<string name="home_susfs">SuSFS: %s</string>
|
||||||
|
<string name="home_susfs_version">SuSFS Version</string>
|
||||||
|
<string name="home_susfs_sus_su">SuS SU</string>
|
||||||
<string name="home_manager_version">Manager Version</string>
|
<string name="home_manager_version">Manager Version</string>
|
||||||
|
<string name="home_fingerprint">Fingeraftryk</string>
|
||||||
<string name="home_selinux_status">SELinux-status</string>
|
<string name="home_selinux_status">SELinux-status</string>
|
||||||
<string name="selinux_status_disabled">Deaktiveret</string>
|
<string name="selinux_status_disabled">Deaktiveret</string>
|
||||||
<string name="selinux_status_permissive">Tilladende</string>
|
|
||||||
<string name="superuser">Superbruger</string>
|
|
||||||
<string name="selinux_status_enforcing">Håndhævende</string>
|
<string name="selinux_status_enforcing">Håndhævende</string>
|
||||||
|
<string name="selinux_status_permissive">Tilladende</string>
|
||||||
|
<string name="selinux_status_unknown">Ukendt</string>
|
||||||
|
<string name="superuser">Superbruger</string>
|
||||||
|
<string name="module_failed_to_enable">Aktivering af modul fejlede: %s</string>
|
||||||
<string name="module_failed_to_disable">Deaktivering af modul fejlede: %s</string>
|
<string name="module_failed_to_disable">Deaktivering af modul fejlede: %s</string>
|
||||||
<string name="module_empty">Intet modul installeret</string>
|
<string name="module_empty">Intet modul installeret</string>
|
||||||
|
<string name="module">Modul</string>
|
||||||
|
<string name="module_sort_action_first">Sort (Action first)</string>
|
||||||
|
<string name="module_sort_enabled_first">Sort (Enabled first)</string>
|
||||||
<string name="uninstall">Afinstaller</string>
|
<string name="uninstall">Afinstaller</string>
|
||||||
|
<string name="restore">Restore</string>
|
||||||
<string name="module_install">Installer</string>
|
<string name="module_install">Installer</string>
|
||||||
<string name="install">Installer</string>
|
<string name="install">Installer</string>
|
||||||
<string name="reboot">Genstart</string>
|
<string name="reboot">Genstart</string>
|
||||||
<string name="settings">Indstillinger</string>
|
<string name="settings">Indstillinger</string>
|
||||||
<string name="reboot_userspace">Blød Genstart</string>
|
<string name="reboot_userspace">Blød Genstart</string>
|
||||||
|
<string name="reboot_recovery">Genstart til Recovery</string>
|
||||||
|
<string name="reboot_bootloader">Genstart til Bootloader</string>
|
||||||
<string name="reboot_download">Genstart til Download</string>
|
<string name="reboot_download">Genstart til Download</string>
|
||||||
<string name="reboot_edl">Genstart til EDL</string>
|
<string name="reboot_edl">Genstart til EDL</string>
|
||||||
<string name="about">Om</string>
|
<string name="about">Om</string>
|
||||||
<string name="module_uninstall_confirm">Er du sikker på, at du vil afinstallere modulet %s\?</string>
|
<string name="module_uninstall_confirm">Er du sikker på, at du vil afinstallere modulet %s\?</string>
|
||||||
<string name="module_uninstall_success">%s afinstalleret</string>
|
<string name="module_uninstall_success">%s afinstalleret</string>
|
||||||
<string name="module_uninstall_failed">Afinstallation af: %s fejlede</string>
|
<string name="module_uninstall_failed">Afinstallation af: %s fejlede</string>
|
||||||
|
<string name="module_version">Version</string>
|
||||||
|
<string name="module_author">Forfatter</string>
|
||||||
<string name="refresh">Opdater</string>
|
<string name="refresh">Opdater</string>
|
||||||
|
<string name="show_system_apps">Vis system-apps</string>
|
||||||
|
<string name="hide_system_apps">Gem system-apps</string>
|
||||||
<string name="send_log">Send Log</string>
|
<string name="send_log">Send Log</string>
|
||||||
<string name="safe_mode">Sikker tilstand</string>
|
<string name="safe_mode">Sikker tilstand</string>
|
||||||
<string name="reboot_to_apply">Genstart for at tage effekt</string>
|
<string name="reboot_to_apply">Genstart for at tage effekt</string>
|
||||||
|
<string name="module_magisk_conflict">Moduler er deaktiveret, fordi der er konflikt med Magiskes!</string>
|
||||||
<string name="home_learn_kernelsu">Lær KernelSU</string>
|
<string name="home_learn_kernelsu">Lær KernelSU</string>
|
||||||
<string name="home_learn_kernelsu_url">https://kernelsu.org/guide/what-is-kernelsu.html</string>
|
<string name="home_learn_kernelsu_url">https://kernelsu.org/guide/what-is-kernelsu.html</string>
|
||||||
<string name="home_click_to_learn_kernelsu">Lær hvordan man installerer KernelSU og moduler</string>
|
<string name="home_click_to_learn_kernelsu">Lær hvordan man installerer KernelSU og moduler</string>
|
||||||
|
<string name="home_support_title">Støt Os</string>
|
||||||
|
<string name="home_support_content">KernelSU er, og vil altid være gratis og open source. Du kan stadig vise os din støtte ved at donere.</string>
|
||||||
|
<string name="about_source_code"><![CDATA[View source code at %1$s<br/>Join our %2$s channel]]></string>
|
||||||
<string name="profile_default">Standard</string>
|
<string name="profile_default">Standard</string>
|
||||||
<string name="profile_template">Skabelon</string>
|
<string name="profile_template">Skabelon</string>
|
||||||
|
<string name="profile_custom">Brugerdefineret</string>
|
||||||
|
<string name="profile_name">Profilnavn</string>
|
||||||
<string name="profile_namespace">Monter navnerum</string>
|
<string name="profile_namespace">Monter navnerum</string>
|
||||||
<string name="profile_namespace_inherited">Arvet</string>
|
<string name="profile_namespace_inherited">Arvet</string>
|
||||||
<string name="profile_namespace_global">Global</string>
|
<string name="profile_namespace_global">Global</string>
|
||||||
|
<string name="profile_namespace_individual">Individuel</string>
|
||||||
<string name="profile_groups">Grupper</string>
|
<string name="profile_groups">Grupper</string>
|
||||||
<string name="profile_capabilities">Evner</string>
|
<string name="profile_capabilities">Evner</string>
|
||||||
<string name="profile_selinux_context">SELinux-kontext</string>
|
<string name="profile_selinux_context">SELinux-kontext</string>
|
||||||
<string name="profile_umount_modules">Afmonteret moduler</string>
|
<string name="profile_umount_modules">Afmonteret moduler</string>
|
||||||
|
<string name="failed_to_update_app_profile">Opdatering af App Profil for %s fejlede</string>
|
||||||
|
<string name="require_kernel_version" formatted="false">The current KernelSU version %d is too low for the manager to work properly. Please upgrade to version %d or higher!</string>
|
||||||
<string name="settings_umount_modules_default">Afmontere moduler som standard</string>
|
<string name="settings_umount_modules_default">Afmontere moduler som standard</string>
|
||||||
|
<string name="settings_umount_modules_default_summary">Den globale standard værdi for \"Afmonter moduler\" i App Profiler. Hvis aktiveret vil den fjerne alle modulers modifikationer til system applikationerne der ikke har en sat Profil.</string>
|
||||||
|
<string name="settings_susfs_toggle">Disable kprobe hooks</string>
|
||||||
<string name="profile_umount_modules_summary">Aktivering af denne indstilling vil tillade KernelSU at gendanne hvilken som helst modificeret filer af modulet for denne applikation.</string>
|
<string name="profile_umount_modules_summary">Aktivering af denne indstilling vil tillade KernelSU at gendanne hvilken som helst modificeret filer af modulet for denne applikation.</string>
|
||||||
|
<string name="profile_selinux_domain">Domæne</string>
|
||||||
|
<string name="profile_selinux_rules">Regler</string>
|
||||||
<string name="module_update">Opdatering</string>
|
<string name="module_update">Opdatering</string>
|
||||||
<string name="module_downloading">Downloader modulet: %s</string>
|
<string name="module_downloading">Downloader modulet: %s</string>
|
||||||
|
<string name="module_start_downloading">Start download: %s</string>
|
||||||
<string name="new_version_available">Ny version: %s er tilgængelig, kilk for at downloade</string>
|
<string name="new_version_available">Ny version: %s er tilgængelig, kilk for at downloade</string>
|
||||||
<string name="launch_app">Start</string>
|
<string name="launch_app">Start</string>
|
||||||
<string name="force_stop_app">Tving Stop</string>
|
<string name="force_stop_app" formatted="false">Tving Stop</string>
|
||||||
<string name="failed_to_update_sepolicy">Opdatering af SELinux-regler for: %s fejlede</string>
|
|
||||||
<string name="module_start_downloading">Start download: %s</string>
|
|
||||||
<string name="home_click_to_install">Klik for at installere</string>
|
|
||||||
<string name="home_working_version">Version: %d</string>
|
|
||||||
<string name="home">Hjem</string>
|
|
||||||
<string name="home_not_installed">Ikke installeret</string>
|
|
||||||
<string name="home_superuser_count">Superbrugere: %d</string>
|
|
||||||
<string name="home_fingerprint">Fingeraftryk</string>
|
|
||||||
<string name="selinux_status_unknown">Ukendt</string>
|
|
||||||
<string name="module_failed_to_enable">Aktivering af modul fejlede: %s</string>
|
|
||||||
<string name="reboot_recovery">Genstart til Recovery</string>
|
|
||||||
<string name="module">Modul</string>
|
|
||||||
<string name="module_author">Forfatter</string>
|
|
||||||
<string name="reboot_bootloader">Genstart til Bootloader</string>
|
|
||||||
<string name="module_version">Version</string>
|
|
||||||
<string name="hide_system_apps">Gem system-apps</string>
|
|
||||||
<string name="show_system_apps">Vis system-apps</string>
|
|
||||||
<string name="module_magisk_conflict">Moduler er deaktiveret, fordi der er konflikt med Magiskes!</string>
|
|
||||||
<string name="home_support_title">Støt Os</string>
|
|
||||||
<string name="home_support_content">KernelSU er, og vil altid være gratis og open source. Du kan stadig vise os din støtte ved at donere.</string>
|
|
||||||
<string name="profile_custom">Brugerdefineret</string>
|
|
||||||
<string name="profile_name">Profilnavn</string>
|
|
||||||
<string name="profile_namespace_individual">Individuel</string>
|
|
||||||
<string name="failed_to_update_app_profile">Opdatering af App Profil for %s fejlede</string>
|
|
||||||
<string name="settings_umount_modules_default_summary">Den globale standard værdi for \"Afmonter moduler\" i App Profiler. Hvis aktiveret vil den fjerne alle modulers modifikationer til system applikationerne der ikke har en sat Profil.</string>
|
|
||||||
<string name="profile_selinux_domain">Domæne</string>
|
|
||||||
<string name="profile_selinux_rules" formatted="false">Regler</string>
|
|
||||||
<string name="restart_app">Genstart</string>
|
<string name="restart_app">Genstart</string>
|
||||||
|
<string name="failed_to_update_sepolicy">Opdatering af SELinux-regler for: %s fejlede</string>
|
||||||
|
<string name="module_changelog">Changelog</string>
|
||||||
|
<string name="settings_profile_template">App Profile Template</string>
|
||||||
|
<string name="settings_profile_template_summary">Manage local and online template of App Profile</string>
|
||||||
|
<string name="app_profile_template_create">Create template</string>
|
||||||
|
<string name="app_profile_template_edit">Edit template</string>
|
||||||
|
<string name="app_profile_template_id">ID</string>
|
||||||
|
<string name="app_profile_template_id_invalid">Invalid template ID</string>
|
||||||
|
<string name="app_profile_template_name">Name</string>
|
||||||
|
<string name="app_profile_template_description">Description</string>
|
||||||
|
<string name="app_profile_template_save">Save</string>
|
||||||
|
<string name="app_profile_template_delete">Delete</string>
|
||||||
|
<string name="app_profile_template_view">View template</string>
|
||||||
|
<string name="app_profile_template_readonly">Read only</string>
|
||||||
|
<string name="app_profile_template_id_exist">Template ID already exists!</string>
|
||||||
|
<string name="app_profile_import_export">Import/Export</string>
|
||||||
|
<string name="app_profile_import_from_clipboard">Import from clipboard</string>
|
||||||
|
<string name="app_profile_export_to_clipboard">Export to clipboard</string>
|
||||||
|
<string name="app_profile_template_export_empty">Cannot find local template to export!</string>
|
||||||
|
<string name="app_profile_template_import_success">Imported successfully</string>
|
||||||
|
<string name="app_profile_template_sync">Sync online templates</string>
|
||||||
|
<string name="app_profile_template_save_failed">Failed to save template</string>
|
||||||
|
<string name="app_profile_template_import_empty">Clipboard is empty!</string>
|
||||||
|
<string name="module_changelog_failed">Fetch changelog failed: %s</string>
|
||||||
|
<string name="settings_check_update">Check update</string>
|
||||||
|
<string name="settings_check_update_summary">Automatically check for updates when opening the app</string>
|
||||||
|
<string name="grant_root_failed">Failed to grant root!</string>
|
||||||
|
<string name="action">Action</string>
|
||||||
|
<string name="open">Open</string>
|
||||||
|
<string name="close">Close</string>
|
||||||
|
<string name="enable_web_debugging">Enable WebView debugging</string>
|
||||||
|
<string name="enable_web_debugging_summary">Can be used to debug WebUI. Please enable only when needed.</string>
|
||||||
|
<string name="direct_install">Direct install (Recommended)</string>
|
||||||
|
<string name="select_file">Select a image that needs to be patched</string>
|
||||||
|
<string name="install_inactive_slot">Install to inactive slot (After OTA)</string>
|
||||||
|
<string name="install_inactive_slot_warning">Your device will be **FORCED** to boot to the current inactive slot after a reboot!\nOnly use this option after OTA is done.\nContinue?</string>
|
||||||
|
<string name="install_next">Next</string>
|
||||||
|
<string name="select_file_tip">%1$s partition image is recommended</string>
|
||||||
|
<string name="select_kmi">Select KMI</string>
|
||||||
|
<string name="settings_uninstall">Uninstall</string>
|
||||||
|
<string name="settings_uninstall_temporary">Uninstall temporarily</string>
|
||||||
|
<string name="settings_uninstall_permanent">Uninstall permanently</string>
|
||||||
|
<string name="settings_restore_stock_image">Restore stock image</string>
|
||||||
|
<string name="settings_uninstall_temporary_message">Temporarily uninstall KernelSU, restore to original state after next reboot.</string>
|
||||||
|
<string name="settings_uninstall_permanent_message">Uninstalling KernelSU (Root and all modules) completely and permanently.</string>
|
||||||
|
<string name="settings_restore_stock_image_message">Restore the stock factory image (If a backup exists), usually used before OTA; if you need to uninstall KernelSU, please use \"Uninstall permanently\".</string>
|
||||||
|
<string name="flashing">Flashing</string>
|
||||||
|
<string name="flash_success">Flash success</string>
|
||||||
|
<string name="flash_failed">Flash failed</string>
|
||||||
|
<string name="selected_lkm">Selected LKM: %s</string>
|
||||||
<string name="save_log">Gem Logfiler</string>
|
<string name="save_log">Gem Logfiler</string>
|
||||||
|
<string name="log_saved">Logs saved</string>
|
||||||
|
<string name="status_supported">Supported</string>
|
||||||
|
<string name="status_not_supported">Not Supported</string>
|
||||||
|
<string name="status_unknown">Unknown</string>
|
||||||
|
<string name="sus_su_mode">SuS SU mode:</string>
|
||||||
|
<!-- Module related -->
|
||||||
|
<string name="module_install_confirm">confirm install module %1$s?</string>
|
||||||
|
<string name="unknown_module">unknown module</string>
|
||||||
|
<!-- Restore related -->
|
||||||
|
<string name="restore_confirm_title">Confirm Module Restoration</string>
|
||||||
|
<string name="restore_confirm_message">This operation will overwrite all existing modules. Continue?</string>
|
||||||
|
<string name="confirm">Confirm</string>
|
||||||
|
<string name="cancel">Cancel</string>
|
||||||
|
<!-- Backup related -->
|
||||||
|
<string name="backup_success">Backup successful (tar.gz)</string>
|
||||||
|
<string name="backup_failed">Backup failed: %1$s</string>
|
||||||
|
<string name="backup_modules">backup modules</string>
|
||||||
|
<string name="restore_modules">restore modules</string>
|
||||||
|
<!-- Restore related messages -->
|
||||||
|
<string name="restore_success">Modules restored successfully, restart required</string>
|
||||||
|
<string name="restore_failed">Restore failed: %1$s</string>
|
||||||
|
<string name="restart_now">Restart Now</string>
|
||||||
|
<string name="unknown_error">Unknown error</string>
|
||||||
|
<!-- Command related -->
|
||||||
|
<string name="command_execution_failed">Command execution failed: %1$s</string>
|
||||||
|
<!-- Allowlist related -->
|
||||||
|
<string name="allowlist_backup_success">Allowlist backup successful</string>
|
||||||
|
<string name="allowlist_backup_failed">Allowlist backup failed: %1$s</string>
|
||||||
|
<string name="allowlist_restore_confirm_title">Confirm Allowlist Restoration</string>
|
||||||
|
<string name="allowlist_restore_confirm_message">This operation will overwrite the current allowlist. Continue?</string>
|
||||||
|
<string name="allowlist_restore_success">Allowlist restored successfully</string>
|
||||||
|
<string name="allowlist_restore_failed">Allowlist restore failed: %1$s</string>
|
||||||
|
<string name="backup_allowlist">Backup Allowlist</string>
|
||||||
|
<string name="restore_allowlist">Restore Allowlist</string>
|
||||||
|
<string name="settings_custom_background">Custom App Background</string>
|
||||||
|
<string name="settings_custom_background_summary">Select an image as background</string>
|
||||||
|
<string name="settings_card_alpha">Navigation bar transparency</string>
|
||||||
|
<string name="settings_restore_default">Restore default</string>
|
||||||
|
<string name="home_android_version">Android version</string>
|
||||||
|
<string name="home_device_model">Device model</string>
|
||||||
|
<string name="su_not_allowed">Granting superuser to %s is not allowed</string>
|
||||||
|
<string name="settings_disable_su">Disable su compatibility</string>
|
||||||
|
<string name="settings_disable_su_summary">Temporarily disable any applications from obtaining root privileges via the su command (existing root processes will not be affected).</string>
|
||||||
|
<string name="using_mksu_manager">You are using the SukiSU Beta manager</string>
|
||||||
|
<string name="module_install_multiple_confirm">Are you sure you want to install the selected %d modules?</string>
|
||||||
|
<string name="module_install_multiple_confirm_with_names">Sure you want to install the following %1$d modules? \n\n%2$s</string>
|
||||||
|
<string name="more_settings">More settings</string>
|
||||||
|
<string name="selinux">SELinux</string>
|
||||||
|
<string name="selinux_enabled">Enabled</string>
|
||||||
|
<string name="selinux_disabled">Disabled</string>
|
||||||
|
<string name="simple_mode">Simplicity mode</string>
|
||||||
|
<string name="simple_mode_summary">Hides unnecessary cards when turned on</string>
|
||||||
|
<string name="hide_kernel_kernelsu_version">Hide kernel version</string>
|
||||||
|
<string name="hide_kernel_kernelsu_version_summary">Hide kernel version</string>
|
||||||
|
<string name="hide_other_info">Hide other info</string>
|
||||||
|
<string name="hide_other_info_summary">Hides information about the number of super users, modules and KPM modules on the home page</string>
|
||||||
|
<string name="hide_susfs_status">Hide SuSFS status</string>
|
||||||
|
<string name="hide_susfs_status_summary">Hide SuSFS status information on the home page</string>
|
||||||
|
<string name="hide_link_card">Hide Link Card Status</string>
|
||||||
|
<string name="hide_link_card_summary">Hide link card information on the home page</string>
|
||||||
|
<string name="theme_mode">Theme</string>
|
||||||
|
<string name="theme_follow_system">Follow system</string>
|
||||||
|
<string name="theme_light">Light</string>
|
||||||
|
<string name="theme_dark">Dark</string>
|
||||||
|
<string name="manual_hook">Manual Hook</string>
|
||||||
|
<string name="dynamic_color_title">Dynamic colours</string>
|
||||||
|
<string name="dynamic_color_summary">Dynamic colours using system themes</string>
|
||||||
|
<string name="choose_theme_color">Choose a theme colour</string>
|
||||||
|
<string name="color_default">Blue</string>
|
||||||
|
<string name="color_green">Green</string>
|
||||||
|
<string name="color_purple">Purple</string>
|
||||||
|
<string name="color_orange">Orange</string>
|
||||||
|
<string name="color_pink">Pink</string>
|
||||||
|
<string name="color_gray">Gray</string>
|
||||||
|
<string name="color_yellow">Yellow</string>
|
||||||
|
<string name="flash_option">Brush Options</string>
|
||||||
|
<string name="flash_option_tip">Select the file to be flashed</string>
|
||||||
|
<string name="horizon_kernel">Install Anykernel3</string>
|
||||||
|
<string name="horizon_kernel_summary">Flash AnyKernel3 kernel file</string>
|
||||||
|
<string name="root_required">Requires root privileges</string>
|
||||||
|
<string name="copy_failed">File Copy Failure</string>
|
||||||
|
<string name="reboot_complete_title">Scrubbing complete</string>
|
||||||
|
<string name="reboot_complete_msg">Whether to reboot immediately?</string>
|
||||||
|
<string name="yes">Yes</string>
|
||||||
|
<string name="no">No</string>
|
||||||
|
<string name="failed_reboot">Reboot Failed</string>
|
||||||
|
<string name="batch_authorization">empower</string>
|
||||||
|
<string name="batch_cancel_authorization">withdraw</string>
|
||||||
|
<string name="backup">Backup</string>
|
||||||
|
<string name="kpm_title">KPM</string>
|
||||||
|
<string name="kpm_empty">No installed kernel modules at this time</string>
|
||||||
|
<string name="kpm_version">Version</string>
|
||||||
|
<string name="kpm_author">Author</string>
|
||||||
|
<string name="kpm_uninstall">Uninstall</string>
|
||||||
|
<string name="kpm_uninstall_success">Uninstalled successfully</string>
|
||||||
|
<string name="kpm_uninstall_failed">Failed to uninstall</string>
|
||||||
|
<string name="kpm_install">Install</string>
|
||||||
|
<string name="kpm_install_success">Load of kpm module successful</string>
|
||||||
|
<string name="kpm_install_failed">Load of kpm module failed</string>
|
||||||
|
<string name="kpm_args">Parameters</string>
|
||||||
|
<string name="kpm_control">Execute</string>
|
||||||
|
<string name="home_kpm_version">KPM Version</string>
|
||||||
|
<string name="close_notice">Close</string>
|
||||||
|
<string name="kernel_module_notice">The following kernel module functions were developed by KernelPatch and modified to include the kernel module functions of SukiSU Ultra</string>
|
||||||
|
<string name="home_ContributionCard_kernelsu">SukiSU Ultra Look forward to</string>
|
||||||
|
<string name="kpm_control_success">Success</string>
|
||||||
|
<string name="kpm_control_failed">Failed</string>
|
||||||
|
<string name="home_click_to_ContributionCard_kernelsu">SukiSU Ultra will be a relatively independent branch of KSU in the future, but we still appreciate the official KernelSU and MKSU etc. for their contributions!</string>
|
||||||
|
<string name="not_supported">Unsupported</string>
|
||||||
|
<string name="supported">Supported</string>
|
||||||
|
<string name="home_kpm_module">"Number of KPM modules: %d "</string>
|
||||||
|
<string name="kpm_invalid_file">Invalid KPM file</string>
|
||||||
|
<string name="kernel_patched">Kernel not patched</string>
|
||||||
|
<string name="kernel_not_enabled">Kernel not configured</string>
|
||||||
|
<string name="custom_settings">Custom settings</string>
|
||||||
|
<string name="kpm_install_mode">KPM Install</string>
|
||||||
|
<string name="kpm_install_mode_load">Load</string>
|
||||||
|
<string name="kpm_install_mode_embed">Embed</string>
|
||||||
|
<string name="kpm_install_mode_description">Please select: %1\$s Module Installation Mode \n\nLoad: Temporarily load the module \nEmbedded: Permanently install into the system</string>
|
||||||
|
<string name="log_failed_to_check_module_file">Failed to check module file existence</string>
|
||||||
|
<string name="snackbar_failed_to_check_module_file">Unable to check if module file exists</string>
|
||||||
|
<string name="confirm_uninstall_title">Confirm uninstallation</string>
|
||||||
|
<string name="confirm_uninstall_confirm">Uninstall</string>
|
||||||
|
<string name="confirm_uninstall_dismiss">Cancel</string>
|
||||||
|
<string name="theme_color">Theme Color</string>
|
||||||
|
<string name="invalid_file_type">Incorrect file type! Please select .kpm file.</string>
|
||||||
|
<string name="confirm_uninstall_title_with_filename">Uninstall</string>
|
||||||
|
<string name="confirm_uninstall_content">The following KPM will be uninstalled: %s</string>
|
||||||
|
<string name="settings_susfs_toggle_summary">Disable kprobe hooks created by KernelSU, using inline hooks instead, which is similar to non-GKI kernel hooking method.</string>
|
||||||
|
<string name="image_editor_title">Adjust background image</string>
|
||||||
|
<string name="image_editor_hint">Use two fingers to zoom the image, and one finger to drag it to adjust the position</string>
|
||||||
|
<string name="background_image_error">Could not load image</string>
|
||||||
|
<string name="reprovision">Reprovision</string>
|
||||||
|
<!-- Kernel Flash Progress Related -->
|
||||||
|
<string name="horizon_flash_title">Kernel Flashing</string>
|
||||||
|
<string name="horizon_logs_label">Logs:</string>
|
||||||
|
<string name="horizon_flash_complete">Flash Complete</string>
|
||||||
|
<!-- Flash Status Related -->
|
||||||
|
<string name="horizon_preparing">Preparing…</string>
|
||||||
|
<string name="horizon_cleaning_files">Cleaning files…</string>
|
||||||
|
<string name="horizon_copying_files">Copying files…</string>
|
||||||
|
<string name="horizon_extracting_tool">Extracting flash tool…</string>
|
||||||
|
<string name="horizon_patching_script">Patching flash script…</string>
|
||||||
|
<string name="horizon_flashing">Flashing kernel…</string>
|
||||||
|
<string name="horizon_flash_complete_status">Flash completed</string>
|
||||||
|
<!-- Slot selection related strings -->
|
||||||
|
<string name="select_slot_title">Select Flash Slot</string>
|
||||||
|
<string name="select_slot_description">Please select the target slot for flashing boot</string>
|
||||||
|
<string name="slot_a">Slot A</string>
|
||||||
|
<string name="slot_b">Slot B</string>
|
||||||
|
<string name="selected_slot">Selected slot: %1$s</string>
|
||||||
|
<string name="horizon_getting_original_slot">Getting the original slot</string>
|
||||||
|
<string name="horizon_setting_target_slot">Setting the specified slot</string>
|
||||||
|
<string name="horizon_restoring_original_slot">Restore Default Slot</string>
|
||||||
|
<string name="current_slot">Current Slot:%1$s </string>
|
||||||
|
<!-- Error Messages -->
|
||||||
|
<string name="horizon_copy_failed">Copy failed</string>
|
||||||
|
<string name="horizon_unknown_error">Unknown error</string>
|
||||||
|
<string name="flash_failed_message">Flash failed</string>
|
||||||
|
<!-- lkm/gki install -->
|
||||||
|
<string name="Lkm_install_methods">LKM repair/installation</string>
|
||||||
|
<string name="GKI_install_methods">Flashing AnyKernel3</string>
|
||||||
|
<string name="kernel_version_log">Kernel version:%1$s</string>
|
||||||
|
<string name="tool_version_log">Using the patching tool:%1$s</string>
|
||||||
|
<string name="configuration">Configure</string>
|
||||||
|
<string name="app_settings">Application Settings</string>
|
||||||
|
<string name="tools">Tools</string>
|
||||||
|
<string name="currently_selected">Currently</string>
|
||||||
|
<!-- String resources used in SuperUser -->
|
||||||
|
<string name="clear">Removals</string>
|
||||||
|
<string name="apps_with_root">Applications with root privileges</string>
|
||||||
|
<string name="apps_with_custom_profile">Applications with customized configurations</string>
|
||||||
|
<string name="other_apps">Applications with unchanged defaults</string>
|
||||||
|
<string name="no_apps_found">Application not found</string>
|
||||||
|
<string name="selinux_enabled_toast">SELinux Enabled</string>
|
||||||
|
<string name="selinux_disabled_toast">SELinux Disabled</string>
|
||||||
|
<string name="selinux_change_failed">SELinux Status change failed</string>
|
||||||
|
<string name="advanced_settings">Advanced Settings</string>
|
||||||
|
<string name="appearance_settings">Customize the toolbar</string>
|
||||||
|
<string name="back">Comeback</string>
|
||||||
|
<string name="expand">Be in full swing</string>
|
||||||
|
<string name="collapse">put away</string>
|
||||||
|
<string name="susfs_enabled">SuSFS enabled</string>
|
||||||
|
<string name="susfs_disabled">SuSFS disabled</string>
|
||||||
|
<string name="background_set_success">Background set successfully</string>
|
||||||
|
<string name="background_removed">Removed custom backgrounds</string>
|
||||||
|
<string name="root_require_for_install">Requires root privileges</string>
|
||||||
|
<!-- KPM display settings -->
|
||||||
|
<string name="show_kpm_info">Display KPM Function</string>
|
||||||
|
<string name="show_kpm_info_summary">Display KPM information and Function in home and bottom bar (Need to reopen the app)</string>
|
||||||
|
<!-- Webui X settings -->
|
||||||
|
<string name="use_webuix">Select the WebUI engine to use</string>
|
||||||
|
<string name="engine_auto_select">Automatic Selection</string>
|
||||||
|
<string name="engine_force_webuix">Force the use of WebUI X</string>
|
||||||
|
<string name="engine_force_ksu">Mandatory use of KSU WebUI</string>
|
||||||
|
<string name="use_webuix_eruda">Inject Eruda into WebUI X</string>
|
||||||
|
<string name="use_webuix_eruda_summary">Inject a debug console into WebUI X to make debugging easier. Requires web debugging to be on.</string>
|
||||||
|
<!-- DPI setting related strings -->
|
||||||
|
<string name="dpi_settings">DPI setting</string>
|
||||||
|
<string name="app_dpi_title">Applied DPI</string>
|
||||||
|
<string name="app_dpi_summary">Adjust the screen display density for the current application only</string>
|
||||||
|
<string name="dpi_size_small">Small </string>
|
||||||
|
<string name="dpi_size_medium">Medium </string>
|
||||||
|
<string name="dpi_size_large">Big</string>
|
||||||
|
<string name="dpi_size_extra_large">oversize</string>
|
||||||
|
<string name="dpi_size_custom">customizable</string>
|
||||||
|
<string name="dpi_apply_settings">Applying DPI settings</string>
|
||||||
|
<string name="dpi_confirm_title">Confirm DPI change</string>
|
||||||
|
<string name="dpi_confirm_message">Are you sure you want to change the application DPI from %1$d to %2$d?</string>
|
||||||
|
<string name="dpi_confirm_summary">Application needs to be restarted to apply the new DPI settings, does not affect the system status bar or other applications</string>
|
||||||
|
<string name="dpi_applied_success">DPI has been set to %1$d, effective after restarting the application</string>
|
||||||
|
<!-- Language settings related strings -->
|
||||||
|
<string name="language_setting">App Language</string>
|
||||||
|
<string name="language_follow_system">Follow System</string>
|
||||||
|
<string name="language_changed">Language changed, restarting to apply changes</string>
|
||||||
|
<string name="settings_card_dim">Card Darkness Adjustment</string>
|
||||||
|
<!-- Super User Related -->
|
||||||
|
<string name="scroll_to_top">Top</string>
|
||||||
|
<string name="scroll_to_bottom">Bottom</string>
|
||||||
|
<string name="scroll_to_top_description">Scroll to top</string>
|
||||||
|
<string name="scroll_to_bottom_description">Scroll to the bottom</string>
|
||||||
|
<string name="authorized">authorized</string>
|
||||||
|
<string name="unauthorized">unauthorized</string>
|
||||||
|
<string name="selected">Selected</string>
|
||||||
|
<string name="select">option</string>
|
||||||
|
<string name="profile_umount_modules_disable">Disable custom uninstallation module</string>
|
||||||
|
<!-- Flash related -->
|
||||||
|
<string name="error_code">error code</string>
|
||||||
|
<string name="check_log">Please check the log</string>
|
||||||
|
<string name="installing_module">Module being installed %1$d/%2$d</string>
|
||||||
|
<string name="module_failed_count">%d Failed to install a new module</string>
|
||||||
|
<string name="module_download_error">Module download failed</string>
|
||||||
|
<string name="kernel_flashing">Kernel Flashing</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -2,130 +2,372 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<string name="home">Startseite</string>
|
<string name="home">Startseite</string>
|
||||||
<string name="home_not_installed">Nicht installiert</string>
|
<string name="home_not_installed">Nicht installiert</string>
|
||||||
<string name="selinux_status_permissive">Permissiv</string>
|
<string name="home_click_to_install">Tippe zum Installieren</string>
|
||||||
<string name="home_working">Funktioniert</string>
|
<string name="home_working">Funktioniert</string>
|
||||||
<string name="home_working_version">Version: %d</string>
|
<string name="home_working_version">Version: %d</string>
|
||||||
<string name="superuser">Superuser</string>
|
|
||||||
<string name="home_click_to_install">Tippe zum Installieren</string>
|
|
||||||
<string name="home_superuser_count">Superuser: %d</string>
|
<string name="home_superuser_count">Superuser: %d</string>
|
||||||
<string name="selinux_status_unknown">Unbekannt</string>
|
|
||||||
<string name="selinux_status_enforcing">Erzwingen</string>
|
|
||||||
<string name="reboot_bootloader">In den Bootloader-Modus neustarten</string>
|
|
||||||
<string name="reboot_download">In den Download-Modus neustarten</string>
|
|
||||||
<string name="reboot_edl">In den EDL-Modus neustarten</string>
|
|
||||||
<string name="module_author">Autor</string>
|
|
||||||
<string name="about">Über KernelSU</string>
|
|
||||||
<string name="module_magisk_conflict">Module sind aufgrund eines Konfliktes mit Magisk nicht verfügbar!</string>
|
|
||||||
<string name="home_learn_kernelsu_url">https://kernelsu.org/guide/what-is-kernelsu.html</string>
|
|
||||||
<string name="home_click_to_learn_kernelsu">Erfahre, wie KernelSU installiert wird und wie Module verwendet werden</string>
|
|
||||||
<string name="home_support_title">Unterstütze uns</string>
|
|
||||||
<string name="home_support_content">KernelSU ist und wird immer frei und quelloffen sein. Du kannst uns jedoch deine Unterstützung zeigen, indem du eine Spende tätigst.</string>
|
|
||||||
<string name="profile_selinux_context">SELinux-Kontext</string>
|
|
||||||
<string name="settings_umount_modules_default">Module standardmäßig aushängen</string>
|
|
||||||
<string name="settings_umount_modules_default_summary">Globaler Standardwert für \"Module aushängen\" im App-Profil. Falls er aktiviert ist, werden alle Moduländerungen im System für alle Apps entfernt, für die kein Profil festgelegt ist.</string>
|
|
||||||
<string name="profile_default">Standard</string>
|
|
||||||
<string name="profile_template">Vorlage</string>
|
|
||||||
<string name="profile_custom">Benutzerdefiniert</string>
|
|
||||||
<string name="failed_to_update_app_profile">App-Profilaktualisierung für %s fehlgeschlagen</string>
|
|
||||||
<string name="profile_namespace_inherited">Geerbt</string>
|
|
||||||
<string name="profile_namespace_global">Global</string>
|
|
||||||
<string name="profile_namespace_individual">Individuell</string>
|
|
||||||
<string name="profile_selinux_domain">Domäne</string>
|
|
||||||
<string name="module_update">Aktualisieren</string>
|
|
||||||
<string name="profile_umount_modules_summary">Wenn du diese Option aktivierst, kann KernelSU alle von den Modulen für diese App geänderten Dateien wiederherstellen.</string>
|
|
||||||
<string name="profile_selinux_rules">Regeln</string>
|
|
||||||
<string name="module_start_downloading">Starte Download: %s</string>
|
|
||||||
<string name="failed_to_update_sepolicy">Aktualisieren der SELinux-Regeln schlug fehl für: %s</string>
|
|
||||||
<string name="launch_app">Starten</string>
|
|
||||||
<string name="new_version_available">Neue Version %s verfügbar, tippen zum Aktualisieren.</string>
|
|
||||||
<string name="force_stop_app" formatted="false">Stopp erzwingen</string>
|
|
||||||
<string name="restart_app">Neustarten</string>
|
|
||||||
<string name="home_module_count">Module: %d</string>
|
<string name="home_module_count">Module: %d</string>
|
||||||
|
<string name="home_unsupported">Nicht unterstützt</string>
|
||||||
|
<string name="home_unsupported_reason">KernelSU unterstützt derzeit nur GKI-Kernel</string>
|
||||||
|
<string name="home_kernel">Kernel</string>
|
||||||
|
<string name="home_susfs">SuSFS: %s</string>
|
||||||
|
<string name="home_susfs_version">SuSFS version</string>
|
||||||
|
<string name="home_susfs_sus_su">SuS SU</string>
|
||||||
<string name="home_manager_version">Manager-Version</string>
|
<string name="home_manager_version">Manager-Version</string>
|
||||||
|
<string name="home_fingerprint">Fingerabdruck</string>
|
||||||
<string name="home_selinux_status">SELinux Status</string>
|
<string name="home_selinux_status">SELinux Status</string>
|
||||||
<string name="selinux_status_disabled">Deaktiviert</string>
|
<string name="selinux_status_disabled">Deaktiviert</string>
|
||||||
|
<string name="selinux_status_enforcing">Erzwingen</string>
|
||||||
|
<string name="selinux_status_permissive">Permissiv</string>
|
||||||
|
<string name="selinux_status_unknown">Unbekannt</string>
|
||||||
|
<string name="superuser">Superuser</string>
|
||||||
<string name="module_failed_to_enable">Modulaktivierung fehlgeschlagen: %s</string>
|
<string name="module_failed_to_enable">Modulaktivierung fehlgeschlagen: %s</string>
|
||||||
<string name="module_failed_to_disable">Moduldeaktivierung fehlgeschlagen: %s</string>
|
<string name="module_failed_to_disable">Moduldeaktivierung fehlgeschlagen: %s</string>
|
||||||
<string name="module_empty">Keine Modul installiert</string>
|
<string name="module_empty">Keine Modul installiert</string>
|
||||||
<string name="module">Modul</string>
|
<string name="module">Modul</string>
|
||||||
|
<string name="module_sort_action_first">Sortiere zuerst (Aktion)</string>
|
||||||
|
<string name="module_sort_enabled_first">Sortieren (zuerst aktiviert)</string>
|
||||||
<string name="uninstall">Deinstallieren</string>
|
<string name="uninstall">Deinstallieren</string>
|
||||||
|
<string name="restore">Wiederherstellen</string>
|
||||||
|
<string name="module_install">Installieren</string>
|
||||||
<string name="install">Installieren</string>
|
<string name="install">Installieren</string>
|
||||||
<string name="reboot">Neustarten</string>
|
<string name="reboot">Neustarten</string>
|
||||||
<string name="settings">Einstellungen</string>
|
<string name="settings">Einstellungen</string>
|
||||||
|
<string name="reboot_userspace">Soft-Reboot</string>
|
||||||
<string name="reboot_recovery">In den Recovery-Modus neustarten</string>
|
<string name="reboot_recovery">In den Recovery-Modus neustarten</string>
|
||||||
|
<string name="reboot_bootloader">In den Bootloader-Modus neustarten</string>
|
||||||
|
<string name="reboot_download">In den Download-Modus neustarten</string>
|
||||||
|
<string name="reboot_edl">In den EDL-Modus neustarten</string>
|
||||||
|
<string name="about">Über KernelSU</string>
|
||||||
|
<string name="module_uninstall_confirm">Möchtest du wirklich Modul %s deinstallieren?</string>
|
||||||
<string name="module_uninstall_success">%s deinstalliert</string>
|
<string name="module_uninstall_success">%s deinstalliert</string>
|
||||||
|
<string name="module_uninstall_failed">Deinstallation fehlgeschlagen: %s</string>
|
||||||
<string name="module_version">Version</string>
|
<string name="module_version">Version</string>
|
||||||
|
<string name="module_author">Autor</string>
|
||||||
<string name="refresh">Aktualisieren</string>
|
<string name="refresh">Aktualisieren</string>
|
||||||
<string name="show_system_apps">System-Apps anzeigen</string>
|
<string name="show_system_apps">System-Apps anzeigen</string>
|
||||||
<string name="hide_system_apps">System-Apps ausblenden</string>
|
<string name="hide_system_apps">System-Apps ausblenden</string>
|
||||||
<string name="send_log">Protokoll senden</string>
|
<string name="send_log">Protokoll senden</string>
|
||||||
<string name="home_learn_kernelsu">KernelSU verstehen</string>
|
|
||||||
<string name="safe_mode">Sicherer Modus</string>
|
<string name="safe_mode">Sicherer Modus</string>
|
||||||
<string name="reboot_to_apply">Neustarten, damit Änderungen wirksam werden</string>
|
<string name="reboot_to_apply">Neustarten, damit Änderungen wirksam werden</string>
|
||||||
|
<string name="module_magisk_conflict">Module sind aufgrund eines Konfliktes mit Magisk nicht verfügbar!</string>
|
||||||
|
<string name="home_learn_kernelsu">KernelSU verstehen</string>
|
||||||
|
<string name="home_learn_kernelsu_url">https://kernelsu.org/guide/what-is-kernelsu.html</string>
|
||||||
|
<string name="home_click_to_learn_kernelsu">Erfahre, wie KernelSU installiert wird und wie Module verwendet werden</string>
|
||||||
|
<string name="home_support_title">Unterstütze uns</string>
|
||||||
|
<string name="home_support_content">KernelSU ist und wird immer frei und quelloffen sein. Du kannst uns jedoch deine Unterstützung zeigen, indem du eine Spende tätigst.</string>
|
||||||
|
<string name="about_source_code"><![CDATA[Den Quellcode finden Sie unter %1$s<br/>Begleiten Sie uns %2$s-Kanal]]></string>
|
||||||
|
<string name="profile_default">Standard</string>
|
||||||
|
<string name="profile_template">Vorlage</string>
|
||||||
|
<string name="profile_custom">Benutzerdefiniert</string>
|
||||||
<string name="profile_name">Profilname</string>
|
<string name="profile_name">Profilname</string>
|
||||||
<string name="profile_namespace">Namespace einhängen</string>
|
<string name="profile_namespace">Namespace einhängen</string>
|
||||||
|
<string name="profile_namespace_inherited">Geerbt</string>
|
||||||
|
<string name="profile_namespace_global">Global</string>
|
||||||
|
<string name="profile_namespace_individual">Individuell</string>
|
||||||
<string name="profile_groups">Gruppen</string>
|
<string name="profile_groups">Gruppen</string>
|
||||||
<string name="profile_capabilities">Fähigkeiten</string>
|
<string name="profile_capabilities">Fähigkeiten</string>
|
||||||
|
<string name="profile_selinux_context">SELinux-Kontext</string>
|
||||||
<string name="profile_umount_modules">Module aushängen</string>
|
<string name="profile_umount_modules">Module aushängen</string>
|
||||||
|
<string name="failed_to_update_app_profile">App-Profilaktualisierung für %s fehlgeschlagen</string>
|
||||||
|
<string name="require_kernel_version" formatted="false">Die aktuelle KernelSU-Version %d ist zu alt für diese Manager-Version. Bitte auf Version %d oder höher aktualisieren!</string>
|
||||||
|
<string name="settings_umount_modules_default">Module standardmäßig aushängen</string>
|
||||||
|
<string name="settings_umount_modules_default_summary">Globaler Standardwert für \"Module aushängen\" im App-Profil. Falls er aktiviert ist, werden alle Moduländerungen im System für alle Apps entfernt, für die kein Profil festgelegt ist.</string>
|
||||||
|
<string name="settings_susfs_toggle">Kprobe-Hooks deaktivieren</string>
|
||||||
|
<string name="profile_umount_modules_summary">Wenn du diese Option aktivierst, kann KernelSU alle von den Modulen für diese App geänderten Dateien wiederherstellen.</string>
|
||||||
|
<string name="profile_selinux_domain">Domäne</string>
|
||||||
|
<string name="profile_selinux_rules">Regeln</string>
|
||||||
|
<string name="module_update">Aktualisieren</string>
|
||||||
<string name="module_downloading">Lädt Modul %s herunter</string>
|
<string name="module_downloading">Lädt Modul %s herunter</string>
|
||||||
<string name="home_unsupported">Nicht unterstützt</string>
|
<string name="module_start_downloading">Starte Download: %s</string>
|
||||||
<string name="home_unsupported_reason">KernelSU unterstützt derzeit nur GKI-Kernel</string>
|
<string name="new_version_available">Neue Version %s verfügbar, tippen zum Aktualisieren.</string>
|
||||||
<string name="home_kernel">Kernel</string>
|
<string name="launch_app">Starten</string>
|
||||||
<string name="home_fingerprint">Fingerabdruck</string>
|
<string name="force_stop_app" formatted="false">Stopp erzwingen</string>
|
||||||
<string name="module_install">Installieren</string>
|
<string name="restart_app">Neustarten</string>
|
||||||
<string name="reboot_userspace">Soft-Reboot</string>
|
<string name="failed_to_update_sepolicy">Aktualisieren der SELinux-Regeln schlug fehl für: %s</string>
|
||||||
<string name="module_uninstall_confirm">Möchtest du wirklich Modul %s deinstallieren?</string>
|
|
||||||
<string name="module_uninstall_failed">Deinstallation fehlgeschlagen: %s</string>
|
|
||||||
<string name="module_changelog">Änderungsprotokoll</string>
|
<string name="module_changelog">Änderungsprotokoll</string>
|
||||||
<string name="app_profile_template_import_success">Erfolgreich importiert</string>
|
<string name="settings_profile_template">App-Profil-Vorlage</string>
|
||||||
<string name="app_profile_export_to_clipboard">In Zwischenablage exportieren</string>
|
<string name="settings_profile_template_summary">Verwalte die lokale und online Vorlage des App-Profils</string>
|
||||||
<string name="app_profile_template_export_empty">Kann lokale Vorlage nicht finden!</string>
|
|
||||||
<string name="app_profile_template_id_exist">Vorlagen-ID existiert bereits!</string>
|
|
||||||
<string name="app_profile_import_from_clipboard">Aus Zwischenablage importieren</string>
|
|
||||||
<string name="module_changelog_failed">Konnte Veränderungs-Protokoll nicht laden: %s</string>
|
|
||||||
<string name="app_profile_template_name">Name</string>
|
|
||||||
<string name="app_profile_template_id_invalid">Ungültige Vorlagen-ID</string>
|
|
||||||
<string name="app_profile_template_sync">Online-Vorlagen synchronisieren</string>
|
|
||||||
<string name="app_profile_template_create">Vorlage erstellen</string>
|
<string name="app_profile_template_create">Vorlage erstellen</string>
|
||||||
<string name="app_profile_template_readonly">Schreibgeschützt</string>
|
|
||||||
<string name="app_profile_import_export">Import/Export</string>
|
|
||||||
<string name="app_profile_template_save_failed">Schlug beim Speichern der Vorlage fehl</string>
|
|
||||||
<string name="app_profile_template_edit">Vorlage bearbeiten</string>
|
<string name="app_profile_template_edit">Vorlage bearbeiten</string>
|
||||||
<string name="app_profile_template_id">ID</string>
|
<string name="app_profile_template_id">ID</string>
|
||||||
<string name="settings_profile_template">App-Profil-Vorlage</string>
|
<string name="app_profile_template_id_invalid">Ungültige Vorlagen-ID</string>
|
||||||
|
<string name="app_profile_template_name">Name</string>
|
||||||
<string name="app_profile_template_description">Beschreibung</string>
|
<string name="app_profile_template_description">Beschreibung</string>
|
||||||
<string name="app_profile_template_save">Speichern</string>
|
<string name="app_profile_template_save">Speichern</string>
|
||||||
<string name="settings_profile_template_summary">Verwalte die lokale und online Vorlage des App-Profils</string>
|
|
||||||
<string name="app_profile_template_delete">Löschen</string>
|
<string name="app_profile_template_delete">Löschen</string>
|
||||||
<string name="app_profile_template_import_empty">Zwischenablage ist leer!</string>
|
|
||||||
<string name="app_profile_template_view">Vorlage ansehen</string>
|
<string name="app_profile_template_view">Vorlage ansehen</string>
|
||||||
|
<string name="app_profile_template_readonly">Schreibgeschützt</string>
|
||||||
|
<string name="app_profile_template_id_exist">Vorlagen-ID existiert bereits!</string>
|
||||||
|
<string name="app_profile_import_export">Import/Export</string>
|
||||||
|
<string name="app_profile_import_from_clipboard">Aus Zwischenablage importieren</string>
|
||||||
|
<string name="app_profile_export_to_clipboard">In Zwischenablage exportieren</string>
|
||||||
|
<string name="app_profile_template_export_empty">Kann lokale Vorlage nicht finden!</string>
|
||||||
|
<string name="app_profile_template_import_success">Erfolgreich importiert</string>
|
||||||
|
<string name="app_profile_template_sync">Online-Vorlagen synchronisieren</string>
|
||||||
|
<string name="app_profile_template_save_failed">Schlug beim Speichern der Vorlage fehl</string>
|
||||||
|
<string name="app_profile_template_import_empty">Zwischenablage ist leer!</string>
|
||||||
|
<string name="module_changelog_failed">Konnte Veränderungs-Protokoll nicht laden: %s</string>
|
||||||
|
<string name="settings_check_update">Auf Aktualisierung prüfen</string>
|
||||||
|
<string name="settings_check_update_summary">Prüfe automatisch auf Aktualisierungen, wenn die App geöffnet wird</string>
|
||||||
|
<string name="grant_root_failed">Root-Zugriff konnte nicht gewährt werden!</string>
|
||||||
|
<string name="action">Aktion</string>
|
||||||
|
<string name="open">Öffnen</string>
|
||||||
|
<string name="close">Schließen</string>
|
||||||
<string name="enable_web_debugging">WebView-Debugging aktivieren</string>
|
<string name="enable_web_debugging">WebView-Debugging aktivieren</string>
|
||||||
<string name="enable_web_debugging_summary">Kann zum Fehlerbeheben der WebUI verwendet werden, bitte nur im Notfall aktivieren.</string>
|
<string name="enable_web_debugging_summary">Kann zum Fehlerbeheben der WebUI verwendet werden, bitte nur im Notfall aktivieren.</string>
|
||||||
<string name="select_file_tip">%1$s Partitionsabbild empfohlen</string>
|
|
||||||
<string name="select_kmi">KMI auswählen</string>
|
|
||||||
<string name="install_next">Weiter</string>
|
|
||||||
<string name="direct_install">Direkte Installation (empfohlen)</string>
|
<string name="direct_install">Direkte Installation (empfohlen)</string>
|
||||||
<string name="select_file">Datei auswählen</string>
|
<string name="select_file">Datei auswählen</string>
|
||||||
<string name="install_inactive_slot">In inaktiven Slot installieren (nach OTA)</string>
|
<string name="install_inactive_slot">In inaktiven Slot installieren (nach OTA)</string>
|
||||||
<string name="install_inactive_slot_warning">Nach einem Neustart wird dein Gerät **GEZWUNGEN** in den derzeit inaktiven Slot zu starten!
|
<string name="install_inactive_slot_warning">Nach einem Neustart wird dein Gerät **GEZWUNGEN** in den derzeit inaktiven Slot zu starten!
|
||||||
\nBenutze dies nur nach Fertigstellung des OTA.
|
\nBenutze dies nur nach Fertigstellung des OTA.
|
||||||
\nFortfahren?</string>
|
\nFortfahren?</string>
|
||||||
<string name="grant_root_failed">Root-Zugriff konnte nicht gewährt werden!</string>
|
<string name="install_next">Weiter</string>
|
||||||
<string name="open">Öffnen</string>
|
<string name="select_file_tip">%1$s Partitionsabbild empfohlen</string>
|
||||||
<string name="settings_check_update">Auf Aktualisierung prüfen</string>
|
<string name="select_kmi">KMI auswählen</string>
|
||||||
<string name="settings_check_update_summary">Prüfe automatisch auf Aktualisierungen, wenn die App geöffnet wird</string>
|
|
||||||
<string name="settings_uninstall_temporary">Temporär deinstallieren</string>
|
|
||||||
<string name="settings_uninstall">Deinstallieren</string>
|
<string name="settings_uninstall">Deinstallieren</string>
|
||||||
<string name="settings_uninstall_permanent_message">KernelSU (Root und alle Module) vollständig und dauerhaft deinstallieren.</string>
|
<string name="settings_uninstall_temporary">Temporär deinstallieren</string>
|
||||||
<string name="save_log">Protokolle Speichern</string>
|
|
||||||
<string name="settings_uninstall_permanent">Permanent deinstallieren</string>
|
<string name="settings_uninstall_permanent">Permanent deinstallieren</string>
|
||||||
<string name="settings_restore_stock_image">Standard-Abbild wiederherstellen</string>
|
<string name="settings_restore_stock_image">Standard-Abbild wiederherstellen</string>
|
||||||
<string name="settings_uninstall_temporary_message">KernelSU temporär deinstallieren, originalen Status nach dem nächsten Neustart wiederherstellen.</string>
|
<string name="settings_uninstall_temporary_message">KernelSU temporär deinstallieren, originalen Status nach dem nächsten Neustart wiederherstellen.</string>
|
||||||
|
<string name="settings_uninstall_permanent_message">KernelSU (Root und alle Module) vollständig und dauerhaft deinstallieren.</string>
|
||||||
<string name="settings_restore_stock_image_message">Das Standard Werksabbild wiederherstellen (falls ein Backup existiert), normalerweise vor einem OTA zu verwenden; falls Sie KernelSU deinstallieren müssen, nutzen Sie bitte \"Permanent deinstallieren\".</string>
|
<string name="settings_restore_stock_image_message">Das Standard Werksabbild wiederherstellen (falls ein Backup existiert), normalerweise vor einem OTA zu verwenden; falls Sie KernelSU deinstallieren müssen, nutzen Sie bitte \"Permanent deinstallieren\".</string>
|
||||||
<string name="flashing">Schreibt</string>
|
<string name="flashing">Schreibt</string>
|
||||||
<string name="flash_success">Schreiben erfolgreich</string>
|
<string name="flash_success">Schreiben erfolgreich</string>
|
||||||
<string name="flash_failed">Schreiben fehlgeschlagen</string>
|
<string name="flash_failed">Schreiben fehlgeschlagen</string>
|
||||||
<string name="selected_lkm">Wähle LKM: %s</string>
|
<string name="selected_lkm">Wähle LKM: %s</string>
|
||||||
<string name="action">Aktion</string>
|
<string name="save_log">Protokolle Speichern</string>
|
||||||
<string name="log_saved">Protokolle gespeichert</string>
|
<string name="log_saved">Protokolle gespeichert</string>
|
||||||
|
<string name="status_supported">Unterstützt:</string>
|
||||||
|
<string name="status_not_supported">Nicht unterstützt</string>
|
||||||
|
<string name="status_unknown">Unbekannt</string>
|
||||||
|
<string name="sus_su_mode">SuS SU-Modus:</string>
|
||||||
|
<!-- Module related -->
|
||||||
|
<string name="module_install_confirm">das Installationsmodul %1$s bestätigen ?</string>
|
||||||
|
<string name="unknown_module">unbekannter Modul</string>
|
||||||
|
<!-- Restore related -->
|
||||||
|
<string name="restore_confirm_title">Modul-Wiederherstellung bestätigen</string>
|
||||||
|
<string name="restore_confirm_message">Diese Operation wird alle vorhandenen Module überschreiben. Fortfahren?</string>
|
||||||
|
<string name="confirm">Bestätigen</string>
|
||||||
|
<string name="cancel">Abbrechen</string>
|
||||||
|
<!-- Backup related -->
|
||||||
|
<string name="backup_success">Sicherung erfolgreich (tar.gz)</string>
|
||||||
|
<string name="backup_failed">Sicherung fehlgeschlagen: %1$s</string>
|
||||||
|
<string name="backup_modules">sicherungsmodule</string>
|
||||||
|
<string name="restore_modules">wiederherstellen</string>
|
||||||
|
<!-- Restore related messages -->
|
||||||
|
<string name="restore_success">Module erfolgreich wiederhergestellt, Neustart erforderlich</string>
|
||||||
|
<string name="restore_failed">Wiederherstellung fehlgeschlagen: %1$s</string>
|
||||||
|
<string name="restart_now">Jetzt Neustarten</string>
|
||||||
|
<string name="unknown_error">Ein unbekannter Fehler ist aufgetreten</string>
|
||||||
|
<!-- Command related -->
|
||||||
|
<string name="command_execution_failed">Befehlsausführung fehlgeschlagen: %1$s</string>
|
||||||
|
<!-- Allowlist related -->
|
||||||
|
<string name="allowlist_backup_success">Sicherung erfolgreich erlaubt</string>
|
||||||
|
<string name="allowlist_backup_failed">Sicherung der erlaubten Liste fehlgeschlagen: %1$s</string>
|
||||||
|
<string name="allowlist_restore_confirm_title">Allowlist-Wiederherstellung bestätigen</string>
|
||||||
|
<string name="allowlist_restore_confirm_message">Dieser Vorgang wird die aktuelle Berechtigungsliste überschreiben. Fortfahren?</string>
|
||||||
|
<string name="allowlist_restore_success">Liste erfolgreich wiederhergestellt</string>
|
||||||
|
<string name="allowlist_restore_failed">Wiederherstellung der erlaubten Liste fehlgeschlagen: %1$s</string>
|
||||||
|
<string name="backup_allowlist">Sicherungsliste</string>
|
||||||
|
<string name="restore_allowlist">Allowlist wiederherstellen</string>
|
||||||
|
<string name="settings_custom_background">Eigener App-Hintergrund</string>
|
||||||
|
<string name="settings_custom_background_summary">Wählen Sie ein Bild als Hintergrund</string>
|
||||||
|
<string name="settings_card_alpha">Transparenz der Navigationsleiste</string>
|
||||||
|
<string name="settings_restore_default">Standard wiederherstellen</string>
|
||||||
|
<string name="home_android_version">Androidversion</string>
|
||||||
|
<string name="home_device_model">Geräteausführung</string>
|
||||||
|
<string name="su_not_allowed">Superuser %s zu erlauben ist nicht erlaubt</string>
|
||||||
|
<string name="settings_disable_su">Su Kompatibilität deaktivieren</string>
|
||||||
|
<string name="settings_disable_su_summary">Deaktivieren Sie temporär alle Anwendungen, die root-Privilegien über den Befehl <unk> su zu erhalten (bestehende root-Prozesse werden nicht beeinflusst).</string>
|
||||||
|
<string name="using_mksu_manager">Sie verwenden den SukiSU Beta-Manager</string>
|
||||||
|
<string name="module_install_multiple_confirm">Sind Sie sicher, dass Sie die ausgewählten %d -Module installieren möchten?</string>
|
||||||
|
<string name="module_install_multiple_confirm_with_names">Möchten Sie die folgenden %1$d Module installieren? \n\n\n%2$s</string>
|
||||||
|
<string name="more_settings">Weitere Einstellungen</string>
|
||||||
|
<string name="selinux">SELinux</string>
|
||||||
|
<string name="selinux_enabled">Aktiviert</string>
|
||||||
|
<string name="selinux_disabled">Deaktiviert</string>
|
||||||
|
<string name="simple_mode">Einfachheit Modus</string>
|
||||||
|
<string name="simple_mode_summary">Versteckt unnötige Karten beim Einschalten</string>
|
||||||
|
<string name="hide_kernel_kernelsu_version">Kernel-Version ausblenden</string>
|
||||||
|
<string name="hide_kernel_kernelsu_version_summary">Kernel-Version ausblenden</string>
|
||||||
|
<string name="hide_other_info">Andere Infos ausblenden</string>
|
||||||
|
<string name="hide_other_info_summary">Versteckt Informationen über die Anzahl der Supernutzer, Module und KPM-Module auf der Startseite</string>
|
||||||
|
<string name="hide_susfs_status">SuSFS-Status ausblenden</string>
|
||||||
|
<string name="hide_susfs_status_summary">SuSFS Statusinformationen auf der Startseite ausblenden</string>
|
||||||
|
<string name="hide_link_card">Link-Kartenstatus ausblenden</string>
|
||||||
|
<string name="hide_link_card_summary">Link Karteninformationen auf der Startseite ausblenden</string>
|
||||||
|
<string name="theme_mode">Thema</string>
|
||||||
|
<string name="theme_follow_system">Systemkonform</string>
|
||||||
|
<string name="theme_light">Licht</string>
|
||||||
|
<string name="theme_dark">Dunkel</string>
|
||||||
|
<string name="manual_hook">Manueller Hook</string>
|
||||||
|
<string name="dynamic_color_title">Dynamische Farbe</string>
|
||||||
|
<string name="dynamic_color_summary">Dynamische Farben mit System-Themes</string>
|
||||||
|
<string name="choose_theme_color">Wähle eine Theme-Farbe</string>
|
||||||
|
<string name="color_default">Blau</string>
|
||||||
|
<string name="color_green">Grün</string>
|
||||||
|
<string name="color_purple">Lila</string>
|
||||||
|
<string name="color_orange">Orange</string>
|
||||||
|
<string name="color_pink">Pink</string>
|
||||||
|
<string name="color_gray">Grau</string>
|
||||||
|
<string name="color_yellow">Gelb</string>
|
||||||
|
<string name="flash_option">Pinseloptionen</string>
|
||||||
|
<string name="flash_option_tip">Wählen Sie die zu flashende Datei</string>
|
||||||
|
<string name="horizon_kernel">Install Anykernel3</string>
|
||||||
|
<string name="horizon_kernel_summary">Flash AnyKernel3 Kernel-Datei</string>
|
||||||
|
<string name="root_required">Erfordert Root-Rechte</string>
|
||||||
|
<string name="copy_failed">Datei-Kopierfehler</string>
|
||||||
|
<string name="reboot_complete_title">Scrubbing abgeschlossen</string>
|
||||||
|
<string name="reboot_complete_msg">Ob sofort neu gestartet werden soll?</string>
|
||||||
|
<string name="yes">Ja</string>
|
||||||
|
<string name="no">Nein</string>
|
||||||
|
<string name="failed_reboot">Neustart fehlgeschlagen</string>
|
||||||
|
<string name="batch_authorization">empowern</string>
|
||||||
|
<string name="batch_cancel_authorization">abheben</string>
|
||||||
|
<string name="backup">Sicherung</string>
|
||||||
|
<string name="kpm_title">KPM</string>
|
||||||
|
<string name="kpm_empty">Keine installierten Kernelmodule</string>
|
||||||
|
<string name="kpm_version">Version</string>
|
||||||
|
<string name="kpm_author">Autor</string>
|
||||||
|
<string name="kpm_uninstall">Deinstallieren</string>
|
||||||
|
<string name="kpm_uninstall_success">Erfolgreich deinstalliert</string>
|
||||||
|
<string name="kpm_uninstall_failed">Deinstallation fehlgeschlagen</string>
|
||||||
|
<string name="kpm_install">Installieren</string>
|
||||||
|
<string name="kpm_install_success">Laden des kpm Moduls erfolgreich</string>
|
||||||
|
<string name="kpm_install_failed">Laden des kpm-Moduls fehlgeschlagen</string>
|
||||||
|
<string name="kpm_args">Parameter</string>
|
||||||
|
<string name="kpm_control">Ausführen</string>
|
||||||
|
<string name="home_kpm_version">KPM-Version</string>
|
||||||
|
<string name="close_notice">Schließen</string>
|
||||||
|
<string name="kernel_module_notice">Die folgenden Kernel-Modulfunktionen wurden von KernelPatch entwickelt und so modifiziert, dass die Funktionen des Kernel-Moduls von SukiSU Ultra enthalten sind</string>
|
||||||
|
<string name="home_ContributionCard_kernelsu">SukiSU Ultra freut sich auf</string>
|
||||||
|
<string name="kpm_control_success">Erfolgreich</string>
|
||||||
|
<string name="kpm_control_failed">Fehlgeschlagen</string>
|
||||||
|
<string name="home_click_to_ContributionCard_kernelsu">SukiSU Ultra wird in Zukunft ein relativ unabhängiger Zweig der KSU sein, aber wir schätzen immer noch die offiziellen KernelSU und MKSU usw. für ihre Beiträge!</string>
|
||||||
|
<string name="not_supported">Nicht unterstützt</string>
|
||||||
|
<string name="supported">Unterstützt:</string>
|
||||||
|
<string name="home_kpm_module">"Anzahl der KPM-Module: %d "</string>
|
||||||
|
<string name="kpm_invalid_file">Ungültige KPM-Datei</string>
|
||||||
|
<string name="kernel_patched">Kernel nicht gepatcht</string>
|
||||||
|
<string name="kernel_not_enabled">Kernel nicht konfiguriert</string>
|
||||||
|
<string name="custom_settings">Eigene Einstellungen</string>
|
||||||
|
<string name="kpm_install_mode">KPM Install</string>
|
||||||
|
<string name="kpm_install_mode_load">Laden</string>
|
||||||
|
<string name="kpm_install_mode_embed">Einbetten</string>
|
||||||
|
<string name="kpm_install_mode_description">Bitte wählen: %1\$s Modul-Installationsmodus \n\nLaden: Das Modul \ntemporär laden: Dauerhaft in das System installieren</string>
|
||||||
|
<string name="log_failed_to_check_module_file">Fehler beim Prüfen der Moduldatei-Existenz</string>
|
||||||
|
<string name="snackbar_failed_to_check_module_file">Kann nicht überprüfen, ob die Moduldatei existiert</string>
|
||||||
|
<string name="confirm_uninstall_title">Deinstallation bestätigen.</string>
|
||||||
|
<string name="confirm_uninstall_confirm">Deinstallieren</string>
|
||||||
|
<string name="confirm_uninstall_dismiss">Abbrechen</string>
|
||||||
|
<string name="theme_color">Themenfarbe</string>
|
||||||
|
<string name="invalid_file_type">Falscher Dateityp! Bitte wählen Sie eine .kpm Datei.</string>
|
||||||
|
<string name="confirm_uninstall_title_with_filename">Deinstallieren</string>
|
||||||
|
<string name="confirm_uninstall_content">Folgende KPM wird deinstalliert: %s</string>
|
||||||
|
<string name="settings_susfs_toggle_summary">Deaktiviere kprobe Hooks die von KernelSU erstellt wurden und stattdessen inline Hooks verwenden, was der Nicht-GKI-Kernel-Hooking Methode ähnlich ist.</string>
|
||||||
|
<string name="image_editor_title">Hintergrundbild anpassen</string>
|
||||||
|
<string name="image_editor_hint">Verwende zwei Finger um das Bild zu vergrößern und einen Finger um die Position anzupassen</string>
|
||||||
|
<string name="background_image_error">Bild konnte nicht geladen werden</string>
|
||||||
|
<string name="reprovision">Rückzahlung</string>
|
||||||
|
<!-- Kernel Flash Progress Related -->
|
||||||
|
<string name="horizon_flash_title">Kernel-Flashen</string>
|
||||||
|
<string name="horizon_logs_label">Logs:</string>
|
||||||
|
<string name="horizon_flash_complete">Blitz abgeschlossen</string>
|
||||||
|
<!-- Flash Status Related -->
|
||||||
|
<string name="horizon_preparing">Vorbereiten…</string>
|
||||||
|
<string name="horizon_cleaning_files">Bereinigung von Dateien…</string>
|
||||||
|
<string name="horizon_copying_files">Kopiere Datei…</string>
|
||||||
|
<string name="horizon_extracting_tool">Entpacken des Flash-Tools…</string>
|
||||||
|
<string name="horizon_patching_script">Patcht Flash-Skript…</string>
|
||||||
|
<string name="horizon_flashing">Flashen des Kernels…</string>
|
||||||
|
<string name="horizon_flash_complete_status">Blitz abgeschlossen</string>
|
||||||
|
<!-- Slot selection related strings -->
|
||||||
|
<string name="select_slot_title">Wähle Flash Slot</string>
|
||||||
|
<string name="select_slot_description">Bitte wählen Sie den Ziel-Slot zum Blinken des Boots aus</string>
|
||||||
|
<string name="slot_a">Slot A</string>
|
||||||
|
<string name="slot_b">Steckplatz B</string>
|
||||||
|
<string name="selected_slot">Wähle LKM: %1$s</string>
|
||||||
|
<string name="horizon_getting_original_slot">Den ursprünglichen Slot erhalten</string>
|
||||||
|
<string name="horizon_setting_target_slot">Setze den angegebenen Slot</string>
|
||||||
|
<string name="horizon_restoring_original_slot">Standard wiederherstellen</string>
|
||||||
|
<string name="current_slot">Aktueller Slot:%1$s </string>
|
||||||
|
<!-- Error Messages -->
|
||||||
|
<string name="horizon_copy_failed">Kopieren fehlgeschlagen</string>
|
||||||
|
<string name="horizon_unknown_error">Ein unbekannter Fehler ist aufgetreten</string>
|
||||||
|
<string name="flash_failed_message">Schreiben fehlgeschlagen</string>
|
||||||
|
<!-- lkm/gki install -->
|
||||||
|
<string name="Lkm_install_methods">LKM Reparatur/Installation</string>
|
||||||
|
<string name="GKI_install_methods">Flashing AnyKernel3</string>
|
||||||
|
<string name="kernel_version_log">Kernel</string>
|
||||||
|
<string name="tool_version_log">Benutze das Patchwerkzeug:%1$s</string>
|
||||||
|
<string name="configuration">Konfigurieren</string>
|
||||||
|
<string name="app_settings">Anwendungs-Einstellungen</string>
|
||||||
|
<string name="tools">Tools</string>
|
||||||
|
<string name="currently_selected">Derzeit</string>
|
||||||
|
<!-- String resources used in SuperUser -->
|
||||||
|
<string name="clear">Entfernen</string>
|
||||||
|
<string name="apps_with_root">Anwendungen mit Root-Rechten</string>
|
||||||
|
<string name="apps_with_custom_profile">Anwendungen mit angepassten Konfigurationen</string>
|
||||||
|
<string name="other_apps">Anwendungen mit unveränderten Standardeinstellungen</string>
|
||||||
|
<string name="no_apps_found">Anwendung nicht gefunden</string>
|
||||||
|
<string name="selinux_enabled_toast">SELinux aktiviert</string>
|
||||||
|
<string name="selinux_disabled_toast">SELinux deaktiviert</string>
|
||||||
|
<string name="selinux_change_failed">SELinux Statusänderung fehlgeschlagen</string>
|
||||||
|
<string name="advanced_settings">Erweiterte Einstellungen</string>
|
||||||
|
<string name="appearance_settings">Passt die Symbolleiste an.</string>
|
||||||
|
<string name="back">Comeback</string>
|
||||||
|
<string name="expand">Seien Sie in vollem Gange</string>
|
||||||
|
<string name="collapse">wegziehen</string>
|
||||||
|
<string name="susfs_enabled">SuSFS aktiviert</string>
|
||||||
|
<string name="susfs_disabled">SuSFS deaktiviert</string>
|
||||||
|
<string name="background_set_success">Hintergrund erfolgreich gesetzt</string>
|
||||||
|
<string name="background_removed">Eigene Hintergründe entfernt</string>
|
||||||
|
<string name="root_require_for_install">Erfordert Root-Rechte</string>
|
||||||
|
<!-- KPM display settings -->
|
||||||
|
<string name="show_kpm_info">KPM-Funktion anzeigen</string>
|
||||||
|
<string name="show_kpm_info_summary">KPM-Informationen und Funktion in der Home- und unteren Leiste anzeigen (muss die App erneut öffnen)</string>
|
||||||
|
<!-- Webui X settings -->
|
||||||
|
<string name="use_webuix">Select the WebUI engine to use</string>
|
||||||
|
<string name="engine_auto_select">Automatic Selection</string>
|
||||||
|
<string name="engine_force_webuix">Force the use of WebUI X</string>
|
||||||
|
<string name="engine_force_ksu">Mandatory use of KSU WebUI</string>
|
||||||
|
<string name="use_webuix_eruda">Eruda in WebUI X injizieren</string>
|
||||||
|
<string name="use_webuix_eruda_summary">Fügen Sie eine Debug-Konsole in WebUI X ein, um das Debuggen zu vereinfachen. Benötigt Debugging im WebUI X.</string>
|
||||||
|
<!-- DPI setting related strings -->
|
||||||
|
<string name="dpi_settings">DPI-Einstellung</string>
|
||||||
|
<string name="app_dpi_title">Angewendeter DPI</string>
|
||||||
|
<string name="app_dpi_summary">Bildschirmanzahl nur für die aktuelle Anwendung anpassen</string>
|
||||||
|
<string name="dpi_size_small">Klein </string>
|
||||||
|
<string name="dpi_size_medium">Mittel </string>
|
||||||
|
<string name="dpi_size_large">Groß</string>
|
||||||
|
<string name="dpi_size_extra_large">übergröße</string>
|
||||||
|
<string name="dpi_size_custom">anpassbar</string>
|
||||||
|
<string name="dpi_apply_settings">DPI-Einstellungen anwenden</string>
|
||||||
|
<string name="dpi_confirm_title">DPI-Änderung bestätigen</string>
|
||||||
|
<string name="dpi_confirm_message">Sind Sie sicher, dass Sie die Anwendung DPI von %1$d auf %2$d ändern möchten?</string>
|
||||||
|
<string name="dpi_confirm_summary">Die Anwendung muss neu gestartet werden, um die neuen DPI-Einstellungen zu übernehmen, hat keine Auswirkungen auf die System-Statusleiste oder andere Anwendungen</string>
|
||||||
|
<string name="dpi_applied_success">DPI wurde auf %1$dgesetzt, wirksam nach dem Neustart der Anwendung</string>
|
||||||
|
<!-- Language settings related strings -->
|
||||||
|
<string name="language_setting">App Sprache</string>
|
||||||
|
<string name="language_follow_system">Folge Systemeinstellung</string>
|
||||||
|
<string name="language_changed">Sprache geändert, Neustart um Änderungen zu übernehmen</string>
|
||||||
|
<string name="settings_card_dim">Kartenfinsternis Anpassung</string>
|
||||||
|
<!-- Super User Related -->
|
||||||
|
<string name="scroll_to_top">Top</string>
|
||||||
|
<string name="scroll_to_bottom">Unten</string>
|
||||||
|
<string name="scroll_to_top_description">Bildlauf nach oben scrollen</string>
|
||||||
|
<string name="scroll_to_bottom_description">Scrolle zum Ende</string>
|
||||||
|
<string name="authorized">Autorisiert</string>
|
||||||
|
<string name="unauthorized">Unberechtigt</string>
|
||||||
|
<string name="selected">Ausgewählt</string>
|
||||||
|
<string name="select">variieren</string>
|
||||||
|
<string name="profile_umount_modules_disable">Eigenes Deinstallationsmodul deaktivieren</string>
|
||||||
|
<!-- Flash related -->
|
||||||
|
<string name="error_code">fehlercode</string>
|
||||||
|
<string name="check_log">Bitte überprüfen Sie das Log</string>
|
||||||
|
<string name="installing_module">Modul wird installiert %1$d/%2$d</string>
|
||||||
|
<string name="module_failed_count">%d Fehler bei der Installation eines neuen Moduls</string>
|
||||||
|
<string name="module_download_error">Modul-Download fehlgeschlagen</string>
|
||||||
|
<string name="kernel_flashing">Kernel Flashing</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||