Compare commits
135 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 |
74
.github/workflows/build-lkm-local.yml
vendored
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
name: Build LKM for KernelSU Local
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
inputs:
|
||||||
|
upload:
|
||||||
|
required: true
|
||||||
|
type: boolean
|
||||||
|
default: true
|
||||||
|
description: "Whether to upload to branch"
|
||||||
|
secrets:
|
||||||
|
# username:github_pat
|
||||||
|
TOKEN:
|
||||||
|
required: true
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
upload:
|
||||||
|
required: true
|
||||||
|
type: boolean
|
||||||
|
default: true
|
||||||
|
description: "Whether to upload to branch"
|
||||||
|
jobs:
|
||||||
|
build-lkm:
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- version: "android12-5.10"
|
||||||
|
sub_level: 236
|
||||||
|
os_patch_level: 2025-05
|
||||||
|
- version: "android13-5.10"
|
||||||
|
sub_level: 234
|
||||||
|
os_patch_level: 2025-03
|
||||||
|
- version: "android13-5.15"
|
||||||
|
sub_level: 178
|
||||||
|
os_patch_level: 2025-03
|
||||||
|
- version: "android14-5.15"
|
||||||
|
sub_level: 178
|
||||||
|
os_patch_level: 2025-03
|
||||||
|
- version: "android14-6.1"
|
||||||
|
sub_level: 134
|
||||||
|
os_patch_level: 2025-05
|
||||||
|
- version: "android15-6.6"
|
||||||
|
sub_level: 87
|
||||||
|
os_patch_level: 2025-05
|
||||||
|
# uses: ./.github/workflows/gki-kernel-mock.yml when debugging
|
||||||
|
uses: ./.github/workflows/gki-kernel-local.yml
|
||||||
|
with:
|
||||||
|
version: ${{ matrix.version }}
|
||||||
|
version_name: ${{ matrix.version }}.${{ matrix.sub_level }}
|
||||||
|
tag: ${{ matrix.version }}-${{ matrix.os_patch_level }}
|
||||||
|
os_patch_level: ${{ matrix.os_patch_level }}
|
||||||
|
build_lkm: true
|
||||||
|
|
||||||
|
push-to-branch:
|
||||||
|
needs: [build-lkm]
|
||||||
|
runs-on: self-hosted
|
||||||
|
if: ${{ inputs.upload }}
|
||||||
|
steps:
|
||||||
|
- name: Download all workflow run artifacts
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
path: bin/
|
||||||
|
merge-multiple: true
|
||||||
|
- name: Push to branch LKM
|
||||||
|
run: |
|
||||||
|
cd bin
|
||||||
|
git config --global init.defaultBranch lkm
|
||||||
|
git init
|
||||||
|
git remote add origin https://${{ secrets.TOKEN }}@github.com/${{ github.repository }}
|
||||||
|
git config --local user.name "github-actions[bot]"
|
||||||
|
git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||||
|
find . -type f
|
||||||
|
git add .
|
||||||
|
git commit -m "Upload LKM from ${{ github.sha }}" -m "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
|
||||||
|
git push --force --set-upstream origin lkm
|
||||||
12
.github/workflows/build-lkm.yml
vendored
@@ -24,8 +24,8 @@ jobs:
|
|||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- version: "android12-5.10"
|
- version: "android12-5.10"
|
||||||
sub_level: 233
|
sub_level: 236
|
||||||
os_patch_level: 2025-02
|
os_patch_level: 2025-05
|
||||||
- version: "android13-5.10"
|
- version: "android13-5.10"
|
||||||
sub_level: 234
|
sub_level: 234
|
||||||
os_patch_level: 2025-03
|
os_patch_level: 2025-03
|
||||||
@@ -36,11 +36,11 @@ jobs:
|
|||||||
sub_level: 178
|
sub_level: 178
|
||||||
os_patch_level: 2025-03
|
os_patch_level: 2025-03
|
||||||
- version: "android14-6.1"
|
- version: "android14-6.1"
|
||||||
sub_level: 129
|
sub_level: 134
|
||||||
os_patch_level: 2025-04
|
os_patch_level: 2025-05
|
||||||
- version: "android15-6.6"
|
- version: "android15-6.6"
|
||||||
sub_level: 82
|
sub_level: 87
|
||||||
os_patch_level: 2025-04
|
os_patch_level: 2025-05
|
||||||
# uses: ./.github/workflows/gki-kernel-mock.yml when debugging
|
# uses: ./.github/workflows/gki-kernel-mock.yml when debugging
|
||||||
uses: ./.github/workflows/gki-kernel.yml
|
uses: ./.github/workflows/gki-kernel.yml
|
||||||
with:
|
with:
|
||||||
|
|||||||
252
.github/workflows/build-manager-manual.yml
vendored
Normal file
@@ -0,0 +1,252 @@
|
|||||||
|
name: Build Manager Manual
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
build_lkm:
|
||||||
|
required: true
|
||||||
|
type: choice
|
||||||
|
default: "auto"
|
||||||
|
options:
|
||||||
|
- "true"
|
||||||
|
- "false"
|
||||||
|
- "auto"
|
||||||
|
description: "Whether to build lkm"
|
||||||
|
upload_lkm:
|
||||||
|
required: true
|
||||||
|
type: boolean
|
||||||
|
default: true
|
||||||
|
description: "Whether to upload lkm"
|
||||||
|
jobs:
|
||||||
|
check-build-lkm:
|
||||||
|
runs-on: self-hosted
|
||||||
|
outputs:
|
||||||
|
build_lkm: ${{ steps.check-build.outputs.build_lkm }}
|
||||||
|
upload_lkm: ${{ steps.check-build.outputs.upload_lkm }}
|
||||||
|
steps:
|
||||||
|
- name: check build
|
||||||
|
id: check-build
|
||||||
|
run: |
|
||||||
|
if [ "${{ github.event_name }}" == "workflow_dispatch" ] && [ "${{ inputs.build_lkm }}" != "auto" ]; then
|
||||||
|
kernel_changed="${{ inputs.build_lkm }}"
|
||||||
|
else
|
||||||
|
kernel_changed=true
|
||||||
|
mkdir tmp
|
||||||
|
cd tmp
|
||||||
|
git config --global init.defaultBranch bot
|
||||||
|
git config --global user.name 'Bot'
|
||||||
|
git config --global user.email 'bot@github.shirkneko.io'
|
||||||
|
git init .
|
||||||
|
git remote add origin https://github.com/${{ github.repository }}
|
||||||
|
CURRENT_COMMIT="${{ github.event.head_commit.id }}"
|
||||||
|
git fetch origin $CURRENT_COMMIT --depth=1
|
||||||
|
git fetch origin lkm --depth=1
|
||||||
|
LKM_COMMIT="$(git log --format=%B -n 1 origin/lkm | head -n 1)"
|
||||||
|
LKM_COMMIT="${LKM_COMMIT#Upload LKM from }"
|
||||||
|
LKM_COMMIT=$(echo "$LKM_COMMIT" | tr -d '[:space:]')
|
||||||
|
echo "LKM_COMMIT=$LKM_COMMIT"
|
||||||
|
git fetch origin "$LKM_COMMIT" --depth=1
|
||||||
|
git diff --quiet "$LKM_COMMIT" "$CURRENT_COMMIT" -- kernel :!kernel/setup.sh .github/workflows/build-lkm-local.yml .github/workflows/build-kernel-*.yml && kernel_changed=false
|
||||||
|
cd ..
|
||||||
|
rm -rf tmp
|
||||||
|
fi
|
||||||
|
if [ "${{ github.event_name }}" == "push" ] && [ "${{ github.ref }}" == 'refs/heads/main' ]; then
|
||||||
|
need_upload=true
|
||||||
|
elif [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
|
||||||
|
need_upload="${{ inputs.upload_lkm }}"
|
||||||
|
else
|
||||||
|
need_upload=false
|
||||||
|
fi
|
||||||
|
echo "kernel changed: $kernel_changed"
|
||||||
|
echo "need upload: $need_upload"
|
||||||
|
echo "build_lkm=$kernel_changed" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "upload_lkm=$need_upload" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
build-lkm:
|
||||||
|
needs: check-build-lkm
|
||||||
|
uses: ./.github/workflows/build-lkm-local.yml
|
||||||
|
if: ${{ needs.check-build-lkm.outputs.build_lkm == 'true' }}
|
||||||
|
with:
|
||||||
|
upload: ${{ needs.check-build-lkm.outputs.upload_lkm == 'true' }}
|
||||||
|
secrets: inherit
|
||||||
|
build-susfs:
|
||||||
|
if: ${{ always() }}
|
||||||
|
needs: [ check-build-lkm, build-lkm ]
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- target: aarch64-linux-android
|
||||||
|
os: ubuntu-latest
|
||||||
|
uses: ./.github/workflows/susfs.yml
|
||||||
|
with:
|
||||||
|
target: ${{ matrix.target }}
|
||||||
|
os: ${{ matrix.os }}
|
||||||
|
|
||||||
|
build-kpmmgr:
|
||||||
|
if: ${{ always() }}
|
||||||
|
needs: [ check-build-lkm, build-lkm ]
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- target: aarch64-linux-android
|
||||||
|
os: ubuntu-latest
|
||||||
|
uses: ./.github/workflows/kpmmgr.yml
|
||||||
|
with:
|
||||||
|
target: ${{ matrix.target }}
|
||||||
|
os: ${{ matrix.os }}
|
||||||
|
|
||||||
|
build-ksud:
|
||||||
|
if: ${{ always() }}
|
||||||
|
needs: [ check-build-lkm, build-lkm ]
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- target: aarch64-linux-android
|
||||||
|
os: ubuntu-latest
|
||||||
|
- target: x86_64-linux-android
|
||||||
|
os: ubuntu-latest
|
||||||
|
- target: armv7-linux-androideabi
|
||||||
|
os: ubuntu-latest
|
||||||
|
uses: ./.github/workflows/ksud.yml
|
||||||
|
with:
|
||||||
|
target: ${{ matrix.target }}
|
||||||
|
os: ${{ matrix.os }}
|
||||||
|
pack_lkm: true
|
||||||
|
pull_lkm: ${{ needs.check-build-lkm.outputs.build_lkm != 'true' }}
|
||||||
|
|
||||||
|
build-manager:
|
||||||
|
if: ${{ always() }}
|
||||||
|
needs: build-ksud
|
||||||
|
runs-on: self-hosted
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: ./manager
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Setup need_upload
|
||||||
|
id: need_upload
|
||||||
|
run: |
|
||||||
|
if [ ! -z "${{ secrets.BOT_TOKEN }}" ]; then
|
||||||
|
echo "UPLOAD=true" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "UPLOAD=false" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Write key
|
||||||
|
if: ${{ ( github.event_name != 'pull_request' && github.ref == 'refs/heads/main' ) || github.ref == 'refs/heads/susfs' || github.ref_type == 'tag' }}
|
||||||
|
run: |
|
||||||
|
if [ ! -z "${{ secrets.KEYSTORE }}" ]; then
|
||||||
|
{
|
||||||
|
echo KEYSTORE_PASSWORD='${{ secrets.KEYSTORE_PASSWORD }}'
|
||||||
|
echo KEY_ALIAS='${{ secrets.KEY_ALIAS }}'
|
||||||
|
echo KEY_PASSWORD='${{ secrets.KEY_PASSWORD }}'
|
||||||
|
echo KEYSTORE_FILE='key.jks'
|
||||||
|
} >> gradle.properties
|
||||||
|
echo "${{ secrets.KEYSTORE }}" | base64 -d > key.jks
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Download arm64 susfs
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: susfs-aarch64-linux-android
|
||||||
|
path: .
|
||||||
|
|
||||||
|
- name: Download arm64 kpmmgr
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: kpmmgr-aarch64-linux-android
|
||||||
|
path: .
|
||||||
|
|
||||||
|
- name: Download arm64 ksud
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: ksud-aarch64-linux-android
|
||||||
|
path: .
|
||||||
|
|
||||||
|
- name: Download x86_64 ksud
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: ksud-x86_64-linux-android
|
||||||
|
path: .
|
||||||
|
|
||||||
|
- name: Download arm ksud
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: ksud-armv7-linux-androideabi
|
||||||
|
path: .
|
||||||
|
|
||||||
|
- name: Copy ksud to app jniLibs
|
||||||
|
run: |
|
||||||
|
mkdir -p app/src/main/jniLibs/arm64-v8a
|
||||||
|
mkdir -p app/src/main/jniLibs/x86_64
|
||||||
|
mkdir -p app/src/main/jniLibs/armeabi-v7a
|
||||||
|
cp -f ../aarch64-linux-android/release/zakozako ../manager/app/src/main/jniLibs/arm64-v8a/libzakozako.so
|
||||||
|
cp -f ../x86_64-linux-android/release/zakozako ../manager/app/src/main/jniLibs/x86_64/libzakozako.so
|
||||||
|
cp -f ../armv7-linux-androideabi/release/zakozako ../manager/app/src/main/jniLibs/armeabi-v7a/libzakozako.so
|
||||||
|
|
||||||
|
- name: Copy kpmmgr to app jniLibs
|
||||||
|
run: |
|
||||||
|
mkdir -p app/src/main/jniLibs/arm64-v8a
|
||||||
|
cp -f ../arm64-v8a/kpmmgr ../manager/app/src/main/jniLibs/arm64-v8a/libkpmmgr.so
|
||||||
|
|
||||||
|
- name: Copy susfs to app jniLibs
|
||||||
|
run: |
|
||||||
|
mkdir -p app/src/main/jniLibs/arm64-v8a
|
||||||
|
cp -f ../arm64-v8a/zakozakozako ../manager/app/src/main/jniLibs/arm64-v8a/libzakozakozako.so
|
||||||
|
|
||||||
|
- name: Build with Gradle
|
||||||
|
run: |
|
||||||
|
export ANDROID_HOME=/root/.android/sdk
|
||||||
|
export PATH=$ANDROID_HOME/platform-tools:$ANDROID_HOME/tools:$ANDROID_HOME/tools/bin:$PATH
|
||||||
|
{
|
||||||
|
echo 'org.gradle.parallel=true'
|
||||||
|
echo 'org.gradle.vfs.watch=true'
|
||||||
|
echo 'org.gradle.jvmargs=-Xmx2048m'
|
||||||
|
echo 'android.native.buildOutput=verbose'
|
||||||
|
} >> gradle.properties
|
||||||
|
sed -i 's/org.gradle.configuration-cache=true//g' gradle.properties
|
||||||
|
./gradlew clean assembleRelease
|
||||||
|
|
||||||
|
- name: Upload build artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
if: ${{ ( github.event_name != 'pull_request' && github.ref == 'refs/heads/main' ) || github.ref_type == 'tag' }}
|
||||||
|
with:
|
||||||
|
name: manager
|
||||||
|
path: manager/app/build/outputs/apk/release/*.apk
|
||||||
|
|
||||||
|
- name: Upload mappings
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
if: ${{ ( github.event_name != 'pull_request' && github.ref == 'refs/heads/main' ) || github.ref_type == 'tag' }}
|
||||||
|
with:
|
||||||
|
name: "mappings"
|
||||||
|
path: "manager/app/build/outputs/mapping/release/"
|
||||||
|
|
||||||
|
- name: Bot session cache
|
||||||
|
if: github.event_name != 'pull_request' && steps.need_upload.outputs.UPLOAD == 'true'
|
||||||
|
id: bot_session_cache
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: scripts/ksubot.session
|
||||||
|
key: ${{ runner.os }}-bot-session
|
||||||
|
|
||||||
|
- name: Upload to telegram
|
||||||
|
if: github.event_name != 'pull_request' && steps.need_upload.outputs.UPLOAD == 'true'
|
||||||
|
env:
|
||||||
|
CHAT_ID: ${{ vars.CHAT_ID }}
|
||||||
|
BOT_TOKEN: ${{ secrets.BOT_TOKEN }}
|
||||||
|
MESSAGE_THREAD_ID: ${{ vars.MESSAGE_THREAD_ID }}
|
||||||
|
COMMIT_MESSAGE: ${{ github.event.head_commit.message }}
|
||||||
|
COMMIT_URL: ${{ github.event.head_commit.url }}
|
||||||
|
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||||
|
TITLE: Manager
|
||||||
|
run: |
|
||||||
|
if [ ! -z "${{ secrets.BOT_TOKEN }}" ]; then
|
||||||
|
export VERSION=$(git rev-list --count HEAD)
|
||||||
|
APK=$(find ./app/build/outputs/apk/release -name "*.apk")
|
||||||
|
python3 $GITHUB_WORKSPACE/scripts/ksubot.py $APK
|
||||||
|
fi
|
||||||
10
.github/workflows/build-manager.yml
vendored
@@ -119,6 +119,8 @@ jobs:
|
|||||||
os: ubuntu-latest
|
os: ubuntu-latest
|
||||||
- target: x86_64-linux-android
|
- target: x86_64-linux-android
|
||||||
os: ubuntu-latest
|
os: ubuntu-latest
|
||||||
|
- target: armv7-linux-androideabi
|
||||||
|
os: ubuntu-latest
|
||||||
uses: ./.github/workflows/ksud.yml
|
uses: ./.github/workflows/ksud.yml
|
||||||
with:
|
with:
|
||||||
target: ${{ matrix.target }}
|
target: ${{ matrix.target }}
|
||||||
@@ -198,12 +200,20 @@ jobs:
|
|||||||
name: ksud-x86_64-linux-android
|
name: ksud-x86_64-linux-android
|
||||||
path: .
|
path: .
|
||||||
|
|
||||||
|
- name: Download arm ksud
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: ksud-armv7-linux-androideabi
|
||||||
|
path: .
|
||||||
|
|
||||||
- name: Copy ksud to app jniLibs
|
- name: Copy ksud to app jniLibs
|
||||||
run: |
|
run: |
|
||||||
mkdir -p app/src/main/jniLibs/arm64-v8a
|
mkdir -p app/src/main/jniLibs/arm64-v8a
|
||||||
mkdir -p app/src/main/jniLibs/x86_64
|
mkdir -p app/src/main/jniLibs/x86_64
|
||||||
|
mkdir -p app/src/main/jniLibs/armeabi-v7a
|
||||||
cp -f ../aarch64-linux-android/release/zakozako ../manager/app/src/main/jniLibs/arm64-v8a/libzakozako.so
|
cp -f ../aarch64-linux-android/release/zakozako ../manager/app/src/main/jniLibs/arm64-v8a/libzakozako.so
|
||||||
cp -f ../x86_64-linux-android/release/zakozako ../manager/app/src/main/jniLibs/x86_64/libzakozako.so
|
cp -f ../x86_64-linux-android/release/zakozako ../manager/app/src/main/jniLibs/x86_64/libzakozako.so
|
||||||
|
cp -f ../armv7-linux-androideabi/release/zakozako ../manager/app/src/main/jniLibs/armeabi-v7a/libzakozako.so
|
||||||
|
|
||||||
- name: Copy kpmmgr to app jniLibs
|
- name: Copy kpmmgr to app jniLibs
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
38
.github/workflows/crowdin.yml
vendored
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
name: Crowdin Action
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ main ]
|
||||||
|
paths:
|
||||||
|
- 'manager/app/src/main/res/**'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
synchronize-with-crowdin:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: crowdin action
|
||||||
|
uses: crowdin/github-action@master
|
||||||
|
with:
|
||||||
|
upload_sources: true
|
||||||
|
upload_translations: true
|
||||||
|
download_translations: true
|
||||||
|
localization_branch_name: "Crowdin"
|
||||||
|
crowdin_branch_name: "main"
|
||||||
|
create_pull_request: true
|
||||||
|
pull_request_title: 'New Crowdin Translations'
|
||||||
|
pull_request_body: 'New Crowdin translations by [Crowdin GH Action](https://github.com/crowdin/github-action)'
|
||||||
|
pull_request_base_branch_name: 'main'
|
||||||
|
skip_untranslated_files: true
|
||||||
|
env:
|
||||||
|
# A classic GitHub Personal Access Token with the 'repo' scope selected (the user should have write access to the repository).
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||||
|
|
||||||
|
# A numeric ID, found at https://crowdin.com/project/<projectName>/tools/api
|
||||||
|
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
|
||||||
|
|
||||||
|
# Visit https://crowdin.com/settings#api-key to create this token
|
||||||
|
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
|
||||||
252
.github/workflows/gki-kernel-local.yml
vendored
Normal file
@@ -0,0 +1,252 @@
|
|||||||
|
name: GKI Kernel Build Local
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
inputs:
|
||||||
|
version:
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
description: >
|
||||||
|
Output directory of gki,
|
||||||
|
for example: android12-5.10
|
||||||
|
version_name:
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
description: >
|
||||||
|
With SUBLEVEL of kernel,
|
||||||
|
for example: android12-5.10.66
|
||||||
|
tag:
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
description: >
|
||||||
|
Part of branch name of common kernel manifest,
|
||||||
|
for example: android12-5.10-2021-11
|
||||||
|
os_patch_level:
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
description: >
|
||||||
|
Patch level of common kernel manifest,
|
||||||
|
for example: 2021-11
|
||||||
|
default: 2022-05
|
||||||
|
patch_path:
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
description: >
|
||||||
|
Directory name of .github/patches/<patch_path>
|
||||||
|
for example: 5.10
|
||||||
|
use_cache:
|
||||||
|
required: false
|
||||||
|
type: boolean
|
||||||
|
default: true
|
||||||
|
embed_ksud:
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
default: ksud-aarch64-linux-android
|
||||||
|
description: >
|
||||||
|
Artifact name of prebuilt ksud to be embedded
|
||||||
|
for example: ksud-aarch64-linux-android
|
||||||
|
debug:
|
||||||
|
required: false
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
build_lkm:
|
||||||
|
required: false
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
secrets:
|
||||||
|
BOOT_SIGN_KEY:
|
||||||
|
required: false
|
||||||
|
CHAT_ID:
|
||||||
|
required: false
|
||||||
|
BOT_TOKEN:
|
||||||
|
required: false
|
||||||
|
MESSAGE_THREAD_ID:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build ${{ inputs.version_name }}
|
||||||
|
runs-on: self-hosted
|
||||||
|
env:
|
||||||
|
CCACHE_COMPILERCHECK: "%compiler% -dumpmachine; %compiler% -dumpversion"
|
||||||
|
CCACHE_NOHASHDIR: "true"
|
||||||
|
CCACHE_HARDLINK: "true"
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
path: KernelSU
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Setup need_upload
|
||||||
|
id: need_upload
|
||||||
|
run: |
|
||||||
|
if [ ! -z "${{ secrets.BOT_TOKEN }}" ]; then
|
||||||
|
echo "UPLOAD=true" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "UPLOAD=false" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Setup kernel source
|
||||||
|
run: |
|
||||||
|
echo "Free space:"
|
||||||
|
df -h
|
||||||
|
cd $GITHUB_WORKSPACE
|
||||||
|
sudo apt-get install repo -y
|
||||||
|
export REPO_URL='https://mirrors.tuna.tsinghua.edu.cn/git/git-repo'
|
||||||
|
mkdir android-kernel && cd android-kernel
|
||||||
|
repo init --depth=1 --u https://mirrors.tuna.tsinghua.edu.cn/git/AOSP/kernel/manifest -b common-${{ inputs.tag }} --repo-rev=v2.35
|
||||||
|
REMOTE_BRANCH=$(git ls-remote https://mirrors.tuna.tsinghua.edu.cn/git/AOSP/kernel/common ${{ inputs.tag }})
|
||||||
|
DEFAULT_MANIFEST_PATH=.repo/manifests/default.xml
|
||||||
|
if grep -q deprecated <<< $REMOTE_BRANCH; then
|
||||||
|
echo "Found deprecated branch: ${{ inputs.tag }}"
|
||||||
|
sed -i 's/"${{ inputs.tag }}"/"deprecated\/${{ inputs.tag }}"/g' $DEFAULT_MANIFEST_PATH
|
||||||
|
cat $DEFAULT_MANIFEST_PATH
|
||||||
|
fi
|
||||||
|
repo --version
|
||||||
|
repo --trace sync -c -j$(nproc --all) --no-tags
|
||||||
|
df -h
|
||||||
|
|
||||||
|
- name: Setup KernelSU
|
||||||
|
env:
|
||||||
|
PATCH_PATH: ${{ inputs.patch_path }}
|
||||||
|
IS_DEBUG_KERNEL: ${{ inputs.debug }}
|
||||||
|
run: |
|
||||||
|
cd $GITHUB_WORKSPACE/android-kernel
|
||||||
|
echo "[+] KernelSU setup"
|
||||||
|
GKI_ROOT=$(pwd)
|
||||||
|
echo "[+] GKI_ROOT: $GKI_ROOT"
|
||||||
|
echo "[+] Copy KernelSU driver to $GKI_ROOT/common/drivers"
|
||||||
|
ln -sf $GITHUB_WORKSPACE/KernelSU/kernel $GKI_ROOT/common/drivers/kernelsu
|
||||||
|
echo "[+] Add KernelSU driver to Makefile"
|
||||||
|
DRIVER_MAKEFILE=$GKI_ROOT/common/drivers/Makefile
|
||||||
|
DRIVER_KCONFIG=$GKI_ROOT/common/drivers/Kconfig
|
||||||
|
grep -q "kernelsu" "$DRIVER_MAKEFILE" || printf "\nobj-\$(CONFIG_KSU) += kernelsu/\n" >> "$DRIVER_MAKEFILE"
|
||||||
|
grep -q "kernelsu" "$DRIVER_KCONFIG" || sed -i "/endmenu/i\\source \"drivers/kernelsu/Kconfig\"" "$DRIVER_KCONFIG"
|
||||||
|
echo "[+] Apply Compilation Patches"
|
||||||
|
if [ ! -e build/build.sh ]; then
|
||||||
|
GLIBC_VERSION=$(ldd --version 2>/dev/null | head -n 1 | awk '{print $NF}')
|
||||||
|
echo "GLIBC_VERSION: $GLIBC_VERSION"
|
||||||
|
if [ "$(printf '%s\n' "2.38" "$GLIBC_VERSION" | sort -V | head -n1)" = "2.38" ]; then
|
||||||
|
echo "Patching resolve_btfids/Makefile"
|
||||||
|
cd $GKI_ROOT/common/ && sed -i '/\$(Q)\$(MAKE) -C \$(SUBCMD_SRC) OUTPUT=\$(abspath \$(dir \$@))\/ \$(abspath \$@)/s//$(Q)$(MAKE) -C $(SUBCMD_SRC) EXTRA_CFLAGS="$(CFLAGS)" OUTPUT=$(abspath $(dir $@))\/ $(abspath $@)/' tools/bpf/resolve_btfids/Makefile || echo "No patch needed."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$IS_DEBUG_KERNEL" = "true" ]; then
|
||||||
|
echo "[+] Enable debug features for kernel"
|
||||||
|
printf "\nccflags-y += -DCONFIG_KSU_DEBUG\n" >> $GITHUB_WORKSPACE/KernelSU/kernel/Makefile
|
||||||
|
fi
|
||||||
|
repo status
|
||||||
|
echo "[+] KernelSU setup done."
|
||||||
|
|
||||||
|
- name: Symbol magic
|
||||||
|
run: |
|
||||||
|
echo "[+] Export all symbol from abi_gki_aarch64.xml"
|
||||||
|
COMMON_ROOT=$GITHUB_WORKSPACE/android-kernel/common
|
||||||
|
KSU_ROOT=$GITHUB_WORKSPACE/KernelSU
|
||||||
|
ABI_XML=$COMMON_ROOT/android/abi_gki_aarch64.xml
|
||||||
|
SYMBOL_LIST=$COMMON_ROOT/android/abi_gki_aarch64
|
||||||
|
# python3 $KSU_ROOT/scripts/abi_gki_all.py $ABI_XML > $SYMBOL_LIST
|
||||||
|
echo "[+] Add KernelSU symbols"
|
||||||
|
cat $KSU_ROOT/kernel/export_symbol.txt | awk '{sub("[ \t]+","");print " "$0}' >> $SYMBOL_LIST
|
||||||
|
|
||||||
|
- name: Setup ccache
|
||||||
|
if: inputs.use_cache == true
|
||||||
|
uses: hendrikmuhs/ccache-action@v1
|
||||||
|
with:
|
||||||
|
key: gki-kernel-aarch64-${{ inputs.version_name }}
|
||||||
|
max-size: 2G
|
||||||
|
save: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
|
||||||
|
|
||||||
|
- name: Setup for LKM
|
||||||
|
if: ${{ inputs.build_lkm == true }}
|
||||||
|
working-directory: android-kernel
|
||||||
|
run: |
|
||||||
|
pip install ast-grep-cli
|
||||||
|
sudo apt-get install llvm-15 -y
|
||||||
|
ast-grep -U -p '$$$ check_exports($$$) {$$$}' -r '' common/scripts/mod/modpost.c
|
||||||
|
ast-grep -U -p 'check_exports($$$);' -r '' common/scripts/mod/modpost.c
|
||||||
|
sed -i '/config KSU/,/help/{s/default y/default m/}' common/drivers/kernelsu/Kconfig
|
||||||
|
echo "drivers/kernelsu/kernelsu.ko" >> common/android/gki_aarch64_modules
|
||||||
|
|
||||||
|
# bazel build, android14-5.15, android14-6.1 use bazel
|
||||||
|
if [ ! -e build/build.sh ]; then
|
||||||
|
sed -i 's/needs unknown symbol/Dont abort when unknown symbol/g' build/kernel/*.sh || echo "No unknown symbol scripts found"
|
||||||
|
if [ -e common/modules.bzl ]; then
|
||||||
|
sed -i 's/_COMMON_GKI_MODULES_LIST = \[/_COMMON_GKI_MODULES_LIST = \[ "drivers\/kernelsu\/kernelsu.ko",/g' common/modules.bzl
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
TARGET_FILE="build/kernel/build.sh"
|
||||||
|
if [ ! -e "$TARGET_FILE" ]; then
|
||||||
|
TARGET_FILE="build/build.sh"
|
||||||
|
fi
|
||||||
|
sed -i 's/needs unknown symbol/Dont abort when unknown symbol/g' $TARGET_FILE || echo "No unknown symbol in $TARGET_FILE"
|
||||||
|
sed -i 's/if ! diff -u "\${KERNEL_DIR}\/\${MODULES_ORDER}" "\${OUT_DIR}\/modules\.order"; then/if false; then/g' $TARGET_FILE
|
||||||
|
sed -i 's@${ROOT_DIR}/build/abi/compare_to_symbol_list@echo@g' $TARGET_FILE
|
||||||
|
sed -i 's/needs unknown symbol/Dont abort when unknown symbol/g' build/kernel/*.sh || echo "No unknown symbol scripts found"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Make working directory clean to avoid dirty
|
||||||
|
working-directory: android-kernel
|
||||||
|
run: |
|
||||||
|
if [ -e common/BUILD.bazel ]; then
|
||||||
|
sed -i '/^[[:space:]]*"protected_exports_list"[[:space:]]*:[[:space:]]*"android\/abi_gki_protected_exports_aarch64",$/d' common/BUILD.bazel
|
||||||
|
fi
|
||||||
|
rm common/android/abi_gki_protected_exports_* || echo "No protected exports!"
|
||||||
|
git config --global user.email "bot@kernelsu.org"
|
||||||
|
git config --global user.name "KernelSUBot"
|
||||||
|
cd common/ && git add -A && git commit -a -m "Add KernelSU"
|
||||||
|
repo status
|
||||||
|
|
||||||
|
- name: Build Kernel/LKM
|
||||||
|
working-directory: android-kernel
|
||||||
|
run: |
|
||||||
|
if [ ! -z ${{ vars.EXPECTED_SIZE }} ] && [ ! -z ${{ vars.EXPECTED_HASH }} ]; then
|
||||||
|
export KSU_EXPECTED_SIZE=${{ vars.EXPECTED_SIZE }}
|
||||||
|
export KSU_EXPECTED_HASH=${{ vars.EXPECTED_HASH }}
|
||||||
|
fi
|
||||||
|
if [ -e build/build.sh ]; then
|
||||||
|
LTO=thin BUILD_CONFIG=common/build.config.gki.aarch64 build/build.sh CC="/usr/bin/ccache clang"
|
||||||
|
else
|
||||||
|
tools/bazel run --disk_cache=/home/runner/.cache/bazel --config=fast --config=stamp --lto=thin //common:kernel_aarch64_dist -- --dist_dir=dist
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Prepare artifacts
|
||||||
|
id: prepareArtifacts
|
||||||
|
run: |
|
||||||
|
OUTDIR=android-kernel/out/${{ inputs.version }}/dist
|
||||||
|
if [ ! -e $OUTDIR ]; then
|
||||||
|
OUTDIR=android-kernel/dist
|
||||||
|
fi
|
||||||
|
mkdir output
|
||||||
|
if [ "${{ inputs.build_lkm}}" = "true" ]; then
|
||||||
|
llvm-strip-15 -d $OUTDIR/kernelsu.ko
|
||||||
|
mv $OUTDIR/kernelsu.ko ./output/${{ inputs.version }}_kernelsu.ko
|
||||||
|
else
|
||||||
|
cp $OUTDIR/Image ./output/
|
||||||
|
cp $OUTDIR/Image.lz4 ./output/
|
||||||
|
git clone https://github.com/Kernel-SU/AnyKernel3
|
||||||
|
rm -rf ./AnyKernel3/.git
|
||||||
|
cp $OUTDIR/Image ./AnyKernel3/
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Upload Image and Image.gz
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
if: ${{ inputs.build_lkm == false }}
|
||||||
|
with:
|
||||||
|
name: Image-${{ inputs.version_name }}_${{ inputs.os_patch_level }}
|
||||||
|
path: ./output/*
|
||||||
|
|
||||||
|
- name: Upload AnyKernel3
|
||||||
|
if: ${{ inputs.build_lkm == false }}
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: AnyKernel3-${{ inputs.version_name }}_${{ inputs.os_patch_level }}
|
||||||
|
path: ./AnyKernel3/*
|
||||||
|
|
||||||
|
- name: Upload LKM
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
if: ${{ inputs.build_lkm == true }}
|
||||||
|
with:
|
||||||
|
name: ${{ inputs.version }}-lkm
|
||||||
|
path: ./output/*_kernelsu.ko
|
||||||
3
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
|
# SukiSU Ultra
|
||||||
|
|
||||||
**English** | [简体中文](README.md) | [日本語](README-ja.md)
|
**English** | [简体中文](README.md) | [日本語](README-ja.md) | [Türkçe](README-tr.md)
|
||||||
|
|
||||||
Android device root solution based on [KernelSU](https://github.com/tiann/KernelSU)
|
Android device root solution based on [KernelSU](https://github.com/tiann/KernelSU)
|
||||||
|
|
||||||
@@ -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
|
> However, we will be a separately maintained branch of KSU in the future
|
||||||
|
|
||||||
- Fully adapted for non-GKI devices (susfs-dev and unsusfs-patched dev branches only)
|
|
||||||
|
|
||||||
## How to add
|
## How to add
|
||||||
|
|
||||||
Use the susfs-stable or susfs-dev branch (integrated susfs with support for non-GKI devices)
|
Using main branching (non-GKI device builds are not supported) (requires manual integration of susfs)
|
||||||
```
|
```
|
||||||
curl -LSs "https://raw.githubusercontent.com/ShirkNeko/SukiSU-Ultra/main/kernel/setup.sh" | bash -s susfs-dev
|
curl -LSs "https://raw.githubusercontent.com/SukiSU-Ultra/SukiSU-Ultra/main/kernel/setup.sh" | bash -s main
|
||||||
```
|
```
|
||||||
|
|
||||||
Use the main branch
|
Using branches that support non-GKI devices (requires manual integration of susfs)
|
||||||
```
|
```
|
||||||
curl -LSs "https://raw.githubusercontent.com/ShirkNeko/KernelSU/main/kernel/setup.sh" | bash -s main
|
curl -LSs "https://raw.githubusercontent.com/SukiSU-Ultra/SukiSU-Ultra/main/kernel/setup.sh" | bash -s nongki
|
||||||
```
|
```
|
||||||
|
|
||||||
## How to use integrated susfs
|
## How to use integrated susfs
|
||||||
|
|
||||||
1. Use the susfs-dev branch directly without any patching
|
1. Use the susfs-dev branch directly without any patching (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.
|
- 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
|
## More links
|
||||||
|
|
||||||
|
**If you need to submit a translation for the manager go to** https://crowdin.com/project/SukiSU-Ultra
|
||||||
|
|
||||||
Projects compiled based on Sukisu and susfs
|
Projects compiled based on Sukisu and susfs
|
||||||
- [GKI](https://github.com/ShirkNeko/GKI_KernelSU_SUSFS)
|
- [GKI](https://github.com/ShirkNeko/GKI_KernelSU_SUSFS)
|
||||||
- [OnePlus](https://github.com/ShirkNeko/Action_OnePlus_MKSU_SUSFS)
|
- [OnePlus](https://github.com/ShirkNeko/Action_OnePlus_MKSU_SUSFS)
|
||||||
@@ -59,11 +82,14 @@ Projects compiled based on Sukisu and susfs
|
|||||||
|
|
||||||
## Usage
|
## 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
|
### OnePlus
|
||||||
@@ -84,10 +110,17 @@ https://kernelsu.org/guide/installation.html
|
|||||||
5. More customization
|
5. More customization
|
||||||
6. Support for KPM kernel modules
|
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
|
## License
|
||||||
|
|
||||||
- The file in the “kernel” directory is under [GPL-2.0-only](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html) license.
|
- The file in the “kernel” directory is under [GPL-2.0-only](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html) license.
|
||||||
- All other parts except the “kernel” directory are under [GPL-3.0 or later](https://www.gnu.org/licenses/gpl-3.0.html) license.
|
|
||||||
|
- The images of the files `ic_launcher(?!.*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
|
## Sponsorship list
|
||||||
|
|
||||||
@@ -96,8 +129,8 @@ https://kernelsu.org/guide/installation.html
|
|||||||
- [wswzgdg](https://github.com/wswzgdg) Many thanks for supporting this project
|
- [wswzgdg](https://github.com/wswzgdg) Many thanks for supporting this project
|
||||||
- [yspbwx2010](https://github.com/yspbwx2010) Many thanks
|
- [yspbwx2010](https://github.com/yspbwx2010) Many thanks
|
||||||
- [DARKWWEE](https://github.com/DARKWWEE) Thanks for the 100 USDT Lao
|
- [DARKWWEE](https://github.com/DARKWWEE) Thanks for the 100 USDT Lao
|
||||||
|
- [Saksham Singla](https://github.com/TypeFlu) Website provision as well as maintenance
|
||||||
If the above list does not have your name, I will update it as soon as possible, and thanks again for your support!
|
- [OukaroMF](https://github.com/OukaroMF) Donation of website domain name
|
||||||
|
|
||||||
## Contributions
|
## Contributions
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# SukiSU Ultra
|
# SukiSU Ultra
|
||||||
|
|
||||||
**日本語** | [简体中文](README.md) | [English](README-en.md)
|
**日本語** | [简体中文](README.md) | [English](README-en.md) | [Türkçe](README-tr.md)
|
||||||
|
|
||||||
[KernelSU](https://github.com/tiann/KernelSU) をベースとした Android デバイスの root ソリューション
|
[KernelSU](https://github.com/tiann/KernelSU) をベースとした Android デバイスの root ソリューション
|
||||||
|
|
||||||
@@ -11,22 +11,24 @@
|
|||||||
>
|
>
|
||||||
> ただし、将来的には KSU とは別に管理されるブランチとなる予定です。
|
> ただし、将来的には KSU とは別に管理されるブランチとなる予定です。
|
||||||
|
|
||||||
- GKI 非対応なデバイスに完全に適応 (susfs-dev と unsusfs-patched dev ブランチのみ)
|
|
||||||
|
|
||||||
## 追加方法
|
## 追加方法
|
||||||
|
|
||||||
susfs-stable または susfs-dev ブランチ (GKI 非対応デバイスに対応する統合された susfs) 使用してください。
|
メイン分岐の使用(GKI デバイス以外のビルドはサポートされていません。) (手動によるサスフ統合が必要)
|
||||||
```
|
```
|
||||||
curl -LSs "https://raw.githubusercontent.com/ShirkNeko/SukiSU-Ultra/main/kernel/setup.sh" | bash -s susfs-dev
|
curl -LSs "https://raw.githubusercontent.com/SukiSU-Ultra/SukiSU-Ultra/main/kernel/setup.sh" | bash -s main
|
||||||
```
|
```
|
||||||
|
|
||||||
メインブランチを使用する場合
|
GKI以外のデバイスをサポートするブランチを使用する (手動によるサスフ統合が必要)
|
||||||
```
|
```
|
||||||
curl -LSs "https://raw.githubusercontent.com/ShirkNeko/KernelSU/main/kernel/setup.sh" | bash -s main
|
curl -LSs "https://raw.githubusercontent.com/SukiSU-Ultra/SukiSU-Ultra/main/kernel/setup.sh" | bash -s nongki
|
||||||
```
|
```
|
||||||
|
|
||||||
## 統合された susfs の使い方
|
## 統合された susfs の使い方
|
||||||
|
|
||||||
1. パッチを当てずに susfs-dev ブランチを直接使用してください。
|
1. パッチを当てずに susfs-dev ブランチを直接使用してください (非GKIデバイスビルドをサポート)
|
||||||
|
```
|
||||||
|
curl -LSs "https://raw.githubusercontent.com/SukiSU-Ultra/SukiSU-Ultra/main/kernel/setup.sh" | bash -s susfs-dev
|
||||||
|
```
|
||||||
|
|
||||||
## KPM に対応
|
## KPM に対応
|
||||||
|
|
||||||
@@ -37,9 +39,28 @@ curl -LSs "https://raw.githubusercontent.com/ShirkNeko/KernelSU/main/kernel/setu
|
|||||||
|
|
||||||
KPM テンプレートのアドレス: https://github.com/udochina/KPM-Build-Anywhere
|
KPM テンプレートのアドレス: https://github.com/udochina/KPM-Build-Anywhere
|
||||||
|
|
||||||
|
> [!Note]
|
||||||
|
> 1. `CONFIG_KPM=y` が必要である。
|
||||||
|
> 2.非 GKI デバイスには `CONFIG_KALLSYMS=y` と `CONFIG_KALLSYMS_ALL=y` も必要です。
|
||||||
|
> 3.いくつかのカーネル `4.19` およびそれ以降のソースコードでは、 `4.19` からバックポートされた `set_memory.h` ヘッダーファイルも必要です。
|
||||||
|
|
||||||
|
|
||||||
|
## ROOT を保持するシステムアップデートの方法
|
||||||
|
- OTAの後、最初に再起動せず、マネージャのフラッシュ/パッチカーネルインターフェイスに移動し、`GKI/non_GKI 取り付け`を見つけ、フラッシュする必要があるAnykernel3カーネルzipファイルを選択し、フラッシュするためにシステムの現在の実行スロットと反対のスロットを選択し、GKIモードアップデートを保持するために再起動します(この方法は、現時点ではすべてのnon_GKIデバイスでサポートされていませんので、各自でお試しください。 (この方法は、すべての非GKIデバイスでサポートされていませんので、ご自身でお試しください)。
|
||||||
|
- または、LKMモードを使用して未使用のスロットにインストールします(OTA後)。
|
||||||
|
|
||||||
|
## 互換性ステータス
|
||||||
|
- KernelSU(v1.0.0より前のバージョン)はAndroid GKI 2.0デバイス(カーネル5.10以上)を公式にサポートしています。
|
||||||
|
|
||||||
|
- 古いカーネル(4.4+)も互換性がありますが、カーネルは手動でビルドする必要があります。
|
||||||
|
|
||||||
|
- KernelSU は追加のリバースポートを通じて 3.x カーネル (3.4-3.18) をサポートしています。
|
||||||
|
|
||||||
|
- 現在は `arm64-v8a`、`armeabi-v7a (bare)`、いくつかの `X86_64` をサポートしています。
|
||||||
|
|
||||||
## その他のリンク
|
## その他のリンク
|
||||||
|
|
||||||
SukiSU と susfs をベースにコンパイルされたプロジェクトです。
|
**監督に翻訳を提出する必要がある場合は、https://crowdin.com/project/SukiSU-Ultra。
|
||||||
|
|
||||||
- [GKI](https://github.com/ShirkNeko/GKI_KernelSU_SUSFS)
|
- [GKI](https://github.com/ShirkNeko/GKI_KernelSU_SUSFS)
|
||||||
- [OnePlus](https://github.com/ShirkNeko/Action_OnePlus_MKSU_SUSFS)
|
- [OnePlus](https://github.com/ShirkNeko/Action_OnePlus_MKSU_SUSFS)
|
||||||
@@ -61,12 +82,14 @@ SukiSU と susfs をベースにコンパイルされたプロジェクトです
|
|||||||
|
|
||||||
## 使い方
|
## 使い方
|
||||||
|
|
||||||
### GKI
|
### ユニバーサルGKI
|
||||||
|
|
||||||
このガイドに従ってください。
|
https://kernelsu.org/zh_CN/guide/installation.html をご参照ください。
|
||||||
|
|
||||||
https://kernelsu.org/ja_JP/guide/installation.html
|
|
||||||
|
|
||||||
|
> [!Note]
|
||||||
|
> 1.Xiaomi、Redmi、Samsung などの GKI 2.0 を搭載したデバイス用 (Meizu、Yiga、Zenith、oppo などのマジックカーネルを搭載したメーカーは除く)。
|
||||||
|
> 2. [more links](#%E6%9B%B4%E5%A4%9A%E9%93%BE%E6%8E%A5) で GKI ビルドを検索します。 デバイスのカーネルバージョンを検索します。 次に、それをダウンロードし、TWRPまたはカーネルフラッシングツールを使用して、AnyKernel3の接尾辞が付いたzipファイルをフラッシュします。
|
||||||
|
> 接尾辞なしの.zipアーカイブは非圧縮で、接尾辞gzはTenguetモデルで使用されている圧縮方法です。
|
||||||
### OnePlus
|
### OnePlus
|
||||||
|
|
||||||
1. `その他のリンク`の項目に記載されているリンクを開き、デバイス情報を使用してカスタマイズされたカーネルをビルドし、AnyKernel3 の接頭辞を持つ .zip ファイルをフラッシュします。
|
1. `その他のリンク`の項目に記載されているリンクを開き、デバイス情報を使用してカスタマイズされたカーネルをビルドし、AnyKernel3 の接頭辞を持つ .zip ファイルをフラッシュします。
|
||||||
@@ -85,10 +108,15 @@ https://kernelsu.org/ja_JP/guide/installation.html
|
|||||||
5. その他のカスタマイズ
|
5. その他のカスタマイズ
|
||||||
6. KPM カーネルモジュールに対応
|
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-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)が著作権を所有しており、画像内のブランド知的財産権は[明风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) このプロジェクトを支援していただき、ありがとうございます。
|
- [wswzgdg](https://github.com/wswzgdg) このプロジェクトを支援していただき、ありがとうございます。
|
||||||
- [yspbwx2010](https://github.com/yspbwx2010) どうもありがとう。
|
- [yspbwx2010](https://github.com/yspbwx2010) どうもありがとう。
|
||||||
- [DARKWWEE](https://github.com/DARKWWEE) ラオウ100USDTありがとう!
|
- [DARKWWEE](https://github.com/DARKWWEE) ラオウ100USDTありがとう!
|
||||||
|
- [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
|
# SukiSU Ultra
|
||||||
|
|
||||||
**简体中文** | [English](README-en.md) | [日本語](README-ja.md)
|
**简体中文** | [English](README-en.md) | [日本語](README-ja.md) | [Türkçe](README-tr.md)
|
||||||
|
|
||||||
基于 [KernelSU](https://github.com/tiann/KernelSU) 的安卓设备 root 解决方案
|
基于 [KernelSU](https://github.com/tiann/KernelSU) 的安卓设备 root 解决方案
|
||||||
|
|
||||||
@@ -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
|
## 如何集成 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)
|
- 此部分引用自 [rsuntk 的钩子方法](https://github.com/rsuntk/KernelSU)
|
||||||
|
|
||||||
1. **KPROBES 钩子:**
|
1. **KPROBES 钩子:**
|
||||||
|
|
||||||
- 用于可加载内核模块 (LKM)
|
- 用于可加载内核模块 (LKM)
|
||||||
- GKI 2.0 内核的默认钩子方法
|
- GKI 2.0 内核的默认钩子方法
|
||||||
- 需要 `CONFIG_KPROBES=y`
|
- 需要 `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
|
KPM 模板地址: https://github.com/udochina/KPM-Build-Anywhere
|
||||||
|
|
||||||
|
> [!Note]
|
||||||
|
>
|
||||||
|
> 1. 需要 `CONFIG_KPM=y`
|
||||||
|
> 2. 非 GKI 设备还需要 `CONFIG_KALLSYMS=y` 和 `CONFIG_KALLSYMS_ALL=y`
|
||||||
|
> 3. 部分内核 `4.19` 以下源码还需要从 `4.19` 向后移植头文件 `set_memory.h`
|
||||||
|
|
||||||
|
## 如何进行系统更新保留 ROOT
|
||||||
|
|
||||||
|
- OTA 后先不要重启,进入管理器刷写/修补内核界面,找到 `GKI/non_GKI安装` 选择需要刷写的 Anykernel3 内核压缩文件,选择与现在系统运行槽位相反的槽位进行刷写并重启即可保留 GKI 模式更新(暂不支持所有非 GKI 设备使用这种方法,请自行尝试。非 GKI 设备使用 TWRP 刷写是最稳妥的)
|
||||||
|
- 或者使用 LKM 模式的安装到未使用的槽位(OTA 后)
|
||||||
|
|
||||||
|
## 兼容状态
|
||||||
|
|
||||||
|
- KernelSU(v1.0.0 之前版本)正式支持 Android GKI 2.0 设备(内核 5.10+)
|
||||||
|
|
||||||
|
- 旧内核(4.4+)也兼容,但必须手动构建内核
|
||||||
|
|
||||||
|
- 通过更多的反向移植,KernelSU 可以支持 3.x 内核(3.4-3.18)
|
||||||
|
|
||||||
|
- 目前支持 `arm64-v8a` ,`armeabi-v7a (bare)` 和部分 `X86_64`
|
||||||
|
|
||||||
## 更多链接
|
## 更多链接
|
||||||
|
|
||||||
|
**如果你需要为管理器提交翻译请前往** https://crowdin.com/project/SukiSU-Ultra
|
||||||
|
|
||||||
基于 SukiSU 和 susfs 编译的项目
|
基于 SukiSU 和 susfs 编译的项目
|
||||||
|
|
||||||
- [GKI](https://github.com/ShirkNeko/GKI_KernelSU_SUSFS)
|
- [GKI](https://github.com/ShirkNeko/GKI_KernelSU_SUSFS)
|
||||||
- [一加](https://github.com/ShirkNeko/Action_OnePlus_MKSU_SUSFS)
|
- [一加](https://github.com/ShirkNeko/Action_OnePlus_MKSU_SUSFS)
|
||||||
|
|
||||||
@@ -65,16 +96,17 @@ KPM 模板地址: https://github.com/udochina/KPM-Build-Anywhere
|
|||||||
请**全部**参考 https://kernelsu.org/zh_CN/guide/installation.html
|
请**全部**参考 https://kernelsu.org/zh_CN/guide/installation.html
|
||||||
|
|
||||||
> [!Note]
|
> [!Note]
|
||||||
|
>
|
||||||
> 1. 适用于如小米、红米、三星等的 GKI 2.0 的设备 (不包含魔改内核的厂商如魅族、一加、真我和 oppo)
|
> 1. 适用于如小米、红米、三星等的 GKI 2.0 的设备 (不包含魔改内核的厂商如魅族、一加、真我和 oppo)
|
||||||
> 2. 找到[更多链接](#%E6%9B%B4%E5%A4%9A%E9%93%BE%E6%8E%A5)里的 GKI 构建的项目。找到设备内核版本。然后下载下来,用 TWRP 或者内核刷写工具刷入带 AnyKernel3 后缀的压缩包即可
|
> 2. 找到[更多链接](#%E6%9B%B4%E5%A4%9A%E9%93%BE%E6%8E%A5)里的 GKI 构建的项目。找到设备内核版本。然后下载下来,用 TWRP 或者内核刷写工具刷入带 AnyKernel3 后缀的压缩包即可
|
||||||
> 3. 一般不带后缀的 .zip 压缩包是未压缩的,gz 后缀的为天玑机型所使用的压缩方式
|
> 3. 一般不带后缀的 .zip 压缩包是未压缩的,gz 后缀的为天玑机型所使用的压缩方式
|
||||||
|
|
||||||
|
|
||||||
### 一加
|
### 一加
|
||||||
|
|
||||||
1.找到更多链接里的一加项目进行自行填写,然后云编译构建,最后刷入带 AnyKernel3 后缀的压缩包即可
|
1.找到更多链接里的一加项目进行自行填写,然后云编译构建,最后刷入带 AnyKernel3 后缀的压缩包即可
|
||||||
|
|
||||||
> [!Note]
|
> [!Note]
|
||||||
|
>
|
||||||
> - 内核版本只需要填写前两位即可,如 5.10,5.15,6.1,6.6
|
> - 内核版本只需要填写前两位即可,如 5.10,5.15,6.1,6.6
|
||||||
> - 处理器代号请自行搜索,一般为全英文不带数字的代号
|
> - 处理器代号请自行搜索,一般为全英文不带数字的代号
|
||||||
> - 分支和配置文件请自行到一加内核开源地址进行填写
|
> - 分支和配置文件请自行到一加内核开源地址进行填写
|
||||||
@@ -88,10 +120,18 @@ KPM 模板地址: https://github.com/udochina/KPM-Build-Anywhere
|
|||||||
5. 更多自定义功能
|
5. 更多自定义功能
|
||||||
6. 对 KPM 内核模块的支持
|
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-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) 非常感谢对此项目的支持
|
- [wswzgdg](https://github.com/wswzgdg) 非常感谢对此项目的支持
|
||||||
- [yspbwx2010](https://github.com/yspbwx2010) 非常感谢
|
- [yspbwx2010](https://github.com/yspbwx2010) 非常感谢
|
||||||
- [DARKWWEE](https://github.com/DARKWWEE) 感谢老哥的 100 USDT
|
- [DARKWWEE](https://github.com/DARKWWEE) 感谢老哥的 100 USDT
|
||||||
|
- [Saksham Singla](https://github.com/TypeFlu) 网站的提供以及维护
|
||||||
如果以上名单没有你的名称,我会及时更新,再次感谢大家的支持
|
- [OukaroMF](https://github.com/OukaroMF) 网站域名捐赠
|
||||||
|
|
||||||
## 贡献
|
## 贡献
|
||||||
|
|
||||||
|
|||||||
@@ -16,20 +16,13 @@ config KSU_DEBUG
|
|||||||
help
|
help
|
||||||
Enable KernelSU debug mode.
|
Enable KernelSU debug mode.
|
||||||
|
|
||||||
config KSU_HOOK
|
|
||||||
bool "Enable KernelSU Hook"
|
|
||||||
default n
|
|
||||||
help
|
|
||||||
This option enables the KernelSU Hook feature. If enabled, it will
|
|
||||||
override the kernel version check and enable the hook functionality.
|
|
||||||
|
|
||||||
config KPM
|
config KPM
|
||||||
bool "Enable SukiSU KPM"
|
bool "Enable SukiSU KPM"
|
||||||
|
depends on KSU && 64BIT
|
||||||
default n
|
default n
|
||||||
help
|
help
|
||||||
Enabling this option will activate the KPM feature of SukiSU.
|
Enabling this option will activate the KPM feature of SukiSU.
|
||||||
This option is suitable for scenarios where you need to force KPM to be enabled.
|
This option is suitable for scenarios where you need to force KPM to be enabled.
|
||||||
but it may affect system stability.
|
but it may affect system stability.
|
||||||
|
|
||||||
|
|
||||||
endmenu
|
endmenu
|
||||||
|
|||||||
@@ -226,6 +226,7 @@ int ksu_handle_rename(struct dentry *old_dentry, struct dentry *new_dentry)
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef CONFIG_EXT4_FS
|
||||||
static void nuke_ext4_sysfs() {
|
static void nuke_ext4_sysfs() {
|
||||||
struct path path;
|
struct path path;
|
||||||
int err = kern_path("/data/adb/modules", 0, &path);
|
int err = kern_path("/data/adb/modules", 0, &path);
|
||||||
@@ -243,6 +244,9 @@ static void nuke_ext4_sysfs() {
|
|||||||
|
|
||||||
ext4_unregister_sysfs(sb);
|
ext4_unregister_sysfs(sb);
|
||||||
}
|
}
|
||||||
|
#else
|
||||||
|
static inline void nuke_ext4_sysfs() { }
|
||||||
|
#endif
|
||||||
|
|
||||||
int ksu_handle_prctl(int option, unsigned long arg2, unsigned long arg3,
|
int ksu_handle_prctl(int option, unsigned long arg2, unsigned long arg3,
|
||||||
unsigned long arg4, unsigned long arg5)
|
unsigned long arg4, unsigned long arg5)
|
||||||
@@ -425,6 +429,13 @@ int ksu_handle_prctl(int option, unsigned long arg2, unsigned long arg3,
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
if (arg2 == CMD_ENABLE_KPM) {
|
||||||
|
bool KPM_Enabled = IS_ENABLED(CONFIG_KPM);
|
||||||
|
if (copy_to_user((void __user *)arg3, &KPM_Enabled, sizeof(KPM_Enabled)))
|
||||||
|
pr_info("KPM: copy_to_user() failed\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
// all other cmds are for 'root manager'
|
// all other cmds are for 'root manager'
|
||||||
if (!from_manager) {
|
if (!from_manager) {
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
@@ -167,7 +167,11 @@ DYNAMIC_STRUCT_BEGIN(task_struct)
|
|||||||
DEFINE_MEMBER(task_struct, group_leader)
|
DEFINE_MEMBER(task_struct, group_leader)
|
||||||
DEFINE_MEMBER(task_struct, mm)
|
DEFINE_MEMBER(task_struct, mm)
|
||||||
DEFINE_MEMBER(task_struct, active_mm)
|
DEFINE_MEMBER(task_struct, active_mm)
|
||||||
|
#if LINUX_VERSION_CODE < KERNEL_VERSION(4, 19, 0)
|
||||||
|
DEFINE_MEMBER(task_struct, pids[PIDTYPE_PID].pid)
|
||||||
|
#else
|
||||||
DEFINE_MEMBER(task_struct, thread_pid)
|
DEFINE_MEMBER(task_struct, thread_pid)
|
||||||
|
#endif
|
||||||
DEFINE_MEMBER(task_struct, files)
|
DEFINE_MEMBER(task_struct, files)
|
||||||
DEFINE_MEMBER(task_struct, seccomp)
|
DEFINE_MEMBER(task_struct, seccomp)
|
||||||
#ifdef CONFIG_THREAD_INFO_IN_TASK
|
#ifdef CONFIG_THREAD_INFO_IN_TASK
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
#define CMD_UID_SHOULD_UMOUNT 13
|
#define CMD_UID_SHOULD_UMOUNT 13
|
||||||
#define CMD_IS_SU_ENABLED 14
|
#define CMD_IS_SU_ENABLED 14
|
||||||
#define CMD_ENABLE_SU 15
|
#define CMD_ENABLE_SU 15
|
||||||
|
#define CMD_ENABLE_KPM 100
|
||||||
|
|
||||||
#define EVENT_POST_FS_DATA 1
|
#define EVENT_POST_FS_DATA 1
|
||||||
#define EVENT_BOOT_COMPLETED 2
|
#define EVENT_BOOT_COMPLETED 2
|
||||||
|
|||||||
@@ -63,6 +63,10 @@ u32 ksu_devpts_sid;
|
|||||||
// Detect whether it is on or not
|
// Detect whether it is on or not
|
||||||
static bool is_boot_phase = true;
|
static bool is_boot_phase = true;
|
||||||
|
|
||||||
|
#ifdef CONFIG_COMPAT
|
||||||
|
bool ksu_is_compat __read_mostly = false;
|
||||||
|
#endif
|
||||||
|
|
||||||
void on_post_fs_data(void)
|
void on_post_fs_data(void)
|
||||||
{
|
{
|
||||||
static bool done = false;
|
static bool done = false;
|
||||||
@@ -107,6 +111,7 @@ static const char __user *get_user_arg_ptr(struct user_arg_ptr argv, int nr)
|
|||||||
if (get_user(compat, argv.ptr.compat + nr))
|
if (get_user(compat, argv.ptr.compat + nr))
|
||||||
return ERR_PTR(-EFAULT);
|
return ERR_PTR(-EFAULT);
|
||||||
|
|
||||||
|
ksu_is_compat = true;
|
||||||
return compat_ptr(compat);
|
return compat_ptr(compat);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -137,17 +137,45 @@ void apply_kernelsu_rules()
|
|||||||
#define CMD_TYPE_CHANGE 8
|
#define CMD_TYPE_CHANGE 8
|
||||||
#define CMD_GENFSCON 9
|
#define CMD_GENFSCON 9
|
||||||
|
|
||||||
|
#ifdef CONFIG_64BIT
|
||||||
struct sepol_data {
|
struct sepol_data {
|
||||||
u32 cmd;
|
u32 cmd;
|
||||||
u32 subcmd;
|
u32 subcmd;
|
||||||
char __user *sepol1;
|
u64 field_sepol1;
|
||||||
char __user *sepol2;
|
u64 field_sepol2;
|
||||||
char __user *sepol3;
|
u64 field_sepol3;
|
||||||
char __user *sepol4;
|
u64 field_sepol4;
|
||||||
char __user *sepol5;
|
u64 field_sepol5;
|
||||||
char __user *sepol6;
|
u64 field_sepol6;
|
||||||
char __user *sepol7;
|
u64 field_sepol7;
|
||||||
};
|
};
|
||||||
|
#ifdef CONFIG_COMPAT
|
||||||
|
extern bool ksu_is_compat __read_mostly;
|
||||||
|
struct sepol_compat_data {
|
||||||
|
u32 cmd;
|
||||||
|
u32 subcmd;
|
||||||
|
u32 field_sepol1;
|
||||||
|
u32 field_sepol2;
|
||||||
|
u32 field_sepol3;
|
||||||
|
u32 field_sepol4;
|
||||||
|
u32 field_sepol5;
|
||||||
|
u32 field_sepol6;
|
||||||
|
u32 field_sepol7;
|
||||||
|
};
|
||||||
|
#endif // CONFIG_COMPAT
|
||||||
|
#else
|
||||||
|
struct sepol_data {
|
||||||
|
u32 cmd;
|
||||||
|
u32 subcmd;
|
||||||
|
u32 field_sepol1;
|
||||||
|
u32 field_sepol2;
|
||||||
|
u32 field_sepol3;
|
||||||
|
u32 field_sepol4;
|
||||||
|
u32 field_sepol5;
|
||||||
|
u32 field_sepol6;
|
||||||
|
u32 field_sepol7;
|
||||||
|
};
|
||||||
|
#endif // CONFIG_64BIT
|
||||||
|
|
||||||
static int get_object(char *buf, char __user *user_object, size_t buf_sz,
|
static int get_object(char *buf, char __user *user_object, size_t buf_sz,
|
||||||
char **object)
|
char **object)
|
||||||
@@ -192,14 +220,58 @@ int handle_sepolicy(unsigned long arg3, void __user *arg4)
|
|||||||
pr_info("SELinux permissive or disabled when handle policy!\n");
|
pr_info("SELinux permissive or disabled when handle policy!\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
u32 cmd, subcmd;
|
||||||
|
char __user *sepol1, *sepol2, *sepol3, *sepol4, *sepol5, *sepol6, *sepol7;
|
||||||
|
|
||||||
|
#if defined(CONFIG_64BIT) && defined(CONFIG_COMPAT)
|
||||||
|
if (unlikely(ksu_is_compat)) {
|
||||||
|
struct sepol_compat_data compat_data;
|
||||||
|
if (copy_from_user(&compat_data, arg4, sizeof(struct sepol_compat_data))) {
|
||||||
|
pr_err("sepol: copy sepol_data failed.\n");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
sepol1 = compat_ptr(compat_data.field_sepol1);
|
||||||
|
sepol2 = compat_ptr(compat_data.field_sepol2);
|
||||||
|
sepol3 = compat_ptr(compat_data.field_sepol3);
|
||||||
|
sepol4 = compat_ptr(compat_data.field_sepol4);
|
||||||
|
sepol5 = compat_ptr(compat_data.field_sepol5);
|
||||||
|
sepol6 = compat_ptr(compat_data.field_sepol6);
|
||||||
|
sepol7 = compat_ptr(compat_data.field_sepol7);
|
||||||
|
cmd = compat_data.cmd;
|
||||||
|
subcmd = compat_data.subcmd;
|
||||||
|
} else {
|
||||||
struct sepol_data data;
|
struct sepol_data data;
|
||||||
if (copy_from_user(&data, arg4, sizeof(struct sepol_data))) {
|
if (copy_from_user(&data, arg4, sizeof(struct sepol_data))) {
|
||||||
pr_err("sepol: copy sepol_data failed.\n");
|
pr_err("sepol: copy sepol_data failed.\n");
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
sepol1 = data.field_sepol1;
|
||||||
u32 cmd = data.cmd;
|
sepol2 = data.field_sepol2;
|
||||||
u32 subcmd = data.subcmd;
|
sepol3 = data.field_sepol3;
|
||||||
|
sepol4 = data.field_sepol4;
|
||||||
|
sepol5 = data.field_sepol5;
|
||||||
|
sepol6 = data.field_sepol6;
|
||||||
|
sepol7 = data.field_sepol7;
|
||||||
|
cmd = data.cmd;
|
||||||
|
subcmd = data.subcmd;
|
||||||
|
}
|
||||||
|
#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();
|
rcu_read_lock();
|
||||||
|
|
||||||
@@ -213,22 +285,22 @@ int handle_sepolicy(unsigned long arg3, void __user *arg4)
|
|||||||
char perm_buf[MAX_SEPOL_LEN];
|
char perm_buf[MAX_SEPOL_LEN];
|
||||||
|
|
||||||
char *s, *t, *c, *p;
|
char *s, *t, *c, *p;
|
||||||
if (get_object(src_buf, data.sepol1, sizeof(src_buf), &s) < 0) {
|
if (get_object(src_buf, sepol1, sizeof(src_buf), &s) < 0) {
|
||||||
pr_err("sepol: copy src failed.\n");
|
pr_err("sepol: copy src failed.\n");
|
||||||
goto exit;
|
goto exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (get_object(tgt_buf, data.sepol2, sizeof(tgt_buf), &t) < 0) {
|
if (get_object(tgt_buf, sepol2, sizeof(tgt_buf), &t) < 0) {
|
||||||
pr_err("sepol: copy tgt failed.\n");
|
pr_err("sepol: copy tgt failed.\n");
|
||||||
goto exit;
|
goto exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (get_object(cls_buf, data.sepol3, sizeof(cls_buf), &c) < 0) {
|
if (get_object(cls_buf, sepol3, sizeof(cls_buf), &c) < 0) {
|
||||||
pr_err("sepol: copy cls failed.\n");
|
pr_err("sepol: copy cls failed.\n");
|
||||||
goto exit;
|
goto exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (get_object(perm_buf, data.sepol4, sizeof(perm_buf), &p) <
|
if (get_object(perm_buf, sepol4, sizeof(perm_buf), &p) <
|
||||||
0) {
|
0) {
|
||||||
pr_err("sepol: copy perm failed.\n");
|
pr_err("sepol: copy perm failed.\n");
|
||||||
goto exit;
|
goto exit;
|
||||||
@@ -258,24 +330,24 @@ int handle_sepolicy(unsigned long arg3, void __user *arg4)
|
|||||||
char perm_set[MAX_SEPOL_LEN];
|
char perm_set[MAX_SEPOL_LEN];
|
||||||
|
|
||||||
char *s, *t, *c;
|
char *s, *t, *c;
|
||||||
if (get_object(src_buf, data.sepol1, sizeof(src_buf), &s) < 0) {
|
if (get_object(src_buf, sepol1, sizeof(src_buf), &s) < 0) {
|
||||||
pr_err("sepol: copy src failed.\n");
|
pr_err("sepol: copy src failed.\n");
|
||||||
goto exit;
|
goto exit;
|
||||||
}
|
}
|
||||||
if (get_object(tgt_buf, data.sepol2, sizeof(tgt_buf), &t) < 0) {
|
if (get_object(tgt_buf, sepol2, sizeof(tgt_buf), &t) < 0) {
|
||||||
pr_err("sepol: copy tgt failed.\n");
|
pr_err("sepol: copy tgt failed.\n");
|
||||||
goto exit;
|
goto exit;
|
||||||
}
|
}
|
||||||
if (get_object(cls_buf, data.sepol3, sizeof(cls_buf), &c) < 0) {
|
if (get_object(cls_buf, sepol3, sizeof(cls_buf), &c) < 0) {
|
||||||
pr_err("sepol: copy cls failed.\n");
|
pr_err("sepol: copy cls failed.\n");
|
||||||
goto exit;
|
goto exit;
|
||||||
}
|
}
|
||||||
if (strncpy_from_user(operation, data.sepol4,
|
if (strncpy_from_user(operation, sepol4,
|
||||||
sizeof(operation)) < 0) {
|
sizeof(operation)) < 0) {
|
||||||
pr_err("sepol: copy operation failed.\n");
|
pr_err("sepol: copy operation failed.\n");
|
||||||
goto exit;
|
goto exit;
|
||||||
}
|
}
|
||||||
if (strncpy_from_user(perm_set, data.sepol5, sizeof(perm_set)) <
|
if (strncpy_from_user(perm_set, sepol5, sizeof(perm_set)) <
|
||||||
0) {
|
0) {
|
||||||
pr_err("sepol: copy perm_set failed.\n");
|
pr_err("sepol: copy perm_set failed.\n");
|
||||||
goto exit;
|
goto exit;
|
||||||
@@ -295,7 +367,7 @@ int handle_sepolicy(unsigned long arg3, void __user *arg4)
|
|||||||
} else if (cmd == CMD_TYPE_STATE) {
|
} else if (cmd == CMD_TYPE_STATE) {
|
||||||
char src[MAX_SEPOL_LEN];
|
char src[MAX_SEPOL_LEN];
|
||||||
|
|
||||||
if (strncpy_from_user(src, data.sepol1, sizeof(src)) < 0) {
|
if (strncpy_from_user(src, sepol1, sizeof(src)) < 0) {
|
||||||
pr_err("sepol: copy src failed.\n");
|
pr_err("sepol: copy src failed.\n");
|
||||||
goto exit;
|
goto exit;
|
||||||
}
|
}
|
||||||
@@ -315,11 +387,11 @@ int handle_sepolicy(unsigned long arg3, void __user *arg4)
|
|||||||
char type[MAX_SEPOL_LEN];
|
char type[MAX_SEPOL_LEN];
|
||||||
char attr[MAX_SEPOL_LEN];
|
char attr[MAX_SEPOL_LEN];
|
||||||
|
|
||||||
if (strncpy_from_user(type, data.sepol1, sizeof(type)) < 0) {
|
if (strncpy_from_user(type, sepol1, sizeof(type)) < 0) {
|
||||||
pr_err("sepol: copy type failed.\n");
|
pr_err("sepol: copy type failed.\n");
|
||||||
goto exit;
|
goto exit;
|
||||||
}
|
}
|
||||||
if (strncpy_from_user(attr, data.sepol2, sizeof(attr)) < 0) {
|
if (strncpy_from_user(attr, sepol2, sizeof(attr)) < 0) {
|
||||||
pr_err("sepol: copy attr failed.\n");
|
pr_err("sepol: copy attr failed.\n");
|
||||||
goto exit;
|
goto exit;
|
||||||
}
|
}
|
||||||
@@ -339,7 +411,7 @@ int handle_sepolicy(unsigned long arg3, void __user *arg4)
|
|||||||
} else if (cmd == CMD_ATTR) {
|
} else if (cmd == CMD_ATTR) {
|
||||||
char attr[MAX_SEPOL_LEN];
|
char attr[MAX_SEPOL_LEN];
|
||||||
|
|
||||||
if (strncpy_from_user(attr, data.sepol1, sizeof(attr)) < 0) {
|
if (strncpy_from_user(attr, sepol1, sizeof(attr)) < 0) {
|
||||||
pr_err("sepol: copy attr failed.\n");
|
pr_err("sepol: copy attr failed.\n");
|
||||||
goto exit;
|
goto exit;
|
||||||
}
|
}
|
||||||
@@ -356,28 +428,28 @@ int handle_sepolicy(unsigned long arg3, void __user *arg4)
|
|||||||
char default_type[MAX_SEPOL_LEN];
|
char default_type[MAX_SEPOL_LEN];
|
||||||
char object[MAX_SEPOL_LEN];
|
char object[MAX_SEPOL_LEN];
|
||||||
|
|
||||||
if (strncpy_from_user(src, data.sepol1, sizeof(src)) < 0) {
|
if (strncpy_from_user(src, sepol1, sizeof(src)) < 0) {
|
||||||
pr_err("sepol: copy src failed.\n");
|
pr_err("sepol: copy src failed.\n");
|
||||||
goto exit;
|
goto exit;
|
||||||
}
|
}
|
||||||
if (strncpy_from_user(tgt, data.sepol2, sizeof(tgt)) < 0) {
|
if (strncpy_from_user(tgt, sepol2, sizeof(tgt)) < 0) {
|
||||||
pr_err("sepol: copy tgt failed.\n");
|
pr_err("sepol: copy tgt failed.\n");
|
||||||
goto exit;
|
goto exit;
|
||||||
}
|
}
|
||||||
if (strncpy_from_user(cls, data.sepol3, sizeof(cls)) < 0) {
|
if (strncpy_from_user(cls, sepol3, sizeof(cls)) < 0) {
|
||||||
pr_err("sepol: copy cls failed.\n");
|
pr_err("sepol: copy cls failed.\n");
|
||||||
goto exit;
|
goto exit;
|
||||||
}
|
}
|
||||||
if (strncpy_from_user(default_type, data.sepol4,
|
if (strncpy_from_user(default_type, sepol4,
|
||||||
sizeof(default_type)) < 0) {
|
sizeof(default_type)) < 0) {
|
||||||
pr_err("sepol: copy default_type failed.\n");
|
pr_err("sepol: copy default_type failed.\n");
|
||||||
goto exit;
|
goto exit;
|
||||||
}
|
}
|
||||||
char *real_object;
|
char *real_object;
|
||||||
if (data.sepol5 == NULL) {
|
if (sepol5 == NULL) {
|
||||||
real_object = NULL;
|
real_object = NULL;
|
||||||
} else {
|
} else {
|
||||||
if (strncpy_from_user(object, data.sepol5,
|
if (strncpy_from_user(object, sepol5,
|
||||||
sizeof(object)) < 0) {
|
sizeof(object)) < 0) {
|
||||||
pr_err("sepol: copy object failed.\n");
|
pr_err("sepol: copy object failed.\n");
|
||||||
goto exit;
|
goto exit;
|
||||||
@@ -396,19 +468,19 @@ int handle_sepolicy(unsigned long arg3, void __user *arg4)
|
|||||||
char cls[MAX_SEPOL_LEN];
|
char cls[MAX_SEPOL_LEN];
|
||||||
char default_type[MAX_SEPOL_LEN];
|
char default_type[MAX_SEPOL_LEN];
|
||||||
|
|
||||||
if (strncpy_from_user(src, data.sepol1, sizeof(src)) < 0) {
|
if (strncpy_from_user(src, sepol1, sizeof(src)) < 0) {
|
||||||
pr_err("sepol: copy src failed.\n");
|
pr_err("sepol: copy src failed.\n");
|
||||||
goto exit;
|
goto exit;
|
||||||
}
|
}
|
||||||
if (strncpy_from_user(tgt, data.sepol2, sizeof(tgt)) < 0) {
|
if (strncpy_from_user(tgt, sepol2, sizeof(tgt)) < 0) {
|
||||||
pr_err("sepol: copy tgt failed.\n");
|
pr_err("sepol: copy tgt failed.\n");
|
||||||
goto exit;
|
goto exit;
|
||||||
}
|
}
|
||||||
if (strncpy_from_user(cls, data.sepol3, sizeof(cls)) < 0) {
|
if (strncpy_from_user(cls, sepol3, sizeof(cls)) < 0) {
|
||||||
pr_err("sepol: copy cls failed.\n");
|
pr_err("sepol: copy cls failed.\n");
|
||||||
goto exit;
|
goto exit;
|
||||||
}
|
}
|
||||||
if (strncpy_from_user(default_type, data.sepol4,
|
if (strncpy_from_user(default_type, sepol4,
|
||||||
sizeof(default_type)) < 0) {
|
sizeof(default_type)) < 0) {
|
||||||
pr_err("sepol: copy default_type failed.\n");
|
pr_err("sepol: copy default_type failed.\n");
|
||||||
goto exit;
|
goto exit;
|
||||||
@@ -429,15 +501,15 @@ int handle_sepolicy(unsigned long arg3, void __user *arg4)
|
|||||||
char name[MAX_SEPOL_LEN];
|
char name[MAX_SEPOL_LEN];
|
||||||
char path[MAX_SEPOL_LEN];
|
char path[MAX_SEPOL_LEN];
|
||||||
char context[MAX_SEPOL_LEN];
|
char context[MAX_SEPOL_LEN];
|
||||||
if (strncpy_from_user(name, data.sepol1, sizeof(name)) < 0) {
|
if (strncpy_from_user(name, sepol1, sizeof(name)) < 0) {
|
||||||
pr_err("sepol: copy name failed.\n");
|
pr_err("sepol: copy name failed.\n");
|
||||||
goto exit;
|
goto exit;
|
||||||
}
|
}
|
||||||
if (strncpy_from_user(path, data.sepol2, sizeof(path)) < 0) {
|
if (strncpy_from_user(path, sepol2, sizeof(path)) < 0) {
|
||||||
pr_err("sepol: copy path failed.\n");
|
pr_err("sepol: copy path failed.\n");
|
||||||
goto exit;
|
goto exit;
|
||||||
}
|
}
|
||||||
if (strncpy_from_user(context, data.sepol3, sizeof(context)) <
|
if (strncpy_from_user(context, sepol3, sizeof(context)) <
|
||||||
0) {
|
0) {
|
||||||
pr_err("sepol: copy context failed.\n");
|
pr_err("sepol: copy context failed.\n");
|
||||||
goto exit;
|
goto exit;
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ setup_kernelsu() {
|
|||||||
echo "[+] Setting up KernelSU..."
|
echo "[+] Setting up KernelSU..."
|
||||||
# Clone the repository and rename it to KernelSU
|
# Clone the repository and rename it to KernelSU
|
||||||
if [ ! -d "$GKI_ROOT/KernelSU" ]; then
|
if [ ! -d "$GKI_ROOT/KernelSU" ]; then
|
||||||
git clone https://github.com/ShirkNeko/SukiSU-Ultra SukiSU-Ultra
|
git clone https://github.com/SukiSU-Ultra/SukiSU-Ultra SukiSU-Ultra
|
||||||
mv SukiSU-Ultra KernelSU
|
mv SukiSU-Ultra KernelSU
|
||||||
echo "[+] Repository cloned and renamed to KernelSU."
|
echo "[+] Repository cloned and renamed to KernelSU."
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -22,6 +22,10 @@
|
|||||||
|
|
||||||
extern void escape_to_root();
|
extern void escape_to_root();
|
||||||
|
|
||||||
|
#ifndef CONFIG_KPROBES
|
||||||
|
static bool ksu_sucompat_non_kp __read_mostly = true;
|
||||||
|
#endif
|
||||||
|
|
||||||
static void __user *userspace_stack_buffer(const void *d, size_t len)
|
static void __user *userspace_stack_buffer(const void *d, size_t len)
|
||||||
{
|
{
|
||||||
/* To avoid having to mmap a page in userspace, just write below the stack
|
/* To avoid having to mmap a page in userspace, just write below the stack
|
||||||
@@ -50,6 +54,12 @@ int ksu_handle_faccessat(int *dfd, const char __user **filename_user, int *mode,
|
|||||||
{
|
{
|
||||||
const char su[] = SU_PATH;
|
const char su[] = SU_PATH;
|
||||||
|
|
||||||
|
#ifndef CONFIG_KPROBES
|
||||||
|
if (!ksu_sucompat_non_kp) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
if (!ksu_is_allow_uid(current_uid().val)) {
|
if (!ksu_is_allow_uid(current_uid().val)) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -71,6 +81,11 @@ int ksu_handle_stat(int *dfd, const char __user **filename_user, int *flags)
|
|||||||
// const char sh[] = SH_PATH;
|
// const char sh[] = SH_PATH;
|
||||||
const char su[] = SU_PATH;
|
const char su[] = SU_PATH;
|
||||||
|
|
||||||
|
#ifndef CONFIG_KPROBES
|
||||||
|
if (!ksu_sucompat_non_kp) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
if (!ksu_is_allow_uid(current_uid().val)) {
|
if (!ksu_is_allow_uid(current_uid().val)) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -115,6 +130,11 @@ int ksu_handle_execveat_sucompat(int *fd, struct filename **filename_ptr,
|
|||||||
const char sh[] = KSUD_PATH;
|
const char sh[] = KSUD_PATH;
|
||||||
const char su[] = SU_PATH;
|
const char su[] = SU_PATH;
|
||||||
|
|
||||||
|
#ifndef CONFIG_KPROBES
|
||||||
|
if (!ksu_sucompat_non_kp) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
if (unlikely(!filename_ptr))
|
if (unlikely(!filename_ptr))
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
@@ -144,6 +164,11 @@ int ksu_handle_execve_sucompat(int *fd, const char __user **filename_user,
|
|||||||
const char su[] = SU_PATH;
|
const char su[] = SU_PATH;
|
||||||
char path[sizeof(su) + 1];
|
char path[sizeof(su) + 1];
|
||||||
|
|
||||||
|
#ifndef CONFIG_KPROBES
|
||||||
|
if (!ksu_sucompat_non_kp){
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
if (unlikely(!filename_user))
|
if (unlikely(!filename_user))
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
@@ -237,6 +262,9 @@ void ksu_sucompat_init()
|
|||||||
su_kps[0] = init_kprobe(SYS_EXECVE_SYMBOL, execve_handler_pre);
|
su_kps[0] = init_kprobe(SYS_EXECVE_SYMBOL, execve_handler_pre);
|
||||||
su_kps[1] = init_kprobe(SYS_FACCESSAT_SYMBOL, faccessat_handler_pre);
|
su_kps[1] = init_kprobe(SYS_FACCESSAT_SYMBOL, faccessat_handler_pre);
|
||||||
su_kps[2] = init_kprobe(SYS_NEWFSTATAT_SYMBOL, newfstatat_handler_pre);
|
su_kps[2] = init_kprobe(SYS_NEWFSTATAT_SYMBOL, newfstatat_handler_pre);
|
||||||
|
#else
|
||||||
|
ksu_sucompat_non_kp = true;
|
||||||
|
pr_info("ksu_sucompat_init: hooks enabled: execve/execveat_su, faccessat, stat\n");
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -246,5 +274,8 @@ void ksu_sucompat_exit()
|
|||||||
for (int i = 0; i < ARRAY_SIZE(su_kps); i++) {
|
for (int i = 0; i < ARRAY_SIZE(su_kps); i++) {
|
||||||
destroy_kprobe(&su_kps[i]);
|
destroy_kprobe(&su_kps[i]);
|
||||||
}
|
}
|
||||||
|
#else
|
||||||
|
ksu_sucompat_non_kp = false;
|
||||||
|
pr_info("ksu_sucompat_exit: hooks disabled: execve/execveat_su, faccessat, stat\n");
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
#include <linux/string.h>
|
#include <linux/string.h>
|
||||||
#include <linux/types.h>
|
#include <linux/types.h>
|
||||||
#include <linux/version.h>
|
#include <linux/version.h>
|
||||||
|
#include <linux/namei.h>
|
||||||
|
|
||||||
#include "allowlist.h"
|
#include "allowlist.h"
|
||||||
#include "klog.h" // IWYU pragma: keep
|
#include "klog.h" // IWYU pragma: keep
|
||||||
@@ -115,6 +116,7 @@ struct my_dir_context {
|
|||||||
void *private_data;
|
void *private_data;
|
||||||
int depth;
|
int depth;
|
||||||
int *stop;
|
int *stop;
|
||||||
|
struct super_block* root_sb;
|
||||||
};
|
};
|
||||||
// https://docs.kernel.org/filesystems/porting.html
|
// 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.
|
// 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 =
|
struct my_dir_context *my_ctx =
|
||||||
container_of(ctx, struct my_dir_context, ctx);
|
container_of(ctx, struct my_dir_context, ctx);
|
||||||
char dirpath[DATA_PATH_LEN];
|
char dirpath[DATA_PATH_LEN];
|
||||||
|
int err;
|
||||||
|
struct path path;
|
||||||
|
|
||||||
if (!my_ctx) {
|
if (!my_ctx) {
|
||||||
pr_err("Invalid context\n");
|
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;
|
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 &&
|
if (d_type == DT_DIR && my_ctx->depth > 0 &&
|
||||||
(my_ctx->stop && !*my_ctx->stop)) {
|
(my_ctx->stop && !*my_ctx->stop)) {
|
||||||
struct data_path *data = kmalloc(sizeof(struct data_path), GFP_ATOMIC);
|
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)
|
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 list_head data_path_list;
|
||||||
|
struct path kpath;
|
||||||
|
struct super_block* root_sb;
|
||||||
INIT_LIST_HEAD(&data_path_list);
|
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
|
// Initialize APK cache list
|
||||||
struct apk_path_hash *pos, *n;
|
struct apk_path_hash *pos, *n;
|
||||||
list_for_each_entry(pos, &apk_path_hash_list, list) {
|
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;
|
data.depth = depth;
|
||||||
list_add_tail(&data.list, &data_path_list);
|
list_add_tail(&data.list, &data_path_list);
|
||||||
|
|
||||||
|
root_sb = kpath.dentry->d_inode->i_sb;
|
||||||
|
|
||||||
for (i = depth; i >= 0; i--) {
|
for (i = depth; i >= 0; i--) {
|
||||||
struct data_path *pos, *n;
|
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,
|
.parent_dir = pos->dirpath,
|
||||||
.private_data = uid_data,
|
.private_data = uid_data,
|
||||||
.depth = pos->depth,
|
.depth = pos->depth,
|
||||||
.stop = &stop };
|
.stop = &stop,
|
||||||
|
.root_sb = root_sb };
|
||||||
struct file *file;
|
struct file *file;
|
||||||
|
|
||||||
if (!stop) {
|
if (!stop) {
|
||||||
@@ -358,12 +386,14 @@ void track_throne()
|
|||||||
if (ksu_is_manager_uid_valid()) {
|
if (ksu_is_manager_uid_valid()) {
|
||||||
pr_info("manager is uninstalled, invalidate it!\n");
|
pr_info("manager is uninstalled, invalidate it!\n");
|
||||||
ksu_invalidate_manager_uid();
|
ksu_invalidate_manager_uid();
|
||||||
|
goto prune;
|
||||||
}
|
}
|
||||||
pr_info("Searching manager...\n");
|
pr_info("Searching manager...\n");
|
||||||
search_manager("/data/app", 2, &uid_list);
|
search_manager("/data/app", 2, &uid_list);
|
||||||
pr_info("Search manager finished\n");
|
pr_info("Search manager finished\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
prune:
|
||||||
// then prune the allowlist
|
// then prune the allowlist
|
||||||
ksu_prune_allowlist(is_uid_exist, &uid_list);
|
ksu_prune_allowlist(is_uid_exist, &uid_list);
|
||||||
out:
|
out:
|
||||||
|
|||||||
@@ -50,7 +50,6 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
aidl = true
|
|
||||||
buildConfig = true
|
buildConfig = true
|
||||||
compose = true
|
compose = true
|
||||||
prefab = true
|
prefab = true
|
||||||
@@ -109,6 +108,7 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
implementation(libs.gson)
|
||||||
implementation(libs.androidx.activity.compose)
|
implementation(libs.androidx.activity.compose)
|
||||||
implementation(libs.androidx.navigation.compose)
|
implementation(libs.androidx.navigation.compose)
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,19 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</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
|
<activity
|
||||||
android:name=".ui.webui.WebUIActivity"
|
android:name=".ui.webui.WebUIActivity"
|
||||||
android:autoRemoveFromRecents="true"
|
android:autoRemoveFromRecents="true"
|
||||||
|
|||||||
@@ -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) {
|
Java_com_sukisu_ultra_Natives_setSuEnabled(JNIEnv *env, jobject thiz, jboolean enabled) {
|
||||||
return set_su_enabled(enabled);
|
return set_su_enabled(enabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extern "C" JNIEXPORT jboolean JNICALL
|
||||||
|
Java_com_sukisu_ultra_Natives_isKPMEnabled(JNIEnv *env, jobject) {
|
||||||
|
return is_KPM_enable();
|
||||||
|
}
|
||||||
@@ -29,6 +29,7 @@
|
|||||||
#define CMD_IS_UID_SHOULD_UMOUNT 13
|
#define CMD_IS_UID_SHOULD_UMOUNT 13
|
||||||
#define CMD_IS_SU_ENABLED 14
|
#define CMD_IS_SU_ENABLED 14
|
||||||
#define CMD_ENABLE_SU 15
|
#define CMD_ENABLE_SU 15
|
||||||
|
#define CMD_ENABLE_KPM 100
|
||||||
|
|
||||||
static bool ksuctl(int cmd, void* arg1, void* arg2) {
|
static bool ksuctl(int cmd, void* arg1, void* arg2) {
|
||||||
int32_t result = 0;
|
int32_t result = 0;
|
||||||
@@ -97,3 +98,8 @@ bool is_su_enabled() {
|
|||||||
ksuctl(CMD_IS_SU_ENABLED, &enabled, nullptr);
|
ksuctl(CMD_IS_SU_ENABLED, &enabled, nullptr);
|
||||||
return enabled;
|
return enabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool is_KPM_enable() {
|
||||||
|
bool enabled = false;
|
||||||
|
return ksuctl(CMD_ENABLE_KPM, &enabled, nullptr), enabled;
|
||||||
|
}
|
||||||
@@ -83,4 +83,6 @@ bool set_su_enabled(bool enabled);
|
|||||||
|
|
||||||
bool is_su_enabled();
|
bool is_su_enabled();
|
||||||
|
|
||||||
|
bool is_KPM_enable();
|
||||||
|
|
||||||
#endif //KERNELSU_KSU_H
|
#endif //KERNELSU_KSU_H
|
||||||
|
|||||||
BIN
manager/app/src/main/ic_launcher-playstore.png
Normal file
|
After Width: | Height: | Size: 87 KiB |
@@ -1,21 +1,95 @@
|
|||||||
package com.sukisu.ultra
|
package com.sukisu.ultra
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.Activity
|
||||||
|
import android.app.ActivityOptions
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.res.Configuration
|
||||||
|
import android.content.res.Resources
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
import coil.Coil
|
import coil.Coil
|
||||||
import coil.ImageLoader
|
import coil.ImageLoader
|
||||||
import com.dergoogler.mmrl.platform.Platform
|
import com.dergoogler.mmrl.platform.Platform
|
||||||
import me.zhanghai.android.appiconloader.coil.AppIconFetcher
|
import me.zhanghai.android.appiconloader.coil.AppIconFetcher
|
||||||
import me.zhanghai.android.appiconloader.coil.AppIconKeyer
|
import me.zhanghai.android.appiconloader.coil.AppIconKeyer
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
@SuppressLint("StaticFieldLeak")
|
||||||
lateinit var ksuApp: KernelSUApplication
|
lateinit var ksuApp: KernelSUApplication
|
||||||
|
|
||||||
class KernelSUApplication : Application() {
|
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() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
ksuApp = this
|
ksuApp = this
|
||||||
|
|
||||||
|
// 注册Activity生命周期回调
|
||||||
|
registerActivityLifecycleCallbacks(activityLifecycleCallbacks)
|
||||||
|
|
||||||
Platform.setHiddenApiExemptions()
|
Platform.setHiddenApiExemptions()
|
||||||
|
|
||||||
val context = this
|
val context = this
|
||||||
@@ -35,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) {
|
data class KernelVersion(val major: Int, val patchLevel: Int, val subLevel: Int) {
|
||||||
override fun toString(): String {
|
override fun toString(): String = "$major.$patchLevel.$subLevel"
|
||||||
return "$major.$patchLevel.$subLevel"
|
fun isGKI(): Boolean = when {
|
||||||
}
|
major > 5 -> true
|
||||||
|
major == 5 && patchLevel >= 10 -> true
|
||||||
fun isGKI(): Boolean {
|
else -> false
|
||||||
|
|
||||||
// kernel 6.x
|
|
||||||
if (major > 5) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// kernel 5.10.x
|
|
||||||
if (major == 5) {
|
|
||||||
return patchLevel >= 10
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
fun isGKI1(): Boolean = (major == 4 && patchLevel >= 19) || (major == 5 && patchLevel < 10)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun parseKernelVersion(version: String): KernelVersion {
|
fun parseKernelVersion(version: String): KernelVersion {
|
||||||
|
|||||||
@@ -16,16 +16,18 @@ object Natives {
|
|||||||
// 10946: add capabilities
|
// 10946: add capabilities
|
||||||
// 10977: change groups_count and groups to avoid overflow write
|
// 10977: change groups_count and groups to avoid overflow write
|
||||||
// 11071: Fix the issue of failing to set a custom SELinux type.
|
// 11071: Fix the issue of failing to set a custom SELinux type.
|
||||||
const val MINIMAL_SUPPORTED_KERNEL = 12800
|
const val MINIMAL_SUPPORTED_KERNEL = 11071
|
||||||
|
|
||||||
// 11640: Support query working mode, LKM or GKI
|
// 11640: Support query working mode, LKM or GKI
|
||||||
// when MINIMAL_SUPPORTED_KERNEL > 11640, we can remove this constant.
|
// when MINIMAL_SUPPORTED_KERNEL > 11640, we can remove this constant.
|
||||||
const val MINIMAL_SUPPORTED_KERNEL_LKM = 12800
|
const val MINIMAL_SUPPORTED_KERNEL_LKM = 11648
|
||||||
|
|
||||||
// 12040: Support disable sucompat mode
|
// 12040: Support disable sucompat mode
|
||||||
const val MINIMAL_SUPPORTED_SU_COMPAT = 12800
|
const val MINIMAL_SUPPORTED_SU_COMPAT = 12040
|
||||||
const val KERNEL_SU_DOMAIN = "u:r:su:s0"
|
const val KERNEL_SU_DOMAIN = "u:r:su:s0"
|
||||||
|
|
||||||
|
const val MINIMAL_SUPPORTED_KPM = 12800
|
||||||
|
|
||||||
const val ROOT_UID = 0
|
const val ROOT_UID = 0
|
||||||
const val ROOT_GID = 0
|
const val ROOT_GID = 0
|
||||||
|
|
||||||
@@ -66,6 +68,7 @@ object Natives {
|
|||||||
*/
|
*/
|
||||||
external fun isSuEnabled(): Boolean
|
external fun isSuEnabled(): Boolean
|
||||||
external fun setSuEnabled(enabled: Boolean): Boolean
|
external fun setSuEnabled(enabled: Boolean): Boolean
|
||||||
|
external fun isKPMEnabled(): Boolean
|
||||||
|
|
||||||
private const val NON_ROOT_DEFAULT_PROFILE_KEY = "$"
|
private const val NON_ROOT_DEFAULT_PROFILE_KEY = "$"
|
||||||
private const val NOBODY_UID = 9999
|
private const val NOBODY_UID = 9999
|
||||||
|
|||||||
@@ -1,56 +1,64 @@
|
|||||||
package com.sukisu.ultra.ui
|
package com.sukisu.ultra.ui
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.database.ContentObserver
|
import android.content.res.Configuration
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Handler
|
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
import androidx.compose.animation.*
|
|
||||||
import androidx.compose.animation.core.tween
|
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||||
import androidx.navigation.NavBackStackEntry
|
|
||||||
import androidx.navigation.NavHostController
|
|
||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
import com.ramcosta.composedestinations.DestinationsNavHost
|
import com.ramcosta.composedestinations.DestinationsNavHost
|
||||||
import com.ramcosta.composedestinations.animations.NavHostAnimatedDestinationStyle
|
|
||||||
import com.ramcosta.composedestinations.generated.NavGraphs
|
import com.ramcosta.composedestinations.generated.NavGraphs
|
||||||
|
import com.ramcosta.composedestinations.generated.destinations.ExecuteModuleActionScreenDestination
|
||||||
import com.ramcosta.composedestinations.spec.NavHostGraphSpec
|
import com.ramcosta.composedestinations.spec.NavHostGraphSpec
|
||||||
import com.ramcosta.composedestinations.spec.RouteOrDirection
|
|
||||||
import com.ramcosta.composedestinations.utils.isRouteOnBackStackAsState
|
|
||||||
import com.ramcosta.composedestinations.utils.rememberDestinationsNavigator
|
|
||||||
import io.sukisu.ultra.UltraToolInstall
|
import io.sukisu.ultra.UltraToolInstall
|
||||||
import com.sukisu.ultra.Natives
|
|
||||||
import com.sukisu.ultra.ksuApp
|
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.*
|
||||||
import com.sukisu.ultra.ui.theme.CardConfig.cardAlpha
|
import zako.zako.zako.zakoui.activity.util.*
|
||||||
import com.sukisu.ultra.ui.util.*
|
import zako.zako.zako.zakoui.activity.component.BottomBar
|
||||||
import androidx.core.content.edit
|
import com.sukisu.ultra.ui.util.LocalSnackbarHost
|
||||||
import com.sukisu.ultra.ui.theme.CardConfig.cardElevation
|
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 com.sukisu.ultra.ui.webui.initPlatform
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
private inner class ThemeChangeContentObserver(
|
private lateinit var superUserViewModel: SuperUserViewModel
|
||||||
handler: Handler,
|
private lateinit var homeViewModel: HomeViewModel
|
||||||
private val onThemeChanged: () -> Unit
|
internal val settingsStateFlow = MutableStateFlow(SettingsState())
|
||||||
) : ContentObserver(handler) {
|
|
||||||
override fun onChange(selfChange: Boolean) {
|
data class SettingsState(
|
||||||
super.onChange(selfChange)
|
val isHideOtherInfo: Boolean = false,
|
||||||
onThemeChanged()
|
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?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
// 应用DPI设置(仅对当前应用生效)
|
try {
|
||||||
applyCustomDpi()
|
// 确保应用正确的语言设置
|
||||||
|
LocaleUtils.applyLanguageSetting(this)
|
||||||
|
|
||||||
|
// 应用自定义 DPI
|
||||||
|
DisplayUtils.applyCustomDpi(this)
|
||||||
|
|
||||||
// Enable edge to edge
|
// Enable edge to edge
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
@@ -61,97 +69,115 @@ class MainActivity : ComponentActivity() {
|
|||||||
|
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
val prefs = getSharedPreferences("settings", MODE_PRIVATE)
|
// 使用标记控制初始化流程
|
||||||
val isFirstRun = prefs.getBoolean("is_first_run", true)
|
if (!isInitialized) {
|
||||||
|
initializeViewModels()
|
||||||
if (isFirstRun) {
|
initializeData()
|
||||||
ThemeConfig.preventBackgroundRefresh = false
|
isInitialized = true
|
||||||
getSharedPreferences("theme_prefs", MODE_PRIVATE).edit {
|
|
||||||
putBoolean("prevent_background_refresh", false)
|
|
||||||
}
|
|
||||||
prefs.edit { putBoolean("is_first_run", false) }
|
|
||||||
}
|
|
||||||
|
|
||||||
// 加载保存的背景设置
|
|
||||||
loadThemeMode()
|
|
||||||
loadThemeColors()
|
|
||||||
loadDynamicColorState()
|
|
||||||
CardConfig.load(applicationContext)
|
|
||||||
|
|
||||||
val contentObserver = ThemeChangeContentObserver(Handler(mainLooper)) {
|
|
||||||
runOnUiThread {
|
|
||||||
if (!ThemeConfig.preventBackgroundRefresh) {
|
|
||||||
ThemeConfig.backgroundImageLoaded = false
|
|
||||||
loadCustomBackground()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
contentResolver.registerContentObserver(
|
|
||||||
android.provider.Settings.System.getUriFor("ui_night_mode"),
|
|
||||||
false,
|
|
||||||
contentObserver
|
|
||||||
)
|
|
||||||
|
|
||||||
val destroyListeners = mutableListOf<() -> Unit>()
|
|
||||||
destroyListeners.add {
|
|
||||||
contentResolver.unregisterContentObserver(contentObserver)
|
|
||||||
}
|
|
||||||
|
|
||||||
val isManager = Natives.becomeManager(ksuApp.packageName)
|
|
||||||
if (isManager) {
|
|
||||||
install()
|
|
||||||
UltraToolInstall.tryToInstall()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setContent {
|
setContent {
|
||||||
KernelSUTheme {
|
KernelSUTheme {
|
||||||
val navController = rememberNavController()
|
val navController = rememberNavController()
|
||||||
val snackBarHostState = remember { SnackbarHostState() }
|
val snackBarHostState = remember { SnackbarHostState() }
|
||||||
|
val currentDestination = navController.currentBackStackEntryAsState().value?.destination
|
||||||
|
|
||||||
|
val showBottomBar = when (currentDestination?.route) {
|
||||||
|
ExecuteModuleActionScreenDestination.route -> false
|
||||||
|
else -> true
|
||||||
|
}
|
||||||
|
|
||||||
// pre-init platform to faster start WebUI X activities
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
initPlatform()
|
initPlatform()
|
||||||
}
|
}
|
||||||
|
|
||||||
Scaffold(
|
|
||||||
bottomBar = { BottomBar(navController) },
|
|
||||||
contentWindowInsets = WindowInsets(0, 0, 0, 0)
|
|
||||||
) { innerPadding ->
|
|
||||||
CompositionLocalProvider(
|
CompositionLocalProvider(
|
||||||
LocalSnackbarHost provides snackBarHostState
|
LocalSnackbarHost provides snackBarHostState
|
||||||
) {
|
) {
|
||||||
|
Scaffold(
|
||||||
|
bottomBar = {
|
||||||
|
AnimatedBottomBar.AnimatedBottomBarWrapper(
|
||||||
|
showBottomBar = showBottomBar,
|
||||||
|
content = { BottomBar(navController) }
|
||||||
|
)
|
||||||
|
},
|
||||||
|
contentWindowInsets = WindowInsets(0, 0, 0, 0)
|
||||||
|
) { innerPadding ->
|
||||||
DestinationsNavHost(
|
DestinationsNavHost(
|
||||||
modifier = Modifier.padding(innerPadding),
|
modifier = Modifier.padding(innerPadding),
|
||||||
navGraph = NavGraphs.root as NavHostGraphSpec,
|
navGraph = NavGraphs.root as NavHostGraphSpec,
|
||||||
navController = navController,
|
navController = navController,
|
||||||
defaultTransitions = object : NavHostAnimatedDestinationStyle() {
|
defaultTransitions = NavigationUtils.defaultTransitions()
|
||||||
override val enterTransition: AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition
|
|
||||||
get() = { fadeIn(animationSpec = tween(340)) }
|
|
||||||
override val exitTransition: AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition
|
|
||||||
get() = { fadeOut(animationSpec = tween(340)) }
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 应用自定义DPI设置(仅对当前应用生效)
|
private fun initializeViewModels() {
|
||||||
private fun applyCustomDpi() {
|
superUserViewModel = SuperUserViewModel()
|
||||||
val prefs = getSharedPreferences("settings", MODE_PRIVATE)
|
homeViewModel = HomeViewModel()
|
||||||
val customDpi = prefs.getInt("app_dpi", 0)
|
|
||||||
|
|
||||||
if (customDpi > 0) {
|
// 设置主题变化监听器
|
||||||
|
themeChangeObserver = ThemeUtils.registerThemeChangeObserver(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initializeData() {
|
||||||
|
lifecycleScope.launch {
|
||||||
try {
|
try {
|
||||||
val resources = resources
|
superUserViewModel.fetchAppList()
|
||||||
val metrics = resources.displayMetrics
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 仅更新应用内显示,不影响系统状态栏
|
lifecycleScope.launch {
|
||||||
metrics.density = customDpi / 160f
|
try {
|
||||||
metrics.scaledDensity = customDpi / 160f
|
homeViewModel.initializeData()
|
||||||
metrics.densityDpi = customDpi
|
} 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) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
@@ -159,125 +185,29 @@ class MainActivity : ComponentActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
|
try {
|
||||||
super.onPause()
|
super.onPause()
|
||||||
CardConfig.save(applicationContext)
|
ThemeUtils.onActivityPause(this)
|
||||||
getSharedPreferences("theme_prefs", MODE_PRIVATE).edit {
|
} catch (e: Exception) {
|
||||||
putBoolean("prevent_background_refresh", true)
|
e.printStackTrace()
|
||||||
}
|
|
||||||
ThemeConfig.preventBackgroundRefresh = true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResume() {
|
|
||||||
super.onResume()
|
|
||||||
if (!ThemeConfig.backgroundImageLoaded && !ThemeConfig.preventBackgroundRefresh) {
|
|
||||||
loadCustomBackground()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val destroyListeners = mutableListOf<() -> Unit>()
|
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
destroyListeners.forEach { it() }
|
try {
|
||||||
|
ThemeUtils.unregisterThemeChangeObserver(this, themeChangeObserver)
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||||
@Composable
|
try {
|
||||||
private fun BottomBar(navController: NavHostController) {
|
super.onConfigurationChanged(newConfig)
|
||||||
val navigator = navController.rememberDestinationsNavigator()
|
LocaleUtils.applyLanguageSetting(this)
|
||||||
val isManager = Natives.becomeManager(ksuApp.packageName)
|
} catch (e: Exception) {
|
||||||
val fullFeatured = isManager && !Natives.requireNewKernel() && rootAvailable()
|
e.printStackTrace()
|
||||||
val kpmVersion = getKpmVersion()
|
|
||||||
val containerColor = MaterialTheme.colorScheme.surfaceVariant
|
|
||||||
val cardColor = MaterialTheme.colorScheme.surfaceVariant
|
|
||||||
|
|
||||||
// 检查是否显示KPM
|
|
||||||
val showKpmInfo = LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
|
||||||
.getBoolean("show_kpm_info", true)
|
|
||||||
|
|
||||||
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") && showKpmInfo) {
|
|
||||||
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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -20,7 +20,6 @@ import androidx.compose.ui.graphics.graphicsLayer
|
|||||||
import androidx.compose.ui.input.pointer.pointerInput
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.window.Dialog
|
import androidx.compose.ui.window.Dialog
|
||||||
@@ -138,7 +137,7 @@ fun ImageEditorDialog(
|
|||||||
0f
|
0f
|
||||||
}
|
}
|
||||||
updateTransformation(newScale, newOffsetX, newOffsetY)
|
updateTransformation(newScale, newOffsetX, newOffsetY)
|
||||||
} catch (e: Exception) {
|
} catch (_: Exception) {
|
||||||
updateTransformation(lastScale, lastOffsetX, lastOffsetY)
|
updateTransformation(lastScale, lastOffsetX, lastOffsetY)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -186,7 +185,7 @@ fun ImageEditorDialog(
|
|||||||
val transformation = BackgroundTransformation(scale, offsetX, offsetY)
|
val transformation = BackgroundTransformation(scale, offsetX, offsetY)
|
||||||
val savedUri = context.saveTransformedBackground(imageUri, transformation)
|
val savedUri = context.saveTransformedBackground(imageUri, transformation)
|
||||||
savedUri?.let { onConfirm(it) }
|
savedUri?.let { onConfirm(it) }
|
||||||
} catch (e: Exception) {
|
} catch (_: Exception) {
|
||||||
""
|
""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,7 +63,12 @@ fun SearchAppBar(
|
|||||||
var onSearch by remember { mutableStateOf(false) }
|
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
|
val cardAlpha = CardConfig.cardAlpha
|
||||||
|
|
||||||
if (onSearch) {
|
if (onSearch) {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import androidx.compose.foundation.interaction.MutableInteractionSource
|
|||||||
import androidx.compose.foundation.selection.toggleable
|
import androidx.compose.foundation.selection.toggleable
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.ListItem
|
import androidx.compose.material3.ListItem
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.RadioButton
|
import androidx.compose.material3.RadioButton
|
||||||
import androidx.compose.material3.Switch
|
import androidx.compose.material3.Switch
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
@@ -12,10 +13,12 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.alpha
|
import androidx.compose.ui.draw.alpha
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.semantics.Role
|
import androidx.compose.ui.semantics.Role
|
||||||
import com.dergoogler.mmrl.ui.component.LabelItem
|
import com.dergoogler.mmrl.ui.component.LabelItem
|
||||||
import com.dergoogler.mmrl.ui.component.text.TextRow
|
import com.dergoogler.mmrl.ui.component.text.TextRow
|
||||||
|
import com.sukisu.ultra.ui.theme.CardConfig
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SwitchItem(
|
fun SwitchItem(
|
||||||
@@ -25,11 +28,16 @@ fun SwitchItem(
|
|||||||
checked: Boolean,
|
checked: Boolean,
|
||||||
enabled: Boolean = true,
|
enabled: Boolean = true,
|
||||||
beta: Boolean = false,
|
beta: Boolean = false,
|
||||||
onCheckedChange: (Boolean) -> Unit,
|
onCheckedChange: (Boolean) -> Unit
|
||||||
) {
|
) {
|
||||||
val interactionSource = remember { MutableInteractionSource() }
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
val stateAlpha = remember(checked, enabled) { Modifier.alpha(if (enabled) 1f else 0.5f) }
|
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(
|
ListItem(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.toggleable(
|
.toggleable(
|
||||||
@@ -84,6 +92,7 @@ fun SwitchItem(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun RadioItem(
|
fun RadioItem(
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package com.sukisu.ultra.ui.component
|
package com.sukisu.ultra.ui.component
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.horizontalScroll
|
import androidx.compose.foundation.horizontalScroll
|
||||||
@@ -10,12 +9,10 @@ import androidx.compose.material3.*
|
|||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.sukisu.ultra.R
|
import com.sukisu.ultra.R
|
||||||
import com.sukisu.ultra.ui.theme.ThemeConfig
|
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.SdStorage
|
import androidx.compose.material.icons.filled.SdStorage
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
@@ -32,13 +29,19 @@ fun SlotSelectionDialog(
|
|||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
onSlotSelected: (String) -> Unit
|
onSlotSelected: (String) -> Unit
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
|
||||||
var currentSlot by remember { mutableStateOf<String?>(null) }
|
var currentSlot by remember { mutableStateOf<String?>(null) }
|
||||||
var errorMessage by remember { mutableStateOf<String?>(null) }
|
var errorMessage by remember { mutableStateOf<String?>(null) }
|
||||||
|
var selectedSlot by remember { mutableStateOf<String?>(null) }
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
try {
|
try {
|
||||||
currentSlot = getCurrentSlot(context)
|
currentSlot = getCurrentSlot()
|
||||||
|
// 设置默认选择为当前槽位
|
||||||
|
selectedSlot = when (currentSlot) {
|
||||||
|
"a" -> "a"
|
||||||
|
"b" -> "b"
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
errorMessage = null
|
errorMessage = null
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
errorMessage = e.message
|
errorMessage = e.message
|
||||||
@@ -103,12 +106,12 @@ fun SlotSelectionDialog(
|
|||||||
val slotOptions = listOf(
|
val slotOptions = listOf(
|
||||||
ListOption(
|
ListOption(
|
||||||
titleText = stringResource(id = R.string.slot_a),
|
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
|
icon = Icons.Filled.SdStorage
|
||||||
),
|
),
|
||||||
ListOption(
|
ListOption(
|
||||||
titleText = stringResource(id = R.string.slot_b),
|
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
|
icon = Icons.Filled.SdStorage
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -124,19 +127,20 @@ fun SlotSelectionDialog(
|
|||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clip(MaterialTheme.shapes.medium)
|
.clip(MaterialTheme.shapes.medium)
|
||||||
.background(
|
.background(
|
||||||
color = if (option.subtitleText != null) {
|
color = if (selectedSlot == when(index) {
|
||||||
|
0 -> "a"
|
||||||
|
else -> "b"
|
||||||
|
}) {
|
||||||
MaterialTheme.colorScheme.primary.copy(alpha = 0.9f)
|
MaterialTheme.colorScheme.primary.copy(alpha = 0.9f)
|
||||||
} else {
|
} else {
|
||||||
MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f)
|
MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.clickable {
|
.clickable {
|
||||||
onSlotSelected(
|
selectedSlot = when(index) {
|
||||||
when (index) {
|
|
||||||
0 -> "a"
|
0 -> "a"
|
||||||
else -> "b"
|
else -> "b"
|
||||||
}
|
}
|
||||||
)
|
|
||||||
}
|
}
|
||||||
.padding(vertical = 12.dp, horizontal = 16.dp),
|
.padding(vertical = 12.dp, horizontal = 16.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
@@ -144,7 +148,10 @@ fun SlotSelectionDialog(
|
|||||||
Icon(
|
Icon(
|
||||||
imageVector = option.icon,
|
imageVector = option.icon,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
tint = if (option.subtitleText != null) {
|
tint = if (selectedSlot == when(index) {
|
||||||
|
0 -> "a"
|
||||||
|
else -> "b"
|
||||||
|
}) {
|
||||||
MaterialTheme.colorScheme.onPrimary
|
MaterialTheme.colorScheme.onPrimary
|
||||||
} else {
|
} else {
|
||||||
MaterialTheme.colorScheme.primary
|
MaterialTheme.colorScheme.primary
|
||||||
@@ -159,7 +166,10 @@ fun SlotSelectionDialog(
|
|||||||
Text(
|
Text(
|
||||||
text = option.titleText,
|
text = option.titleText,
|
||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
color = if (option.subtitleText != null) {
|
color = if (selectedSlot == when(index) {
|
||||||
|
0 -> "a"
|
||||||
|
else -> "b"
|
||||||
|
}) {
|
||||||
MaterialTheme.colorScheme.onPrimary
|
MaterialTheme.colorScheme.onPrimary
|
||||||
} else {
|
} else {
|
||||||
MaterialTheme.colorScheme.primary
|
MaterialTheme.colorScheme.primary
|
||||||
@@ -169,7 +179,10 @@ fun SlotSelectionDialog(
|
|||||||
Text(
|
Text(
|
||||||
text = it,
|
text = it,
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
color = if (true) {
|
color = if (selectedSlot == when(index) {
|
||||||
|
0 -> "a"
|
||||||
|
else -> "b"
|
||||||
|
}) {
|
||||||
MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.8f)
|
MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.8f)
|
||||||
} else {
|
} else {
|
||||||
MaterialTheme.colorScheme.onSurfaceVariant
|
MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
@@ -186,9 +199,10 @@ fun SlotSelectionDialog(
|
|||||||
confirmButton = {
|
confirmButton = {
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
currentSlot?.let { onSlotSelected(it) }
|
selectedSlot?.let { onSlotSelected(it) }
|
||||||
onDismiss()
|
onDismiss()
|
||||||
}
|
},
|
||||||
|
enabled = selectedSlot != null
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(android.R.string.ok),
|
text = stringResource(android.R.string.ok),
|
||||||
@@ -221,7 +235,7 @@ data class ListOption(
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Utility function to get current slot
|
// 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 {
|
return runCommandGetOutput(true, "getprop ro.boot.slot_suffix")?.let {
|
||||||
if (it.startsWith("_")) it.substring(1) else it
|
if (it.startsWith("_")) it.substring(1) else it
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,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)) }
|
onValueChange = { onProfileChange(profile.copy(name = it)) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
SwitchItem(
|
SwitchItem(
|
||||||
title = stringResource(R.string.profile_umount_modules),
|
title = stringResource(R.string.profile_umount_modules),
|
||||||
summary = stringResource(R.string.profile_umount_modules_summary),
|
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.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.input.ImeAction
|
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.Capabilities
|
||||||
import com.sukisu.ultra.profile.Groups
|
import com.sukisu.ultra.profile.Groups
|
||||||
import com.sukisu.ultra.ui.component.rememberCustomDialog
|
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
|
import com.sukisu.ultra.ui.util.isSepolicyValid
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@@ -206,6 +209,13 @@ fun GroupsPanel(selected: List<Groups>, closeSelection: (selection: Set<Groups>)
|
|||||||
}
|
}
|
||||||
|
|
||||||
val selection = HashSet(selected)
|
val selection = HashSet(selected)
|
||||||
|
val backgroundColor = if (CardConfig.isCustomBackgroundEnabled) Color.Transparent else MaterialTheme.colorScheme.surfaceContainerHigh
|
||||||
|
|
||||||
|
MaterialTheme(
|
||||||
|
colorScheme = MaterialTheme.colorScheme.copy(
|
||||||
|
surface = backgroundColor
|
||||||
|
)
|
||||||
|
) {
|
||||||
ListDialog(
|
ListDialog(
|
||||||
state = rememberUseCaseState(visible = true, onFinishedRequest = {
|
state = rememberUseCaseState(visible = true, onFinishedRequest = {
|
||||||
closeSelection(selection)
|
closeSelection(selection)
|
||||||
@@ -229,6 +239,7 @@ fun GroupsPanel(selected: List<Groups>, closeSelection: (selection: Set<Groups>)
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
OutlinedCard(
|
OutlinedCard(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -278,6 +289,13 @@ fun CapsPanel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val selection = HashSet(selected)
|
val selection = HashSet(selected)
|
||||||
|
val backgroundColor = if (CardConfig.isCustomBackgroundEnabled) Color.Transparent else MaterialTheme.colorScheme.surfaceContainerHigh
|
||||||
|
|
||||||
|
MaterialTheme(
|
||||||
|
colorScheme = MaterialTheme.colorScheme.copy(
|
||||||
|
surface = backgroundColor
|
||||||
|
)
|
||||||
|
) {
|
||||||
ListDialog(
|
ListDialog(
|
||||||
state = rememberUseCaseState(visible = true, onFinishedRequest = {
|
state = rememberUseCaseState(visible = true, onFinishedRequest = {
|
||||||
closeSelection(selection)
|
closeSelection(selection)
|
||||||
@@ -300,6 +318,7 @@ fun CapsPanel(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
OutlinedCard(
|
OutlinedCard(
|
||||||
modifier = Modifier
|
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(
|
InputDialog(
|
||||||
state = rememberUseCaseState(visible = true,
|
state = rememberUseCaseState(
|
||||||
|
visible = true,
|
||||||
onFinishedRequest = {
|
onFinishedRequest = {
|
||||||
onSELinuxChange(domain, rules)
|
onSELinuxChange(domain, rules)
|
||||||
},
|
},
|
||||||
@@ -444,6 +471,7 @@ private fun SELinuxPanel(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ListItem(headlineContent = {
|
ListItem(headlineContent = {
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
|
|||||||
@@ -37,7 +37,6 @@ import androidx.compose.material3.FilterChip
|
|||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.IconButtonDefaults
|
|
||||||
import androidx.compose.material3.ListItem
|
import androidx.compose.material3.ListItem
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Scaffold
|
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.alpha
|
||||||
import androidx.compose.ui.draw.shadow
|
import androidx.compose.ui.draw.shadow
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
import androidx.compose.ui.platform.LocalContext
|
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.RootProfileConfig
|
||||||
import com.sukisu.ultra.ui.component.profile.TemplateConfig
|
import com.sukisu.ultra.ui.component.profile.TemplateConfig
|
||||||
import com.sukisu.ultra.ui.theme.CardConfig
|
import com.sukisu.ultra.ui.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.LocalSnackbarHost
|
||||||
import com.sukisu.ultra.ui.util.forceStopApp
|
import com.sukisu.ultra.ui.util.forceStopApp
|
||||||
import com.sukisu.ultra.ui.util.getSepolicy
|
import com.sukisu.ultra.ui.util.getSepolicy
|
||||||
@@ -122,7 +125,12 @@ fun AppProfileScreen(
|
|||||||
mutableStateOf(initialProfile)
|
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
|
val cardAlpha = CardConfig.cardAlpha
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
@@ -203,13 +211,21 @@ private fun AppProfileInner(
|
|||||||
onProfileChange: (Natives.Profile) -> Unit,
|
onProfileChange: (Natives.Profile) -> Unit,
|
||||||
) {
|
) {
|
||||||
val isRootGranted = profile.allowSu
|
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) {
|
Column(modifier = modifier) {
|
||||||
ElevatedCard(
|
ElevatedCard(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||||
shape = MaterialTheme.shapes.medium
|
shape = MaterialTheme.shapes.medium,
|
||||||
|
colors = cardColors,
|
||||||
|
elevation = getCardElevation(),
|
||||||
) {
|
) {
|
||||||
AppMenuBox(packageName) {
|
AppMenuBox(packageName) {
|
||||||
ListItem(
|
ListItem(
|
||||||
@@ -235,7 +251,9 @@ private fun AppProfileInner(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||||
shape = MaterialTheme.shapes.medium
|
shape = MaterialTheme.shapes.medium,
|
||||||
|
colors = cardColors,
|
||||||
|
elevation = getCardElevation(),
|
||||||
) {
|
) {
|
||||||
SwitchItem(
|
SwitchItem(
|
||||||
icon = Icons.Filled.Security,
|
icon = Icons.Filled.Security,
|
||||||
@@ -268,7 +286,9 @@ private fun AppProfileInner(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||||
shape = MaterialTheme.shapes.medium
|
shape = MaterialTheme.shapes.medium,
|
||||||
|
colors = cardColors,
|
||||||
|
elevation = getCardElevation(),
|
||||||
) {
|
) {
|
||||||
ProfileBox(mode, true) {
|
ProfileBox(mode, true) {
|
||||||
// template mode shouldn't change profile here!
|
// template mode shouldn't change profile here!
|
||||||
@@ -288,10 +308,15 @@ private fun AppProfileInner(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
.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)) {
|
Column(modifier = Modifier.padding(vertical = 8.dp)) {
|
||||||
Crossfade(targetState = mode, label = "ProfileMode") { currentMode ->
|
Crossfade(
|
||||||
|
targetState = mode,
|
||||||
|
label = "ProfileMode"
|
||||||
|
) { currentMode ->
|
||||||
when (currentMode) {
|
when (currentMode) {
|
||||||
Mode.Template -> {
|
Mode.Template -> {
|
||||||
TemplateConfig(
|
TemplateConfig(
|
||||||
@@ -301,6 +326,7 @@ private fun AppProfileInner(
|
|||||||
onProfileChange = onProfileChange
|
onProfileChange = onProfileChange
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Mode.Custom -> {
|
Mode.Custom -> {
|
||||||
RootProfileConfig(
|
RootProfileConfig(
|
||||||
fixedName = true,
|
fixedName = true,
|
||||||
@@ -308,6 +334,7 @@ private fun AppProfileInner(
|
|||||||
onProfileChange = onProfileChange
|
onProfileChange = onProfileChange
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {}
|
else -> {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -321,7 +348,9 @@ private fun AppProfileInner(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||||
shape = MaterialTheme.shapes.medium
|
shape = MaterialTheme.shapes.medium,
|
||||||
|
colors = cardColors,
|
||||||
|
elevation = getCardElevation(),
|
||||||
) {
|
) {
|
||||||
ProfileBox(mode, false) {
|
ProfileBox(mode, false) {
|
||||||
onProfileChange(profile.copy(nonRootUseDefault = (it == Mode.Default)))
|
onProfileChange(profile.copy(nonRootUseDefault = (it == Mode.Default)))
|
||||||
@@ -337,7 +366,9 @@ private fun AppProfileInner(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
.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)) {
|
Column(modifier = Modifier.padding(vertical = 8.dp)) {
|
||||||
AppProfileConfig(
|
AppProfileConfig(
|
||||||
@@ -354,6 +385,7 @@ private fun AppProfileInner(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private enum class Mode(@StringRes private val res: Int) {
|
private enum class Mode(@StringRes private val res: Int) {
|
||||||
Default(R.string.profile_default), Template(R.string.profile_template), Custom(R.string.profile_custom);
|
Default(R.string.profile_default), Template(R.string.profile_template), Custom(R.string.profile_custom);
|
||||||
@@ -377,12 +409,10 @@ private fun TopBar(
|
|||||||
Text(
|
Text(
|
||||||
text = title,
|
text = title,
|
||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
color = MaterialTheme.colorScheme.onSurface
|
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = packageName,
|
text = packageName,
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
modifier = Modifier.alpha(0.8f)
|
modifier = Modifier.alpha(0.8f)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -391,9 +421,6 @@ private fun TopBar(
|
|||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = onBack,
|
onClick = onBack,
|
||||||
colors = IconButtonDefaults.iconButtonColors(
|
|
||||||
contentColor = MaterialTheme.colorScheme.onSurface
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||||
@@ -408,7 +435,6 @@ private fun TopBar(
|
|||||||
modifier = Modifier.shadow(
|
modifier = Modifier.shadow(
|
||||||
elevation = if ((scrollBehavior?.state?.overlappedFraction ?: 0f) > 0.01f)
|
elevation = if ((scrollBehavior?.state?.overlappedFraction ?: 0f) > 0.01f)
|
||||||
4.dp else 0.dp,
|
4.dp else 0.dp,
|
||||||
spotColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f)
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -431,7 +457,6 @@ private fun ProfileBox(
|
|||||||
Text(
|
Text(
|
||||||
text = mode.text,
|
text = mode.text,
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
leadingContent = {
|
leadingContent = {
|
||||||
@@ -444,7 +469,6 @@ private fun ProfileBox(
|
|||||||
|
|
||||||
HorizontalDivider(
|
HorizontalDivider(
|
||||||
thickness = Dp.Hairline,
|
thickness = Dp.Hairline,
|
||||||
color = MaterialTheme.colorScheme.outlineVariant
|
|
||||||
)
|
)
|
||||||
|
|
||||||
ListItem(
|
ListItem(
|
||||||
@@ -574,6 +598,11 @@ private fun AppMenuOption(text: String, onClick: () -> Unit) {
|
|||||||
@Composable
|
@Composable
|
||||||
private fun AppProfilePreview() {
|
private fun AppProfilePreview() {
|
||||||
var profile by remember { mutableStateOf(Natives.Profile("")) }
|
var profile by remember { mutableStateOf(Natives.Profile("")) }
|
||||||
|
MaterialTheme(
|
||||||
|
colorScheme = MaterialTheme.colorScheme.copy(
|
||||||
|
surface = if (CardConfig.isCustomBackgroundEnabled) Color.Transparent else MaterialTheme.colorScheme.surfaceContainerHigh
|
||||||
|
)
|
||||||
|
) {
|
||||||
Surface {
|
Surface {
|
||||||
AppProfileInner(
|
AppProfileInner(
|
||||||
packageName = "icu.nullptr.test",
|
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.annotation.StringRes
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.*
|
import androidx.compose.material.icons.filled.*
|
||||||
|
import androidx.compose.material.icons.filled.Archive
|
||||||
import androidx.compose.material.icons.outlined.*
|
import androidx.compose.material.icons.outlined.*
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import com.ramcosta.composedestinations.generated.destinations.HomeScreenDestination
|
import com.ramcosta.composedestinations.generated.destinations.HomeScreenDestination
|
||||||
@@ -21,8 +22,8 @@ enum class BottomBarDestination(
|
|||||||
val rootRequired: Boolean,
|
val rootRequired: Boolean,
|
||||||
) {
|
) {
|
||||||
Home(HomeScreenDestination, R.string.home, Icons.Filled.Home, Icons.Outlined.Home, false),
|
Home(HomeScreenDestination, R.string.home, Icons.Filled.Home, Icons.Outlined.Home, false),
|
||||||
Kpm(KpmScreenDestination, R.string.kpm_title, Icons.Filled.Build, Icons.Outlined.Build, true),
|
Kpm(KpmScreenDestination, R.string.kpm_title, Icons.Filled.Archive, Icons.Outlined.Archive, true),
|
||||||
SuperUser(SuperUserScreenDestination, R.string.superuser, Icons.Filled.Security, Icons.Outlined.Security, true),
|
SuperUser(SuperUserScreenDestination, R.string.superuser, Icons.Filled.AdminPanelSettings, Icons.Outlined.AdminPanelSettings, true),
|
||||||
Module(ModuleScreenDestination, R.string.module, Icons.Filled.Apps, Icons.Outlined.Apps, true),
|
Module(ModuleScreenDestination, R.string.module, Icons.Filled.Extension, Icons.Outlined.Extension, true),
|
||||||
Settings(SettingScreenDestination, R.string.settings, Icons.Filled.Settings, Icons.Outlined.Settings, false),
|
Settings(SettingScreenDestination, R.string.settings, Icons.Filled.Settings, Icons.Outlined.Settings, false),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,22 @@
|
|||||||
package com.sukisu.ultra.ui.screen
|
package com.sukisu.ultra.ui.screen
|
||||||
|
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
|
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||||
|
import androidx.compose.foundation.layout.defaultMinSize
|
||||||
|
import androidx.compose.foundation.layout.safeDrawing
|
||||||
|
import androidx.compose.foundation.layout.only
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
import androidx.compose.material.icons.filled.Close
|
||||||
import androidx.compose.material.icons.filled.Save
|
import androidx.compose.material.icons.filled.Save
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.ExtendedFloatingActionButton
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
@@ -30,7 +37,6 @@ import androidx.compose.ui.input.key.key
|
|||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.compose.dropUnlessResumed
|
|
||||||
import com.ramcosta.composedestinations.annotation.Destination
|
import com.ramcosta.composedestinations.annotation.Destination
|
||||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||||
@@ -55,7 +61,11 @@ fun ExecuteModuleActionScreen(navigator: DestinationsNavigator, moduleId: String
|
|||||||
val snackBarHost = LocalSnackbarHost.current
|
val snackBarHost = LocalSnackbarHost.current
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val scrollState = rememberScrollState()
|
val scrollState = rememberScrollState()
|
||||||
var actionResult: Boolean
|
var isActionRunning by rememberSaveable { mutableStateOf(true) }
|
||||||
|
|
||||||
|
BackHandler(enabled = isActionRunning) {
|
||||||
|
// Disable back button if action is running
|
||||||
|
}
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
if (text.isNotEmpty()) {
|
if (text.isNotEmpty()) {
|
||||||
@@ -76,20 +86,17 @@ fun ExecuteModuleActionScreen(navigator: DestinationsNavigator, moduleId: String
|
|||||||
onStderr = {
|
onStderr = {
|
||||||
logContent.append(it).append("\n")
|
logContent.append(it).append("\n")
|
||||||
}
|
}
|
||||||
).let {
|
)
|
||||||
actionResult = it
|
|
||||||
}
|
}
|
||||||
}
|
isActionRunning = false
|
||||||
if (actionResult) navigator.popBackStack()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
TopBar(
|
TopBar(
|
||||||
onBack = dropUnlessResumed {
|
isActionRunning = isActionRunning,
|
||||||
navigator.popBackStack()
|
|
||||||
},
|
|
||||||
onSave = {
|
onSave = {
|
||||||
|
if (!isActionRunning) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
val format = SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.getDefault())
|
val format = SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.getDefault())
|
||||||
val date = format.format(Date())
|
val date = format.format(Date())
|
||||||
@@ -101,8 +108,21 @@ fun ExecuteModuleActionScreen(navigator: DestinationsNavigator, moduleId: String
|
|||||||
snackBarHost.showSnackbar("Log saved to ${file.absolutePath}")
|
snackBarHost.showSnackbar("Log saved to ${file.absolutePath}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
floatingActionButton = {
|
||||||
|
if (!isActionRunning) {
|
||||||
|
ExtendedFloatingActionButton(
|
||||||
|
text = { Text(text = stringResource(R.string.close)) },
|
||||||
|
icon = { Icon(Icons.Filled.Close, contentDescription = null) },
|
||||||
|
onClick = {
|
||||||
|
navigator.popBackStack()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
contentWindowInsets = WindowInsets.safeDrawing,
|
||||||
snackbarHost = { SnackbarHost(snackBarHost) }
|
snackbarHost = { SnackbarHost(snackBarHost) }
|
||||||
) { innerPadding ->
|
) { innerPadding ->
|
||||||
KeyEventBlocker {
|
KeyEventBlocker {
|
||||||
@@ -130,16 +150,14 @@ fun ExecuteModuleActionScreen(navigator: DestinationsNavigator, moduleId: String
|
|||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
private fun TopBar(onBack: () -> Unit = {}, onSave: () -> Unit = {}) {
|
private fun TopBar(isActionRunning: Boolean, onSave: () -> Unit = {}) {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = { Text(stringResource(R.string.action)) },
|
title = { Text(stringResource(R.string.action)) },
|
||||||
navigationIcon = {
|
|
||||||
IconButton(
|
|
||||||
onClick = onBack
|
|
||||||
) { Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null) }
|
|
||||||
},
|
|
||||||
actions = {
|
actions = {
|
||||||
IconButton(onClick = onSave) {
|
IconButton(
|
||||||
|
onClick = onSave,
|
||||||
|
enabled = !isActionRunning
|
||||||
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Filled.Save,
|
imageVector = Icons.Filled.Save,
|
||||||
contentDescription = stringResource(id = R.string.save_log),
|
contentDescription = stringResource(id = R.string.save_log),
|
||||||
|
|||||||
@@ -3,27 +3,40 @@ package com.sukisu.ultra.ui.screen
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
|
import androidx.activity.compose.BackHandler
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
|
import androidx.compose.animation.expandVertically
|
||||||
|
import androidx.compose.animation.fadeIn
|
||||||
|
import androidx.compose.animation.fadeOut
|
||||||
|
import androidx.compose.animation.shrinkVertically
|
||||||
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
|
import androidx.compose.material.icons.filled.Error
|
||||||
import androidx.compose.material.icons.filled.Refresh
|
import androidx.compose.material.icons.filled.Refresh
|
||||||
import androidx.compose.material.icons.filled.Save
|
import androidx.compose.material.icons.filled.Save
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.input.key.Key
|
import androidx.compose.ui.input.key.Key
|
||||||
import androidx.compose.ui.input.key.key
|
import androidx.compose.ui.input.key.key
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.compose.dropUnlessResumed
|
|
||||||
import com.ramcosta.composedestinations.annotation.Destination
|
import com.ramcosta.composedestinations.annotation.Destination
|
||||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||||
|
import com.ramcosta.composedestinations.generated.destinations.FlashScreenDestination
|
||||||
|
import com.ramcosta.composedestinations.generated.destinations.ModuleScreenDestination
|
||||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||||
import com.ramcosta.composedestinations.navigation.EmptyDestinationsNavigator
|
import com.ramcosta.composedestinations.navigation.EmptyDestinationsNavigator
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -33,10 +46,17 @@ import kotlinx.parcelize.Parcelize
|
|||||||
import com.sukisu.ultra.ui.component.KeyEventBlocker
|
import com.sukisu.ultra.ui.component.KeyEventBlocker
|
||||||
import com.sukisu.ultra.ui.util.*
|
import com.sukisu.ultra.ui.util.*
|
||||||
import com.sukisu.ultra.R
|
import com.sukisu.ultra.R
|
||||||
|
import com.sukisu.ultra.ui.theme.CardConfig
|
||||||
|
import com.sukisu.ultra.ui.viewmodel.ModuleViewModel
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author ShirkNeko
|
||||||
|
* @date 2025/5/31.
|
||||||
|
*/
|
||||||
enum class FlashingStatus {
|
enum class FlashingStatus {
|
||||||
FLASHING,
|
FLASHING,
|
||||||
SUCCESS,
|
SUCCESS,
|
||||||
@@ -45,14 +65,47 @@ enum class FlashingStatus {
|
|||||||
|
|
||||||
private var currentFlashingStatus = mutableStateOf(FlashingStatus.FLASHING)
|
private var currentFlashingStatus = mutableStateOf(FlashingStatus.FLASHING)
|
||||||
|
|
||||||
|
// 添加模块安装状态跟踪
|
||||||
|
data class ModuleInstallStatus(
|
||||||
|
val totalModules: Int = 0,
|
||||||
|
val currentModule: Int = 0,
|
||||||
|
val currentModuleName: String = "",
|
||||||
|
val failedModules: MutableList<String> = mutableListOf()
|
||||||
|
)
|
||||||
|
|
||||||
|
private var moduleInstallStatus = mutableStateOf(ModuleInstallStatus())
|
||||||
|
|
||||||
fun setFlashingStatus(status: FlashingStatus) {
|
fun setFlashingStatus(status: FlashingStatus) {
|
||||||
currentFlashingStatus.value = status
|
currentFlashingStatus.value = status
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun updateModuleInstallStatus(
|
||||||
|
totalModules: Int? = null,
|
||||||
|
currentModule: Int? = null,
|
||||||
|
currentModuleName: String? = null,
|
||||||
|
failedModule: String? = null
|
||||||
|
) {
|
||||||
|
val current = moduleInstallStatus.value
|
||||||
|
moduleInstallStatus.value = current.copy(
|
||||||
|
totalModules = totalModules ?: current.totalModules,
|
||||||
|
currentModule = currentModule ?: current.currentModule,
|
||||||
|
currentModuleName = currentModuleName ?: current.currentModuleName
|
||||||
|
)
|
||||||
|
|
||||||
|
if (failedModule != null) {
|
||||||
|
val updatedFailedModules = current.failedModules.toMutableList()
|
||||||
|
updatedFailedModules.add(failedModule)
|
||||||
|
moduleInstallStatus.value = moduleInstallStatus.value.copy(
|
||||||
|
failedModules = updatedFailedModules
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
@Destination<RootGraph>
|
@Destination<RootGraph>
|
||||||
fun FlashScreen(navigator: DestinationsNavigator, flashIt: FlashIt) {
|
fun FlashScreen(navigator: DestinationsNavigator, flashIt: FlashIt) {
|
||||||
|
val context = LocalContext.current
|
||||||
var text by rememberSaveable { mutableStateOf("") }
|
var text by rememberSaveable { mutableStateOf("") }
|
||||||
var tempText: String
|
var tempText: String
|
||||||
val logContent = rememberSaveable { StringBuilder() }
|
val logContent = rememberSaveable { StringBuilder() }
|
||||||
@@ -62,6 +115,25 @@ fun FlashScreen(navigator: DestinationsNavigator, flashIt: FlashIt) {
|
|||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val scrollState = rememberScrollState()
|
val scrollState = rememberScrollState()
|
||||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||||
|
val viewModel: ModuleViewModel = viewModel()
|
||||||
|
|
||||||
|
val errorCodeString = stringResource(R.string.error_code)
|
||||||
|
val checkLogString = stringResource(R.string.check_log)
|
||||||
|
val logSavedString = stringResource(R.string.log_saved)
|
||||||
|
val installingModuleString = stringResource(R.string.installing_module)
|
||||||
|
|
||||||
|
// 当前模块安装状态
|
||||||
|
val currentStatus = moduleInstallStatus.value
|
||||||
|
|
||||||
|
// 重置状态
|
||||||
|
LaunchedEffect(flashIt) {
|
||||||
|
if (flashIt is FlashIt.FlashModules && flashIt.currentIndex == 0) {
|
||||||
|
moduleInstallStatus.value = ModuleInstallStatus(
|
||||||
|
totalModules = flashIt.uris.size,
|
||||||
|
currentModule = 1
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
if (text.isNotEmpty()) {
|
if (text.isNotEmpty()) {
|
||||||
@@ -69,17 +141,50 @@ fun FlashScreen(navigator: DestinationsNavigator, flashIt: FlashIt) {
|
|||||||
}
|
}
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
setFlashingStatus(FlashingStatus.FLASHING)
|
setFlashingStatus(FlashingStatus.FLASHING)
|
||||||
flashIt(flashIt, onFinish = { showReboot, code ->
|
|
||||||
|
if (flashIt is FlashIt.FlashModules) {
|
||||||
|
try {
|
||||||
|
val currentUri = flashIt.uris[flashIt.currentIndex]
|
||||||
|
val moduleName = getModuleNameFromUri(context, currentUri)
|
||||||
|
updateModuleInstallStatus(
|
||||||
|
currentModuleName = moduleName
|
||||||
|
)
|
||||||
|
text = installingModuleString.format(flashIt.currentIndex + 1, flashIt.uris.size, moduleName)
|
||||||
|
logContent.append(text).append("\n")
|
||||||
|
} catch (_: Exception) {
|
||||||
|
text = installingModuleString.format(flashIt.currentIndex + 1, flashIt.uris.size, "Module")
|
||||||
|
logContent.append(text).append("\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
flashIt(context, flashIt, onFinish = { showReboot, code ->
|
||||||
if (code != 0) {
|
if (code != 0) {
|
||||||
text += "Error: exit code = $code.\nPlease save and check the log.\n"
|
text += "$errorCodeString $code.\n$checkLogString\n"
|
||||||
setFlashingStatus(FlashingStatus.FAILED)
|
setFlashingStatus(FlashingStatus.FAILED)
|
||||||
|
|
||||||
|
if (flashIt is FlashIt.FlashModules) {
|
||||||
|
updateModuleInstallStatus(
|
||||||
|
failedModule = moduleInstallStatus.value.currentModuleName
|
||||||
|
)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
setFlashingStatus(FlashingStatus.SUCCESS)
|
setFlashingStatus(FlashingStatus.SUCCESS)
|
||||||
|
viewModel.markNeedRefresh()
|
||||||
}
|
}
|
||||||
if (showReboot) {
|
if (showReboot) {
|
||||||
text += "\n\n\n"
|
text += "\n\n\n"
|
||||||
showFloatAction = true
|
showFloatAction = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (flashIt is FlashIt.FlashModules && flashIt.currentIndex < flashIt.uris.size - 1) {
|
||||||
|
val nextFlashIt = flashIt.copy(
|
||||||
|
currentIndex = flashIt.currentIndex + 1
|
||||||
|
)
|
||||||
|
scope.launch {
|
||||||
|
kotlinx.coroutines.delay(500)
|
||||||
|
navigator.navigate(FlashScreenDestination(nextFlashIt))
|
||||||
|
}
|
||||||
|
}
|
||||||
}, onStdout = {
|
}, onStdout = {
|
||||||
tempText = "$it\n"
|
tempText = "$it\n"
|
||||||
if (tempText.startsWith("[H[J")) { // clear command
|
if (tempText.startsWith("[H[J")) { // clear command
|
||||||
@@ -94,13 +199,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(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
TopBar(
|
TopBar(
|
||||||
currentFlashingStatus.value,
|
currentFlashingStatus.value,
|
||||||
onBack = dropUnlessResumed {
|
currentStatus,
|
||||||
navigator.popBackStack()
|
navigator = navigator,
|
||||||
},
|
flashIt = flashIt,
|
||||||
|
onBack = onBack,
|
||||||
onSave = {
|
onSave = {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
val format = SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.getDefault())
|
val format = SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.getDefault())
|
||||||
@@ -110,7 +234,7 @@ fun FlashScreen(navigator: DestinationsNavigator, flashIt: FlashIt) {
|
|||||||
"KernelSU_install_log_${date}.log"
|
"KernelSU_install_log_${date}.log"
|
||||||
)
|
)
|
||||||
file.writeText(logContent.toString())
|
file.writeText(logContent.toString())
|
||||||
snackBarHost.showSnackbar("Log saved to ${file.absolutePath}")
|
snackBarHost.showSnackbar(logSavedString.format(file.absolutePath))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
scrollBehavior = scrollBehavior
|
scrollBehavior = scrollBehavior
|
||||||
@@ -118,8 +242,6 @@ fun FlashScreen(navigator: DestinationsNavigator, flashIt: FlashIt) {
|
|||||||
},
|
},
|
||||||
floatingActionButton = {
|
floatingActionButton = {
|
||||||
if (showFloatAction) {
|
if (showFloatAction) {
|
||||||
val cardColor = MaterialTheme.colorScheme.secondaryContainer
|
|
||||||
val reboot = stringResource(id = R.string.reboot)
|
|
||||||
ExtendedFloatingActionButton(
|
ExtendedFloatingActionButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
@@ -128,49 +250,288 @@ fun FlashScreen(navigator: DestinationsNavigator, flashIt: FlashIt) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
icon = { Icon(Icons.Filled.Refresh, reboot) },
|
icon = {
|
||||||
text = { Text(text = reboot) },
|
Icon(
|
||||||
containerColor = cardColor.copy(alpha = 1f),
|
Icons.Filled.Refresh,
|
||||||
contentColor = MaterialTheme.colorScheme.onSecondaryContainer
|
contentDescription = stringResource(id = R.string.reboot)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
text = {
|
||||||
|
Text(text = stringResource(id = R.string.reboot))
|
||||||
|
},
|
||||||
|
containerColor = MaterialTheme.colorScheme.secondaryContainer,
|
||||||
|
contentColor = MaterialTheme.colorScheme.onSecondaryContainer,
|
||||||
|
expanded = true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
snackbarHost = { SnackbarHost(hostState = snackBarHost) },
|
snackbarHost = { SnackbarHost(hostState = snackBarHost) },
|
||||||
contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal)
|
contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),
|
||||||
|
containerColor = MaterialTheme.colorScheme.background
|
||||||
) { innerPadding ->
|
) { innerPadding ->
|
||||||
KeyEventBlocker {
|
KeyEventBlocker {
|
||||||
it.key == Key.VolumeDown || it.key == Key.VolumeUp
|
it.key == Key.VolumeDown || it.key == Key.VolumeUp
|
||||||
}
|
}
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize(1f)
|
.fillMaxSize(1f)
|
||||||
.padding(innerPadding)
|
.padding(innerPadding)
|
||||||
.nestedScroll(scrollBehavior.nestedScrollConnection)
|
.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||||
.verticalScroll(scrollState),
|
) {
|
||||||
|
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) {
|
LaunchedEffect(text) {
|
||||||
scrollState.animateScrollTo(scrollState.maxValue)
|
scrollState.animateScrollTo(scrollState.maxValue)
|
||||||
}
|
}
|
||||||
Text(
|
Text(
|
||||||
modifier = Modifier.padding(8.dp),
|
modifier = Modifier.padding(16.dp),
|
||||||
text = text,
|
text = text,
|
||||||
fontSize = MaterialTheme.typography.bodySmall.fontSize,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
fontFamily = FontFamily.Monospace,
|
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
|
@Parcelize
|
||||||
sealed class FlashIt : Parcelable {
|
sealed class FlashIt : Parcelable {
|
||||||
data class FlashBoot(val boot: Uri? = null, val lkm: LkmSelection, val ota: Boolean) : FlashIt()
|
data class FlashBoot(val boot: Uri? = null, val lkm: LkmSelection, val ota: Boolean) : FlashIt()
|
||||||
data class FlashModule(val uri: Uri) : FlashIt()
|
data class FlashModule(val uri: Uri) : FlashIt()
|
||||||
|
data class FlashModules(val uris: List<Uri>, val currentIndex: Int = 0) : FlashIt()
|
||||||
data object FlashRestore : FlashIt()
|
data object FlashRestore : FlashIt()
|
||||||
data object FlashUninstall : FlashIt()
|
data object FlashUninstall : FlashIt()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun flashIt(
|
fun flashIt(
|
||||||
|
context: android.content.Context,
|
||||||
flashIt: FlashIt,
|
flashIt: FlashIt,
|
||||||
onFinish: (Boolean, Int) -> Unit,
|
onFinish: (Boolean, Int) -> Unit,
|
||||||
onStdout: (String) -> Unit,
|
onStdout: (String) -> Unit,
|
||||||
@@ -186,49 +547,22 @@ fun flashIt(
|
|||||||
onStderr
|
onStderr
|
||||||
)
|
)
|
||||||
is FlashIt.FlashModule -> flashModule(flashIt.uri, onFinish, onStdout, onStderr)
|
is FlashIt.FlashModule -> flashModule(flashIt.uri, onFinish, onStdout, onStderr)
|
||||||
|
is FlashIt.FlashModules -> {
|
||||||
|
if (flashIt.uris.isEmpty() || flashIt.currentIndex >= flashIt.uris.size) {
|
||||||
|
onFinish(false, 0)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val currentUri = flashIt.uris[flashIt.currentIndex]
|
||||||
|
onStdout("\n")
|
||||||
|
|
||||||
|
flashModule(currentUri, onFinish, onStdout, onStderr)
|
||||||
|
}
|
||||||
FlashIt.FlashRestore -> restoreBoot(onFinish, onStdout, onStderr)
|
FlashIt.FlashRestore -> restoreBoot(onFinish, onStdout, onStderr)
|
||||||
FlashIt.FlashUninstall -> uninstallPermanently(onFinish, onStdout, onStderr)
|
FlashIt.FlashUninstall -> uninstallPermanently(onFinish, onStdout, onStderr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
|
||||||
private fun TopBar(
|
|
||||||
status: FlashingStatus,
|
|
||||||
onBack: () -> Unit = {},
|
|
||||||
onSave: () -> Unit = {},
|
|
||||||
scrollBehavior: TopAppBarScrollBehavior? = null
|
|
||||||
) {
|
|
||||||
TopAppBar(
|
|
||||||
title = {
|
|
||||||
Text(
|
|
||||||
stringResource(
|
|
||||||
when (status) {
|
|
||||||
FlashingStatus.FLASHING -> R.string.flashing
|
|
||||||
FlashingStatus.SUCCESS -> R.string.flash_success
|
|
||||||
FlashingStatus.FAILED -> R.string.flash_failed
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
navigationIcon = {
|
|
||||||
IconButton(
|
|
||||||
onClick = onBack
|
|
||||||
) { Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null) }
|
|
||||||
},
|
|
||||||
actions = {
|
|
||||||
IconButton(onClick = onSave) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Filled.Save,
|
|
||||||
contentDescription = "Localized description"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),
|
|
||||||
scrollBehavior = scrollBehavior
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Preview
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
fun FlashScreenPreview() {
|
fun FlashScreenPreview() {
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ import androidx.compose.material.icons.filled.FileUpload
|
|||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.ButtonDefaults
|
import androidx.compose.material3.ButtonDefaults
|
||||||
import androidx.compose.material3.CardDefaults
|
|
||||||
import androidx.compose.material3.ElevatedCard
|
import androidx.compose.material3.ElevatedCard
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
@@ -51,7 +50,6 @@ import androidx.compose.material3.TopAppBarDefaults
|
|||||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||||
import androidx.compose.material3.rememberTopAppBarState
|
import androidx.compose.material3.rememberTopAppBarState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.produceState
|
import androidx.compose.runtime.produceState
|
||||||
@@ -61,6 +59,7 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.draw.shadow
|
import androidx.compose.ui.draw.shadow
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
@@ -75,12 +74,10 @@ import com.maxkeppeler.sheets.list.models.ListSelection
|
|||||||
import com.ramcosta.composedestinations.annotation.Destination
|
import com.ramcosta.composedestinations.annotation.Destination
|
||||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||||
import com.ramcosta.composedestinations.generated.destinations.FlashScreenDestination
|
import com.ramcosta.composedestinations.generated.destinations.FlashScreenDestination
|
||||||
|
import com.ramcosta.composedestinations.generated.destinations.KernelFlashScreenDestination
|
||||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||||
import com.ramcosta.composedestinations.navigation.EmptyDestinationsNavigator
|
import com.ramcosta.composedestinations.navigation.EmptyDestinationsNavigator
|
||||||
import com.sukisu.ultra.R
|
import com.sukisu.ultra.R
|
||||||
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.DialogHandle
|
||||||
import com.sukisu.ultra.ui.component.SlotSelectionDialog
|
import com.sukisu.ultra.ui.component.SlotSelectionDialog
|
||||||
import com.sukisu.ultra.ui.component.rememberConfirmDialog
|
import com.sukisu.ultra.ui.component.rememberConfirmDialog
|
||||||
@@ -95,10 +92,12 @@ import com.sukisu.ultra.ui.util.isAbDevice
|
|||||||
import com.sukisu.ultra.ui.util.isInitBoot
|
import com.sukisu.ultra.ui.util.isInitBoot
|
||||||
import com.sukisu.ultra.ui.util.rootAvailable
|
import com.sukisu.ultra.ui.util.rootAvailable
|
||||||
import com.sukisu.ultra.getKernelVersion
|
import com.sukisu.ultra.getKernelVersion
|
||||||
|
import com.sukisu.ultra.ui.theme.CardConfig
|
||||||
|
import com.sukisu.ultra.ui.theme.getCardElevation
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author weishu
|
* @author ShirkNeko
|
||||||
* @date 2024/3/12.
|
* @date 2025/5/31.
|
||||||
*/
|
*/
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Destination<RootGraph>
|
@Destination<RootGraph>
|
||||||
@@ -110,16 +109,10 @@ fun InstallScreen(navigator: DestinationsNavigator) {
|
|||||||
var showRebootDialog by remember { mutableStateOf(false) }
|
var showRebootDialog by remember { mutableStateOf(false) }
|
||||||
var showSlotSelectionDialog by remember { mutableStateOf(false) }
|
var showSlotSelectionDialog by remember { mutableStateOf(false) }
|
||||||
var tempKernelUri by remember { mutableStateOf<Uri?>(null) }
|
var tempKernelUri by remember { mutableStateOf<Uri?>(null) }
|
||||||
val horizonKernelState = remember { HorizonKernelState() }
|
|
||||||
val flashState by horizonKernelState.state.collectAsState()
|
|
||||||
val summary = stringResource(R.string.horizon_kernel_summary)
|
|
||||||
val kernelVersion = getKernelVersion()
|
val kernelVersion = getKernelVersion()
|
||||||
val isGKI = kernelVersion.isGKI()
|
val isGKI = kernelVersion.isGKI()
|
||||||
val isAbDevice = isAbDevice()
|
val isAbDevice = isAbDevice()
|
||||||
|
val summary = stringResource(R.string.horizon_kernel_summary)
|
||||||
val onFlashComplete = {
|
|
||||||
showRebootDialog = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showRebootDialog) {
|
if (showRebootDialog) {
|
||||||
RebootDialog(
|
RebootDialog(
|
||||||
@@ -145,14 +138,12 @@ fun InstallScreen(navigator: DestinationsNavigator) {
|
|||||||
when (method) {
|
when (method) {
|
||||||
is InstallMethod.HorizonKernel -> {
|
is InstallMethod.HorizonKernel -> {
|
||||||
method.uri?.let { uri ->
|
method.uri?.let { uri ->
|
||||||
val worker = HorizonKernelWorker(
|
navigator.navigate(
|
||||||
context = context,
|
KernelFlashScreenDestination(
|
||||||
state = horizonKernelState,
|
kernelUri = uri,
|
||||||
slot = method.slot
|
selectedSlot = method.slot
|
||||||
|
)
|
||||||
)
|
)
|
||||||
worker.uri = uri
|
|
||||||
worker.setOnFlashCompleteListener(onFlashComplete)
|
|
||||||
worker.start()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
@@ -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())
|
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||||
|
|
||||||
@@ -224,7 +200,6 @@ fun InstallScreen(navigator: DestinationsNavigator) {
|
|||||||
topBar = {
|
topBar = {
|
||||||
TopBar(
|
TopBar(
|
||||||
onBack = { navigator.popBackStack() },
|
onBack = { navigator.popBackStack() },
|
||||||
onLkmUpload = onLkmUpload,
|
|
||||||
scrollBehavior = scrollBehavior
|
scrollBehavior = scrollBehavior
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -241,7 +216,6 @@ fun InstallScreen(navigator: DestinationsNavigator) {
|
|||||||
) {
|
) {
|
||||||
SelectInstallMethod(
|
SelectInstallMethod(
|
||||||
isGKI = isGKI,
|
isGKI = isGKI,
|
||||||
isAbDevice = isAbDevice,
|
|
||||||
onSelected = { method ->
|
onSelected = { method ->
|
||||||
if (method is InstallMethod.HorizonKernel && method.uri != null) {
|
if (method is InstallMethod.HorizonKernel && method.uri != null) {
|
||||||
if (isAbDevice) {
|
if (isAbDevice) {
|
||||||
@@ -253,18 +227,9 @@ fun InstallScreen(navigator: DestinationsNavigator) {
|
|||||||
} else {
|
} else {
|
||||||
installMethod = method
|
installMethod = method
|
||||||
}
|
}
|
||||||
horizonKernelState.reset()
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
AnimatedVisibility(
|
|
||||||
visible = flashState.isFlashing && installMethod is InstallMethod.HorizonKernel,
|
|
||||||
enter = fadeIn() + expandVertically(),
|
|
||||||
exit = fadeOut() + shrinkVertically()
|
|
||||||
) {
|
|
||||||
HorizonKernelFlashProgress(flashState)
|
|
||||||
}
|
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -273,7 +238,7 @@ fun InstallScreen(navigator: DestinationsNavigator) {
|
|||||||
(lkmSelection as? LkmSelection.LkmUri)?.let {
|
(lkmSelection as? LkmSelection.LkmUri)?.let {
|
||||||
ElevatedCard(
|
ElevatedCard(
|
||||||
colors = getCardColors(MaterialTheme.colorScheme.surfaceVariant),
|
colors = getCardColors(MaterialTheme.colorScheme.surfaceVariant),
|
||||||
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation),
|
elevation = getCardElevation(),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(bottom = 12.dp)
|
.padding(bottom = 12.dp)
|
||||||
@@ -299,7 +264,7 @@ fun InstallScreen(navigator: DestinationsNavigator) {
|
|||||||
if (method.slot != null) {
|
if (method.slot != null) {
|
||||||
ElevatedCard(
|
ElevatedCard(
|
||||||
colors = getCardColors(MaterialTheme.colorScheme.surfaceVariant),
|
colors = getCardColors(MaterialTheme.colorScheme.surfaceVariant),
|
||||||
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation),
|
elevation = getCardElevation(),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(bottom = 12.dp)
|
.padding(bottom = 12.dp)
|
||||||
@@ -325,7 +290,7 @@ fun InstallScreen(navigator: DestinationsNavigator) {
|
|||||||
|
|
||||||
Button(
|
Button(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
enabled = installMethod != null && !flashState.isFlashing,
|
enabled = installMethod != null,
|
||||||
onClick = onClickNext,
|
onClick = onClickNext,
|
||||||
shape = MaterialTheme.shapes.medium,
|
shape = MaterialTheme.shapes.medium,
|
||||||
colors = ButtonDefaults.buttonColors(
|
colors = ButtonDefaults.buttonColors(
|
||||||
@@ -401,7 +366,6 @@ sealed class InstallMethod {
|
|||||||
@Composable
|
@Composable
|
||||||
private fun SelectInstallMethod(
|
private fun SelectInstallMethod(
|
||||||
isGKI: Boolean = false,
|
isGKI: Boolean = false,
|
||||||
isAbDevice: Boolean = false,
|
|
||||||
onSelected: (InstallMethod) -> Unit = {}
|
onSelected: (InstallMethod) -> Unit = {}
|
||||||
) {
|
) {
|
||||||
val rootAvailable = rootAvailable()
|
val rootAvailable = rootAvailable()
|
||||||
@@ -488,8 +452,8 @@ private fun SelectInstallMethod(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var LKMExpanded by remember { mutableStateOf(false) }
|
var lkmExpanded by remember { mutableStateOf(false) }
|
||||||
var GKIExpanded by remember { mutableStateOf(false) }
|
var gkiExpanded by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.padding(horizontal = 16.dp)
|
modifier = Modifier.padding(horizontal = 16.dp)
|
||||||
@@ -498,15 +462,15 @@ private fun SelectInstallMethod(
|
|||||||
if (isGKI) {
|
if (isGKI) {
|
||||||
ElevatedCard(
|
ElevatedCard(
|
||||||
colors = getCardColors(MaterialTheme.colorScheme.surfaceVariant),
|
colors = getCardColors(MaterialTheme.colorScheme.surfaceVariant),
|
||||||
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation),
|
elevation = getCardElevation(),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(bottom = 12.dp)
|
.padding(bottom = 16.dp)
|
||||||
.clip(MaterialTheme.shapes.large)
|
.clip(MaterialTheme.shapes.large)
|
||||||
.shadow(
|
) {
|
||||||
elevation = cardElevation,
|
MaterialTheme(
|
||||||
shape = MaterialTheme.shapes.large,
|
colorScheme = MaterialTheme.colorScheme.copy(
|
||||||
spotColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
|
surface = if (CardConfig.isCustomBackgroundEnabled) Color.Transparent else MaterialTheme.colorScheme.surfaceVariant
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
ListItem(
|
ListItem(
|
||||||
@@ -524,12 +488,13 @@ private fun SelectInstallMethod(
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
modifier = Modifier.clickable {
|
modifier = Modifier.clickable {
|
||||||
LKMExpanded = !LKMExpanded
|
lkmExpanded = !lkmExpanded
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = LKMExpanded,
|
visible = lkmExpanded,
|
||||||
enter = fadeIn() + expandVertically(),
|
enter = fadeIn() + expandVertically(),
|
||||||
exit = shrinkVertically() + fadeOut()
|
exit = shrinkVertically() + fadeOut()
|
||||||
) {
|
) {
|
||||||
@@ -604,15 +569,15 @@ private fun SelectInstallMethod(
|
|||||||
if (rootAvailable) {
|
if (rootAvailable) {
|
||||||
ElevatedCard(
|
ElevatedCard(
|
||||||
colors = getCardColors(MaterialTheme.colorScheme.surfaceVariant),
|
colors = getCardColors(MaterialTheme.colorScheme.surfaceVariant),
|
||||||
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation),
|
elevation = getCardElevation(),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(bottom = 12.dp)
|
.padding(bottom = 12.dp)
|
||||||
.clip(MaterialTheme.shapes.large)
|
.clip(MaterialTheme.shapes.large)
|
||||||
.shadow(
|
) {
|
||||||
elevation = cardElevation,
|
MaterialTheme(
|
||||||
shape = MaterialTheme.shapes.large,
|
colorScheme = MaterialTheme.colorScheme.copy(
|
||||||
spotColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
|
surface = if (CardConfig.isCustomBackgroundEnabled) Color.Transparent else MaterialTheme.colorScheme.surfaceVariant
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
ListItem(
|
ListItem(
|
||||||
@@ -630,12 +595,13 @@ private fun SelectInstallMethod(
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
modifier = Modifier.clickable {
|
modifier = Modifier.clickable {
|
||||||
GKIExpanded = !GKIExpanded
|
gkiExpanded = !gkiExpanded
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = GKIExpanded,
|
visible = gkiExpanded,
|
||||||
enter = fadeIn() + expandVertically(),
|
enter = fadeIn() + expandVertically(),
|
||||||
exit = shrinkVertically() + fadeOut()
|
exit = shrinkVertically() + fadeOut()
|
||||||
) {
|
) {
|
||||||
@@ -722,7 +688,7 @@ fun rememberSelectKmiDialog(onSelected: (String?) -> Unit): DialogHandle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var selection by remember { mutableStateOf<String?>(null) }
|
var selection by remember { mutableStateOf<String?>(null) }
|
||||||
val backgroundColor = MaterialTheme.colorScheme.surfaceContainerHighest
|
val backgroundColor = MaterialTheme.colorScheme.surfaceContainerHigh
|
||||||
|
|
||||||
MaterialTheme(
|
MaterialTheme(
|
||||||
colorScheme = MaterialTheme.colorScheme.copy(
|
colorScheme = MaterialTheme.colorScheme.copy(
|
||||||
@@ -749,10 +715,14 @@ fun rememberSelectKmiDialog(onSelected: (String?) -> Unit): DialogHandle {
|
|||||||
@Composable
|
@Composable
|
||||||
private fun TopBar(
|
private fun TopBar(
|
||||||
onBack: () -> Unit = {},
|
onBack: () -> Unit = {},
|
||||||
onLkmUpload: () -> Unit = {},
|
|
||||||
scrollBehavior: TopAppBarScrollBehavior? = null
|
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
|
val cardAlpha = cardAlpha
|
||||||
|
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
|
|||||||
@@ -112,7 +112,6 @@ fun KpmScreen(
|
|||||||
Text(
|
Text(
|
||||||
text = kpmInstallMode,
|
text = kpmInstallMode,
|
||||||
style = MaterialTheme.typography.headlineSmall,
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
color = MaterialTheme.colorScheme.onSurface
|
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
text = {
|
text = {
|
||||||
@@ -121,7 +120,6 @@ fun KpmScreen(
|
|||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.kpm_install_mode_description, it),
|
text = stringResource(R.string.kpm_install_mode_description, it),
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
@@ -146,9 +144,6 @@ fun KpmScreen(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
colors = ButtonDefaults.buttonColors(
|
|
||||||
containerColor = MaterialTheme.colorScheme.primary
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Filled.Download,
|
imageVector = Icons.Filled.Download,
|
||||||
@@ -176,9 +171,6 @@ fun KpmScreen(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
colors = ButtonDefaults.buttonColors(
|
|
||||||
containerColor = MaterialTheme.colorScheme.secondary
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Filled.Inventory,
|
imageVector = Icons.Filled.Inventory,
|
||||||
@@ -209,7 +201,6 @@ fun KpmScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
|
|
||||||
shape = MaterialTheme.shapes.extraLarge
|
shape = MaterialTheme.shapes.extraLarge
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -309,10 +300,8 @@ fun KpmScreen(
|
|||||||
text = {
|
text = {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.kpm_install),
|
text = stringResource(R.string.kpm_install),
|
||||||
color = MaterialTheme.colorScheme.onPrimaryContainer
|
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
contentColor = MaterialTheme.colorScheme.onPrimaryContainer,
|
|
||||||
expanded = true,
|
expanded = true,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -321,9 +310,6 @@ fun KpmScreen(
|
|||||||
Column(modifier = Modifier.padding(padding)) {
|
Column(modifier = Modifier.padding(padding)) {
|
||||||
if (!isNoticeClosed) {
|
if (!isNoticeClosed) {
|
||||||
Card(
|
Card(
|
||||||
colors = CardDefaults.cardColors(
|
|
||||||
containerColor = MaterialTheme.colorScheme.secondaryContainer
|
|
||||||
),
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(16.dp)
|
.padding(16.dp)
|
||||||
@@ -348,7 +334,6 @@ fun KpmScreen(
|
|||||||
text = stringResource(R.string.kernel_module_notice),
|
text = stringResource(R.string.kernel_module_notice),
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
color = MaterialTheme.colorScheme.onSecondaryContainer
|
|
||||||
)
|
)
|
||||||
|
|
||||||
IconButton(
|
IconButton(
|
||||||
@@ -357,9 +342,6 @@ fun KpmScreen(
|
|||||||
sharedPreferences.edit { putBoolean("is_notice_closed", true) }
|
sharedPreferences.edit { putBoolean("is_notice_closed", true) }
|
||||||
},
|
},
|
||||||
modifier = Modifier.size(24.dp),
|
modifier = Modifier.size(24.dp),
|
||||||
colors = IconButtonDefaults.iconButtonColors(
|
|
||||||
contentColor = MaterialTheme.colorScheme.onSecondaryContainer
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Filled.Close,
|
imageVector = Icons.Filled.Close,
|
||||||
@@ -391,7 +373,6 @@ fun KpmScreen(
|
|||||||
stringResource(R.string.kpm_empty),
|
stringResource(R.string.kpm_empty),
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -585,7 +566,6 @@ private fun KpmModuleItem(
|
|||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.kpm_control),
|
text = stringResource(R.string.kpm_control),
|
||||||
style = MaterialTheme.typography.headlineSmall,
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
color = MaterialTheme.colorScheme.onSurface
|
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
text = {
|
text = {
|
||||||
@@ -595,20 +575,14 @@ private fun KpmModuleItem(
|
|||||||
label = {
|
label = {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.kpm_args),
|
text = stringResource(R.string.kpm_args),
|
||||||
color = MaterialTheme.colorScheme.primary
|
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
placeholder = {
|
placeholder = {
|
||||||
Text(
|
Text(
|
||||||
text = module.args,
|
text = module.args,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f)
|
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
colors = OutlinedTextFieldDefaults.colors(
|
|
||||||
focusedBorderColor = MaterialTheme.colorScheme.primary,
|
|
||||||
unfocusedBorderColor = MaterialTheme.colorScheme.outline
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
@@ -627,7 +601,6 @@ private fun KpmModuleItem(
|
|||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.confirm),
|
text = stringResource(R.string.confirm),
|
||||||
color = MaterialTheme.colorScheme.primary
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -635,26 +608,16 @@ private fun KpmModuleItem(
|
|||||||
TextButton(onClick = { viewModel.hideInputDialog() }) {
|
TextButton(onClick = { viewModel.hideInputDialog() }) {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.cancel),
|
text = stringResource(R.string.cancel),
|
||||||
color = MaterialTheme.colorScheme.primary
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
|
|
||||||
shape = MaterialTheme.shapes.extraLarge
|
shape = MaterialTheme.shapes.extraLarge
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Card(
|
Card(
|
||||||
colors = getCardColors(MaterialTheme.colorScheme.surfaceContainerHigh),
|
colors = getCardColors(MaterialTheme.colorScheme.surfaceContainerHigh),
|
||||||
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation),
|
elevation = getCardElevation()
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.clip(MaterialTheme.shapes.large)
|
|
||||||
.shadow(
|
|
||||||
elevation = cardElevation,
|
|
||||||
shape = MaterialTheme.shapes.large,
|
|
||||||
spotColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.padding(20.dp)
|
modifier = Modifier.padding(20.dp)
|
||||||
@@ -668,7 +631,6 @@ private fun KpmModuleItem(
|
|||||||
Text(
|
Text(
|
||||||
text = module.name,
|
text = module.name,
|
||||||
style = MaterialTheme.typography.titleLarge,
|
style = MaterialTheme.typography.titleLarge,
|
||||||
color = MaterialTheme.colorScheme.onSurface
|
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
@@ -676,19 +638,16 @@ private fun KpmModuleItem(
|
|||||||
Text(
|
Text(
|
||||||
text = "${stringResource(R.string.kpm_version)}: ${module.version}",
|
text = "${stringResource(R.string.kpm_version)}: ${module.version}",
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
)
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = "${stringResource(R.string.kpm_author)}: ${module.author}",
|
text = "${stringResource(R.string.kpm_author)}: ${module.author}",
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
)
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = "${stringResource(R.string.kpm_args)}: ${module.args}",
|
text = "${stringResource(R.string.kpm_args)}: ${module.args}",
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -698,7 +657,6 @@ private fun KpmModuleItem(
|
|||||||
Text(
|
Text(
|
||||||
text = module.description,
|
text = module.description,
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(20.dp))
|
Spacer(modifier = Modifier.height(20.dp))
|
||||||
@@ -711,10 +669,6 @@ private fun KpmModuleItem(
|
|||||||
onClick = { viewModel.showInputDialog(module.id) },
|
onClick = { viewModel.showInputDialog(module.id) },
|
||||||
enabled = module.hasAction,
|
enabled = module.hasAction,
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
colors = ButtonDefaults.buttonColors(
|
|
||||||
containerColor = MaterialTheme.colorScheme.primary,
|
|
||||||
disabledContainerColor = MaterialTheme.colorScheme.surfaceVariant
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Filled.Settings,
|
imageVector = Icons.Filled.Settings,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import android.app.Activity.*
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.util.Log
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
@@ -22,9 +23,7 @@ import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
|||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
|
||||||
import androidx.compose.ui.draw.rotate
|
import androidx.compose.ui.draw.rotate
|
||||||
import androidx.compose.ui.draw.shadow
|
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.platform.*
|
import androidx.compose.ui.platform.*
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
@@ -64,17 +63,21 @@ import okhttp3.OkHttpClient
|
|||||||
import com.sukisu.ultra.ui.util.ModuleModify
|
import com.sukisu.ultra.ui.util.ModuleModify
|
||||||
import com.sukisu.ultra.ui.theme.getCardColors
|
import com.sukisu.ultra.ui.theme.getCardColors
|
||||||
import com.sukisu.ultra.ui.viewmodel.ModuleViewModel
|
import com.sukisu.ultra.ui.viewmodel.ModuleViewModel
|
||||||
import java.io.BufferedReader
|
import java.util.concurrent.TimeUnit
|
||||||
import java.io.InputStreamReader
|
|
||||||
import java.util.zip.ZipInputStream
|
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
import com.sukisu.ultra.R
|
import com.sukisu.ultra.R
|
||||||
import com.sukisu.ultra.ui.theme.CardConfig.cardElevation
|
import com.sukisu.ultra.ui.theme.CardConfig.cardElevation
|
||||||
import com.sukisu.ultra.ui.webui.WebUIXActivity
|
import com.sukisu.ultra.ui.webui.WebUIXActivity
|
||||||
import com.dergoogler.mmrl.platform.Platform
|
import com.dergoogler.mmrl.platform.Platform
|
||||||
import androidx.core.net.toUri
|
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)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Destination<RootGraph>
|
@Destination<RootGraph>
|
||||||
@Composable
|
@Composable
|
||||||
@@ -84,6 +87,7 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
|
|||||||
val snackBarHost = LocalSnackbarHost.current
|
val snackBarHost = LocalSnackbarHost.current
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val confirmDialog = rememberConfirmDialog()
|
val confirmDialog = rememberConfirmDialog()
|
||||||
|
var lastClickTime by remember { mutableStateOf(0L) }
|
||||||
|
|
||||||
val selectZipLauncher = rememberLauncherForActivityResult(
|
val selectZipLauncher = rememberLauncherForActivityResult(
|
||||||
contract = ActivityResultContracts.StartActivityForResult()
|
contract = ActivityResultContracts.StartActivityForResult()
|
||||||
@@ -96,38 +100,21 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
|
|||||||
scope.launch {
|
scope.launch {
|
||||||
val clipData = data.clipData
|
val clipData = data.clipData
|
||||||
if (clipData != null) {
|
if (clipData != null) {
|
||||||
// 处理多选结果
|
val selectedModules = mutableListOf<Uri>()
|
||||||
val selectedModules = mutableSetOf<Uri>()
|
|
||||||
val selectedModuleNames = mutableMapOf<Uri, String>()
|
val selectedModuleNames = mutableMapOf<Uri, String>()
|
||||||
|
|
||||||
suspend fun processUri(uri: Uri) {
|
fun processUri(uri: Uri) {
|
||||||
val moduleName = withContext(Dispatchers.IO) {
|
|
||||||
try {
|
try {
|
||||||
val zipInputStream = ZipInputStream(context.contentResolver.openInputStream(uri))
|
if (!ModuleUtils.isUriAccessible(context, uri)) {
|
||||||
var entry = zipInputStream.nextEntry
|
return
|
||||||
var name = context.getString(R.string.unknown_module)
|
|
||||||
|
|
||||||
while (entry != null) {
|
|
||||||
if (entry.name == "module.prop") {
|
|
||||||
val reader = BufferedReader(InputStreamReader(zipInputStream))
|
|
||||||
var line: String?
|
|
||||||
while (reader.readLine().also { line = it } != null) {
|
|
||||||
if (line?.startsWith("name=") == true) {
|
|
||||||
name = line.substringAfter("=")
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
entry = zipInputStream.nextEntry
|
|
||||||
}
|
|
||||||
name
|
|
||||||
} catch (e: Exception) {
|
|
||||||
context.getString(R.string.unknown_module)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
ModuleUtils.takePersistableUriPermission(context, uri)
|
||||||
|
val moduleName = ModuleUtils.extractModuleName(context, uri)
|
||||||
selectedModules.add(uri)
|
selectedModules.add(uri)
|
||||||
selectedModuleNames[uri] = moduleName
|
selectedModuleNames[uri] = moduleName
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("ModuleScreen", "Error while processing URI: $uri, Error: ${e.message}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (i in 0 until clipData.itemCount) {
|
for (i in 0 until clipData.itemCount) {
|
||||||
@@ -135,7 +122,11 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
|
|||||||
processUri(uri)
|
processUri(uri)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 显示确认对话框
|
if (selectedModules.isEmpty()) {
|
||||||
|
snackBarHost.showSnackbar("Unable to access selected module files")
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
val modulesList = selectedModuleNames.values.joinToString("\n• ", "• ")
|
val modulesList = selectedModuleNames.values.joinToString("\n• ", "• ")
|
||||||
val confirmResult = confirmDialog.awaitConfirm(
|
val confirmResult = confirmDialog.awaitConfirm(
|
||||||
title = context.getString(R.string.module_install),
|
title = context.getString(R.string.module_install),
|
||||||
@@ -145,40 +136,27 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (confirmResult == ConfirmResult.Confirmed) {
|
if (confirmResult == ConfirmResult.Confirmed) {
|
||||||
|
try {
|
||||||
// 批量安装模块
|
// 批量安装模块
|
||||||
selectedModules.forEach { uri ->
|
navigator.navigate(FlashScreenDestination(FlashIt.FlashModules(selectedModules)))
|
||||||
navigator.navigate(FlashScreenDestination(FlashIt.FlashModule(uri)))
|
|
||||||
}
|
|
||||||
viewModel.markNeedRefresh()
|
viewModel.markNeedRefresh()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("ModuleScreen", "Error navigating to FlashScreen: ${e.message}")
|
||||||
|
snackBarHost.showSnackbar("Error while installing module: ${e.message}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 单个文件安装逻辑
|
|
||||||
val uri = data.data ?: return@launch
|
val uri = data.data ?: return@launch
|
||||||
val moduleName = withContext(Dispatchers.IO) {
|
// 单个安装模块
|
||||||
try {
|
try {
|
||||||
val zipInputStream = ZipInputStream(context.contentResolver.openInputStream(uri))
|
if (!ModuleUtils.isUriAccessible(context, uri)) {
|
||||||
var entry = zipInputStream.nextEntry
|
snackBarHost.showSnackbar("Unable to access selected module files")
|
||||||
var name = context.getString(R.string.unknown_module)
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
while (entry != null) {
|
ModuleUtils.takePersistableUriPermission(context, uri)
|
||||||
if (entry.name == "module.prop") {
|
|
||||||
val reader = BufferedReader(InputStreamReader(zipInputStream))
|
val moduleName = ModuleUtils.extractModuleName(context, uri)
|
||||||
var line: String?
|
|
||||||
while (reader.readLine().also { line = it } != null) {
|
|
||||||
if (line?.startsWith("name=") == true) {
|
|
||||||
name = line.substringAfter("=")
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
entry = zipInputStream.nextEntry
|
|
||||||
}
|
|
||||||
name
|
|
||||||
} catch (e: Exception) {
|
|
||||||
context.getString(R.string.unknown_module)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val confirmResult = confirmDialog.awaitConfirm(
|
val confirmResult = confirmDialog.awaitConfirm(
|
||||||
title = context.getString(R.string.module_install),
|
title = context.getString(R.string.module_install),
|
||||||
@@ -191,6 +169,10 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
|
|||||||
navigator.navigate(FlashScreenDestination(FlashIt.FlashModule(uri)))
|
navigator.navigate(FlashScreenDestination(FlashIt.FlashModule(uri)))
|
||||||
viewModel.markNeedRefresh()
|
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}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -247,10 +229,6 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
|
|||||||
Checkbox(
|
Checkbox(
|
||||||
checked = viewModel.sortActionFirst,
|
checked = viewModel.sortActionFirst,
|
||||||
onCheckedChange = null,
|
onCheckedChange = null,
|
||||||
colors = CheckboxDefaults.colors(
|
|
||||||
checkedColor = MaterialTheme.colorScheme.primary,
|
|
||||||
uncheckedColor = MaterialTheme.colorScheme.outline
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
onClick = {
|
onClick = {
|
||||||
@@ -272,10 +250,6 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
|
|||||||
Checkbox(
|
Checkbox(
|
||||||
checked = viewModel.sortEnabledFirst,
|
checked = viewModel.sortEnabledFirst,
|
||||||
onCheckedChange = null,
|
onCheckedChange = null,
|
||||||
colors = CheckboxDefaults.colors(
|
|
||||||
checkedColor = MaterialTheme.colorScheme.primary,
|
|
||||||
uncheckedColor = MaterialTheme.colorScheme.outline
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
onClick = {
|
onClick = {
|
||||||
@@ -293,7 +267,7 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
|
|||||||
text = { Text(stringResource(R.string.backup_modules)) },
|
text = { Text(stringResource(R.string.backup_modules)) },
|
||||||
leadingIcon = {
|
leadingIcon = {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Outlined.Download,
|
imageVector = Icons.Outlined.Save,
|
||||||
contentDescription = stringResource(R.string.backup),
|
contentDescription = stringResource(R.string.backup),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -306,7 +280,7 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
|
|||||||
text = { Text(stringResource(R.string.restore_modules)) },
|
text = { Text(stringResource(R.string.restore_modules)) },
|
||||||
leadingIcon = {
|
leadingIcon = {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Outlined.Refresh,
|
imageVector = Icons.Outlined.RestoreFromTrash,
|
||||||
contentDescription = stringResource(R.string.restore),
|
contentDescription = stringResource(R.string.restore),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -342,10 +316,8 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
|
|||||||
text = {
|
text = {
|
||||||
Text(
|
Text(
|
||||||
text = moduleInstall,
|
text = moduleInstall,
|
||||||
color = MaterialTheme.colorScheme.onPrimaryContainer
|
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
contentColor = MaterialTheme.colorScheme.onPrimaryContainer,
|
|
||||||
expanded = true,
|
expanded = true,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -370,7 +342,6 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
|
|||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Outlined.Warning,
|
imageVector = Icons.Outlined.Warning,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
tint = MaterialTheme.colorScheme.error,
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(64.dp)
|
.size(64.dp)
|
||||||
.padding(bottom = 16.dp)
|
.padding(bottom = 16.dp)
|
||||||
@@ -379,7 +350,6 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
|
|||||||
stringResource(R.string.module_magisk_conflict),
|
stringResource(R.string.module_magisk_conflict),
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -394,20 +364,60 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
|
|||||||
navigator.navigate(FlashScreenDestination(FlashIt.FlashModule(it)))
|
navigator.navigate(FlashScreenDestination(FlashIt.FlashModule(it)))
|
||||||
},
|
},
|
||||||
onClickModule = { id, name, hasWebUi ->
|
onClickModule = { id, name, hasWebUi ->
|
||||||
|
val currentTime = System.currentTimeMillis()
|
||||||
|
if (currentTime - lastClickTime < 600) {
|
||||||
|
Log.d("ModuleScreen", "Click too fast, ignoring")
|
||||||
|
return@ModuleList
|
||||||
|
}
|
||||||
|
lastClickTime = currentTime
|
||||||
|
|
||||||
if (hasWebUi) {
|
if (hasWebUi) {
|
||||||
webUILauncher.launch(
|
try {
|
||||||
if (prefs.getBoolean("use_webuix", false) && Platform.isAlive) {
|
val wxEngine = Intent(context, WebUIXActivity::class.java)
|
||||||
Intent(context, WebUIXActivity::class.java)
|
|
||||||
.setData("kernelsu://webuix/$id".toUri())
|
.setData("kernelsu://webuix/$id".toUri())
|
||||||
.putExtra("id", id)
|
.putExtra("id", id)
|
||||||
.putExtra("name", name)
|
.putExtra("name", name)
|
||||||
} else {
|
|
||||||
Intent(context, WebUIActivity::class.java)
|
val ksuEngine = Intent(context, WebUIActivity::class.java)
|
||||||
.setData("kernelsu://webui/$id".toUri())
|
.setData("kernelsu://webui/$id".toUri())
|
||||||
.putExtra("id", id)
|
.putExtra("id", id)
|
||||||
.putExtra("name", name)
|
.putExtra("name", name)
|
||||||
|
|
||||||
|
val config = try {
|
||||||
|
id.asModuleConfig
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("ModuleScreen", "Failed to get config from id: $id", e)
|
||||||
|
null
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
val globalEngine = prefs.getString("webui_engine", "default") ?: "default"
|
||||||
|
val moduleEngine = config?.getWebuiEngine(context)
|
||||||
|
val selectedEngine = when (globalEngine) {
|
||||||
|
"wx" -> wxEngine
|
||||||
|
"ksu" -> ksuEngine
|
||||||
|
"default" -> {
|
||||||
|
when (moduleEngine) {
|
||||||
|
"wx" -> wxEngine
|
||||||
|
"ksu" -> ksuEngine
|
||||||
|
else -> {
|
||||||
|
if (Platform.isAlive) {
|
||||||
|
wxEngine
|
||||||
|
} else {
|
||||||
|
ksuEngine
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> ksuEngine
|
||||||
|
}
|
||||||
|
webUILauncher.launch(selectedEngine)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("ModuleScreen", "Error launching WebUI: ${e.message}", e)
|
||||||
|
scope.launch {
|
||||||
|
snackBarHost.showSnackbar("Error launching WebUI: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return@ModuleList
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
context = context,
|
context = context,
|
||||||
@@ -445,6 +455,7 @@ private fun ModuleList(
|
|||||||
val downloadingText = stringResource(R.string.module_downloading)
|
val downloadingText = stringResource(R.string.module_downloading)
|
||||||
val startDownloadingText = stringResource(R.string.module_start_downloading)
|
val startDownloadingText = stringResource(R.string.module_start_downloading)
|
||||||
val fetchChangeLogFailed = stringResource(R.string.module_changelog_failed)
|
val fetchChangeLogFailed = stringResource(R.string.module_changelog_failed)
|
||||||
|
val downloadErrorText = stringResource(R.string.module_download_error)
|
||||||
|
|
||||||
val loadingDialog = rememberLoadingDialog()
|
val loadingDialog = rememberLoadingDialog()
|
||||||
val confirmDialog = rememberConfirmDialog()
|
val confirmDialog = rememberConfirmDialog()
|
||||||
@@ -455,12 +466,20 @@ private fun ModuleList(
|
|||||||
downloadUrl: String,
|
downloadUrl: String,
|
||||||
fileName: String
|
fileName: String
|
||||||
) {
|
) {
|
||||||
|
val client = OkHttpClient.Builder()
|
||||||
|
.connectTimeout(15, TimeUnit.SECONDS)
|
||||||
|
.readTimeout(30, TimeUnit.SECONDS)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val request = okhttp3.Request.Builder()
|
||||||
|
.url(changelogUrl)
|
||||||
|
.header("User-Agent", "SukiSU-Ultra/2.0")
|
||||||
|
.build()
|
||||||
|
|
||||||
val changelogResult = loadingDialog.withLoading {
|
val changelogResult = loadingDialog.withLoading {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
runCatching {
|
runCatching {
|
||||||
OkHttpClient().newCall(
|
client.newCall(request).execute().body!!.string()
|
||||||
okhttp3.Request.Builder().url(changelogUrl).build()
|
|
||||||
).execute().body!!.string()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -509,6 +528,11 @@ private fun ModuleList(
|
|||||||
launch(Dispatchers.Main) {
|
launch(Dispatchers.Main) {
|
||||||
Toast.makeText(context, downloading, Toast.LENGTH_SHORT).show()
|
Toast.makeText(context, downloading, Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
onError = { errorMsg ->
|
||||||
|
launch(Dispatchers.Main) {
|
||||||
|
Toast.makeText(context, "$downloadErrorText: $errorMsg", Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -603,7 +627,6 @@ private fun ModuleList(
|
|||||||
text = stringResource(R.string.module_empty),
|
text = stringResource(R.string.module_empty),
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -689,15 +712,7 @@ fun ModuleItem(
|
|||||||
) {
|
) {
|
||||||
ElevatedCard(
|
ElevatedCard(
|
||||||
colors = getCardColors(MaterialTheme.colorScheme.surfaceContainerHigh),
|
colors = getCardColors(MaterialTheme.colorScheme.surfaceContainerHigh),
|
||||||
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation),
|
elevation = getCardElevation(),
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.clip(MaterialTheme.shapes.large)
|
|
||||||
.shadow(
|
|
||||||
elevation = cardElevation,
|
|
||||||
shape = MaterialTheme.shapes.large,
|
|
||||||
spotColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
val textDecoration = if (!module.remove) null else TextDecoration.LineThrough
|
val textDecoration = if (!module.remove) null else TextDecoration.LineThrough
|
||||||
val interactionSource = remember { MutableInteractionSource() }
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
@@ -740,7 +755,6 @@ fun ModuleItem(
|
|||||||
lineHeight = MaterialTheme.typography.bodySmall.lineHeight,
|
lineHeight = MaterialTheme.typography.bodySmall.lineHeight,
|
||||||
fontFamily = MaterialTheme.typography.titleMedium.fontFamily,
|
fontFamily = MaterialTheme.typography.titleMedium.fontFamily,
|
||||||
textDecoration = textDecoration,
|
textDecoration = textDecoration,
|
||||||
color = MaterialTheme.colorScheme.onSurface
|
|
||||||
)
|
)
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
@@ -749,7 +763,6 @@ fun ModuleItem(
|
|||||||
lineHeight = MaterialTheme.typography.bodySmall.lineHeight,
|
lineHeight = MaterialTheme.typography.bodySmall.lineHeight,
|
||||||
fontFamily = MaterialTheme.typography.bodySmall.fontFamily,
|
fontFamily = MaterialTheme.typography.bodySmall.fontFamily,
|
||||||
textDecoration = textDecoration,
|
textDecoration = textDecoration,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
)
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
@@ -758,7 +771,6 @@ fun ModuleItem(
|
|||||||
lineHeight = MaterialTheme.typography.bodySmall.lineHeight,
|
lineHeight = MaterialTheme.typography.bodySmall.lineHeight,
|
||||||
fontFamily = MaterialTheme.typography.bodySmall.fontFamily,
|
fontFamily = MaterialTheme.typography.bodySmall.fontFamily,
|
||||||
textDecoration = textDecoration,
|
textDecoration = textDecoration,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -773,14 +785,6 @@ fun ModuleItem(
|
|||||||
checked = module.enabled,
|
checked = module.enabled,
|
||||||
onCheckedChange = onCheckChanged,
|
onCheckedChange = onCheckChanged,
|
||||||
interactionSource = if (!module.hasWebUi) interactionSource else null,
|
interactionSource = if (!module.hasWebUi) interactionSource else null,
|
||||||
colors = SwitchDefaults.colors(
|
|
||||||
checkedThumbColor = MaterialTheme.colorScheme.onPrimary,
|
|
||||||
checkedTrackColor = MaterialTheme.colorScheme.primary,
|
|
||||||
checkedIconColor = MaterialTheme.colorScheme.primary,
|
|
||||||
uncheckedThumbColor = MaterialTheme.colorScheme.outline,
|
|
||||||
uncheckedTrackColor = MaterialTheme.colorScheme.surfaceVariant,
|
|
||||||
uncheckedIconColor = MaterialTheme.colorScheme.surfaceVariant
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -796,7 +800,6 @@ fun ModuleItem(
|
|||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
maxLines = 4,
|
maxLines = 4,
|
||||||
textDecoration = textDecoration,
|
textDecoration = textDecoration,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
@@ -818,21 +821,12 @@ fun ModuleItem(
|
|||||||
viewModel.markNeedRefresh()
|
viewModel.markNeedRefresh()
|
||||||
},
|
},
|
||||||
contentPadding = ButtonDefaults.TextButtonContentPadding,
|
contentPadding = ButtonDefaults.TextButtonContentPadding,
|
||||||
colors = ButtonDefaults.filledTonalButtonColors()
|
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
modifier = Modifier.size(20.dp),
|
modifier = Modifier.size(20.dp),
|
||||||
imageVector = Icons.Outlined.PlayArrow,
|
imageVector = Icons.Outlined.PlayArrow,
|
||||||
contentDescription = null
|
contentDescription = null
|
||||||
)
|
)
|
||||||
//if (!module.hasWebUi && updateUrl.isEmpty()) {
|
|
||||||
//Text(
|
|
||||||
// modifier = Modifier.padding(start = 7.dp),
|
|
||||||
// text = stringResource(R.string.action),
|
|
||||||
// fontFamily = MaterialTheme.typography.labelMedium.fontFamily,
|
|
||||||
// fontSize = MaterialTheme.typography.labelMedium.fontSize
|
|
||||||
//)
|
|
||||||
//}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -843,7 +837,6 @@ fun ModuleItem(
|
|||||||
onClick = { onClick(module) },
|
onClick = { onClick(module) },
|
||||||
interactionSource = interactionSource,
|
interactionSource = interactionSource,
|
||||||
contentPadding = ButtonDefaults.TextButtonContentPadding,
|
contentPadding = ButtonDefaults.TextButtonContentPadding,
|
||||||
colors = ButtonDefaults.filledTonalButtonColors()
|
|
||||||
|
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
@@ -851,14 +844,6 @@ fun ModuleItem(
|
|||||||
imageVector = Icons.AutoMirrored.Outlined.Wysiwyg,
|
imageVector = Icons.AutoMirrored.Outlined.Wysiwyg,
|
||||||
contentDescription = null
|
contentDescription = null
|
||||||
)
|
)
|
||||||
//if (!module.hasActionScript && updateUrl.isEmpty()) {
|
|
||||||
//Text(
|
|
||||||
// modifier = Modifier.padding(start = 7.dp),
|
|
||||||
// fontFamily = MaterialTheme.typography.labelMedium.fontFamily,
|
|
||||||
// fontSize = MaterialTheme.typography.labelMedium.fontSize,
|
|
||||||
// text = stringResource(R.string.open)
|
|
||||||
//)
|
|
||||||
//}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -877,14 +862,6 @@ fun ModuleItem(
|
|||||||
imageVector = Icons.Outlined.Download,
|
imageVector = Icons.Outlined.Download,
|
||||||
contentDescription = null
|
contentDescription = null
|
||||||
)
|
)
|
||||||
//if (!module.hasActionScript || !module.hasWebUi) {
|
|
||||||
//Text(
|
|
||||||
// modifier = Modifier.padding(start = 7.dp),
|
|
||||||
// fontFamily = MaterialTheme.typography.labelMedium.fontFamily,
|
|
||||||
// fontSize = MaterialTheme.typography.labelMedium.fontSize,
|
|
||||||
// text = stringResource(R.string.module_update)
|
|
||||||
//)
|
|
||||||
//}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -892,8 +869,6 @@ fun ModuleItem(
|
|||||||
modifier = Modifier.defaultMinSize(minWidth = 52.dp, minHeight = 32.dp),
|
modifier = Modifier.defaultMinSize(minWidth = 52.dp, minHeight = 32.dp),
|
||||||
onClick = { onUninstallClicked(module) },
|
onClick = { onUninstallClicked(module) },
|
||||||
contentPadding = ButtonDefaults.TextButtonContentPadding,
|
contentPadding = ButtonDefaults.TextButtonContentPadding,
|
||||||
colors = ButtonDefaults.filledTonalButtonColors(
|
|
||||||
containerColor = if (!module.remove) MaterialTheme.colorScheme.secondaryContainer else MaterialTheme.colorScheme.errorContainer)
|
|
||||||
) {
|
) {
|
||||||
if (!module.remove) {
|
if (!module.remove) {
|
||||||
Icon(
|
Icon(
|
||||||
@@ -908,15 +883,6 @@ fun ModuleItem(
|
|||||||
contentDescription = null
|
contentDescription = null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
//if (!module.hasActionScript && !module.hasWebUi && updateUrl.isEmpty()) {
|
|
||||||
//Text(
|
|
||||||
// modifier = Modifier.padding(start = 7.dp),
|
|
||||||
// fontFamily = MaterialTheme.typography.labelMedium.fontFamily,
|
|
||||||
// fontSize = MaterialTheme.typography.labelMedium.fontSize,
|
|
||||||
// text = stringResource(if (!module.remove) R.string.uninstall else R.string.restore),
|
|
||||||
// color = if (!module.remove) MaterialTheme.colorScheme.onErrorContainer else MaterialTheme.colorScheme.onSecondaryContainer
|
|
||||||
//)
|
|
||||||
//}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -939,7 +905,8 @@ fun ModuleItemPreview() {
|
|||||||
updateJson = "",
|
updateJson = "",
|
||||||
hasWebUi = false,
|
hasWebUi = false,
|
||||||
hasActionScript = false,
|
hasActionScript = false,
|
||||||
dirId = "dirId"
|
dirId = "dirId",
|
||||||
|
config = ModuleConfig(),
|
||||||
)
|
)
|
||||||
ModuleItem(EmptyDestinationsNavigator, module, "", {}, {}, {}, {})
|
ModuleItem(EmptyDestinationsNavigator, module, "", {}, {}, {}, {})
|
||||||
}
|
}
|
||||||
@@ -13,7 +13,6 @@ import androidx.compose.animation.fadeOut
|
|||||||
import androidx.compose.animation.shrinkVertically
|
import androidx.compose.animation.shrinkVertically
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
@@ -32,6 +31,7 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
@@ -53,18 +53,22 @@ import kotlinx.coroutines.withContext
|
|||||||
import com.sukisu.ultra.BuildConfig
|
import com.sukisu.ultra.BuildConfig
|
||||||
import com.sukisu.ultra.Natives
|
import com.sukisu.ultra.Natives
|
||||||
import com.sukisu.ultra.R
|
import com.sukisu.ultra.R
|
||||||
import com.sukisu.ultra.*
|
|
||||||
import com.sukisu.ultra.ui.component.*
|
import com.sukisu.ultra.ui.component.*
|
||||||
import com.sukisu.ultra.ui.theme.*
|
import com.sukisu.ultra.ui.theme.*
|
||||||
import com.sukisu.ultra.ui.theme.CardConfig.cardAlpha
|
import com.sukisu.ultra.ui.theme.CardConfig.cardAlpha
|
||||||
import com.sukisu.ultra.ui.theme.CardConfig.cardElevation
|
|
||||||
import com.sukisu.ultra.ui.util.LocalSnackbarHost
|
import com.sukisu.ultra.ui.util.LocalSnackbarHost
|
||||||
import com.sukisu.ultra.ui.util.getBugreportFile
|
import com.sukisu.ultra.ui.util.getBugreportFile
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
import java.time.format.DateTimeFormatter
|
import java.time.format.DateTimeFormatter
|
||||||
import com.sukisu.ultra.ui.component.KsuIsValid
|
import com.sukisu.ultra.ui.component.KsuIsValid
|
||||||
import com.dergoogler.mmrl.platform.Platform
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Destination<RootGraph>
|
@Destination<RootGraph>
|
||||||
@@ -72,12 +76,18 @@ import com.dergoogler.mmrl.platform.Platform
|
|||||||
fun SettingScreen(navigator: DestinationsNavigator) {
|
fun SettingScreen(navigator: DestinationsNavigator) {
|
||||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||||
val snackBarHost = LocalSnackbarHost.current
|
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(
|
Scaffold(
|
||||||
|
// containerColor = MaterialTheme.colorScheme.surfaceBright,
|
||||||
topBar = {
|
topBar = {
|
||||||
TopBar(
|
TopBar(scrollBehavior = scrollBehavior)
|
||||||
scrollBehavior = scrollBehavior
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
snackbarHost = { SnackbarHost(snackBarHost) },
|
snackbarHost = { SnackbarHost(snackBarHost) },
|
||||||
contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal)
|
contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal)
|
||||||
@@ -111,31 +121,16 @@ fun SettingScreen(navigator: DestinationsNavigator) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 配置
|
// 配置卡片
|
||||||
Card(
|
SettingsGroupCard(
|
||||||
modifier = Modifier
|
title = stringResource(R.string.configuration),
|
||||||
.fillMaxWidth()
|
content = {
|
||||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
|
||||||
colors = CardDefaults.cardColors(
|
|
||||||
containerColor = MaterialTheme.colorScheme.surfaceContainerLow.copy(alpha = cardAlpha)
|
|
||||||
),
|
|
||||||
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation)
|
|
||||||
) {
|
|
||||||
Column(modifier = Modifier.padding(vertical = 8.dp)) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.configuration),
|
|
||||||
style = MaterialTheme.typography.titleMedium,
|
|
||||||
color = MaterialTheme.colorScheme.primary,
|
|
||||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
|
|
||||||
)
|
|
||||||
|
|
||||||
// 配置文件模板入口
|
// 配置文件模板入口
|
||||||
val profileTemplate = stringResource(id = R.string.settings_profile_template)
|
|
||||||
KsuIsValid {
|
KsuIsValid {
|
||||||
SettingItem(
|
SettingItem(
|
||||||
icon = Icons.Filled.Fence,
|
icon = Icons.Filled.Fence,
|
||||||
title = profileTemplate,
|
title = stringResource(R.string.settings_profile_template),
|
||||||
summary = stringResource(id = R.string.settings_profile_template_summary),
|
summary = stringResource(R.string.settings_profile_template_summary),
|
||||||
onClick = {
|
onClick = {
|
||||||
navigator.navigate(AppProfileTemplateScreenDestination)
|
navigator.navigate(AppProfileTemplateScreenDestination)
|
||||||
}
|
}
|
||||||
@@ -148,210 +143,146 @@ fun SettingScreen(navigator: DestinationsNavigator) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
KsuIsValid {
|
KsuIsValid {
|
||||||
SwitchSettingItem(
|
SwitchItem(
|
||||||
icon = Icons.Filled.FolderDelete,
|
icon = Icons.Filled.FolderDelete,
|
||||||
title = stringResource(id = R.string.settings_umount_modules_default),
|
title = stringResource(R.string.settings_umount_modules_default),
|
||||||
summary = stringResource(id = R.string.settings_umount_modules_default_summary),
|
summary = stringResource(R.string.settings_umount_modules_default_summary),
|
||||||
checked = umountChecked,
|
checked = umountChecked,
|
||||||
onCheckedChange = {
|
onCheckedChange = { enabled ->
|
||||||
if (Natives.setDefaultUmountModules(it)) {
|
if (Natives.setDefaultUmountModules(enabled)) {
|
||||||
umountChecked = it
|
umountChecked = enabled
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SU 禁用开关(仅在兼容版本显示)
|
// SU 禁用开关
|
||||||
KsuIsValid {
|
KsuIsValid {
|
||||||
if (Natives.version >= Natives.MINIMAL_SUPPORTED_SU_COMPAT) {
|
if (Natives.version >= Natives.MINIMAL_SUPPORTED_SU_COMPAT) {
|
||||||
var isSuDisabled by rememberSaveable {
|
var isSuDisabled by rememberSaveable {
|
||||||
mutableStateOf(!Natives.isSuEnabled())
|
mutableStateOf(!Natives.isSuEnabled())
|
||||||
}
|
}
|
||||||
SwitchSettingItem(
|
SwitchItem(
|
||||||
icon = Icons.Filled.RemoveModerator,
|
icon = Icons.Filled.RemoveModerator,
|
||||||
title = stringResource(id = R.string.settings_disable_su),
|
title = stringResource(R.string.settings_disable_su),
|
||||||
summary = stringResource(id = R.string.settings_disable_su_summary),
|
summary = stringResource(R.string.settings_disable_su_summary),
|
||||||
checked = isSuDisabled,
|
checked = isSuDisabled,
|
||||||
onCheckedChange = { checked ->
|
onCheckedChange = { enabled ->
|
||||||
val shouldEnable = !checked
|
val shouldEnable = !enabled
|
||||||
if (Natives.setSuEnabled(shouldEnable)) {
|
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 {
|
var checkUpdate by rememberSaveable {
|
||||||
mutableStateOf(
|
mutableStateOf(prefs.getBoolean("check_update", true))
|
||||||
prefs.getBoolean("check_update", true)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
SwitchSettingItem(
|
SwitchItem(
|
||||||
icon = Icons.Filled.Update,
|
icon = Icons.Filled.Update,
|
||||||
title = stringResource(id = R.string.settings_check_update),
|
title = stringResource(R.string.settings_check_update),
|
||||||
summary = stringResource(id = R.string.settings_check_update_summary),
|
summary = stringResource(R.string.settings_check_update_summary),
|
||||||
checked = checkUpdate,
|
checked = checkUpdate,
|
||||||
onCheckedChange = {
|
onCheckedChange = { enabled ->
|
||||||
prefs.edit {putBoolean("check_update", it) }
|
prefs.edit { putBoolean("check_update", enabled) }
|
||||||
checkUpdate = it
|
checkUpdate = enabled
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// Web调试开关
|
// WebUI引擎选择
|
||||||
var enableWebDebugging by rememberSaveable {
|
|
||||||
mutableStateOf(
|
|
||||||
prefs.getBoolean("enable_web_debugging", false)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
KsuIsValid {
|
KsuIsValid {
|
||||||
SwitchSettingItem(
|
WebUIEngineSelector(
|
||||||
icon = Icons.Filled.DeveloperMode,
|
selectedEngine = selectedEngine,
|
||||||
title = stringResource(id = R.string.enable_web_debugging),
|
onEngineSelected = { engine ->
|
||||||
summary = stringResource(id = R.string.enable_web_debugging_summary),
|
selectedEngine = engine
|
||||||
checked = enableWebDebugging,
|
prefs.edit { putString("webui_engine", engine) }
|
||||||
onCheckedChange = {
|
|
||||||
prefs.edit { putBoolean("enable_web_debugging", it) }
|
|
||||||
enableWebDebugging = it
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Web X 开关
|
// Web调试和Web X Eruda 开关
|
||||||
var useWebUIX by rememberSaveable {
|
var enableWebDebugging by rememberSaveable {
|
||||||
mutableStateOf(
|
mutableStateOf(prefs.getBoolean("enable_web_debugging", false))
|
||||||
prefs.getBoolean("use_webuix", false)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
var useWebUIXEruda by rememberSaveable {
|
||||||
|
mutableStateOf(prefs.getBoolean("use_webuix_eruda", false))
|
||||||
|
}
|
||||||
|
|
||||||
KsuIsValid {
|
KsuIsValid {
|
||||||
SwitchItem(
|
SwitchItem(
|
||||||
beta = true,
|
icon = Icons.Filled.DeveloperMode,
|
||||||
enabled = Platform.isAlive,
|
title = stringResource(R.string.enable_web_debugging),
|
||||||
icon = Icons.Filled.WebAsset,
|
summary = stringResource(R.string.enable_web_debugging_summary),
|
||||||
title = stringResource(id = R.string.use_webuix),
|
checked = enableWebDebugging,
|
||||||
summary = stringResource(id = R.string.use_webuix_summary),
|
onCheckedChange = { enabled ->
|
||||||
checked = useWebUIX
|
prefs.edit { putBoolean("enable_web_debugging", enabled) }
|
||||||
) {
|
enableWebDebugging = enabled
|
||||||
prefs.edit { putBoolean("use_webuix", it) }
|
|
||||||
useWebUIX = it
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Web X Eruda 开关
|
|
||||||
var useWebUIXEruda by rememberSaveable {
|
|
||||||
mutableStateOf(
|
|
||||||
prefs.getBoolean("use_webuix_eruda", false)
|
|
||||||
)
|
)
|
||||||
}
|
|
||||||
KsuIsValid {
|
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = useWebUIX && enableWebDebugging,
|
visible = enableWebDebugging && selectedEngine == "wx",
|
||||||
enter = fadeIn() + expandVertically(),
|
enter = fadeIn() + expandVertically(),
|
||||||
exit = fadeOut() + shrinkVertically()
|
exit = fadeOut() + shrinkVertically()
|
||||||
) {
|
) {
|
||||||
SwitchItem(
|
SwitchItem(
|
||||||
beta = true,
|
|
||||||
enabled = Platform.isAlive && useWebUIX && enableWebDebugging,
|
|
||||||
icon = Icons.Filled.FormatListNumbered,
|
icon = Icons.Filled.FormatListNumbered,
|
||||||
title = stringResource(id = R.string.use_webuix_eruda),
|
title = stringResource(R.string.use_webuix_eruda),
|
||||||
summary = stringResource(id = R.string.use_webuix_eruda_summary),
|
summary = stringResource(R.string.use_webuix_eruda_summary),
|
||||||
checked = useWebUIXEruda
|
checked = useWebUIXEruda,
|
||||||
) {
|
onCheckedChange = { enabled ->
|
||||||
prefs.edit { putBoolean("use_webuix_eruda", it) }
|
prefs.edit { putBoolean("use_webuix_eruda", enabled) }
|
||||||
useWebUIXEruda = it
|
useWebUIXEruda = enabled
|
||||||
}
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更多设置
|
// 更多设置
|
||||||
SettingItem(
|
SettingItem(
|
||||||
icon = Icons.Filled.Settings,
|
icon = Icons.Filled.Settings,
|
||||||
title = stringResource(id = R.string.more_settings),
|
title = stringResource(R.string.more_settings),
|
||||||
summary = stringResource(id = R.string.more_settings),
|
summary = stringResource(R.string.more_settings),
|
||||||
onClick = {
|
onClick = {
|
||||||
navigator.navigate(MoreSettingsScreenDestination)
|
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) }
|
var showBottomsheet by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
SettingItem(
|
SettingItem(
|
||||||
icon = Icons.Filled.BugReport,
|
icon = Icons.Filled.BugReport,
|
||||||
title = stringResource(id = R.string.send_log),
|
title = stringResource(R.string.send_log),
|
||||||
onClick = {
|
onClick = {
|
||||||
showBottomsheet = true
|
showBottomsheet = true
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
if (showBottomsheet) {
|
if (showBottomsheet) {
|
||||||
ModalBottomSheet(
|
LogBottomSheet(
|
||||||
onDismissRequest = { showBottomsheet = false },
|
onDismiss = { showBottomsheet = false },
|
||||||
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
|
onSaveLog = {
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(16.dp),
|
|
||||||
horizontalArrangement = Arrangement.SpaceEvenly
|
|
||||||
) {
|
|
||||||
LogActionButton(
|
|
||||||
icon = Icons.Filled.Save,
|
|
||||||
text = stringResource(R.string.save_log),
|
|
||||||
onClick = {
|
|
||||||
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH_mm")
|
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH_mm")
|
||||||
val current = LocalDateTime.now().format(formatter)
|
val current = LocalDateTime.now().format(formatter)
|
||||||
exportBugreportLauncher.launch("KernelSU_bugreport_${current}.tar.gz")
|
exportBugreportLauncher.launch("KernelSU_bugreport_${current}.tar.gz")
|
||||||
showBottomsheet = false
|
showBottomsheet = false
|
||||||
}
|
},
|
||||||
)
|
onShareLog = {
|
||||||
|
|
||||||
LogActionButton(
|
|
||||||
icon = Icons.Filled.Share,
|
|
||||||
text = stringResource(R.string.send_log),
|
|
||||||
onClick = {
|
|
||||||
scope.launch {
|
scope.launch {
|
||||||
val bugreport = loadingDialog.withLoading {
|
val bugreport = loadingDialog.withLoading {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
@@ -359,8 +290,7 @@ fun SettingScreen(navigator: DestinationsNavigator) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val uri: Uri =
|
val uri = FileProvider.getUriForFile(
|
||||||
FileProvider.getUriForFile(
|
|
||||||
context,
|
context,
|
||||||
"${BuildConfig.APPLICATION_ID}.fileprovider",
|
"${BuildConfig.APPLICATION_ID}.fileprovider",
|
||||||
bugreport
|
bugreport
|
||||||
@@ -384,9 +314,6 @@ fun SettingScreen(navigator: DestinationsNavigator) {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val lkmMode = Natives.version >= Natives.MINIMAL_SUPPORTED_KERNEL_LKM && Natives.isLkmMode
|
val lkmMode = Natives.version >= Natives.MINIMAL_SUPPORTED_KERNEL_LKM && Natives.isLkmMode
|
||||||
if (lkmMode) {
|
if (lkmMode) {
|
||||||
@@ -395,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(
|
SettingItem(
|
||||||
icon = Icons.Filled.Info,
|
icon = Icons.Filled.Info,
|
||||||
title = stringResource(R.string.about),
|
title = stringResource(R.string.about),
|
||||||
@@ -423,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
|
@Composable
|
||||||
fun LogActionButton(
|
fun LogActionButton(
|
||||||
icon: ImageVector,
|
icon: ImageVector,
|
||||||
@@ -440,7 +468,7 @@ fun LogActionButton(
|
|||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clickable(onClick = onClick)
|
.clickable(onClick = onClick)
|
||||||
.padding(8.dp)
|
.padding(SPACING_MEDIUM)
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
@@ -456,11 +484,10 @@ fun LogActionButton(
|
|||||||
modifier = Modifier.size(24.dp)
|
modifier = Modifier.size(24.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(SPACING_MEDIUM))
|
||||||
Text(
|
Text(
|
||||||
text = text,
|
text = text,
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium
|
||||||
color = MaterialTheme.colorScheme.onSurface
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -476,33 +503,31 @@ fun SettingItem(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clickable(onClick = onClick)
|
.clickable(onClick = onClick)
|
||||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
.padding(horizontal = SPACING_LARGE, vertical = 12.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.Top
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = icon,
|
imageVector = icon,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
tint = MaterialTheme.colorScheme.primary,
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(end = 16.dp)
|
.padding(end = SPACING_LARGE)
|
||||||
.size(24.dp)
|
.size(24.dp)
|
||||||
)
|
)
|
||||||
|
|
||||||
Column(modifier = Modifier.weight(1f)) {
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
Text(
|
Text(
|
||||||
text = title,
|
text = title,
|
||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleMedium
|
||||||
color = MaterialTheme.colorScheme.onSurface
|
|
||||||
)
|
)
|
||||||
if (summary != null) {
|
if (summary != null) {
|
||||||
|
Spacer(modifier = Modifier.height(SPACING_SMALL))
|
||||||
Text(
|
Text(
|
||||||
text = summary,
|
text = summary,
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Filled.ChevronRight,
|
imageVector = Icons.Filled.ChevronRight,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
@@ -513,7 +538,7 @@ fun SettingItem(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SwitchSettingItem(
|
fun SwitchItem(
|
||||||
icon: ImageVector,
|
icon: ImageVector,
|
||||||
title: String,
|
title: String,
|
||||||
summary: String? = null,
|
summary: String? = null,
|
||||||
@@ -524,44 +549,34 @@ fun SwitchSettingItem(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clickable { onCheckedChange(!checked) }
|
.clickable { onCheckedChange(!checked) }
|
||||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
.padding(horizontal = SPACING_LARGE, vertical = 12.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.Top
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = icon,
|
imageVector = icon,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
tint = MaterialTheme.colorScheme.primary,
|
tint = if (checked) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(end = 16.dp)
|
.padding(end = SPACING_LARGE)
|
||||||
.size(24.dp)
|
.size(24.dp)
|
||||||
)
|
)
|
||||||
|
|
||||||
Column(modifier = Modifier.weight(1f)) {
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
Text(
|
Text(
|
||||||
text = title,
|
text = title,
|
||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleMedium
|
||||||
color = MaterialTheme.colorScheme.onSurface
|
|
||||||
)
|
)
|
||||||
if (summary != null) {
|
if (summary != null) {
|
||||||
|
Spacer(modifier = Modifier.height(SPACING_SMALL))
|
||||||
Text(
|
Text(
|
||||||
text = summary,
|
text = summary,
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Switch(
|
Switch(
|
||||||
checked = checked,
|
checked = checked,
|
||||||
onCheckedChange = onCheckedChange,
|
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
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -645,12 +660,7 @@ fun rememberUninstallDialog(onSelected: (UninstallType) -> Unit): DialogHandle {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
var selection = UninstallType.NONE
|
var selectedOption by remember { mutableStateOf<UninstallType?>(null) }
|
||||||
val cardColor = if (!ThemeConfig.useDynamicColor) {
|
|
||||||
ThemeConfig.currentTheme.ButtonContrast
|
|
||||||
} else {
|
|
||||||
MaterialTheme.colorScheme.surfaceContainerHigh
|
|
||||||
}
|
|
||||||
|
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = {
|
onDismissRequest = {
|
||||||
@@ -660,7 +670,6 @@ fun rememberUninstallDialog(onSelected: (UninstallType) -> Unit): DialogHandle {
|
|||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.settings_uninstall),
|
text = stringResource(R.string.settings_uninstall),
|
||||||
style = MaterialTheme.typography.headlineSmall,
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
color = MaterialTheme.colorScheme.onSurface
|
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
text = {
|
text = {
|
||||||
@@ -668,55 +677,83 @@ fun rememberUninstallDialog(onSelected: (UninstallType) -> Unit): DialogHandle {
|
|||||||
modifier = Modifier.padding(vertical = 8.dp),
|
modifier = Modifier.padding(vertical = 8.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(16.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(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clip(MaterialTheme.shapes.medium)
|
.clip(MaterialTheme.shapes.medium)
|
||||||
|
.background(backgroundColor)
|
||||||
.clickable {
|
.clickable {
|
||||||
selection = options[index]
|
selectedOption = option
|
||||||
}
|
}
|
||||||
.padding(vertical = 12.dp, horizontal = 8.dp),
|
.padding(vertical = 12.dp, horizontal = 8.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = options[index].icon,
|
imageVector = option.icon,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
tint = MaterialTheme.colorScheme.primary,
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(end = 16.dp)
|
.padding(end = 16.dp)
|
||||||
.size(24.dp)
|
.size(24.dp)
|
||||||
)
|
)
|
||||||
Column {
|
Column(
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = option.titleText,
|
text = listOptions[index].titleText,
|
||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
color = MaterialTheme.colorScheme.onSurface
|
|
||||||
)
|
)
|
||||||
option.subtitleText?.let {
|
listOptions[index].subtitleText?.let {
|
||||||
Text(
|
Text(
|
||||||
text = it,
|
text = it,
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
color = if (isSelected)
|
||||||
|
contentColor.copy(alpha = 0.8f)
|
||||||
|
else
|
||||||
|
MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (isSelected) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.RadioButtonChecked,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.RadioButtonUnchecked,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
TextButton(
|
Button(
|
||||||
onClick = {
|
onClick = {
|
||||||
if (selection != UninstallType.NONE) {
|
selectedOption?.let { onSelected(it) }
|
||||||
onSelected(selection)
|
|
||||||
}
|
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
},
|
||||||
|
enabled = selectedOption != null,
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(android.R.string.ok),
|
text = stringResource(android.R.string.ok)
|
||||||
color = MaterialTheme.colorScheme.primary
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -728,11 +765,9 @@ fun rememberUninstallDialog(onSelected: (UninstallType) -> Unit): DialogHandle {
|
|||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(android.R.string.cancel),
|
text = stringResource(android.R.string.cancel),
|
||||||
color = MaterialTheme.colorScheme.primary
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
containerColor = cardColor,
|
|
||||||
shape = MaterialTheme.shapes.extraLarge,
|
shape = MaterialTheme.shapes.extraLarge,
|
||||||
tonalElevation = 4.dp
|
tonalElevation = 4.dp
|
||||||
)
|
)
|
||||||
@@ -744,14 +779,12 @@ fun rememberUninstallDialog(onSelected: (UninstallType) -> Unit): DialogHandle {
|
|||||||
private fun TopBar(
|
private fun TopBar(
|
||||||
scrollBehavior: TopAppBarScrollBehavior? = null
|
scrollBehavior: TopAppBarScrollBehavior? = null
|
||||||
) {
|
) {
|
||||||
val systemIsDark = isSystemInDarkTheme()
|
val colorScheme = MaterialTheme.colorScheme
|
||||||
val cardColor = MaterialTheme.colorScheme.surfaceVariant
|
val cardColor = if (CardConfig.isCustomBackgroundEnabled) {
|
||||||
val cardAlpha = if (ThemeConfig.customBackgroundUri != null) {
|
colorScheme.surfaceContainerLow
|
||||||
cardAlpha
|
|
||||||
} else {
|
} else {
|
||||||
if (systemIsDark) 0.8f else 1f
|
colorScheme.background
|
||||||
}
|
}
|
||||||
|
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = {
|
title = {
|
||||||
Text(
|
Text(
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
package com.sukisu.ultra.ui.screen
|
package com.sukisu.ultra.ui.screen
|
||||||
|
|
||||||
import androidx.compose.animation.*
|
import androidx.compose.animation.*
|
||||||
|
import androidx.compose.animation.core.*
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.border
|
|
||||||
import androidx.compose.foundation.gestures.detectTapGestures
|
import androidx.compose.foundation.gestures.detectTapGestures
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.interaction.collectIsPressedAsState
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.ExperimentalMaterialApi
|
import androidx.compose.material.ExperimentalMaterialApi
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
@@ -18,14 +21,12 @@ import androidx.compose.runtime.*
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.draw.shadow
|
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
@@ -40,10 +41,17 @@ import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import com.sukisu.ultra.Natives
|
import com.sukisu.ultra.Natives
|
||||||
import com.sukisu.ultra.ui.component.SearchAppBar
|
import com.sukisu.ultra.ui.component.SearchAppBar
|
||||||
import com.sukisu.ultra.ui.theme.CardConfig.cardElevation
|
|
||||||
import com.sukisu.ultra.ui.util.ModuleModify
|
import com.sukisu.ultra.ui.util.ModuleModify
|
||||||
import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel
|
import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel
|
||||||
|
import com.dergoogler.mmrl.ui.component.LabelItem
|
||||||
|
import com.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)
|
@OptIn(ExperimentalMaterialApi::class, ExperimentalMaterial3Api::class)
|
||||||
@Destination<RootGraph>
|
@Destination<RootGraph>
|
||||||
@Composable
|
@Composable
|
||||||
@@ -62,13 +70,14 @@ fun SuperUserScreen(navigator: DestinationsNavigator) {
|
|||||||
LaunchedEffect(key1 = navigator) {
|
LaunchedEffect(key1 = navigator) {
|
||||||
viewModel.search = ""
|
viewModel.search = ""
|
||||||
if (viewModel.appList.isEmpty()) {
|
if (viewModel.appList.isEmpty()) {
|
||||||
viewModel.fetchAppList()
|
// viewModel.fetchAppList()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(viewModel.search) {
|
LaunchedEffect(viewModel.search) {
|
||||||
if (viewModel.search.isEmpty()) {
|
if (viewModel.search.isEmpty()) {
|
||||||
listState.scrollToItem(0)
|
// 取消自动滚动到顶部的行为
|
||||||
|
// listState.scrollToItem(0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,7 +135,7 @@ fun SuperUserScreen(navigator: DestinationsNavigator) {
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
onClick = {
|
onClick = {
|
||||||
viewModel.showSystemApps = !viewModel.showSystemApps
|
viewModel.updateShowSystemApps(!viewModel.showSystemApps)
|
||||||
showDropdown = false
|
showDropdown = false
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -165,77 +174,298 @@ fun SuperUserScreen(navigator: DestinationsNavigator) {
|
|||||||
},
|
},
|
||||||
snackbarHost = { SnackbarHost(snackBarHostState) },
|
snackbarHost = { SnackbarHost(snackBarHostState) },
|
||||||
contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),
|
contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),
|
||||||
bottomBar = {
|
floatingActionButton = {
|
||||||
AnimatedVisibility(
|
// 侧边悬浮按钮集合
|
||||||
visible = viewModel.showBatchActions && viewModel.selectedApps.isNotEmpty(),
|
Column(
|
||||||
enter = slideInVertically(initialOffsetY = { it }),
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
exit = slideOutVertically(targetOffsetY = { it })
|
horizontalAlignment = Alignment.End
|
||||||
) {
|
) {
|
||||||
Surface(
|
// 批量操作相关按钮
|
||||||
color = MaterialTheme.colorScheme.surfaceContainerHighest,
|
// 只有在批量模式且有选中应用时才显示批量操作按钮
|
||||||
tonalElevation = cardElevation,
|
if (viewModel.showBatchActions && viewModel.selectedApps.isNotEmpty()) {
|
||||||
shadowElevation = cardElevation
|
// 取消按钮
|
||||||
) {
|
val cancelInteractionSource = remember { MutableInteractionSource() }
|
||||||
Row(
|
val isCancelPressed by cancelInteractionSource.collectIsPressedAsState()
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
FloatingActionButton(
|
||||||
.padding(16.dp),
|
|
||||||
horizontalArrangement = Arrangement.SpaceEvenly
|
|
||||||
) {
|
|
||||||
OutlinedButton(
|
|
||||||
onClick = {
|
onClick = {
|
||||||
viewModel.selectedApps = emptySet()
|
viewModel.selectedApps = emptySet()
|
||||||
viewModel.showBatchActions = false
|
viewModel.showBatchActions = false
|
||||||
},
|
},
|
||||||
colors = ButtonDefaults.outlinedButtonColors(
|
modifier = Modifier.size(if (isCancelPressed) 56.dp else 40.dp),
|
||||||
contentColor = MaterialTheme.colorScheme.primary
|
shape = CircleShape,
|
||||||
)
|
interactionSource = cancelInteractionSource,
|
||||||
|
elevation = FloatingActionButtonDefaults.elevation(4.dp, 6.dp),
|
||||||
|
containerColor = Color.Gray
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.Center
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Filled.Close,
|
imageVector = Icons.Filled.Close,
|
||||||
contentDescription = null,
|
contentDescription = stringResource(android.R.string.cancel),
|
||||||
modifier = Modifier.size(16.dp)
|
modifier = Modifier.size(24.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
|
|
||||||
)
|
)
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = isCancelPressed,
|
||||||
|
enter = expandHorizontally() + fadeIn(),
|
||||||
|
exit = shrinkHorizontally() + fadeOut()
|
||||||
) {
|
) {
|
||||||
Icon(
|
Text(
|
||||||
imageVector = Icons.Filled.Check,
|
stringResource(android.R.string.cancel),
|
||||||
contentDescription = null,
|
modifier = Modifier.padding(end = 4.dp),
|
||||||
modifier = Modifier.size(16.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 = {
|
onClick = {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
viewModel.updateBatchPermissions(false)
|
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
|
containerColor = MaterialTheme.colorScheme.error
|
||||||
)
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.Center
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Filled.Block,
|
imageVector = Icons.Filled.Block,
|
||||||
contentDescription = null,
|
contentDescription = stringResource(R.string.batch_cancel_authorization),
|
||||||
modifier = Modifier.size(16.dp)
|
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()
|
.fillMaxSize()
|
||||||
.nestedScroll(scrollBehavior.nestedScrollConnection),
|
.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||||
contentPadding = PaddingValues(
|
contentPadding = PaddingValues(
|
||||||
top = 8.dp,
|
start = 16.dp,
|
||||||
bottom = if (viewModel.showBatchActions && viewModel.selectedApps.isNotEmpty()) 88.dp else 16.dp
|
end = 16.dp,
|
||||||
)
|
top = 16.dp,
|
||||||
|
bottom = 16.dp
|
||||||
|
),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
) {
|
) {
|
||||||
// 获取分组后的应用列表
|
// 获取分组后的应用列表
|
||||||
val rootApps = viewModel.appList.filter { it.allowSu }
|
val rootApps = viewModel.appList.filter { it.allowSu }
|
||||||
@@ -266,10 +499,6 @@ fun SuperUserScreen(navigator: DestinationsNavigator) {
|
|||||||
|
|
||||||
// 显示ROOT权限应用组
|
// 显示ROOT权限应用组
|
||||||
if (rootApps.isNotEmpty()) {
|
if (rootApps.isNotEmpty()) {
|
||||||
item {
|
|
||||||
GroupHeader(title = stringResource(R.string.apps_with_root))
|
|
||||||
}
|
|
||||||
|
|
||||||
items(rootApps, key = { "root_" + it.packageName + it.uid }) { app ->
|
items(rootApps, key = { "root_" + it.packageName + it.uid }) { app ->
|
||||||
AppItem(
|
AppItem(
|
||||||
app = app,
|
app = app,
|
||||||
@@ -280,7 +509,7 @@ fun SuperUserScreen(navigator: DestinationsNavigator) {
|
|||||||
val profile = Natives.getAppProfile(app.packageName, app.uid)
|
val profile = Natives.getAppProfile(app.packageName, app.uid)
|
||||||
val updatedProfile = profile.copy(allowSu = allowSu)
|
val updatedProfile = profile.copy(allowSu = allowSu)
|
||||||
if (Natives.setAppProfile(updatedProfile)) {
|
if (Natives.setAppProfile(updatedProfile)) {
|
||||||
viewModel.fetchAppList()
|
viewModel.updateAppProfileLocally(app.packageName, updatedProfile)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -305,10 +534,6 @@ fun SuperUserScreen(navigator: DestinationsNavigator) {
|
|||||||
|
|
||||||
// 显示自定义配置应用组
|
// 显示自定义配置应用组
|
||||||
if (customApps.isNotEmpty()) {
|
if (customApps.isNotEmpty()) {
|
||||||
item {
|
|
||||||
GroupHeader(title = stringResource(R.string.apps_with_custom_profile))
|
|
||||||
}
|
|
||||||
|
|
||||||
items(customApps, key = { "custom_" + it.packageName + it.uid }) { app ->
|
items(customApps, key = { "custom_" + it.packageName + it.uid }) { app ->
|
||||||
AppItem(
|
AppItem(
|
||||||
app = app,
|
app = app,
|
||||||
@@ -319,7 +544,7 @@ fun SuperUserScreen(navigator: DestinationsNavigator) {
|
|||||||
val profile = Natives.getAppProfile(app.packageName, app.uid)
|
val profile = Natives.getAppProfile(app.packageName, app.uid)
|
||||||
val updatedProfile = profile.copy(allowSu = allowSu)
|
val updatedProfile = profile.copy(allowSu = allowSu)
|
||||||
if (Natives.setAppProfile(updatedProfile)) {
|
if (Natives.setAppProfile(updatedProfile)) {
|
||||||
viewModel.fetchAppList()
|
viewModel.updateAppProfileLocally(app.packageName, updatedProfile)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -344,10 +569,6 @@ fun SuperUserScreen(navigator: DestinationsNavigator) {
|
|||||||
|
|
||||||
// 显示其他应用组
|
// 显示其他应用组
|
||||||
if (otherApps.isNotEmpty()) {
|
if (otherApps.isNotEmpty()) {
|
||||||
item {
|
|
||||||
GroupHeader(title = stringResource(R.string.other_apps))
|
|
||||||
}
|
|
||||||
|
|
||||||
items(otherApps, key = { "other_" + it.packageName + it.uid }) { app ->
|
items(otherApps, key = { "other_" + it.packageName + it.uid }) { app ->
|
||||||
AppItem(
|
AppItem(
|
||||||
app = app,
|
app = app,
|
||||||
@@ -358,7 +579,7 @@ fun SuperUserScreen(navigator: DestinationsNavigator) {
|
|||||||
val profile = Natives.getAppProfile(app.packageName, app.uid)
|
val profile = Natives.getAppProfile(app.packageName, app.uid)
|
||||||
val updatedProfile = profile.copy(allowSu = allowSu)
|
val updatedProfile = profile.copy(allowSu = allowSu)
|
||||||
if (Natives.setAppProfile(updatedProfile)) {
|
if (Natives.setAppProfile(updatedProfile)) {
|
||||||
viewModel.fetchAppList()
|
viewModel.updateAppProfileLocally(app.packageName, updatedProfile)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -395,7 +616,7 @@ fun SuperUserScreen(navigator: DestinationsNavigator) {
|
|||||||
verticalArrangement = Arrangement.Center
|
verticalArrangement = Arrangement.Center
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Filled.Apps,
|
imageVector = Icons.Filled.Archive,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.6f),
|
tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.6f),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -406,7 +627,6 @@ fun SuperUserScreen(navigator: DestinationsNavigator) {
|
|||||||
text = stringResource(R.string.no_apps_found),
|
text = stringResource(R.string.no_apps_found),
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -417,26 +637,7 @@ fun SuperUserScreen(navigator: DestinationsNavigator) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@OptIn(ExperimentalLayoutApi::class, ExperimentalMaterial3Api::class)
|
||||||
fun GroupHeader(title: String) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.background(MaterialTheme.colorScheme.surfaceContainerHighest.copy(alpha = 0.7f))
|
|
||||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = title,
|
|
||||||
style = TextStyle(
|
|
||||||
fontSize = 14.sp,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
color = MaterialTheme.colorScheme.primary
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalLayoutApi::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun AppItem(
|
private fun AppItem(
|
||||||
app: SuperUserViewModel.AppInfo,
|
app: SuperUserViewModel.AppInfo,
|
||||||
@@ -447,35 +648,10 @@ private fun AppItem(
|
|||||||
onLongClick: () -> Unit,
|
onLongClick: () -> Unit,
|
||||||
viewModel: SuperUserViewModel
|
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(
|
Card(
|
||||||
colors = CardDefaults.cardColors(containerColor = cardColor),
|
colors = getCardColors(MaterialTheme.colorScheme.surfaceContainerHigh),
|
||||||
elevation = CardDefaults.cardElevation(defaultElevation = 0.dp),
|
elevation = getCardElevation(),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 8.dp, vertical = 4.dp)
|
|
||||||
.clip(MaterialTheme.shapes.medium)
|
|
||||||
.shadow(
|
|
||||||
elevation = 0.dp,
|
|
||||||
shape = MaterialTheme.shapes.medium,
|
|
||||||
spotColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
|
|
||||||
)
|
|
||||||
.then(
|
|
||||||
if (isSelected)
|
|
||||||
Modifier.border(
|
|
||||||
width = 2.dp,
|
|
||||||
color = MaterialTheme.colorScheme.primary,
|
|
||||||
shape = MaterialTheme.shapes.medium
|
|
||||||
)
|
|
||||||
else
|
|
||||||
Modifier
|
|
||||||
)
|
|
||||||
.pointerInput(Unit) {
|
.pointerInput(Unit) {
|
||||||
detectTapGestures(
|
detectTapGestures(
|
||||||
onLongPress = { onLongClick() },
|
onLongPress = { onLongClick() },
|
||||||
@@ -509,17 +685,13 @@ private fun AppItem(
|
|||||||
Text(
|
Text(
|
||||||
text = app.label,
|
text = app.label,
|
||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis
|
|
||||||
)
|
)
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = app.packageName,
|
text = app.packageName,
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis
|
|
||||||
)
|
)
|
||||||
|
|
||||||
FlowRow(
|
FlowRow(
|
||||||
@@ -527,51 +699,81 @@ private fun AppItem(
|
|||||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||||
) {
|
) {
|
||||||
if (app.allowSu) {
|
if (app.allowSu) {
|
||||||
LabelText(label = "ROOT", backgroundColor = MaterialTheme.colorScheme.primary)
|
LabelItem(text = "ROOT")
|
||||||
}
|
}
|
||||||
if (Natives.uidShouldUmount(app.uid)) {
|
if (Natives.uidShouldUmount(app.uid)) {
|
||||||
LabelText(label = "UMOUNT", backgroundColor = MaterialTheme.colorScheme.tertiary)
|
LabelItem(text = "UNMOUNT")
|
||||||
}
|
}
|
||||||
if (app.hasCustomProfile) {
|
if (app.hasCustomProfile) {
|
||||||
LabelText(label = "CUSTOM", backgroundColor = MaterialTheme.colorScheme.secondary)
|
LabelItem(text = "CUSTOM")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!viewModel.showBatchActions) {
|
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(
|
Switch(
|
||||||
checked = app.allowSu,
|
checked = app.allowSu,
|
||||||
onCheckedChange = onSwitchChange,
|
onCheckedChange = onSwitchChange,
|
||||||
colors = SwitchDefaults.colors(
|
interactionSource = switchInteractionSource,
|
||||||
checkedThumbColor = MaterialTheme.colorScheme.onPrimary,
|
|
||||||
checkedTrackColor = MaterialTheme.colorScheme.primary,
|
|
||||||
checkedIconColor = MaterialTheme.colorScheme.primary,
|
|
||||||
uncheckedThumbColor = MaterialTheme.colorScheme.outline,
|
|
||||||
uncheckedTrackColor = MaterialTheme.colorScheme.surfaceVariant,
|
|
||||||
uncheckedIconColor = MaterialTheme.colorScheme.surfaceVariant
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
|
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(
|
Checkbox(
|
||||||
checked = isSelected,
|
checked = isSelected,
|
||||||
onCheckedChange = { onToggleSelection() },
|
onCheckedChange = { onToggleSelection() },
|
||||||
colors = CheckboxDefaults.colors(
|
interactionSource = checkboxInteractionSource,
|
||||||
checkedColor = MaterialTheme.colorScheme.primary,
|
|
||||||
uncheckedColor = MaterialTheme.colorScheme.outline
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun LabelText(label: String, backgroundColor: Color) {
|
fun LabelText(label: String) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(top = 2.dp, end = 2.dp)
|
.padding(top = 2.dp, end = 2.dp)
|
||||||
.background(
|
.background(
|
||||||
backgroundColor,
|
Color.Black,
|
||||||
shape = RoundedCornerShape(4.dp)
|
shape = RoundedCornerShape(4.dp)
|
||||||
)
|
)
|
||||||
.clip(RoundedCornerShape(4.dp))
|
.clip(RoundedCornerShape(4.dp))
|
||||||
@@ -580,9 +782,8 @@ fun LabelText(label: String, backgroundColor: Color) {
|
|||||||
text = label,
|
text = label,
|
||||||
modifier = Modifier.padding(vertical = 2.dp, horizontal = 6.dp),
|
modifier = Modifier.padding(vertical = 2.dp, horizontal = 6.dp),
|
||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
fontSize = 10.sp,
|
fontSize = 8.sp,
|
||||||
color = Color.White,
|
color = Color.White,
|
||||||
fontWeight = FontWeight.Medium
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -207,13 +207,13 @@ private fun TemplateItem(
|
|||||||
)
|
)
|
||||||
Text(template.description)
|
Text(template.description)
|
||||||
FlowRow {
|
FlowRow {
|
||||||
LabelText(label = "UID: ${template.uid}", backgroundColor = MaterialTheme.colorScheme.surface)
|
LabelText(label = "UID: ${template.uid}")
|
||||||
LabelText(label = "GID: ${template.gid}", backgroundColor = MaterialTheme.colorScheme.surface)
|
LabelText(label = "GID: ${template.gid}")
|
||||||
LabelText(label = template.context, backgroundColor = MaterialTheme.colorScheme.surface)
|
LabelText(label = template.context,)
|
||||||
if (template.local) {
|
if (template.local) {
|
||||||
LabelText(label = "local", backgroundColor = MaterialTheme.colorScheme.surface)
|
LabelText(label = "local")
|
||||||
} else {
|
} else {
|
||||||
LabelText(label = "remote", backgroundColor = MaterialTheme.colorScheme.surface)
|
LabelText(label = "remote")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -231,7 +231,12 @@ private fun TopBar(
|
|||||||
colors: TopAppBarColors,
|
colors: TopAppBarColors,
|
||||||
scrollBehavior: TopAppBarScrollBehavior? = null
|
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
|
val cardAlpha = CardConfig.cardAlpha
|
||||||
|
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
|
|||||||
@@ -5,21 +5,23 @@ import androidx.compose.foundation.isSystemInDarkTheme
|
|||||||
import androidx.compose.material3.CardDefaults
|
import androidx.compose.material3.CardDefaults
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.luminance
|
import androidx.compose.ui.graphics.luminance
|
||||||
import androidx.compose.ui.unit.Dp
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
object CardConfig {
|
object CardConfig {
|
||||||
val settingElevation: Dp = 4.dp
|
// 卡片透明度
|
||||||
val customBackgroundElevation: Dp = 0.dp
|
var cardAlpha by mutableFloatStateOf(1f)
|
||||||
|
// 卡片亮度
|
||||||
var cardAlpha by mutableStateOf(1f)
|
var cardDim by mutableFloatStateOf(0f)
|
||||||
var cardElevation by mutableStateOf(settingElevation)
|
// 卡片阴影
|
||||||
|
var cardElevation by mutableStateOf(0.dp)
|
||||||
var isShadowEnabled by mutableStateOf(true)
|
var isShadowEnabled by mutableStateOf(true)
|
||||||
var isCustomAlphaSet by mutableStateOf(false)
|
var isCustomAlphaSet by mutableStateOf(false)
|
||||||
|
var isCustomDimSet by mutableStateOf(false)
|
||||||
var isUserDarkModeEnabled by mutableStateOf(false)
|
var isUserDarkModeEnabled by mutableStateOf(false)
|
||||||
var isUserLightModeEnabled by mutableStateOf(false)
|
var isUserLightModeEnabled by mutableStateOf(false)
|
||||||
var isCustomBackgroundEnabled by mutableStateOf(false)
|
var isCustomBackgroundEnabled by mutableStateOf(false)
|
||||||
@@ -31,9 +33,11 @@ object CardConfig {
|
|||||||
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||||
prefs.edit().apply {
|
prefs.edit().apply {
|
||||||
putFloat("card_alpha", cardAlpha)
|
putFloat("card_alpha", cardAlpha)
|
||||||
|
putFloat("card_dim", cardDim)
|
||||||
putBoolean("custom_background_enabled", isCustomBackgroundEnabled)
|
putBoolean("custom_background_enabled", isCustomBackgroundEnabled)
|
||||||
putBoolean("is_shadow_enabled", isShadowEnabled)
|
putBoolean("is_shadow_enabled", isShadowEnabled)
|
||||||
putBoolean("is_custom_alpha_set", isCustomAlphaSet)
|
putBoolean("is_custom_alpha_set", isCustomAlphaSet)
|
||||||
|
putBoolean("is_custom_dim_set", isCustomDimSet)
|
||||||
putBoolean("is_user_dark_mode_enabled", isUserDarkModeEnabled)
|
putBoolean("is_user_dark_mode_enabled", isUserDarkModeEnabled)
|
||||||
putBoolean("is_user_light_mode_enabled", isUserLightModeEnabled)
|
putBoolean("is_user_light_mode_enabled", isUserLightModeEnabled)
|
||||||
apply()
|
apply()
|
||||||
@@ -46,9 +50,11 @@ object CardConfig {
|
|||||||
fun load(context: Context) {
|
fun load(context: Context) {
|
||||||
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||||
cardAlpha = prefs.getFloat("card_alpha", 1f)
|
cardAlpha = prefs.getFloat("card_alpha", 1f)
|
||||||
|
cardDim = prefs.getFloat("card_dim", 0f)
|
||||||
isCustomBackgroundEnabled = prefs.getBoolean("custom_background_enabled", false)
|
isCustomBackgroundEnabled = prefs.getBoolean("custom_background_enabled", false)
|
||||||
isShadowEnabled = prefs.getBoolean("is_shadow_enabled", true)
|
isShadowEnabled = prefs.getBoolean("is_shadow_enabled", true)
|
||||||
isCustomAlphaSet = prefs.getBoolean("is_custom_alpha_set", false)
|
isCustomAlphaSet = prefs.getBoolean("is_custom_alpha_set", false)
|
||||||
|
isCustomDimSet = prefs.getBoolean("is_custom_dim_set", false)
|
||||||
isUserDarkModeEnabled = prefs.getBoolean("is_user_dark_mode_enabled", false)
|
isUserDarkModeEnabled = prefs.getBoolean("is_user_dark_mode_enabled", false)
|
||||||
isUserLightModeEnabled = prefs.getBoolean("is_user_light_mode_enabled", false)
|
isUserLightModeEnabled = prefs.getBoolean("is_user_light_mode_enabled", false)
|
||||||
updateShadowEnabled(isShadowEnabled)
|
updateShadowEnabled(isShadowEnabled)
|
||||||
@@ -59,22 +65,19 @@ object CardConfig {
|
|||||||
*/
|
*/
|
||||||
fun updateShadowEnabled(enabled: Boolean) {
|
fun updateShadowEnabled(enabled: Boolean) {
|
||||||
isShadowEnabled = enabled
|
isShadowEnabled = enabled
|
||||||
cardElevation = if (isCustomBackgroundEnabled && cardAlpha != 1f) {
|
cardElevation = 0.dp
|
||||||
customBackgroundElevation
|
|
||||||
} else if (enabled) {
|
|
||||||
settingElevation
|
|
||||||
} else {
|
|
||||||
customBackgroundElevation
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 设置深色模式默认值
|
* 设置主题模式默认值
|
||||||
*/
|
*/
|
||||||
fun setDarkModeDefaults() {
|
fun setThemeDefaults(isDarkMode: Boolean) {
|
||||||
if (!isCustomAlphaSet) {
|
if (!isCustomAlphaSet) {
|
||||||
cardAlpha = 1f
|
cardAlpha = 1f
|
||||||
}
|
}
|
||||||
|
if (!isCustomDimSet) {
|
||||||
|
cardDim = if (isDarkMode) 0.5f else 0f
|
||||||
|
}
|
||||||
updateShadowEnabled(isShadowEnabled)
|
updateShadowEnabled(isShadowEnabled)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -88,6 +91,19 @@ fun getCardColors(originalColor: Color) = CardDefaults.cardColors(
|
|||||||
contentColor = determineContentColor(originalColor)
|
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
|
import androidx.compose.ui.graphics.Color
|
||||||
|
|
||||||
sealed class ThemeColors {
|
sealed class ThemeColors {
|
||||||
abstract val Primary: Color
|
// 浅色
|
||||||
abstract val Secondary: Color
|
abstract val primaryLight: Color
|
||||||
abstract val Tertiary: Color
|
abstract val onPrimaryLight: Color
|
||||||
abstract val OnPrimary: Color
|
abstract val primaryContainerLight: Color
|
||||||
abstract val OnSecondary: Color
|
abstract val onPrimaryContainerLight: Color
|
||||||
abstract val OnTertiary: Color
|
abstract val secondaryLight: Color
|
||||||
abstract val PrimaryContainer: Color
|
abstract val onSecondaryLight: Color
|
||||||
abstract val SecondaryContainer: Color
|
abstract val secondaryContainerLight: Color
|
||||||
abstract val TertiaryContainer: Color
|
abstract val onSecondaryContainerLight: Color
|
||||||
abstract val OnPrimaryContainer: Color
|
abstract val tertiaryLight: Color
|
||||||
abstract val OnSecondaryContainer: Color
|
abstract val onTertiaryLight: Color
|
||||||
abstract val OnTertiaryContainer: Color
|
abstract val tertiaryContainerLight: Color
|
||||||
abstract val ButtonContrast: Color
|
abstract val onTertiaryContainerLight: Color
|
||||||
|
abstract val errorLight: Color
|
||||||
// 表面颜色
|
abstract val onErrorLight: Color
|
||||||
abstract val Surface: Color
|
abstract val errorContainerLight: Color
|
||||||
abstract val SurfaceVariant: Color
|
abstract val onErrorContainerLight: Color
|
||||||
abstract val OnSurface: Color
|
abstract val backgroundLight: Color
|
||||||
abstract val OnSurfaceVariant: Color
|
abstract val onBackgroundLight: Color
|
||||||
|
abstract val surfaceLight: Color
|
||||||
// 错误状态颜色
|
abstract val onSurfaceLight: Color
|
||||||
abstract val Error: Color
|
abstract val surfaceVariantLight: Color
|
||||||
abstract val OnError: Color
|
abstract val onSurfaceVariantLight: Color
|
||||||
abstract val ErrorContainer: Color
|
abstract val outlineLight: Color
|
||||||
abstract val OnErrorContainer: Color
|
abstract val outlineVariantLight: Color
|
||||||
|
abstract val scrimLight: Color
|
||||||
// 边框和背景色
|
abstract val inverseSurfaceLight: Color
|
||||||
abstract val Outline: Color
|
abstract val inverseOnSurfaceLight: Color
|
||||||
abstract val OutlineVariant: Color
|
abstract val inversePrimaryLight: Color
|
||||||
abstract val Background: Color
|
abstract val surfaceDimLight: Color
|
||||||
abstract val OnBackground: 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() {
|
object Default : ThemeColors() {
|
||||||
override val Primary = Color(0xFF2196F3)
|
override val primaryLight = Color(0xFF415F91)
|
||||||
override val Secondary = Color(0xFF64B5F6)
|
override val onPrimaryLight = Color(0xFFFFFFFF)
|
||||||
override val Tertiary = Color(0xFF0D47A1)
|
override val primaryContainerLight = Color(0xFFD6E3FF)
|
||||||
override val OnPrimary = Color(0xFFFFFFFF)
|
override val onPrimaryContainerLight = Color(0xFF284777)
|
||||||
override val OnSecondary = Color(0xFFFFFFFF)
|
override val secondaryLight = Color(0xFF565F71)
|
||||||
override val OnTertiary = Color(0xFFFFFFFF)
|
override val onSecondaryLight = Color(0xFFFFFFFF)
|
||||||
override val PrimaryContainer = Color(0xFFD6EAFF)
|
override val secondaryContainerLight = Color(0xFFDAE2F9)
|
||||||
override val SecondaryContainer = Color(0xFFE3F2FD)
|
override val onSecondaryContainerLight = Color(0xFF3E4759)
|
||||||
override val TertiaryContainer = Color(0xFFCFD8DC)
|
override val tertiaryLight = Color(0xFF705575)
|
||||||
override val OnPrimaryContainer = Color(0xFF0A3049)
|
override val onTertiaryLight = Color(0xFFFFFFFF)
|
||||||
override val OnSecondaryContainer = Color(0xFF0D3C61)
|
override val tertiaryContainerLight = Color(0xFFFAD8FD)
|
||||||
override val OnTertiaryContainer = Color(0xFF071D41)
|
override val onTertiaryContainerLight = Color(0xFF573E5C)
|
||||||
override val ButtonContrast = Color(0xFF2196F3)
|
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 primaryDark = Color(0xFFAAC7FF)
|
||||||
override val SurfaceVariant = Color(0xFFEDF5FE)
|
override val onPrimaryDark = Color(0xFF0A305F)
|
||||||
override val OnSurface = Color(0xFF1A1C1E)
|
override val primaryContainerDark = Color(0xFF284777)
|
||||||
override val OnSurfaceVariant = Color(0xFF42474E)
|
override val onPrimaryContainerDark = Color(0xFFD6E3FF)
|
||||||
|
override val secondaryDark = Color(0xFFBEC6DC)
|
||||||
override val Error = Color(0xFFB00020)
|
override val onSecondaryDark = Color(0xFF283141)
|
||||||
override val OnError = Color(0xFFFFFFFF)
|
override val secondaryContainerDark = Color(0xFF3E4759)
|
||||||
override val ErrorContainer = Color(0xFFFDE7E9)
|
override val onSecondaryContainerDark = Color(0xFFDAE2F9)
|
||||||
override val OnErrorContainer = Color(0xFF410008)
|
override val tertiaryDark = Color(0xFFDDBCE0)
|
||||||
|
override val onTertiaryDark = Color(0xFF3F2844)
|
||||||
override val Outline = Color(0xFFBAC3CF)
|
override val tertiaryContainerDark = Color(0xFF573E5C)
|
||||||
override val OutlineVariant = Color(0xFFDFE3EB)
|
override val onTertiaryContainerDark = Color(0xFFFAD8FD)
|
||||||
override val Background = Color(0xFFFAFCFF)
|
override val errorDark = Color(0xFFFFB4AB)
|
||||||
override val OnBackground = Color(0xFF1A1C1E)
|
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() {
|
object Green : ThemeColors() {
|
||||||
override val Primary = Color(0xFF43A047)
|
override val primaryLight = Color(0xFF4C662B)
|
||||||
override val Secondary = Color(0xFF66BB6A)
|
override val onPrimaryLight = Color(0xFFFFFFFF)
|
||||||
override val Tertiary = Color(0xFF1B5E20)
|
override val primaryContainerLight = Color(0xFFCDEDA3)
|
||||||
override val OnPrimary = Color(0xFFFFFFFF)
|
override val onPrimaryContainerLight = Color(0xFF354E16)
|
||||||
override val OnSecondary = Color(0xFFFFFFFF)
|
override val secondaryLight = Color(0xFF586249)
|
||||||
override val OnTertiary = Color(0xFFFFFFFF)
|
override val onSecondaryLight = Color(0xFFFFFFFF)
|
||||||
override val PrimaryContainer = Color(0xFFD8EFDB)
|
override val secondaryContainerLight = Color(0xFFDCE7C8)
|
||||||
override val SecondaryContainer = Color(0xFFE8F5E9)
|
override val onSecondaryContainerLight = Color(0xFF404A33)
|
||||||
override val TertiaryContainer = Color(0xFFB9F6CA)
|
override val tertiaryLight = Color(0xFF386663)
|
||||||
override val OnPrimaryContainer = Color(0xFF0A280D)
|
override val onTertiaryLight = Color(0xFFFFFFFF)
|
||||||
override val OnSecondaryContainer = Color(0xFF0E2912)
|
override val tertiaryContainerLight = Color(0xFFBCECE7)
|
||||||
override val OnTertiaryContainer = Color(0xFF051B07)
|
override val onTertiaryContainerLight = Color(0xFF1F4E4B)
|
||||||
override val ButtonContrast = Color(0xFF43A047)
|
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 primaryDark = Color(0xFFB1D18A)
|
||||||
override val SurfaceVariant = Color(0xFFEDF7EE)
|
override val onPrimaryDark = Color(0xFF1F3701)
|
||||||
override val OnSurface = Color(0xFF191C19)
|
override val primaryContainerDark = Color(0xFF354E16)
|
||||||
override val OnSurfaceVariant = Color(0xFF414941)
|
override val onPrimaryContainerDark = Color(0xFFCDEDA3)
|
||||||
|
override val secondaryDark = Color(0xFFBFCBAD)
|
||||||
override val Error = Color(0xFFC62828)
|
override val onSecondaryDark = Color(0xFF2A331E)
|
||||||
override val OnError = Color(0xFFFFFFFF)
|
override val secondaryContainerDark = Color(0xFF404A33)
|
||||||
override val ErrorContainer = Color(0xFFF8D7DA)
|
override val onSecondaryContainerDark = Color(0xFFDCE7C8)
|
||||||
override val OnErrorContainer = Color(0xFF4A0808)
|
override val tertiaryDark = Color(0xFFA0D0CB)
|
||||||
|
override val onTertiaryDark = Color(0xFF003735)
|
||||||
override val Outline = Color(0xFFBDC9BF)
|
override val tertiaryContainerDark = Color(0xFF1F4E4B)
|
||||||
override val OutlineVariant = Color(0xFFDDE6DE)
|
override val onTertiaryContainerDark = Color(0xFFBCECE7)
|
||||||
override val Background = Color(0xFFFBFDFB)
|
override val errorDark = Color(0xFFFFB4AB)
|
||||||
override val OnBackground = Color(0xFF191C19)
|
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() {
|
object Purple : ThemeColors() {
|
||||||
override val Primary = Color(0xFF9C27B0)
|
override val primaryLight = Color(0xFF7C4E7E)
|
||||||
override val Secondary = Color(0xFFBA68C8)
|
override val onPrimaryLight = Color(0xFFFFFFFF)
|
||||||
override val Tertiary = Color(0xFF6A1B9A)
|
override val primaryContainerLight = Color(0xFFFFD6FC)
|
||||||
override val OnPrimary = Color(0xFFFFFFFF)
|
override val onPrimaryContainerLight = Color(0xFF623765)
|
||||||
override val OnSecondary = Color(0xFFFFFFFF)
|
override val secondaryLight = Color(0xFF6C586B)
|
||||||
override val OnTertiary = Color(0xFFFFFFFF)
|
override val onSecondaryLight = Color(0xFFFFFFFF)
|
||||||
override val PrimaryContainer = Color(0xFFF3D8F8)
|
override val secondaryContainerLight = Color(0xFFF5DBF1)
|
||||||
override val SecondaryContainer = Color(0xFFF5E9F7)
|
override val onSecondaryContainerLight = Color(0xFF534152)
|
||||||
override val TertiaryContainer = Color(0xFFE1BEE7)
|
override val tertiaryLight = Color(0xFF825249)
|
||||||
override val OnPrimaryContainer = Color(0xFF2A0934)
|
override val onTertiaryLight = Color(0xFFFFFFFF)
|
||||||
override val OnSecondaryContainer = Color(0xFF3C0F50)
|
override val tertiaryContainerLight = Color(0xFFFFDAD4)
|
||||||
override val OnTertiaryContainer = Color(0xFF1D0830)
|
override val onTertiaryContainerLight = Color(0xFF673B33)
|
||||||
override val ButtonContrast = Color(0xFF9C27B0)
|
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 primaryDark = Color(0xFFECB4EC)
|
||||||
override val SurfaceVariant = Color(0xFFF5EEFA)
|
override val onPrimaryDark = Color(0xFF49204D)
|
||||||
override val OnSurface = Color(0xFF1D1B1E)
|
override val primaryContainerDark = Color(0xFF623765)
|
||||||
override val OnSurfaceVariant = Color(0xFF49454E)
|
override val onPrimaryContainerDark = Color(0xFFFFD6FC)
|
||||||
|
override val secondaryDark = Color(0xFFD8BFD5)
|
||||||
override val Error = Color(0xFFD50000)
|
override val onSecondaryDark = Color(0xFF3B2B3B)
|
||||||
override val OnError = Color(0xFFFFFFFF)
|
override val secondaryContainerDark = Color(0xFF534152)
|
||||||
override val ErrorContainer = Color(0xFFFFDCD5)
|
override val onSecondaryContainerDark = Color(0xFFF5DBF1)
|
||||||
override val OnErrorContainer = Color(0xFF480000)
|
override val tertiaryDark = Color(0xFFF6B8AD)
|
||||||
|
override val onTertiaryDark = Color(0xFF4C251F)
|
||||||
override val Outline = Color(0xFFC9B9D0)
|
override val tertiaryContainerDark = Color(0xFF673B33)
|
||||||
override val OutlineVariant = Color(0xFFE8DAED)
|
override val onTertiaryContainerDark = Color(0xFFFFDAD4)
|
||||||
override val Background = Color(0xFFFFFBFF)
|
override val errorDark = Color(0xFFFFB4AB)
|
||||||
override val OnBackground = Color(0xFF1D1B1E)
|
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() {
|
object Orange : ThemeColors() {
|
||||||
override val Primary = Color(0xFFFF9800)
|
override val primaryLight = Color(0xFF8B4F24)
|
||||||
override val Secondary = Color(0xFFFFB74D)
|
override val onPrimaryLight = Color(0xFFFFFFFF)
|
||||||
override val Tertiary = Color(0xFFE65100)
|
override val primaryContainerLight = Color(0xFFFFDCC7)
|
||||||
override val OnPrimary = Color(0xFFFFFFFF)
|
override val onPrimaryContainerLight = Color(0xFF6E390E)
|
||||||
override val OnSecondary = Color(0xFF000000)
|
override val secondaryLight = Color(0xFF755846)
|
||||||
override val OnTertiary = Color(0xFFFFFFFF)
|
override val onSecondaryLight = Color(0xFFFFFFFF)
|
||||||
override val PrimaryContainer = Color(0xFFFFECCC)
|
override val secondaryContainerLight = Color(0xFFFFDCC7)
|
||||||
override val SecondaryContainer = Color(0xFFFFF0D9)
|
override val onSecondaryContainerLight = Color(0xFF5B4130)
|
||||||
override val TertiaryContainer = Color(0xFFFFD180)
|
override val tertiaryLight = Color(0xFF865219)
|
||||||
override val OnPrimaryContainer = Color(0xFF351F00)
|
override val onTertiaryLight = Color(0xFFFFFFFF)
|
||||||
override val OnSecondaryContainer = Color(0xFF3D2800)
|
override val tertiaryContainerLight = Color(0xFFFFDCBF)
|
||||||
override val OnTertiaryContainer = Color(0xFF2E1500)
|
override val onTertiaryContainerLight = Color(0xFF6A3B01)
|
||||||
override val ButtonContrast = Color(0xFFFF9800)
|
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 primaryDark = Color(0xFFFFB787)
|
||||||
override val SurfaceVariant = Color(0xFFFFF0E6)
|
override val onPrimaryDark = Color(0xFF502400)
|
||||||
override val OnSurface = Color(0xFF1F1B16)
|
override val primaryContainerDark = Color(0xFF6E390E)
|
||||||
override val OnSurfaceVariant = Color(0xFF4E4639)
|
override val onPrimaryContainerDark = Color(0xFFFFDCC7)
|
||||||
|
override val secondaryDark = Color(0xFFE5BFA8)
|
||||||
override val Error = Color(0xFFD32F2F)
|
override val onSecondaryDark = Color(0xFF422B1B)
|
||||||
override val OnError = Color(0xFFFFFFFF)
|
override val secondaryContainerDark = Color(0xFF5B4130)
|
||||||
override val ErrorContainer = Color(0xFFFFDBC8)
|
override val onSecondaryContainerDark = Color(0xFFFFDCC7)
|
||||||
override val OnErrorContainer = Color(0xFF490700)
|
override val tertiaryDark = Color(0xFFFDB876)
|
||||||
|
override val onTertiaryDark = Color(0xFF4B2800)
|
||||||
override val Outline = Color(0xFFD6C3AD)
|
override val tertiaryContainerDark = Color(0xFF6A3B01)
|
||||||
override val OutlineVariant = Color(0xFFEFDFCC)
|
override val onTertiaryContainerDark = Color(0xFFFFDCBF)
|
||||||
override val Background = Color(0xFFFFFBFF)
|
override val errorDark = Color(0xFFFFB4AB)
|
||||||
override val OnBackground = Color(0xFF1F1B16)
|
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() {
|
object Pink : ThemeColors() {
|
||||||
override val Primary = Color(0xFFE91E63)
|
override val primaryLight = Color(0xFF8C4A60)
|
||||||
override val Secondary = Color(0xFFF06292)
|
override val onPrimaryLight = Color(0xFFFFFFFF)
|
||||||
override val Tertiary = Color(0xFF880E4F)
|
override val primaryContainerLight = Color(0xFFFFD9E2)
|
||||||
override val OnPrimary = Color(0xFFFFFFFF)
|
override val onPrimaryContainerLight = Color(0xFF703348)
|
||||||
override val OnSecondary = Color(0xFFFFFFFF)
|
override val secondaryLight = Color(0xFF8B4A62)
|
||||||
override val OnTertiary = Color(0xFFFFFFFF)
|
override val onSecondaryLight = Color(0xFFFFFFFF)
|
||||||
override val PrimaryContainer = Color(0xFFFCE4EC)
|
override val secondaryContainerLight = Color(0xFFFFD9E3)
|
||||||
override val SecondaryContainer = Color(0xFFFCE4EC)
|
override val onSecondaryContainerLight = Color(0xFF6F334B)
|
||||||
override val TertiaryContainer = Color(0xFFF8BBD0)
|
override val tertiaryLight = Color(0xFF8B4A62)
|
||||||
override val OnPrimaryContainer = Color(0xFF3B0819)
|
override val onTertiaryLight = Color(0xFFFFFFFF)
|
||||||
override val OnSecondaryContainer = Color(0xFF3B0819)
|
override val tertiaryContainerLight = Color(0xFFFFD9E3)
|
||||||
override val OnTertiaryContainer = Color(0xFF2B0516)
|
override val onTertiaryContainerLight = Color(0xFF6F334B)
|
||||||
override val ButtonContrast = Color(0xFFE91E63)
|
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 primaryDark = Color(0xFFFFB1C7)
|
||||||
override val SurfaceVariant = Color(0xFFFCEEF2)
|
override val onPrimaryDark = Color(0xFF541D32)
|
||||||
override val OnSurface = Color(0xFF201A1C)
|
override val primaryContainerDark = Color(0xFF703348)
|
||||||
override val OnSurfaceVariant = Color(0xFF534347)
|
override val onPrimaryContainerDark = Color(0xFFFFD9E2)
|
||||||
|
override val secondaryDark = Color(0xFFFFB0CB)
|
||||||
override val Error = Color(0xFFB71C1C)
|
override val onSecondaryDark = Color(0xFF541D34)
|
||||||
override val OnError = Color(0xFFFFFFFF)
|
override val secondaryContainerDark = Color(0xFF6F334B)
|
||||||
override val ErrorContainer = Color(0xFFFFDAD6)
|
override val onSecondaryContainerDark = Color(0xFFFFD9E3)
|
||||||
override val OnErrorContainer = Color(0xFF410002)
|
override val tertiaryDark = Color(0xFFFFB0CB)
|
||||||
|
override val onTertiaryDark = Color(0xFF541D34)
|
||||||
override val Outline = Color(0xFFD6BABF)
|
override val tertiaryContainerDark = Color(0xFF6F334B)
|
||||||
override val OutlineVariant = Color(0xFFEFDDE0)
|
override val onTertiaryContainerDark = Color(0xFFFFD9E3)
|
||||||
override val Background = Color(0xFFFFFBFF)
|
override val errorDark = Color(0xFFFFB4AB)
|
||||||
override val OnBackground = Color(0xFF201A1C)
|
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() {
|
object Gray : ThemeColors() {
|
||||||
override val Primary = Color(0xFF607D8B)
|
override val primaryLight = Color(0xFF5B5C5C)
|
||||||
override val Secondary = Color(0xFF90A4AE)
|
override val onPrimaryLight = Color(0xFFFFFFFF)
|
||||||
override val Tertiary = Color(0xFF455A64)
|
override val primaryContainerLight = Color(0xFF747474)
|
||||||
override val OnPrimary = Color(0xFFFFFFFF)
|
override val onPrimaryContainerLight = Color(0xFFFEFCFC)
|
||||||
override val OnSecondary = Color(0xFFFFFFFF)
|
override val secondaryLight = Color(0xFF5F5E5E)
|
||||||
override val OnTertiary = Color(0xFFFFFFFF)
|
override val onSecondaryLight = Color(0xFFFFFFFF)
|
||||||
override val PrimaryContainer = Color(0xFFECEFF1)
|
override val secondaryContainerLight = Color(0xFFE4E2E1)
|
||||||
override val SecondaryContainer = Color(0xFFECEFF1)
|
override val onSecondaryContainerLight = Color(0xFF656464)
|
||||||
override val TertiaryContainer = Color(0xFFCFD8DC)
|
override val tertiaryLight = Color(0xFF5E5B5D)
|
||||||
override val OnPrimaryContainer = Color(0xFF1A2327)
|
override val onTertiaryLight = Color(0xFFFFFFFF)
|
||||||
override val OnSecondaryContainer = Color(0xFF1A2327)
|
override val tertiaryContainerLight = Color(0xFF777375)
|
||||||
override val OnTertiaryContainer = Color(0xFF121A1D)
|
override val onTertiaryContainerLight = Color(0xFFFFFBFF)
|
||||||
override val ButtonContrast = Color(0xFF607D8B)
|
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 primaryDark = Color(0xFFC7C6C6)
|
||||||
override val SurfaceVariant = Color(0xFFEEF2F4)
|
override val onPrimaryDark = Color(0xFF303031)
|
||||||
override val OnSurface = Color(0xFF191C1E)
|
override val primaryContainerDark = Color(0xFF919190)
|
||||||
override val OnSurfaceVariant = Color(0xFF41484D)
|
override val onPrimaryContainerDark = Color(0xFF161718)
|
||||||
|
override val secondaryDark = Color(0xFFC8C6C5)
|
||||||
override val Error = Color(0xFFC62828)
|
override val onSecondaryDark = Color(0xFF303030)
|
||||||
override val OnError = Color(0xFFFFFFFF)
|
override val secondaryContainerDark = Color(0xFF474746)
|
||||||
override val ErrorContainer = Color(0xFFFFDAD6)
|
override val onSecondaryContainerDark = Color(0xFFB7B5B4)
|
||||||
override val OnErrorContainer = Color(0xFF410002)
|
override val tertiaryDark = Color(0xFFCAC5C7)
|
||||||
|
override val onTertiaryDark = Color(0xFF323031)
|
||||||
override val Outline = Color(0xFFBDC1C4)
|
override val tertiaryContainerDark = Color(0xFF948F91)
|
||||||
override val OutlineVariant = Color(0xFFDDE1E3)
|
override val onTertiaryContainerDark = Color(0xFF181718)
|
||||||
override val Background = Color(0xFFFBFCFE)
|
override val errorDark = Color(0xFFFFB4AB)
|
||||||
override val OnBackground = Color(0xFF191C1E)
|
override val onErrorDark = Color(0xFF690005)
|
||||||
|
override val errorContainerDark = Color(0xFF93000A)
|
||||||
|
override val onErrorContainerDark = Color(0xFFFFDAD6)
|
||||||
|
override val backgroundDark = Color(0xFF141313)
|
||||||
|
override val onBackgroundDark = Color(0xFFE5E2E1)
|
||||||
|
override val surfaceDark = Color(0xFF141313)
|
||||||
|
override val onSurfaceDark = Color(0xFFE5E2E1)
|
||||||
|
override val surfaceVariantDark = Color(0xFF444748)
|
||||||
|
override val onSurfaceVariantDark = Color(0xFFC4C7C7)
|
||||||
|
override val outlineDark = Color(0xFF8E9192)
|
||||||
|
override val outlineVariantDark = Color(0xFF444748)
|
||||||
|
override val scrimDark = Color(0xFF000000)
|
||||||
|
override val inverseSurfaceDark = Color(0xFFE5E2E1)
|
||||||
|
override val inverseOnSurfaceDark = Color(0xFF313030)
|
||||||
|
override val inversePrimaryDark = Color(0xFF5E5E5E)
|
||||||
|
override val surfaceDimDark = Color(0xFF141313)
|
||||||
|
override val surfaceBrightDark = Color(0xFF3A3939)
|
||||||
|
override val surfaceContainerLowestDark = Color(0xFF0E0E0E)
|
||||||
|
override val surfaceContainerLowDark = Color(0xFF1C1B1B)
|
||||||
|
override val surfaceContainerDark = Color(0xFF201F1F)
|
||||||
|
override val surfaceContainerHighDark = Color(0xFF2A2A2A)
|
||||||
|
override val surfaceContainerHighestDark = Color(0xFF353434)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 黄色主题
|
// 黄色主题
|
||||||
object Yellow : ThemeColors() {
|
object Yellow : ThemeColors() {
|
||||||
override val Primary = Color(0xFFFFC107)
|
override val primaryLight = Color(0xFF6D5E0F)
|
||||||
override val Secondary = Color(0xFFFFD54F)
|
override val onPrimaryLight = Color(0xFFFFFFFF)
|
||||||
override val Tertiary = Color(0xFFFF8F00)
|
override val primaryContainerLight = Color(0xFFF8E288)
|
||||||
override val OnPrimary = Color(0xFF000000)
|
override val onPrimaryContainerLight = Color(0xFF534600)
|
||||||
override val OnSecondary = Color(0xFF000000)
|
override val secondaryLight = Color(0xFF6D5E0F)
|
||||||
override val OnTertiary = Color(0xFFFFFFFF)
|
override val onSecondaryLight = Color(0xFFFFFFFF)
|
||||||
override val PrimaryContainer = Color(0xFFFFF8E1)
|
override val secondaryContainerLight = Color(0xFFF7E388)
|
||||||
override val SecondaryContainer = Color(0xFFFFF8E1)
|
override val onSecondaryContainerLight = Color(0xFF534600)
|
||||||
override val TertiaryContainer = Color(0xFFFFECB3)
|
override val tertiaryLight = Color(0xFF685F13)
|
||||||
override val OnPrimaryContainer = Color(0xFF332A00)
|
override val onTertiaryLight = Color(0xFFFFFFFF)
|
||||||
override val OnSecondaryContainer = Color(0xFF332A00)
|
override val tertiaryContainerLight = Color(0xFFF1E58A)
|
||||||
override val OnTertiaryContainer = Color(0xFF221200)
|
override val onTertiaryContainerLight = Color(0xFF4F4800)
|
||||||
override val ButtonContrast = Color(0xFFFFC107)
|
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 primaryDark = Color(0xFFDAC66F)
|
||||||
override val SurfaceVariant = Color(0xFFFFF7E6)
|
override val onPrimaryDark = Color(0xFF393000)
|
||||||
override val OnSurface = Color(0xFF1F1C17)
|
override val primaryContainerDark = Color(0xFF534600)
|
||||||
override val OnSurfaceVariant = Color(0xFF4E4A3C)
|
override val onPrimaryContainerDark = Color(0xFFF8E288)
|
||||||
|
override val secondaryDark = Color(0xFFDAC76F)
|
||||||
override val Error = Color(0xFFB71C1C)
|
override val onSecondaryDark = Color(0xFF393000)
|
||||||
override val OnError = Color(0xFFFFFFFF)
|
override val secondaryContainerDark = Color(0xFF534600)
|
||||||
override val ErrorContainer = Color(0xFFFFDAD6)
|
override val onSecondaryContainerDark = Color(0xFFF7E388)
|
||||||
override val OnErrorContainer = Color(0xFF410002)
|
override val tertiaryDark = Color(0xFFD4C871)
|
||||||
|
override val onTertiaryDark = Color(0xFF363100)
|
||||||
override val Outline = Color(0xFFD1C8AF)
|
override val tertiaryContainerDark = Color(0xFF4F4800)
|
||||||
override val OutlineVariant = Color(0xFFEEE8D7)
|
override val onTertiaryContainerDark = Color(0xFFF1E58A)
|
||||||
override val Background = Color(0xFFFFFCF8)
|
override val errorDark = Color(0xFFFFB4AB)
|
||||||
override val OnBackground = Color(0xFF1F1C17)
|
override val onErrorDark = Color(0xFF690005)
|
||||||
|
override val errorContainerDark = Color(0xFF93000A)
|
||||||
|
override val onErrorContainerDark = Color(0xFFFFDAD6)
|
||||||
|
override val backgroundDark = Color(0xFF15130B)
|
||||||
|
override val onBackgroundDark = Color(0xFFE8E2D4)
|
||||||
|
override val surfaceDark = Color(0xFF15130B)
|
||||||
|
override val onSurfaceDark = Color(0xFFE8E2D4)
|
||||||
|
override val surfaceVariantDark = Color(0xFF4B4739)
|
||||||
|
override val onSurfaceVariantDark = Color(0xFFCDC6B4)
|
||||||
|
override val outlineDark = Color(0xFF969080)
|
||||||
|
override val outlineVariantDark = Color(0xFF4B4739)
|
||||||
|
override val scrimDark = Color(0xFF000000)
|
||||||
|
override val inverseSurfaceDark = Color(0xFFE8E2D4)
|
||||||
|
override val inverseOnSurfaceDark = Color(0xFF333027)
|
||||||
|
override val inversePrimaryDark = Color(0xFF6D5E0F)
|
||||||
|
override val surfaceDimDark = Color(0xFF15130B)
|
||||||
|
override val surfaceBrightDark = Color(0xFF3C3930)
|
||||||
|
override val surfaceContainerLowestDark = Color(0xFF100E07)
|
||||||
|
override val surfaceContainerLowDark = Color(0xFF1E1C13)
|
||||||
|
override val surfaceContainerDark = Color(0xFF222017)
|
||||||
|
override val surfaceContainerHighDark = Color(0xFF2C2A21)
|
||||||
|
override val surfaceContainerHighestDark = Color(0xFF37352B)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ import com.sukisu.ultra.ui.util.saveTransformedBackground
|
|||||||
import androidx.activity.SystemBarStyle
|
import androidx.activity.SystemBarStyle
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
|
import androidx.compose.material3.ColorScheme
|
||||||
import androidx.compose.runtime.SideEffect
|
import androidx.compose.runtime.SideEffect
|
||||||
import androidx.compose.ui.graphics.toArgb
|
import androidx.compose.ui.graphics.toArgb
|
||||||
|
|
||||||
@@ -110,6 +111,9 @@ fun KernelSUTheme(
|
|||||||
if (!isCustomAlphaSet) {
|
if (!isCustomAlphaSet) {
|
||||||
cardAlpha = if (systemIsDark) 0.50f else 1f
|
cardAlpha = if (systemIsDark) 0.50f else 1f
|
||||||
}
|
}
|
||||||
|
if (!isCustomDimSet) {
|
||||||
|
cardDim = if (systemIsDark) 0.5f else 0f
|
||||||
|
}
|
||||||
save(context)
|
save(context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -147,7 +151,9 @@ fun KernelSUTheme(
|
|||||||
// 根据暗色模式和自定义背景调整卡片配置
|
// 根据暗色模式和自定义背景调整卡片配置
|
||||||
val isDarkModeWithCustomBackground = darkTheme && ThemeConfig.customBackgroundUri != null
|
val isDarkModeWithCustomBackground = darkTheme && ThemeConfig.customBackgroundUri != null
|
||||||
if (darkTheme && !dynamicColor) {
|
if (darkTheme && !dynamicColor) {
|
||||||
CardConfig.setDarkModeDefaults()
|
CardConfig.setThemeDefaults(true)
|
||||||
|
} else if (!darkTheme && !dynamicColor) {
|
||||||
|
CardConfig.setThemeDefaults(false)
|
||||||
}
|
}
|
||||||
CardConfig.updateShadowEnabled(!isDarkModeWithCustomBackground)
|
CardConfig.updateShadowEnabled(!isDarkModeWithCustomBackground)
|
||||||
|
|
||||||
@@ -199,6 +205,9 @@ fun KernelSUTheme(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 计算适用的暗化值
|
||||||
|
val dimFactor = CardConfig.cardDim
|
||||||
|
|
||||||
MaterialTheme(
|
MaterialTheme(
|
||||||
colorScheme = colorScheme,
|
colorScheme = colorScheme,
|
||||||
typography = Typography
|
typography = Typography
|
||||||
@@ -208,7 +217,8 @@ fun KernelSUTheme(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.zIndex(-2f)
|
.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 })
|
||||||
)
|
)
|
||||||
|
|
||||||
// 自定义背景层
|
// 自定义背景层
|
||||||
@@ -234,13 +244,13 @@ fun KernelSUTheme(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 亮度调节层
|
// 亮度调节层 (根据cardDim调整)
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.background(
|
.background(
|
||||||
if (darkTheme) Color.Black.copy(alpha = 0.6f)
|
if (darkTheme) Color.Black.copy(alpha = 0.6f + dimFactor * 0.3f)
|
||||||
else Color.White.copy(alpha = 0.1f)
|
else Color.White.copy(alpha = 0.1f + dimFactor * 0.2f)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -252,8 +262,8 @@ fun KernelSUTheme(
|
|||||||
Brush.radialGradient(
|
Brush.radialGradient(
|
||||||
colors = listOf(
|
colors = listOf(
|
||||||
Color.Transparent,
|
Color.Transparent,
|
||||||
if (darkTheme) Color.Black.copy(alpha = 0.5f)
|
if (darkTheme) Color.Black.copy(alpha = 0.5f + dimFactor * 0.2f)
|
||||||
else Color.Black.copy(alpha = 0.2f)
|
else Color.Black.copy(alpha = 0.2f + dimFactor * 0.1f)
|
||||||
),
|
),
|
||||||
radius = 1200f
|
radius = 1200f
|
||||||
)
|
)
|
||||||
@@ -279,54 +289,73 @@ fun KernelSUTheme(
|
|||||||
*/
|
*/
|
||||||
@RequiresApi(Build.VERSION_CODES.S)
|
@RequiresApi(Build.VERSION_CODES.S)
|
||||||
@Composable
|
@Composable
|
||||||
private fun createDynamicDarkColorScheme(context: Context) =
|
private fun createDynamicDarkColorScheme(context: Context): ColorScheme {
|
||||||
dynamicDarkColorScheme(context).copy(
|
val scheme = dynamicDarkColorScheme(context)
|
||||||
background = Color.Transparent,
|
return scheme.copy(
|
||||||
surface = Color.Transparent,
|
background = if (CardConfig.isCustomBackgroundEnabled) Color.Transparent else scheme.background,
|
||||||
onBackground = Color.White,
|
surface = if (CardConfig.isCustomBackgroundEnabled) Color.Transparent else scheme.surface,
|
||||||
onSurface = Color.White
|
onBackground = scheme.onBackground,
|
||||||
|
onSurface = scheme.onSurface
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建动态浅色颜色方案
|
* 创建动态浅色颜色方案
|
||||||
*/
|
*/
|
||||||
@RequiresApi(Build.VERSION_CODES.S)
|
@RequiresApi(Build.VERSION_CODES.S)
|
||||||
@Composable
|
@Composable
|
||||||
private fun createDynamicLightColorScheme(context: Context) =
|
private fun createDynamicLightColorScheme(context: Context): ColorScheme {
|
||||||
dynamicLightColorScheme(context).copy(
|
val scheme = dynamicLightColorScheme(context)
|
||||||
background = Color.Transparent,
|
return scheme.copy(
|
||||||
surface = Color.Transparent
|
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
|
@Composable
|
||||||
private fun createDarkColorScheme() = darkColorScheme(
|
private fun createDarkColorScheme() = darkColorScheme(
|
||||||
primary = ThemeConfig.currentTheme.Primary.copy(alpha = 0.8f),
|
primary = ThemeConfig.currentTheme.primaryDark,
|
||||||
onPrimary = Color.White,
|
onPrimary = ThemeConfig.currentTheme.onPrimaryDark,
|
||||||
primaryContainer = ThemeConfig.currentTheme.PrimaryContainer.copy(alpha = 0.15f),
|
primaryContainer = ThemeConfig.currentTheme.primaryContainerDark,
|
||||||
onPrimaryContainer = Color.White,
|
onPrimaryContainer = ThemeConfig.currentTheme.onPrimaryContainerDark,
|
||||||
secondary = ThemeConfig.currentTheme.Secondary.copy(alpha = 0.8f),
|
secondary = ThemeConfig.currentTheme.secondaryDark,
|
||||||
onSecondary = Color.White,
|
onSecondary = ThemeConfig.currentTheme.onSecondaryDark,
|
||||||
secondaryContainer = ThemeConfig.currentTheme.SecondaryContainer.copy(alpha = 0.15f),
|
secondaryContainer = ThemeConfig.currentTheme.secondaryContainerDark,
|
||||||
onSecondaryContainer = Color.White,
|
onSecondaryContainer = ThemeConfig.currentTheme.onSecondaryContainerDark,
|
||||||
tertiary = ThemeConfig.currentTheme.Tertiary.copy(alpha = 0.8f),
|
tertiary = ThemeConfig.currentTheme.tertiaryDark,
|
||||||
onTertiary = Color.White,
|
onTertiary = ThemeConfig.currentTheme.onTertiaryDark,
|
||||||
tertiaryContainer = ThemeConfig.currentTheme.TertiaryContainer.copy(alpha = 0.15f),
|
tertiaryContainer = ThemeConfig.currentTheme.tertiaryContainerDark,
|
||||||
onTertiaryContainer = Color.White,
|
onTertiaryContainer = ThemeConfig.currentTheme.onTertiaryContainerDark,
|
||||||
background = Color.Transparent,
|
error = ThemeConfig.currentTheme.errorDark,
|
||||||
surface = Color.Transparent,
|
onError = ThemeConfig.currentTheme.onErrorDark,
|
||||||
onBackground = Color.White,
|
errorContainer = ThemeConfig.currentTheme.errorContainerDark,
|
||||||
onSurface = Color.White,
|
onErrorContainer = ThemeConfig.currentTheme.onErrorContainerDark,
|
||||||
surfaceVariant = Color(0xFF2F2F2F),
|
background = if (CardConfig.isCustomBackgroundEnabled) Color.Transparent else ThemeConfig.currentTheme.backgroundDark,
|
||||||
onSurfaceVariant = Color.White.copy(alpha = 0.7f),
|
onBackground = ThemeConfig.currentTheme.onBackgroundDark,
|
||||||
outline = Color.White.copy(alpha = 0.12f),
|
surface = if (CardConfig.isCustomBackgroundEnabled) Color.Transparent else ThemeConfig.currentTheme.surfaceDark,
|
||||||
outlineVariant = Color.White.copy(alpha = 0.12f),
|
onSurface = ThemeConfig.currentTheme.onSurfaceDark,
|
||||||
error = ThemeConfig.currentTheme.Error,
|
surfaceVariant = ThemeConfig.currentTheme.surfaceVariantDark,
|
||||||
onError = ThemeConfig.currentTheme.OnError,
|
onSurfaceVariant = ThemeConfig.currentTheme.onSurfaceVariantDark,
|
||||||
errorContainer = ThemeConfig.currentTheme.ErrorContainer.copy(alpha = 0.15f),
|
outline = ThemeConfig.currentTheme.outlineDark,
|
||||||
onErrorContainer = Color.White
|
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,
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -334,32 +363,44 @@ private fun createDarkColorScheme() = darkColorScheme(
|
|||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
private fun createLightColorScheme() = lightColorScheme(
|
private fun createLightColorScheme() = lightColorScheme(
|
||||||
primary = ThemeConfig.currentTheme.Primary,
|
primary = ThemeConfig.currentTheme.primaryLight,
|
||||||
onPrimary = ThemeConfig.currentTheme.OnPrimary,
|
onPrimary = ThemeConfig.currentTheme.onPrimaryLight,
|
||||||
primaryContainer = ThemeConfig.currentTheme.PrimaryContainer,
|
primaryContainer = ThemeConfig.currentTheme.primaryContainerLight,
|
||||||
onPrimaryContainer = ThemeConfig.currentTheme.OnPrimaryContainer,
|
onPrimaryContainer = ThemeConfig.currentTheme.onPrimaryContainerLight,
|
||||||
secondary = ThemeConfig.currentTheme.Secondary,
|
secondary = ThemeConfig.currentTheme.secondaryLight,
|
||||||
onSecondary = ThemeConfig.currentTheme.OnSecondary,
|
onSecondary = ThemeConfig.currentTheme.onSecondaryLight,
|
||||||
secondaryContainer = ThemeConfig.currentTheme.SecondaryContainer,
|
secondaryContainer = ThemeConfig.currentTheme.secondaryContainerLight,
|
||||||
onSecondaryContainer = ThemeConfig.currentTheme.OnSecondaryContainer,
|
onSecondaryContainer = ThemeConfig.currentTheme.onSecondaryContainerLight,
|
||||||
tertiary = ThemeConfig.currentTheme.Tertiary,
|
tertiary = ThemeConfig.currentTheme.tertiaryLight,
|
||||||
onTertiary = ThemeConfig.currentTheme.OnTertiary,
|
onTertiary = ThemeConfig.currentTheme.onTertiaryLight,
|
||||||
tertiaryContainer = ThemeConfig.currentTheme.TertiaryContainer,
|
tertiaryContainer = ThemeConfig.currentTheme.tertiaryContainerLight,
|
||||||
onTertiaryContainer = ThemeConfig.currentTheme.OnTertiaryContainer,
|
onTertiaryContainer = ThemeConfig.currentTheme.onTertiaryContainerLight,
|
||||||
background = Color.Transparent,
|
error = ThemeConfig.currentTheme.errorLight,
|
||||||
surface = Color.Transparent,
|
onError = ThemeConfig.currentTheme.onErrorLight,
|
||||||
onBackground = Color.Black.copy(alpha = 0.87f),
|
errorContainer = ThemeConfig.currentTheme.errorContainerLight,
|
||||||
onSurface = Color.Black.copy(alpha = 0.87f),
|
onErrorContainer = ThemeConfig.currentTheme.onErrorContainerLight,
|
||||||
surfaceVariant = Color(0xFFF5F5F5),
|
background = if (CardConfig.isCustomBackgroundEnabled) Color.Transparent else ThemeConfig.currentTheme.backgroundLight,
|
||||||
onSurfaceVariant = Color.Black.copy(alpha = 0.78f),
|
onBackground = ThemeConfig.currentTheme.onBackgroundLight,
|
||||||
outline = Color.Black.copy(alpha = 0.12f),
|
surface = if (CardConfig.isCustomBackgroundEnabled) Color.Transparent else ThemeConfig.currentTheme.surfaceLight,
|
||||||
outlineVariant = Color.Black.copy(alpha = 0.12f),
|
onSurface = ThemeConfig.currentTheme.onSurfaceLight,
|
||||||
error = ThemeConfig.currentTheme.Error,
|
surfaceVariant = ThemeConfig.currentTheme.surfaceVariantLight,
|
||||||
onError = ThemeConfig.currentTheme.OnError,
|
onSurfaceVariant = ThemeConfig.currentTheme.onSurfaceVariantLight,
|
||||||
errorContainer = ThemeConfig.currentTheme.ErrorContainer,
|
outline = ThemeConfig.currentTheme.outlineLight,
|
||||||
onErrorContainer = ThemeConfig.currentTheme.OnErrorContainer
|
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,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 复制图片到应用内部存储并提升持久性
|
* 复制图片到应用内部存储并提升持久性
|
||||||
*/
|
*/
|
||||||
@@ -546,9 +587,6 @@ fun Context.loadDynamicColorState() {
|
|||||||
ThemeConfig.useDynamicColor = enabled
|
ThemeConfig.useDynamicColor = enabled
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* webui X样式
|
|
||||||
*/
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun SystemBarStyle(
|
private fun SystemBarStyle(
|
||||||
darkMode: Boolean,
|
darkMode: Boolean,
|
||||||
|
|||||||
@@ -7,13 +7,23 @@ import android.content.Context
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.IntentFilter
|
import android.content.IntentFilter
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import com.sukisu.ultra.ui.util.module.LatestVersionInfo
|
import com.sukisu.ultra.ui.util.module.LatestVersionInfo
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
|
import java.io.File
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
private const val TAG = "DownloadUtil"
|
||||||
|
private val CUSTOM_USER_AGENT = "SukiSU-Ultra/2.0 (Linux; Android ${Build.VERSION.RELEASE}; ${Build.MODEL})"
|
||||||
|
private const val MAX_RETRY_COUNT = 3
|
||||||
|
private const val RETRY_DELAY_MS = 3000L
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author weishu
|
* @author weishu
|
||||||
@@ -26,8 +36,10 @@ fun download(
|
|||||||
fileName: String,
|
fileName: String,
|
||||||
description: String,
|
description: String,
|
||||||
onDownloaded: (Uri) -> Unit = {},
|
onDownloaded: (Uri) -> Unit = {},
|
||||||
onDownloading: () -> Unit = {}
|
onDownloading: () -> Unit = {},
|
||||||
|
onError: (String) -> Unit = {}
|
||||||
) {
|
) {
|
||||||
|
Log.d(TAG, "Start Download: $url")
|
||||||
val downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
val downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
||||||
|
|
||||||
val query = DownloadManager.Query()
|
val query = DownloadManager.Query()
|
||||||
@@ -49,6 +61,13 @@ fun download(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
val downloadFile = File(
|
||||||
|
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
|
||||||
|
fileName
|
||||||
|
)
|
||||||
|
if (downloadFile.exists()) {
|
||||||
|
downloadFile.delete()
|
||||||
|
}
|
||||||
|
|
||||||
val request = DownloadManager.Request(url.toUri())
|
val request = DownloadManager.Request(url.toUri())
|
||||||
.setDestinationInExternalPublicDir(
|
.setDestinationInExternalPublicDir(
|
||||||
@@ -59,26 +78,168 @@ fun download(
|
|||||||
.setMimeType("application/zip")
|
.setMimeType("application/zip")
|
||||||
.setTitle(fileName)
|
.setTitle(fileName)
|
||||||
.setDescription(description)
|
.setDescription(description)
|
||||||
|
.addRequestHeader("User-Agent", CUSTOM_USER_AGENT)
|
||||||
|
.setAllowedOverMetered(true)
|
||||||
|
.setAllowedOverRoaming(true)
|
||||||
|
.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI or DownloadManager.Request.NETWORK_MOBILE)
|
||||||
|
|
||||||
downloadManager.enqueue(request)
|
try {
|
||||||
|
val downloadId = downloadManager.enqueue(request)
|
||||||
|
Log.d(TAG, "Successful launch of the download,ID: $downloadId")
|
||||||
|
monitorDownload(context, downloadManager, downloadId, url, fileName, description, onDownloaded, onDownloading, onError)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Download startup failure", e)
|
||||||
|
onError("Download startup failure: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun monitorDownload(
|
||||||
|
context: Context,
|
||||||
|
downloadManager: DownloadManager,
|
||||||
|
downloadId: Long,
|
||||||
|
url: String,
|
||||||
|
fileName: String,
|
||||||
|
description: String,
|
||||||
|
onDownloaded: (Uri) -> Unit,
|
||||||
|
onDownloading: () -> Unit,
|
||||||
|
onError: (String) -> Unit,
|
||||||
|
retryCount: Int = 0
|
||||||
|
) {
|
||||||
|
val handler = Handler(Looper.getMainLooper())
|
||||||
|
val query = DownloadManager.Query().setFilterById(downloadId)
|
||||||
|
|
||||||
|
var lastProgress = -1
|
||||||
|
var stuckCounter = 0
|
||||||
|
|
||||||
|
val runnable = object : Runnable {
|
||||||
|
override fun run() {
|
||||||
|
downloadManager.query(query).use { cursor ->
|
||||||
|
if (cursor != null && cursor.moveToFirst()) {
|
||||||
|
@SuppressLint("Range")
|
||||||
|
val status = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS))
|
||||||
|
|
||||||
|
when (status) {
|
||||||
|
DownloadManager.STATUS_SUCCESSFUL -> {
|
||||||
|
@SuppressLint("Range")
|
||||||
|
val localUri = cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI))
|
||||||
|
Log.d(TAG, "Download Successfully: $localUri")
|
||||||
|
onDownloaded(localUri.toUri())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
DownloadManager.STATUS_FAILED -> {
|
||||||
|
@SuppressLint("Range")
|
||||||
|
val reason = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_REASON))
|
||||||
|
Log.d(TAG, "Download failed with reason code: $reason")
|
||||||
|
|
||||||
|
if (retryCount < MAX_RETRY_COUNT) {
|
||||||
|
Log.d(TAG, "Attempts to re download, number of retries: ${retryCount + 1}")
|
||||||
|
handler.postDelayed({
|
||||||
|
downloadManager.remove(downloadId)
|
||||||
|
download(context, url, fileName, description, onDownloaded, onDownloading, onError)
|
||||||
|
}, RETRY_DELAY_MS)
|
||||||
|
} else {
|
||||||
|
onError("Download failed, please check network connection or storage space")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
DownloadManager.STATUS_RUNNING, DownloadManager.STATUS_PENDING, DownloadManager.STATUS_PAUSED -> {
|
||||||
|
@SuppressLint("Range")
|
||||||
|
val totalBytes = cursor.getLong(cursor.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES))
|
||||||
|
@SuppressLint("Range")
|
||||||
|
val downloadedBytes = cursor.getLong(cursor.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR))
|
||||||
|
|
||||||
|
if (totalBytes > 0) {
|
||||||
|
val progress = (downloadedBytes * 100 / totalBytes).toInt()
|
||||||
|
if (progress == lastProgress) {
|
||||||
|
stuckCounter++
|
||||||
|
if (stuckCounter > 30) {
|
||||||
|
if (retryCount < MAX_RETRY_COUNT) {
|
||||||
|
Log.d(TAG, "Download stalled and restarted")
|
||||||
|
downloadManager.remove(downloadId)
|
||||||
|
download(context, url, fileName, description, onDownloaded, onDownloading, onError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
lastProgress = progress
|
||||||
|
stuckCounter = 0
|
||||||
|
Log.d(TAG, "Download progress: $progress% ($downloadedBytes/$totalBytes)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
handler.postDelayed(this, 1000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
handler.post(runnable)
|
||||||
|
|
||||||
|
val receiver = object : BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context?, intent: Intent?) {
|
||||||
|
val id = intent?.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1) ?: -1
|
||||||
|
if (id == downloadId) {
|
||||||
|
handler.removeCallbacks(runnable)
|
||||||
|
|
||||||
|
val query = DownloadManager.Query().setFilterById(downloadId)
|
||||||
|
downloadManager.query(query).use { cursor ->
|
||||||
|
if (cursor != null && cursor.moveToFirst()) {
|
||||||
|
@SuppressLint("Range")
|
||||||
|
val status = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS))
|
||||||
|
|
||||||
|
if (status == DownloadManager.STATUS_SUCCESSFUL) {
|
||||||
|
@SuppressLint("Range")
|
||||||
|
val localUri = cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI))
|
||||||
|
onDownloaded(localUri.toUri())
|
||||||
|
} else {
|
||||||
|
if (retryCount < MAX_RETRY_COUNT) {
|
||||||
|
download(context!!, url, fileName, description, onDownloaded, onDownloading, onError)
|
||||||
|
} else {
|
||||||
|
onError("Download failed, please try again later")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
context?.unregisterReceiver(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ContextCompat.registerReceiver(
|
||||||
|
context,
|
||||||
|
receiver,
|
||||||
|
IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE),
|
||||||
|
ContextCompat.RECEIVER_EXPORTED
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun checkNewVersion(): LatestVersionInfo {
|
fun checkNewVersion(): LatestVersionInfo {
|
||||||
val url = "https://api.github.com/repos/ShirkNeko/SukiSU-Ultra/releases/latest"
|
val url = "https://api.github.com/repos/ShirkNeko/SukiSU-Ultra/releases/latest"
|
||||||
val defaultValue = LatestVersionInfo()
|
val defaultValue = LatestVersionInfo()
|
||||||
return runCatching {
|
return runCatching {
|
||||||
okhttp3.OkHttpClient().newCall(okhttp3.Request.Builder().url(url).build()).execute()
|
val client = okhttp3.OkHttpClient.Builder()
|
||||||
.use { response ->
|
.connectTimeout(15, TimeUnit.SECONDS)
|
||||||
|
.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) {
|
if (!response.isSuccessful) {
|
||||||
Log.d("CheckUpdate", "Network request failed: ${response.message}")
|
Log.d("CheckUpdate", "Network request failed: ${response.message}")
|
||||||
return defaultValue
|
return defaultValue
|
||||||
}
|
}
|
||||||
val body = response.body?.string()
|
val body = response.body?.string()
|
||||||
if (body == null) {
|
if (body == null) {
|
||||||
Log.d("CheckUpdate", "Response body is null")
|
Log.d("CheckUpdate", "Return data is null")
|
||||||
return defaultValue
|
return defaultValue
|
||||||
}
|
}
|
||||||
Log.d("CheckUpdate", "Response body: $body")
|
Log.d("CheckUpdate", "Return data: $body")
|
||||||
val json = org.json.JSONObject(body)
|
val json = org.json.JSONObject(body)
|
||||||
|
|
||||||
// 直接从 tag_name 提取版本号(如 v1.1)
|
// 直接从 tag_name 提取版本号(如 v1.1)
|
||||||
@@ -95,11 +256,10 @@ fun checkNewVersion(): LatestVersionInfo {
|
|||||||
val name = asset.getString("name")
|
val name = asset.getString("name")
|
||||||
if (!name.endsWith(".apk")) continue
|
if (!name.endsWith(".apk")) continue
|
||||||
|
|
||||||
// 修改正则表达式,只匹配 SukiSU 和版本号
|
|
||||||
val regex = Regex("SukiSU.*_(\\d+)-release")
|
val regex = Regex("SukiSU.*_(\\d+)-release")
|
||||||
val matchResult = regex.find(name)
|
val matchResult = regex.find(name)
|
||||||
if (matchResult == null) {
|
if (matchResult == null) {
|
||||||
Log.d("CheckUpdate", "No match found in $name, skipping")
|
Log.d("CheckUpdate", "No matches found: $name, skip over")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
val versionCode = matchResult.groupValues[1].toInt()
|
val versionCode = matchResult.groupValues[1].toInt()
|
||||||
@@ -112,13 +272,12 @@ fun checkNewVersion(): LatestVersionInfo {
|
|||||||
versionName
|
versionName
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Log.d("CheckUpdate", "No valid apk asset found, returning default value")
|
Log.d("CheckUpdate", "No valid APK resource found, return default value")
|
||||||
defaultValue
|
defaultValue
|
||||||
}
|
}
|
||||||
}.getOrDefault(defaultValue)
|
}.getOrDefault(defaultValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DownloadListener(context: Context, onDownloaded: (Uri) -> Unit) {
|
fun DownloadListener(context: Context, onDownloaded: (Uri) -> Unit) {
|
||||||
DisposableEffect(context) {
|
DisposableEffect(context) {
|
||||||
@@ -158,4 +317,3 @@ fun DownloadListener(context: Context, onDownloaded: (Uri) -> Unit) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -76,9 +76,9 @@ fun createRootShell(globalMnt: Boolean = false): Shell {
|
|||||||
Log.w(TAG, "ksu failed: ", e)
|
Log.w(TAG, "ksu failed: ", e)
|
||||||
try {
|
try {
|
||||||
if (globalMnt) {
|
if (globalMnt) {
|
||||||
builder.build("su")
|
|
||||||
} else {
|
|
||||||
builder.build("su", "-mm")
|
builder.build("su", "-mm")
|
||||||
|
} else {
|
||||||
|
builder.build("su")
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Log.e(TAG, "su failed: ", e)
|
Log.e(TAG, "su failed: ", e)
|
||||||
|
|||||||
@@ -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
|
package com.sukisu.ultra.ui.util
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import android.content.Context
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
import com.sukisu.ultra.R
|
import com.sukisu.ultra.R
|
||||||
|
|
||||||
@Composable
|
fun getSELinuxStatus(context: Context): String {
|
||||||
fun getSELinuxStatus(): String {
|
|
||||||
val shell = Shell.Builder.create().build("sh")
|
val shell = Shell.Builder.create().build("sh")
|
||||||
val list = ArrayList<String>()
|
val list = ArrayList<String>()
|
||||||
|
|
||||||
@@ -18,16 +16,16 @@ fun getSELinuxStatus(): String {
|
|||||||
|
|
||||||
return if (result.isSuccess) {
|
return if (result.isSuccess) {
|
||||||
when (output) {
|
when (output) {
|
||||||
"Enforcing" -> stringResource(R.string.selinux_status_enforcing)
|
"Enforcing" -> context.getString(R.string.selinux_status_enforcing)
|
||||||
"Permissive" -> stringResource(R.string.selinux_status_permissive)
|
"Permissive" -> context.getString(R.string.selinux_status_permissive)
|
||||||
"Disabled" -> stringResource(R.string.selinux_status_disabled)
|
"Disabled" -> context.getString(R.string.selinux_status_disabled)
|
||||||
else -> stringResource(R.string.selinux_status_unknown)
|
else -> context.getString(R.string.selinux_status_unknown)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (output.contains("Permission denied")) {
|
if (output.contains("Permission denied")) {
|
||||||
stringResource(R.string.selinux_status_enforcing)
|
context.getString(R.string.selinux_status_enforcing)
|
||||||
} else {
|
} else {
|
||||||
stringResource(R.string.selinux_status_unknown)
|
context.getString(R.string.selinux_status_unknown)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,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 kotlinx.coroutines.withContext
|
||||||
import com.sukisu.ultra.ui.util.*
|
import com.sukisu.ultra.ui.util.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author ShirkNeko
|
||||||
|
* @date 2025/5/31.
|
||||||
|
*/
|
||||||
class KpmViewModel : ViewModel() {
|
class KpmViewModel : ViewModel() {
|
||||||
var moduleList by mutableStateOf(emptyList<ModuleInfo>())
|
var moduleList by mutableStateOf(emptyList<ModuleInfo>())
|
||||||
private set
|
private set
|
||||||
|
|||||||
@@ -8,20 +8,29 @@ import androidx.compose.runtime.mutableStateOf
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.dergoogler.mmrl.platform.model.ModuleConfig
|
||||||
|
import com.dergoogler.mmrl.platform.model.ModuleConfig.Companion.asModuleConfig
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import com.sukisu.ultra.ui.util.HanziToPinyin
|
import com.sukisu.ultra.ui.util.HanziToPinyin
|
||||||
import com.sukisu.ultra.ui.util.listModules
|
import com.sukisu.ultra.ui.util.listModules
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import org.json.JSONArray
|
import org.json.JSONArray
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
import java.text.Collator
|
import java.text.Collator
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author ShirkNeko
|
||||||
|
* @date 2025/5/31.
|
||||||
|
*/
|
||||||
class ModuleViewModel : ViewModel() {
|
class ModuleViewModel : ViewModel() {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "ModuleViewModel"
|
private const val TAG = "ModuleViewModel"
|
||||||
private var modules by mutableStateOf<List<ModuleInfo>>(emptyList())
|
private var modules by mutableStateOf<List<ModuleInfo>>(emptyList())
|
||||||
|
private const val CUSTOM_USER_AGENT = "SukiSU-Ultra/2.0"
|
||||||
}
|
}
|
||||||
|
|
||||||
class ModuleInfo(
|
class ModuleInfo(
|
||||||
@@ -38,6 +47,7 @@ class ModuleViewModel : ViewModel() {
|
|||||||
val hasWebUi: Boolean,
|
val hasWebUi: Boolean,
|
||||||
val hasActionScript: Boolean,
|
val hasActionScript: Boolean,
|
||||||
val dirId: String, // real module id (dir name)
|
val dirId: String, // real module id (dir name)
|
||||||
|
var config: ModuleConfig? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
var isRefreshing by mutableStateOf(false)
|
var isRefreshing by mutableStateOf(false)
|
||||||
@@ -98,9 +108,42 @@ class ModuleViewModel : ViewModel() {
|
|||||||
obj.optString("updateJson"),
|
obj.optString("updateJson"),
|
||||||
obj.optBoolean("web"),
|
obj.optBoolean("web"),
|
||||||
obj.optBoolean("action"),
|
obj.optBoolean("action"),
|
||||||
obj.getString("dir_id"),
|
obj.getString("dir_id")
|
||||||
)
|
)
|
||||||
}.toList()
|
}.toList()
|
||||||
|
launch {
|
||||||
|
modules.forEach { module ->
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
runCatching {
|
||||||
|
module.config = module.id.asModuleConfig
|
||||||
|
}.onFailure { e ->
|
||||||
|
Log.e(TAG, "Failed to load config from id for module ${module.id}", e)
|
||||||
|
}
|
||||||
|
if (module.config == null) {
|
||||||
|
runCatching {
|
||||||
|
module.config = module.name.asModuleConfig
|
||||||
|
}.onFailure { e ->
|
||||||
|
Log.e(TAG, "Failed to load config from name for module ${module.id}", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (module.config == null) {
|
||||||
|
runCatching {
|
||||||
|
module.config = module.description.asModuleConfig
|
||||||
|
}.onFailure { e ->
|
||||||
|
Log.e(TAG, "Failed to load config from description for module ${module.id}", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (module.config == null) {
|
||||||
|
module.config = ModuleConfig()
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Failed to load any config for module ${module.id}", e)
|
||||||
|
module.config = ModuleConfig()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
isNeedRefresh = false
|
isNeedRefresh = false
|
||||||
}.onFailure { e ->
|
}.onFailure { e ->
|
||||||
Log.e(TAG, "fetchModuleList: ", e)
|
Log.e(TAG, "fetchModuleList: ", e)
|
||||||
@@ -117,6 +160,10 @@ class ModuleViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun sanitizeVersionString(version: String): String {
|
||||||
|
return version.replace(Regex("[^a-zA-Z0-9.\\-_]"), "_")
|
||||||
|
}
|
||||||
|
|
||||||
fun checkUpdate(m: ModuleInfo): Triple<String, String, String> {
|
fun checkUpdate(m: ModuleInfo): Triple<String, String, String> {
|
||||||
val empty = Triple("", "", "")
|
val empty = Triple("", "", "")
|
||||||
if (m.updateJson.isEmpty() || m.remove || m.update || !m.enabled) {
|
if (m.updateJson.isEmpty() || m.remove || m.update || !m.enabled) {
|
||||||
@@ -126,19 +173,32 @@ class ModuleViewModel : ViewModel() {
|
|||||||
val result = kotlin.runCatching {
|
val result = kotlin.runCatching {
|
||||||
val url = m.updateJson
|
val url = m.updateJson
|
||||||
Log.i(TAG, "checkUpdate url: $url")
|
Log.i(TAG, "checkUpdate url: $url")
|
||||||
val response = okhttp3.OkHttpClient()
|
|
||||||
.newCall(
|
val client = okhttp3.OkHttpClient.Builder()
|
||||||
okhttp3.Request.Builder()
|
.connectTimeout(15, TimeUnit.SECONDS)
|
||||||
.url(url)
|
.readTimeout(30, TimeUnit.SECONDS)
|
||||||
|
.writeTimeout(15, TimeUnit.SECONDS)
|
||||||
.build()
|
.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}")
|
Log.d(TAG, "checkUpdate code: ${response.code}")
|
||||||
if (response.isSuccessful) {
|
if (response.isSuccessful) {
|
||||||
response.body?.string() ?: ""
|
response.body?.string() ?: ""
|
||||||
} else {
|
} else {
|
||||||
|
Log.d(TAG, "checkUpdate failed: ${response.message}")
|
||||||
""
|
""
|
||||||
}
|
}
|
||||||
}.getOrDefault("")
|
}.getOrElse { e ->
|
||||||
|
Log.e(TAG, "checkUpdate exception", e)
|
||||||
|
""
|
||||||
|
}
|
||||||
|
|
||||||
Log.i(TAG, "checkUpdate result: $result")
|
Log.i(TAG, "checkUpdate result: $result")
|
||||||
|
|
||||||
if (result.isEmpty()) {
|
if (result.isEmpty()) {
|
||||||
@@ -149,7 +209,8 @@ class ModuleViewModel : ViewModel() {
|
|||||||
JSONObject(result)
|
JSONObject(result)
|
||||||
}.getOrNull() ?: return empty
|
}.getOrNull() ?: return empty
|
||||||
|
|
||||||
val version = updateJson.optString("version", "")
|
var version = updateJson.optString("version", "")
|
||||||
|
version = sanitizeVersionString(version)
|
||||||
val versionCode = updateJson.optInt("versionCode", 0)
|
val versionCode = updateJson.optInt("versionCode", 0)
|
||||||
val zipUrl = updateJson.optString("zipUrl", "")
|
val zipUrl = updateJson.optString("zipUrl", "")
|
||||||
val changelog = updateJson.optString("changelog", "")
|
val changelog = updateJson.optString("changelog", "")
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
package com.sukisu.ultra.ui.viewmodel
|
package com.sukisu.ultra.ui.viewmodel
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.content.pm.ApplicationInfo
|
import android.content.pm.ApplicationInfo
|
||||||
import android.content.pm.PackageInfo
|
import android.content.pm.PackageInfo
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import android.os.SystemClock
|
import android.os.SystemClock
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
@@ -20,16 +22,21 @@ import java.text.Collator
|
|||||||
import java.util.*
|
import java.util.*
|
||||||
import com.dergoogler.mmrl.platform.Platform
|
import com.dergoogler.mmrl.platform.Platform
|
||||||
import com.dergoogler.mmrl.platform.TIMEOUT_MILLIS
|
import com.dergoogler.mmrl.platform.TIMEOUT_MILLIS
|
||||||
import com.sukisu.ultra.ui.webui.packageManager
|
import com.sukisu.ultra.ui.webui.getInstalledPackagesAll
|
||||||
import com.sukisu.ultra.ui.webui.userManager
|
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.withTimeoutOrNull
|
import kotlinx.coroutines.withTimeoutOrNull
|
||||||
|
import androidx.core.content.edit
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author ShirkNeko
|
||||||
|
* @date 2025/5/31.
|
||||||
|
*/
|
||||||
class SuperUserViewModel : ViewModel() {
|
class SuperUserViewModel : ViewModel() {
|
||||||
val isPlatformAlive get() = Platform.isAlive
|
val isPlatformAlive get() = Platform.isAlive
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "SuperUserViewModel"
|
private const val TAG = "SuperUserViewModel"
|
||||||
private var apps by mutableStateOf<List<AppInfo>>(emptyList())
|
var apps by mutableStateOf<List<AppInfo>>(emptyList())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
@@ -57,9 +64,11 @@ class SuperUserViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
private val prefs = ksuApp.getSharedPreferences("settings", Context.MODE_PRIVATE)!!
|
||||||
|
|
||||||
var search by mutableStateOf("")
|
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)
|
var isRefreshing by mutableStateOf(false)
|
||||||
private set
|
private set
|
||||||
|
|
||||||
@@ -69,6 +78,11 @@ class SuperUserViewModel : ViewModel() {
|
|||||||
var selectedApps by mutableStateOf<Set<String>>(emptySet())
|
var selectedApps by mutableStateOf<Set<String>>(emptySet())
|
||||||
internal set
|
internal set
|
||||||
|
|
||||||
|
fun updateShowSystemApps(newValue: Boolean) {
|
||||||
|
showSystemApps = newValue
|
||||||
|
prefs.edit { putBoolean("show_system_apps", newValue) }
|
||||||
|
}
|
||||||
|
|
||||||
private val sortedList by derivedStateOf {
|
private val sortedList by derivedStateOf {
|
||||||
val comparator = compareBy<AppInfo> {
|
val comparator = compareBy<AppInfo> {
|
||||||
when {
|
when {
|
||||||
@@ -139,6 +153,43 @@ class SuperUserViewModel : ViewModel() {
|
|||||||
fetchAppList() // 刷新列表以显示最新状态
|
fetchAppList() // 刷新列表以显示最新状态
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 批量更新权限和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
|
||||||
|
)
|
||||||
|
if (Natives.setAppProfile(updatedProfile)) {
|
||||||
|
apps = apps.map { app ->
|
||||||
|
if (app.packageName == packageName) {
|
||||||
|
app.copy(profile = updatedProfile)
|
||||||
|
} else {
|
||||||
|
app
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
clearSelection()
|
||||||
|
showBatchActions = false // 批量操作完成后退出批量模式
|
||||||
|
fetchAppList() // 刷新列表以显示最新状态
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新本地应用配置
|
||||||
|
fun updateAppProfileLocally(packageName: String, updatedProfile: Natives.Profile) {
|
||||||
|
apps = apps.map { app ->
|
||||||
|
if (app.packageName == packageName) {
|
||||||
|
app.copy(profile = updatedProfile)
|
||||||
|
} else {
|
||||||
|
app
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun fetchAppList() {
|
suspend fun fetchAppList() {
|
||||||
isRefreshing = true
|
isRefreshing = true
|
||||||
@@ -152,15 +203,10 @@ class SuperUserViewModel : ViewModel() {
|
|||||||
val pm = ksuApp.packageManager
|
val pm = ksuApp.packageManager
|
||||||
val start = SystemClock.elapsedRealtime()
|
val start = SystemClock.elapsedRealtime()
|
||||||
|
|
||||||
val userInfos = Platform.userManager.getUsers()
|
val packages = Platform.getInstalledPackagesAll {
|
||||||
val packages = mutableListOf<PackageInfo>()
|
Log.e(TAG, "getInstalledPackagesAll:", it)
|
||||||
val packageManager = Platform.packageManager
|
Toast.makeText(ksuApp, "Something went wrong, check logs", Toast.LENGTH_SHORT).show()
|
||||||
|
|
||||||
for (userInfo in userInfos) {
|
|
||||||
Log.i(TAG, "fetchAppList: ${userInfo.id}")
|
|
||||||
packages.addAll(packageManager.getInstalledPackages(0, userInfo.id))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
apps = packages.map {
|
apps = packages.map {
|
||||||
val appInfo = it.applicationInfo
|
val appInfo = it.applicationInfo
|
||||||
val uid = appInfo!!.uid
|
val uid = appInfo!!.uid
|
||||||
|
|||||||
@@ -2,9 +2,8 @@ package com.sukisu.ultra.ui.webui
|
|||||||
|
|
||||||
import android.content.ServiceConnection
|
import android.content.ServiceConnection
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import android.content.pm.PackageInfo
|
||||||
import com.dergoogler.mmrl.platform.Platform
|
import com.dergoogler.mmrl.platform.Platform
|
||||||
import com.dergoogler.mmrl.platform.hiddenApi.HiddenPackageManager
|
|
||||||
import com.dergoogler.mmrl.platform.hiddenApi.HiddenUserManager
|
|
||||||
import com.dergoogler.mmrl.platform.model.IProvider
|
import com.dergoogler.mmrl.platform.model.IProvider
|
||||||
import com.dergoogler.mmrl.platform.model.PlatformIntent
|
import com.dergoogler.mmrl.platform.model.PlatformIntent
|
||||||
import com.sukisu.ultra.ksuApp
|
import com.sukisu.ultra.ksuApp
|
||||||
@@ -57,6 +56,17 @@ suspend fun initPlatform() = withContext(Dispatchers.IO) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val Platform.Companion.packageManager get(): HiddenPackageManager = HiddenPackageManager(this.mService)
|
fun Platform.Companion.getInstalledPackagesAll(catch: (Exception) -> Unit = {}): List<PackageInfo> =
|
||||||
val Platform.Companion.userManager get(): HiddenUserManager = HiddenUserManager(this.mService)
|
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)
|
||||||
|
}
|
||||||
@@ -82,7 +82,6 @@ class WebUIXActivity : ComponentActivity() {
|
|||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
Loading()
|
Loading()
|
||||||
|
|
||||||
return@KernelSUTheme
|
return@KernelSUTheme
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ import android.webkit.JavascriptInterface
|
|||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.core.view.WindowInsetsCompat
|
import androidx.core.view.WindowInsetsCompat
|
||||||
import androidx.core.view.WindowInsetsControllerCompat
|
import androidx.core.view.WindowInsetsControllerCompat
|
||||||
|
import com.dergoogler.mmrl.webui.interfaces.WXInterface
|
||||||
import com.dergoogler.mmrl.webui.interfaces.WXOptions
|
import com.dergoogler.mmrl.webui.interfaces.WXOptions
|
||||||
import com.dergoogler.mmrl.webui.interfaces.WebUIInterface
|
|
||||||
import com.dergoogler.mmrl.webui.model.JavaScriptInterface
|
import com.dergoogler.mmrl.webui.model.JavaScriptInterface
|
||||||
import com.topjohnwu.superuser.CallbackList
|
import com.topjohnwu.superuser.CallbackList
|
||||||
import com.topjohnwu.superuser.ShellUtils
|
import com.topjohnwu.superuser.ShellUtils
|
||||||
@@ -20,14 +20,13 @@ import com.sukisu.ultra.ui.util.listModules
|
|||||||
import com.sukisu.ultra.ui.util.withNewRootShell
|
import com.sukisu.ultra.ui.util.withNewRootShell
|
||||||
import org.json.JSONArray
|
import org.json.JSONArray
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
import com.sukisu.ultra.ui.util.controlKpmModule
|
import com.sukisu.ultra.ui.util.*
|
||||||
import com.sukisu.ultra.ui.util.listKpmModules
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util.concurrent.CompletableFuture
|
import java.util.concurrent.CompletableFuture
|
||||||
|
|
||||||
class WebViewInterface(
|
class WebViewInterface(
|
||||||
wxOptions: WXOptions,
|
wxOptions: WXOptions,
|
||||||
) : WebUIInterface(wxOptions) {
|
) : WXInterface(wxOptions) {
|
||||||
override var name: String = "ksu"
|
override var name: String = "ksu"
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -187,7 +186,7 @@ class WebViewInterface(
|
|||||||
@JavascriptInterface
|
@JavascriptInterface
|
||||||
fun moduleInfo(): String {
|
fun moduleInfo(): String {
|
||||||
val moduleInfos = JSONArray(listModules())
|
val moduleInfos = JSONArray(listModules())
|
||||||
var currentModuleInfo = JSONObject()
|
val currentModuleInfo = JSONObject()
|
||||||
currentModuleInfo.put("moduleDir", modDir)
|
currentModuleInfo.put("moduleDir", modDir)
|
||||||
val moduleId = File(modDir).getName()
|
val moduleId = File(modDir).getName()
|
||||||
for (i in 0 until moduleInfos.length()) {
|
for (i in 0 until moduleInfos.length()) {
|
||||||
@@ -197,7 +196,7 @@ class WebViewInterface(
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
var keys = currentInfo.keys()
|
val keys = currentInfo.keys()
|
||||||
for (key in keys) {
|
for (key in keys) {
|
||||||
currentModuleInfo.put(key, currentInfo.get(key))
|
currentModuleInfo.put(key, currentInfo.get(key))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.app.Activity
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
@@ -27,6 +27,11 @@ import java.io.File
|
|||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author ShirkNeko
|
||||||
|
* @date 2025/5/31.
|
||||||
|
*/
|
||||||
data class FlashState(
|
data class FlashState(
|
||||||
val isFlashing: Boolean = false,
|
val isFlashing: Boolean = false,
|
||||||
val isCompleted: Boolean = false,
|
val isCompleted: Boolean = false,
|
||||||
@@ -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"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:width="108dp"
|
android:width="108dp"
|
||||||
android:height="108dp"
|
android:height="108dp"
|
||||||
android:viewportWidth="108"
|
android:viewportWidth="512"
|
||||||
android:viewportHeight="108">
|
android:viewportHeight="512">
|
||||||
|
<group android:scaleX="0.45"
|
||||||
|
android:scaleY="0.45"
|
||||||
|
android:translateX="140.8"
|
||||||
|
android:translateY="140.8">
|
||||||
|
|
||||||
<group
|
|
||||||
android:scaleX="0.135"
|
|
||||||
android:scaleY="0.135">
|
|
||||||
<path
|
<path
|
||||||
android:pathData="M 259 259 H 541 V 541 H 259 V 259 Z"
|
android:fillColor="#fff9f6"
|
||||||
android:strokeWidth="18"
|
android:strokeColor="#4c4f59"
|
||||||
android:strokeColor="#1e110d" />
|
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
|
<path
|
||||||
android:fillColor="#1e110d"
|
android:fillColor="#93d4fa"
|
||||||
android:pathData="M 257 257 H 407 V 407 H 257 V 257 Z" />
|
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
|
<path
|
||||||
android:fillColor="#1e110d"
|
android:fillColor="#ace0fe"
|
||||||
android:pathData="M 393 393 H 543 V 543 H 393 V 393 Z" />
|
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>
|
</group>
|
||||||
</vector>
|
</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"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:width="108dp"
|
android:width="512dp"
|
||||||
android:height="108dp"
|
android:height="512dp"
|
||||||
android:viewportWidth="108"
|
android:viewportWidth="512"
|
||||||
android:viewportHeight="108">
|
android:viewportHeight="512">
|
||||||
|
|
||||||
<group
|
|
||||||
android:scaleX="0.135"
|
|
||||||
android:scaleY="0.135">
|
|
||||||
<path
|
<path
|
||||||
android:pathData="M 259 259 H 541 V 541 H 259 V 259 Z"
|
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:strokeWidth="18"
|
android:strokeLineJoin="round"
|
||||||
android:strokeColor="#000000" />
|
android:strokeWidth="8.74077"
|
||||||
|
android:fillColor="#ffffff"
|
||||||
|
android:strokeColor="#000000"
|
||||||
|
android:fillAlpha="0.734285"
|
||||||
|
android:strokeLineCap="round"/>
|
||||||
<path
|
<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:fillColor="#000000"
|
||||||
android:pathData="M 257 257 H 407 V 407 H 257 V 257 Z" />
|
android:strokeColor="#00000000"/>
|
||||||
<path
|
|
||||||
android:fillColor="#000000"
|
|
||||||
android:pathData="M 393 393 H 543 V 543 H 393 V 393 Z" />
|
|
||||||
</group>
|
|
||||||
</vector>
|
</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 |
BIN
manager/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 342 B |
BIN
manager/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
manager/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 18 KiB |