Compare commits
141 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fa060dca58 | ||
|
|
9c7ba5b998 | ||
|
|
061136900a | ||
|
|
6375bf4b7c | ||
|
|
17288c086a | ||
|
|
15747ceaa5 | ||
|
|
675bb20f52 | ||
|
|
ec0b26a174 | ||
|
|
92f6f2f51e | ||
|
|
587e73b449 | ||
|
|
07c9cce4b9 | ||
|
|
1d34ea4995 | ||
|
|
d58ec6952c | ||
|
|
50631aade6 | ||
|
|
6df8f6f5d4 | ||
|
|
4aee26b48e | ||
|
|
3bbe415c7e | ||
|
|
892fa9040f | ||
|
|
cadc123eab | ||
|
|
3a27537648 | ||
|
|
6fa1a5c8b8 | ||
|
|
b772c8ece1 | ||
|
|
c0e839dd8e | ||
|
|
a6ed7befdc | ||
|
|
c210b00d54 | ||
|
|
13b5290598 | ||
|
|
b99516da69 | ||
|
|
fe8b5f2135 | ||
|
|
04e1b9bf77 | ||
|
|
b8aaf918fe | ||
|
|
54925188e8 | ||
|
|
3443e48ef1 | ||
|
|
53b3e84890 | ||
|
|
a5b85bfdad | ||
|
|
2817583e3c | ||
|
|
8a6116b4ec | ||
|
|
6a4270787a | ||
|
|
5457a4772b | ||
|
|
ee4c3bb03b | ||
|
|
dd1d17d2cf | ||
|
|
3c353e8f88 | ||
|
|
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 |
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:
|
||||
include:
|
||||
- version: "android12-5.10"
|
||||
sub_level: 233
|
||||
os_patch_level: 2025-02
|
||||
sub_level: 236
|
||||
os_patch_level: 2025-05
|
||||
- version: "android13-5.10"
|
||||
sub_level: 234
|
||||
os_patch_level: 2025-03
|
||||
@@ -36,11 +36,11 @@ jobs:
|
||||
sub_level: 178
|
||||
os_patch_level: 2025-03
|
||||
- version: "android14-6.1"
|
||||
sub_level: 129
|
||||
os_patch_level: 2025-04
|
||||
sub_level: 134
|
||||
os_patch_level: 2025-05
|
||||
- version: "android15-6.6"
|
||||
sub_level: 82
|
||||
os_patch_level: 2025-04
|
||||
sub_level: 87
|
||||
os_patch_level: 2025-05
|
||||
# uses: ./.github/workflows/gki-kernel-mock.yml when debugging
|
||||
uses: ./.github/workflows/gki-kernel.yml
|
||||
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
|
||||
- target: x86_64-linux-android
|
||||
os: ubuntu-latest
|
||||
- target: armv7-linux-androideabi
|
||||
os: ubuntu-latest
|
||||
uses: ./.github/workflows/ksud.yml
|
||||
with:
|
||||
target: ${{ matrix.target }}
|
||||
@@ -198,12 +200,20 @@ jobs:
|
||||
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: |
|
||||
|
||||
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
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,6 +1,6 @@
|
||||
# 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)
|
||||
|
||||
@@ -10,35 +10,58 @@ Android device root solution based on [KernelSU](https://github.com/tiann/Kernel
|
||||
>
|
||||
> 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
|
||||
|
||||
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
|
||||
|
||||
1. Use the susfs-dev branch directly without any patching
|
||||
1. Use the susfs-dev branch directly without any patching (Support for non-GKI device builds)
|
||||
|
||||
## KPM support
|
||||
```
|
||||
curl -LSs "https://raw.githubusercontent.com/SukiSU-Ultra/SukiSU-Ultra/main/kernel/setup.sh" | bash -s susfs-dev
|
||||
```
|
||||
|
||||
- We have removed duplicate KSU functions based on KernelPatch and retained KPM support.
|
||||
## KPM Support
|
||||
|
||||
- Based on KernelPatch, we have removed duplicates of KSU and kept only KPM support.
|
||||
- We will introduce more APatch-compatible functions to ensure the integrity of KPM functionality.
|
||||
|
||||
Open source address: https://github.com/ShirkNeko/SukiSU_KernelPatch_patch
|
||||
We will introduce more APatch-compatible functions to ensure the completeness of KPM functionality.
|
||||
|
||||
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
|
||||
|
||||
**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
|
||||
- [GKI](https://github.com/ShirkNeko/GKI_KernelSU_SUSFS)
|
||||
- [OnePlus](https://github.com/ShirkNeko/Action_OnePlus_MKSU_SUSFS)
|
||||
@@ -59,11 +82,14 @@ Projects compiled based on Sukisu and susfs
|
||||
|
||||
## Usage
|
||||
|
||||
### GKI
|
||||
### Universal GKI
|
||||
|
||||
Please follow this guide.
|
||||
Please **all** refer to https://kernelsu.org/zh_CN/guide/installation.html
|
||||
|
||||
https://kernelsu.org/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
|
||||
@@ -84,10 +110,17 @@ https://kernelsu.org/guide/installation.html
|
||||
5. More customization
|
||||
6. Support for KPM kernel modules
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
1. Uninstalling the KernelSU Manager device is stuck. → Uninstall the application with package name com.sony.playmemories.mobile.
|
||||
|
||||
## 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(?!.*alt.*).*` 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
|
||||
|
||||
@@ -96,8 +129,8 @@ https://kernelsu.org/guide/installation.html
|
||||
- [wswzgdg](https://github.com/wswzgdg) Many thanks for supporting this project
|
||||
- [yspbwx2010](https://github.com/yspbwx2010) Many thanks
|
||||
- [DARKWWEE](https://github.com/DARKWWEE) Thanks for the 100 USDT Lao
|
||||
|
||||
If the above list does not have your name, I will update it as soon as possible, and thanks again for your support!
|
||||
- [Saksham Singla](https://github.com/TypeFlu) Website provision as well as maintenance
|
||||
- [OukaroMF](https://github.com/OukaroMF) Donation of website domain name
|
||||
|
||||
## Contributions
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 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 ソリューション
|
||||
|
||||
@@ -11,22 +11,24 @@
|
||||
>
|
||||
> ただし、将来的には 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 の使い方
|
||||
|
||||
1. パッチを当てずに susfs-dev ブランチを直接使用してください。
|
||||
1. パッチを当てずに susfs-dev ブランチを直接使用してください (非GKIデバイスビルドをサポート)
|
||||
```
|
||||
curl -LSs "https://raw.githubusercontent.com/SukiSU-Ultra/SukiSU-Ultra/main/kernel/setup.sh" | bash -s susfs-dev
|
||||
```
|
||||
|
||||
## KPM に対応
|
||||
|
||||
@@ -37,9 +39,28 @@ curl -LSs "https://raw.githubusercontent.com/ShirkNeko/KernelSU/main/kernel/setu
|
||||
|
||||
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)
|
||||
- [OnePlus](https://github.com/ShirkNeko/Action_OnePlus_MKSU_SUSFS)
|
||||
@@ -61,12 +82,14 @@ SukiSU と susfs をベースにコンパイルされたプロジェクトです
|
||||
|
||||
## 使い方
|
||||
|
||||
### GKI
|
||||
### ユニバーサルGKI
|
||||
|
||||
このガイドに従ってください。
|
||||
|
||||
https://kernelsu.org/ja_JP/guide/installation.html
|
||||
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
|
||||
|
||||
1. `その他のリンク`の項目に記載されているリンクを開き、デバイス情報を使用してカスタマイズされたカーネルをビルドし、AnyKernel3 の接頭辞を持つ .zip ファイルをフラッシュします。
|
||||
@@ -85,10 +108,15 @@ https://kernelsu.org/ja_JP/guide/installation.html
|
||||
5. その他のカスタマイズ
|
||||
6. KPM カーネルモジュールに対応
|
||||
|
||||
## トラブルシューティング
|
||||
|
||||
1.KernelSU Managerのアンインストールができない。 →パッケージ名com.sony.playmemories.mobile.KernelSU Managerのアプリケーションをアンインストールする。
|
||||
|
||||
## ライセンス
|
||||
|
||||
- “kernel” ディレクトリ内のファイルは [GPL-2.0](https://www.gnu.org/licenses/old-licenses/gpl-2.0.ja.html) のみライセンス下にあります。
|
||||
- “kernel” ディレクトリを除くその他すべての部分は [GPL-3.0 またはそれ以降](https://www.gnu.org/licenses/gpl-3.0.html) のライセンス下にあります。
|
||||
- `kernel` ディレクトリ以下のファイルは[GPL-2.0-only](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html)です。
|
||||
- アニメキャラクターの絵文字を含むファイル `ic_launcher(?!.*alt.*).*` の画像は[五十根大虾仁](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)です。
|
||||
|
||||
## スポンサーシップの一覧
|
||||
|
||||
@@ -97,8 +125,8 @@ https://kernelsu.org/ja_JP/guide/installation.html
|
||||
- [wswzgdg](https://github.com/wswzgdg) このプロジェクトを支援していただき、ありがとうございます。
|
||||
- [yspbwx2010](https://github.com/yspbwx2010) どうもありがとう。
|
||||
- [DARKWWEE](https://github.com/DARKWWEE) ラオウ100USDTありがとう!
|
||||
|
||||
上記の一覧にあなたの名前がない場合は、できるだけ早急に更新しますので再度ご支援をお願いします。
|
||||
- [Saksham Singla](https://github.com/TypeFlu) ウェブサイトの提供およびメンテナンス
|
||||
- [OukaroMF](https://github.com/OukaroMF) ウェブサイトドメイン名の寄付
|
||||
|
||||
## 貢献者
|
||||
|
||||
|
||||
149
docs/README-tr.md
Normal file
@@ -0,0 +1,149 @@
|
||||
# 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(?!.*alt.*).*` 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
|
||||
|
||||
## 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
|
||||
@@ -1,6 +1,6 @@
|
||||
# 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 解决方案
|
||||
|
||||
@@ -14,25 +14,32 @@
|
||||
|
||||
在内核源码的根目录下执行以下命令:
|
||||
|
||||
使用 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
|
||||
```
|
||||
|
||||
使用 main 分支
|
||||
使用支持非 GKI 设备的分支 (需要手动集成 susfs)
|
||||
|
||||
```
|
||||
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
|
||||
|
||||
1. 直接使用 susfs-stable 或者 susfs-dev 分支,不需要再集成 susfs
|
||||
1. 直接使用 susfs-stable 或者 susfs-dev 分支,不需要再集成 susfs (支持非 GKI 设备构建)
|
||||
|
||||
```
|
||||
curl -LSs "https://raw.githubusercontent.com/SukiSU-Ultra/SukiSU-Ultra/main/kernel/setup.sh" | bash -s susfs-dev
|
||||
```
|
||||
|
||||
## 钩子方法
|
||||
|
||||
- 此部分引用自 [rsuntk 的钩子方法](https://github.com/rsuntk/KernelSU)
|
||||
|
||||
1. **KPROBES 钩子:**
|
||||
|
||||
- 用于可加载内核模块 (LKM)
|
||||
- GKI 2.0 内核的默认钩子方法
|
||||
- 需要 `CONFIG_KPROBES=y`
|
||||
@@ -52,9 +59,33 @@ curl -LSs "https://raw.githubusercontent.com/ShirkNeko/SukiSU-Ultra/main/kernel/
|
||||
|
||||
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 编译的项目
|
||||
|
||||
- [GKI](https://github.com/ShirkNeko/GKI_KernelSU_SUSFS)
|
||||
- [一加](https://github.com/ShirkNeko/Action_OnePlus_MKSU_SUSFS)
|
||||
|
||||
@@ -65,16 +96,17 @@ KPM 模板地址: https://github.com/udochina/KPM-Build-Anywhere
|
||||
请**全部**参考 https://kernelsu.org/zh_CN/guide/installation.html
|
||||
|
||||
> [!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 后缀的压缩包即可
|
||||
|
||||
> [!Note]
|
||||
>
|
||||
> - 内核版本只需要填写前两位即可,如 5.10,5.15,6.1,6.6
|
||||
> - 处理器代号请自行搜索,一般为全英文不带数字的代号
|
||||
> - 分支和配置文件请自行到一加内核开源地址进行填写
|
||||
@@ -88,10 +120,18 @@ KPM 模板地址: https://github.com/udochina/KPM-Build-Anywhere
|
||||
5. 更多自定义功能
|
||||
6. 对 KPM 内核模块的支持
|
||||
|
||||
## 疑难解答
|
||||
1. 卸载 KernelSU 管理器设备卡死。→ 卸载包名为 com.sony.playmemories.mobile 的应用。
|
||||
|
||||
## 许可证
|
||||
|
||||
- `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(?!.*alt.*).*` 的图像版权为[五十根大虾仁](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
|
||||
|
||||
## 赞助名单
|
||||
|
||||
@@ -100,8 +140,8 @@ KPM 模板地址: https://github.com/udochina/KPM-Build-Anywhere
|
||||
- [wswzgdg](https://github.com/wswzgdg) 非常感谢对此项目的支持
|
||||
- [yspbwx2010](https://github.com/yspbwx2010) 非常感谢
|
||||
- [DARKWWEE](https://github.com/DARKWWEE) 感谢老哥的 100 USDT
|
||||
|
||||
如果以上名单没有你的名称,我会及时更新,再次感谢大家的支持
|
||||
- [Saksham Singla](https://github.com/TypeFlu) 网站的提供以及维护
|
||||
- [OukaroMF](https://github.com/OukaroMF) 网站域名捐赠
|
||||
|
||||
## 贡献
|
||||
|
||||
|
||||
@@ -16,20 +16,13 @@ config KSU_DEBUG
|
||||
help
|
||||
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
|
||||
bool "Enable SukiSU KPM"
|
||||
depends on KSU && 64BIT
|
||||
default n
|
||||
help
|
||||
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.
|
||||
but it may affect system stability.
|
||||
|
||||
|
||||
endmenu
|
||||
|
||||
@@ -226,6 +226,7 @@ int ksu_handle_rename(struct dentry *old_dentry, struct dentry *new_dentry)
|
||||
return 0;
|
||||
}
|
||||
|
||||
#ifdef CONFIG_EXT4_FS
|
||||
static void nuke_ext4_sysfs() {
|
||||
struct path path;
|
||||
int err = kern_path("/data/adb/modules", 0, &path);
|
||||
@@ -243,6 +244,9 @@ static void nuke_ext4_sysfs() {
|
||||
|
||||
ext4_unregister_sysfs(sb);
|
||||
}
|
||||
#else
|
||||
static inline void nuke_ext4_sysfs() { }
|
||||
#endif
|
||||
|
||||
int ksu_handle_prctl(int option, unsigned long arg2, unsigned long arg3,
|
||||
unsigned long arg4, unsigned long arg5)
|
||||
@@ -425,6 +429,13 @@ int ksu_handle_prctl(int option, unsigned long arg2, unsigned long arg3,
|
||||
}
|
||||
#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'
|
||||
if (!from_manager) {
|
||||
return 0;
|
||||
|
||||
@@ -167,7 +167,11 @@ DYNAMIC_STRUCT_BEGIN(task_struct)
|
||||
DEFINE_MEMBER(task_struct, group_leader)
|
||||
DEFINE_MEMBER(task_struct, 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)
|
||||
#endif
|
||||
DEFINE_MEMBER(task_struct, files)
|
||||
DEFINE_MEMBER(task_struct, seccomp)
|
||||
#ifdef CONFIG_THREAD_INFO_IN_TASK
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
#define CMD_UID_SHOULD_UMOUNT 13
|
||||
#define CMD_IS_SU_ENABLED 14
|
||||
#define CMD_ENABLE_SU 15
|
||||
#define CMD_ENABLE_KPM 100
|
||||
|
||||
#define EVENT_POST_FS_DATA 1
|
||||
#define EVENT_BOOT_COMPLETED 2
|
||||
|
||||
@@ -63,6 +63,10 @@ u32 ksu_devpts_sid;
|
||||
// Detect whether it is on or not
|
||||
static bool is_boot_phase = true;
|
||||
|
||||
#ifdef CONFIG_COMPAT
|
||||
bool ksu_is_compat __read_mostly = false;
|
||||
#endif
|
||||
|
||||
void on_post_fs_data(void)
|
||||
{
|
||||
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))
|
||||
return ERR_PTR(-EFAULT);
|
||||
|
||||
ksu_is_compat = true;
|
||||
return compat_ptr(compat);
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -137,17 +137,45 @@ void apply_kernelsu_rules()
|
||||
#define CMD_TYPE_CHANGE 8
|
||||
#define CMD_GENFSCON 9
|
||||
|
||||
#ifdef CONFIG_64BIT
|
||||
struct sepol_data {
|
||||
u32 cmd;
|
||||
u32 subcmd;
|
||||
char __user *sepol1;
|
||||
char __user *sepol2;
|
||||
char __user *sepol3;
|
||||
char __user *sepol4;
|
||||
char __user *sepol5;
|
||||
char __user *sepol6;
|
||||
char __user *sepol7;
|
||||
u64 field_sepol1;
|
||||
u64 field_sepol2;
|
||||
u64 field_sepol3;
|
||||
u64 field_sepol4;
|
||||
u64 field_sepol5;
|
||||
u64 field_sepol6;
|
||||
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,
|
||||
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");
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
u32 cmd = data.cmd;
|
||||
u32 subcmd = data.subcmd;
|
||||
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;
|
||||
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;
|
||||
#endif
|
||||
|
||||
rcu_read_lock();
|
||||
|
||||
@@ -213,22 +285,22 @@ int handle_sepolicy(unsigned long arg3, void __user *arg4)
|
||||
char perm_buf[MAX_SEPOL_LEN];
|
||||
|
||||
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");
|
||||
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");
|
||||
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");
|
||||
goto exit;
|
||||
}
|
||||
|
||||
if (get_object(perm_buf, data.sepol4, sizeof(perm_buf), &p) <
|
||||
if (get_object(perm_buf, sepol4, sizeof(perm_buf), &p) <
|
||||
0) {
|
||||
pr_err("sepol: copy perm failed.\n");
|
||||
goto exit;
|
||||
@@ -258,24 +330,24 @@ int handle_sepolicy(unsigned long arg3, void __user *arg4)
|
||||
char perm_set[MAX_SEPOL_LEN];
|
||||
|
||||
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");
|
||||
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");
|
||||
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");
|
||||
goto exit;
|
||||
}
|
||||
if (strncpy_from_user(operation, data.sepol4,
|
||||
if (strncpy_from_user(operation, sepol4,
|
||||
sizeof(operation)) < 0) {
|
||||
pr_err("sepol: copy operation failed.\n");
|
||||
goto exit;
|
||||
}
|
||||
if (strncpy_from_user(perm_set, data.sepol5, sizeof(perm_set)) <
|
||||
if (strncpy_from_user(perm_set, sepol5, sizeof(perm_set)) <
|
||||
0) {
|
||||
pr_err("sepol: copy perm_set failed.\n");
|
||||
goto exit;
|
||||
@@ -295,7 +367,7 @@ int handle_sepolicy(unsigned long arg3, void __user *arg4)
|
||||
} else if (cmd == CMD_TYPE_STATE) {
|
||||
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");
|
||||
goto exit;
|
||||
}
|
||||
@@ -315,11 +387,11 @@ int handle_sepolicy(unsigned long arg3, void __user *arg4)
|
||||
char type[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");
|
||||
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");
|
||||
goto exit;
|
||||
}
|
||||
@@ -339,7 +411,7 @@ int handle_sepolicy(unsigned long arg3, void __user *arg4)
|
||||
} else if (cmd == CMD_ATTR) {
|
||||
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");
|
||||
goto exit;
|
||||
}
|
||||
@@ -356,28 +428,28 @@ int handle_sepolicy(unsigned long arg3, void __user *arg4)
|
||||
char default_type[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");
|
||||
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");
|
||||
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");
|
||||
goto exit;
|
||||
}
|
||||
if (strncpy_from_user(default_type, data.sepol4,
|
||||
if (strncpy_from_user(default_type, sepol4,
|
||||
sizeof(default_type)) < 0) {
|
||||
pr_err("sepol: copy default_type failed.\n");
|
||||
goto exit;
|
||||
}
|
||||
char *real_object;
|
||||
if (data.sepol5 == NULL) {
|
||||
if (sepol5 == NULL) {
|
||||
real_object = NULL;
|
||||
} else {
|
||||
if (strncpy_from_user(object, data.sepol5,
|
||||
if (strncpy_from_user(object, sepol5,
|
||||
sizeof(object)) < 0) {
|
||||
pr_err("sepol: copy object failed.\n");
|
||||
goto exit;
|
||||
@@ -396,19 +468,19 @@ int handle_sepolicy(unsigned long arg3, void __user *arg4)
|
||||
char cls[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");
|
||||
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");
|
||||
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");
|
||||
goto exit;
|
||||
}
|
||||
if (strncpy_from_user(default_type, data.sepol4,
|
||||
if (strncpy_from_user(default_type, sepol4,
|
||||
sizeof(default_type)) < 0) {
|
||||
pr_err("sepol: copy default_type failed.\n");
|
||||
goto exit;
|
||||
@@ -429,15 +501,15 @@ int handle_sepolicy(unsigned long arg3, void __user *arg4)
|
||||
char name[MAX_SEPOL_LEN];
|
||||
char path[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");
|
||||
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");
|
||||
goto exit;
|
||||
}
|
||||
if (strncpy_from_user(context, data.sepol3, sizeof(context)) <
|
||||
if (strncpy_from_user(context, sepol3, sizeof(context)) <
|
||||
0) {
|
||||
pr_err("sepol: copy context failed.\n");
|
||||
goto exit;
|
||||
|
||||
@@ -41,7 +41,7 @@ setup_kernelsu() {
|
||||
echo "[+] Setting up KernelSU..."
|
||||
# Clone the repository and rename it to KernelSU
|
||||
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
|
||||
echo "[+] Repository cloned and renamed to KernelSU."
|
||||
fi
|
||||
|
||||
@@ -22,6 +22,10 @@
|
||||
|
||||
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)
|
||||
{
|
||||
/* 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;
|
||||
|
||||
#ifndef CONFIG_KPROBES
|
||||
if (!ksu_sucompat_non_kp) {
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (!ksu_is_allow_uid(current_uid().val)) {
|
||||
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 su[] = SU_PATH;
|
||||
|
||||
#ifndef CONFIG_KPROBES
|
||||
if (!ksu_sucompat_non_kp) {
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
if (!ksu_is_allow_uid(current_uid().val)) {
|
||||
return 0;
|
||||
}
|
||||
@@ -115,6 +130,11 @@ int ksu_handle_execveat_sucompat(int *fd, struct filename **filename_ptr,
|
||||
const char sh[] = KSUD_PATH;
|
||||
const char su[] = SU_PATH;
|
||||
|
||||
#ifndef CONFIG_KPROBES
|
||||
if (!ksu_sucompat_non_kp) {
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
if (unlikely(!filename_ptr))
|
||||
return 0;
|
||||
|
||||
@@ -144,6 +164,11 @@ int ksu_handle_execve_sucompat(int *fd, const char __user **filename_user,
|
||||
const char su[] = SU_PATH;
|
||||
char path[sizeof(su) + 1];
|
||||
|
||||
#ifndef CONFIG_KPROBES
|
||||
if (!ksu_sucompat_non_kp){
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
if (unlikely(!filename_user))
|
||||
return 0;
|
||||
|
||||
@@ -237,6 +262,9 @@ void ksu_sucompat_init()
|
||||
su_kps[0] = init_kprobe(SYS_EXECVE_SYMBOL, execve_handler_pre);
|
||||
su_kps[1] = init_kprobe(SYS_FACCESSAT_SYMBOL, faccessat_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
|
||||
}
|
||||
|
||||
@@ -246,5 +274,8 @@ void ksu_sucompat_exit()
|
||||
for (int i = 0; i < ARRAY_SIZE(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
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <linux/string.h>
|
||||
#include <linux/types.h>
|
||||
#include <linux/version.h>
|
||||
#include <linux/namei.h>
|
||||
|
||||
#include "allowlist.h"
|
||||
#include "klog.h" // IWYU pragma: keep
|
||||
@@ -115,6 +116,7 @@ struct my_dir_context {
|
||||
void *private_data;
|
||||
int depth;
|
||||
int *stop;
|
||||
struct super_block* root_sb;
|
||||
};
|
||||
// https://docs.kernel.org/filesystems/porting.html
|
||||
// filldir_t (readdir callbacks) calling conventions have changed. Instead of returning 0 or -E... it returns bool now. false means "no more" (as -E... used to) and true - "keep going" (as 0 in old calling conventions). Rationale: callers never looked at specific -E... values anyway. -> iterate_shared() instances require no changes at all, all filldir_t ones in the tree converted.
|
||||
@@ -135,6 +137,8 @@ FILLDIR_RETURN_TYPE my_actor(struct dir_context *ctx, const char *name,
|
||||
struct my_dir_context *my_ctx =
|
||||
container_of(ctx, struct my_dir_context, ctx);
|
||||
char dirpath[DATA_PATH_LEN];
|
||||
int err;
|
||||
struct path path;
|
||||
|
||||
if (!my_ctx) {
|
||||
pr_err("Invalid context\n");
|
||||
@@ -162,6 +166,18 @@ FILLDIR_RETURN_TYPE my_actor(struct dir_context *ctx, const char *name,
|
||||
return FILLDIR_ACTOR_CONTINUE;
|
||||
}
|
||||
|
||||
err = kern_path(dirpath, 0, &path);
|
||||
|
||||
if (err) {
|
||||
pr_err("get dirpath %s err: %d\n", dirpath, err);
|
||||
return FILLDIR_ACTOR_CONTINUE;
|
||||
}
|
||||
|
||||
if (my_ctx->root_sb != path.dentry->d_inode->i_sb) {
|
||||
pr_info("skip cross fs: %s", dirpath);
|
||||
return FILLDIR_ACTOR_CONTINUE;
|
||||
}
|
||||
|
||||
if (d_type == DT_DIR && my_ctx->depth > 0 &&
|
||||
(my_ctx->stop && !*my_ctx->stop)) {
|
||||
struct data_path *data = kmalloc(sizeof(struct data_path), GFP_ATOMIC);
|
||||
@@ -211,10 +227,19 @@ FILLDIR_RETURN_TYPE my_actor(struct dir_context *ctx, const char *name,
|
||||
|
||||
void search_manager(const char *path, int depth, struct list_head *uid_data)
|
||||
{
|
||||
int i, stop = 0;
|
||||
int i, stop = 0, err;
|
||||
struct list_head data_path_list;
|
||||
struct path kpath;
|
||||
struct super_block* root_sb;
|
||||
INIT_LIST_HEAD(&data_path_list);
|
||||
|
||||
err = kern_path(path, 0, &kpath);
|
||||
|
||||
if (err) {
|
||||
pr_err("get search root %s err: %d\n", path, err);
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize APK cache list
|
||||
struct apk_path_hash *pos, *n;
|
||||
list_for_each_entry(pos, &apk_path_hash_list, list) {
|
||||
@@ -227,6 +252,8 @@ void search_manager(const char *path, int depth, struct list_head *uid_data)
|
||||
data.depth = depth;
|
||||
list_add_tail(&data.list, &data_path_list);
|
||||
|
||||
root_sb = kpath.dentry->d_inode->i_sb;
|
||||
|
||||
for (i = depth; i >= 0; i--) {
|
||||
struct data_path *pos, *n;
|
||||
|
||||
@@ -236,7 +263,8 @@ void search_manager(const char *path, int depth, struct list_head *uid_data)
|
||||
.parent_dir = pos->dirpath,
|
||||
.private_data = uid_data,
|
||||
.depth = pos->depth,
|
||||
.stop = &stop };
|
||||
.stop = &stop,
|
||||
.root_sb = root_sb };
|
||||
struct file *file;
|
||||
|
||||
if (!stop) {
|
||||
@@ -358,12 +386,14 @@ void track_throne()
|
||||
if (ksu_is_manager_uid_valid()) {
|
||||
pr_info("manager is uninstalled, invalidate it!\n");
|
||||
ksu_invalidate_manager_uid();
|
||||
goto prune;
|
||||
}
|
||||
pr_info("Searching manager...\n");
|
||||
search_manager("/data/app", 2, &uid_list);
|
||||
pr_info("Search manager finished\n");
|
||||
}
|
||||
|
||||
prune:
|
||||
// then prune the allowlist
|
||||
ksu_prune_allowlist(is_uid_exist, &uid_list);
|
||||
out:
|
||||
|
||||
@@ -50,7 +50,6 @@ android {
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
aidl = true
|
||||
buildConfig = true
|
||||
compose = true
|
||||
prefab = true
|
||||
@@ -109,6 +108,7 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.gson)
|
||||
implementation(libs.androidx.activity.compose)
|
||||
implementation(libs.androidx.navigation.compose)
|
||||
|
||||
@@ -154,4 +154,9 @@ dependencies {
|
||||
|
||||
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 { *; }
|
||||
@@ -32,6 +32,19 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity-alias
|
||||
android:name=".ui.MainActivityAlias"
|
||||
android:exported="true"
|
||||
android:enabled="false"
|
||||
android:icon="@mipmap/ic_launcher_alt"
|
||||
android:roundIcon="@mipmap/ic_launcher_alt_round"
|
||||
android:targetActivity=".ui.MainActivity">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity-alias>
|
||||
|
||||
<activity
|
||||
android:name=".ui.webui.WebUIActivity"
|
||||
android:autoRemoveFromRecents="true"
|
||||
@@ -39,6 +52,13 @@
|
||||
android:exported="false"
|
||||
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
|
||||
android:name="androidx.core.content.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);
|
||||
}
|
||||
@@ -306,3 +306,8 @@ JNIEXPORT jboolean JNICALL
|
||||
Java_com_sukisu_ultra_Natives_setSuEnabled(JNIEnv *env, jobject thiz, jboolean 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_SU_ENABLED 14
|
||||
#define CMD_ENABLE_SU 15
|
||||
#define CMD_ENABLE_KPM 100
|
||||
|
||||
static bool ksuctl(int cmd, void* arg1, void* arg2) {
|
||||
int32_t result = 0;
|
||||
@@ -97,3 +98,8 @@ bool is_su_enabled() {
|
||||
ksuctl(CMD_IS_SU_ENABLED, &enabled, nullptr);
|
||||
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_KPM_enable();
|
||||
|
||||
#endif //KERNELSU_KSU_H
|
||||
|
||||
BIN
manager/app/src/main/ic_launcher-playstore.png
Normal file
|
After Width: | Height: | Size: 87 KiB |
@@ -1,20 +1,97 @@
|
||||
package com.sukisu.ultra
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.app.ActivityOptions
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import android.content.res.Resources
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import coil.Coil
|
||||
import coil.ImageLoader
|
||||
import com.dergoogler.mmrl.platform.Platform
|
||||
import me.zhanghai.android.appiconloader.coil.AppIconFetcher
|
||||
import me.zhanghai.android.appiconloader.coil.AppIconKeyer
|
||||
import java.io.File
|
||||
import java.util.Locale
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
lateinit var ksuApp: KernelSUApplication
|
||||
|
||||
class KernelSUApplication : Application() {
|
||||
private var currentActivity: Activity? = null
|
||||
|
||||
private val activityLifecycleCallbacks = object : ActivityLifecycleCallbacks {
|
||||
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
|
||||
currentActivity = activity
|
||||
}
|
||||
override fun onActivityStarted(activity: Activity) {
|
||||
currentActivity = activity
|
||||
}
|
||||
override fun onActivityResumed(activity: Activity) {
|
||||
currentActivity = activity
|
||||
}
|
||||
override fun onActivityPaused(activity: Activity) {}
|
||||
override fun onActivityStopped(activity: Activity) {}
|
||||
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {}
|
||||
override fun onActivityDestroyed(activity: Activity) {
|
||||
if (currentActivity == activity) {
|
||||
currentActivity = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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() {
|
||||
super.onCreate()
|
||||
ksuApp = this
|
||||
|
||||
// 注册Activity生命周期回调
|
||||
registerActivityLifecycleCallbacks(activityLifecycleCallbacks)
|
||||
|
||||
Platform.setHiddenApiExemptions()
|
||||
|
||||
val context = this
|
||||
val iconSize = resources.getDimensionPixelSize(android.R.dimen.app_icon_size)
|
||||
Coil.setImageLoader(
|
||||
@@ -32,5 +109,43 @@ 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 添加刷新当前Activity的方法
|
||||
fun refreshCurrentActivity() {
|
||||
currentActivity?.let { activity ->
|
||||
val intent = activity.intent
|
||||
activity.finish()
|
||||
|
||||
val options = ActivityOptions.makeCustomAnimation(
|
||||
activity, android.R.anim.fade_in, android.R.anim.fade_out
|
||||
)
|
||||
activity.startActivity(intent, options.toBundle())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,24 +8,13 @@ import android.system.Os
|
||||
*/
|
||||
|
||||
data class KernelVersion(val major: Int, val patchLevel: Int, val subLevel: Int) {
|
||||
override fun toString(): String {
|
||||
return "$major.$patchLevel.$subLevel"
|
||||
}
|
||||
|
||||
fun isGKI(): Boolean {
|
||||
|
||||
// kernel 6.x
|
||||
if (major > 5) {
|
||||
return true
|
||||
}
|
||||
|
||||
// kernel 5.10.x
|
||||
if (major == 5) {
|
||||
return patchLevel >= 10
|
||||
}
|
||||
|
||||
return false
|
||||
override fun toString(): String = "$major.$patchLevel.$subLevel"
|
||||
fun isGKI(): Boolean = when {
|
||||
major > 5 -> true
|
||||
major == 5 && patchLevel >= 10 -> true
|
||||
else -> false
|
||||
}
|
||||
fun isGKI1(): Boolean = (major == 4 && patchLevel >= 19) || (major == 5 && patchLevel < 10)
|
||||
}
|
||||
|
||||
fun parseKernelVersion(version: String): KernelVersion {
|
||||
|
||||
@@ -16,16 +16,18 @@ object Natives {
|
||||
// 10946: add capabilities
|
||||
// 10977: change groups_count and groups to avoid overflow write
|
||||
// 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
|
||||
// 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
|
||||
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 MINIMAL_SUPPORTED_KPM = 12800
|
||||
|
||||
const val ROOT_UID = 0
|
||||
const val ROOT_GID = 0
|
||||
|
||||
@@ -66,6 +68,7 @@ object Natives {
|
||||
*/
|
||||
external fun isSuEnabled(): Boolean
|
||||
external fun setSuEnabled(enabled: Boolean): Boolean
|
||||
external fun isKPMEnabled(): Boolean
|
||||
|
||||
private const val NON_ROOT_DEFAULT_PROFILE_KEY = "$"
|
||||
private const val NOBODY_UID = 9999
|
||||
|
||||
@@ -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,52 +1,65 @@
|
||||
package com.sukisu.ultra.ui
|
||||
|
||||
import android.database.ContentObserver
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavBackStackEntry
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import com.ramcosta.composedestinations.DestinationsNavHost
|
||||
import com.ramcosta.composedestinations.animations.NavHostAnimatedDestinationStyle
|
||||
import com.ramcosta.composedestinations.generated.NavGraphs
|
||||
import com.ramcosta.composedestinations.generated.destinations.ExecuteModuleActionScreenDestination
|
||||
import com.ramcosta.composedestinations.spec.NavHostGraphSpec
|
||||
import com.ramcosta.composedestinations.spec.RouteOrDirection
|
||||
import com.ramcosta.composedestinations.utils.isRouteOnBackStackAsState
|
||||
import com.ramcosta.composedestinations.utils.rememberDestinationsNavigator
|
||||
import io.sukisu.ultra.UltraToolInstall
|
||||
import com.sukisu.ultra.Natives
|
||||
import com.sukisu.ultra.ksuApp
|
||||
import com.sukisu.ultra.ui.screen.BottomBarDestination
|
||||
import zako.zako.zako.zakoui.activity.util.AppData
|
||||
import com.sukisu.ultra.ui.theme.*
|
||||
import com.sukisu.ultra.ui.theme.CardConfig.cardAlpha
|
||||
import com.sukisu.ultra.ui.util.*
|
||||
import androidx.core.content.edit
|
||||
import com.sukisu.ultra.ui.theme.CardConfig.cardElevation
|
||||
import zako.zako.zako.zakoui.activity.util.*
|
||||
import zako.zako.zako.zakoui.activity.component.BottomBar
|
||||
import com.sukisu.ultra.ui.util.LocalSnackbarHost
|
||||
import com.sukisu.ultra.ui.util.install
|
||||
import com.sukisu.ultra.ui.viewmodel.HomeViewModel
|
||||
import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel
|
||||
import com.sukisu.ultra.ui.webui.initPlatform
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
private inner class ThemeChangeContentObserver(
|
||||
handler: Handler,
|
||||
private val onThemeChanged: () -> Unit
|
||||
) : ContentObserver(handler) {
|
||||
override fun onChange(selfChange: Boolean) {
|
||||
super.onChange(selfChange)
|
||||
onThemeChanged()
|
||||
}
|
||||
private lateinit var superUserViewModel: SuperUserViewModel
|
||||
private lateinit var homeViewModel: HomeViewModel
|
||||
internal val settingsStateFlow = MutableStateFlow(SettingsState())
|
||||
|
||||
data class SettingsState(
|
||||
val isHideOtherInfo: Boolean = false,
|
||||
val showKpmInfo: Boolean = false
|
||||
)
|
||||
|
||||
private lateinit var themeChangeObserver: ThemeChangeContentObserver
|
||||
|
||||
// 添加标记避免重复初始化
|
||||
private var isInitialized = false
|
||||
|
||||
override fun attachBaseContext(newBase: Context) {
|
||||
val context = LocaleUtils.applyLocale(newBase)
|
||||
super.attachBaseContext(context)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
try {
|
||||
// 确保应用正确的语言设置
|
||||
LocaleUtils.applyLanguageSetting(this)
|
||||
|
||||
// 应用自定义 DPI
|
||||
DisplayUtils.applyCustomDpi(this)
|
||||
|
||||
// Enable edge to edge
|
||||
enableEdgeToEdge()
|
||||
|
||||
@@ -56,194 +69,145 @@ class MainActivity : ComponentActivity() {
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val prefs = getSharedPreferences("settings", MODE_PRIVATE)
|
||||
val isFirstRun = prefs.getBoolean("is_first_run", true)
|
||||
|
||||
if (isFirstRun) {
|
||||
ThemeConfig.preventBackgroundRefresh = false
|
||||
getSharedPreferences("theme_prefs", MODE_PRIVATE).edit {
|
||||
putBoolean("prevent_background_refresh", false)
|
||||
}
|
||||
prefs.edit { putBoolean("is_first_run", false) }
|
||||
}
|
||||
|
||||
// 加载保存的背景设置
|
||||
loadThemeMode()
|
||||
loadThemeColors()
|
||||
loadDynamicColorState()
|
||||
CardConfig.load(applicationContext)
|
||||
|
||||
val contentObserver = ThemeChangeContentObserver(Handler(mainLooper)) {
|
||||
runOnUiThread {
|
||||
if (!ThemeConfig.preventBackgroundRefresh) {
|
||||
ThemeConfig.backgroundImageLoaded = false
|
||||
loadCustomBackground()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
contentResolver.registerContentObserver(
|
||||
android.provider.Settings.System.getUriFor("ui_night_mode"),
|
||||
false,
|
||||
contentObserver
|
||||
)
|
||||
|
||||
val destroyListeners = mutableListOf<() -> Unit>()
|
||||
destroyListeners.add {
|
||||
contentResolver.unregisterContentObserver(contentObserver)
|
||||
}
|
||||
|
||||
val isManager = Natives.becomeManager(ksuApp.packageName)
|
||||
if (isManager) {
|
||||
install()
|
||||
UltraToolInstall.tryToInstall()
|
||||
// 使用标记控制初始化流程
|
||||
if (!isInitialized) {
|
||||
initializeViewModels()
|
||||
initializeData()
|
||||
isInitialized = true
|
||||
}
|
||||
|
||||
setContent {
|
||||
KernelSUTheme {
|
||||
val navController = rememberNavController()
|
||||
val snackBarHostState = remember { SnackbarHostState() }
|
||||
val currentDestination = navController.currentBackStackEntryAsState().value?.destination
|
||||
|
||||
val showBottomBar = when (currentDestination?.route) {
|
||||
ExecuteModuleActionScreenDestination.route -> false
|
||||
else -> true
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
initPlatform()
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
bottomBar = { BottomBar(navController) },
|
||||
contentWindowInsets = WindowInsets(0, 0, 0, 0)
|
||||
) { innerPadding ->
|
||||
CompositionLocalProvider(
|
||||
LocalSnackbarHost provides snackBarHostState
|
||||
) {
|
||||
Scaffold(
|
||||
bottomBar = {
|
||||
AnimatedBottomBar.AnimatedBottomBarWrapper(
|
||||
showBottomBar = showBottomBar,
|
||||
content = { BottomBar(navController) }
|
||||
)
|
||||
},
|
||||
contentWindowInsets = WindowInsets(0, 0, 0, 0)
|
||||
) { innerPadding ->
|
||||
DestinationsNavHost(
|
||||
modifier = Modifier.padding(innerPadding),
|
||||
navGraph = NavGraphs.root as NavHostGraphSpec,
|
||||
navController = navController,
|
||||
defaultTransitions = object : NavHostAnimatedDestinationStyle() {
|
||||
override val enterTransition: AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition
|
||||
get() = { fadeIn(animationSpec = tween(340)) }
|
||||
override val exitTransition: AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition
|
||||
get() = { fadeOut(animationSpec = tween(340)) }
|
||||
}
|
||||
defaultTransitions = NavigationUtils.defaultTransitions()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
private fun initializeViewModels() {
|
||||
superUserViewModel = SuperUserViewModel()
|
||||
homeViewModel = HomeViewModel()
|
||||
|
||||
// 设置主题变化监听器
|
||||
themeChangeObserver = ThemeUtils.registerThemeChangeObserver(this)
|
||||
}
|
||||
|
||||
private fun initializeData() {
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
superUserViewModel.fetchAppList()
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
homeViewModel.initializeData()
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
// 数据刷新协程
|
||||
DataRefreshUtils.startDataRefreshCoroutine(lifecycleScope)
|
||||
DataRefreshUtils.startSettingsMonitorCoroutine(lifecycleScope, this, settingsStateFlow)
|
||||
|
||||
// 初始化主题相关设置
|
||||
ThemeUtils.initializeThemeSettings(this, settingsStateFlow)
|
||||
|
||||
val isManager = AppData.isManager(ksuApp.packageName)
|
||||
if (isManager) {
|
||||
install()
|
||||
UltraToolInstall.tryToInstall()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
try {
|
||||
super.onResume()
|
||||
LocaleUtils.applyLanguageSetting(this)
|
||||
ThemeUtils.onActivityResume()
|
||||
|
||||
// 仅在需要时刷新数据
|
||||
if (isInitialized) {
|
||||
refreshData()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
private fun refreshData() {
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
superUserViewModel.fetchAppList()
|
||||
homeViewModel.initializeData()
|
||||
DataRefreshUtils.refreshData(lifecycleScope)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
try {
|
||||
super.onPause()
|
||||
CardConfig.save(applicationContext)
|
||||
getSharedPreferences("theme_prefs", MODE_PRIVATE).edit() {
|
||||
putBoolean("prevent_background_refresh", true)
|
||||
}
|
||||
ThemeConfig.preventBackgroundRefresh = true
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
if (!ThemeConfig.backgroundImageLoaded && !ThemeConfig.preventBackgroundRefresh) {
|
||||
loadCustomBackground()
|
||||
ThemeUtils.onActivityPause(this)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
private val destroyListeners = mutableListOf<() -> Unit>()
|
||||
|
||||
override fun onDestroy() {
|
||||
destroyListeners.forEach { it() }
|
||||
try {
|
||||
ThemeUtils.unregisterThemeChangeObserver(this, themeChangeObserver)
|
||||
super.onDestroy()
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun BottomBar(navController: NavHostController) {
|
||||
val navigator = navController.rememberDestinationsNavigator()
|
||||
val isManager = Natives.becomeManager(ksuApp.packageName)
|
||||
val fullFeatured = isManager && !Natives.requireNewKernel() && rootAvailable()
|
||||
val kpmVersion = getKpmVersion()
|
||||
val containerColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
val cardColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
|
||||
NavigationBar(
|
||||
modifier = Modifier.windowInsetsPadding(
|
||||
WindowInsets.navigationBars.only(WindowInsetsSides.Horizontal)
|
||||
),
|
||||
containerColor = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = cardColor.copy(alpha = cardAlpha),
|
||||
scrolledContainerColor = containerColor.copy(alpha = cardAlpha)
|
||||
).containerColor,
|
||||
tonalElevation = cardElevation
|
||||
) {
|
||||
BottomBarDestination.entries.forEach { destination ->
|
||||
if (destination == BottomBarDestination.Kpm) {
|
||||
if (kpmVersion.isNotEmpty() && !kpmVersion.startsWith("Error")) {
|
||||
if (!fullFeatured && destination.rootRequired) return@forEach
|
||||
val isCurrentDestOnBackStack by navController.isRouteOnBackStackAsState(destination.direction)
|
||||
NavigationBarItem(
|
||||
selected = isCurrentDestOnBackStack,
|
||||
onClick = {
|
||||
if (!isCurrentDestOnBackStack) {
|
||||
navigator.navigate(destination.direction) {
|
||||
popUpTo(NavGraphs.root as RouteOrDirection) {
|
||||
saveState = true
|
||||
}
|
||||
launchSingleTop = true
|
||||
restoreState = true
|
||||
}
|
||||
}
|
||||
},
|
||||
icon = {
|
||||
Icon(
|
||||
imageVector = if (isCurrentDestOnBackStack) {
|
||||
destination.iconSelected
|
||||
} else {
|
||||
destination.iconNotSelected
|
||||
},
|
||||
contentDescription = stringResource(destination.label),
|
||||
tint = if (isCurrentDestOnBackStack) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
},
|
||||
label = {
|
||||
Text(
|
||||
text = stringResource(destination.label),
|
||||
style = MaterialTheme.typography.labelMedium
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
} else {
|
||||
if (!fullFeatured && destination.rootRequired) return@forEach
|
||||
val isCurrentDestOnBackStack by navController.isRouteOnBackStackAsState(destination.direction)
|
||||
NavigationBarItem(
|
||||
selected = isCurrentDestOnBackStack,
|
||||
onClick = {
|
||||
if (!isCurrentDestOnBackStack) {
|
||||
navigator.navigate(destination.direction) {
|
||||
popUpTo(NavGraphs.root as RouteOrDirection) {
|
||||
saveState = true
|
||||
}
|
||||
launchSingleTop = true
|
||||
restoreState = true
|
||||
}
|
||||
}
|
||||
},
|
||||
icon = {
|
||||
Icon(
|
||||
imageVector = if (isCurrentDestOnBackStack) {
|
||||
destination.iconSelected
|
||||
} else {
|
||||
destination.iconNotSelected
|
||||
},
|
||||
contentDescription = stringResource(destination.label),
|
||||
tint = if (isCurrentDestOnBackStack) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
},
|
||||
label = {
|
||||
Text(
|
||||
text = stringResource(destination.label),
|
||||
style = MaterialTheme.typography.labelMedium
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
try {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
LocaleUtils.applyLanguageSetting(this)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,6 @@ import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
@@ -138,7 +137,7 @@ fun ImageEditorDialog(
|
||||
0f
|
||||
}
|
||||
updateTransformation(newScale, newOffsetX, newOffsetY)
|
||||
} catch (e: Exception) {
|
||||
} catch (_: Exception) {
|
||||
updateTransformation(lastScale, lastOffsetX, lastOffsetY)
|
||||
}
|
||||
}
|
||||
@@ -186,7 +185,7 @@ fun ImageEditorDialog(
|
||||
val transformation = BackgroundTransformation(scale, offsetX, offsetY)
|
||||
val savedUri = context.saveTransformedBackground(imageUri, transformation)
|
||||
savedUri?.let { onConfirm(it) }
|
||||
} catch (e: Exception) {
|
||||
} catch (_: Exception) {
|
||||
""
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,7 +63,12 @@ fun SearchAppBar(
|
||||
var onSearch by remember { mutableStateOf(false) }
|
||||
|
||||
// 获取卡片颜色和透明度
|
||||
val cardColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
val colorScheme = MaterialTheme.colorScheme
|
||||
val cardColor = if (CardConfig.isCustomBackgroundEnabled) {
|
||||
colorScheme.surfaceContainerLow
|
||||
} else {
|
||||
colorScheme.background
|
||||
}
|
||||
val cardAlpha = CardConfig.cardAlpha
|
||||
|
||||
if (onSearch) {
|
||||
|
||||
@@ -5,14 +5,20 @@ import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.selection.toggleable
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.RadioButton
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import com.dergoogler.mmrl.ui.component.LabelItem
|
||||
import com.dergoogler.mmrl.ui.component.text.TextRow
|
||||
import com.sukisu.ultra.ui.theme.CardConfig
|
||||
|
||||
@Composable
|
||||
fun SwitchItem(
|
||||
@@ -21,10 +27,17 @@ fun SwitchItem(
|
||||
summary: String? = null,
|
||||
checked: Boolean,
|
||||
enabled: Boolean = true,
|
||||
beta: Boolean = false,
|
||||
onCheckedChange: (Boolean) -> Unit
|
||||
) {
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
val stateAlpha = remember(checked, enabled) { Modifier.alpha(if (enabled) 1f else 0.5f) }
|
||||
|
||||
MaterialTheme(
|
||||
colorScheme = MaterialTheme.colorScheme.copy(
|
||||
surface = if (CardConfig.isCustomBackgroundEnabled) Color.Transparent else MaterialTheme.colorScheme.surfaceContainerHigh
|
||||
)
|
||||
) {
|
||||
ListItem(
|
||||
modifier = Modifier
|
||||
.toggleable(
|
||||
@@ -36,10 +49,30 @@ fun SwitchItem(
|
||||
onValueChange = onCheckedChange
|
||||
),
|
||||
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 {
|
||||
{ Icon(icon, title) }
|
||||
{
|
||||
Icon(
|
||||
modifier = Modifier.then(stateAlpha),
|
||||
imageVector = icon,
|
||||
contentDescription = title
|
||||
)
|
||||
}
|
||||
},
|
||||
trailingContent = {
|
||||
Switch(
|
||||
@@ -51,11 +84,15 @@ fun SwitchItem(
|
||||
},
|
||||
supportingContent = {
|
||||
if (summary != null) {
|
||||
Text(summary)
|
||||
Text(
|
||||
modifier = Modifier.then(stateAlpha),
|
||||
text = summary
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RadioItem(
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.sukisu.ultra.ui.component
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.horizontalScroll
|
||||
@@ -10,12 +9,10 @@ import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.sukisu.ultra.R
|
||||
import com.sukisu.ultra.ui.theme.ThemeConfig
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.SdStorage
|
||||
import androidx.compose.ui.draw.clip
|
||||
@@ -32,13 +29,19 @@ fun SlotSelectionDialog(
|
||||
onDismiss: () -> Unit,
|
||||
onSlotSelected: (String) -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
var currentSlot by remember { mutableStateOf<String?>(null) }
|
||||
var errorMessage by remember { mutableStateOf<String?>(null) }
|
||||
var selectedSlot by remember { mutableStateOf<String?>(null) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
try {
|
||||
currentSlot = getCurrentSlot(context)
|
||||
currentSlot = getCurrentSlot()
|
||||
// 设置默认选择为当前槽位
|
||||
selectedSlot = when (currentSlot) {
|
||||
"a" -> "a"
|
||||
"b" -> "b"
|
||||
else -> null
|
||||
}
|
||||
errorMessage = null
|
||||
} catch (e: Exception) {
|
||||
errorMessage = e.message
|
||||
@@ -47,11 +50,7 @@ fun SlotSelectionDialog(
|
||||
}
|
||||
|
||||
if (show) {
|
||||
val cardColor = if (!ThemeConfig.useDynamicColor) {
|
||||
ThemeConfig.currentTheme.ButtonContrast
|
||||
} else {
|
||||
MaterialTheme.colorScheme.surfaceContainerHigh
|
||||
}
|
||||
val cardColor = MaterialTheme.colorScheme.surfaceContainerHighest
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
@@ -107,12 +106,12 @@ fun SlotSelectionDialog(
|
||||
val slotOptions = listOf(
|
||||
ListOption(
|
||||
titleText = stringResource(id = R.string.slot_a),
|
||||
subtitleText = if (currentSlot == "a" || currentSlot == "_a") stringResource(id = R.string.currently_selected) else null,
|
||||
subtitleText = null,
|
||||
icon = Icons.Filled.SdStorage
|
||||
),
|
||||
ListOption(
|
||||
titleText = stringResource(id = R.string.slot_b),
|
||||
subtitleText = if (currentSlot == "b" || currentSlot == "_b") stringResource(id = R.string.currently_selected) else null,
|
||||
subtitleText = null,
|
||||
icon = Icons.Filled.SdStorage
|
||||
)
|
||||
)
|
||||
@@ -128,19 +127,20 @@ fun SlotSelectionDialog(
|
||||
.fillMaxWidth()
|
||||
.clip(MaterialTheme.shapes.medium)
|
||||
.background(
|
||||
color = if (option.subtitleText != null) {
|
||||
color = if (selectedSlot == when(index) {
|
||||
0 -> "a"
|
||||
else -> "b"
|
||||
}) {
|
||||
MaterialTheme.colorScheme.primary.copy(alpha = 0.9f)
|
||||
} else {
|
||||
MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f)
|
||||
}
|
||||
)
|
||||
.clickable {
|
||||
onSlotSelected(
|
||||
when (index) {
|
||||
selectedSlot = when(index) {
|
||||
0 -> "a"
|
||||
else -> "b"
|
||||
}
|
||||
)
|
||||
}
|
||||
.padding(vertical = 12.dp, horizontal = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
@@ -148,7 +148,10 @@ fun SlotSelectionDialog(
|
||||
Icon(
|
||||
imageVector = option.icon,
|
||||
contentDescription = null,
|
||||
tint = if (option.subtitleText != null) {
|
||||
tint = if (selectedSlot == when(index) {
|
||||
0 -> "a"
|
||||
else -> "b"
|
||||
}) {
|
||||
MaterialTheme.colorScheme.onPrimary
|
||||
} else {
|
||||
MaterialTheme.colorScheme.primary
|
||||
@@ -163,7 +166,10 @@ fun SlotSelectionDialog(
|
||||
Text(
|
||||
text = option.titleText,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = if (option.subtitleText != null) {
|
||||
color = if (selectedSlot == when(index) {
|
||||
0 -> "a"
|
||||
else -> "b"
|
||||
}) {
|
||||
MaterialTheme.colorScheme.onPrimary
|
||||
} else {
|
||||
MaterialTheme.colorScheme.primary
|
||||
@@ -173,7 +179,10 @@ fun SlotSelectionDialog(
|
||||
Text(
|
||||
text = it,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = if (true) {
|
||||
color = if (selectedSlot == when(index) {
|
||||
0 -> "a"
|
||||
else -> "b"
|
||||
}) {
|
||||
MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.8f)
|
||||
} else {
|
||||
MaterialTheme.colorScheme.onSurfaceVariant
|
||||
@@ -190,9 +199,10 @@ fun SlotSelectionDialog(
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
currentSlot?.let { onSlotSelected(it) }
|
||||
selectedSlot?.let { onSlotSelected(it) }
|
||||
onDismiss()
|
||||
}
|
||||
},
|
||||
enabled = selectedSlot != null
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(android.R.string.ok),
|
||||
@@ -225,7 +235,7 @@ data class ListOption(
|
||||
)
|
||||
|
||||
// Utility function to get current slot
|
||||
private fun getCurrentSlot(context: Context): String? {
|
||||
private fun getCurrentSlot(): String? {
|
||||
return runCommandGetOutput(true, "getprop ro.boot.slot_suffix")?.let {
|
||||
if (it.startsWith("_")) it.substring(1) else it
|
||||
}
|
||||
|
||||
@@ -1,101 +0,0 @@
|
||||
package com.sukisu.ultra.ui.component
|
||||
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.SwitchDefaults
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
fun SwitchItem(
|
||||
icon: ImageVector,
|
||||
title: String,
|
||||
summary: String? = null,
|
||||
checked: Boolean,
|
||||
enabled: Boolean = true,
|
||||
onCheckedChange: (Boolean) -> Unit
|
||||
) {
|
||||
// 颜色动画
|
||||
val iconTint by animateColorAsState(
|
||||
targetValue = if (checked) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f),
|
||||
animationSpec = tween(300),
|
||||
label = "iconTint"
|
||||
)
|
||||
|
||||
// 开关颜色
|
||||
val switchColors = SwitchDefaults.colors(
|
||||
checkedThumbColor = MaterialTheme.colorScheme.primary,
|
||||
checkedTrackColor = MaterialTheme.colorScheme.primaryContainer,
|
||||
checkedBorderColor = MaterialTheme.colorScheme.primary,
|
||||
checkedIconColor = MaterialTheme.colorScheme.onPrimary,
|
||||
uncheckedThumbColor = MaterialTheme.colorScheme.outline,
|
||||
uncheckedTrackColor = MaterialTheme.colorScheme.surfaceVariant,
|
||||
uncheckedBorderColor = MaterialTheme.colorScheme.outline,
|
||||
uncheckedIconColor = MaterialTheme.colorScheme.surfaceVariant,
|
||||
disabledCheckedThumbColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f),
|
||||
disabledCheckedTrackColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f),
|
||||
disabledCheckedBorderColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f),
|
||||
disabledCheckedIconColor = MaterialTheme.colorScheme.surfaceVariant,
|
||||
disabledUncheckedThumbColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f),
|
||||
disabledUncheckedTrackColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f),
|
||||
disabledUncheckedBorderColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f),
|
||||
disabledUncheckedIconColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
)
|
||||
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
},
|
||||
supportingContent = summary?.let {
|
||||
{
|
||||
Text(
|
||||
text = it,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
maxLines = Int.MAX_VALUE,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
},
|
||||
leadingContent = {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(24.dp),
|
||||
tint = iconTint
|
||||
)
|
||||
},
|
||||
trailingContent = {
|
||||
Switch(
|
||||
checked = checked,
|
||||
onCheckedChange = null,
|
||||
enabled = enabled,
|
||||
colors = switchColors
|
||||
)
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(enabled = enabled) {
|
||||
onCheckedChange(!checked)
|
||||
}
|
||||
.padding(vertical = 4.dp)
|
||||
)
|
||||
}
|
||||
@@ -31,7 +31,6 @@ fun AppProfileConfig(
|
||||
onValueChange = { onProfileChange(profile.copy(name = it)) }
|
||||
)
|
||||
}
|
||||
|
||||
SwitchItem(
|
||||
title = stringResource(R.string.profile_umount_modules),
|
||||
summary = stringResource(R.string.profile_umount_modules_summary),
|
||||
|
||||
@@ -24,6 +24,7 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
@@ -47,6 +48,8 @@ import com.sukisu.ultra.R
|
||||
import com.sukisu.ultra.profile.Capabilities
|
||||
import com.sukisu.ultra.profile.Groups
|
||||
import com.sukisu.ultra.ui.component.rememberCustomDialog
|
||||
import com.sukisu.ultra.ui.theme.CardConfig
|
||||
import com.sukisu.ultra.ui.theme.CardConfig.cardAlpha
|
||||
import com.sukisu.ultra.ui.util.isSepolicyValid
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@@ -206,6 +209,13 @@ fun GroupsPanel(selected: List<Groups>, closeSelection: (selection: Set<Groups>)
|
||||
}
|
||||
|
||||
val selection = HashSet(selected)
|
||||
val backgroundColor = if (CardConfig.isCustomBackgroundEnabled) Color.Transparent else MaterialTheme.colorScheme.surfaceContainerHigh
|
||||
|
||||
MaterialTheme(
|
||||
colorScheme = MaterialTheme.colorScheme.copy(
|
||||
surface = backgroundColor
|
||||
)
|
||||
) {
|
||||
ListDialog(
|
||||
state = rememberUseCaseState(visible = true, onFinishedRequest = {
|
||||
closeSelection(selection)
|
||||
@@ -229,6 +239,7 @@ fun GroupsPanel(selected: List<Groups>, closeSelection: (selection: Set<Groups>)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
OutlinedCard(
|
||||
modifier = Modifier
|
||||
@@ -278,6 +289,13 @@ fun CapsPanel(
|
||||
}
|
||||
|
||||
val selection = HashSet(selected)
|
||||
val backgroundColor = if (CardConfig.isCustomBackgroundEnabled) Color.Transparent else MaterialTheme.colorScheme.surfaceContainerHigh
|
||||
|
||||
MaterialTheme(
|
||||
colorScheme = MaterialTheme.colorScheme.copy(
|
||||
surface = backgroundColor
|
||||
)
|
||||
) {
|
||||
ListDialog(
|
||||
state = rememberUseCaseState(visible = true, onFinishedRequest = {
|
||||
closeSelection(selection)
|
||||
@@ -300,6 +318,7 @@ fun CapsPanel(
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
OutlinedCard(
|
||||
modifier = Modifier
|
||||
@@ -425,8 +444,16 @@ private fun SELinuxPanel(
|
||||
)
|
||||
)
|
||||
|
||||
val backgroundColor = if (CardConfig.isCustomBackgroundEnabled) Color.Transparent else MaterialTheme.colorScheme.surfaceContainerHigh
|
||||
|
||||
MaterialTheme(
|
||||
colorScheme = MaterialTheme.colorScheme.copy(
|
||||
surface = backgroundColor
|
||||
)
|
||||
) {
|
||||
InputDialog(
|
||||
state = rememberUseCaseState(visible = true,
|
||||
state = rememberUseCaseState(
|
||||
visible = true,
|
||||
onFinishedRequest = {
|
||||
onSELinuxChange(domain, rules)
|
||||
},
|
||||
@@ -444,6 +471,7 @@ private fun SELinuxPanel(
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
ListItem(headlineContent = {
|
||||
OutlinedTextField(
|
||||
|
||||
@@ -37,7 +37,6 @@ import androidx.compose.material3.FilterChip
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.IconButtonDefaults
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
@@ -60,6 +59,7 @@ 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.graphics.Color
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
@@ -84,6 +84,9 @@ import com.sukisu.ultra.ui.component.profile.AppProfileConfig
|
||||
import com.sukisu.ultra.ui.component.profile.RootProfileConfig
|
||||
import com.sukisu.ultra.ui.component.profile.TemplateConfig
|
||||
import com.sukisu.ultra.ui.theme.CardConfig
|
||||
import com.sukisu.ultra.ui.theme.CardConfig.cardAlpha
|
||||
import com.sukisu.ultra.ui.theme.getCardColors
|
||||
import com.sukisu.ultra.ui.theme.getCardElevation
|
||||
import com.sukisu.ultra.ui.util.LocalSnackbarHost
|
||||
import com.sukisu.ultra.ui.util.forceStopApp
|
||||
import com.sukisu.ultra.ui.util.getSepolicy
|
||||
@@ -122,7 +125,12 @@ fun AppProfileScreen(
|
||||
mutableStateOf(initialProfile)
|
||||
}
|
||||
|
||||
val cardColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
val colorScheme = MaterialTheme.colorScheme
|
||||
val cardColor = if (CardConfig.isCustomBackgroundEnabled) {
|
||||
colorScheme.surfaceContainerLow
|
||||
} else {
|
||||
colorScheme.background
|
||||
}
|
||||
val cardAlpha = CardConfig.cardAlpha
|
||||
|
||||
Scaffold(
|
||||
@@ -203,13 +211,21 @@ private fun AppProfileInner(
|
||||
onProfileChange: (Natives.Profile) -> Unit,
|
||||
) {
|
||||
val isRootGranted = profile.allowSu
|
||||
val cardColors = getCardColors(MaterialTheme.colorScheme.surfaceContainerHigh)
|
||||
|
||||
MaterialTheme(
|
||||
colorScheme = MaterialTheme.colorScheme.copy(
|
||||
surface = if (CardConfig.isCustomBackgroundEnabled) Color.Transparent else MaterialTheme.colorScheme.surfaceContainerHigh
|
||||
)
|
||||
) {
|
||||
Column(modifier = modifier) {
|
||||
ElevatedCard(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
shape = MaterialTheme.shapes.medium
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
colors = cardColors,
|
||||
elevation = getCardElevation(),
|
||||
) {
|
||||
AppMenuBox(packageName) {
|
||||
ListItem(
|
||||
@@ -235,7 +251,9 @@ private fun AppProfileInner(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
shape = MaterialTheme.shapes.medium
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
colors = cardColors,
|
||||
elevation = getCardElevation(),
|
||||
) {
|
||||
SwitchItem(
|
||||
icon = Icons.Filled.Security,
|
||||
@@ -268,7 +286,9 @@ private fun AppProfileInner(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
shape = MaterialTheme.shapes.medium
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
colors = cardColors,
|
||||
elevation = getCardElevation(),
|
||||
) {
|
||||
ProfileBox(mode, true) {
|
||||
// template mode shouldn't change profile here!
|
||||
@@ -288,10 +308,15 @@ private fun AppProfileInner(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
shape = MaterialTheme.shapes.medium
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
colors = cardColors,
|
||||
elevation = getCardElevation(),
|
||||
) {
|
||||
Column(modifier = Modifier.padding(vertical = 8.dp)) {
|
||||
Crossfade(targetState = mode, label = "ProfileMode") { currentMode ->
|
||||
Crossfade(
|
||||
targetState = mode,
|
||||
label = "ProfileMode"
|
||||
) { currentMode ->
|
||||
when (currentMode) {
|
||||
Mode.Template -> {
|
||||
TemplateConfig(
|
||||
@@ -301,6 +326,7 @@ private fun AppProfileInner(
|
||||
onProfileChange = onProfileChange
|
||||
)
|
||||
}
|
||||
|
||||
Mode.Custom -> {
|
||||
RootProfileConfig(
|
||||
fixedName = true,
|
||||
@@ -308,6 +334,7 @@ private fun AppProfileInner(
|
||||
onProfileChange = onProfileChange
|
||||
)
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
@@ -321,7 +348,9 @@ private fun AppProfileInner(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
shape = MaterialTheme.shapes.medium
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
colors = cardColors,
|
||||
elevation = getCardElevation(),
|
||||
) {
|
||||
ProfileBox(mode, false) {
|
||||
onProfileChange(profile.copy(nonRootUseDefault = (it == Mode.Default)))
|
||||
@@ -337,7 +366,9 @@ private fun AppProfileInner(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
shape = MaterialTheme.shapes.medium
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
colors = cardColors,
|
||||
elevation = getCardElevation(),
|
||||
) {
|
||||
Column(modifier = Modifier.padding(vertical = 8.dp)) {
|
||||
AppProfileConfig(
|
||||
@@ -354,6 +385,7 @@ private fun AppProfileInner(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private enum class Mode(@StringRes private val res: Int) {
|
||||
Default(R.string.profile_default), Template(R.string.profile_template), Custom(R.string.profile_custom);
|
||||
@@ -377,12 +409,10 @@ private fun TopBar(
|
||||
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)
|
||||
)
|
||||
}
|
||||
@@ -391,9 +421,6 @@ private fun TopBar(
|
||||
navigationIcon = {
|
||||
IconButton(
|
||||
onClick = onBack,
|
||||
colors = IconButtonDefaults.iconButtonColors(
|
||||
contentColor = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
@@ -408,7 +435,6 @@ private fun TopBar(
|
||||
modifier = Modifier.shadow(
|
||||
elevation = if ((scrollBehavior?.state?.overlappedFraction ?: 0f) > 0.01f)
|
||||
4.dp else 0.dp,
|
||||
spotColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f)
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -431,7 +457,6 @@ private fun ProfileBox(
|
||||
Text(
|
||||
text = mode.text,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
},
|
||||
leadingContent = {
|
||||
@@ -444,7 +469,6 @@ private fun ProfileBox(
|
||||
|
||||
HorizontalDivider(
|
||||
thickness = Dp.Hairline,
|
||||
color = MaterialTheme.colorScheme.outlineVariant
|
||||
)
|
||||
|
||||
ListItem(
|
||||
@@ -574,6 +598,11 @@ private fun AppMenuOption(text: String, onClick: () -> Unit) {
|
||||
@Composable
|
||||
private fun AppProfilePreview() {
|
||||
var profile by remember { mutableStateOf(Natives.Profile("")) }
|
||||
MaterialTheme(
|
||||
colorScheme = MaterialTheme.colorScheme.copy(
|
||||
surface = if (CardConfig.isCustomBackgroundEnabled) Color.Transparent else MaterialTheme.colorScheme.surfaceContainerHigh
|
||||
)
|
||||
) {
|
||||
Surface {
|
||||
AppProfileInner(
|
||||
packageName = "icu.nullptr.test",
|
||||
@@ -591,3 +620,4 @@ private fun AppProfilePreview() {
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package com.sukisu.ultra.ui.screen
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material.icons.filled.Archive
|
||||
import androidx.compose.material.icons.outlined.*
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import com.ramcosta.composedestinations.generated.destinations.HomeScreenDestination
|
||||
@@ -21,8 +22,8 @@ enum class BottomBarDestination(
|
||||
val rootRequired: Boolean,
|
||||
) {
|
||||
Home(HomeScreenDestination, R.string.home, Icons.Filled.Home, Icons.Outlined.Home, false),
|
||||
Kpm(KpmScreenDestination, R.string.kpm_title, Icons.Filled.Build, Icons.Outlined.Build, true),
|
||||
SuperUser(SuperUserScreenDestination, R.string.superuser, Icons.Filled.Security, Icons.Outlined.Security, true),
|
||||
Module(ModuleScreenDestination, R.string.module, Icons.Filled.Apps, Icons.Outlined.Apps, true),
|
||||
Kpm(KpmScreenDestination, R.string.kpm_title, Icons.Filled.Archive, Icons.Outlined.Archive, true),
|
||||
SuperUser(SuperUserScreenDestination, R.string.superuser, Icons.Filled.AdminPanelSettings, Icons.Outlined.AdminPanelSettings, 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),
|
||||
}
|
||||
|
||||
@@ -1,15 +1,22 @@
|
||||
package com.sukisu.ultra.ui.screen
|
||||
|
||||
import android.os.Environment
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
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.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.Save
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExtendedFloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
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.text.font.FontFamily
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.dropUnlessResumed
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
@@ -55,7 +61,11 @@ fun ExecuteModuleActionScreen(navigator: DestinationsNavigator, moduleId: String
|
||||
val snackBarHost = LocalSnackbarHost.current
|
||||
val scope = rememberCoroutineScope()
|
||||
val scrollState = rememberScrollState()
|
||||
var actionResult: Boolean
|
||||
var isActionRunning by rememberSaveable { mutableStateOf(true) }
|
||||
|
||||
BackHandler(enabled = isActionRunning) {
|
||||
// Disable back button if action is running
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
if (text.isNotEmpty()) {
|
||||
@@ -76,20 +86,17 @@ fun ExecuteModuleActionScreen(navigator: DestinationsNavigator, moduleId: String
|
||||
onStderr = {
|
||||
logContent.append(it).append("\n")
|
||||
}
|
||||
).let {
|
||||
actionResult = it
|
||||
)
|
||||
}
|
||||
}
|
||||
if (actionResult) navigator.popBackStack()
|
||||
isActionRunning = false
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopBar(
|
||||
onBack = dropUnlessResumed {
|
||||
navigator.popBackStack()
|
||||
},
|
||||
isActionRunning = isActionRunning,
|
||||
onSave = {
|
||||
if (!isActionRunning) {
|
||||
scope.launch {
|
||||
val format = SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.getDefault())
|
||||
val date = format.format(Date())
|
||||
@@ -101,8 +108,21 @@ fun ExecuteModuleActionScreen(navigator: DestinationsNavigator, moduleId: String
|
||||
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) }
|
||||
) { innerPadding ->
|
||||
KeyEventBlocker {
|
||||
@@ -130,16 +150,14 @@ fun ExecuteModuleActionScreen(navigator: DestinationsNavigator, moduleId: String
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun TopBar(onBack: () -> Unit = {}, onSave: () -> Unit = {}) {
|
||||
private fun TopBar(isActionRunning: Boolean, onSave: () -> Unit = {}) {
|
||||
TopAppBar(
|
||||
title = { Text(stringResource(R.string.action)) },
|
||||
navigationIcon = {
|
||||
IconButton(
|
||||
onClick = onBack
|
||||
) { Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null) }
|
||||
},
|
||||
actions = {
|
||||
IconButton(onClick = onSave) {
|
||||
IconButton(
|
||||
onClick = onSave,
|
||||
enabled = !isActionRunning
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Save,
|
||||
contentDescription = stringResource(id = R.string.save_log),
|
||||
|
||||
@@ -3,27 +3,40 @@ package com.sukisu.ultra.ui.screen
|
||||
import android.net.Uri
|
||||
import android.os.Environment
|
||||
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.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.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.key.Key
|
||||
import androidx.compose.ui.input.key.key
|
||||
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.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.dropUnlessResumed
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
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.EmptyDestinationsNavigator
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -33,10 +46,17 @@ import kotlinx.parcelize.Parcelize
|
||||
import com.sukisu.ultra.ui.component.KeyEventBlocker
|
||||
import com.sukisu.ultra.ui.util.*
|
||||
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.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* @author ShirkNeko
|
||||
* @date 2025/5/31.
|
||||
*/
|
||||
enum class FlashingStatus {
|
||||
FLASHING,
|
||||
SUCCESS,
|
||||
@@ -45,14 +65,47 @@ enum class FlashingStatus {
|
||||
|
||||
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) {
|
||||
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)
|
||||
@Composable
|
||||
@Destination<RootGraph>
|
||||
fun FlashScreen(navigator: DestinationsNavigator, flashIt: FlashIt) {
|
||||
val context = LocalContext.current
|
||||
var text by rememberSaveable { mutableStateOf("") }
|
||||
var tempText: String
|
||||
val logContent = rememberSaveable { StringBuilder() }
|
||||
@@ -62,6 +115,25 @@ fun FlashScreen(navigator: DestinationsNavigator, flashIt: FlashIt) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val scrollState = rememberScrollState()
|
||||
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) {
|
||||
if (text.isNotEmpty()) {
|
||||
@@ -69,17 +141,50 @@ fun FlashScreen(navigator: DestinationsNavigator, flashIt: FlashIt) {
|
||||
}
|
||||
withContext(Dispatchers.IO) {
|
||||
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) {
|
||||
text += "Error: exit code = $code.\nPlease save and check the log.\n"
|
||||
text += "$errorCodeString $code.\n$checkLogString\n"
|
||||
setFlashingStatus(FlashingStatus.FAILED)
|
||||
|
||||
if (flashIt is FlashIt.FlashModules) {
|
||||
updateModuleInstallStatus(
|
||||
failedModule = moduleInstallStatus.value.currentModuleName
|
||||
)
|
||||
}
|
||||
} else {
|
||||
setFlashingStatus(FlashingStatus.SUCCESS)
|
||||
viewModel.markNeedRefresh()
|
||||
}
|
||||
if (showReboot) {
|
||||
text += "\n\n\n"
|
||||
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 = {
|
||||
tempText = "$it\n"
|
||||
if (tempText.startsWith("[H[J")) { // clear command
|
||||
@@ -94,13 +199,32 @@ fun FlashScreen(navigator: DestinationsNavigator, flashIt: FlashIt) {
|
||||
}
|
||||
}
|
||||
|
||||
val onBack: () -> Unit = {
|
||||
if (currentFlashingStatus.value != FlashingStatus.FLASHING) {
|
||||
if (flashIt is FlashIt.FlashModules) {
|
||||
viewModel.markNeedRefresh()
|
||||
viewModel.fetchModuleList()
|
||||
navigator.navigate(ModuleScreenDestination)
|
||||
} else {
|
||||
viewModel.markNeedRefresh()
|
||||
viewModel.fetchModuleList()
|
||||
navigator.popBackStack()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BackHandler(enabled = true) {
|
||||
onBack()
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopBar(
|
||||
currentFlashingStatus.value,
|
||||
onBack = dropUnlessResumed {
|
||||
navigator.popBackStack()
|
||||
},
|
||||
currentStatus,
|
||||
navigator = navigator,
|
||||
flashIt = flashIt,
|
||||
onBack = onBack,
|
||||
onSave = {
|
||||
scope.launch {
|
||||
val format = SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.getDefault())
|
||||
@@ -110,7 +234,7 @@ fun FlashScreen(navigator: DestinationsNavigator, flashIt: FlashIt) {
|
||||
"KernelSU_install_log_${date}.log"
|
||||
)
|
||||
file.writeText(logContent.toString())
|
||||
snackBarHost.showSnackbar("Log saved to ${file.absolutePath}")
|
||||
snackBarHost.showSnackbar(logSavedString.format(file.absolutePath))
|
||||
}
|
||||
},
|
||||
scrollBehavior = scrollBehavior
|
||||
@@ -118,8 +242,6 @@ fun FlashScreen(navigator: DestinationsNavigator, flashIt: FlashIt) {
|
||||
},
|
||||
floatingActionButton = {
|
||||
if (showFloatAction) {
|
||||
val cardColor = MaterialTheme.colorScheme.secondaryContainer
|
||||
val reboot = stringResource(id = R.string.reboot)
|
||||
ExtendedFloatingActionButton(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
@@ -128,49 +250,288 @@ fun FlashScreen(navigator: DestinationsNavigator, flashIt: FlashIt) {
|
||||
}
|
||||
}
|
||||
},
|
||||
icon = { Icon(Icons.Filled.Refresh, reboot) },
|
||||
text = { Text(text = reboot) },
|
||||
containerColor = cardColor.copy(alpha = 1f),
|
||||
contentColor = MaterialTheme.colorScheme.onSecondaryContainer
|
||||
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)
|
||||
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(1f)
|
||||
.padding(innerPadding)
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection)
|
||||
.verticalScroll(scrollState),
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
) {
|
||||
if (flashIt is FlashIt.FlashModules) {
|
||||
ModuleInstallProgressBar(
|
||||
currentIndex = flashIt.currentIndex + 1,
|
||||
totalCount = flashIt.uris.size,
|
||||
currentModuleName = currentStatus.currentModuleName,
|
||||
status = currentFlashingStatus.value,
|
||||
failedModules = currentStatus.failedModules
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f)
|
||||
.verticalScroll(scrollState)
|
||||
) {
|
||||
LaunchedEffect(text) {
|
||||
scrollState.animateScrollTo(scrollState.maxValue)
|
||||
}
|
||||
Text(
|
||||
modifier = Modifier.padding(8.dp),
|
||||
modifier = Modifier.padding(16.dp),
|
||||
text = text,
|
||||
fontSize = MaterialTheme.typography.bodySmall.fontSize,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
lineHeight = MaterialTheme.typography.bodySmall.lineHeight,
|
||||
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 colorScheme = MaterialTheme.colorScheme
|
||||
val cardColor = if (CardConfig.isCustomBackgroundEnabled) {
|
||||
colorScheme.surfaceContainerLow
|
||||
} else {
|
||||
colorScheme.background
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
sealed class FlashIt : Parcelable {
|
||||
data class FlashBoot(val boot: Uri? = null, val lkm: LkmSelection, val ota: Boolean) : 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 FlashUninstall : FlashIt()
|
||||
}
|
||||
|
||||
fun flashIt(
|
||||
context: android.content.Context,
|
||||
flashIt: FlashIt,
|
||||
onFinish: (Boolean, Int) -> Unit,
|
||||
onStdout: (String) -> Unit,
|
||||
@@ -186,49 +547,22 @@ fun flashIt(
|
||||
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.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
|
||||
@Composable
|
||||
fun FlashScreenPreview() {
|
||||
|
||||
@@ -33,7 +33,6 @@ import androidx.compose.material.icons.filled.FileUpload
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.ElevatedCard
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
@@ -51,7 +50,6 @@ import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||
import androidx.compose.material3.rememberTopAppBarState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.produceState
|
||||
@@ -61,6 +59,7 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
@@ -75,12 +74,10 @@ import com.maxkeppeler.sheets.list.models.ListSelection
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||
import com.ramcosta.composedestinations.generated.destinations.FlashScreenDestination
|
||||
import com.ramcosta.composedestinations.generated.destinations.KernelFlashScreenDestination
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
import com.ramcosta.composedestinations.navigation.EmptyDestinationsNavigator
|
||||
import com.sukisu.ultra.R
|
||||
import com.sukisu.ultra.flash.HorizonKernelFlashProgress
|
||||
import com.sukisu.ultra.flash.HorizonKernelState
|
||||
import com.sukisu.ultra.flash.HorizonKernelWorker
|
||||
import com.sukisu.ultra.ui.component.DialogHandle
|
||||
import com.sukisu.ultra.ui.component.SlotSelectionDialog
|
||||
import com.sukisu.ultra.ui.component.rememberConfirmDialog
|
||||
@@ -95,10 +92,12 @@ 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
|
||||
import com.sukisu.ultra.ui.theme.CardConfig
|
||||
import com.sukisu.ultra.ui.theme.getCardElevation
|
||||
|
||||
/**
|
||||
* @author weishu
|
||||
* @date 2024/3/12.
|
||||
* @author ShirkNeko
|
||||
* @date 2025/5/31.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Destination<RootGraph>
|
||||
@@ -110,16 +109,10 @@ fun InstallScreen(navigator: DestinationsNavigator) {
|
||||
var showRebootDialog by remember { mutableStateOf(false) }
|
||||
var showSlotSelectionDialog by remember { mutableStateOf(false) }
|
||||
var tempKernelUri by remember { mutableStateOf<Uri?>(null) }
|
||||
val horizonKernelState = remember { HorizonKernelState() }
|
||||
val flashState by horizonKernelState.state.collectAsState()
|
||||
val summary = stringResource(R.string.horizon_kernel_summary)
|
||||
val kernelVersion = getKernelVersion()
|
||||
val isGKI = kernelVersion.isGKI()
|
||||
val isAbDevice = isAbDevice()
|
||||
|
||||
val onFlashComplete = {
|
||||
showRebootDialog = true
|
||||
}
|
||||
val summary = stringResource(R.string.horizon_kernel_summary)
|
||||
|
||||
if (showRebootDialog) {
|
||||
RebootDialog(
|
||||
@@ -145,14 +138,12 @@ fun InstallScreen(navigator: DestinationsNavigator) {
|
||||
when (method) {
|
||||
is InstallMethod.HorizonKernel -> {
|
||||
method.uri?.let { uri ->
|
||||
val worker = HorizonKernelWorker(
|
||||
context = context,
|
||||
state = horizonKernelState,
|
||||
slot = method.slot
|
||||
navigator.navigate(
|
||||
KernelFlashScreenDestination(
|
||||
kernelUri = uri,
|
||||
selectedSlot = method.slot
|
||||
)
|
||||
)
|
||||
worker.uri = uri
|
||||
worker.setOnFlashCompleteListener(onFlashComplete)
|
||||
worker.start()
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
@@ -202,21 +193,6 @@ fun InstallScreen(navigator: DestinationsNavigator) {
|
||||
}
|
||||
}
|
||||
|
||||
val selectLkmLauncher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.StartActivityForResult()
|
||||
) {
|
||||
if (it.resultCode == Activity.RESULT_OK) {
|
||||
it.data?.data?.let { uri ->
|
||||
lkmSelection = LkmSelection.LkmUri(uri)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val onLkmUpload = {
|
||||
selectLkmLauncher.launch(Intent(Intent.ACTION_GET_CONTENT).apply {
|
||||
type = "application/octet-stream"
|
||||
})
|
||||
}
|
||||
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||
|
||||
@@ -224,7 +200,6 @@ fun InstallScreen(navigator: DestinationsNavigator) {
|
||||
topBar = {
|
||||
TopBar(
|
||||
onBack = { navigator.popBackStack() },
|
||||
onLkmUpload = onLkmUpload,
|
||||
scrollBehavior = scrollBehavior
|
||||
)
|
||||
},
|
||||
@@ -241,7 +216,6 @@ fun InstallScreen(navigator: DestinationsNavigator) {
|
||||
) {
|
||||
SelectInstallMethod(
|
||||
isGKI = isGKI,
|
||||
isAbDevice = isAbDevice,
|
||||
onSelected = { method ->
|
||||
if (method is InstallMethod.HorizonKernel && method.uri != null) {
|
||||
if (isAbDevice) {
|
||||
@@ -253,18 +227,9 @@ fun InstallScreen(navigator: DestinationsNavigator) {
|
||||
} else {
|
||||
installMethod = method
|
||||
}
|
||||
horizonKernelState.reset()
|
||||
}
|
||||
)
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = flashState.isFlashing && installMethod is InstallMethod.HorizonKernel,
|
||||
enter = fadeIn() + expandVertically(),
|
||||
exit = fadeOut() + shrinkVertically()
|
||||
) {
|
||||
HorizonKernelFlashProgress(flashState)
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -273,7 +238,7 @@ fun InstallScreen(navigator: DestinationsNavigator) {
|
||||
(lkmSelection as? LkmSelection.LkmUri)?.let {
|
||||
ElevatedCard(
|
||||
colors = getCardColors(MaterialTheme.colorScheme.surfaceVariant),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation),
|
||||
elevation = getCardElevation(),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 12.dp)
|
||||
@@ -299,7 +264,7 @@ fun InstallScreen(navigator: DestinationsNavigator) {
|
||||
if (method.slot != null) {
|
||||
ElevatedCard(
|
||||
colors = getCardColors(MaterialTheme.colorScheme.surfaceVariant),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation),
|
||||
elevation = getCardElevation(),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 12.dp)
|
||||
@@ -325,7 +290,7 @@ fun InstallScreen(navigator: DestinationsNavigator) {
|
||||
|
||||
Button(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
enabled = installMethod != null && !flashState.isFlashing,
|
||||
enabled = installMethod != null,
|
||||
onClick = onClickNext,
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
@@ -401,7 +366,6 @@ sealed class InstallMethod {
|
||||
@Composable
|
||||
private fun SelectInstallMethod(
|
||||
isGKI: Boolean = false,
|
||||
isAbDevice: Boolean = false,
|
||||
onSelected: (InstallMethod) -> Unit = {}
|
||||
) {
|
||||
val rootAvailable = rootAvailable()
|
||||
@@ -488,8 +452,8 @@ private fun SelectInstallMethod(
|
||||
}
|
||||
}
|
||||
|
||||
var LKMExpanded by remember { mutableStateOf(false) }
|
||||
var GKIExpanded by remember { mutableStateOf(false) }
|
||||
var lkmExpanded by remember { mutableStateOf(false) }
|
||||
var gkiExpanded by remember { mutableStateOf(false) }
|
||||
|
||||
Column(
|
||||
modifier = Modifier.padding(horizontal = 16.dp)
|
||||
@@ -498,15 +462,15 @@ private fun SelectInstallMethod(
|
||||
if (isGKI) {
|
||||
ElevatedCard(
|
||||
colors = getCardColors(MaterialTheme.colorScheme.surfaceVariant),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation),
|
||||
elevation = getCardElevation(),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 12.dp)
|
||||
.padding(bottom = 16.dp)
|
||||
.clip(MaterialTheme.shapes.large)
|
||||
.shadow(
|
||||
elevation = cardElevation,
|
||||
shape = MaterialTheme.shapes.large,
|
||||
spotColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
|
||||
) {
|
||||
MaterialTheme(
|
||||
colorScheme = MaterialTheme.colorScheme.copy(
|
||||
surface = if (CardConfig.isCustomBackgroundEnabled) Color.Transparent else MaterialTheme.colorScheme.surfaceVariant
|
||||
)
|
||||
) {
|
||||
ListItem(
|
||||
@@ -524,12 +488,13 @@ private fun SelectInstallMethod(
|
||||
)
|
||||
},
|
||||
modifier = Modifier.clickable {
|
||||
LKMExpanded = !LKMExpanded
|
||||
lkmExpanded = !lkmExpanded
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = LKMExpanded,
|
||||
visible = lkmExpanded,
|
||||
enter = fadeIn() + expandVertically(),
|
||||
exit = shrinkVertically() + fadeOut()
|
||||
) {
|
||||
@@ -604,15 +569,15 @@ private fun SelectInstallMethod(
|
||||
if (rootAvailable) {
|
||||
ElevatedCard(
|
||||
colors = getCardColors(MaterialTheme.colorScheme.surfaceVariant),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation),
|
||||
elevation = getCardElevation(),
|
||||
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)
|
||||
) {
|
||||
MaterialTheme(
|
||||
colorScheme = MaterialTheme.colorScheme.copy(
|
||||
surface = if (CardConfig.isCustomBackgroundEnabled) Color.Transparent else MaterialTheme.colorScheme.surfaceVariant
|
||||
)
|
||||
) {
|
||||
ListItem(
|
||||
@@ -630,12 +595,13 @@ private fun SelectInstallMethod(
|
||||
)
|
||||
},
|
||||
modifier = Modifier.clickable {
|
||||
GKIExpanded = !GKIExpanded
|
||||
gkiExpanded = !gkiExpanded
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = GKIExpanded,
|
||||
visible = gkiExpanded,
|
||||
enter = fadeIn() + expandVertically(),
|
||||
exit = shrinkVertically() + fadeOut()
|
||||
) {
|
||||
@@ -722,7 +688,7 @@ fun rememberSelectKmiDialog(onSelected: (String?) -> Unit): DialogHandle {
|
||||
}
|
||||
|
||||
var selection by remember { mutableStateOf<String?>(null) }
|
||||
val backgroundColor = MaterialTheme.colorScheme.surfaceContainerHighest
|
||||
val backgroundColor = MaterialTheme.colorScheme.surfaceContainerHigh
|
||||
|
||||
MaterialTheme(
|
||||
colorScheme = MaterialTheme.colorScheme.copy(
|
||||
@@ -749,10 +715,14 @@ fun rememberSelectKmiDialog(onSelected: (String?) -> Unit): DialogHandle {
|
||||
@Composable
|
||||
private fun TopBar(
|
||||
onBack: () -> Unit = {},
|
||||
onLkmUpload: () -> Unit = {},
|
||||
scrollBehavior: TopAppBarScrollBehavior? = null
|
||||
) {
|
||||
val cardColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
val colorScheme = MaterialTheme.colorScheme
|
||||
val cardColor = if (CardConfig.isCustomBackgroundEnabled) {
|
||||
colorScheme.surfaceContainerLow
|
||||
} else {
|
||||
colorScheme.background
|
||||
}
|
||||
val cardAlpha = cardAlpha
|
||||
|
||||
TopAppBar(
|
||||
|
||||
@@ -112,7 +112,6 @@ fun KpmScreen(
|
||||
Text(
|
||||
text = kpmInstallMode,
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
},
|
||||
text = {
|
||||
@@ -121,7 +120,6 @@ fun KpmScreen(
|
||||
Text(
|
||||
text = stringResource(R.string.kpm_install_mode_description, it),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
@@ -146,9 +144,6 @@ fun KpmScreen(
|
||||
}
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Download,
|
||||
@@ -176,9 +171,6 @@ fun KpmScreen(
|
||||
}
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.secondary
|
||||
)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Inventory,
|
||||
@@ -209,7 +201,6 @@ fun KpmScreen(
|
||||
}
|
||||
}
|
||||
},
|
||||
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
|
||||
shape = MaterialTheme.shapes.extraLarge
|
||||
)
|
||||
}
|
||||
@@ -309,10 +300,8 @@ fun KpmScreen(
|
||||
text = {
|
||||
Text(
|
||||
text = stringResource(R.string.kpm_install),
|
||||
color = MaterialTheme.colorScheme.onPrimaryContainer
|
||||
)
|
||||
},
|
||||
contentColor = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||
expanded = true,
|
||||
)
|
||||
},
|
||||
@@ -321,9 +310,6 @@ fun KpmScreen(
|
||||
Column(modifier = Modifier.padding(padding)) {
|
||||
if (!isNoticeClosed) {
|
||||
Card(
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.secondaryContainer
|
||||
),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp)
|
||||
@@ -348,7 +334,6 @@ fun KpmScreen(
|
||||
text = stringResource(R.string.kernel_module_notice),
|
||||
modifier = Modifier.weight(1f),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSecondaryContainer
|
||||
)
|
||||
|
||||
IconButton(
|
||||
@@ -357,9 +342,6 @@ fun KpmScreen(
|
||||
sharedPreferences.edit { putBoolean("is_notice_closed", true) }
|
||||
},
|
||||
modifier = Modifier.size(24.dp),
|
||||
colors = IconButtonDefaults.iconButtonColors(
|
||||
contentColor = MaterialTheme.colorScheme.onSecondaryContainer
|
||||
)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Close,
|
||||
@@ -391,7 +373,6 @@ fun KpmScreen(
|
||||
stringResource(R.string.kpm_empty),
|
||||
textAlign = TextAlign.Center,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -585,7 +566,6 @@ private fun KpmModuleItem(
|
||||
Text(
|
||||
text = stringResource(R.string.kpm_control),
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
},
|
||||
text = {
|
||||
@@ -595,20 +575,14 @@ private fun KpmModuleItem(
|
||||
label = {
|
||||
Text(
|
||||
text = stringResource(R.string.kpm_args),
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
},
|
||||
placeholder = {
|
||||
Text(
|
||||
text = module.args,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f)
|
||||
)
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
focusedBorderColor = MaterialTheme.colorScheme.primary,
|
||||
unfocusedBorderColor = MaterialTheme.colorScheme.outline
|
||||
)
|
||||
)
|
||||
},
|
||||
confirmButton = {
|
||||
@@ -627,7 +601,6 @@ private fun KpmModuleItem(
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.confirm),
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
},
|
||||
@@ -635,26 +608,16 @@ private fun KpmModuleItem(
|
||||
TextButton(onClick = { viewModel.hideInputDialog() }) {
|
||||
Text(
|
||||
text = stringResource(R.string.cancel),
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
},
|
||||
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
|
||||
shape = MaterialTheme.shapes.extraLarge
|
||||
)
|
||||
}
|
||||
|
||||
Card(
|
||||
colors = getCardColors(MaterialTheme.colorScheme.surfaceContainerHigh),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(MaterialTheme.shapes.large)
|
||||
.shadow(
|
||||
elevation = cardElevation,
|
||||
shape = MaterialTheme.shapes.large,
|
||||
spotColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
|
||||
)
|
||||
elevation = getCardElevation()
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(20.dp)
|
||||
@@ -668,7 +631,6 @@ private fun KpmModuleItem(
|
||||
Text(
|
||||
text = module.name,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
@@ -676,19 +638,16 @@ private fun KpmModuleItem(
|
||||
Text(
|
||||
text = "${stringResource(R.string.kpm_version)}: ${module.version}",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
|
||||
Text(
|
||||
text = "${stringResource(R.string.kpm_author)}: ${module.author}",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
|
||||
Text(
|
||||
text = "${stringResource(R.string.kpm_args)}: ${module.args}",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -698,7 +657,6 @@ private fun KpmModuleItem(
|
||||
Text(
|
||||
text = module.description,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(20.dp))
|
||||
@@ -711,10 +669,6 @@ private fun KpmModuleItem(
|
||||
onClick = { viewModel.showInputDialog(module.id) },
|
||||
enabled = module.hasAction,
|
||||
modifier = Modifier.weight(1f),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
disabledContainerColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Settings,
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.app.Activity.*
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
@@ -22,9 +23,7 @@ import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.rotate
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.*
|
||||
import androidx.compose.ui.res.stringResource
|
||||
@@ -64,15 +63,21 @@ import okhttp3.OkHttpClient
|
||||
import com.sukisu.ultra.ui.util.ModuleModify
|
||||
import com.sukisu.ultra.ui.theme.getCardColors
|
||||
import com.sukisu.ultra.ui.viewmodel.ModuleViewModel
|
||||
import java.io.BufferedReader
|
||||
import java.io.InputStreamReader
|
||||
import java.util.zip.ZipInputStream
|
||||
import java.util.concurrent.TimeUnit
|
||||
import androidx.core.content.edit
|
||||
import androidx.core.net.toUri
|
||||
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
|
||||
import com.sukisu.ultra.ui.theme.getCardElevation
|
||||
|
||||
|
||||
/**
|
||||
* @author ShirkNeko
|
||||
* @date 2025/5/31.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Destination<RootGraph>
|
||||
@Composable
|
||||
@@ -82,6 +87,7 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
|
||||
val snackBarHost = LocalSnackbarHost.current
|
||||
val scope = rememberCoroutineScope()
|
||||
val confirmDialog = rememberConfirmDialog()
|
||||
var lastClickTime by remember { mutableStateOf(0L) }
|
||||
|
||||
val selectZipLauncher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.StartActivityForResult()
|
||||
@@ -94,38 +100,21 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
|
||||
scope.launch {
|
||||
val clipData = data.clipData
|
||||
if (clipData != null) {
|
||||
// 处理多选结果
|
||||
val selectedModules = mutableSetOf<Uri>()
|
||||
val selectedModules = mutableListOf<Uri>()
|
||||
val selectedModuleNames = mutableMapOf<Uri, String>()
|
||||
|
||||
suspend fun processUri(uri: Uri) {
|
||||
val moduleName = withContext(Dispatchers.IO) {
|
||||
fun processUri(uri: Uri) {
|
||||
try {
|
||||
val zipInputStream = ZipInputStream(context.contentResolver.openInputStream(uri))
|
||||
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)
|
||||
}
|
||||
if (!ModuleUtils.isUriAccessible(context, uri)) {
|
||||
return
|
||||
}
|
||||
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}")
|
||||
}
|
||||
}
|
||||
|
||||
for (i in 0 until clipData.itemCount) {
|
||||
@@ -133,7 +122,11 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
|
||||
processUri(uri)
|
||||
}
|
||||
|
||||
// 显示确认对话框
|
||||
if (selectedModules.isEmpty()) {
|
||||
snackBarHost.showSnackbar("Unable to access selected module files")
|
||||
return@launch
|
||||
}
|
||||
|
||||
val modulesList = selectedModuleNames.values.joinToString("\n• ", "• ")
|
||||
val confirmResult = confirmDialog.awaitConfirm(
|
||||
title = context.getString(R.string.module_install),
|
||||
@@ -143,40 +136,27 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
|
||||
)
|
||||
|
||||
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}")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 单个文件安装逻辑
|
||||
val uri = data.data ?: return@launch
|
||||
val moduleName = withContext(Dispatchers.IO) {
|
||||
// 单个安装模块
|
||||
try {
|
||||
val zipInputStream = ZipInputStream(context.contentResolver.openInputStream(uri))
|
||||
var entry = zipInputStream.nextEntry
|
||||
var name = context.getString(R.string.unknown_module)
|
||||
if (!ModuleUtils.isUriAccessible(context, uri)) {
|
||||
snackBarHost.showSnackbar("Unable to access selected module files")
|
||||
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)
|
||||
}
|
||||
}
|
||||
ModuleUtils.takePersistableUriPermission(context, uri)
|
||||
|
||||
val moduleName = ModuleUtils.extractModuleName(context, uri)
|
||||
|
||||
val confirmResult = confirmDialog.awaitConfirm(
|
||||
title = context.getString(R.string.module_install),
|
||||
@@ -189,6 +169,10 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
|
||||
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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -245,10 +229,6 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
|
||||
Checkbox(
|
||||
checked = viewModel.sortActionFirst,
|
||||
onCheckedChange = null,
|
||||
colors = CheckboxDefaults.colors(
|
||||
checkedColor = MaterialTheme.colorScheme.primary,
|
||||
uncheckedColor = MaterialTheme.colorScheme.outline
|
||||
)
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
@@ -270,10 +250,6 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
|
||||
Checkbox(
|
||||
checked = viewModel.sortEnabledFirst,
|
||||
onCheckedChange = null,
|
||||
colors = CheckboxDefaults.colors(
|
||||
checkedColor = MaterialTheme.colorScheme.primary,
|
||||
uncheckedColor = MaterialTheme.colorScheme.outline
|
||||
)
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
@@ -291,7 +267,7 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
|
||||
text = { Text(stringResource(R.string.backup_modules)) },
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Download,
|
||||
imageVector = Icons.Outlined.Save,
|
||||
contentDescription = stringResource(R.string.backup),
|
||||
)
|
||||
},
|
||||
@@ -304,7 +280,7 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
|
||||
text = { Text(stringResource(R.string.restore_modules)) },
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Refresh,
|
||||
imageVector = Icons.Outlined.RestoreFromTrash,
|
||||
contentDescription = stringResource(R.string.restore),
|
||||
)
|
||||
},
|
||||
@@ -340,10 +316,8 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
|
||||
text = {
|
||||
Text(
|
||||
text = moduleInstall,
|
||||
color = MaterialTheme.colorScheme.onPrimaryContainer
|
||||
)
|
||||
},
|
||||
contentColor = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||
expanded = true,
|
||||
)
|
||||
}
|
||||
@@ -368,7 +342,6 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Warning,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.error,
|
||||
modifier = Modifier
|
||||
.size(64.dp)
|
||||
.padding(bottom = 16.dp)
|
||||
@@ -377,7 +350,6 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
|
||||
stringResource(R.string.module_magisk_conflict),
|
||||
textAlign = TextAlign.Center,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -392,13 +364,60 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
|
||||
navigator.navigate(FlashScreenDestination(FlashIt.FlashModule(it)))
|
||||
},
|
||||
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) {
|
||||
webUILauncher.launch(
|
||||
Intent(context, WebUIActivity::class.java)
|
||||
try {
|
||||
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())
|
||||
.putExtra("id", id)
|
||||
.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,
|
||||
@@ -436,6 +455,7 @@ private fun ModuleList(
|
||||
val downloadingText = stringResource(R.string.module_downloading)
|
||||
val startDownloadingText = stringResource(R.string.module_start_downloading)
|
||||
val fetchChangeLogFailed = stringResource(R.string.module_changelog_failed)
|
||||
val downloadErrorText = stringResource(R.string.module_download_error)
|
||||
|
||||
val loadingDialog = rememberLoadingDialog()
|
||||
val confirmDialog = rememberConfirmDialog()
|
||||
@@ -446,12 +466,20 @@ private fun ModuleList(
|
||||
downloadUrl: 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 {
|
||||
withContext(Dispatchers.IO) {
|
||||
runCatching {
|
||||
OkHttpClient().newCall(
|
||||
okhttp3.Request.Builder().url(changelogUrl).build()
|
||||
).execute().body!!.string()
|
||||
client.newCall(request).execute().body!!.string()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -500,6 +528,11 @@ private fun ModuleList(
|
||||
launch(Dispatchers.Main) {
|
||||
Toast.makeText(context, downloading, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
},
|
||||
onError = { errorMsg ->
|
||||
launch(Dispatchers.Main) {
|
||||
Toast.makeText(context, "$downloadErrorText: $errorMsg", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -594,7 +627,6 @@ private fun ModuleList(
|
||||
text = stringResource(R.string.module_empty),
|
||||
textAlign = TextAlign.Center,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -680,15 +712,7 @@ fun ModuleItem(
|
||||
) {
|
||||
ElevatedCard(
|
||||
colors = getCardColors(MaterialTheme.colorScheme.surfaceContainerHigh),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(MaterialTheme.shapes.large)
|
||||
.shadow(
|
||||
elevation = cardElevation,
|
||||
shape = MaterialTheme.shapes.large,
|
||||
spotColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
|
||||
)
|
||||
elevation = getCardElevation(),
|
||||
) {
|
||||
val textDecoration = if (!module.remove) null else TextDecoration.LineThrough
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
@@ -731,7 +755,6 @@ fun ModuleItem(
|
||||
lineHeight = MaterialTheme.typography.bodySmall.lineHeight,
|
||||
fontFamily = MaterialTheme.typography.titleMedium.fontFamily,
|
||||
textDecoration = textDecoration,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
|
||||
Text(
|
||||
@@ -740,7 +763,6 @@ fun ModuleItem(
|
||||
lineHeight = MaterialTheme.typography.bodySmall.lineHeight,
|
||||
fontFamily = MaterialTheme.typography.bodySmall.fontFamily,
|
||||
textDecoration = textDecoration,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
|
||||
Text(
|
||||
@@ -749,7 +771,6 @@ fun ModuleItem(
|
||||
lineHeight = MaterialTheme.typography.bodySmall.lineHeight,
|
||||
fontFamily = MaterialTheme.typography.bodySmall.fontFamily,
|
||||
textDecoration = textDecoration,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
|
||||
@@ -764,14 +785,6 @@ fun ModuleItem(
|
||||
checked = module.enabled,
|
||||
onCheckedChange = onCheckChanged,
|
||||
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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -787,7 +800,6 @@ fun ModuleItem(
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 4,
|
||||
textDecoration = textDecoration,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
@@ -809,21 +821,12 @@ fun ModuleItem(
|
||||
viewModel.markNeedRefresh()
|
||||
},
|
||||
contentPadding = ButtonDefaults.TextButtonContentPadding,
|
||||
colors = ButtonDefaults.filledTonalButtonColors()
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.size(20.dp),
|
||||
imageVector = Icons.Outlined.PlayArrow,
|
||||
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
|
||||
//)
|
||||
//}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -834,7 +837,6 @@ fun ModuleItem(
|
||||
onClick = { onClick(module) },
|
||||
interactionSource = interactionSource,
|
||||
contentPadding = ButtonDefaults.TextButtonContentPadding,
|
||||
colors = ButtonDefaults.filledTonalButtonColors()
|
||||
|
||||
) {
|
||||
Icon(
|
||||
@@ -842,14 +844,6 @@ fun ModuleItem(
|
||||
imageVector = Icons.AutoMirrored.Outlined.Wysiwyg,
|
||||
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)
|
||||
//)
|
||||
//}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -868,14 +862,6 @@ fun ModuleItem(
|
||||
imageVector = Icons.Outlined.Download,
|
||||
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)
|
||||
//)
|
||||
//}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -883,8 +869,6 @@ fun ModuleItem(
|
||||
modifier = Modifier.defaultMinSize(minWidth = 52.dp, minHeight = 32.dp),
|
||||
onClick = { onUninstallClicked(module) },
|
||||
contentPadding = ButtonDefaults.TextButtonContentPadding,
|
||||
colors = ButtonDefaults.filledTonalButtonColors(
|
||||
containerColor = if (!module.remove) MaterialTheme.colorScheme.secondaryContainer else MaterialTheme.colorScheme.errorContainer)
|
||||
) {
|
||||
if (!module.remove) {
|
||||
Icon(
|
||||
@@ -899,15 +883,6 @@ fun ModuleItem(
|
||||
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),
|
||||
// color = if (!module.remove) MaterialTheme.colorScheme.onErrorContainer else MaterialTheme.colorScheme.onSecondaryContainer
|
||||
//)
|
||||
//}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -930,7 +905,8 @@ fun ModuleItemPreview() {
|
||||
updateJson = "",
|
||||
hasWebUi = false,
|
||||
hasActionScript = false,
|
||||
dirId = "dirId"
|
||||
dirId = "dirId",
|
||||
config = ModuleConfig(),
|
||||
)
|
||||
ModuleItem(EmptyDestinationsNavigator, module, "", {}, {}, {}, {})
|
||||
}
|
||||
@@ -1,925 +0,0 @@
|
||||
package com.sukisu.ultra.ui.screen
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.widget.Toast
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.expandVertically
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.shrinkVertically
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.automirrored.filled.NavigateNext
|
||||
import androidx.compose.material.icons.filled.Brush
|
||||
import androidx.compose.material.icons.filled.ColorLens
|
||||
import androidx.compose.material.icons.filled.DarkMode
|
||||
import androidx.compose.material.icons.filled.KeyboardArrowDown
|
||||
import androidx.compose.material.icons.filled.KeyboardArrowUp
|
||||
import androidx.compose.material.icons.filled.Opacity
|
||||
import androidx.compose.material.icons.filled.Palette
|
||||
import androidx.compose.material.icons.filled.Security
|
||||
import androidx.compose.material.icons.filled.VisibilityOff
|
||||
import androidx.compose.material.icons.filled.Wallpaper
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.ListItemDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.RadioButton
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Slider
|
||||
import androidx.compose.material3.SliderDefaults
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.rememberTopAppBarState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.content.edit
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
import com.sukisu.ultra.Natives
|
||||
import com.sukisu.ultra.R
|
||||
import com.sukisu.ultra.ksuApp
|
||||
import com.sukisu.ultra.ui.component.ImageEditorDialog
|
||||
import com.sukisu.ultra.ui.component.KsuIsValid
|
||||
import com.sukisu.ultra.ui.component.SwitchItem
|
||||
import com.sukisu.ultra.ui.theme.CardConfig
|
||||
import com.sukisu.ultra.ui.theme.ThemeColors
|
||||
import com.sukisu.ultra.ui.theme.ThemeConfig
|
||||
import com.sukisu.ultra.ui.theme.saveAndApplyCustomBackground
|
||||
import com.sukisu.ultra.ui.theme.saveCustomBackground
|
||||
import com.sukisu.ultra.ui.theme.saveDynamicColorState
|
||||
import com.sukisu.ultra.ui.theme.saveThemeColors
|
||||
import com.sukisu.ultra.ui.theme.saveThemeMode
|
||||
import com.sukisu.ultra.ui.util.getSuSFS
|
||||
import com.sukisu.ultra.ui.util.getSuSFSFeatures
|
||||
import com.sukisu.ultra.ui.util.susfsSUS_SU_0
|
||||
import com.sukisu.ultra.ui.util.susfsSUS_SU_2
|
||||
import com.sukisu.ultra.ui.util.susfsSUS_SU_Mode
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
fun saveCardConfig(context: Context) {
|
||||
CardConfig.save(context)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Destination<RootGraph>
|
||||
@Composable
|
||||
fun MoreSettingsScreen(navigator: DestinationsNavigator) {
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||
val context = LocalContext.current
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val prefs = remember { context.getSharedPreferences("settings", Context.MODE_PRIVATE) }
|
||||
val systemIsDark = isSystemInDarkTheme()
|
||||
|
||||
// 主题模式选择
|
||||
var themeMode by remember {
|
||||
mutableIntStateOf(
|
||||
when(ThemeConfig.forceDarkMode) {
|
||||
true -> 2 // 深色
|
||||
false -> 1 // 浅色
|
||||
null -> 0 // 跟随系统
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// 动态颜色开关状态
|
||||
var useDynamicColor by remember {
|
||||
mutableStateOf(ThemeConfig.useDynamicColor)
|
||||
}
|
||||
|
||||
var showThemeModeDialog by remember { mutableStateOf(false) }
|
||||
// 主题模式选项
|
||||
val themeOptions = listOf(
|
||||
stringResource(R.string.theme_follow_system),
|
||||
stringResource(R.string.theme_light),
|
||||
stringResource(R.string.theme_dark)
|
||||
)
|
||||
|
||||
// 简洁模式开关状态
|
||||
var isSimpleMode by remember {
|
||||
mutableStateOf(prefs.getBoolean("is_simple_mode", false))
|
||||
}
|
||||
|
||||
// 更新简洁模式开关状态
|
||||
val onSimpleModeChange = { newValue: Boolean ->
|
||||
prefs.edit { putBoolean("is_simple_mode", newValue) }
|
||||
isSimpleMode = newValue
|
||||
}
|
||||
|
||||
// 隐藏内核版本号开关状态
|
||||
var isHideVersion by remember {
|
||||
mutableStateOf(prefs.getBoolean("is_hide_version", false))
|
||||
}
|
||||
|
||||
// 隐藏内核版本号开关状态
|
||||
val onHideVersionChange = { newValue: Boolean ->
|
||||
prefs.edit { putBoolean("is_hide_version", newValue) }
|
||||
isHideVersion = newValue
|
||||
}
|
||||
|
||||
// 隐藏模块数量等信息开关状态
|
||||
var isHideOtherInfo by remember {
|
||||
mutableStateOf(prefs.getBoolean("is_hide_other_info", false))
|
||||
}
|
||||
|
||||
// 隐藏模块数量等信息开关状态
|
||||
val onHideOtherInfoChange = { newValue: Boolean ->
|
||||
prefs.edit { putBoolean("is_hide_other_info", newValue) }
|
||||
isHideOtherInfo = newValue
|
||||
}
|
||||
|
||||
// 隐藏SuSFS状态开关状态
|
||||
var isHideSusfsStatus by remember {
|
||||
mutableStateOf(prefs.getBoolean("is_hide_susfs_status", false))
|
||||
}
|
||||
|
||||
// 隐藏SuSFS状态开关状态
|
||||
val onHideSusfsStatusChange = { newValue: Boolean ->
|
||||
prefs.edit { putBoolean("is_hide_susfs_status", newValue) }
|
||||
isHideSusfsStatus = newValue
|
||||
}
|
||||
|
||||
// 隐藏链接状态开关状态
|
||||
var isHideLinkCard by remember {
|
||||
mutableStateOf(prefs.getBoolean("is_hide_link_card", false))
|
||||
}
|
||||
|
||||
val onHideLinkCardChange = { newValue: Boolean ->
|
||||
prefs.edit { putBoolean("is_hide_link_card", newValue) }
|
||||
isHideLinkCard = newValue
|
||||
}
|
||||
|
||||
// SELinux状态
|
||||
var selinuxEnabled by remember {
|
||||
mutableStateOf(Shell.cmd("getenforce").exec().out.firstOrNull() == "Enforcing")
|
||||
}
|
||||
|
||||
// 卡片配置状态
|
||||
var cardAlpha by rememberSaveable { mutableFloatStateOf(CardConfig.cardAlpha) }
|
||||
var isCustomBackgroundEnabled by rememberSaveable {
|
||||
mutableStateOf(ThemeConfig.customBackgroundUri != null)
|
||||
}
|
||||
|
||||
// 图片编辑状态
|
||||
var showImageEditor by remember { mutableStateOf(false) }
|
||||
var selectedImageUri by remember { mutableStateOf<Uri?>(null) }
|
||||
|
||||
// 展开/折叠状态
|
||||
var isCustomizeExpanded by remember { mutableStateOf(false) }
|
||||
var isAppearanceExpanded by remember { mutableStateOf(true) }
|
||||
var isAdvancedExpanded by remember { mutableStateOf(false) }
|
||||
|
||||
// 初始化卡片配置
|
||||
LaunchedEffect(Unit) {
|
||||
// 加载设置
|
||||
CardConfig.load(context)
|
||||
cardAlpha = CardConfig.cardAlpha
|
||||
isCustomBackgroundEnabled = ThemeConfig.customBackgroundUri != null
|
||||
|
||||
// 设置主题模式
|
||||
themeMode = when (ThemeConfig.forceDarkMode) {
|
||||
true -> 2
|
||||
false -> 1
|
||||
null -> 0
|
||||
}
|
||||
|
||||
// 确保卡片样式跟随主题模式
|
||||
when (themeMode) {
|
||||
2 -> { // 深色
|
||||
CardConfig.isUserDarkModeEnabled = true
|
||||
CardConfig.isUserLightModeEnabled = false
|
||||
}
|
||||
1 -> { // 浅色
|
||||
CardConfig.isUserDarkModeEnabled = false
|
||||
CardConfig.isUserLightModeEnabled = true
|
||||
}
|
||||
0 -> { // 跟随系统
|
||||
CardConfig.isUserDarkModeEnabled = false
|
||||
CardConfig.isUserLightModeEnabled = false
|
||||
}
|
||||
}
|
||||
|
||||
// 如果启用了系统跟随且系统是深色模式,应用深色模式默认值
|
||||
if (themeMode == 0 && systemIsDark) {
|
||||
CardConfig.setDarkModeDefaults()
|
||||
}
|
||||
|
||||
// 保存设置
|
||||
CardConfig.save(context)
|
||||
}
|
||||
|
||||
// 主题色选项
|
||||
val themeColorOptions = listOf(
|
||||
stringResource(R.string.color_default) to ThemeColors.Default,
|
||||
stringResource(R.string.color_green) to ThemeColors.Green,
|
||||
stringResource(R.string.color_purple) to ThemeColors.Purple,
|
||||
stringResource(R.string.color_orange) to ThemeColors.Orange,
|
||||
stringResource(R.string.color_pink) to ThemeColors.Pink,
|
||||
stringResource(R.string.color_gray) to ThemeColors.Gray,
|
||||
stringResource(R.string.color_yellow) to ThemeColors.Yellow
|
||||
)
|
||||
|
||||
var showThemeColorDialog by remember { mutableStateOf(false) }
|
||||
|
||||
// 图片选择器
|
||||
val pickImageLauncher = rememberLauncherForActivityResult(
|
||||
ActivityResultContracts.GetContent()
|
||||
) { uri: Uri? ->
|
||||
uri?.let {
|
||||
selectedImageUri = it
|
||||
showImageEditor = true
|
||||
}
|
||||
}
|
||||
|
||||
// 显示图片编辑对话框
|
||||
if (showImageEditor && selectedImageUri != null) {
|
||||
ImageEditorDialog(
|
||||
imageUri = selectedImageUri!!,
|
||||
onDismiss = {
|
||||
showImageEditor = false
|
||||
selectedImageUri = null
|
||||
},
|
||||
onConfirm = { transformedUri ->
|
||||
context.saveAndApplyCustomBackground(transformedUri)
|
||||
isCustomBackgroundEnabled = true
|
||||
CardConfig.cardElevation = 0.dp
|
||||
CardConfig.isCustomBackgroundEnabled = true
|
||||
saveCardConfig(context)
|
||||
showImageEditor = false
|
||||
selectedImageUri = null
|
||||
|
||||
// 显示成功提示
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.background_set_success),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
val cardColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
val cardAlphaUse = CardConfig.cardAlpha
|
||||
|
||||
Scaffold(
|
||||
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text(stringResource(R.string.more_settings)) },
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = cardColor.copy(alpha = cardAlphaUse),
|
||||
scrolledContainerColor = cardColor.copy(alpha = cardAlphaUse)),
|
||||
navigationIcon = {
|
||||
IconButton(onClick = { navigator.popBackStack() }) {
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = stringResource(R.string.back))
|
||||
}
|
||||
},
|
||||
scrollBehavior = scrollBehavior,
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||
) {
|
||||
// 外观设置部分
|
||||
SectionHeader(
|
||||
title = stringResource(R.string.appearance_settings),
|
||||
expanded = isAppearanceExpanded,
|
||||
onToggle = { isAppearanceExpanded = !isAppearanceExpanded }
|
||||
)
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = isAppearanceExpanded,
|
||||
enter = fadeIn() + expandVertically(),
|
||||
exit = fadeOut() + shrinkVertically()
|
||||
) {
|
||||
Surface(
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
tonalElevation = 1.dp,
|
||||
modifier = Modifier.padding(bottom = 16.dp)
|
||||
) {
|
||||
Column {
|
||||
// 主题模式
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(R.string.theme_mode)) },
|
||||
supportingContent = { Text(themeOptions[themeMode]) },
|
||||
leadingContent = {
|
||||
Icon(
|
||||
Icons.Default.DarkMode,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
},
|
||||
trailingContent = {
|
||||
Icon(
|
||||
Icons.AutoMirrored.Filled.NavigateNext,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
},
|
||||
colors = ListItemDefaults.colors(
|
||||
containerColor = Color.Transparent
|
||||
),
|
||||
modifier = Modifier.clickable { showThemeModeDialog = true }
|
||||
)
|
||||
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
color = MaterialTheme.colorScheme.outlineVariant
|
||||
)
|
||||
|
||||
// 动态颜色开关
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
SwitchItem(
|
||||
icon = Icons.Filled.ColorLens,
|
||||
title = stringResource(R.string.dynamic_color_title),
|
||||
summary = stringResource(R.string.dynamic_color_summary),
|
||||
checked = useDynamicColor
|
||||
) { enabled ->
|
||||
useDynamicColor = enabled
|
||||
context.saveDynamicColorState(enabled)
|
||||
}
|
||||
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
color = MaterialTheme.colorScheme.outlineVariant
|
||||
)
|
||||
}
|
||||
|
||||
// 只在未启用动态颜色时显示主题色选择
|
||||
AnimatedVisibility(
|
||||
visible = Build.VERSION.SDK_INT < Build.VERSION_CODES.S || !useDynamicColor,
|
||||
enter = fadeIn() + expandVertically(),
|
||||
exit = fadeOut() + shrinkVertically()
|
||||
) {
|
||||
Column {
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(R.string.theme_color)) },
|
||||
supportingContent = {
|
||||
val currentThemeName = when (ThemeConfig.currentTheme) {
|
||||
is ThemeColors.Default -> stringResource(R.string.color_default)
|
||||
is ThemeColors.Green -> stringResource(R.string.color_green)
|
||||
is ThemeColors.Purple -> stringResource(R.string.color_purple)
|
||||
is ThemeColors.Orange -> stringResource(R.string.color_orange)
|
||||
is ThemeColors.Pink -> stringResource(R.string.color_pink)
|
||||
is ThemeColors.Gray -> stringResource(R.string.color_gray)
|
||||
is ThemeColors.Yellow -> stringResource(R.string.color_yellow)
|
||||
else -> stringResource(R.string.color_default)
|
||||
}
|
||||
Text(currentThemeName)
|
||||
},
|
||||
leadingContent = {
|
||||
Icon(
|
||||
Icons.Default.Palette,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
},
|
||||
trailingContent = {
|
||||
Icon(
|
||||
Icons.AutoMirrored.Filled.NavigateNext,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
},
|
||||
colors = ListItemDefaults.colors(
|
||||
containerColor = Color.Transparent
|
||||
),
|
||||
modifier = Modifier.clickable { showThemeColorDialog = true }
|
||||
)
|
||||
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
color = MaterialTheme.colorScheme.outlineVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 自定义背景开关
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(id = R.string.settings_custom_background)) },
|
||||
supportingContent = { Text(stringResource(id = R.string.settings_custom_background_summary)) },
|
||||
leadingContent = {
|
||||
Icon(
|
||||
Icons.Filled.Wallpaper,
|
||||
contentDescription = null,
|
||||
tint = if (isCustomBackgroundEnabled)
|
||||
MaterialTheme.colorScheme.primary
|
||||
else
|
||||
MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
},
|
||||
trailingContent = {
|
||||
Switch(
|
||||
checked = isCustomBackgroundEnabled,
|
||||
onCheckedChange = { isChecked ->
|
||||
if (isChecked) {
|
||||
pickImageLauncher.launch("image/*")
|
||||
} else {
|
||||
context.saveCustomBackground(null)
|
||||
isCustomBackgroundEnabled = false
|
||||
CardConfig.cardElevation
|
||||
CardConfig.cardAlpha = 1f
|
||||
CardConfig.isCustomAlphaSet = false
|
||||
CardConfig.isCustomBackgroundEnabled = false
|
||||
saveCardConfig(context)
|
||||
cardAlpha = 1f
|
||||
|
||||
// 重置其他相关设置
|
||||
ThemeConfig.needsResetOnThemeChange = true
|
||||
ThemeConfig.preventBackgroundRefresh = false
|
||||
|
||||
context.getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
||||
.edit {
|
||||
putBoolean(
|
||||
"prevent_background_refresh",
|
||||
false
|
||||
)
|
||||
}
|
||||
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.background_removed),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
colors = ListItemDefaults.colors(
|
||||
containerColor = Color.Transparent
|
||||
)
|
||||
)
|
||||
|
||||
// 透明度 Slider
|
||||
AnimatedVisibility(
|
||||
visible = ThemeConfig.customBackgroundUri != null,
|
||||
enter = fadeIn() + expandVertically(),
|
||||
exit = fadeOut() + shrinkVertically(),
|
||||
modifier = Modifier.padding(horizontal = 32.dp)
|
||||
) {
|
||||
Column(modifier = Modifier.padding(vertical = 8.dp)) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(bottom = 4.dp)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Filled.Opacity,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(20.dp),
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.settings_card_alpha),
|
||||
style = MaterialTheme.typography.titleSmall
|
||||
)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Text(
|
||||
text = "${(cardAlpha * 100).roundToInt()}%",
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
|
||||
Slider(
|
||||
value = cardAlpha,
|
||||
onValueChange = { newValue ->
|
||||
cardAlpha = newValue
|
||||
CardConfig.cardAlpha = newValue
|
||||
CardConfig.isCustomAlphaSet = true
|
||||
prefs.edit {
|
||||
putBoolean("is_custom_alpha_set", true)
|
||||
putFloat("card_alpha", newValue)
|
||||
}
|
||||
},
|
||||
onValueChangeFinished = {
|
||||
coroutineScope.launch(Dispatchers.IO) {
|
||||
saveCardConfig(context)
|
||||
}
|
||||
},
|
||||
valueRange = 0f..1f,
|
||||
steps = 20,
|
||||
colors = SliderDefaults.colors(
|
||||
thumbColor = MaterialTheme.colorScheme.primary,
|
||||
activeTrackColor = MaterialTheme.colorScheme.primary,
|
||||
inactiveTrackColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 自定义设置部分
|
||||
SectionHeader(
|
||||
title = stringResource(R.string.custom_settings),
|
||||
expanded = isCustomizeExpanded,
|
||||
onToggle = { isCustomizeExpanded = !isCustomizeExpanded }
|
||||
)
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = isCustomizeExpanded,
|
||||
enter = fadeIn() + expandVertically(),
|
||||
exit = fadeOut() + shrinkVertically()
|
||||
) {
|
||||
Surface(
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
tonalElevation = 1.dp,
|
||||
modifier = Modifier.padding(bottom = 16.dp)
|
||||
) {
|
||||
Column {
|
||||
// 添加简洁模式开关
|
||||
SwitchItem(
|
||||
icon = Icons.Filled.Brush,
|
||||
title = stringResource(R.string.simple_mode),
|
||||
summary = stringResource(R.string.simple_mode_summary),
|
||||
checked = isSimpleMode
|
||||
) {
|
||||
onSimpleModeChange(it)
|
||||
}
|
||||
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
color = MaterialTheme.colorScheme.outlineVariant
|
||||
)
|
||||
|
||||
// 隐藏内核部分版本号
|
||||
SwitchItem(
|
||||
icon = Icons.Filled.VisibilityOff,
|
||||
title = stringResource(R.string.hide_kernel_kernelsu_version),
|
||||
summary = stringResource(R.string.hide_kernel_kernelsu_version_summary),
|
||||
checked = isHideVersion
|
||||
) {
|
||||
onHideVersionChange(it)
|
||||
}
|
||||
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
color = MaterialTheme.colorScheme.outlineVariant
|
||||
)
|
||||
|
||||
// 模块数量等信息
|
||||
SwitchItem(
|
||||
icon = Icons.Filled.VisibilityOff,
|
||||
title = stringResource(R.string.hide_other_info),
|
||||
summary = stringResource(R.string.hide_other_info_summary),
|
||||
checked = isHideOtherInfo
|
||||
) {
|
||||
onHideOtherInfoChange(it)
|
||||
}
|
||||
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
color = MaterialTheme.colorScheme.outlineVariant
|
||||
)
|
||||
|
||||
// SuSFS 状态信息
|
||||
SwitchItem(
|
||||
icon = Icons.Filled.VisibilityOff,
|
||||
title = stringResource(R.string.hide_susfs_status),
|
||||
summary = stringResource(R.string.hide_susfs_status_summary),
|
||||
checked = isHideSusfsStatus
|
||||
) {
|
||||
onHideSusfsStatusChange(it)
|
||||
}
|
||||
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
color = MaterialTheme.colorScheme.outlineVariant
|
||||
)
|
||||
|
||||
// 隐藏链接信息
|
||||
SwitchItem(
|
||||
icon = Icons.Filled.VisibilityOff,
|
||||
title = stringResource(R.string.hide_link_card),
|
||||
summary = stringResource(R.string.hide_link_card_summary),
|
||||
checked = isHideLinkCard
|
||||
) {
|
||||
onHideLinkCardChange(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 高级设置部分
|
||||
SectionHeader(
|
||||
title = stringResource(R.string.advanced_settings),
|
||||
expanded = isAdvancedExpanded,
|
||||
onToggle = { isAdvancedExpanded = !isAdvancedExpanded }
|
||||
)
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = isAdvancedExpanded,
|
||||
enter = fadeIn() + expandVertically(),
|
||||
exit = fadeOut() + shrinkVertically()
|
||||
) {
|
||||
Surface(
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
tonalElevation = 1.dp,
|
||||
modifier = Modifier.padding(bottom = 16.dp)
|
||||
) {
|
||||
Column {
|
||||
// SELinux 开关
|
||||
KsuIsValid {
|
||||
SwitchItem(
|
||||
icon = Icons.Filled.Security,
|
||||
title = stringResource(R.string.selinux),
|
||||
summary = if (selinuxEnabled)
|
||||
stringResource(R.string.selinux_enabled) else
|
||||
stringResource(R.string.selinux_disabled),
|
||||
checked = selinuxEnabled
|
||||
) { enabled ->
|
||||
val command = if (enabled) "setenforce 1" else "setenforce 0"
|
||||
Shell.getShell().newJob().add(command).exec().let { result ->
|
||||
if (result.isSuccess) {
|
||||
selinuxEnabled = enabled
|
||||
// 显示成功提示
|
||||
val message = if (enabled)
|
||||
context.getString(R.string.selinux_enabled_toast)
|
||||
else
|
||||
context.getString(R.string.selinux_disabled_toast)
|
||||
|
||||
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
|
||||
} else {
|
||||
// 显示失败提示
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.selinux_change_failed),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
color = MaterialTheme.colorScheme.outlineVariant
|
||||
)
|
||||
}
|
||||
|
||||
// SuSFS 配置(仅在支持时显示)
|
||||
val suSFS = getSuSFS()
|
||||
val isSUS_SU = getSuSFSFeatures()
|
||||
if (suSFS == "Supported" && isSUS_SU == "CONFIG_KSU_SUSFS_SUS_SU") {
|
||||
// 初始化时,默认启用
|
||||
var isEnabled by rememberSaveable {
|
||||
mutableStateOf(true) // 默认启用
|
||||
}
|
||||
|
||||
// 在启动时检查状态
|
||||
LaunchedEffect(Unit) {
|
||||
// 如果当前模式不是2就强制启用
|
||||
val currentMode = susfsSUS_SU_Mode()
|
||||
val wasManuallyDisabled = prefs.getBoolean("enable_sus_su", true)
|
||||
if (currentMode != "2" && wasManuallyDisabled) {
|
||||
susfsSUS_SU_2() // 强制切换到模式2
|
||||
prefs.edit { putBoolean("enable_sus_su", true) }
|
||||
}
|
||||
isEnabled = currentMode == "2"
|
||||
}
|
||||
|
||||
SwitchItem(
|
||||
icon = Icons.Filled.Security,
|
||||
title = stringResource(id = R.string.settings_susfs_toggle),
|
||||
summary = stringResource(id = R.string.settings_susfs_toggle_summary),
|
||||
checked = isEnabled
|
||||
) {
|
||||
if (it) {
|
||||
// 手动启用
|
||||
susfsSUS_SU_2()
|
||||
prefs.edit { putBoolean("enable_sus_su", true) }
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.susfs_enabled),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
} else {
|
||||
// 手动关闭
|
||||
susfsSUS_SU_0()
|
||||
prefs.edit { putBoolean("enable_sus_su", false) }
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.susfs_disabled),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
isEnabled = it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 主题模式选择对话框
|
||||
if (showThemeModeDialog) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { showThemeModeDialog = false },
|
||||
title = { Text(stringResource(R.string.theme_mode)) },
|
||||
text = {
|
||||
Column {
|
||||
themeOptions.forEachIndexed { index, option ->
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
themeMode = index
|
||||
val newThemeMode = when(index) {
|
||||
0 -> null // 跟随系统
|
||||
1 -> false // 浅色
|
||||
2 -> true // 深色
|
||||
else -> null
|
||||
}
|
||||
context.saveThemeMode(newThemeMode)
|
||||
when (index) {
|
||||
2 -> { // 深色
|
||||
ThemeConfig.forceDarkMode = true
|
||||
CardConfig.isUserDarkModeEnabled = true
|
||||
CardConfig.isUserLightModeEnabled = false
|
||||
if (!CardConfig.isCustomAlphaSet) {
|
||||
CardConfig.cardAlpha = 1f
|
||||
}
|
||||
CardConfig.save(context)
|
||||
}
|
||||
1 -> { // 浅色
|
||||
ThemeConfig.forceDarkMode = false
|
||||
CardConfig.isUserLightModeEnabled = true
|
||||
CardConfig.isUserDarkModeEnabled = false
|
||||
if (!CardConfig.isCustomAlphaSet) {
|
||||
CardConfig.cardAlpha = 1f
|
||||
}
|
||||
CardConfig.save(context)
|
||||
}
|
||||
0 -> { // 跟随系统
|
||||
ThemeConfig.forceDarkMode = null
|
||||
CardConfig.isUserLightModeEnabled = false
|
||||
CardConfig.isUserDarkModeEnabled = false
|
||||
CardConfig.save(context)
|
||||
}
|
||||
}
|
||||
showThemeModeDialog = false
|
||||
}
|
||||
.padding(vertical = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
RadioButton(
|
||||
selected = themeMode == index,
|
||||
onClick = null
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(option)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = { showThemeModeDialog = false }
|
||||
) {
|
||||
Text(stringResource(R.string.cancel))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// 主题色选择对话框
|
||||
if (showThemeColorDialog) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { showThemeColorDialog = false },
|
||||
title = { Text(stringResource(R.string.choose_theme_color)) },
|
||||
text = {
|
||||
Column {
|
||||
themeColorOptions.forEach { (name, theme) ->
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
context.saveThemeColors(when (theme) {
|
||||
ThemeColors.Default -> "default"
|
||||
ThemeColors.Green -> "green"
|
||||
ThemeColors.Purple -> "purple"
|
||||
ThemeColors.Orange -> "orange"
|
||||
ThemeColors.Pink -> "pink"
|
||||
ThemeColors.Gray -> "gray"
|
||||
ThemeColors.Yellow -> "yellow"
|
||||
else -> "default"
|
||||
})
|
||||
showThemeColorDialog = false
|
||||
}
|
||||
.padding(vertical = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
RadioButton(
|
||||
selected = ThemeConfig.currentTheme::class == theme::class,
|
||||
onClick = null
|
||||
)
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(24.dp)
|
||||
.clip(CircleShape)
|
||||
.background(theme.Primary)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
Text(name)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = { showThemeColorDialog = false }
|
||||
) {
|
||||
Text(stringResource(R.string.cancel))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SectionHeader(
|
||||
title: String,
|
||||
expanded: Boolean,
|
||||
onToggle: () -> Unit
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { onToggle() }
|
||||
.padding(vertical = 12.dp, horizontal = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.titleMedium.copy(
|
||||
fontWeight = FontWeight.Bold
|
||||
),
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Icon(
|
||||
imageVector = if (expanded) Icons.Default.KeyboardArrowUp else Icons.Default.KeyboardArrowDown,
|
||||
contentDescription = if (expanded)
|
||||
stringResource(R.string.collapse)
|
||||
else
|
||||
stringResource(R.string.expand),
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -6,9 +6,13 @@ import android.net.Uri
|
||||
import android.widget.Toast
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.expandVertically
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.shrinkVertically
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
@@ -27,6 +31,7 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
@@ -48,17 +53,22 @@ import kotlinx.coroutines.withContext
|
||||
import com.sukisu.ultra.BuildConfig
|
||||
import com.sukisu.ultra.Natives
|
||||
import com.sukisu.ultra.R
|
||||
import com.sukisu.ultra.*
|
||||
import com.sukisu.ultra.ui.component.*
|
||||
import com.sukisu.ultra.ui.theme.*
|
||||
import com.sukisu.ultra.ui.theme.CardConfig.cardAlpha
|
||||
import com.sukisu.ultra.ui.theme.CardConfig.cardElevation
|
||||
import com.sukisu.ultra.ui.util.LocalSnackbarHost
|
||||
import com.sukisu.ultra.ui.util.getBugreportFile
|
||||
import java.time.LocalDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
import com.sukisu.ultra.ui.component.KsuIsValid
|
||||
|
||||
/**
|
||||
* @author ShirkNeko
|
||||
* @date 2025/5/31.
|
||||
*/
|
||||
private val SPACING_SMALL = 3.dp
|
||||
private val SPACING_MEDIUM = 8.dp
|
||||
private val SPACING_LARGE = 16.dp
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Destination<RootGraph>
|
||||
@@ -66,12 +76,18 @@ import com.sukisu.ultra.ui.component.KsuIsValid
|
||||
fun SettingScreen(navigator: DestinationsNavigator) {
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||
val snackBarHost = LocalSnackbarHost.current
|
||||
val context = LocalContext.current
|
||||
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||
var selectedEngine by rememberSaveable {
|
||||
mutableStateOf(
|
||||
prefs.getString("webui_engine", "default") ?: "default"
|
||||
)
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
// containerColor = MaterialTheme.colorScheme.surfaceBright,
|
||||
topBar = {
|
||||
TopBar(
|
||||
scrollBehavior = scrollBehavior
|
||||
)
|
||||
TopBar(scrollBehavior = scrollBehavior)
|
||||
},
|
||||
snackbarHost = { SnackbarHost(snackBarHost) },
|
||||
contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal)
|
||||
@@ -80,7 +96,6 @@ fun SettingScreen(navigator: DestinationsNavigator) {
|
||||
AboutDialog(it)
|
||||
}
|
||||
val loadingDialog = rememberLoadingDialog()
|
||||
// endregion
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
@@ -88,12 +103,8 @@ fun SettingScreen(navigator: DestinationsNavigator) {
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
// region 上下文与协程
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
// endregion
|
||||
|
||||
// region 日志导出功能
|
||||
val exportBugreportLauncher = rememberLauncherForActivityResult(
|
||||
ActivityResultContracts.CreateDocument("application/gzip")
|
||||
) { uri: Uri? ->
|
||||
@@ -110,31 +121,16 @@ fun SettingScreen(navigator: DestinationsNavigator) {
|
||||
}
|
||||
}
|
||||
|
||||
// 配置
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceContainerLow.copy(alpha = cardAlpha)
|
||||
),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation)
|
||||
) {
|
||||
Column(modifier = Modifier.padding(vertical = 8.dp)) {
|
||||
Text(
|
||||
text = stringResource(R.string.configuration),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||
)
|
||||
|
||||
// 配置卡片
|
||||
SettingsGroupCard(
|
||||
title = stringResource(R.string.configuration),
|
||||
content = {
|
||||
// 配置文件模板入口
|
||||
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),
|
||||
title = stringResource(R.string.settings_profile_template),
|
||||
summary = stringResource(R.string.settings_profile_template_summary),
|
||||
onClick = {
|
||||
navigator.navigate(AppProfileTemplateScreenDestination)
|
||||
}
|
||||
@@ -147,164 +143,146 @@ fun SettingScreen(navigator: DestinationsNavigator) {
|
||||
}
|
||||
|
||||
KsuIsValid {
|
||||
SwitchSettingItem(
|
||||
SwitchItem(
|
||||
icon = Icons.Filled.FolderDelete,
|
||||
title = stringResource(id = R.string.settings_umount_modules_default),
|
||||
summary = stringResource(id = R.string.settings_umount_modules_default_summary),
|
||||
title = stringResource(R.string.settings_umount_modules_default),
|
||||
summary = stringResource(R.string.settings_umount_modules_default_summary),
|
||||
checked = umountChecked,
|
||||
onCheckedChange = {
|
||||
if (Natives.setDefaultUmountModules(it)) {
|
||||
umountChecked = it
|
||||
onCheckedChange = { enabled ->
|
||||
if (Natives.setDefaultUmountModules(enabled)) {
|
||||
umountChecked = enabled
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// SU 禁用开关(仅在兼容版本显示)
|
||||
// SU 禁用开关
|
||||
KsuIsValid {
|
||||
if (Natives.version >= Natives.MINIMAL_SUPPORTED_SU_COMPAT) {
|
||||
var isSuDisabled by rememberSaveable {
|
||||
mutableStateOf(!Natives.isSuEnabled())
|
||||
}
|
||||
SwitchSettingItem(
|
||||
SwitchItem(
|
||||
icon = Icons.Filled.RemoveModerator,
|
||||
title = stringResource(id = R.string.settings_disable_su),
|
||||
summary = stringResource(id = R.string.settings_disable_su_summary),
|
||||
title = stringResource(R.string.settings_disable_su),
|
||||
summary = stringResource(R.string.settings_disable_su_summary),
|
||||
checked = isSuDisabled,
|
||||
onCheckedChange = { checked ->
|
||||
val shouldEnable = !checked
|
||||
onCheckedChange = { enabled ->
|
||||
val shouldEnable = !enabled
|
||||
if (Natives.setSuEnabled(shouldEnable)) {
|
||||
isSuDisabled = !shouldEnable
|
||||
isSuDisabled = enabled
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 设置分组卡片 - 应用设置
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceContainerLow.copy(alpha = cardAlpha)
|
||||
),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation)
|
||||
) {
|
||||
Column(modifier = Modifier.padding(vertical = 8.dp)) {
|
||||
Text(
|
||||
text = stringResource(R.string.app_settings),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||
)
|
||||
|
||||
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||
|
||||
// 应用设置卡片
|
||||
SettingsGroupCard(
|
||||
title = stringResource(R.string.app_settings),
|
||||
content = {
|
||||
// 更新检查开关
|
||||
var checkUpdate by rememberSaveable {
|
||||
mutableStateOf(
|
||||
prefs.getBoolean("check_update", true)
|
||||
)
|
||||
mutableStateOf(prefs.getBoolean("check_update", true))
|
||||
}
|
||||
SwitchSettingItem(
|
||||
SwitchItem(
|
||||
icon = Icons.Filled.Update,
|
||||
title = stringResource(id = R.string.settings_check_update),
|
||||
summary = stringResource(id = R.string.settings_check_update_summary),
|
||||
title = stringResource(R.string.settings_check_update),
|
||||
summary = stringResource(R.string.settings_check_update_summary),
|
||||
checked = checkUpdate,
|
||||
onCheckedChange = {
|
||||
prefs.edit {putBoolean("check_update", it) }
|
||||
checkUpdate = it
|
||||
onCheckedChange = { enabled ->
|
||||
prefs.edit { putBoolean("check_update", enabled) }
|
||||
checkUpdate = enabled
|
||||
}
|
||||
)
|
||||
|
||||
// Web调试开关
|
||||
var enableWebDebugging by rememberSaveable {
|
||||
mutableStateOf(
|
||||
prefs.getBoolean("enable_web_debugging", false)
|
||||
)
|
||||
}
|
||||
// WebUI引擎选择
|
||||
KsuIsValid {
|
||||
SwitchSettingItem(
|
||||
icon = Icons.Filled.DeveloperMode,
|
||||
title = stringResource(id = R.string.enable_web_debugging),
|
||||
summary = stringResource(id = R.string.enable_web_debugging_summary),
|
||||
checked = enableWebDebugging,
|
||||
onCheckedChange = {
|
||||
prefs.edit { putBoolean("enable_web_debugging", it) }
|
||||
enableWebDebugging = it
|
||||
WebUIEngineSelector(
|
||||
selectedEngine = selectedEngine,
|
||||
onEngineSelected = { engine ->
|
||||
selectedEngine = engine
|
||||
prefs.edit { putString("webui_engine", engine) }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// 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(R.string.enable_web_debugging),
|
||||
summary = stringResource(R.string.enable_web_debugging_summary),
|
||||
checked = enableWebDebugging,
|
||||
onCheckedChange = { 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(R.string.use_webuix_eruda),
|
||||
summary = stringResource(R.string.use_webuix_eruda_summary),
|
||||
checked = useWebUIXEruda,
|
||||
onCheckedChange = { enabled ->
|
||||
prefs.edit { putBoolean("use_webuix_eruda", enabled) }
|
||||
useWebUIXEruda = enabled
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 更多设置
|
||||
SettingItem(
|
||||
icon = Icons.Filled.Settings,
|
||||
title = stringResource(id = R.string.more_settings),
|
||||
summary = stringResource(id = R.string.more_settings),
|
||||
title = stringResource(R.string.more_settings),
|
||||
summary = stringResource(R.string.more_settings),
|
||||
onClick = {
|
||||
navigator.navigate(MoreSettingsScreenDestination)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 设置分组卡片 - 工具
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceContainerLow.copy(alpha = cardAlpha)
|
||||
),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation)
|
||||
) {
|
||||
Column(modifier = Modifier.padding(vertical = 8.dp)) {
|
||||
Text(
|
||||
text = stringResource(R.string.tools),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||
)
|
||||
|
||||
// 工具卡片
|
||||
SettingsGroupCard(
|
||||
title = stringResource(R.string.tools),
|
||||
content = {
|
||||
var showBottomsheet by remember { mutableStateOf(false) }
|
||||
|
||||
SettingItem(
|
||||
icon = Icons.Filled.BugReport,
|
||||
title = stringResource(id = R.string.send_log),
|
||||
title = stringResource(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 = {
|
||||
LogBottomSheet(
|
||||
onDismiss = { showBottomsheet = false },
|
||||
onSaveLog = {
|
||||
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 = {
|
||||
},
|
||||
onShareLog = {
|
||||
scope.launch {
|
||||
val bugreport = loadingDialog.withLoading {
|
||||
withContext(Dispatchers.IO) {
|
||||
@@ -312,8 +290,7 @@ fun SettingScreen(navigator: DestinationsNavigator) {
|
||||
}
|
||||
}
|
||||
|
||||
val uri: Uri =
|
||||
FileProvider.getUriForFile(
|
||||
val uri = FileProvider.getUriForFile(
|
||||
context,
|
||||
"${BuildConfig.APPLICATION_ID}.fileprovider",
|
||||
bugreport
|
||||
@@ -337,9 +314,6 @@ fun SettingScreen(navigator: DestinationsNavigator) {
|
||||
}
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
}
|
||||
}
|
||||
|
||||
val lkmMode = Natives.version >= Natives.MINIMAL_SUPPORTED_KERNEL_LKM && Natives.isLkmMode
|
||||
if (lkmMode) {
|
||||
@@ -348,26 +322,12 @@ fun SettingScreen(navigator: DestinationsNavigator) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 设置分组卡片 - 关于
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceContainerLow.copy(alpha = cardAlpha)
|
||||
),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation)
|
||||
) {
|
||||
Column(modifier = Modifier.padding(vertical = 8.dp)) {
|
||||
Text(
|
||||
text = stringResource(R.string.about),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||
)
|
||||
|
||||
// 关于卡片
|
||||
SettingsGroupCard(
|
||||
title = stringResource(R.string.about),
|
||||
content = {
|
||||
SettingItem(
|
||||
icon = Icons.Filled.Info,
|
||||
title = stringResource(R.string.about),
|
||||
@@ -376,13 +336,128 @@ fun SettingScreen(navigator: DestinationsNavigator) {
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(SPACING_LARGE))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
@Composable
|
||||
private fun SettingsGroupCard(
|
||||
title: String,
|
||||
content: @Composable ColumnScope.() -> Unit
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = SPACING_LARGE, vertical = SPACING_MEDIUM),
|
||||
colors = getCardColors(MaterialTheme.colorScheme.surfaceContainerLow),
|
||||
elevation = getCardElevation()
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(vertical = SPACING_MEDIUM)
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
modifier = Modifier.padding(horizontal = SPACING_LARGE, vertical = SPACING_MEDIUM),
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun WebUIEngineSelector(
|
||||
selectedEngine: String,
|
||||
onEngineSelected: (String) -> Unit
|
||||
) {
|
||||
var showDialog by remember { mutableStateOf(false) }
|
||||
val engineOptions = listOf(
|
||||
"default" to stringResource(R.string.engine_auto_select),
|
||||
"wx" to stringResource(R.string.engine_force_webuix),
|
||||
"ksu" to stringResource(R.string.engine_force_ksu)
|
||||
)
|
||||
|
||||
SettingItem(
|
||||
icon = Icons.Filled.WebAsset,
|
||||
title = stringResource(R.string.use_webuix),
|
||||
summary = engineOptions.find { it.first == selectedEngine }?.second
|
||||
?: stringResource(R.string.engine_auto_select),
|
||||
onClick = { showDialog = true }
|
||||
)
|
||||
|
||||
if (showDialog) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { showDialog = false },
|
||||
title = { Text(stringResource(R.string.use_webuix)) },
|
||||
text = {
|
||||
Column {
|
||||
engineOptions.forEach { (value, label) ->
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
onEngineSelected(value)
|
||||
showDialog = false
|
||||
}
|
||||
.padding(vertical = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
RadioButton(
|
||||
selected = selectedEngine == value,
|
||||
onClick = null
|
||||
)
|
||||
Spacer(modifier = Modifier.width(SPACING_MEDIUM))
|
||||
Text(text = label)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = { showDialog = false }) {
|
||||
Text(stringResource(R.string.cancel))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun LogBottomSheet(
|
||||
onDismiss: () -> Unit,
|
||||
onSaveLog: () -> Unit,
|
||||
onShareLog: () -> Unit
|
||||
) {
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = onDismiss,
|
||||
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(SPACING_LARGE),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly
|
||||
) {
|
||||
LogActionButton(
|
||||
icon = Icons.Filled.Save,
|
||||
text = stringResource(R.string.save_log),
|
||||
onClick = onSaveLog
|
||||
)
|
||||
|
||||
LogActionButton(
|
||||
icon = Icons.Filled.Share,
|
||||
text = stringResource(R.string.send_log),
|
||||
onClick = onShareLog
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(SPACING_LARGE))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LogActionButton(
|
||||
icon: ImageVector,
|
||||
@@ -393,7 +468,7 @@ fun LogActionButton(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier
|
||||
.clickable(onClick = onClick)
|
||||
.padding(8.dp)
|
||||
.padding(SPACING_MEDIUM)
|
||||
) {
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
@@ -409,11 +484,10 @@ fun LogActionButton(
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Spacer(modifier = Modifier.height(SPACING_MEDIUM))
|
||||
Text(
|
||||
text = text,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -429,33 +503,31 @@ fun SettingItem(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(onClick = onClick)
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
.padding(horizontal = SPACING_LARGE, vertical = 12.dp),
|
||||
verticalAlignment = Alignment.Top
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier
|
||||
.padding(end = 16.dp)
|
||||
.padding(end = SPACING_LARGE)
|
||||
.size(24.dp)
|
||||
)
|
||||
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
if (summary != null) {
|
||||
Spacer(modifier = Modifier.height(SPACING_SMALL))
|
||||
Text(
|
||||
text = summary,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Icon(
|
||||
imageVector = Icons.Filled.ChevronRight,
|
||||
contentDescription = null,
|
||||
@@ -466,7 +538,7 @@ fun SettingItem(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SwitchSettingItem(
|
||||
fun SwitchItem(
|
||||
icon: ImageVector,
|
||||
title: String,
|
||||
summary: String? = null,
|
||||
@@ -477,44 +549,34 @@ fun SwitchSettingItem(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { onCheckedChange(!checked) }
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
.padding(horizontal = SPACING_LARGE, vertical = 12.dp),
|
||||
verticalAlignment = Alignment.Top
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
tint = if (checked) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier
|
||||
.padding(end = 16.dp)
|
||||
.padding(end = SPACING_LARGE)
|
||||
.size(24.dp)
|
||||
)
|
||||
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
if (summary != null) {
|
||||
Spacer(modifier = Modifier.height(SPACING_SMALL))
|
||||
Text(
|
||||
text = summary,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Switch(
|
||||
checked = checked,
|
||||
onCheckedChange = onCheckedChange,
|
||||
colors = SwitchDefaults.colors(
|
||||
checkedThumbColor = MaterialTheme.colorScheme.onPrimary,
|
||||
checkedTrackColor = MaterialTheme.colorScheme.primary,
|
||||
checkedIconColor = MaterialTheme.colorScheme.primary,
|
||||
uncheckedThumbColor = MaterialTheme.colorScheme.outline,
|
||||
uncheckedTrackColor = MaterialTheme.colorScheme.surfaceVariant,
|
||||
uncheckedIconColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
)
|
||||
onCheckedChange = onCheckedChange
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -598,12 +660,7 @@ fun rememberUninstallDialog(onSelected: (UninstallType) -> Unit): DialogHandle {
|
||||
)
|
||||
}
|
||||
|
||||
var selection = UninstallType.NONE
|
||||
val cardColor = if (!ThemeConfig.useDynamicColor) {
|
||||
ThemeConfig.currentTheme.ButtonContrast
|
||||
} else {
|
||||
MaterialTheme.colorScheme.surfaceContainerHigh
|
||||
}
|
||||
var selectedOption by remember { mutableStateOf<UninstallType?>(null) }
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = {
|
||||
@@ -613,7 +670,6 @@ fun rememberUninstallDialog(onSelected: (UninstallType) -> Unit): DialogHandle {
|
||||
Text(
|
||||
text = stringResource(R.string.settings_uninstall),
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
},
|
||||
text = {
|
||||
@@ -621,55 +677,83 @@ fun rememberUninstallDialog(onSelected: (UninstallType) -> Unit): DialogHandle {
|
||||
modifier = Modifier.padding(vertical = 8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
listOptions.forEachIndexed { index, option ->
|
||||
options.forEachIndexed { index, option ->
|
||||
val isSelected = selectedOption == option
|
||||
val backgroundColor = if (isSelected)
|
||||
MaterialTheme.colorScheme.primaryContainer
|
||||
else
|
||||
Color.Transparent
|
||||
val contentColor = if (isSelected)
|
||||
MaterialTheme.colorScheme.onPrimaryContainer
|
||||
else
|
||||
MaterialTheme.colorScheme.onSurface
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(MaterialTheme.shapes.medium)
|
||||
.background(backgroundColor)
|
||||
.clickable {
|
||||
selection = options[index]
|
||||
selectedOption = option
|
||||
}
|
||||
.padding(vertical = 12.dp, horizontal = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = options[index].icon,
|
||||
imageVector = option.icon,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier
|
||||
.padding(end = 16.dp)
|
||||
.size(24.dp)
|
||||
)
|
||||
Column {
|
||||
Column(
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Text(
|
||||
text = option.titleText,
|
||||
text = listOptions[index].titleText,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
option.subtitleText?.let {
|
||||
listOptions[index].subtitleText?.let {
|
||||
Text(
|
||||
text = it,
|
||||
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 = {
|
||||
TextButton(
|
||||
Button(
|
||||
onClick = {
|
||||
if (selection != UninstallType.NONE) {
|
||||
onSelected(selection)
|
||||
}
|
||||
selectedOption?.let { onSelected(it) }
|
||||
dismiss()
|
||||
}
|
||||
},
|
||||
enabled = selectedOption != null,
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(android.R.string.ok),
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
text = stringResource(android.R.string.ok)
|
||||
)
|
||||
}
|
||||
},
|
||||
@@ -681,11 +765,9 @@ fun rememberUninstallDialog(onSelected: (UninstallType) -> Unit): DialogHandle {
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(android.R.string.cancel),
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
},
|
||||
containerColor = cardColor,
|
||||
shape = MaterialTheme.shapes.extraLarge,
|
||||
tonalElevation = 4.dp
|
||||
)
|
||||
@@ -697,14 +779,12 @@ fun rememberUninstallDialog(onSelected: (UninstallType) -> Unit): DialogHandle {
|
||||
private fun TopBar(
|
||||
scrollBehavior: TopAppBarScrollBehavior? = null
|
||||
) {
|
||||
val systemIsDark = isSystemInDarkTheme()
|
||||
val cardColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
val cardAlpha = if (ThemeConfig.customBackgroundUri != null) {
|
||||
cardAlpha
|
||||
val colorScheme = MaterialTheme.colorScheme
|
||||
val cardColor = if (CardConfig.isCustomBackgroundEnabled) {
|
||||
colorScheme.surfaceContainerLow
|
||||
} else {
|
||||
if (systemIsDark) 0.8f else 1f
|
||||
colorScheme.background
|
||||
}
|
||||
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
package com.sukisu.ultra.ui.screen
|
||||
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.animation.core.*
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
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.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material.icons.Icons
|
||||
@@ -18,14 +21,12 @@ import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
@@ -40,10 +41,17 @@ import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
import kotlinx.coroutines.launch
|
||||
import com.sukisu.ultra.Natives
|
||||
import com.sukisu.ultra.ui.component.SearchAppBar
|
||||
import com.sukisu.ultra.ui.theme.CardConfig.cardElevation
|
||||
import com.sukisu.ultra.ui.util.ModuleModify
|
||||
import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel
|
||||
import com.dergoogler.mmrl.ui.component.LabelItem
|
||||
import com.sukisu.ultra.ui.theme.CardConfig
|
||||
import com.sukisu.ultra.ui.theme.getCardColors
|
||||
import com.sukisu.ultra.ui.theme.getCardElevation
|
||||
|
||||
/**
|
||||
* @author ShirkNeko
|
||||
* @date 2025/5/31.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterialApi::class, ExperimentalMaterial3Api::class)
|
||||
@Destination<RootGraph>
|
||||
@Composable
|
||||
@@ -62,13 +70,14 @@ fun SuperUserScreen(navigator: DestinationsNavigator) {
|
||||
LaunchedEffect(key1 = navigator) {
|
||||
viewModel.search = ""
|
||||
if (viewModel.appList.isEmpty()) {
|
||||
viewModel.fetchAppList()
|
||||
// viewModel.fetchAppList()
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(viewModel.search) {
|
||||
if (viewModel.search.isEmpty()) {
|
||||
listState.scrollToItem(0)
|
||||
// 取消自动滚动到顶部的行为
|
||||
// listState.scrollToItem(0)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,7 +135,7 @@ fun SuperUserScreen(navigator: DestinationsNavigator) {
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
viewModel.showSystemApps = !viewModel.showSystemApps
|
||||
viewModel.updateShowSystemApps(!viewModel.showSystemApps)
|
||||
showDropdown = false
|
||||
}
|
||||
)
|
||||
@@ -165,77 +174,298 @@ fun SuperUserScreen(navigator: DestinationsNavigator) {
|
||||
},
|
||||
snackbarHost = { SnackbarHost(snackBarHostState) },
|
||||
contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),
|
||||
bottomBar = {
|
||||
AnimatedVisibility(
|
||||
visible = viewModel.showBatchActions && viewModel.selectedApps.isNotEmpty(),
|
||||
enter = slideInVertically(initialOffsetY = { it }),
|
||||
exit = slideOutVertically(targetOffsetY = { it })
|
||||
floatingActionButton = {
|
||||
// 侧边悬浮按钮集合
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
horizontalAlignment = Alignment.End
|
||||
) {
|
||||
Surface(
|
||||
color = MaterialTheme.colorScheme.surfaceContainerHighest,
|
||||
tonalElevation = cardElevation,
|
||||
shadowElevation = cardElevation
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly
|
||||
) {
|
||||
OutlinedButton(
|
||||
// 批量操作相关按钮
|
||||
// 只有在批量模式且有选中应用时才显示批量操作按钮
|
||||
if (viewModel.showBatchActions && viewModel.selectedApps.isNotEmpty()) {
|
||||
// 取消按钮
|
||||
val cancelInteractionSource = remember { MutableInteractionSource() }
|
||||
val isCancelPressed by cancelInteractionSource.collectIsPressedAsState()
|
||||
|
||||
FloatingActionButton(
|
||||
onClick = {
|
||||
viewModel.selectedApps = emptySet()
|
||||
viewModel.showBatchActions = false
|
||||
},
|
||||
colors = ButtonDefaults.outlinedButtonColors(
|
||||
contentColor = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
modifier = Modifier.size(if (isCancelPressed) 56.dp else 40.dp),
|
||||
shape = CircleShape,
|
||||
interactionSource = cancelInteractionSource,
|
||||
elevation = FloatingActionButtonDefaults.elevation(4.dp, 6.dp),
|
||||
containerColor = Color.Gray
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Close,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(6.dp))
|
||||
Text(stringResource(android.R.string.cancel))
|
||||
}
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
viewModel.updateBatchPermissions(true)
|
||||
}
|
||||
},
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.primary
|
||||
contentDescription = stringResource(android.R.string.cancel),
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
AnimatedVisibility(
|
||||
visible = isCancelPressed,
|
||||
enter = expandHorizontally() + fadeIn(),
|
||||
exit = shrinkHorizontally() + fadeOut()
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Check,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(16.dp)
|
||||
Text(
|
||||
stringResource(android.R.string.cancel),
|
||||
modifier = Modifier.padding(end = 4.dp),
|
||||
style = MaterialTheme.typography.labelMedium
|
||||
)
|
||||
Spacer(modifier = Modifier.width(6.dp))
|
||||
Text(stringResource(R.string.batch_authorization))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Button(
|
||||
// 取消授权按钮
|
||||
val unauthorizeInteractionSource = remember { MutableInteractionSource() }
|
||||
val isUnauthorizePressed by unauthorizeInteractionSource.collectIsPressedAsState()
|
||||
|
||||
FloatingActionButton(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
viewModel.updateBatchPermissions(false)
|
||||
}
|
||||
},
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
modifier = Modifier.size(if (isUnauthorizePressed) 56.dp else 40.dp),
|
||||
shape = CircleShape,
|
||||
interactionSource = unauthorizeInteractionSource,
|
||||
elevation = FloatingActionButtonDefaults.elevation(4.dp, 6.dp),
|
||||
containerColor = MaterialTheme.colorScheme.error
|
||||
)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Block,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(16.dp)
|
||||
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),
|
||||
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),
|
||||
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),
|
||||
shape = CircleShape,
|
||||
interactionSource = umountDisableInteractionSource,
|
||||
elevation = FloatingActionButtonDefaults.elevation(4.dp, 6.dp),
|
||||
containerColor = MaterialTheme.colorScheme.error
|
||||
) {
|
||||
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),
|
||||
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),
|
||||
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
|
||||
)
|
||||
Spacer(modifier = Modifier.width(6.dp))
|
||||
Text(stringResource(R.string.batch_cancel_authorization))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -255,9 +485,12 @@ fun SuperUserScreen(navigator: DestinationsNavigator) {
|
||||
.fillMaxSize()
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
contentPadding = PaddingValues(
|
||||
top = 8.dp,
|
||||
bottom = if (viewModel.showBatchActions && viewModel.selectedApps.isNotEmpty()) 88.dp else 16.dp
|
||||
)
|
||||
start = 16.dp,
|
||||
end = 16.dp,
|
||||
top = 16.dp,
|
||||
bottom = 16.dp
|
||||
),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
// 获取分组后的应用列表
|
||||
val rootApps = viewModel.appList.filter { it.allowSu }
|
||||
@@ -266,10 +499,6 @@ fun SuperUserScreen(navigator: DestinationsNavigator) {
|
||||
|
||||
// 显示ROOT权限应用组
|
||||
if (rootApps.isNotEmpty()) {
|
||||
item {
|
||||
GroupHeader(title = stringResource(R.string.apps_with_root))
|
||||
}
|
||||
|
||||
items(rootApps, key = { "root_" + it.packageName + it.uid }) { app ->
|
||||
AppItem(
|
||||
app = app,
|
||||
@@ -280,7 +509,7 @@ fun SuperUserScreen(navigator: DestinationsNavigator) {
|
||||
val profile = Natives.getAppProfile(app.packageName, app.uid)
|
||||
val updatedProfile = profile.copy(allowSu = allowSu)
|
||||
if (Natives.setAppProfile(updatedProfile)) {
|
||||
viewModel.fetchAppList()
|
||||
viewModel.updateAppProfileLocally(app.packageName, updatedProfile)
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -305,10 +534,6 @@ fun SuperUserScreen(navigator: DestinationsNavigator) {
|
||||
|
||||
// 显示自定义配置应用组
|
||||
if (customApps.isNotEmpty()) {
|
||||
item {
|
||||
GroupHeader(title = stringResource(R.string.apps_with_custom_profile))
|
||||
}
|
||||
|
||||
items(customApps, key = { "custom_" + it.packageName + it.uid }) { app ->
|
||||
AppItem(
|
||||
app = app,
|
||||
@@ -319,7 +544,7 @@ fun SuperUserScreen(navigator: DestinationsNavigator) {
|
||||
val profile = Natives.getAppProfile(app.packageName, app.uid)
|
||||
val updatedProfile = profile.copy(allowSu = allowSu)
|
||||
if (Natives.setAppProfile(updatedProfile)) {
|
||||
viewModel.fetchAppList()
|
||||
viewModel.updateAppProfileLocally(app.packageName, updatedProfile)
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -344,10 +569,6 @@ fun SuperUserScreen(navigator: DestinationsNavigator) {
|
||||
|
||||
// 显示其他应用组
|
||||
if (otherApps.isNotEmpty()) {
|
||||
item {
|
||||
GroupHeader(title = stringResource(R.string.other_apps))
|
||||
}
|
||||
|
||||
items(otherApps, key = { "other_" + it.packageName + it.uid }) { app ->
|
||||
AppItem(
|
||||
app = app,
|
||||
@@ -358,7 +579,7 @@ fun SuperUserScreen(navigator: DestinationsNavigator) {
|
||||
val profile = Natives.getAppProfile(app.packageName, app.uid)
|
||||
val updatedProfile = profile.copy(allowSu = allowSu)
|
||||
if (Natives.setAppProfile(updatedProfile)) {
|
||||
viewModel.fetchAppList()
|
||||
viewModel.updateAppProfileLocally(app.packageName, updatedProfile)
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -395,7 +616,7 @@ fun SuperUserScreen(navigator: DestinationsNavigator) {
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Apps,
|
||||
imageVector = Icons.Filled.Archive,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.6f),
|
||||
modifier = Modifier
|
||||
@@ -406,7 +627,6 @@ fun SuperUserScreen(navigator: DestinationsNavigator) {
|
||||
text = stringResource(R.string.no_apps_found),
|
||||
textAlign = TextAlign.Center,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -417,26 +637,7 @@ fun SuperUserScreen(navigator: DestinationsNavigator) {
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun GroupHeader(title: String) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(MaterialTheme.colorScheme.surfaceContainerHighest.copy(alpha = 0.7f))
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
style = TextStyle(
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@OptIn(ExperimentalLayoutApi::class, ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun AppItem(
|
||||
app: SuperUserViewModel.AppInfo,
|
||||
@@ -447,35 +648,10 @@ private fun AppItem(
|
||||
onLongClick: () -> Unit,
|
||||
viewModel: SuperUserViewModel
|
||||
) {
|
||||
val cardColor = if (app.allowSu)
|
||||
MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f)
|
||||
else if (app.hasCustomProfile)
|
||||
MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.3f)
|
||||
else
|
||||
MaterialTheme.colorScheme.surfaceContainerLow
|
||||
|
||||
Card(
|
||||
colors = CardDefaults.cardColors(containerColor = cardColor),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 0.dp),
|
||||
colors = getCardColors(MaterialTheme.colorScheme.surfaceContainerHigh),
|
||||
elevation = getCardElevation(),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 8.dp, vertical = 4.dp)
|
||||
.clip(MaterialTheme.shapes.medium)
|
||||
.shadow(
|
||||
elevation = 0.dp,
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
spotColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
|
||||
)
|
||||
.then(
|
||||
if (isSelected)
|
||||
Modifier.border(
|
||||
width = 2.dp,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
shape = MaterialTheme.shapes.medium
|
||||
)
|
||||
else
|
||||
Modifier
|
||||
)
|
||||
.pointerInput(Unit) {
|
||||
detectTapGestures(
|
||||
onLongPress = { onLongClick() },
|
||||
@@ -509,17 +685,13 @@ private fun AppItem(
|
||||
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(
|
||||
@@ -527,51 +699,81 @@ private fun AppItem(
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
if (app.allowSu) {
|
||||
LabelText(label = "ROOT", backgroundColor = MaterialTheme.colorScheme.primary)
|
||||
LabelItem(text = "ROOT")
|
||||
}
|
||||
if (Natives.uidShouldUmount(app.uid)) {
|
||||
LabelText(label = "UMOUNT", backgroundColor = MaterialTheme.colorScheme.tertiary)
|
||||
LabelItem(text = "UNMOUNT")
|
||||
}
|
||||
if (app.hasCustomProfile) {
|
||||
LabelText(label = "CUSTOM", backgroundColor = MaterialTheme.colorScheme.secondary)
|
||||
LabelItem(text = "CUSTOM")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!viewModel.showBatchActions) {
|
||||
// 开关交互源
|
||||
val switchInteractionSource = remember { MutableInteractionSource() }
|
||||
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,
|
||||
modifier = Modifier.padding(end = 4.dp)
|
||||
)
|
||||
}
|
||||
|
||||
Switch(
|
||||
checked = app.allowSu,
|
||||
onCheckedChange = onSwitchChange,
|
||||
colors = SwitchDefaults.colors(
|
||||
checkedThumbColor = MaterialTheme.colorScheme.onPrimary,
|
||||
checkedTrackColor = MaterialTheme.colorScheme.primary,
|
||||
checkedIconColor = MaterialTheme.colorScheme.primary,
|
||||
uncheckedThumbColor = MaterialTheme.colorScheme.outline,
|
||||
uncheckedTrackColor = MaterialTheme.colorScheme.surfaceVariant,
|
||||
uncheckedIconColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
)
|
||||
interactionSource = switchInteractionSource,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
val checkboxInteractionSource = remember { MutableInteractionSource() }
|
||||
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,
|
||||
modifier = Modifier.padding(end = 4.dp)
|
||||
)
|
||||
}
|
||||
Checkbox(
|
||||
checked = isSelected,
|
||||
onCheckedChange = { onToggleSelection() },
|
||||
colors = CheckboxDefaults.colors(
|
||||
checkedColor = MaterialTheme.colorScheme.primary,
|
||||
uncheckedColor = MaterialTheme.colorScheme.outline
|
||||
)
|
||||
interactionSource = checkboxInteractionSource,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LabelText(label: String, backgroundColor: Color) {
|
||||
fun LabelText(label: String) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(top = 2.dp, end = 2.dp)
|
||||
.background(
|
||||
backgroundColor,
|
||||
Color.Black,
|
||||
shape = RoundedCornerShape(4.dp)
|
||||
)
|
||||
.clip(RoundedCornerShape(4.dp))
|
||||
@@ -580,9 +782,8 @@ fun LabelText(label: String, backgroundColor: Color) {
|
||||
text = label,
|
||||
modifier = Modifier.padding(vertical = 2.dp, horizontal = 6.dp),
|
||||
style = TextStyle(
|
||||
fontSize = 10.sp,
|
||||
fontSize = 8.sp,
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -207,13 +207,13 @@ private fun TemplateItem(
|
||||
)
|
||||
Text(template.description)
|
||||
FlowRow {
|
||||
LabelText(label = "UID: ${template.uid}", backgroundColor = MaterialTheme.colorScheme.surface)
|
||||
LabelText(label = "GID: ${template.gid}", backgroundColor = MaterialTheme.colorScheme.surface)
|
||||
LabelText(label = template.context, backgroundColor = MaterialTheme.colorScheme.surface)
|
||||
LabelText(label = "UID: ${template.uid}")
|
||||
LabelText(label = "GID: ${template.gid}")
|
||||
LabelText(label = template.context,)
|
||||
if (template.local) {
|
||||
LabelText(label = "local", backgroundColor = MaterialTheme.colorScheme.surface)
|
||||
LabelText(label = "local")
|
||||
} else {
|
||||
LabelText(label = "remote", backgroundColor = MaterialTheme.colorScheme.surface)
|
||||
LabelText(label = "remote")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -231,7 +231,12 @@ private fun TopBar(
|
||||
colors: TopAppBarColors,
|
||||
scrollBehavior: TopAppBarScrollBehavior? = null
|
||||
) {
|
||||
val cardColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
val colorScheme = MaterialTheme.colorScheme
|
||||
val cardColor = if (CardConfig.isCustomBackgroundEnabled) {
|
||||
colorScheme.surfaceContainerLow
|
||||
} else {
|
||||
colorScheme.background
|
||||
}
|
||||
val cardAlpha = CardConfig.cardAlpha
|
||||
|
||||
TopAppBar(
|
||||
|
||||
@@ -5,21 +5,23 @@ import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.luminance
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
object CardConfig {
|
||||
val settingElevation: Dp = 4.dp
|
||||
val customBackgroundElevation: Dp = 0.dp
|
||||
|
||||
var cardAlpha by mutableStateOf(1f)
|
||||
var cardElevation by mutableStateOf(settingElevation)
|
||||
// 卡片透明度
|
||||
var cardAlpha by mutableFloatStateOf(1f)
|
||||
// 卡片亮度
|
||||
var cardDim by mutableFloatStateOf(0f)
|
||||
// 卡片阴影
|
||||
var cardElevation by mutableStateOf(0.dp)
|
||||
var isShadowEnabled by mutableStateOf(true)
|
||||
var isCustomAlphaSet by mutableStateOf(false)
|
||||
var isCustomDimSet by mutableStateOf(false)
|
||||
var isUserDarkModeEnabled by mutableStateOf(false)
|
||||
var isUserLightModeEnabled by mutableStateOf(false)
|
||||
var isCustomBackgroundEnabled by mutableStateOf(false)
|
||||
@@ -31,9 +33,11 @@ object CardConfig {
|
||||
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||
prefs.edit().apply {
|
||||
putFloat("card_alpha", cardAlpha)
|
||||
putFloat("card_dim", cardDim)
|
||||
putBoolean("custom_background_enabled", isCustomBackgroundEnabled)
|
||||
putBoolean("is_shadow_enabled", isShadowEnabled)
|
||||
putBoolean("is_custom_alpha_set", isCustomAlphaSet)
|
||||
putBoolean("is_custom_dim_set", isCustomDimSet)
|
||||
putBoolean("is_user_dark_mode_enabled", isUserDarkModeEnabled)
|
||||
putBoolean("is_user_light_mode_enabled", isUserLightModeEnabled)
|
||||
apply()
|
||||
@@ -46,9 +50,11 @@ object CardConfig {
|
||||
fun load(context: Context) {
|
||||
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||
cardAlpha = prefs.getFloat("card_alpha", 1f)
|
||||
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)
|
||||
isCustomDimSet = prefs.getBoolean("is_custom_dim_set", false)
|
||||
isUserDarkModeEnabled = prefs.getBoolean("is_user_dark_mode_enabled", false)
|
||||
isUserLightModeEnabled = prefs.getBoolean("is_user_light_mode_enabled", false)
|
||||
updateShadowEnabled(isShadowEnabled)
|
||||
@@ -59,22 +65,19 @@ object CardConfig {
|
||||
*/
|
||||
fun updateShadowEnabled(enabled: Boolean) {
|
||||
isShadowEnabled = enabled
|
||||
cardElevation = if (isCustomBackgroundEnabled && cardAlpha != 1f) {
|
||||
customBackgroundElevation
|
||||
} else if (enabled) {
|
||||
settingElevation
|
||||
} else {
|
||||
customBackgroundElevation
|
||||
}
|
||||
cardElevation = 0.dp
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置深色模式默认值
|
||||
* 设置主题模式默认值
|
||||
*/
|
||||
fun setDarkModeDefaults() {
|
||||
fun setThemeDefaults(isDarkMode: Boolean) {
|
||||
if (!isCustomAlphaSet) {
|
||||
cardAlpha = 1f
|
||||
}
|
||||
if (!isCustomDimSet) {
|
||||
cardDim = if (isDarkMode) 0.5f else 0f
|
||||
}
|
||||
updateShadowEnabled(isShadowEnabled)
|
||||
}
|
||||
}
|
||||
@@ -88,6 +91,19 @@ fun getCardColors(originalColor: Color) = CardDefaults.cardColors(
|
||||
contentColor = determineContentColor(originalColor)
|
||||
)
|
||||
|
||||
/**
|
||||
* 获取卡片阴影配置
|
||||
*/
|
||||
@Composable
|
||||
fun getCardElevation() = CardDefaults.cardElevation(
|
||||
defaultElevation = CardConfig.cardElevation,
|
||||
pressedElevation = CardConfig.cardElevation,
|
||||
focusedElevation = CardConfig.cardElevation,
|
||||
hoveredElevation = CardConfig.cardElevation,
|
||||
draggedElevation = CardConfig.cardElevation,
|
||||
disabledElevation = CardConfig.cardElevation
|
||||
)
|
||||
|
||||
/**
|
||||
* 根据背景颜色、主题模式和用户设置确定内容颜色
|
||||
*/
|
||||
|
||||
@@ -3,260 +3,602 @@ package com.sukisu.ultra.ui.theme
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
sealed class ThemeColors {
|
||||
abstract val Primary: Color
|
||||
abstract val Secondary: Color
|
||||
abstract val Tertiary: Color
|
||||
abstract val OnPrimary: Color
|
||||
abstract val OnSecondary: Color
|
||||
abstract val OnTertiary: Color
|
||||
abstract val PrimaryContainer: Color
|
||||
abstract val SecondaryContainer: Color
|
||||
abstract val TertiaryContainer: Color
|
||||
abstract val OnPrimaryContainer: Color
|
||||
abstract val OnSecondaryContainer: Color
|
||||
abstract val OnTertiaryContainer: Color
|
||||
abstract val ButtonContrast: Color
|
||||
|
||||
// 表面颜色
|
||||
abstract val Surface: Color
|
||||
abstract val SurfaceVariant: Color
|
||||
abstract val OnSurface: Color
|
||||
abstract val OnSurfaceVariant: Color
|
||||
|
||||
// 错误状态颜色
|
||||
abstract val Error: Color
|
||||
abstract val OnError: Color
|
||||
abstract val ErrorContainer: Color
|
||||
abstract val OnErrorContainer: Color
|
||||
|
||||
// 边框和背景色
|
||||
abstract val Outline: Color
|
||||
abstract val OutlineVariant: Color
|
||||
abstract val Background: Color
|
||||
abstract val OnBackground: Color
|
||||
// 浅色
|
||||
abstract val primaryLight: Color
|
||||
abstract val onPrimaryLight: Color
|
||||
abstract val primaryContainerLight: Color
|
||||
abstract val onPrimaryContainerLight: Color
|
||||
abstract val secondaryLight: Color
|
||||
abstract val onSecondaryLight: Color
|
||||
abstract val secondaryContainerLight: Color
|
||||
abstract val onSecondaryContainerLight: Color
|
||||
abstract val tertiaryLight: Color
|
||||
abstract val onTertiaryLight: Color
|
||||
abstract val tertiaryContainerLight: 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
|
||||
|
||||
// 默认主题 (蓝色)
|
||||
object Default : ThemeColors() {
|
||||
override val Primary = Color(0xFF2196F3)
|
||||
override val Secondary = Color(0xFF64B5F6)
|
||||
override val Tertiary = Color(0xFF0D47A1)
|
||||
override val OnPrimary = Color(0xFFFFFFFF)
|
||||
override val OnSecondary = Color(0xFFFFFFFF)
|
||||
override val OnTertiary = Color(0xFFFFFFFF)
|
||||
override val PrimaryContainer = Color(0xFFD6EAFF)
|
||||
override val SecondaryContainer = Color(0xFFE3F2FD)
|
||||
override val TertiaryContainer = Color(0xFFCFD8DC)
|
||||
override val OnPrimaryContainer = Color(0xFF0A3049)
|
||||
override val OnSecondaryContainer = Color(0xFF0D3C61)
|
||||
override val OnTertiaryContainer = Color(0xFF071D41)
|
||||
override val ButtonContrast = Color(0xFF2196F3)
|
||||
override val primaryLight = Color(0xFF415F91)
|
||||
override val onPrimaryLight = Color(0xFFFFFFFF)
|
||||
override val primaryContainerLight = Color(0xFFD6E3FF)
|
||||
override val onPrimaryContainerLight = Color(0xFF284777)
|
||||
override val secondaryLight = Color(0xFF565F71)
|
||||
override val onSecondaryLight = Color(0xFFFFFFFF)
|
||||
override val secondaryContainerLight = Color(0xFFDAE2F9)
|
||||
override val onSecondaryContainerLight = Color(0xFF3E4759)
|
||||
override val tertiaryLight = Color(0xFF705575)
|
||||
override val onTertiaryLight = Color(0xFFFFFFFF)
|
||||
override val tertiaryContainerLight = Color(0xFFFAD8FD)
|
||||
override val onTertiaryContainerLight = Color(0xFF573E5C)
|
||||
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 Surface = Color(0xFFF5F9FF)
|
||||
override val SurfaceVariant = Color(0xFFEDF5FE)
|
||||
override val OnSurface = Color(0xFF1A1C1E)
|
||||
override val OnSurfaceVariant = Color(0xFF42474E)
|
||||
|
||||
override val Error = Color(0xFFB00020)
|
||||
override val OnError = Color(0xFFFFFFFF)
|
||||
override val ErrorContainer = Color(0xFFFDE7E9)
|
||||
override val OnErrorContainer = Color(0xFF410008)
|
||||
|
||||
override val Outline = Color(0xFFBAC3CF)
|
||||
override val OutlineVariant = Color(0xFFDFE3EB)
|
||||
override val Background = Color(0xFFFAFCFF)
|
||||
override val OnBackground = Color(0xFF1A1C1E)
|
||||
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)
|
||||
}
|
||||
|
||||
// 绿色主题
|
||||
object Green : ThemeColors() {
|
||||
override val Primary = Color(0xFF43A047)
|
||||
override val Secondary = Color(0xFF66BB6A)
|
||||
override val Tertiary = Color(0xFF1B5E20)
|
||||
override val OnPrimary = Color(0xFFFFFFFF)
|
||||
override val OnSecondary = Color(0xFFFFFFFF)
|
||||
override val OnTertiary = Color(0xFFFFFFFF)
|
||||
override val PrimaryContainer = Color(0xFFD8EFDB)
|
||||
override val SecondaryContainer = Color(0xFFE8F5E9)
|
||||
override val TertiaryContainer = Color(0xFFB9F6CA)
|
||||
override val OnPrimaryContainer = Color(0xFF0A280D)
|
||||
override val OnSecondaryContainer = Color(0xFF0E2912)
|
||||
override val OnTertiaryContainer = Color(0xFF051B07)
|
||||
override val ButtonContrast = Color(0xFF43A047)
|
||||
override val primaryLight = Color(0xFF4C662B)
|
||||
override val onPrimaryLight = Color(0xFFFFFFFF)
|
||||
override val primaryContainerLight = Color(0xFFCDEDA3)
|
||||
override val onPrimaryContainerLight = Color(0xFF354E16)
|
||||
override val secondaryLight = Color(0xFF586249)
|
||||
override val onSecondaryLight = Color(0xFFFFFFFF)
|
||||
override val secondaryContainerLight = Color(0xFFDCE7C8)
|
||||
override val onSecondaryContainerLight = Color(0xFF404A33)
|
||||
override val tertiaryLight = Color(0xFF386663)
|
||||
override val onTertiaryLight = Color(0xFFFFFFFF)
|
||||
override val tertiaryContainerLight = Color(0xFFBCECE7)
|
||||
override val onTertiaryContainerLight = Color(0xFF1F4E4B)
|
||||
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 Surface = Color(0xFFF6FBF6)
|
||||
override val SurfaceVariant = Color(0xFFEDF7EE)
|
||||
override val OnSurface = Color(0xFF191C19)
|
||||
override val OnSurfaceVariant = Color(0xFF414941)
|
||||
|
||||
override val Error = Color(0xFFC62828)
|
||||
override val OnError = Color(0xFFFFFFFF)
|
||||
override val ErrorContainer = Color(0xFFF8D7DA)
|
||||
override val OnErrorContainer = Color(0xFF4A0808)
|
||||
|
||||
override val Outline = Color(0xFFBDC9BF)
|
||||
override val OutlineVariant = Color(0xFFDDE6DE)
|
||||
override val Background = Color(0xFFFBFDFB)
|
||||
override val OnBackground = Color(0xFF191C19)
|
||||
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)
|
||||
}
|
||||
|
||||
// 紫色主题
|
||||
object Purple : ThemeColors() {
|
||||
override val Primary = Color(0xFF9C27B0)
|
||||
override val Secondary = Color(0xFFBA68C8)
|
||||
override val Tertiary = Color(0xFF6A1B9A)
|
||||
override val OnPrimary = Color(0xFFFFFFFF)
|
||||
override val OnSecondary = Color(0xFFFFFFFF)
|
||||
override val OnTertiary = Color(0xFFFFFFFF)
|
||||
override val PrimaryContainer = Color(0xFFF3D8F8)
|
||||
override val SecondaryContainer = Color(0xFFF5E9F7)
|
||||
override val TertiaryContainer = Color(0xFFE1BEE7)
|
||||
override val OnPrimaryContainer = Color(0xFF2A0934)
|
||||
override val OnSecondaryContainer = Color(0xFF3C0F50)
|
||||
override val OnTertiaryContainer = Color(0xFF1D0830)
|
||||
override val ButtonContrast = Color(0xFF9C27B0)
|
||||
override val primaryLight = Color(0xFF7C4E7E)
|
||||
override val onPrimaryLight = Color(0xFFFFFFFF)
|
||||
override val primaryContainerLight = Color(0xFFFFD6FC)
|
||||
override val onPrimaryContainerLight = Color(0xFF623765)
|
||||
override val secondaryLight = Color(0xFF6C586B)
|
||||
override val onSecondaryLight = Color(0xFFFFFFFF)
|
||||
override val secondaryContainerLight = Color(0xFFF5DBF1)
|
||||
override val onSecondaryContainerLight = Color(0xFF534152)
|
||||
override val tertiaryLight = Color(0xFF825249)
|
||||
override val onTertiaryLight = Color(0xFFFFFFFF)
|
||||
override val tertiaryContainerLight = Color(0xFFFFDAD4)
|
||||
override val onTertiaryContainerLight = Color(0xFF673B33)
|
||||
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 Surface = Color(0xFFFCF6FF)
|
||||
override val SurfaceVariant = Color(0xFFF5EEFA)
|
||||
override val OnSurface = Color(0xFF1D1B1E)
|
||||
override val OnSurfaceVariant = Color(0xFF49454E)
|
||||
|
||||
override val Error = Color(0xFFD50000)
|
||||
override val OnError = Color(0xFFFFFFFF)
|
||||
override val ErrorContainer = Color(0xFFFFDCD5)
|
||||
override val OnErrorContainer = Color(0xFF480000)
|
||||
|
||||
override val Outline = Color(0xFFC9B9D0)
|
||||
override val OutlineVariant = Color(0xFFE8DAED)
|
||||
override val Background = Color(0xFFFFFBFF)
|
||||
override val OnBackground = Color(0xFF1D1B1E)
|
||||
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)
|
||||
}
|
||||
|
||||
// 橙色主题
|
||||
object Orange : ThemeColors() {
|
||||
override val Primary = Color(0xFFFF9800)
|
||||
override val Secondary = Color(0xFFFFB74D)
|
||||
override val Tertiary = Color(0xFFE65100)
|
||||
override val OnPrimary = Color(0xFFFFFFFF)
|
||||
override val OnSecondary = Color(0xFF000000)
|
||||
override val OnTertiary = Color(0xFFFFFFFF)
|
||||
override val PrimaryContainer = Color(0xFFFFECCC)
|
||||
override val SecondaryContainer = Color(0xFFFFF0D9)
|
||||
override val TertiaryContainer = Color(0xFFFFD180)
|
||||
override val OnPrimaryContainer = Color(0xFF351F00)
|
||||
override val OnSecondaryContainer = Color(0xFF3D2800)
|
||||
override val OnTertiaryContainer = Color(0xFF2E1500)
|
||||
override val ButtonContrast = Color(0xFFFF9800)
|
||||
override val primaryLight = Color(0xFF8B4F24)
|
||||
override val onPrimaryLight = Color(0xFFFFFFFF)
|
||||
override val primaryContainerLight = Color(0xFFFFDCC7)
|
||||
override val onPrimaryContainerLight = Color(0xFF6E390E)
|
||||
override val secondaryLight = Color(0xFF755846)
|
||||
override val onSecondaryLight = Color(0xFFFFFFFF)
|
||||
override val secondaryContainerLight = Color(0xFFFFDCC7)
|
||||
override val onSecondaryContainerLight = Color(0xFF5B4130)
|
||||
override val tertiaryLight = Color(0xFF865219)
|
||||
override val onTertiaryLight = Color(0xFFFFFFFF)
|
||||
override val tertiaryContainerLight = Color(0xFFFFDCBF)
|
||||
override val onTertiaryContainerLight = Color(0xFF6A3B01)
|
||||
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 Surface = Color(0xFFFFF8F3)
|
||||
override val SurfaceVariant = Color(0xFFFFF0E6)
|
||||
override val OnSurface = Color(0xFF1F1B16)
|
||||
override val OnSurfaceVariant = Color(0xFF4E4639)
|
||||
|
||||
override val Error = Color(0xFFD32F2F)
|
||||
override val OnError = Color(0xFFFFFFFF)
|
||||
override val ErrorContainer = Color(0xFFFFDBC8)
|
||||
override val OnErrorContainer = Color(0xFF490700)
|
||||
|
||||
override val Outline = Color(0xFFD6C3AD)
|
||||
override val OutlineVariant = Color(0xFFEFDFCC)
|
||||
override val Background = Color(0xFFFFFBFF)
|
||||
override val OnBackground = Color(0xFF1F1B16)
|
||||
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)
|
||||
}
|
||||
|
||||
// 粉色主题
|
||||
object Pink : ThemeColors() {
|
||||
override val Primary = Color(0xFFE91E63)
|
||||
override val Secondary = Color(0xFFF06292)
|
||||
override val Tertiary = Color(0xFF880E4F)
|
||||
override val OnPrimary = Color(0xFFFFFFFF)
|
||||
override val OnSecondary = Color(0xFFFFFFFF)
|
||||
override val OnTertiary = Color(0xFFFFFFFF)
|
||||
override val PrimaryContainer = Color(0xFFFCE4EC)
|
||||
override val SecondaryContainer = Color(0xFFFCE4EC)
|
||||
override val TertiaryContainer = Color(0xFFF8BBD0)
|
||||
override val OnPrimaryContainer = Color(0xFF3B0819)
|
||||
override val OnSecondaryContainer = Color(0xFF3B0819)
|
||||
override val OnTertiaryContainer = Color(0xFF2B0516)
|
||||
override val ButtonContrast = Color(0xFFE91E63)
|
||||
override val primaryLight = Color(0xFF8C4A60)
|
||||
override val onPrimaryLight = Color(0xFFFFFFFF)
|
||||
override val primaryContainerLight = Color(0xFFFFD9E2)
|
||||
override val onPrimaryContainerLight = Color(0xFF703348)
|
||||
override val secondaryLight = Color(0xFF8B4A62)
|
||||
override val onSecondaryLight = Color(0xFFFFFFFF)
|
||||
override val secondaryContainerLight = Color(0xFFFFD9E3)
|
||||
override val onSecondaryContainerLight = Color(0xFF6F334B)
|
||||
override val tertiaryLight = Color(0xFF8B4A62)
|
||||
override val onTertiaryLight = Color(0xFFFFFFFF)
|
||||
override val tertiaryContainerLight = Color(0xFFFFD9E3)
|
||||
override val onTertiaryContainerLight = Color(0xFF6F334B)
|
||||
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 Surface = Color(0xFFFFF7F9)
|
||||
override val SurfaceVariant = Color(0xFFFCEEF2)
|
||||
override val OnSurface = Color(0xFF201A1C)
|
||||
override val OnSurfaceVariant = Color(0xFF534347)
|
||||
|
||||
override val Error = Color(0xFFB71C1C)
|
||||
override val OnError = Color(0xFFFFFFFF)
|
||||
override val ErrorContainer = Color(0xFFFFDAD6)
|
||||
override val OnErrorContainer = Color(0xFF410002)
|
||||
|
||||
override val Outline = Color(0xFFD6BABF)
|
||||
override val OutlineVariant = Color(0xFFEFDDE0)
|
||||
override val Background = Color(0xFFFFFBFF)
|
||||
override val OnBackground = Color(0xFF201A1C)
|
||||
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)
|
||||
}
|
||||
|
||||
// 灰色主题
|
||||
object Gray : ThemeColors() {
|
||||
override val Primary = Color(0xFF607D8B)
|
||||
override val Secondary = Color(0xFF90A4AE)
|
||||
override val Tertiary = Color(0xFF455A64)
|
||||
override val OnPrimary = Color(0xFFFFFFFF)
|
||||
override val OnSecondary = Color(0xFFFFFFFF)
|
||||
override val OnTertiary = Color(0xFFFFFFFF)
|
||||
override val PrimaryContainer = Color(0xFFECEFF1)
|
||||
override val SecondaryContainer = Color(0xFFECEFF1)
|
||||
override val TertiaryContainer = Color(0xFFCFD8DC)
|
||||
override val OnPrimaryContainer = Color(0xFF1A2327)
|
||||
override val OnSecondaryContainer = Color(0xFF1A2327)
|
||||
override val OnTertiaryContainer = Color(0xFF121A1D)
|
||||
override val ButtonContrast = Color(0xFF607D8B)
|
||||
override val primaryLight = Color(0xFF5B5C5C)
|
||||
override val onPrimaryLight = Color(0xFFFFFFFF)
|
||||
override val primaryContainerLight = Color(0xFF747474)
|
||||
override val onPrimaryContainerLight = Color(0xFFFEFCFC)
|
||||
override val secondaryLight = Color(0xFF5F5E5E)
|
||||
override val onSecondaryLight = Color(0xFFFFFFFF)
|
||||
override val secondaryContainerLight = Color(0xFFE4E2E1)
|
||||
override val onSecondaryContainerLight = Color(0xFF656464)
|
||||
override val tertiaryLight = Color(0xFF5E5B5D)
|
||||
override val onTertiaryLight = Color(0xFFFFFFFF)
|
||||
override val tertiaryContainerLight = Color(0xFF777375)
|
||||
override val onTertiaryContainerLight = Color(0xFFFFFBFF)
|
||||
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 Surface = Color(0xFFF6F9FB)
|
||||
override val SurfaceVariant = Color(0xFFEEF2F4)
|
||||
override val OnSurface = Color(0xFF191C1E)
|
||||
override val OnSurfaceVariant = Color(0xFF41484D)
|
||||
|
||||
override val Error = Color(0xFFC62828)
|
||||
override val OnError = Color(0xFFFFFFFF)
|
||||
override val ErrorContainer = Color(0xFFFFDAD6)
|
||||
override val OnErrorContainer = Color(0xFF410002)
|
||||
|
||||
override val Outline = Color(0xFFBDC1C4)
|
||||
override val OutlineVariant = Color(0xFFDDE1E3)
|
||||
override val Background = Color(0xFFFBFCFE)
|
||||
override val OnBackground = Color(0xFF191C1E)
|
||||
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() {
|
||||
override val Primary = Color(0xFFFFC107)
|
||||
override val Secondary = Color(0xFFFFD54F)
|
||||
override val Tertiary = Color(0xFFFF8F00)
|
||||
override val OnPrimary = Color(0xFF000000)
|
||||
override val OnSecondary = Color(0xFF000000)
|
||||
override val OnTertiary = Color(0xFFFFFFFF)
|
||||
override val PrimaryContainer = Color(0xFFFFF8E1)
|
||||
override val SecondaryContainer = Color(0xFFFFF8E1)
|
||||
override val TertiaryContainer = Color(0xFFFFECB3)
|
||||
override val OnPrimaryContainer = Color(0xFF332A00)
|
||||
override val OnSecondaryContainer = Color(0xFF332A00)
|
||||
override val OnTertiaryContainer = Color(0xFF221200)
|
||||
override val ButtonContrast = Color(0xFFFFC107)
|
||||
override val primaryLight = Color(0xFF6D5E0F)
|
||||
override val onPrimaryLight = Color(0xFFFFFFFF)
|
||||
override val primaryContainerLight = Color(0xFFF8E288)
|
||||
override val onPrimaryContainerLight = Color(0xFF534600)
|
||||
override val secondaryLight = Color(0xFF6D5E0F)
|
||||
override val onSecondaryLight = Color(0xFFFFFFFF)
|
||||
override val secondaryContainerLight = Color(0xFFF7E388)
|
||||
override val onSecondaryContainerLight = Color(0xFF534600)
|
||||
override val tertiaryLight = Color(0xFF685F13)
|
||||
override val onTertiaryLight = Color(0xFFFFFFFF)
|
||||
override val tertiaryContainerLight = Color(0xFFF1E58A)
|
||||
override val onTertiaryContainerLight = Color(0xFF4F4800)
|
||||
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 Surface = Color(0xFFFFFAF3)
|
||||
override val SurfaceVariant = Color(0xFFFFF7E6)
|
||||
override val OnSurface = Color(0xFF1F1C17)
|
||||
override val OnSurfaceVariant = Color(0xFF4E4A3C)
|
||||
|
||||
override val Error = Color(0xFFB71C1C)
|
||||
override val OnError = Color(0xFFFFFFFF)
|
||||
override val ErrorContainer = Color(0xFFFFDAD6)
|
||||
override val OnErrorContainer = Color(0xFF410002)
|
||||
|
||||
override val Outline = Color(0xFFD1C8AF)
|
||||
override val OutlineVariant = Color(0xFFEEE8D7)
|
||||
override val Background = Color(0xFFFFFCF8)
|
||||
override val OnBackground = Color(0xFF1F1C17)
|
||||
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 {
|
||||
|
||||
@@ -44,6 +44,12 @@ import androidx.core.content.edit
|
||||
import androidx.core.net.toUri
|
||||
import com.sukisu.ultra.ui.util.BackgroundTransformation
|
||||
import com.sukisu.ultra.ui.util.saveTransformedBackground
|
||||
import androidx.activity.SystemBarStyle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.compose.material3.ColorScheme
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
|
||||
/**
|
||||
* 主题配置对象,管理应用的主题相关状态
|
||||
@@ -105,11 +111,18 @@ fun KernelSUTheme(
|
||||
if (!isCustomAlphaSet) {
|
||||
cardAlpha = if (systemIsDark) 0.50f else 1f
|
||||
}
|
||||
if (!isCustomDimSet) {
|
||||
cardDim = if (systemIsDark) 0.5f else 0f
|
||||
}
|
||||
save(context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SystemBarStyle(
|
||||
darkMode = darkTheme
|
||||
)
|
||||
|
||||
// 初始加载配置
|
||||
LaunchedEffect(Unit) {
|
||||
context.loadThemeMode()
|
||||
@@ -138,7 +151,9 @@ fun KernelSUTheme(
|
||||
// 根据暗色模式和自定义背景调整卡片配置
|
||||
val isDarkModeWithCustomBackground = darkTheme && ThemeConfig.customBackgroundUri != null
|
||||
if (darkTheme && !dynamicColor) {
|
||||
CardConfig.setDarkModeDefaults()
|
||||
CardConfig.setThemeDefaults(true)
|
||||
} else if (!darkTheme && !dynamicColor) {
|
||||
CardConfig.setThemeDefaults(false)
|
||||
}
|
||||
CardConfig.updateShadowEnabled(!isDarkModeWithCustomBackground)
|
||||
|
||||
@@ -190,6 +205,9 @@ fun KernelSUTheme(
|
||||
}
|
||||
}
|
||||
|
||||
// 计算适用的暗化值
|
||||
val dimFactor = CardConfig.cardDim
|
||||
|
||||
MaterialTheme(
|
||||
colorScheme = colorScheme,
|
||||
typography = Typography
|
||||
@@ -199,7 +217,8 @@ fun KernelSUTheme(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.zIndex(-2f)
|
||||
.background(if (darkTheme) Color.Black else Color.White)
|
||||
.background(if (darkTheme) if (CardConfig.isCustomBackgroundEnabled) { colorScheme.surfaceContainerLow } else { colorScheme.background }
|
||||
else if (CardConfig.isCustomBackgroundEnabled) { colorScheme.surfaceContainerLow } else { colorScheme.background })
|
||||
)
|
||||
|
||||
// 自定义背景层
|
||||
@@ -225,13 +244,13 @@ fun KernelSUTheme(
|
||||
)
|
||||
}
|
||||
|
||||
// 亮度调节层
|
||||
// 亮度调节层 (根据cardDim调整)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(
|
||||
if (darkTheme) Color.Black.copy(alpha = 0.6f)
|
||||
else Color.White.copy(alpha = 0.1f)
|
||||
if (darkTheme) Color.Black.copy(alpha = 0.6f + dimFactor * 0.3f)
|
||||
else Color.White.copy(alpha = 0.1f + dimFactor * 0.2f)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -243,8 +262,8 @@ fun KernelSUTheme(
|
||||
Brush.radialGradient(
|
||||
colors = listOf(
|
||||
Color.Transparent,
|
||||
if (darkTheme) Color.Black.copy(alpha = 0.5f)
|
||||
else Color.Black.copy(alpha = 0.2f)
|
||||
if (darkTheme) Color.Black.copy(alpha = 0.5f + dimFactor * 0.2f)
|
||||
else Color.Black.copy(alpha = 0.2f + dimFactor * 0.1f)
|
||||
),
|
||||
radius = 1200f
|
||||
)
|
||||
@@ -270,54 +289,73 @@ 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
|
||||
private fun createDynamicDarkColorScheme(context: Context): ColorScheme {
|
||||
val scheme = dynamicDarkColorScheme(context)
|
||||
return scheme.copy(
|
||||
background = if (CardConfig.isCustomBackgroundEnabled) Color.Transparent else scheme.background,
|
||||
surface = if (CardConfig.isCustomBackgroundEnabled) Color.Transparent else scheme.surface,
|
||||
onBackground = scheme.onBackground,
|
||||
onSurface = scheme.onSurface
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建动态浅色颜色方案
|
||||
*/
|
||||
@RequiresApi(Build.VERSION_CODES.S)
|
||||
@Composable
|
||||
private fun createDynamicLightColorScheme(context: Context) =
|
||||
dynamicLightColorScheme(context).copy(
|
||||
background = Color.Transparent,
|
||||
surface = Color.Transparent
|
||||
private fun createDynamicLightColorScheme(context: Context): ColorScheme {
|
||||
val scheme = dynamicLightColorScheme(context)
|
||||
return scheme.copy(
|
||||
background = if (CardConfig.isCustomBackgroundEnabled) Color.Transparent else scheme.background,
|
||||
surface = if (CardConfig.isCustomBackgroundEnabled) Color.Transparent else scheme.surface,
|
||||
onBackground = scheme.onBackground,
|
||||
onSurface = scheme.onSurface
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 创建深色颜色方案
|
||||
*/
|
||||
@Composable
|
||||
private fun createDarkColorScheme() = darkColorScheme(
|
||||
primary = ThemeConfig.currentTheme.Primary.copy(alpha = 0.8f),
|
||||
onPrimary = Color.White,
|
||||
primaryContainer = ThemeConfig.currentTheme.PrimaryContainer.copy(alpha = 0.15f),
|
||||
onPrimaryContainer = Color.White,
|
||||
secondary = ThemeConfig.currentTheme.Secondary.copy(alpha = 0.8f),
|
||||
onSecondary = Color.White,
|
||||
secondaryContainer = ThemeConfig.currentTheme.SecondaryContainer.copy(alpha = 0.15f),
|
||||
onSecondaryContainer = Color.White,
|
||||
tertiary = ThemeConfig.currentTheme.Tertiary.copy(alpha = 0.8f),
|
||||
onTertiary = Color.White,
|
||||
tertiaryContainer = ThemeConfig.currentTheme.TertiaryContainer.copy(alpha = 0.15f),
|
||||
onTertiaryContainer = Color.White,
|
||||
background = Color.Transparent,
|
||||
surface = Color.Transparent,
|
||||
onBackground = Color.White,
|
||||
onSurface = Color.White,
|
||||
surfaceVariant = Color(0xFF2F2F2F),
|
||||
onSurfaceVariant = Color.White.copy(alpha = 0.7f),
|
||||
outline = Color.White.copy(alpha = 0.12f),
|
||||
outlineVariant = Color.White.copy(alpha = 0.12f),
|
||||
error = ThemeConfig.currentTheme.Error,
|
||||
onError = ThemeConfig.currentTheme.OnError,
|
||||
errorContainer = ThemeConfig.currentTheme.ErrorContainer.copy(alpha = 0.15f),
|
||||
onErrorContainer = Color.White
|
||||
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 = if (CardConfig.isCustomBackgroundEnabled) Color.Transparent else ThemeConfig.currentTheme.backgroundDark,
|
||||
onBackground = ThemeConfig.currentTheme.onBackgroundDark,
|
||||
surface = if (CardConfig.isCustomBackgroundEnabled) Color.Transparent else ThemeConfig.currentTheme.surfaceDark,
|
||||
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,
|
||||
)
|
||||
|
||||
/**
|
||||
@@ -325,32 +363,44 @@ private fun createDarkColorScheme() = darkColorScheme(
|
||||
*/
|
||||
@Composable
|
||||
private fun createLightColorScheme() = lightColorScheme(
|
||||
primary = ThemeConfig.currentTheme.Primary,
|
||||
onPrimary = ThemeConfig.currentTheme.OnPrimary,
|
||||
primaryContainer = ThemeConfig.currentTheme.PrimaryContainer,
|
||||
onPrimaryContainer = ThemeConfig.currentTheme.OnPrimaryContainer,
|
||||
secondary = ThemeConfig.currentTheme.Secondary,
|
||||
onSecondary = ThemeConfig.currentTheme.OnSecondary,
|
||||
secondaryContainer = ThemeConfig.currentTheme.SecondaryContainer,
|
||||
onSecondaryContainer = ThemeConfig.currentTheme.OnSecondaryContainer,
|
||||
tertiary = ThemeConfig.currentTheme.Tertiary,
|
||||
onTertiary = ThemeConfig.currentTheme.OnTertiary,
|
||||
tertiaryContainer = ThemeConfig.currentTheme.TertiaryContainer,
|
||||
onTertiaryContainer = ThemeConfig.currentTheme.OnTertiaryContainer,
|
||||
background = Color.Transparent,
|
||||
surface = Color.Transparent,
|
||||
onBackground = Color.Black.copy(alpha = 0.87f),
|
||||
onSurface = Color.Black.copy(alpha = 0.87f),
|
||||
surfaceVariant = Color(0xFFF5F5F5),
|
||||
onSurfaceVariant = Color.Black.copy(alpha = 0.78f),
|
||||
outline = Color.Black.copy(alpha = 0.12f),
|
||||
outlineVariant = Color.Black.copy(alpha = 0.12f),
|
||||
error = ThemeConfig.currentTheme.Error,
|
||||
onError = ThemeConfig.currentTheme.OnError,
|
||||
errorContainer = ThemeConfig.currentTheme.ErrorContainer,
|
||||
onErrorContainer = ThemeConfig.currentTheme.OnErrorContainer
|
||||
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 = if (CardConfig.isCustomBackgroundEnabled) Color.Transparent else ThemeConfig.currentTheme.backgroundLight,
|
||||
onBackground = ThemeConfig.currentTheme.onBackgroundLight,
|
||||
surface = if (CardConfig.isCustomBackgroundEnabled) Color.Transparent else ThemeConfig.currentTheme.surfaceLight,
|
||||
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,
|
||||
)
|
||||
|
||||
|
||||
/**
|
||||
* 复制图片到应用内部存储并提升持久性
|
||||
*/
|
||||
@@ -536,3 +586,32 @@ fun Context.loadDynamicColorState() {
|
||||
|
||||
ThemeConfig.useDynamicColor = enabled
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SystemBarStyle(
|
||||
darkMode: Boolean,
|
||||
statusBarScrim: Color = Color.Transparent,
|
||||
navigationBarScrim: Color = Color.Transparent,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val activity = context as ComponentActivity
|
||||
|
||||
SideEffect {
|
||||
activity.enableEdgeToEdge(
|
||||
statusBarStyle = SystemBarStyle.auto(
|
||||
statusBarScrim.toArgb(),
|
||||
statusBarScrim.toArgb(),
|
||||
) { darkMode },
|
||||
navigationBarStyle = when {
|
||||
darkMode -> SystemBarStyle.dark(
|
||||
navigationBarScrim.toArgb()
|
||||
)
|
||||
|
||||
else -> SystemBarStyle.light(
|
||||
navigationBarScrim.toArgb(),
|
||||
navigationBarScrim.toArgb(),
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -7,13 +7,23 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.sukisu.ultra.ui.util.module.LatestVersionInfo
|
||||
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
|
||||
@@ -26,8 +36,10 @@ fun download(
|
||||
fileName: String,
|
||||
description: String,
|
||||
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 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())
|
||||
.setDestinationInExternalPublicDir(
|
||||
@@ -59,26 +78,168 @@ fun download(
|
||||
.setMimeType("application/zip")
|
||||
.setTitle(fileName)
|
||||
.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 {
|
||||
val url = "https://api.github.com/repos/ShirkNeko/SukiSU-Ultra/releases/latest"
|
||||
val defaultValue = LatestVersionInfo()
|
||||
return runCatching {
|
||||
okhttp3.OkHttpClient().newCall(okhttp3.Request.Builder().url(url).build()).execute()
|
||||
.use { response ->
|
||||
val client = okhttp3.OkHttpClient.Builder()
|
||||
.connectTimeout(15, TimeUnit.SECONDS)
|
||||
.readTimeout(30, TimeUnit.SECONDS)
|
||||
.writeTimeout(15, TimeUnit.SECONDS)
|
||||
.build()
|
||||
|
||||
val request = okhttp3.Request.Builder()
|
||||
.url(url)
|
||||
.header("User-Agent", CUSTOM_USER_AGENT)
|
||||
.build()
|
||||
|
||||
client.newCall(request).execute().use { response ->
|
||||
if (!response.isSuccessful) {
|
||||
Log.d("CheckUpdate", "Network request failed: ${response.message}")
|
||||
return defaultValue
|
||||
}
|
||||
val body = response.body?.string()
|
||||
if (body == null) {
|
||||
Log.d("CheckUpdate", "Response body is null")
|
||||
Log.d("CheckUpdate", "Return data is null")
|
||||
return defaultValue
|
||||
}
|
||||
Log.d("CheckUpdate", "Response body: $body")
|
||||
Log.d("CheckUpdate", "Return data: $body")
|
||||
val json = org.json.JSONObject(body)
|
||||
|
||||
// 直接从 tag_name 提取版本号(如 v1.1)
|
||||
@@ -95,11 +256,10 @@ fun checkNewVersion(): LatestVersionInfo {
|
||||
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")
|
||||
Log.d("CheckUpdate", "No matches found: $name, skip over")
|
||||
continue
|
||||
}
|
||||
val versionCode = matchResult.groupValues[1].toInt()
|
||||
@@ -112,13 +272,12 @@ fun checkNewVersion(): LatestVersionInfo {
|
||||
versionName
|
||||
)
|
||||
}
|
||||
Log.d("CheckUpdate", "No valid apk asset found, returning default value")
|
||||
Log.d("CheckUpdate", "No valid APK resource found, return default value")
|
||||
defaultValue
|
||||
}
|
||||
}.getOrDefault(defaultValue)
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
fun DownloadListener(context: Context, onDownloaded: (Uri) -> Unit) {
|
||||
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)
|
||||
try {
|
||||
if (globalMnt) {
|
||||
builder.build("su")
|
||||
} else {
|
||||
builder.build("su", "-mm")
|
||||
} else {
|
||||
builder.build("su")
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Log.e(TAG, "su failed: ", e)
|
||||
|
||||
@@ -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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.sukisu.ultra.ui.util
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import com.sukisu.ultra.ui.MainActivity
|
||||
|
||||
/**
|
||||
* 重启应用程序
|
||||
**/
|
||||
|
||||
fun Context.restartApp(
|
||||
activityClass: Class<out Activity>,
|
||||
finishCurrent: Boolean = true,
|
||||
clearTask: Boolean = true,
|
||||
newTask: Boolean = true
|
||||
) {
|
||||
val intent = Intent(this, activityClass)
|
||||
if (clearTask) intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
|
||||
if (newTask) intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
startActivity(intent)
|
||||
|
||||
if (finishCurrent && this is Activity) {
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新启动器图标
|
||||
*/
|
||||
fun toggleLauncherIcon(context: Context, useAlt: Boolean) {
|
||||
val pm = context.packageManager
|
||||
val main = ComponentName(context, MainActivity::class.java.name)
|
||||
val alt = ComponentName(context, "${MainActivity::class.java.name}Alias")
|
||||
if (useAlt) {
|
||||
pm.setComponentEnabledSetting(main, PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP)
|
||||
pm.setComponentEnabledSetting(alt, PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP)
|
||||
} else {
|
||||
pm.setComponentEnabledSetting(alt, PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP)
|
||||
pm.setComponentEnabledSetting(main, PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP)
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,10 @@
|
||||
package com.sukisu.ultra.ui.util
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import android.content.Context
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import com.sukisu.ultra.R
|
||||
|
||||
@Composable
|
||||
fun getSELinuxStatus(): String {
|
||||
fun getSELinuxStatus(context: Context): String {
|
||||
val shell = Shell.Builder.create().build("sh")
|
||||
val list = ArrayList<String>()
|
||||
|
||||
@@ -18,16 +16,16 @@ fun getSELinuxStatus(): String {
|
||||
|
||||
return if (result.isSuccess) {
|
||||
when (output) {
|
||||
"Enforcing" -> stringResource(R.string.selinux_status_enforcing)
|
||||
"Permissive" -> stringResource(R.string.selinux_status_permissive)
|
||||
"Disabled" -> stringResource(R.string.selinux_status_disabled)
|
||||
else -> stringResource(R.string.selinux_status_unknown)
|
||||
"Enforcing" -> context.getString(R.string.selinux_status_enforcing)
|
||||
"Permissive" -> context.getString(R.string.selinux_status_permissive)
|
||||
"Disabled" -> context.getString(R.string.selinux_status_disabled)
|
||||
else -> context.getString(R.string.selinux_status_unknown)
|
||||
}
|
||||
} else {
|
||||
if (output.contains("Permission denied")) {
|
||||
stringResource(R.string.selinux_status_enforcing)
|
||||
context.getString(R.string.selinux_status_enforcing)
|
||||
} else {
|
||||
stringResource(R.string.selinux_status_unknown)
|
||||
context.getString(R.string.selinux_status_unknown)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
package com.sukisu.ultra.ui.viewmodel
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
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 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(false)
|
||||
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", false)
|
||||
}
|
||||
}
|
||||
|
||||
fun initializeData() {
|
||||
viewModelScope.launch {
|
||||
loadCachedData()
|
||||
}
|
||||
}
|
||||
|
||||
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 newVersionInfo = checkNewVersion()
|
||||
latestVersionInfo = newVersionInfo
|
||||
prefs.edit {
|
||||
putString(KEY_VERSION_INFO, gson.toJson(newVersionInfo))
|
||||
putLong(KEY_LAST_UPDATE, System.currentTimeMillis())
|
||||
}
|
||||
}
|
||||
} 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getDeviceInfo(): String {
|
||||
var manufacturer =
|
||||
Build.MANUFACTURER[0].uppercaseChar().toString() + Build.MANUFACTURER.substring(1)
|
||||
if (Build.BRAND != Build.MANUFACTURER) {
|
||||
manufacturer += " " + Build.BRAND[0].uppercaseChar() + Build.BRAND.substring(1)
|
||||
}
|
||||
manufacturer += " " + Build.MODEL + " "
|
||||
return manufacturer
|
||||
}
|
||||
|
||||
@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 = getDeviceInfo()
|
||||
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)
|
||||
getDeviceInfo()
|
||||
}
|
||||
}
|
||||
|
||||
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 com.sukisu.ultra.ui.util.*
|
||||
|
||||
/**
|
||||
* @author ShirkNeko
|
||||
* @date 2025/5/31.
|
||||
*/
|
||||
class KpmViewModel : ViewModel() {
|
||||
var moduleList by mutableStateOf(emptyList<ModuleInfo>())
|
||||
private set
|
||||
|
||||
@@ -8,20 +8,29 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
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.launch
|
||||
import com.sukisu.ultra.ui.util.HanziToPinyin
|
||||
import com.sukisu.ultra.ui.util.listModules
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import java.text.Collator
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* @author ShirkNeko
|
||||
* @date 2025/5/31.
|
||||
*/
|
||||
class ModuleViewModel : ViewModel() {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "ModuleViewModel"
|
||||
private var modules by mutableStateOf<List<ModuleInfo>>(emptyList())
|
||||
private const val CUSTOM_USER_AGENT = "SukiSU-Ultra/2.0"
|
||||
}
|
||||
|
||||
class ModuleInfo(
|
||||
@@ -38,6 +47,7 @@ class ModuleViewModel : ViewModel() {
|
||||
val hasWebUi: Boolean,
|
||||
val hasActionScript: Boolean,
|
||||
val dirId: String, // real module id (dir name)
|
||||
var config: ModuleConfig? = null,
|
||||
)
|
||||
|
||||
var isRefreshing by mutableStateOf(false)
|
||||
@@ -98,9 +108,42 @@ class ModuleViewModel : ViewModel() {
|
||||
obj.optString("updateJson"),
|
||||
obj.optBoolean("web"),
|
||||
obj.optBoolean("action"),
|
||||
obj.getString("dir_id"),
|
||||
obj.getString("dir_id")
|
||||
)
|
||||
}.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
|
||||
}.onFailure { 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> {
|
||||
val empty = Triple("", "", "")
|
||||
if (m.updateJson.isEmpty() || m.remove || m.update || !m.enabled) {
|
||||
@@ -126,19 +173,32 @@ class ModuleViewModel : ViewModel() {
|
||||
val result = kotlin.runCatching {
|
||||
val url = m.updateJson
|
||||
Log.i(TAG, "checkUpdate url: $url")
|
||||
val response = okhttp3.OkHttpClient()
|
||||
.newCall(
|
||||
okhttp3.Request.Builder()
|
||||
.url(url)
|
||||
|
||||
val client = okhttp3.OkHttpClient.Builder()
|
||||
.connectTimeout(15, TimeUnit.SECONDS)
|
||||
.readTimeout(30, TimeUnit.SECONDS)
|
||||
.writeTimeout(15, TimeUnit.SECONDS)
|
||||
.build()
|
||||
).execute()
|
||||
|
||||
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}")
|
||||
if (response.isSuccessful) {
|
||||
response.body?.string() ?: ""
|
||||
} else {
|
||||
Log.d(TAG, "checkUpdate failed: ${response.message}")
|
||||
""
|
||||
}
|
||||
}.getOrDefault("")
|
||||
}.getOrElse { e ->
|
||||
Log.e(TAG, "checkUpdate exception", e)
|
||||
""
|
||||
}
|
||||
|
||||
Log.i(TAG, "checkUpdate result: $result")
|
||||
|
||||
if (result.isEmpty()) {
|
||||
@@ -149,7 +209,8 @@ class ModuleViewModel : ViewModel() {
|
||||
JSONObject(result)
|
||||
}.getOrNull() ?: return empty
|
||||
|
||||
val version = updateJson.optString("version", "")
|
||||
var version = updateJson.optString("version", "")
|
||||
version = sanitizeVersionString(version)
|
||||
val versionCode = updateJson.optInt("versionCode", 0)
|
||||
val zipUrl = updateJson.optString("zipUrl", "")
|
||||
val changelog = updateJson.optString("changelog", "")
|
||||
|
||||
@@ -1,38 +1,42 @@
|
||||
package com.sukisu.ultra.ui.viewmodel
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.content.Context
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.content.pm.PackageInfo
|
||||
import android.os.IBinder
|
||||
import android.os.Parcelable
|
||||
import android.os.SystemClock
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import com.sukisu.zako.IKsuInterface
|
||||
import com.sukisu.ultra.Natives
|
||||
import com.sukisu.ultra.ksuApp
|
||||
import com.sukisu.ultra.ui.KsuService
|
||||
import com.sukisu.ultra.ui.util.HanziToPinyin
|
||||
import com.sukisu.ultra.ui.util.KsuCli
|
||||
import java.text.Collator
|
||||
import java.util.*
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
import com.dergoogler.mmrl.platform.Platform
|
||||
import com.dergoogler.mmrl.platform.TIMEOUT_MILLIS
|
||||
import com.sukisu.ultra.ui.webui.getInstalledPackagesAll
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import androidx.core.content.edit
|
||||
|
||||
|
||||
/**
|
||||
* @author ShirkNeko
|
||||
* @date 2025/5/31.
|
||||
*/
|
||||
class SuperUserViewModel : ViewModel() {
|
||||
val isPlatformAlive get() = Platform.isAlive
|
||||
companion object {
|
||||
private const val TAG = "SuperUserViewModel"
|
||||
private var apps by mutableStateOf<List<AppInfo>>(emptyList())
|
||||
var apps by mutableStateOf<List<AppInfo>>(emptyList())
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
@@ -60,9 +64,11 @@ class SuperUserViewModel : ViewModel() {
|
||||
}
|
||||
}
|
||||
}
|
||||
private val prefs = ksuApp.getSharedPreferences("settings", Context.MODE_PRIVATE)!!
|
||||
|
||||
var search by mutableStateOf("")
|
||||
var showSystemApps by mutableStateOf(false)
|
||||
var showSystemApps by mutableStateOf(prefs.getBoolean("show_system_apps", false))
|
||||
private set
|
||||
var isRefreshing by mutableStateOf(false)
|
||||
private set
|
||||
|
||||
@@ -72,6 +78,11 @@ class SuperUserViewModel : ViewModel() {
|
||||
var selectedApps by mutableStateOf<Set<String>>(emptySet())
|
||||
internal set
|
||||
|
||||
fun updateShowSystemApps(newValue: Boolean) {
|
||||
showSystemApps = newValue
|
||||
prefs.edit { putBoolean("show_system_apps", newValue) }
|
||||
}
|
||||
|
||||
private val sortedList by derivedStateOf {
|
||||
val comparator = compareBy<AppInfo> {
|
||||
when {
|
||||
@@ -142,55 +153,60 @@ class SuperUserViewModel : ViewModel() {
|
||||
fetchAppList() // 刷新列表以显示最新状态
|
||||
}
|
||||
|
||||
private suspend fun connectKsuService(
|
||||
onDisconnect: () -> Unit = {}
|
||||
): Pair<IBinder, ServiceConnection> = suspendCoroutine { continuation ->
|
||||
val connection = object : ServiceConnection {
|
||||
override fun onServiceDisconnected(name: ComponentName?) {
|
||||
onDisconnect()
|
||||
}
|
||||
|
||||
override fun onServiceConnected(name: ComponentName?, binder: IBinder?) {
|
||||
continuation.resume(binder as IBinder to this)
|
||||
}
|
||||
}
|
||||
|
||||
val intent = Intent(ksuApp, KsuService::class.java)
|
||||
|
||||
val task = KsuService.bindOrTask(
|
||||
intent,
|
||||
Shell.EXECUTOR,
|
||||
connection,
|
||||
// 批量更新权限和umount模块设置
|
||||
suspend fun updateBatchPermissions(allowSu: Boolean, umountModules: Boolean? = null) {
|
||||
selectedApps.forEach { packageName ->
|
||||
val app = apps.find { it.packageName == packageName }
|
||||
app?.let {
|
||||
val profile = Natives.getAppProfile(packageName, it.uid)
|
||||
val updatedProfile = profile.copy(
|
||||
allowSu = allowSu,
|
||||
umountModules = umountModules ?: profile.umountModules,
|
||||
nonRootUseDefault = false
|
||||
)
|
||||
val shell = KsuCli.SHELL
|
||||
task?.let { it1 -> shell.execTask(it1) }
|
||||
if (Natives.setAppProfile(updatedProfile)) {
|
||||
apps = apps.map { app ->
|
||||
if (app.packageName == packageName) {
|
||||
app.copy(profile = updatedProfile)
|
||||
} else {
|
||||
app
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
clearSelection()
|
||||
showBatchActions = false // 批量操作完成后退出批量模式
|
||||
fetchAppList() // 刷新列表以显示最新状态
|
||||
}
|
||||
|
||||
private fun stopKsuService() {
|
||||
val intent = Intent(ksuApp, KsuService::class.java)
|
||||
KsuService.stop(intent)
|
||||
// 更新本地应用配置
|
||||
fun updateAppProfileLocally(packageName: String, updatedProfile: Natives.Profile) {
|
||||
apps = apps.map { app ->
|
||||
if (app.packageName == packageName) {
|
||||
app.copy(profile = updatedProfile)
|
||||
} else {
|
||||
app
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun fetchAppList() {
|
||||
isRefreshing = true
|
||||
|
||||
val result = connectKsuService {
|
||||
Log.w(TAG, "KsuService disconnected")
|
||||
}
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
withTimeoutOrNull(TIMEOUT_MILLIS) {
|
||||
while (!isPlatformAlive) {
|
||||
delay(500)
|
||||
}
|
||||
} ?: return@withContext // Exit early if timeout
|
||||
val pm = ksuApp.packageManager
|
||||
val start = SystemClock.elapsedRealtime()
|
||||
|
||||
val binder = result.first
|
||||
val allPackages = IKsuInterface.Stub.asInterface(binder).getPackages(0)
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
stopKsuService()
|
||||
val packages = Platform.getInstalledPackagesAll {
|
||||
Log.e(TAG, "getInstalledPackagesAll:", it)
|
||||
Toast.makeText(ksuApp, "Something went wrong, check logs", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
val packages = allPackages.list
|
||||
|
||||
apps = packages.map {
|
||||
val appInfo = it.applicationInfo
|
||||
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.updateLayoutParams
|
||||
import androidx.webkit.WebViewAssetLoader
|
||||
import com.dergoogler.mmrl.platform.model.ModId
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import com.sukisu.ultra.ui.util.createRootShell
|
||||
import java.io.File
|
||||
import com.dergoogler.mmrl.webui.interfaces.WXOptions
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
class WebUIActivity : ComponentActivity() {
|
||||
@@ -41,7 +43,8 @@ class WebUIActivity : ComponentActivity() {
|
||||
@Suppress("DEPRECATION")
|
||||
setTaskDescription(ActivityManager.TaskDescription("KernelSU - $name"))
|
||||
} else {
|
||||
val taskDescription = ActivityManager.TaskDescription.Builder().setLabel("KernelSU - $name").build()
|
||||
val taskDescription =
|
||||
ActivityManager.TaskDescription.Builder().setLabel("KernelSU - $name").build()
|
||||
setTaskDescription(taskDescription)
|
||||
}
|
||||
|
||||
@@ -82,7 +85,7 @@ class WebUIActivity : ComponentActivity() {
|
||||
settings.javaScriptEnabled = true
|
||||
settings.domStorageEnabled = true
|
||||
settings.allowFileAccess = false
|
||||
webviewInterface = WebViewInterface(this@WebUIActivity, this, moduleDir)
|
||||
webviewInterface = WebViewInterface(WXOptions(this@WebUIActivity, this, ModId(moduleId)))
|
||||
addJavascriptInterface(webviewInterface, "ksu")
|
||||
setWebViewClient(webViewClient)
|
||||
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
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.text.TextUtils
|
||||
import android.view.Window
|
||||
import android.webkit.JavascriptInterface
|
||||
import android.webkit.WebView
|
||||
import android.widget.Toast
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
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.ShellUtils
|
||||
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 org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import com.sukisu.ultra.ui.util.controlKpmModule
|
||||
import com.sukisu.ultra.ui.util.listKpmModules
|
||||
import com.sukisu.ultra.ui.util.*
|
||||
import java.io.File
|
||||
import java.util.concurrent.CompletableFuture
|
||||
|
||||
class WebViewInterface(
|
||||
val context: Context,
|
||||
private val webView: WebView,
|
||||
private val modDir: String
|
||||
) {
|
||||
wxOptions: WXOptions,
|
||||
) : WXInterface(wxOptions) {
|
||||
override var name: String = "ksu"
|
||||
|
||||
companion object {
|
||||
fun factory() = JavaScriptInterface(WebViewInterface::class.java)
|
||||
}
|
||||
|
||||
private val modDir get() = "/data/adb/modules/${modId.id}"
|
||||
|
||||
@JavascriptInterface
|
||||
fun exec(cmd: String): String {
|
||||
@@ -170,9 +175,9 @@ class WebViewInterface(
|
||||
if (context is Activity) {
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
if (enable) {
|
||||
hideSystemUI(context.window)
|
||||
hideSystemUI(activity.window)
|
||||
} else {
|
||||
showSystemUI(context.window)
|
||||
showSystemUI(activity.window)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -181,7 +186,7 @@ class WebViewInterface(
|
||||
@JavascriptInterface
|
||||
fun moduleInfo(): String {
|
||||
val moduleInfos = JSONArray(listModules())
|
||||
var currentModuleInfo = JSONObject()
|
||||
val currentModuleInfo = JSONObject()
|
||||
currentModuleInfo.put("moduleDir", modDir)
|
||||
val moduleId = File(modDir).getName()
|
||||
for (i in 0 until moduleInfos.length()) {
|
||||
@@ -191,7 +196,7 @@ class WebViewInterface(
|
||||
continue
|
||||
}
|
||||
|
||||
var keys = currentInfo.keys()
|
||||
val keys = currentInfo.keys()
|
||||
for (key in keys) {
|
||||
currentModuleInfo.put(key, currentInfo.get(key))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,222 @@
|
||||
package zako.zako.zako.zakoui.activity.component
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.navigation.NavHostController
|
||||
import com.ramcosta.composedestinations.utils.isRouteOnBackStackAsState
|
||||
import com.ramcosta.composedestinations.utils.rememberDestinationsNavigator
|
||||
import com.ramcosta.composedestinations.spec.RouteOrDirection
|
||||
import com.ramcosta.composedestinations.generated.NavGraphs
|
||||
import com.sukisu.ultra.Natives
|
||||
import com.sukisu.ultra.ksuApp
|
||||
import com.sukisu.ultra.ui.MainActivity
|
||||
import zako.zako.zako.zakoui.activity.util.AppData
|
||||
import zako.zako.zako.zakoui.activity.util.AppData.getKpmVersionUse
|
||||
import com.sukisu.ultra.ui.screen.BottomBarDestination
|
||||
import com.sukisu.ultra.ui.theme.CardConfig.cardAlpha
|
||||
import com.sukisu.ultra.ui.theme.CardConfig.cardElevation
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||
import androidx.compose.foundation.layout.only
|
||||
import androidx.compose.foundation.layout.navigationBars
|
||||
import zako.zako.zako.zakoui.activity.util.AppData.DataRefreshManager
|
||||
|
||||
@SuppressLint("ContextCastToActivity")
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun BottomBar(navController: NavHostController) {
|
||||
val navigator = navController.rememberDestinationsNavigator()
|
||||
val isFullFeatured = AppData.isFullFeatured(ksuApp.packageName)
|
||||
val kpmVersion = getKpmVersionUse()
|
||||
val cardColor = MaterialTheme.colorScheme.surfaceContainer
|
||||
val activity = LocalContext.current as MainActivity
|
||||
val settings by activity.settingsStateFlow.collectAsState()
|
||||
|
||||
// 检查是否隐藏红点
|
||||
val isHideOtherInfo = settings.isHideOtherInfo
|
||||
val showKpmInfo = settings.showKpmInfo
|
||||
|
||||
// 收集计数数据
|
||||
val superuserCount by DataRefreshManager.superuserCount.collectAsState()
|
||||
val moduleCount by DataRefreshManager.moduleCount.collectAsState()
|
||||
val kpmModuleCount by DataRefreshManager.kpmModuleCount.collectAsState()
|
||||
|
||||
|
||||
NavigationBar(
|
||||
modifier = Modifier.windowInsetsPadding(
|
||||
WindowInsets.navigationBars.only(WindowInsetsSides.Horizontal)
|
||||
),
|
||||
containerColor = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = cardColor.copy(alpha = cardAlpha),
|
||||
scrolledContainerColor = cardColor.copy(alpha = cardAlpha)
|
||||
).containerColor,
|
||||
tonalElevation = cardElevation
|
||||
) {
|
||||
BottomBarDestination.entries.forEach { destination ->
|
||||
if (destination == BottomBarDestination.Kpm) {
|
||||
if (kpmVersion.isNotEmpty() && !kpmVersion.startsWith("Error") && !showKpmInfo && Natives.version >= Natives.MINIMAL_SUPPORTED_KPM) {
|
||||
if (!isFullFeatured && destination.rootRequired) return@forEach
|
||||
val isCurrentDestOnBackStack by navController.isRouteOnBackStackAsState(destination.direction)
|
||||
NavigationBarItem(
|
||||
selected = isCurrentDestOnBackStack,
|
||||
onClick = {
|
||||
if (!isCurrentDestOnBackStack) {
|
||||
navigator.popBackStack(destination.direction, false)
|
||||
}
|
||||
navigator.navigate(destination.direction) {
|
||||
popUpTo(NavGraphs.root as RouteOrDirection) {
|
||||
saveState = true
|
||||
}
|
||||
launchSingleTop = true
|
||||
restoreState = true
|
||||
}
|
||||
},
|
||||
icon = {
|
||||
BadgedBox(
|
||||
badge = {
|
||||
if (kpmModuleCount > 0 && !isHideOtherInfo) {
|
||||
Badge(
|
||||
containerColor = MaterialTheme.colorScheme.secondary
|
||||
) {
|
||||
Text(
|
||||
text = kpmModuleCount.toString(),
|
||||
style = MaterialTheme.typography.labelSmall
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
) {
|
||||
if (isCurrentDestOnBackStack) {
|
||||
Icon(destination.iconSelected, stringResource(destination.label))
|
||||
} else {
|
||||
Icon(destination.iconNotSelected, stringResource(destination.label))
|
||||
}
|
||||
}
|
||||
},
|
||||
label = { Text(stringResource(destination.label),style = MaterialTheme.typography.labelMedium) },
|
||||
alwaysShowLabel = false
|
||||
)
|
||||
}
|
||||
} else if (destination == BottomBarDestination.SuperUser) {
|
||||
if (!isFullFeatured && destination.rootRequired) return@forEach
|
||||
val isCurrentDestOnBackStack by navController.isRouteOnBackStackAsState(destination.direction)
|
||||
|
||||
NavigationBarItem(
|
||||
selected = isCurrentDestOnBackStack,
|
||||
onClick = {
|
||||
if (isCurrentDestOnBackStack) {
|
||||
navigator.popBackStack(destination.direction, false)
|
||||
}
|
||||
navigator.navigate(destination.direction) {
|
||||
popUpTo(NavGraphs.root) {
|
||||
saveState = true
|
||||
}
|
||||
launchSingleTop = true
|
||||
restoreState = true
|
||||
}
|
||||
},
|
||||
icon = {
|
||||
BadgedBox(
|
||||
badge = {
|
||||
if (superuserCount > 0 && !isHideOtherInfo) {
|
||||
Badge(
|
||||
containerColor = MaterialTheme.colorScheme.secondary
|
||||
) {
|
||||
Text(
|
||||
text = superuserCount.toString(),
|
||||
style = MaterialTheme.typography.labelSmall
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
) {
|
||||
if (isCurrentDestOnBackStack) {
|
||||
Icon(destination.iconSelected, stringResource(destination.label))
|
||||
} else {
|
||||
Icon(destination.iconNotSelected, stringResource(destination.label))
|
||||
}
|
||||
}
|
||||
},
|
||||
label = { Text(stringResource(destination.label),style = MaterialTheme.typography.labelMedium) },
|
||||
alwaysShowLabel = false
|
||||
)
|
||||
} else if (destination == BottomBarDestination.Module) {
|
||||
if (!isFullFeatured && destination.rootRequired) return@forEach
|
||||
val isCurrentDestOnBackStack by navController.isRouteOnBackStackAsState(destination.direction)
|
||||
|
||||
NavigationBarItem(
|
||||
selected = isCurrentDestOnBackStack,
|
||||
onClick = {
|
||||
if (isCurrentDestOnBackStack) {
|
||||
navigator.popBackStack(destination.direction, false)
|
||||
}
|
||||
navigator.navigate(destination.direction) {
|
||||
popUpTo(NavGraphs.root) {
|
||||
saveState = true
|
||||
}
|
||||
launchSingleTop = true
|
||||
restoreState = true
|
||||
}
|
||||
},
|
||||
icon = {
|
||||
BadgedBox(
|
||||
badge = {
|
||||
if (moduleCount > 0 && !isHideOtherInfo) {
|
||||
Badge(
|
||||
containerColor = MaterialTheme.colorScheme.secondary)
|
||||
{
|
||||
Text(
|
||||
text = moduleCount.toString(),
|
||||
style = MaterialTheme.typography.labelSmall
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
) {
|
||||
if (isCurrentDestOnBackStack) {
|
||||
Icon(destination.iconSelected, stringResource(destination.label))
|
||||
} else {
|
||||
Icon(destination.iconNotSelected, stringResource(destination.label))
|
||||
}
|
||||
}
|
||||
},
|
||||
label = { Text(stringResource(destination.label),style = MaterialTheme.typography.labelMedium) },
|
||||
alwaysShowLabel = false
|
||||
)
|
||||
} else {
|
||||
if (!isFullFeatured && destination.rootRequired) return@forEach
|
||||
val isCurrentDestOnBackStack by navController.isRouteOnBackStackAsState(destination.direction)
|
||||
|
||||
NavigationBarItem(
|
||||
selected = isCurrentDestOnBackStack,
|
||||
onClick = {
|
||||
if (isCurrentDestOnBackStack) {
|
||||
navigator.popBackStack(destination.direction, false)
|
||||
}
|
||||
navigator.navigate(destination.direction) {
|
||||
popUpTo(NavGraphs.root) {
|
||||
saveState = true
|
||||
}
|
||||
launchSingleTop = true
|
||||
restoreState = true
|
||||
}
|
||||
},
|
||||
icon = {
|
||||
if (isCurrentDestOnBackStack) {
|
||||
Icon(destination.iconSelected, stringResource(destination.label))
|
||||
} else {
|
||||
Icon(destination.iconNotSelected, stringResource(destination.label))
|
||||
}
|
||||
},
|
||||
label = { Text(stringResource(destination.label),style = MaterialTheme.typography.labelMedium) },
|
||||
alwaysShowLabel = false
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package zako.zako.zako.zakoui.activity.util
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.slideInVertically
|
||||
import androidx.compose.animation.slideOutVertically
|
||||
import androidx.compose.runtime.Composable
|
||||
|
||||
object AnimatedBottomBar {
|
||||
@Composable
|
||||
fun AnimatedBottomBarWrapper(
|
||||
showBottomBar: Boolean,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
visible = showBottomBar,
|
||||
enter = slideInVertically(initialOffsetY = { it }) + fadeIn(),
|
||||
exit = slideOutVertically(targetOffsetY = { it }) + fadeOut()
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package zako.zako.zako.zakoui.activity.util
|
||||
|
||||
import com.sukisu.ultra.Natives
|
||||
import com.sukisu.ultra.ui.util.getKpmModuleCount
|
||||
import com.sukisu.ultra.ui.util.getKpmVersion
|
||||
import com.sukisu.ultra.ui.util.getModuleCount
|
||||
import com.sukisu.ultra.ui.util.getSuperuserCount
|
||||
import com.sukisu.ultra.ui.util.rootAvailable
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
object AppData {
|
||||
object DataRefreshManager {
|
||||
// 私有状态流
|
||||
private val _superuserCount = MutableStateFlow(0)
|
||||
private val _moduleCount = MutableStateFlow(0)
|
||||
private val _kpmModuleCount = MutableStateFlow(0)
|
||||
|
||||
// 公开的只读状态流
|
||||
val superuserCount: StateFlow<Int> = _superuserCount.asStateFlow()
|
||||
val moduleCount: StateFlow<Int> = _moduleCount.asStateFlow()
|
||||
val kpmModuleCount: StateFlow<Int> = _kpmModuleCount.asStateFlow()
|
||||
|
||||
/**
|
||||
* 刷新所有数据计数
|
||||
*/
|
||||
fun refreshData() {
|
||||
_superuserCount.value = getSuperuserCountUse()
|
||||
_moduleCount.value = getModuleCountUse()
|
||||
_kpmModuleCount.value = getKpmModuleCountUse()
|
||||
}
|
||||
|
||||
/**
|
||||
* 异步刷新所有数据
|
||||
*/
|
||||
suspend fun refreshDataAsync() = withContext(Dispatchers.IO) {
|
||||
refreshData()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取超级用户应用计数
|
||||
*/
|
||||
fun getSuperuserCountUse(): Int {
|
||||
return try {
|
||||
if (!rootAvailable()) return 0
|
||||
getSuperuserCount()
|
||||
} catch (_: Exception) {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取模块计数
|
||||
*/
|
||||
fun getModuleCountUse(): Int {
|
||||
return try {
|
||||
if (!rootAvailable()) return 0
|
||||
getModuleCount()
|
||||
} catch (_: Exception) {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取KPM模块计数
|
||||
*/
|
||||
fun getKpmModuleCountUse(): Int {
|
||||
return try {
|
||||
if (!rootAvailable()) return 0
|
||||
val kpmVersion = getKpmVersionUse()
|
||||
if (kpmVersion.isEmpty() || kpmVersion.startsWith("Error")) return 0
|
||||
getKpmModuleCount()
|
||||
} catch (_: Exception) {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取KPM版本
|
||||
*/
|
||||
fun getKpmVersionUse(): String {
|
||||
return try {
|
||||
if (!rootAvailable()) return ""
|
||||
val version = getKpmVersion()
|
||||
if (version.isEmpty()) "" else version
|
||||
} catch (e: Exception) {
|
||||
"Error: ${e.message}"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否具有管理员权限
|
||||
*/
|
||||
fun isManager(packageName: String): Boolean {
|
||||
return Natives.becomeManager(packageName)
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否是完整功能模式
|
||||
*/
|
||||
fun isFullFeatured(packageName: String): Boolean {
|
||||
val isManager = Natives.becomeManager(packageName)
|
||||
return isManager && !Natives.requireNewKernel() && rootAvailable()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package zako.zako.zako.zakoui.activity.util
|
||||
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.LifecycleCoroutineScope
|
||||
import com.sukisu.ultra.ui.MainActivity
|
||||
import zako.zako.zako.zakoui.activity.util.AppData.DataRefreshManager
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
object DataRefreshUtils {
|
||||
|
||||
fun startDataRefreshCoroutine(scope: LifecycleCoroutineScope) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
while (isActive) {
|
||||
DataRefreshManager.refreshData()
|
||||
delay(5000)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun startSettingsMonitorCoroutine(
|
||||
scope: LifecycleCoroutineScope,
|
||||
activity: MainActivity,
|
||||
settingsStateFlow: MutableStateFlow<MainActivity.SettingsState>
|
||||
) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
while (isActive) {
|
||||
val prefs = activity.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||
settingsStateFlow.value = MainActivity.SettingsState(
|
||||
isHideOtherInfo = prefs.getBoolean("is_hide_other_info", false),
|
||||
showKpmInfo = prefs.getBoolean("show_kpm_info", false)
|
||||
)
|
||||
delay(1000)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun refreshData(scope: LifecycleCoroutineScope) {
|
||||
scope.launch {
|
||||
DataRefreshManager.refreshData()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package zako.zako.zako.zakoui.activity.util
|
||||
|
||||
import android.content.Context
|
||||
|
||||
object DisplayUtils {
|
||||
|
||||
fun applyCustomDpi(context: Context) {
|
||||
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||
val customDpi = prefs.getInt("app_dpi", 0)
|
||||
|
||||
if (customDpi > 0) {
|
||||
try {
|
||||
val resources = context.resources
|
||||
val metrics = resources.displayMetrics
|
||||
metrics.density = customDpi / 160f
|
||||
@Suppress("DEPRECATION")
|
||||
metrics.scaledDensity = customDpi / 160f
|
||||
metrics.densityDpi = customDpi
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package zako.zako.zako.zakoui.activity.util
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import android.os.Build
|
||||
import java.util.Locale
|
||||
|
||||
object LocaleUtils {
|
||||
|
||||
@SuppressLint("ObsoleteSdkInt")
|
||||
fun applyLanguageSetting(context: Context) {
|
||||
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||
val languageCode = prefs.getString("app_language", "") ?: ""
|
||||
|
||||
if (languageCode.isNotEmpty()) {
|
||||
val locale = Locale.forLanguageTag(languageCode)
|
||||
Locale.setDefault(locale)
|
||||
|
||||
val resources = context.resources
|
||||
val config = Configuration(resources.configuration)
|
||||
config.setLocale(locale)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
context.createConfigurationContext(config)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
resources.updateConfiguration(config, resources.displayMetrics)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun applyLocale(context: Context): Context {
|
||||
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||
val languageCode = prefs.getString("app_language", "") ?: ""
|
||||
|
||||
var newContext = context
|
||||
if (languageCode.isNotEmpty()) {
|
||||
val locale = Locale.forLanguageTag(languageCode)
|
||||
Locale.setDefault(locale)
|
||||
|
||||
val config = Configuration(context.resources.configuration)
|
||||
config.setLocale(locale)
|
||||
newContext = context.createConfigurationContext(config)
|
||||
}
|
||||
|
||||
return newContext
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package zako.zako.zako.zakoui.activity.util
|
||||
|
||||
import androidx.compose.animation.AnimatedContentTransitionScope
|
||||
import androidx.compose.animation.EnterTransition
|
||||
import androidx.compose.animation.ExitTransition
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.navigation.NavBackStackEntry
|
||||
import com.ramcosta.composedestinations.animations.NavHostAnimatedDestinationStyle
|
||||
|
||||
object NavigationUtils {
|
||||
fun defaultTransitions() = object : NavHostAnimatedDestinationStyle() {
|
||||
override val enterTransition: AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition
|
||||
get() = { fadeIn(animationSpec = tween(340)) }
|
||||
override val exitTransition: AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition
|
||||
get() = { fadeOut(animationSpec = tween(340)) }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package zako.zako.zako.zakoui.activity.util
|
||||
|
||||
import android.content.Context
|
||||
import android.database.ContentObserver
|
||||
import android.os.Handler
|
||||
import androidx.core.content.edit
|
||||
import com.sukisu.ultra.ui.MainActivity
|
||||
import com.sukisu.ultra.ui.theme.CardConfig
|
||||
import com.sukisu.ultra.ui.theme.ThemeConfig
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
class ThemeChangeContentObserver(
|
||||
handler: Handler,
|
||||
private val onThemeChanged: () -> Unit
|
||||
) : ContentObserver(handler) {
|
||||
override fun onChange(selfChange: Boolean) {
|
||||
super.onChange(selfChange)
|
||||
onThemeChanged()
|
||||
}
|
||||
}
|
||||
|
||||
object ThemeUtils {
|
||||
|
||||
fun initializeThemeSettings(activity: MainActivity, settingsStateFlow: MutableStateFlow<MainActivity.SettingsState>) {
|
||||
val prefs = activity.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||
val isFirstRun = prefs.getBoolean("is_first_run", true)
|
||||
|
||||
settingsStateFlow.value = MainActivity.SettingsState(
|
||||
isHideOtherInfo = prefs.getBoolean("is_hide_other_info", false),
|
||||
showKpmInfo = prefs.getBoolean("show_kpm_info", false)
|
||||
)
|
||||
|
||||
if (isFirstRun) {
|
||||
ThemeConfig.preventBackgroundRefresh = false
|
||||
activity.getSharedPreferences("theme_prefs", Context.MODE_PRIVATE).edit {
|
||||
putBoolean("prevent_background_refresh", false)
|
||||
}
|
||||
prefs.edit { putBoolean("is_first_run", false) }
|
||||
}
|
||||
|
||||
// 加载保存的背景设置
|
||||
loadThemeMode()
|
||||
loadThemeColors()
|
||||
loadDynamicColorState()
|
||||
CardConfig.load(activity.applicationContext)
|
||||
}
|
||||
|
||||
fun registerThemeChangeObserver(activity: MainActivity): ThemeChangeContentObserver {
|
||||
val contentObserver = ThemeChangeContentObserver(Handler(activity.mainLooper)) {
|
||||
activity.runOnUiThread {
|
||||
if (!ThemeConfig.preventBackgroundRefresh) {
|
||||
ThemeConfig.backgroundImageLoaded = false
|
||||
loadCustomBackground()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
activity.contentResolver.registerContentObserver(
|
||||
android.provider.Settings.System.getUriFor("ui_night_mode"),
|
||||
false,
|
||||
contentObserver
|
||||
)
|
||||
|
||||
return contentObserver
|
||||
}
|
||||
|
||||
fun unregisterThemeChangeObserver(activity: MainActivity, observer: ThemeChangeContentObserver) {
|
||||
activity.contentResolver.unregisterContentObserver(observer)
|
||||
}
|
||||
|
||||
fun onActivityPause(activity: MainActivity) {
|
||||
CardConfig.save(activity.applicationContext)
|
||||
activity.getSharedPreferences("theme_prefs", Context.MODE_PRIVATE).edit {
|
||||
putBoolean("prevent_background_refresh", true)
|
||||
}
|
||||
ThemeConfig.preventBackgroundRefresh = true
|
||||
}
|
||||
|
||||
fun onActivityResume() {
|
||||
if (!ThemeConfig.backgroundImageLoaded && !ThemeConfig.preventBackgroundRefresh) {
|
||||
loadCustomBackground()
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadThemeMode() {
|
||||
}
|
||||
|
||||
private fun loadThemeColors() {
|
||||
}
|
||||
|
||||
private fun loadDynamicColorState() {
|
||||
}
|
||||
|
||||
private fun loadCustomBackground() {
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.sukisu.ultra.flash
|
||||
package zako.zako.zako.zakoui.flash
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
@@ -27,6 +27,11 @@ import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
|
||||
|
||||
/**
|
||||
* @author ShirkNeko
|
||||
* @date 2025/5/31.
|
||||
*/
|
||||
data class FlashState(
|
||||
val isFlashing: Boolean = false,
|
||||
val isCompleted: Boolean = false,
|
||||
@@ -0,0 +1,418 @@
|
||||
package zako.zako.zako.zakoui.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 zako.zako.zako.zakoui.flash.HorizonKernelState
|
||||
import zako.zako.zako.zakoui.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
|
||||
import zako.zako.zako.zakoui.flash.FlashState
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
/**
|
||||
* @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)
|
||||
}
|
||||
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: 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: 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 colorScheme = MaterialTheme.colorScheme
|
||||
val cardColor = if (CardConfig.isCustomBackgroundEnabled) {
|
||||
colorScheme.surfaceContainerLow
|
||||
} else {
|
||||
colorScheme.background
|
||||
}
|
||||
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
|
||||
)
|
||||
}
|
||||
@@ -2,21 +2,772 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
android:viewportWidth="512"
|
||||
android:viewportHeight="512">
|
||||
<group android:scaleX="0.45"
|
||||
android:scaleY="0.45"
|
||||
android:translateX="140.8"
|
||||
android:translateY="140.8">
|
||||
|
||||
<group
|
||||
android:scaleX="0.135"
|
||||
android:scaleY="0.135">
|
||||
<path
|
||||
android:pathData="M 259 259 H 541 V 541 H 259 V 259 Z"
|
||||
android:strokeWidth="18"
|
||||
android:strokeColor="#1e110d" />
|
||||
android:fillColor="#fff9f6"
|
||||
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,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
|
||||
android:fillColor="#1e110d"
|
||||
android:pathData="M 257 257 H 407 V 407 H 257 V 257 Z" />
|
||||
android:fillColor="#93d4fa"
|
||||
android:strokeColor="#4c4f59"
|
||||
android:strokeWidth="6.9"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeMiterLimit="1.3"
|
||||
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="#1e110d"
|
||||
android:pathData="M 393 393 H 543 V 543 H 393 V 393 Z" />
|
||||
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>
|
||||
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
|
||||
<group
|
||||
android:scaleX="0.135"
|
||||
android:scaleY="0.135">
|
||||
<path
|
||||
android:pathData="M 259 259 H 541 V 541 H 259 V 259 Z"
|
||||
android:strokeWidth="18"
|
||||
android:strokeColor="#1e110d" />
|
||||
<path
|
||||
android:fillColor="#1e110d"
|
||||
android:pathData="M 257 257 H 407 V 407 H 257 V 257 Z" />
|
||||
<path
|
||||
android:fillColor="#1e110d"
|
||||
android:pathData="M 393 393 H 543 V 543 H 393 V 393 Z" />
|
||||
</group>
|
||||
</vector>
|
||||
@@ -1,22 +1,27 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
|
||||
<group
|
||||
android:scaleX="0.135"
|
||||
android:scaleY="0.135">
|
||||
android:width="512dp"
|
||||
android:height="512dp"
|
||||
android:viewportWidth="512"
|
||||
android:viewportHeight="512">
|
||||
<path
|
||||
android:pathData="M 259 259 H 541 V 541 H 259 V 259 Z"
|
||||
android:strokeWidth="18"
|
||||
android:strokeColor="#000000" />
|
||||
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:strokeLineJoin="round"
|
||||
android:strokeWidth="8.74077"
|
||||
android:fillColor="#ffffff"
|
||||
android:strokeColor="#000000"
|
||||
android:fillAlpha="0.734285"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
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:strokeLineJoin="round"
|
||||
android:strokeWidth="8.07261"
|
||||
android:fillColor="#ffffff"
|
||||
android:strokeColor="#000000"
|
||||
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:pathData="M 257 257 H 407 V 407 H 257 V 257 Z" />
|
||||
<path
|
||||
android:fillColor="#000000"
|
||||
android:pathData="M 393 393 H 543 V 543 H 393 V 393 Z" />
|
||||
</group>
|
||||
android:strokeColor="#00000000"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
|
||||
<group
|
||||
android:scaleX="0.135"
|
||||
android:scaleY="0.135">
|
||||
<path
|
||||
android:pathData="M 259 259 H 541 V 541 H 259 V 259 Z"
|
||||
android:strokeWidth="18"
|
||||
android:strokeColor="#000000" />
|
||||
<path
|
||||
android:fillColor="#000000"
|
||||
android:pathData="M 257 257 H 407 V 407 H 257 V 257 Z" />
|
||||
<path
|
||||
android:fillColor="#000000"
|
||||
android:pathData="M 393 393 H 543 V 543 H 393 V 393 Z" />
|
||||
</group>
|
||||
</vector>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?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_alt"/>
|
||||
<monochrome android:drawable="@drawable/ic_launcher_monochrome_alt" />
|
||||
</adaptive-icon>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?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_alt"/>
|
||||
<monochrome android:drawable="@drawable/ic_launcher_monochrome_alt" />
|
||||
</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 |