Compare commits
55 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bb2d8fd7e0 | ||
|
|
2b6d418fe6 | ||
|
|
d8b1126b96 | ||
|
|
2eeddcfa80 | ||
|
|
59e3675a36 | ||
|
|
bc386f080d | ||
|
|
2dc1377154 | ||
|
|
610852e2f2 | ||
|
|
15b19bb8ce | ||
|
|
4a598b1837 | ||
|
|
caee2417d6 | ||
|
|
349ca36d4e | ||
|
|
ec86f5caf2 | ||
|
|
b5a5cdfcd2 | ||
|
|
72d799e065 | ||
|
|
d06f22dcd0 | ||
|
|
cb90630f27 | ||
|
|
59ad9204d0 | ||
|
|
cb97c16f5e | ||
|
|
69b48d5345 | ||
|
|
45ed4708c9 | ||
|
|
f3c77bdb3b | ||
|
|
dc0eb9eec1 | ||
|
|
83dd6443cb | ||
|
|
3d77f2d135 | ||
|
|
1ea219bddc | ||
|
|
39adba62d1 | ||
|
|
3526e84e04 | ||
|
|
bfdb706b60 | ||
|
|
a297e07055 | ||
|
|
56b4664ec7 | ||
|
|
70f7c75a92 | ||
|
|
e414b4de92 | ||
|
|
79e68f473f | ||
|
|
6656604809 | ||
|
|
85b4d11912 | ||
|
|
7769a23f59 | ||
|
|
c442f43090 | ||
|
|
d73670bf43 | ||
|
|
dd1967f0d0 | ||
|
|
e3d2fc64ac | ||
|
|
e07f20bf29 | ||
|
|
34f216181f | ||
|
|
8aef775474 | ||
|
|
f669ad92b6 | ||
|
|
cc0b272770 | ||
|
|
9ea6de340d | ||
|
|
be37f8a2a3 | ||
|
|
8a12fac39f | ||
|
|
0242fe12e3 | ||
|
|
acf2e1a5ec | ||
|
|
626db4be56 | ||
|
|
5941fa1ec7 | ||
|
|
1dd8651a1a | ||
|
|
33dd0ca16b |
137
.github/workflows/avd-kernel.yml
vendored
Normal file
137
.github/workflows/avd-kernel.yml
vendored
Normal file
@@ -0,0 +1,137 @@
|
||||
name: GKI Kernel Build
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
version_name:
|
||||
required: true
|
||||
type: string
|
||||
description: >
|
||||
With SUBLEVEL of kernel,
|
||||
for example: android12-5.10.66
|
||||
arch:
|
||||
required: true
|
||||
type: string
|
||||
description: >
|
||||
Build arch: aarch64/x86_64
|
||||
debug:
|
||||
required: false
|
||||
type: boolean
|
||||
default: true
|
||||
manifest_name:
|
||||
required: false
|
||||
type: string
|
||||
description: >
|
||||
Local repo manifest xml path,
|
||||
typically for AVD kernel build.
|
||||
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: ubuntu-22.04
|
||||
steps:
|
||||
- name: Maximize build space
|
||||
uses: easimon/maximize-build-space@master
|
||||
with:
|
||||
root-reserve-mb: 8192
|
||||
temp-reserve-mb: 2048
|
||||
remove-dotnet: 'true'
|
||||
remove-android: 'true'
|
||||
remove-haskell: 'true'
|
||||
remove-codeql: 'true'
|
||||
|
||||
- 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
|
||||
mkdir android-kernel && cd android-kernel
|
||||
repo init --depth=1 -u https://android.googlesource.com/kernel/manifest -m "$GITHUB_WORKSPACE/KernelSU/.github/manifests/${{ inputs.manifest_name }}" --repo-rev=v2.16
|
||||
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 KernelSU patches"
|
||||
cd $GKI_ROOT/common/ && git apply $GITHUB_WORKSPACE/KernelSU/.github/patches/$PATCH_PATH/*.patch || echo "[-] No patch found"
|
||||
|
||||
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."
|
||||
cd $GITHUB_WORKSPACE/KernelSU
|
||||
VERSION=$(($(git rev-list --count HEAD) + 10200))
|
||||
echo "VERSION: $VERSION"
|
||||
echo "kernelsu_version=$VERSION" >> $GITHUB_ENV
|
||||
|
||||
- name: Make working directory clean to avoid dirty
|
||||
working-directory: android-kernel
|
||||
run: |
|
||||
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
|
||||
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
|
||||
tools/bazel run --config=fast --config=stamp --lto=thin //common-modules/virtual-device:virtual_device_${{ inputs.arch }}_dist -- --dist_dir=dist
|
||||
NAME=kernel-${{ inputs.arch }}-avd-${{ inputs.version_name }}-${{ env.kernelsu_version }}
|
||||
TARGET_IMAGE=dist/bzImage
|
||||
if [ ! -e $TARGET_IMAGE ]; then
|
||||
TARGET_IMAGE=dist/Image
|
||||
fi
|
||||
mv $TARGET_IMAGE $NAME
|
||||
echo "file_path=android-kernel/$NAME" >> $GITHUB_ENV
|
||||
|
||||
- name: Upload Kernel
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: kernel-${{ inputs.arch }}-avd-${{ inputs.version_name }}-${{ env.kernelsu_version }}
|
||||
path: "${{ env.file_path }}"
|
||||
74
.github/workflows/build-lkm.yml
vendored
Normal file
74
.github/workflows/build-lkm.yml
vendored
Normal file
@@ -0,0 +1,74 @@
|
||||
name: Build LKM for KernelSU
|
||||
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: 233
|
||||
os_patch_level: 2025-02
|
||||
- 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: 129
|
||||
os_patch_level: 2025-04
|
||||
- version: "android15-6.6"
|
||||
sub_level: 82
|
||||
os_patch_level: 2025-04
|
||||
# uses: ./.github/workflows/gki-kernel-mock.yml when debugging
|
||||
uses: ./.github/workflows/gki-kernel.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: ubuntu-latest
|
||||
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
|
||||
76
.github/workflows/build-manager.yml
vendored
76
.github/workflows/build-manager.yml
vendored
@@ -16,8 +16,76 @@ on:
|
||||
- 'manager/**'
|
||||
workflow_call:
|
||||
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: ubuntu-latest
|
||||
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.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.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:
|
||||
@@ -29,7 +97,8 @@ jobs:
|
||||
os: ${{ matrix.os }}
|
||||
|
||||
build-kpmmgr:
|
||||
needs: build-susfs
|
||||
if: ${{ always() }}
|
||||
needs: [ check-build-lkm, build-lkm ]
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
@@ -41,7 +110,8 @@ jobs:
|
||||
os: ${{ matrix.os }}
|
||||
|
||||
build-ksud:
|
||||
needs: build-kpmmgr
|
||||
if: ${{ always() }}
|
||||
needs: [ check-build-lkm, build-lkm ]
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
@@ -53,6 +123,8 @@ jobs:
|
||||
with:
|
||||
target: ${{ matrix.target }}
|
||||
os: ${{ matrix.os }}
|
||||
pack_lkm: true
|
||||
pull_lkm: ${{ needs.check-build-lkm.outputs.build_lkm != 'true' }}
|
||||
|
||||
build-manager:
|
||||
if: ${{ always() }}
|
||||
|
||||
79
.github/workflows/gki-kernel-mock.yml
vendored
Normal file
79
.github/workflows/gki-kernel-mock.yml
vendored
Normal file
@@ -0,0 +1,79 @@
|
||||
name: GKI Kernel Build
|
||||
|
||||
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:
|
||||
mock_build:
|
||||
name: Mock build ${{ inputs.version_name }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Create mocking ko
|
||||
run: |
|
||||
echo "${{ inputs.version }}_kernelsu.ko" > ${{ inputs.version }}_kernelsu.ko
|
||||
- name: Upload LKM
|
||||
uses: actions/upload-artifact@v4
|
||||
if: ${{ inputs.build_lkm == true }}
|
||||
with:
|
||||
name: ${{ inputs.version }}-lkm
|
||||
path: ./*_kernelsu.ko
|
||||
261
.github/workflows/gki-kernel.yml
vendored
Normal file
261
.github/workflows/gki-kernel.yml
vendored
Normal file
@@ -0,0 +1,261 @@
|
||||
name: GKI Kernel Build
|
||||
|
||||
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: ubuntu-latest
|
||||
env:
|
||||
CCACHE_COMPILERCHECK: "%compiler% -dumpmachine; %compiler% -dumpversion"
|
||||
CCACHE_NOHASHDIR: "true"
|
||||
CCACHE_HARDLINK: "true"
|
||||
steps:
|
||||
- name: Maximize build space
|
||||
uses: easimon/maximize-build-space@master
|
||||
with:
|
||||
root-reserve-mb: 8192
|
||||
temp-reserve-mb: 2048
|
||||
remove-dotnet: 'true'
|
||||
remove-android: 'true'
|
||||
remove-haskell: 'true'
|
||||
remove-codeql: 'true'
|
||||
|
||||
- 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
|
||||
mkdir android-kernel && cd android-kernel
|
||||
repo init --depth=1 --u https://android.googlesource.com/kernel/manifest -b common-${{ inputs.tag }} --repo-rev=v2.35
|
||||
REMOTE_BRANCH=$(git ls-remote https://android.googlesource.com/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
|
||||
@@ -1,23 +1,20 @@
|
||||
# SukiSU
|
||||
# SukiSU Ultra
|
||||
|
||||
**English** | [简体中文](README.md) | [日本語](README-ja.md)
|
||||
|
||||
|
||||
Android device root solution based on [KernelSU](https://github.com/tiann/KernelSU)
|
||||
|
||||
**Experimental! Use at your own risk!** This solution is based on [KernelSU](https://github.com/tiann/KernelSU) and is experimental!
|
||||
|
||||
>
|
||||
> This is an unofficial fork. All rights are reserved to [@tiann](https://github.com/tiann)
|
||||
> However, we will be a separately maintained branch of KSU in the future
|
||||
>
|
||||
> However, we will be a separately maintained branch of KSU in the future
|
||||
|
||||
- Fully adapted for non-GKI devices (susfs-dev and unsusfs-patched dev branches only)
|
||||
|
||||
## How to add
|
||||
|
||||
Use the susfs-stable or susfs-dev branch (integrated susfs with support for non-GKI devices)
|
||||
|
||||
```
|
||||
curl -LSs "https://raw.githubusercontent.com/ShirkNeko/SukiSU-Ultra/main/kernel/setup.sh" | bash -s susfs-dev
|
||||
```
|
||||
@@ -31,20 +28,17 @@ curl -LSs "https://raw.githubusercontent.com/ShirkNeko/KernelSU/main/kernel/setu
|
||||
|
||||
1. Use the susfs-dev branch directly without any patching
|
||||
|
||||
|
||||
|
||||
## KPM support
|
||||
|
||||
- We have removed duplicate KSU functions based on KernelPatch and retained KPM support.
|
||||
- We will introduce more APatch-compatible functions to ensure the integrity of KPM functionality.
|
||||
|
||||
|
||||
Open source address: https://github.com/ShirkNeko/SukiSU_KernelPatch_patch
|
||||
|
||||
|
||||
KPM template address: https://github.com/udochina/KPM-Build-Anywhere
|
||||
|
||||
## More links
|
||||
|
||||
Projects compiled based on Sukisu and susfs
|
||||
- [GKI](https://github.com/ShirkNeko/GKI_KernelSU_SUSFS)
|
||||
- [OnePlus](https://github.com/ShirkNeko/Action_OnePlus_MKSU_SUSFS)
|
||||
@@ -53,32 +47,34 @@ Projects compiled based on Sukisu and susfs
|
||||
- This method references the hook method from (https://github.com/rsuntk/KernelSU)
|
||||
|
||||
1. **KPROBES hook:**
|
||||
- This method only supports GKI (5.10 - 6.x) kernels, and all non-GKI kernels must use manual hooks.
|
||||
- For Loadable Kernel Modules (LKM)
|
||||
- Default hooking method for GKI kernels
|
||||
- Requires `CONFIG_KPROBES=y`.
|
||||
2. **Manual hooks:**
|
||||
- For GKI (5.10 - 6.x) kernels, add `CONFIG_KSU_MANUAL_HOOK=y` to the kernel defconfig and make sure to protect KernelSU hooks by using `#ifdef CONFIG_KSU_MANUAL_HOOK` instead of `#ifdef CONFIG_KSU`.
|
||||
- Standard KernelSU hooks: https://kernelsu.org/guide/how-to-integrate-for-non-gki.html#manually-modify-the-kernel-source
|
||||
- backslashxx syscall hooks: https://github.com/backslashxx/KernelSU/issues/5
|
||||
- Some non-GKI devices that manually integrate KPROBES do not require the manual VFS hook `new_hook.patch` patch
|
||||
- Also used for Loadable Kernel Module (LKM)
|
||||
- Default hook method on GKI kernels.
|
||||
- Need `CONFIG_KPROBES=y`
|
||||
|
||||
2. **Manual hook:**
|
||||
- Standard KernelSU hook: https://kernelsu.org/guide/how-to-integrate-for-non-gki.html#manually-modify-the-kernel-source
|
||||
- backslashxx's syscall manual hook: https://github.com/backslashxx/KernelSU/issues/5
|
||||
- Default hook method on Non-GKI kernels.
|
||||
- Need `CONFIG_KSU_MANUAL_HOOK=y`
|
||||
|
||||
## Usage
|
||||
|
||||
### GKI
|
||||
1. such as Xiaomi, Redmi, Samsung, and other devices (does not include manufacturers that modified the kernel like Meizu, OnePlus, RealMe, and OPPO)
|
||||
2. Use the prebuilt GKI kernel, the ones with their name ending with AnyKernel3, mentioned in the 'More Links' section, and then flash it with recoveries like TWRP
|
||||
3. Generally, packages with a plain .zip suffix are universal. However, if your device has a MediaTek processor, you should use the ones with .gz suffix, and packages with .lz4 suffix are dedicated to Google devices.
|
||||
|
||||
Please follow this guide.
|
||||
|
||||
https://kernelsu.org/guide/installation.html
|
||||
|
||||
|
||||
### OnePlus
|
||||
|
||||
1. Use the link mentioned in the 'More Links' section to create a customized build with your device information, and then flash the zip file with the AnyKernel3 suffix.
|
||||
|
||||
> [!Note]
|
||||
> - You only need to fill in the first two parts of kernel versions, such as 5.10, 5.15, 6.1, or 6.6.
|
||||
> - Please search for the processor codename by yourself, usually it is all English without numbers.
|
||||
> - You can find the branch and configuration files from the OnePlus open-source kernel repository.
|
||||
|
||||
|
||||
|
||||
## Features
|
||||
|
||||
1. Kernel-based `su` and root access management.
|
||||
@@ -88,21 +84,18 @@ Projects compiled based on Sukisu and susfs
|
||||
5. More customization
|
||||
6. Support for KPM kernel modules
|
||||
|
||||
|
||||
|
||||
## 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.
|
||||
|
||||
## Sponsorship list
|
||||
|
||||
- [Ktouls](https://github.com/Ktouls) Thanks so much for bringing me support
|
||||
- [zaoqi123](https://github.com/zaoqi123) It's not a bad idea to buy me a milk tea
|
||||
- [wswzgdg](https://github.com/wswzgdg) Many thanks for supporting this project
|
||||
- [yspbwx2010](https://github.com/yspbwx2010) Many thanks
|
||||
|
||||
|
||||
|
||||
- [DARKWWEE](https://github.com/DARKWWEE) Thanks for the 100 USDT Lao
|
||||
|
||||
If the above list does not have your name, I will update it as soon as possible, and thanks again for your support!
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# SukiSU
|
||||
# SukiSU Ultra
|
||||
|
||||
**日本語** | [简体中文](README.md) | [English](README-en.md)
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
**試験中なビルドです!自己責任で使用してください!**<br>
|
||||
このソリューションは [KernelSU](https://github.com/tiann/KernelSU) に基づいていますが、試験中なビルドです。
|
||||
|
||||
>
|
||||
> これは非公式なフォークです。すべての権利は [@tiann](https://github.com/tiann) に帰属します。
|
||||
>
|
||||
> ただし、将来的には KSU とは別に管理されるブランチとなる予定です。
|
||||
@@ -15,8 +14,8 @@
|
||||
- GKI 非対応なデバイスに完全に適応 (susfs-dev と unsusfs-patched dev ブランチのみ)
|
||||
|
||||
## 追加方法
|
||||
susfs-stable または susfs-dev ブランチ (GKI 非対応デバイスに対応する統合された susfs) 使用してください。
|
||||
|
||||
susfs-stable または susfs-dev ブランチ (GKI 非対応デバイスに対応する統合された susfs) 使用してください。
|
||||
```
|
||||
curl -LSs "https://raw.githubusercontent.com/ShirkNeko/SukiSU-Ultra/main/kernel/setup.sh" | bash -s susfs-dev
|
||||
```
|
||||
@@ -26,52 +25,57 @@ curl -LSs "https://raw.githubusercontent.com/ShirkNeko/SukiSU-Ultra/main/kernel/
|
||||
curl -LSs "https://raw.githubusercontent.com/ShirkNeko/KernelSU/main/kernel/setup.sh" | bash -s main
|
||||
```
|
||||
## 統合された susfs の使い方
|
||||
|
||||
1. パッチを当てずに susfs-dev ブランチを直接使用してください。
|
||||
|
||||
## KPM に対応
|
||||
|
||||
- KernelPatch に基づいて重複した KSU の機能を削除、KPM の対応を維持させています。
|
||||
- KPM 機能の整合性を確保するために、APatch の互換機能を更に向上させる予定です。
|
||||
|
||||
|
||||
オープンソースアドレス: https://github.com/ShirkNeko/SukiSU_KernelPatch_patch
|
||||
|
||||
|
||||
KPM テンプレートのアドレス: https://github.com/udochina/KPM-Build-Anywhere
|
||||
|
||||
## その他のリンク
|
||||
|
||||
SukiSU と susfs をベースにコンパイルされたプロジェクトです。
|
||||
|
||||
- [GKI](https://github.com/ShirkNeko/GKI_KernelSU_SUSFS)
|
||||
- [OnePlus](https://github.com/ShirkNeko/Action_OnePlus_MKSU_SUSFS)
|
||||
|
||||
## フックの方式
|
||||
|
||||
- この方式は (https://github.com/rsuntk/KernelSU) のフック方式を参照してください。
|
||||
|
||||
1. **KPROBES フック:**
|
||||
- この方式は GKI (5.10 - 6.x) のカーネルのみに対応しています。GKI 以外のカーネルは手動でフックを使用する必要があります。
|
||||
- 読み込み可能なカーネルモジュールの場合 (LKM)
|
||||
- GKI カーネルのデフォルトとなるフック方式
|
||||
- `CONFIG_KPROBES=y` が必要です。
|
||||
- `CONFIG_KPROBES=y` が必要です
|
||||
|
||||
2. **手動でフック:**
|
||||
- GKI (5.10 - 6.x) のカーネルの場合、カーネルの defconfig に `CONFIG_KSU_MANUAL_HOOK=y` を追加して `#ifdef CONFIG_KSU` ではなく `#ifdef CONFIG_KSU_MANUAL_HOOK` を使用して KernelSU フックを保護するようにしてください。
|
||||
- 標準の KernelSU フック: https://kernelsu.org/guide/how-to-integrate-for-non-gki.html#manually-modify-the-kernel-source
|
||||
- backslashxx syscall フック: https://github.com/backslashxx/KernelSU/issues/5
|
||||
- KPROBES を手動で統合する一部の非 GKI デバイスでは手動の VFS フック `new_hook.patch` パッチは不要です。
|
||||
|
||||
- 非 GKI カーネル用のデフォルトフッキングメソッド
|
||||
- `CONFIG_KSU_MANUAL_HOOK=y` が必要です
|
||||
|
||||
## 使い方
|
||||
|
||||
### GKI
|
||||
1. Xiaomi、Redmi、Samsung などのデバイス (Meizu、OnePlus、Realme、OPPO などのカーネルを変更したメーカー以外)
|
||||
2. `その他のリンク`の項目で言及されているカーネル名が、AnyKernel3 で終わるビルド済みの GKI カーネルを TWRP などのリカバリーでフラッシュします。
|
||||
3. 一般的な .zip の接頭辞を持つパッケージは汎用的になります。ただし、デバイスに MediaTek 製の SoC が搭載されている場合は、.gz の接頭辞を持つパッケージを使用する必要があります。その他に .lz4 の接頭辞を持つパッケージは Google 製デバイス専用です。
|
||||
|
||||
このガイドに従ってください。
|
||||
|
||||
https://kernelsu.org/ja_JP/guide/installation.html
|
||||
|
||||
### OnePlus
|
||||
|
||||
1. `その他のリンク`の項目に記載されているリンクを開き、デバイス情報を使用してカスタマイズされたカーネルをビルドし、AnyKernel3 の接頭辞を持つ .zip ファイルをフラッシュします。
|
||||
|
||||
> [!Note]
|
||||
> - 5.10、5.15、6.1、6.6 などのカーネルバージョンの最初の 2 文字のみを入力する必要があります。
|
||||
> - SoC のコードネームは自分で検索してください。通常は、数字がなく英語表記のみです。
|
||||
> - ブランチと構成ファイルは、OnePlus オープンソースカーネルリポジトリから見つけることができます。
|
||||
|
||||
|
||||
## 機能
|
||||
|
||||
1. カーネルベースな `su` および root アクセスの管理。
|
||||
@@ -81,21 +85,18 @@ SukiSU と susfs をベースにコンパイルされたプロジェクトです
|
||||
5. その他のカスタマイズ
|
||||
6. KPM カーネルモジュールに対応
|
||||
|
||||
|
||||
|
||||
## ライセンス
|
||||
|
||||
- “kernel” ディレクトリ内のファイルは [GPL-2.0](https://www.gnu.org/licenses/old-licenses/gpl-2.0.ja.html) のみライセンス下にあります。
|
||||
- “kernel” ディレクトリを除くその他すべての部分は [GPL-3.0 またはそれ以降](https://www.gnu.org/licenses/gpl-3.0.html) のライセンス下にあります。
|
||||
|
||||
## スポンサーシップの一覧
|
||||
|
||||
- [Ktouls](https://github.com/Ktouls) 応援をしてくれたことに感謝。
|
||||
- [zaoqi123](https://github.com/zaoqi123) ミルクティーを買ってあげるのも良い考えですね。
|
||||
- [wswzgdg](https://github.com/wswzgdg) このプロジェクトを支援していただき、ありがとうございます。
|
||||
- [yspbwx2010](https://github.com/yspbwx2010) どうもありがとう。
|
||||
|
||||
|
||||
|
||||
- [DARKWWEE](https://github.com/DARKWWEE) ラオウ100USDTありがとう!
|
||||
|
||||
上記の一覧にあなたの名前がない場合は、できるだけ早急に更新しますので再度ご支援をお願いします。
|
||||
|
||||
|
||||
@@ -6,23 +6,19 @@
|
||||
|
||||
**实验性! 使用风险自负!**
|
||||
|
||||
|
||||
>
|
||||
> 这是非官方分支,保留所有权利 [@tiann](https://github.com/tiann)
|
||||
> 但是,我们将会在未来成为一个单独维护的KSU分支
|
||||
>
|
||||
|
||||
> 但是,我们将会在未来成为一个单独维护的 KSU 分支
|
||||
|
||||
## 如何添加
|
||||
|
||||
在内核源码的根目录下执行以下命令:
|
||||
|
||||
使用 susfs-dev 分支(已集成 susfs,带非 GKI 设备的支持)
|
||||
|
||||
```
|
||||
curl -LSs "https://raw.githubusercontent.com/ShirkNeko/SukiSU-Ultra/main/kernel/setup.sh" | bash -s susfs-dev
|
||||
```
|
||||
|
||||
|
||||
使用 main 分支
|
||||
```
|
||||
curl -LSs "https://raw.githubusercontent.com/ShirkNeko/SukiSU-Ultra/main/kernel/setup.sh" | bash -s main
|
||||
@@ -33,46 +29,49 @@ curl -LSs "https://raw.githubusercontent.com/ShirkNeko/SukiSU-Ultra/main/kernel/
|
||||
1. 直接使用 susfs-stable 或者 susfs-dev 分支,不需要再集成 susfs
|
||||
|
||||
## 钩子方法
|
||||
|
||||
- 此部分引用自 [rsuntk 的钩子方法](https://github.com/rsuntk/KernelSU)
|
||||
|
||||
1. **KPROBES 钩子:**
|
||||
- 此方法仅支持 GKI 2.0 (5.10 - 6.x) 内核, 所有非 GKI 2.0 内核都必须使用手动钩子
|
||||
- 用于可加载内核模块 (LKM)
|
||||
- GKI 2.0 内核的默认钩子方法
|
||||
- 需要 `CONFIG_KPROBES=y`
|
||||
|
||||
2. **手动钩子:**
|
||||
- 对于 GKI 2.0 (5.10 - 6.x) 内核,需要在对应设备的 defconfig 文件中添加 `CONFIG_KSU_MANUAL_HOOK=y` 并确保使用 `#ifdef CONFIG_KSU_MANUAL_HOOK` 而不是 `#ifdef CONFIG_KSU` 来保护 KernelSU 钩子
|
||||
- 标准的 KernelSU 钩子:https://kernelsu.org/guide/how-to-integrate-for-non-gki.html#manually-modify-the-kernel-source
|
||||
- backslashxx 的 syscall 手动钩子:https://github.com/backslashxx/KernelSU/issues/5
|
||||
- 部分手动集成 KPROBES 的非 GKI 2.0 设备不需要手动 VFS 钩子 `new_hook.patch` 补丁
|
||||
|
||||
- 非 GKI 内核的默认挂钩方法
|
||||
- 需要 `CONFIG_KSU_MANUAL_HOOK=y`
|
||||
|
||||
## KPM 支持
|
||||
|
||||
- 我们基于KernelPatch去掉了和KSU重复的功能,保留了KPM支持
|
||||
- 我们基于 KernelPatch 去掉了和 KSU 重复的功能,仅保留了 KPM 支持
|
||||
- 我们将会引入更多的兼容 APatch 的函数来确保 KPM 功能的完整性
|
||||
|
||||
|
||||
开源地址: https://github.com/ShirkNeko/SukiSU_KernelPatch_patch
|
||||
|
||||
|
||||
KPM 模板地址: https://github.com/udochina/KPM-Build-Anywhere
|
||||
|
||||
|
||||
## 更多链接
|
||||
|
||||
基于 SukiSU 和 susfs 编译的项目
|
||||
- [GKI](https://github.com/ShirkNeko/GKI_KernelSU_SUSFS)
|
||||
- [一加](https://github.com/ShirkNeko/Action_OnePlus_MKSU_SUSFS)
|
||||
|
||||
|
||||
## 使用方法
|
||||
|
||||
### GKI
|
||||
1. 适用于如小米红米三星等的 GKI 2.0 的设备 (不包含魔改内核的厂商如魅族、一加、真我和 oppo)
|
||||
2. 找到更多链接里的 GKI 构建的项目找到设备内核版本直接下载用TWRP或者内核刷写工具刷入带 AnyKernel3 后缀的压缩包即可
|
||||
3. 一般不带后缀的 .zip 压缩包是通用,gz 后缀的为天玑机型专用,lz4 后缀的为谷歌系机型专用,一般刷不带后缀的即可
|
||||
### 普适的 GKI
|
||||
|
||||
请**全部**参考 https://kernelsu.org/zh_CN/guide/installation.html
|
||||
|
||||
> [!Note]
|
||||
> 1. 适用于如小米、红米、三星等的 GKI 2.0 的设备 (不包含魔改内核的厂商如魅族、一加、真我和 oppo)
|
||||
> 2. 找到[更多链接](#%E6%9B%B4%E5%A4%9A%E9%93%BE%E6%8E%A5)里的 GKI 构建的项目。找到设备内核版本。然后下载下来,用TWRP或者内核刷写工具刷入带 AnyKernel3 后缀的压缩包即可
|
||||
> 3. 一般不带后缀的 .zip 压缩包是未压缩的,gz 后缀的为天玑机型所使用的压缩方式
|
||||
|
||||
|
||||
### 一加
|
||||
|
||||
1.找到更多链接里的一加项目进行自行填写,然后云编译构建,最后刷入带 AnyKernel3 后缀的压缩包即可
|
||||
|
||||
> [!Note]
|
||||
@@ -80,7 +79,6 @@ KPM模板地址: https://github.com/udochina/KPM-Build-Anywhere
|
||||
> - 处理器代号请自行搜索,一般为全英文不带数字的代号
|
||||
> - 分支和配置文件请自行到一加内核开源地址进行填写
|
||||
|
||||
|
||||
## 特点
|
||||
|
||||
1. 基于内核的 `su` 和 root 访问管理
|
||||
@@ -90,20 +88,18 @@ KPM模板地址: https://github.com/udochina/KPM-Build-Anywhere
|
||||
5. 更多自定义功能
|
||||
6. 对 KPM 内核模块的支持
|
||||
|
||||
|
||||
## 许可证
|
||||
|
||||
- `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)。
|
||||
|
||||
## 赞助名单
|
||||
|
||||
- [Ktouls](https://github.com/Ktouls) 非常感谢你给我带来的支持
|
||||
- [zaoqi123](https://github.com/zaoqi123) 请我喝奶茶也不错
|
||||
- [wswzgdg](https://github.com/wswzgdg) 非常感谢对此项目的支持
|
||||
- [yspbwx2010](https://github.com/yspbwx2010) 非常感谢
|
||||
|
||||
|
||||
|
||||
- [DARKWWEE](https://github.com/DARKWWEE) 感谢老哥的 100 USDT
|
||||
|
||||
如果以上名单没有你的名称,我会及时更新,再次感谢大家的支持
|
||||
|
||||
|
||||
@@ -25,11 +25,11 @@ $(shell cd $(srctree)/$(src); /usr/bin/env PATH="$$PATH":/usr/bin:/usr/local/bin
|
||||
KSU_GIT_VERSION := $(shell cd $(srctree)/$(src); /usr/bin/env PATH="$$PATH":/usr/bin:/usr/local/bin git rev-list --count main)
|
||||
# ksu_version: major * 10000 + git version + 606 for historical reasons
|
||||
$(eval KSU_VERSION=$(shell expr 10000 + $(KSU_GIT_VERSION) + 606))
|
||||
$(info -- SukiSU version: $(KSU_VERSION))
|
||||
$(info -- KernelSU version: $(KSU_VERSION))
|
||||
ccflags-y += -DKSU_VERSION=$(KSU_VERSION)
|
||||
else # If there is no .git file, the default version will be passed.
|
||||
$(warning "KSU_GIT_VERSION not defined! It is better to make SukiSU a git submodule!")
|
||||
ccflags-y += -DKSU_VERSION=12800
|
||||
$(warning "KSU_GIT_VERSION not defined! It is better to make KernelSU a git submodule!")
|
||||
ccflags-y += -DKSU_VERSION=16
|
||||
endif
|
||||
|
||||
ifndef KSU_EXPECTED_SIZE
|
||||
@@ -42,13 +42,26 @@ endif
|
||||
|
||||
ifdef KSU_MANAGER_PACKAGE
|
||||
ccflags-y += -DKSU_MANAGER_PACKAGE=\"$(KSU_MANAGER_PACKAGE)\"
|
||||
$(info -- KernelSU Manager package name: $(KSU_MANAGER_PACKAGE))
|
||||
$(info -- SukiSU Manager package name: $(KSU_MANAGER_PACKAGE))
|
||||
endif
|
||||
|
||||
$(info -- KernelSU Manager signature size: $(KSU_EXPECTED_SIZE))
|
||||
$(info -- KernelSU Manager signature hash: $(KSU_EXPECTED_HASH))
|
||||
$(info -- SukiSU Manager signature size: $(KSU_EXPECTED_SIZE))
|
||||
$(info -- SukiSU Manager signature hash: $(KSU_EXPECTED_HASH))
|
||||
$(info -- Supported Unofficial Manager: 5ec1cff (GKI) ShirkNeko udochina (GKI and KPM))
|
||||
KERNEL_VERSION := $(VERSION).$(PATCHLEVEL)
|
||||
KERNEL_TYPE := Non-GKI
|
||||
# Check for GKI 2.0 (5.10+ or 6.x+)
|
||||
ifneq ($(shell test \( $(VERSION) -ge 5 -a $(PATCHLEVEL) -ge 10 \) -o $(VERSION) -ge 6; echo $$?),0)
|
||||
# Check for GKI 1.0 (5.4)
|
||||
ifeq ($(shell test $(VERSION)-$(PATCHLEVEL) = 5-4; echo $$?),0)
|
||||
KERNEL_TYPE := GKI 1.0
|
||||
endif
|
||||
else
|
||||
KERNEL_TYPE := GKI 2.0
|
||||
endif
|
||||
$(info -- KERNEL_VERSION: $(KERNEL_VERSION))
|
||||
$(info -- KERNEL_TYPE: $(KERNEL_TYPE))
|
||||
|
||||
$(info -- KERNEL_VERSION: $(KERNEL_VERSION))
|
||||
ifeq ($(CONFIG_KPM),y)
|
||||
$(info -- KPM is enabled)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
@file:Suppress("UnstableApiUsage")
|
||||
|
||||
import com.android.build.api.dsl.ApkSigningConfig
|
||||
import com.android.build.gradle.internal.api.BaseVariantOutputImpl
|
||||
import com.android.build.gradle.tasks.PackageAndroidArtifact
|
||||
|
||||
@@ -24,8 +25,18 @@ apksign {
|
||||
keyPasswordProperty = "KEY_PASSWORD"
|
||||
}
|
||||
|
||||
|
||||
android {
|
||||
namespace = "zako.zako.zako"
|
||||
|
||||
/**signingConfigs {
|
||||
create("Debug") {
|
||||
storeFile = file("D:\\other\\AndroidTool\\android_key\\keystore\\release-key.keystore")
|
||||
storePassword = ""
|
||||
keyAlias = ""
|
||||
keyPassword = ""
|
||||
}
|
||||
}**/
|
||||
namespace = "com.sukisu.ultra"
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
@@ -33,6 +44,9 @@ android {
|
||||
isShrinkResources = true
|
||||
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
|
||||
}
|
||||
/**debug {
|
||||
signingConfig = signingConfigs.named("Debug").get() as ApkSigningConfig
|
||||
}**/
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
|
||||
@@ -3,8 +3,10 @@
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||
tools:ignore="ScopedStorage" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
tools:ignore="ScopedStorage" />
|
||||
|
||||
|
||||
<application
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package zako.zako.zako;
|
||||
package com.sukisu.zako;
|
||||
|
||||
import android.content.pm.PackageInfo;
|
||||
import rikka.parcelablelist.ParcelableListSlice;
|
||||
BIN
manager/app/src/main/assets/5_15+-mkbootfs
Normal file
BIN
manager/app/src/main/assets/5_15+-mkbootfs
Normal file
Binary file not shown.
@@ -12,7 +12,7 @@
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT jboolean JNICALL
|
||||
Java_zako_zako_zako_Natives_becomeManager(JNIEnv *env, jobject, jstring pkg) {
|
||||
Java_com_sukisu_ultra_Natives_becomeManager(JNIEnv *env, jobject, jstring pkg) {
|
||||
auto cpkg = env->GetStringUTFChars(pkg, nullptr);
|
||||
auto result = become_manager(cpkg);
|
||||
env->ReleaseStringUTFChars(pkg, cpkg);
|
||||
@@ -21,13 +21,13 @@ Java_zako_zako_zako_Natives_becomeManager(JNIEnv *env, jobject, jstring pkg) {
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT jint JNICALL
|
||||
Java_zako_zako_zako_Natives_getVersion(JNIEnv *env, jobject) {
|
||||
Java_com_sukisu_ultra_Natives_getVersion(JNIEnv *env, jobject) {
|
||||
return get_version();
|
||||
}
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT jintArray JNICALL
|
||||
Java_zako_zako_zako_Natives_getAllowList(JNIEnv *env, jobject) {
|
||||
Java_com_sukisu_ultra_Natives_getAllowList(JNIEnv *env, jobject) {
|
||||
int uids[1024];
|
||||
int size = 0;
|
||||
bool result = get_allow_list(uids, &size);
|
||||
@@ -42,13 +42,13 @@ Java_zako_zako_zako_Natives_getAllowList(JNIEnv *env, jobject) {
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT jboolean JNICALL
|
||||
Java_zako_zako_zako_Natives_isSafeMode(JNIEnv *env, jclass clazz) {
|
||||
Java_com_sukisu_ultra_Natives_isSafeMode(JNIEnv *env, jclass clazz) {
|
||||
return is_safe_mode();
|
||||
}
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT jboolean JNICALL
|
||||
Java_zako_zako_zako_Natives_isLkmMode(JNIEnv *env, jclass clazz) {
|
||||
Java_com_sukisu_ultra_Natives_isLkmMode(JNIEnv *env, jclass clazz) {
|
||||
return is_lkm_mode();
|
||||
}
|
||||
|
||||
@@ -111,7 +111,7 @@ static void fillArrayWithList(JNIEnv *env, jobject list, int *data, int count) {
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT jobject JNICALL
|
||||
Java_zako_zako_zako_Natives_getAppProfile(JNIEnv *env, jobject, jstring pkg, jint uid) {
|
||||
Java_com_sukisu_ultra_Natives_getAppProfile(JNIEnv *env, jobject, jstring pkg, jint uid) {
|
||||
if (env->GetStringLength(pkg) > KSU_MAX_PACKAGE_NAME) {
|
||||
return nullptr;
|
||||
}
|
||||
@@ -129,7 +129,7 @@ Java_zako_zako_zako_Natives_getAppProfile(JNIEnv *env, jobject, jstring pkg, jin
|
||||
|
||||
bool useDefaultProfile = !get_app_profile(key, &profile);
|
||||
|
||||
auto cls = env->FindClass("zako/zako/zako/Natives$Profile");
|
||||
auto cls = env->FindClass("com/sukisu/ultra/Natives$Profile");
|
||||
auto constructor = env->GetMethodID(cls, "<init>", "()V");
|
||||
auto obj = env->NewObject(cls, constructor);
|
||||
auto keyField = env->GetFieldID(cls, "name", "Ljava/lang/String;");
|
||||
@@ -207,8 +207,8 @@ Java_zako_zako_zako_Natives_getAppProfile(JNIEnv *env, jobject, jstring pkg, jin
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT jboolean JNICALL
|
||||
Java_zako_zako_zako_Natives_setAppProfile(JNIEnv *env, jobject clazz, jobject profile) {
|
||||
auto cls = env->FindClass("zako/zako/zako/Natives$Profile");
|
||||
Java_com_sukisu_ultra_Natives_setAppProfile(JNIEnv *env, jobject clazz, jobject profile) {
|
||||
auto cls = env->FindClass("com/sukisu/ultra/Natives$Profile");
|
||||
|
||||
auto keyField = env->GetFieldID(cls, "name", "Ljava/lang/String;");
|
||||
auto currentUidField = env->GetFieldID(cls, "currentUid", "I");
|
||||
@@ -293,16 +293,16 @@ Java_zako_zako_zako_Natives_setAppProfile(JNIEnv *env, jobject clazz, jobject pr
|
||||
}
|
||||
extern "C"
|
||||
JNIEXPORT jboolean JNICALL
|
||||
Java_zako_zako_zako_Natives_uidShouldUmount(JNIEnv *env, jobject thiz, jint uid) {
|
||||
Java_com_sukisu_ultra_Natives_uidShouldUmount(JNIEnv *env, jobject thiz, jint uid) {
|
||||
return uid_should_umount(uid);
|
||||
}
|
||||
extern "C"
|
||||
JNIEXPORT jboolean JNICALL
|
||||
Java_zako_zako_zako_Natives_isSuEnabled(JNIEnv *env, jobject thiz) {
|
||||
Java_com_sukisu_ultra_Natives_isSuEnabled(JNIEnv *env, jobject thiz) {
|
||||
return is_su_enabled();
|
||||
}
|
||||
extern "C"
|
||||
JNIEXPORT jboolean JNICALL
|
||||
Java_zako_zako_zako_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);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package zako.zako.zako
|
||||
package com.sukisu.ultra
|
||||
|
||||
import android.app.Application
|
||||
import coil.Coil
|
||||
@@ -1,4 +1,4 @@
|
||||
package zako.zako.zako
|
||||
package com.sukisu.ultra
|
||||
|
||||
import android.system.Os
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package zako.zako.zako
|
||||
package com.sukisu.ultra
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.annotation.Keep
|
||||
@@ -16,14 +16,14 @@ object Natives {
|
||||
// 10946: add capabilities
|
||||
// 10977: change groups_count and groups to avoid overflow write
|
||||
// 11071: Fix the issue of failing to set a custom SELinux type.
|
||||
const val MINIMAL_SUPPORTED_KERNEL = 11071
|
||||
const val MINIMAL_SUPPORTED_KERNEL = 12800
|
||||
|
||||
// 11640: Support query working mode, LKM or GKI
|
||||
// when MINIMAL_SUPPORTED_KERNEL > 11640, we can remove this constant.
|
||||
const val MINIMAL_SUPPORTED_KERNEL_LKM = 11648
|
||||
const val MINIMAL_SUPPORTED_KERNEL_LKM = 12800
|
||||
|
||||
// 12040: Support disable sucompat mode
|
||||
const val MINIMAL_SUPPORTED_SU_COMPAT = 12040
|
||||
const val MINIMAL_SUPPORTED_SU_COMPAT = 12800
|
||||
const val KERNEL_SU_DOMAIN = "u:r:su:s0"
|
||||
|
||||
const val ROOT_UID = 0
|
||||
446
manager/app/src/main/java/com/sukisu/ultra/flash/KernelFlash.kt
Normal file
446
manager/app/src/main/java/com/sukisu/ultra/flash/KernelFlash.kt
Normal file
@@ -0,0 +1,446 @@
|
||||
package com.sukisu.ultra.flash
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.CheckCircle
|
||||
import androidx.compose.material.icons.filled.Error
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import com.sukisu.ultra.R
|
||||
import com.sukisu.ultra.utils.AssetsUtil
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
|
||||
data class FlashState(
|
||||
val isFlashing: Boolean = false,
|
||||
val isCompleted: Boolean = false,
|
||||
val progress: Float = 0f,
|
||||
val currentStep: String = "",
|
||||
val logs: List<String> = emptyList(),
|
||||
val error: String = ""
|
||||
)
|
||||
|
||||
class HorizonKernelState {
|
||||
private val _state = MutableStateFlow(FlashState())
|
||||
val state: StateFlow<FlashState> = _state.asStateFlow()
|
||||
|
||||
fun updateProgress(progress: Float) {
|
||||
_state.update { it.copy(progress = progress) }
|
||||
}
|
||||
|
||||
fun updateStep(step: String) {
|
||||
_state.update { it.copy(currentStep = step) }
|
||||
}
|
||||
|
||||
fun addLog(log: String) {
|
||||
_state.update {
|
||||
it.copy(logs = it.logs + log)
|
||||
}
|
||||
}
|
||||
|
||||
fun setError(error: String) {
|
||||
_state.update { it.copy(error = error) }
|
||||
}
|
||||
|
||||
fun startFlashing() {
|
||||
_state.update {
|
||||
it.copy(
|
||||
isFlashing = true,
|
||||
isCompleted = false,
|
||||
progress = 0f,
|
||||
currentStep = "under preparation...",
|
||||
logs = emptyList(),
|
||||
error = ""
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun completeFlashing() {
|
||||
_state.update { it.copy(isCompleted = true, progress = 1f) }
|
||||
}
|
||||
|
||||
fun reset() {
|
||||
_state.value = FlashState()
|
||||
}
|
||||
}
|
||||
|
||||
class HorizonKernelWorker(
|
||||
private val context: Context,
|
||||
private val state: HorizonKernelState,
|
||||
private val slot: String? = null
|
||||
) : Thread() {
|
||||
var uri: Uri? = null
|
||||
private lateinit var filePath: String
|
||||
private lateinit var binaryPath: String
|
||||
|
||||
private var onFlashComplete: (() -> Unit)? = null
|
||||
private var originalSlot: String? = null
|
||||
|
||||
fun setOnFlashCompleteListener(listener: () -> Unit) {
|
||||
onFlashComplete = listener
|
||||
}
|
||||
|
||||
override fun run() {
|
||||
state.startFlashing()
|
||||
state.updateStep(context.getString(R.string.horizon_preparing))
|
||||
|
||||
filePath = "${context.filesDir.absolutePath}/${DocumentFile.fromSingleUri(context, uri!!)?.name}"
|
||||
binaryPath = "${context.filesDir.absolutePath}/META-INF/com/google/android/update-binary"
|
||||
|
||||
try {
|
||||
state.updateStep(context.getString(R.string.horizon_cleaning_files))
|
||||
state.updateProgress(0.1f)
|
||||
cleanup()
|
||||
|
||||
if (!rootAvailable()) {
|
||||
state.setError(context.getString(R.string.root_required))
|
||||
return
|
||||
}
|
||||
|
||||
state.updateStep(context.getString(R.string.horizon_copying_files))
|
||||
state.updateProgress(0.2f)
|
||||
copy()
|
||||
|
||||
if (!File(filePath).exists()) {
|
||||
state.setError(context.getString(R.string.horizon_copy_failed))
|
||||
return
|
||||
}
|
||||
|
||||
state.updateStep(context.getString(R.string.horizon_extracting_tool))
|
||||
state.updateProgress(0.4f)
|
||||
getBinary()
|
||||
|
||||
state.updateStep(context.getString(R.string.horizon_patching_script))
|
||||
state.updateProgress(0.6f)
|
||||
patch()
|
||||
|
||||
state.updateStep(context.getString(R.string.horizon_flashing))
|
||||
state.updateProgress(0.7f)
|
||||
|
||||
val isAbDevice = isAbDevice()
|
||||
|
||||
if (isAbDevice && slot != null) {
|
||||
state.updateStep(context.getString(R.string.horizon_getting_original_slot))
|
||||
state.updateProgress(0.72f)
|
||||
originalSlot = runCommandGetOutput(true, "getprop ro.boot.slot_suffix")
|
||||
|
||||
state.updateStep(context.getString(R.string.horizon_setting_target_slot))
|
||||
state.updateProgress(0.74f)
|
||||
runCommand(true, "resetprop -n ro.boot.slot_suffix _$slot")
|
||||
}
|
||||
|
||||
flash()
|
||||
|
||||
if (isAbDevice && !originalSlot.isNullOrEmpty()) {
|
||||
state.updateStep(context.getString(R.string.horizon_restoring_original_slot))
|
||||
state.updateProgress(0.8f)
|
||||
runCommand(true, "resetprop ro.boot.slot_suffix $originalSlot")
|
||||
}
|
||||
|
||||
state.updateStep(context.getString(R.string.horizon_flash_complete_status))
|
||||
state.completeFlashing()
|
||||
|
||||
(context as? Activity)?.runOnUiThread {
|
||||
onFlashComplete?.invoke()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
state.setError(e.message ?: context.getString(R.string.horizon_unknown_error))
|
||||
|
||||
if (isAbDevice() && !originalSlot.isNullOrEmpty()) {
|
||||
state.updateStep(context.getString(R.string.horizon_restoring_original_slot))
|
||||
state.updateProgress(0.8f)
|
||||
runCommand(true, "resetprop ro.boot.slot_suffix $originalSlot")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 检查设备是否为AB分区设备
|
||||
private fun isAbDevice(): Boolean {
|
||||
val abUpdate = runCommandGetOutput(true, "getprop ro.build.ab_update")?.trim() ?: ""
|
||||
if (abUpdate.equals("false", ignoreCase = true) || abUpdate.isEmpty()) {
|
||||
return false
|
||||
}
|
||||
|
||||
val slotSuffix = runCommandGetOutput(true, "getprop ro.boot.slot_suffix")
|
||||
return !slotSuffix.isNullOrEmpty()
|
||||
}
|
||||
|
||||
private fun cleanup() {
|
||||
runCommand(false, "find ${context.filesDir.absolutePath} -type f ! -name '*.jpg' ! -name '*.png' -delete")
|
||||
}
|
||||
|
||||
private fun copy() {
|
||||
uri?.let { safeUri ->
|
||||
context.contentResolver.openInputStream(safeUri)?.use { input ->
|
||||
FileOutputStream(File(filePath)).use { output ->
|
||||
input.copyTo(output)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getBinary() {
|
||||
runCommand(false, "unzip \"$filePath\" \"*/update-binary\" -d ${context.filesDir.absolutePath}")
|
||||
if (!File(binaryPath).exists()) {
|
||||
throw IOException("Failed to extract update-binary")
|
||||
}
|
||||
}
|
||||
|
||||
private fun patch() {
|
||||
val kernelVersion = runCommandGetOutput(true, "cat /proc/version")
|
||||
val versionRegex = """\d+\.\d+\.\d+""".toRegex()
|
||||
val version = kernelVersion?.let { versionRegex.find(it) }?.value ?: ""
|
||||
val toolName = if (version.isNotEmpty()) {
|
||||
val parts = version.split('.')
|
||||
if (parts.size >= 2) {
|
||||
val major = parts[0].toIntOrNull() ?: 0
|
||||
val minor = parts[1].toIntOrNull() ?: 0
|
||||
if (major < 5 || (major == 5 && minor <= 10)) "5_10" else "5_15+"
|
||||
} else {
|
||||
"5_15+"
|
||||
}
|
||||
} else {
|
||||
"5_15+"
|
||||
}
|
||||
val toolPath = "${context.filesDir.absolutePath}/mkbootfs"
|
||||
AssetsUtil.exportFiles(context, "$toolName-mkbootfs", toolPath)
|
||||
state.addLog("${context.getString(R.string.kernel_version_log, version)} ${context.getString(R.string.tool_version_log, toolName)}")
|
||||
runCommand(false, "sed -i '/chmod -R 755 tools bin;/i cp -f $toolPath \$AKHOME/tools;' $binaryPath")
|
||||
}
|
||||
|
||||
private fun flash() {
|
||||
val process = ProcessBuilder("su")
|
||||
.redirectErrorStream(true)
|
||||
.start()
|
||||
|
||||
try {
|
||||
process.outputStream.bufferedWriter().use { writer ->
|
||||
writer.write("export POSTINSTALL=${context.filesDir.absolutePath}\n")
|
||||
|
||||
// 写入槽位信息到临时文件
|
||||
slot?.let { selectedSlot ->
|
||||
writer.write("echo \"$selectedSlot\" > ${context.filesDir.absolutePath}/bootslot\n")
|
||||
}
|
||||
|
||||
// 构建刷写命令
|
||||
val flashCommand = buildString {
|
||||
append("sh $binaryPath 3 1 \"$filePath\"")
|
||||
if (slot != null) {
|
||||
append(" \"$(cat ${context.filesDir.absolutePath}/bootslot)\"")
|
||||
}
|
||||
append(" && touch ${context.filesDir.absolutePath}/done\n")
|
||||
}
|
||||
|
||||
writer.write(flashCommand)
|
||||
writer.write("exit\n")
|
||||
writer.flush()
|
||||
}
|
||||
|
||||
process.inputStream.bufferedReader().use { reader ->
|
||||
reader.lineSequence().forEach { line ->
|
||||
if (line.startsWith("ui_print")) {
|
||||
val logMessage = line.removePrefix("ui_print").trim()
|
||||
state.addLog(logMessage)
|
||||
|
||||
when {
|
||||
logMessage.contains("extracting", ignoreCase = true) -> {
|
||||
state.updateProgress(0.75f)
|
||||
}
|
||||
logMessage.contains("installing", ignoreCase = true) -> {
|
||||
state.updateProgress(0.85f)
|
||||
}
|
||||
logMessage.contains("complete", ignoreCase = true) -> {
|
||||
state.updateProgress(0.95f)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
process.destroy()
|
||||
}
|
||||
|
||||
if (!File("${context.filesDir.absolutePath}/done").exists()) {
|
||||
throw IOException(context.getString(R.string.flash_failed_message))
|
||||
}
|
||||
}
|
||||
|
||||
private fun runCommand(su: Boolean, cmd: String): Int {
|
||||
val process = ProcessBuilder(if (su) "su" else "sh")
|
||||
.redirectErrorStream(true)
|
||||
.start()
|
||||
|
||||
return try {
|
||||
process.outputStream.bufferedWriter().use { writer ->
|
||||
writer.write("$cmd\n")
|
||||
writer.write("exit\n")
|
||||
writer.flush()
|
||||
}
|
||||
process.waitFor()
|
||||
} finally {
|
||||
process.destroy()
|
||||
}
|
||||
}
|
||||
|
||||
private fun runCommandGetOutput(su: Boolean, cmd: String): String? {
|
||||
val process = ProcessBuilder(if (su) "su" else "sh")
|
||||
.redirectErrorStream(true)
|
||||
.start()
|
||||
|
||||
return try {
|
||||
process.outputStream.bufferedWriter().use { writer ->
|
||||
writer.write("$cmd\n")
|
||||
writer.write("exit\n")
|
||||
writer.flush()
|
||||
}
|
||||
process.inputStream.bufferedReader().use { reader ->
|
||||
reader.readText().trim()
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
""
|
||||
} finally {
|
||||
process.destroy()
|
||||
}
|
||||
}
|
||||
|
||||
private fun rootAvailable(): Boolean {
|
||||
return try {
|
||||
val process = Runtime.getRuntime().exec("su -c id")
|
||||
val exitValue = process.waitFor()
|
||||
exitValue == 0
|
||||
} catch (_: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun HorizonKernelFlashProgress(state: FlashState) {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.horizon_flash_title),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
modifier = Modifier.padding(bottom = 8.dp)
|
||||
)
|
||||
|
||||
LinearProgressIndicator(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 8.dp),
|
||||
progress = { state.progress },
|
||||
)
|
||||
|
||||
Text(
|
||||
text = state.currentStep,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
modifier = Modifier.padding(vertical = 4.dp)
|
||||
)
|
||||
|
||||
if (state.logs.isNotEmpty()) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.horizon_logs_label),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
modifier = Modifier
|
||||
.align(Alignment.Start)
|
||||
.padding(top = 8.dp, bottom = 4.dp)
|
||||
)
|
||||
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(max = 230.dp)
|
||||
.padding(vertical = 4.dp),
|
||||
color = MaterialTheme.colorScheme.surface,
|
||||
tonalElevation = 1.dp,
|
||||
shape = MaterialTheme.shapes.small
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(8.dp)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
state.logs.forEach { log ->
|
||||
Text(
|
||||
text = log,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
modifier = Modifier.padding(vertical = 2.dp),
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (state.error.isNotEmpty()) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 8.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Error,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.error,
|
||||
modifier = Modifier.padding(end = 8.dp)
|
||||
)
|
||||
Text(
|
||||
text = state.error,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.error
|
||||
)
|
||||
}
|
||||
} else if (state.isCompleted) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 8.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.CheckCircle,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.padding(end = 8.dp)
|
||||
)
|
||||
Text(
|
||||
text = stringResource(id = R.string.horizon_flash_complete),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package zako.zako.zako.profile
|
||||
package com.sukisu.ultra.profile
|
||||
|
||||
/**
|
||||
* @author weishu
|
||||
@@ -1,4 +1,4 @@
|
||||
package zako.zako.zako.profile
|
||||
package com.sukisu.ultra.profile
|
||||
|
||||
/**
|
||||
* https://cs.android.com/android/platform/superproject/main/+/main:system/core/libcutils/include/private/android_filesystem_config.h
|
||||
@@ -1,4 +1,4 @@
|
||||
package zako.zako.zako.ui;
|
||||
package com.sukisu.ultra.ui;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
@@ -17,7 +17,7 @@ import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import zako.zako.zako.IKsuInterface;
|
||||
import com.sukisu.zako.IKsuInterface;
|
||||
import rikka.parcelablelist.ParcelableListSlice;
|
||||
|
||||
/**
|
||||
249
manager/app/src/main/java/com/sukisu/ultra/ui/MainActivity.kt
Normal file
249
manager/app/src/main/java/com/sukisu/ultra/ui/MainActivity.kt
Normal file
@@ -0,0 +1,249 @@
|
||||
package com.sukisu.ultra.ui
|
||||
|
||||
import android.database.ContentObserver
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavBackStackEntry
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import com.ramcosta.composedestinations.DestinationsNavHost
|
||||
import com.ramcosta.composedestinations.animations.NavHostAnimatedDestinationStyle
|
||||
import com.ramcosta.composedestinations.generated.NavGraphs
|
||||
import com.ramcosta.composedestinations.spec.NavHostGraphSpec
|
||||
import com.ramcosta.composedestinations.spec.RouteOrDirection
|
||||
import com.ramcosta.composedestinations.utils.isRouteOnBackStackAsState
|
||||
import com.ramcosta.composedestinations.utils.rememberDestinationsNavigator
|
||||
import io.sukisu.ultra.UltraToolInstall
|
||||
import com.sukisu.ultra.Natives
|
||||
import com.sukisu.ultra.ksuApp
|
||||
import com.sukisu.ultra.ui.screen.BottomBarDestination
|
||||
import com.sukisu.ultra.ui.theme.*
|
||||
import com.sukisu.ultra.ui.theme.CardConfig.cardAlpha
|
||||
import com.sukisu.ultra.ui.util.*
|
||||
import androidx.core.content.edit
|
||||
import com.sukisu.ultra.ui.theme.CardConfig.cardElevation
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
private inner class ThemeChangeContentObserver(
|
||||
handler: Handler,
|
||||
private val onThemeChanged: () -> Unit
|
||||
) : ContentObserver(handler) {
|
||||
override fun onChange(selfChange: Boolean) {
|
||||
super.onChange(selfChange)
|
||||
onThemeChanged()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
// Enable edge to edge
|
||||
enableEdgeToEdge()
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
window.isNavigationBarContrastEnforced = false
|
||||
}
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val prefs = getSharedPreferences("settings", MODE_PRIVATE)
|
||||
val isFirstRun = prefs.getBoolean("is_first_run", true)
|
||||
|
||||
if (isFirstRun) {
|
||||
ThemeConfig.preventBackgroundRefresh = false
|
||||
getSharedPreferences("theme_prefs", MODE_PRIVATE).edit {
|
||||
putBoolean("prevent_background_refresh", false)
|
||||
}
|
||||
prefs.edit { putBoolean("is_first_run", false) }
|
||||
}
|
||||
|
||||
// 加载保存的背景设置
|
||||
loadThemeMode()
|
||||
loadThemeColors()
|
||||
loadDynamicColorState()
|
||||
CardConfig.load(applicationContext)
|
||||
|
||||
val contentObserver = ThemeChangeContentObserver(Handler(mainLooper)) {
|
||||
runOnUiThread {
|
||||
if (!ThemeConfig.preventBackgroundRefresh) {
|
||||
ThemeConfig.backgroundImageLoaded = false
|
||||
loadCustomBackground()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
contentResolver.registerContentObserver(
|
||||
android.provider.Settings.System.getUriFor("ui_night_mode"),
|
||||
false,
|
||||
contentObserver
|
||||
)
|
||||
|
||||
val destroyListeners = mutableListOf<() -> Unit>()
|
||||
destroyListeners.add {
|
||||
contentResolver.unregisterContentObserver(contentObserver)
|
||||
}
|
||||
|
||||
val isManager = Natives.becomeManager(ksuApp.packageName)
|
||||
if (isManager) {
|
||||
install()
|
||||
UltraToolInstall.tryToInstall()
|
||||
}
|
||||
|
||||
setContent {
|
||||
KernelSUTheme {
|
||||
val navController = rememberNavController()
|
||||
val snackBarHostState = remember { SnackbarHostState() }
|
||||
|
||||
Scaffold(
|
||||
bottomBar = { BottomBar(navController) },
|
||||
contentWindowInsets = WindowInsets(0, 0, 0, 0)
|
||||
) { innerPadding ->
|
||||
CompositionLocalProvider(
|
||||
LocalSnackbarHost provides snackBarHostState
|
||||
) {
|
||||
DestinationsNavHost(
|
||||
modifier = Modifier.padding(innerPadding),
|
||||
navGraph = NavGraphs.root as NavHostGraphSpec,
|
||||
navController = navController,
|
||||
defaultTransitions = object : NavHostAnimatedDestinationStyle() {
|
||||
override val enterTransition: AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition
|
||||
get() = { fadeIn(animationSpec = tween(340)) }
|
||||
override val exitTransition: AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition
|
||||
get() = { fadeOut(animationSpec = tween(340)) }
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
CardConfig.save(applicationContext)
|
||||
getSharedPreferences("theme_prefs", MODE_PRIVATE).edit() {
|
||||
putBoolean("prevent_background_refresh", true)
|
||||
}
|
||||
ThemeConfig.preventBackgroundRefresh = true
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
if (!ThemeConfig.backgroundImageLoaded && !ThemeConfig.preventBackgroundRefresh) {
|
||||
loadCustomBackground()
|
||||
}
|
||||
}
|
||||
|
||||
private val destroyListeners = mutableListOf<() -> Unit>()
|
||||
|
||||
override fun onDestroy() {
|
||||
destroyListeners.forEach { it() }
|
||||
super.onDestroy()
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun BottomBar(navController: NavHostController) {
|
||||
val navigator = navController.rememberDestinationsNavigator()
|
||||
val isManager = Natives.becomeManager(ksuApp.packageName)
|
||||
val fullFeatured = isManager && !Natives.requireNewKernel() && rootAvailable()
|
||||
val kpmVersion = getKpmVersion()
|
||||
val containerColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
val cardColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
|
||||
NavigationBar(
|
||||
modifier = Modifier.windowInsetsPadding(
|
||||
WindowInsets.navigationBars.only(WindowInsetsSides.Horizontal)
|
||||
),
|
||||
containerColor = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = cardColor.copy(alpha = cardAlpha),
|
||||
scrolledContainerColor = containerColor.copy(alpha = cardAlpha)
|
||||
).containerColor,
|
||||
tonalElevation = cardElevation
|
||||
) {
|
||||
BottomBarDestination.entries.forEach { destination ->
|
||||
if (destination == BottomBarDestination.Kpm) {
|
||||
if (kpmVersion.isNotEmpty() && !kpmVersion.startsWith("Error")) {
|
||||
if (!fullFeatured && destination.rootRequired) return@forEach
|
||||
val isCurrentDestOnBackStack by navController.isRouteOnBackStackAsState(destination.direction)
|
||||
NavigationBarItem(
|
||||
selected = isCurrentDestOnBackStack,
|
||||
onClick = {
|
||||
if (!isCurrentDestOnBackStack) {
|
||||
navigator.navigate(destination.direction) {
|
||||
popUpTo(NavGraphs.root as RouteOrDirection) {
|
||||
saveState = true
|
||||
}
|
||||
launchSingleTop = true
|
||||
restoreState = true
|
||||
}
|
||||
}
|
||||
},
|
||||
icon = {
|
||||
Icon(
|
||||
imageVector = if (isCurrentDestOnBackStack) {
|
||||
destination.iconSelected
|
||||
} else {
|
||||
destination.iconNotSelected
|
||||
},
|
||||
contentDescription = stringResource(destination.label),
|
||||
tint = if (isCurrentDestOnBackStack) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
},
|
||||
label = {
|
||||
Text(
|
||||
text = stringResource(destination.label),
|
||||
style = MaterialTheme.typography.labelMedium
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
} else {
|
||||
if (!fullFeatured && destination.rootRequired) return@forEach
|
||||
val isCurrentDestOnBackStack by navController.isRouteOnBackStackAsState(destination.direction)
|
||||
NavigationBarItem(
|
||||
selected = isCurrentDestOnBackStack,
|
||||
onClick = {
|
||||
if (!isCurrentDestOnBackStack) {
|
||||
navigator.navigate(destination.direction) {
|
||||
popUpTo(NavGraphs.root as RouteOrDirection) {
|
||||
saveState = true
|
||||
}
|
||||
launchSingleTop = true
|
||||
restoreState = true
|
||||
}
|
||||
}
|
||||
},
|
||||
icon = {
|
||||
Icon(
|
||||
imageVector = if (isCurrentDestOnBackStack) {
|
||||
destination.iconSelected
|
||||
} else {
|
||||
destination.iconNotSelected
|
||||
},
|
||||
contentDescription = stringResource(destination.label),
|
||||
tint = if (isCurrentDestOnBackStack) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
},
|
||||
label = {
|
||||
Text(
|
||||
text = stringResource(destination.label),
|
||||
style = MaterialTheme.typography.labelMedium
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package zako.zako.zako.ui.component
|
||||
package com.sukisu.ultra.ui.component
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@@ -31,8 +31,8 @@ import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import zako.zako.zako.BuildConfig
|
||||
import zako.zako.zako.R
|
||||
import com.sukisu.ultra.BuildConfig
|
||||
import com.sukisu.ultra.R
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
@@ -1,4 +1,4 @@
|
||||
package zako.zako.zako.ui.component
|
||||
package com.sukisu.ultra.ui.component
|
||||
|
||||
import android.graphics.text.LineBreaker
|
||||
import android.os.Build
|
||||
@@ -0,0 +1,223 @@
|
||||
package com.sukisu.ultra.ui.component
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.gestures.detectTransformGestures
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.Fullscreen
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import coil.compose.AsyncImage
|
||||
import coil.request.ImageRequest
|
||||
import com.sukisu.ultra.R
|
||||
import com.sukisu.ultra.ui.util.BackgroundTransformation
|
||||
import com.sukisu.ultra.ui.util.saveTransformedBackground
|
||||
import kotlinx.coroutines.launch
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.layout.onSizeChanged
|
||||
import kotlin.math.max
|
||||
|
||||
@Composable
|
||||
fun ImageEditorDialog(
|
||||
imageUri: Uri,
|
||||
onDismiss: () -> Unit,
|
||||
onConfirm: (Uri) -> Unit
|
||||
) {
|
||||
var scale by remember { mutableFloatStateOf(1f) }
|
||||
var offsetX by remember { mutableFloatStateOf(0f) }
|
||||
var offsetY by remember { mutableFloatStateOf(0f) }
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
var lastScale by remember { mutableFloatStateOf(1f) }
|
||||
var lastOffsetX by remember { mutableFloatStateOf(0f) }
|
||||
var lastOffsetY by remember { mutableFloatStateOf(0f) }
|
||||
var imageSize by remember { mutableStateOf(Size.Zero) }
|
||||
var screenSize by remember { mutableStateOf(Size.Zero) }
|
||||
val animatedScale by animateFloatAsState(
|
||||
targetValue = scale,
|
||||
label = "ScaleAnimation"
|
||||
)
|
||||
val animatedOffsetX by animateFloatAsState(
|
||||
targetValue = offsetX,
|
||||
label = "OffsetXAnimation"
|
||||
)
|
||||
val animatedOffsetY by animateFloatAsState(
|
||||
targetValue = offsetY,
|
||||
label = "OffsetYAnimation"
|
||||
)
|
||||
val updateTransformation = remember {
|
||||
{ newScale: Float, newOffsetX: Float, newOffsetY: Float ->
|
||||
val scaleDiff = kotlin.math.abs(newScale - lastScale)
|
||||
val offsetXDiff = kotlin.math.abs(newOffsetX - lastOffsetX)
|
||||
val offsetYDiff = kotlin.math.abs(newOffsetY - lastOffsetY)
|
||||
if (scaleDiff > 0.01f || offsetXDiff > 1f || offsetYDiff > 1f) {
|
||||
scale = newScale
|
||||
offsetX = newOffsetX
|
||||
offsetY = newOffsetY
|
||||
lastScale = newScale
|
||||
lastOffsetX = newOffsetX
|
||||
lastOffsetY = newOffsetY
|
||||
}
|
||||
}
|
||||
}
|
||||
val scaleToFullScreen = remember {
|
||||
{
|
||||
if (imageSize.height > 0 && screenSize.height > 0) {
|
||||
val newScale = screenSize.height / imageSize.height
|
||||
updateTransformation(newScale, 0f, 0f)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Dialog(
|
||||
onDismissRequest = onDismiss,
|
||||
properties = DialogProperties(
|
||||
dismissOnBackPress = true,
|
||||
dismissOnClickOutside = false,
|
||||
usePlatformDefaultWidth = false
|
||||
)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color.Black.copy(alpha = 0.9f))
|
||||
.onSizeChanged { size ->
|
||||
screenSize = Size(size.width.toFloat(), size.height.toFloat())
|
||||
}
|
||||
) {
|
||||
AsyncImage(
|
||||
model = ImageRequest.Builder(LocalContext.current)
|
||||
.data(imageUri)
|
||||
.crossfade(true)
|
||||
.build(),
|
||||
contentDescription = stringResource(R.string.settings_custom_background),
|
||||
contentScale = ContentScale.Fit,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.graphicsLayer(
|
||||
scaleX = animatedScale,
|
||||
scaleY = animatedScale,
|
||||
translationX = animatedOffsetX,
|
||||
translationY = animatedOffsetY
|
||||
)
|
||||
.pointerInput(Unit) {
|
||||
detectTransformGestures { _, pan, zoom, _ ->
|
||||
scope.launch {
|
||||
try {
|
||||
val newScale = (scale * zoom).coerceIn(0.5f, 3f)
|
||||
val maxOffsetX = max(0f, size.width * (newScale - 1) / 2)
|
||||
val maxOffsetY = max(0f, size.height * (newScale - 1) / 2)
|
||||
val newOffsetX = if (maxOffsetX > 0) {
|
||||
(offsetX + pan.x).coerceIn(-maxOffsetX, maxOffsetX)
|
||||
} else {
|
||||
0f
|
||||
}
|
||||
val newOffsetY = if (maxOffsetY > 0) {
|
||||
(offsetY + pan.y).coerceIn(-maxOffsetY, maxOffsetY)
|
||||
} else {
|
||||
0f
|
||||
}
|
||||
updateTransformation(newScale, newOffsetX, newOffsetY)
|
||||
} catch (e: Exception) {
|
||||
updateTransformation(lastScale, lastOffsetX, lastOffsetY)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.onSizeChanged { size ->
|
||||
imageSize = Size(size.width.toFloat(), size.height.toFloat())
|
||||
}
|
||||
)
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp)
|
||||
.align(Alignment.TopCenter),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
IconButton(
|
||||
onClick = onDismiss,
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.background(Color.Black.copy(alpha = 0.6f))
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Close,
|
||||
contentDescription = stringResource(R.string.cancel),
|
||||
tint = Color.White
|
||||
)
|
||||
}
|
||||
IconButton(
|
||||
onClick = { scaleToFullScreen() },
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.background(Color.Black.copy(alpha = 0.6f))
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Fullscreen,
|
||||
contentDescription = stringResource(R.string.reprovision),
|
||||
tint = Color.White
|
||||
)
|
||||
}
|
||||
IconButton(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
try {
|
||||
val transformation = BackgroundTransformation(scale, offsetX, offsetY)
|
||||
val savedUri = context.saveTransformedBackground(imageUri, transformation)
|
||||
savedUri?.let { onConfirm(it) }
|
||||
} catch (e: Exception) {
|
||||
""
|
||||
}
|
||||
}
|
||||
},
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.background(Color.Black.copy(alpha = 0.6f))
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Check,
|
||||
contentDescription = stringResource(R.string.confirm),
|
||||
tint = Color.White
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp)
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.background(Color.Black.copy(alpha = 0.6f))
|
||||
.padding(16.dp)
|
||||
.align(Alignment.BottomCenter)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.image_editor_hint),
|
||||
color = Color.White,
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package zako.zako.zako.ui.component
|
||||
package com.sukisu.ultra.ui.component
|
||||
|
||||
import androidx.compose.foundation.focusable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.sukisu.ultra.ui.component
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import com.sukisu.ultra.Natives
|
||||
import com.sukisu.ultra.ksuApp
|
||||
|
||||
@Composable
|
||||
fun KsuIsValid(
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val isManager = Natives.becomeManager(ksuApp.packageName)
|
||||
val ksuVersion = if (isManager) Natives.version else null
|
||||
|
||||
if (ksuVersion != null) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package zako.zako.zako.ui.component
|
||||
package com.sukisu.ultra.ui.component
|
||||
|
||||
import android.util.Log
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
@@ -42,7 +42,7 @@ import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import zako.zako.zako.ui.theme.CardConfig
|
||||
import com.sukisu.ultra.ui.theme.CardConfig
|
||||
|
||||
private const val TAG = "SearchBar"
|
||||
|
||||
@@ -63,7 +63,7 @@ fun SearchAppBar(
|
||||
var onSearch by remember { mutableStateOf(false) }
|
||||
|
||||
// 获取卡片颜色和透明度
|
||||
val cardColor = MaterialTheme.colorScheme.secondaryContainer
|
||||
val cardColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
val cardAlpha = CardConfig.cardAlpha
|
||||
|
||||
if (onSearch) {
|
||||
@@ -1,4 +1,4 @@
|
||||
package zako.zako.zako.ui.component
|
||||
package com.sukisu.ultra.ui.component
|
||||
|
||||
import androidx.compose.foundation.LocalIndication
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
@@ -0,0 +1,248 @@
|
||||
package com.sukisu.ultra.ui.component
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.horizontalScroll
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.sukisu.ultra.R
|
||||
import com.sukisu.ultra.ui.theme.ThemeConfig
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.SdStorage
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
|
||||
/**
|
||||
* 槽位选择对话框组件
|
||||
* 用于Kernel刷写时选择目标槽位
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SlotSelectionDialog(
|
||||
show: Boolean,
|
||||
onDismiss: () -> Unit,
|
||||
onSlotSelected: (String) -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
var currentSlot by remember { mutableStateOf<String?>(null) }
|
||||
var errorMessage by remember { mutableStateOf<String?>(null) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
try {
|
||||
currentSlot = getCurrentSlot(context)
|
||||
errorMessage = null
|
||||
} catch (e: Exception) {
|
||||
errorMessage = e.message
|
||||
currentSlot = null
|
||||
}
|
||||
}
|
||||
|
||||
if (show) {
|
||||
val cardColor = if (!ThemeConfig.useDynamicColor) {
|
||||
ThemeConfig.currentTheme.ButtonContrast
|
||||
} else {
|
||||
MaterialTheme.colorScheme.surfaceContainerHigh
|
||||
}
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(id = R.string.select_slot_title),
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
},
|
||||
text = {
|
||||
Column(
|
||||
modifier = Modifier.padding(vertical = 8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
if (errorMessage != null) {
|
||||
Text(
|
||||
text = "Error: $errorMessage",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
text = stringResource(
|
||||
id = R.string.current_slot,
|
||||
currentSlot ?: "Unknown"
|
||||
),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
Text(
|
||||
text = stringResource(id = R.string.select_slot_description),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
// Horizontal arrangement for slot options with highlighted current slot
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.horizontalScroll(rememberScrollState()),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
val slotOptions = listOf(
|
||||
ListOption(
|
||||
titleText = stringResource(id = R.string.slot_a),
|
||||
subtitleText = if (currentSlot == "a" || currentSlot == "_a") stringResource(id = R.string.currently_selected) else null,
|
||||
icon = Icons.Filled.SdStorage
|
||||
),
|
||||
ListOption(
|
||||
titleText = stringResource(id = R.string.slot_b),
|
||||
subtitleText = if (currentSlot == "b" || currentSlot == "_b") stringResource(id = R.string.currently_selected) else null,
|
||||
icon = Icons.Filled.SdStorage
|
||||
)
|
||||
)
|
||||
|
||||
slotOptions.forEachIndexed { index, option ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(horizontal = 8.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(MaterialTheme.shapes.medium)
|
||||
.background(
|
||||
color = if (option.subtitleText != null) {
|
||||
MaterialTheme.colorScheme.primary.copy(alpha = 0.9f)
|
||||
} else {
|
||||
MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f)
|
||||
}
|
||||
)
|
||||
.clickable {
|
||||
onSlotSelected(
|
||||
when (index) {
|
||||
0 -> "a"
|
||||
else -> "b"
|
||||
}
|
||||
)
|
||||
}
|
||||
.padding(vertical = 12.dp, horizontal = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = option.icon,
|
||||
contentDescription = null,
|
||||
tint = if (option.subtitleText != null) {
|
||||
MaterialTheme.colorScheme.onPrimary
|
||||
} else {
|
||||
MaterialTheme.colorScheme.primary
|
||||
},
|
||||
modifier = Modifier
|
||||
.padding(end = 16.dp)
|
||||
.size(24.dp)
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Text(
|
||||
text = option.titleText,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = if (option.subtitleText != null) {
|
||||
MaterialTheme.colorScheme.onPrimary
|
||||
} else {
|
||||
MaterialTheme.colorScheme.primary
|
||||
}
|
||||
)
|
||||
option.subtitleText?.let {
|
||||
Text(
|
||||
text = it,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = if (true) {
|
||||
MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.8f)
|
||||
} else {
|
||||
MaterialTheme.colorScheme.onSurfaceVariant
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
currentSlot?.let { onSlotSelected(it) }
|
||||
onDismiss()
|
||||
}
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(android.R.string.ok),
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(
|
||||
onClick = onDismiss
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(android.R.string.cancel),
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
},
|
||||
containerColor = cardColor,
|
||||
shape = MaterialTheme.shapes.extraLarge,
|
||||
tonalElevation = 4.dp
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Data class for list options
|
||||
data class ListOption(
|
||||
val titleText: String,
|
||||
val subtitleText: String?,
|
||||
val icon: ImageVector
|
||||
)
|
||||
|
||||
// Utility function to get current slot
|
||||
private fun getCurrentSlot(context: Context): String? {
|
||||
return runCommandGetOutput(true, "getprop ro.boot.slot_suffix")?.let {
|
||||
if (it.startsWith("_")) it.substring(1) else it
|
||||
}
|
||||
}
|
||||
|
||||
private fun runCommandGetOutput(su: Boolean, cmd: String): String? {
|
||||
return try {
|
||||
val process = ProcessBuilder(if (su) "su" else "sh").start()
|
||||
process.outputStream.bufferedWriter().use { writer ->
|
||||
writer.write("$cmd\n")
|
||||
writer.write("exit\n")
|
||||
writer.flush()
|
||||
}
|
||||
process.inputStream.bufferedReader().use { reader ->
|
||||
reader.readText().trim()
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package com.sukisu.ultra.ui.component
|
||||
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.SwitchDefaults
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
fun SwitchItem(
|
||||
icon: ImageVector,
|
||||
title: String,
|
||||
summary: String? = null,
|
||||
checked: Boolean,
|
||||
enabled: Boolean = true,
|
||||
onCheckedChange: (Boolean) -> Unit
|
||||
) {
|
||||
// 颜色动画
|
||||
val iconTint by animateColorAsState(
|
||||
targetValue = if (checked) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f),
|
||||
animationSpec = tween(300),
|
||||
label = "iconTint"
|
||||
)
|
||||
|
||||
// 开关颜色
|
||||
val switchColors = SwitchDefaults.colors(
|
||||
checkedThumbColor = MaterialTheme.colorScheme.primary,
|
||||
checkedTrackColor = MaterialTheme.colorScheme.primaryContainer,
|
||||
checkedBorderColor = MaterialTheme.colorScheme.primary,
|
||||
checkedIconColor = MaterialTheme.colorScheme.onPrimary,
|
||||
uncheckedThumbColor = MaterialTheme.colorScheme.outline,
|
||||
uncheckedTrackColor = MaterialTheme.colorScheme.surfaceVariant,
|
||||
uncheckedBorderColor = MaterialTheme.colorScheme.outline,
|
||||
uncheckedIconColor = MaterialTheme.colorScheme.surfaceVariant,
|
||||
disabledCheckedThumbColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f),
|
||||
disabledCheckedTrackColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f),
|
||||
disabledCheckedBorderColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f),
|
||||
disabledCheckedIconColor = MaterialTheme.colorScheme.surfaceVariant,
|
||||
disabledUncheckedThumbColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f),
|
||||
disabledUncheckedTrackColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f),
|
||||
disabledUncheckedBorderColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f),
|
||||
disabledUncheckedIconColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
)
|
||||
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
},
|
||||
supportingContent = summary?.let {
|
||||
{
|
||||
Text(
|
||||
text = it,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
maxLines = Int.MAX_VALUE,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
},
|
||||
leadingContent = {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(24.dp),
|
||||
tint = iconTint
|
||||
)
|
||||
},
|
||||
trailingContent = {
|
||||
Switch(
|
||||
checked = checked,
|
||||
onCheckedChange = null,
|
||||
enabled = enabled,
|
||||
colors = switchColors
|
||||
)
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(enabled = enabled) {
|
||||
onCheckedChange(!checked)
|
||||
}
|
||||
.padding(vertical = 4.dp)
|
||||
)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package zako.zako.zako.ui.component.profile
|
||||
package com.sukisu.ultra.ui.component.profile
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
@@ -11,9 +11,9 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import zako.zako.zako.Natives
|
||||
import zako.zako.zako.R
|
||||
import zako.zako.zako.ui.component.SwitchItem
|
||||
import com.sukisu.ultra.Natives
|
||||
import com.sukisu.ultra.R
|
||||
import com.sukisu.ultra.ui.component.SwitchItem
|
||||
|
||||
@Composable
|
||||
fun AppProfileConfig(
|
||||
@@ -1,4 +1,4 @@
|
||||
package zako.zako.zako.ui.component.profile
|
||||
package com.sukisu.ultra.ui.component.profile
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@@ -42,12 +42,12 @@ import com.maxkeppeler.sheets.input.models.ValidationResult
|
||||
import com.maxkeppeler.sheets.list.ListDialog
|
||||
import com.maxkeppeler.sheets.list.models.ListOption
|
||||
import com.maxkeppeler.sheets.list.models.ListSelection
|
||||
import zako.zako.zako.Natives
|
||||
import zako.zako.zako.R
|
||||
import zako.zako.zako.profile.Capabilities
|
||||
import zako.zako.zako.profile.Groups
|
||||
import zako.zako.zako.ui.component.rememberCustomDialog
|
||||
import zako.zako.zako.ui.util.isSepolicyValid
|
||||
import com.sukisu.ultra.Natives
|
||||
import com.sukisu.ultra.R
|
||||
import com.sukisu.ultra.profile.Capabilities
|
||||
import com.sukisu.ultra.profile.Groups
|
||||
import com.sukisu.ultra.ui.component.rememberCustomDialog
|
||||
import com.sukisu.ultra.ui.util.isSepolicyValid
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@@ -1,4 +1,4 @@
|
||||
package zako.zako.zako.ui.component.profile
|
||||
package com.sukisu.ultra.ui.component.profile
|
||||
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.material.icons.Icons
|
||||
@@ -23,11 +23,11 @@ import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import zako.zako.zako.Natives
|
||||
import zako.zako.zako.R
|
||||
import zako.zako.zako.ui.util.listAppProfileTemplates
|
||||
import zako.zako.zako.ui.util.setSepolicy
|
||||
import zako.zako.zako.ui.viewmodel.getTemplateInfoById
|
||||
import com.sukisu.ultra.Natives
|
||||
import com.sukisu.ultra.R
|
||||
import com.sukisu.ultra.ui.util.listAppProfileTemplates
|
||||
import com.sukisu.ultra.ui.util.setSepolicy
|
||||
import com.sukisu.ultra.ui.viewmodel.getTemplateInfoById
|
||||
|
||||
/**
|
||||
* @author weishu
|
||||
@@ -0,0 +1,593 @@
|
||||
package com.sukisu.ultra.ui.screen
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.animation.expandVertically
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.shrinkVertically
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.only
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.safeDrawing
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.AccountCircle
|
||||
import androidx.compose.material.icons.filled.Android
|
||||
import androidx.compose.material.icons.filled.Security
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ElevatedCard
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FilterChip
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.IconButtonDefaults
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarColors
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.DpOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.dropUnlessResumed
|
||||
import coil.compose.AsyncImage
|
||||
import coil.request.ImageRequest
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||
import com.ramcosta.composedestinations.generated.destinations.AppProfileTemplateScreenDestination
|
||||
import com.ramcosta.composedestinations.generated.destinations.TemplateEditorScreenDestination
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
import com.sukisu.ultra.Natives
|
||||
import com.sukisu.ultra.R
|
||||
import com.sukisu.ultra.ui.component.SwitchItem
|
||||
import com.sukisu.ultra.ui.component.profile.AppProfileConfig
|
||||
import com.sukisu.ultra.ui.component.profile.RootProfileConfig
|
||||
import com.sukisu.ultra.ui.component.profile.TemplateConfig
|
||||
import com.sukisu.ultra.ui.theme.CardConfig
|
||||
import com.sukisu.ultra.ui.util.LocalSnackbarHost
|
||||
import com.sukisu.ultra.ui.util.forceStopApp
|
||||
import com.sukisu.ultra.ui.util.getSepolicy
|
||||
import com.sukisu.ultra.ui.util.launchApp
|
||||
import com.sukisu.ultra.ui.util.restartApp
|
||||
import com.sukisu.ultra.ui.util.setSepolicy
|
||||
import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel
|
||||
import com.sukisu.ultra.ui.viewmodel.getTemplateInfoById
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* @author weishu
|
||||
* @date 2023/5/16.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Destination<RootGraph>
|
||||
@Composable
|
||||
fun AppProfileScreen(
|
||||
navigator: DestinationsNavigator,
|
||||
appInfo: SuperUserViewModel.AppInfo,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val snackBarHost = LocalSnackbarHost.current
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
|
||||
val scope = rememberCoroutineScope()
|
||||
val failToUpdateAppProfile = stringResource(R.string.failed_to_update_app_profile).format(appInfo.label)
|
||||
val failToUpdateSepolicy = stringResource(R.string.failed_to_update_sepolicy).format(appInfo.label)
|
||||
val suNotAllowed = stringResource(R.string.su_not_allowed).format(appInfo.label)
|
||||
|
||||
val packageName = appInfo.packageName
|
||||
val initialProfile = Natives.getAppProfile(packageName, appInfo.uid)
|
||||
if (initialProfile.allowSu) {
|
||||
initialProfile.rules = getSepolicy(packageName)
|
||||
}
|
||||
var profile by rememberSaveable {
|
||||
mutableStateOf(initialProfile)
|
||||
}
|
||||
|
||||
val cardColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
val cardAlpha = CardConfig.cardAlpha
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopBar(
|
||||
title = appInfo.label,
|
||||
packageName = packageName,
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = cardColor.copy(alpha = cardAlpha),
|
||||
scrolledContainerColor = cardColor.copy(alpha = cardAlpha)
|
||||
),
|
||||
onBack = dropUnlessResumed { navigator.popBackStack() },
|
||||
scrollBehavior = scrollBehavior
|
||||
)
|
||||
},
|
||||
snackbarHost = { SnackbarHost(hostState = snackBarHost) },
|
||||
contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal)
|
||||
) { paddingValues ->
|
||||
AppProfileInner(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
packageName = appInfo.packageName,
|
||||
appLabel = appInfo.label,
|
||||
appIcon = {
|
||||
AsyncImage(
|
||||
model = ImageRequest.Builder(context).data(appInfo.packageInfo).crossfade(true).build(),
|
||||
contentDescription = appInfo.label,
|
||||
modifier = Modifier
|
||||
.padding(4.dp)
|
||||
.width(48.dp)
|
||||
.height(48.dp)
|
||||
)
|
||||
},
|
||||
profile = profile,
|
||||
onViewTemplate = {
|
||||
getTemplateInfoById(it)?.let { info ->
|
||||
navigator.navigate(TemplateEditorScreenDestination(info))
|
||||
}
|
||||
},
|
||||
onManageTemplate = {
|
||||
navigator.navigate(AppProfileTemplateScreenDestination())
|
||||
},
|
||||
onProfileChange = {
|
||||
scope.launch {
|
||||
if (it.allowSu) {
|
||||
// sync with allowlist.c - forbid_system_uid
|
||||
if (appInfo.uid < 2000 && appInfo.uid != 1000) {
|
||||
snackBarHost.showSnackbar(suNotAllowed)
|
||||
return@launch
|
||||
}
|
||||
if (!it.rootUseDefault && it.rules.isNotEmpty() && !setSepolicy(profile.name, it.rules)) {
|
||||
snackBarHost.showSnackbar(failToUpdateSepolicy)
|
||||
return@launch
|
||||
}
|
||||
}
|
||||
if (!Natives.setAppProfile(it)) {
|
||||
snackBarHost.showSnackbar(failToUpdateAppProfile.format(appInfo.uid))
|
||||
} else {
|
||||
profile = it
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AppProfileInner(
|
||||
modifier: Modifier = Modifier,
|
||||
packageName: String,
|
||||
appLabel: String,
|
||||
appIcon: @Composable () -> Unit,
|
||||
profile: Natives.Profile,
|
||||
onViewTemplate: (id: String) -> Unit = {},
|
||||
onManageTemplate: () -> Unit = {},
|
||||
onProfileChange: (Natives.Profile) -> Unit,
|
||||
) {
|
||||
val isRootGranted = profile.allowSu
|
||||
|
||||
Column(modifier = modifier) {
|
||||
ElevatedCard(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
shape = MaterialTheme.shapes.medium
|
||||
) {
|
||||
AppMenuBox(packageName) {
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
Text(
|
||||
text = appLabel,
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
},
|
||||
supportingContent = {
|
||||
Text(
|
||||
text = packageName,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
},
|
||||
leadingContent = appIcon,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
ElevatedCard(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
shape = MaterialTheme.shapes.medium
|
||||
) {
|
||||
SwitchItem(
|
||||
icon = Icons.Filled.Security,
|
||||
title = stringResource(id = R.string.superuser),
|
||||
checked = isRootGranted,
|
||||
onCheckedChange = { onProfileChange(profile.copy(allowSu = it)) },
|
||||
)
|
||||
}
|
||||
|
||||
Crossfade(
|
||||
targetState = isRootGranted,
|
||||
label = "RootAccess"
|
||||
) { current ->
|
||||
Column(
|
||||
modifier = Modifier.padding(bottom = 6.dp + 48.dp + 6.dp /* SnackBar height */)
|
||||
) {
|
||||
if (current) {
|
||||
val initialMode = if (profile.rootUseDefault) {
|
||||
Mode.Default
|
||||
} else if (profile.rootTemplate != null) {
|
||||
Mode.Template
|
||||
} else {
|
||||
Mode.Custom
|
||||
}
|
||||
var mode by rememberSaveable {
|
||||
mutableStateOf(initialMode)
|
||||
}
|
||||
|
||||
ElevatedCard(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
shape = MaterialTheme.shapes.medium
|
||||
) {
|
||||
ProfileBox(mode, true) {
|
||||
// template mode shouldn't change profile here!
|
||||
if (it == Mode.Default || it == Mode.Custom) {
|
||||
onProfileChange(profile.copy(rootUseDefault = it == Mode.Default))
|
||||
}
|
||||
mode = it
|
||||
}
|
||||
}
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = mode != Mode.Default,
|
||||
enter = fadeIn() + expandVertically(),
|
||||
exit = fadeOut() + shrinkVertically()
|
||||
) {
|
||||
ElevatedCard(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
shape = MaterialTheme.shapes.medium
|
||||
) {
|
||||
Column(modifier = Modifier.padding(vertical = 8.dp)) {
|
||||
Crossfade(targetState = mode, label = "ProfileMode") { currentMode ->
|
||||
when (currentMode) {
|
||||
Mode.Template -> {
|
||||
TemplateConfig(
|
||||
profile = profile,
|
||||
onViewTemplate = onViewTemplate,
|
||||
onManageTemplate = onManageTemplate,
|
||||
onProfileChange = onProfileChange
|
||||
)
|
||||
}
|
||||
Mode.Custom -> {
|
||||
RootProfileConfig(
|
||||
fixedName = true,
|
||||
profile = profile,
|
||||
onProfileChange = onProfileChange
|
||||
)
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val mode = if (profile.nonRootUseDefault) Mode.Default else Mode.Custom
|
||||
|
||||
ElevatedCard(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
shape = MaterialTheme.shapes.medium
|
||||
) {
|
||||
ProfileBox(mode, false) {
|
||||
onProfileChange(profile.copy(nonRootUseDefault = (it == Mode.Default)))
|
||||
}
|
||||
}
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = mode == Mode.Custom,
|
||||
enter = fadeIn() + expandVertically(),
|
||||
exit = fadeOut() + shrinkVertically()
|
||||
) {
|
||||
ElevatedCard(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
shape = MaterialTheme.shapes.medium
|
||||
) {
|
||||
Column(modifier = Modifier.padding(vertical = 8.dp)) {
|
||||
AppProfileConfig(
|
||||
fixedName = true,
|
||||
profile = profile,
|
||||
enabled = mode == Mode.Custom,
|
||||
onProfileChange = onProfileChange
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private enum class Mode(@StringRes private val res: Int) {
|
||||
Default(R.string.profile_default), Template(R.string.profile_template), Custom(R.string.profile_custom);
|
||||
|
||||
val text: String
|
||||
@Composable get() = stringResource(res)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun TopBar(
|
||||
title: String,
|
||||
packageName: String,
|
||||
onBack: () -> Unit,
|
||||
colors: TopAppBarColors,
|
||||
scrollBehavior: TopAppBarScrollBehavior? = null
|
||||
) {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Column {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
Text(
|
||||
text = packageName,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.alpha(0.8f)
|
||||
)
|
||||
}
|
||||
},
|
||||
colors = colors,
|
||||
navigationIcon = {
|
||||
IconButton(
|
||||
onClick = onBack,
|
||||
colors = IconButtonDefaults.iconButtonColors(
|
||||
contentColor = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = stringResource(R.string.back)
|
||||
)
|
||||
}
|
||||
},
|
||||
windowInsets = WindowInsets.safeDrawing.only(
|
||||
WindowInsetsSides.Top + WindowInsetsSides.Horizontal
|
||||
),
|
||||
scrollBehavior = scrollBehavior,
|
||||
modifier = Modifier.shadow(
|
||||
elevation = if ((scrollBehavior?.state?.overlappedFraction ?: 0f) > 0.01f)
|
||||
4.dp else 0.dp,
|
||||
spotColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ProfileBox(
|
||||
mode: Mode,
|
||||
hasTemplate: Boolean,
|
||||
onModeChange: (Mode) -> Unit,
|
||||
) {
|
||||
Column(modifier = Modifier.padding(vertical = 8.dp)) {
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
Text(
|
||||
text = stringResource(R.string.profile),
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
},
|
||||
supportingContent = {
|
||||
Text(
|
||||
text = mode.text,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
},
|
||||
leadingContent = {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.AccountCircle,
|
||||
contentDescription = null,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
HorizontalDivider(
|
||||
thickness = Dp.Hairline,
|
||||
color = MaterialTheme.colorScheme.outlineVariant
|
||||
)
|
||||
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 8.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally)
|
||||
) {
|
||||
FilterChip(
|
||||
selected = mode == Mode.Default,
|
||||
onClick = { onModeChange(Mode.Default) },
|
||||
label = {
|
||||
Text(
|
||||
text = stringResource(R.string.profile_default),
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
},
|
||||
shape = MaterialTheme.shapes.small
|
||||
)
|
||||
|
||||
if (hasTemplate) {
|
||||
FilterChip(
|
||||
selected = mode == Mode.Template,
|
||||
onClick = { onModeChange(Mode.Template) },
|
||||
label = {
|
||||
Text(
|
||||
text = stringResource(R.string.profile_template),
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
},
|
||||
shape = MaterialTheme.shapes.small
|
||||
)
|
||||
}
|
||||
|
||||
FilterChip(
|
||||
selected = mode == Mode.Custom,
|
||||
onClick = { onModeChange(Mode.Custom) },
|
||||
label = {
|
||||
Text(
|
||||
text = stringResource(R.string.profile_custom),
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
},
|
||||
shape = MaterialTheme.shapes.small
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("UnusedBoxWithConstraintsScope")
|
||||
@Composable
|
||||
private fun AppMenuBox(packageName: String, content: @Composable () -> Unit) {
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
var touchPoint: Offset by remember { mutableStateOf(Offset.Zero) }
|
||||
val density = LocalDensity.current
|
||||
|
||||
BoxWithConstraints(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.pointerInput(Unit) {
|
||||
detectTapGestures(
|
||||
onLongPress = {
|
||||
touchPoint = it
|
||||
expanded = true
|
||||
}
|
||||
)
|
||||
}
|
||||
) {
|
||||
content()
|
||||
|
||||
val (offsetX, offsetY) = with(density) {
|
||||
(touchPoint.x.toDp()) to (touchPoint.y.toDp())
|
||||
}
|
||||
|
||||
DropdownMenu(
|
||||
expanded = expanded,
|
||||
offset = DpOffset(offsetX, -offsetY),
|
||||
onDismissRequest = {
|
||||
expanded = false
|
||||
},
|
||||
) {
|
||||
AppMenuOption(
|
||||
text = stringResource(id = R.string.launch_app),
|
||||
onClick = {
|
||||
expanded = false
|
||||
launchApp(packageName)
|
||||
}
|
||||
)
|
||||
|
||||
AppMenuOption(
|
||||
text = stringResource(id = R.string.force_stop_app),
|
||||
onClick = {
|
||||
expanded = false
|
||||
forceStopApp(packageName)
|
||||
}
|
||||
)
|
||||
|
||||
AppMenuOption(
|
||||
text = stringResource(id = R.string.restart_app),
|
||||
onClick = {
|
||||
expanded = false
|
||||
restartApp(packageName)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AppMenuOption(text: String, onClick: () -> Unit) {
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(
|
||||
text = text,
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
},
|
||||
onClick = onClick
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun AppProfilePreview() {
|
||||
var profile by remember { mutableStateOf(Natives.Profile("")) }
|
||||
Surface {
|
||||
AppProfileInner(
|
||||
packageName = "icu.nullptr.test",
|
||||
appLabel = "Test",
|
||||
appIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Android,
|
||||
contentDescription = null,
|
||||
)
|
||||
},
|
||||
profile = profile,
|
||||
onProfileChange = {
|
||||
profile = it
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package zako.zako.zako.ui.screen
|
||||
package com.sukisu.ultra.ui.screen
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.material.icons.Icons
|
||||
@@ -11,7 +11,7 @@ import com.ramcosta.composedestinations.generated.destinations.SuperUserScreenDe
|
||||
import com.ramcosta.composedestinations.generated.destinations.SettingScreenDestination
|
||||
import com.ramcosta.composedestinations.generated.destinations.KpmScreenDestination
|
||||
import com.ramcosta.composedestinations.spec.DirectionDestinationSpec
|
||||
import zako.zako.zako.R
|
||||
import com.sukisu.ultra.R
|
||||
|
||||
enum class BottomBarDestination(
|
||||
val direction: DirectionDestinationSpec,
|
||||
@@ -1,4 +1,4 @@
|
||||
package zako.zako.zako.ui.screen
|
||||
package com.sukisu.ultra.ui.screen
|
||||
|
||||
import android.os.Environment
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@@ -37,10 +37,10 @@ import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import zako.zako.zako.R
|
||||
import zako.zako.zako.ui.component.KeyEventBlocker
|
||||
import zako.zako.zako.ui.util.LocalSnackbarHost
|
||||
import zako.zako.zako.ui.util.runModuleAction
|
||||
import com.sukisu.ultra.R
|
||||
import com.sukisu.ultra.ui.component.KeyEventBlocker
|
||||
import com.sukisu.ultra.ui.util.LocalSnackbarHost
|
||||
import com.sukisu.ultra.ui.util.runModuleAction
|
||||
import java.io.File
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
@@ -1,4 +1,4 @@
|
||||
package zako.zako.zako.ui.screen
|
||||
package com.sukisu.ultra.ui.screen
|
||||
|
||||
import android.net.Uri
|
||||
import android.os.Environment
|
||||
@@ -30,9 +30,9 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import zako.zako.zako.ui.component.KeyEventBlocker
|
||||
import zako.zako.zako.ui.util.*
|
||||
import zako.zako.zako.R
|
||||
import com.sukisu.ultra.ui.component.KeyEventBlocker
|
||||
import com.sukisu.ultra.ui.util.*
|
||||
import com.sukisu.ultra.R
|
||||
import java.io.File
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
@@ -45,10 +45,6 @@ enum class FlashingStatus {
|
||||
|
||||
private var currentFlashingStatus = mutableStateOf(FlashingStatus.FLASHING)
|
||||
|
||||
fun getFlashingStatus(): FlashingStatus {
|
||||
return currentFlashingStatus.value
|
||||
}
|
||||
|
||||
fun setFlashingStatus(status: FlashingStatus) {
|
||||
currentFlashingStatus.value = status
|
||||
}
|
||||
1024
manager/app/src/main/java/com/sukisu/ultra/ui/screen/Home.kt
Normal file
1024
manager/app/src/main/java/com/sukisu/ultra/ui/screen/Home.kt
Normal file
File diff suppressed because it is too large
Load Diff
788
manager/app/src/main/java/com/sukisu/ultra/ui/screen/Install.kt
Normal file
788
manager/app/src/main/java/com/sukisu/ultra/ui/screen/Install.kt
Normal file
@@ -0,0 +1,788 @@
|
||||
package com.sukisu.ultra.ui.screen
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.widget.Toast
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.expandVertically
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.shrinkVertically
|
||||
import androidx.compose.foundation.LocalIndication
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.only
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.safeDrawing
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.selection.toggleable
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.AutoFixHigh
|
||||
import androidx.compose.material.icons.filled.FileUpload
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.ElevatedCard
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.RadioButton
|
||||
import androidx.compose.material3.RadioButtonDefaults
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||
import androidx.compose.material3.rememberTopAppBarState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.maxkeppeker.sheets.core.models.base.Header
|
||||
import com.maxkeppeker.sheets.core.models.base.rememberUseCaseState
|
||||
import com.maxkeppeler.sheets.list.ListDialog
|
||||
import com.maxkeppeler.sheets.list.models.ListOption
|
||||
import com.maxkeppeler.sheets.list.models.ListSelection
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||
import com.ramcosta.composedestinations.generated.destinations.FlashScreenDestination
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
import com.ramcosta.composedestinations.navigation.EmptyDestinationsNavigator
|
||||
import com.sukisu.ultra.R
|
||||
import com.sukisu.ultra.flash.HorizonKernelFlashProgress
|
||||
import com.sukisu.ultra.flash.HorizonKernelState
|
||||
import com.sukisu.ultra.flash.HorizonKernelWorker
|
||||
import com.sukisu.ultra.ui.component.DialogHandle
|
||||
import com.sukisu.ultra.ui.component.SlotSelectionDialog
|
||||
import com.sukisu.ultra.ui.component.rememberConfirmDialog
|
||||
import com.sukisu.ultra.ui.component.rememberCustomDialog
|
||||
import com.sukisu.ultra.ui.theme.CardConfig.cardAlpha
|
||||
import com.sukisu.ultra.ui.theme.CardConfig.cardElevation
|
||||
import com.sukisu.ultra.ui.theme.getCardColors
|
||||
import com.sukisu.ultra.ui.util.LkmSelection
|
||||
import com.sukisu.ultra.ui.util.getCurrentKmi
|
||||
import com.sukisu.ultra.ui.util.getSupportedKmis
|
||||
import com.sukisu.ultra.ui.util.isAbDevice
|
||||
import com.sukisu.ultra.ui.util.isInitBoot
|
||||
import com.sukisu.ultra.ui.util.rootAvailable
|
||||
import com.sukisu.ultra.getKernelVersion
|
||||
|
||||
/**
|
||||
* @author weishu
|
||||
* @date 2024/3/12.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Destination<RootGraph>
|
||||
@Composable
|
||||
fun InstallScreen(navigator: DestinationsNavigator) {
|
||||
var installMethod by remember { mutableStateOf<InstallMethod?>(null) }
|
||||
var lkmSelection by remember { mutableStateOf<LkmSelection>(LkmSelection.KmiNone) }
|
||||
val context = LocalContext.current
|
||||
var showRebootDialog by remember { mutableStateOf(false) }
|
||||
var showSlotSelectionDialog by remember { mutableStateOf(false) }
|
||||
var tempKernelUri by remember { mutableStateOf<Uri?>(null) }
|
||||
val horizonKernelState = remember { HorizonKernelState() }
|
||||
val flashState by horizonKernelState.state.collectAsState()
|
||||
val summary = stringResource(R.string.horizon_kernel_summary)
|
||||
val kernelVersion = getKernelVersion()
|
||||
val isGKI = kernelVersion.isGKI()
|
||||
val isAbDevice = isAbDevice()
|
||||
|
||||
val onFlashComplete = {
|
||||
showRebootDialog = true
|
||||
}
|
||||
|
||||
if (showRebootDialog) {
|
||||
RebootDialog(
|
||||
show = true,
|
||||
onDismiss = { showRebootDialog = false },
|
||||
onConfirm = {
|
||||
showRebootDialog = false
|
||||
try {
|
||||
val process = Runtime.getRuntime().exec("su")
|
||||
process.outputStream.bufferedWriter().use { writer ->
|
||||
writer.write("svc power reboot\n")
|
||||
writer.write("exit\n")
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
Toast.makeText(context, R.string.failed_reboot, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
val onInstall = {
|
||||
installMethod?.let { method ->
|
||||
when (method) {
|
||||
is InstallMethod.HorizonKernel -> {
|
||||
method.uri?.let { uri ->
|
||||
val worker = HorizonKernelWorker(
|
||||
context = context,
|
||||
state = horizonKernelState,
|
||||
slot = method.slot
|
||||
)
|
||||
worker.uri = uri
|
||||
worker.setOnFlashCompleteListener(onFlashComplete)
|
||||
worker.start()
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
val flashIt = FlashIt.FlashBoot(
|
||||
boot = if (method is InstallMethod.SelectFile) method.uri else null,
|
||||
lkm = lkmSelection,
|
||||
ota = method is InstallMethod.DirectInstallToInactiveSlot
|
||||
)
|
||||
navigator.navigate(FlashScreenDestination(flashIt))
|
||||
}
|
||||
}
|
||||
}
|
||||
Unit
|
||||
}
|
||||
|
||||
// 槽位选择
|
||||
SlotSelectionDialog(
|
||||
show = showSlotSelectionDialog && isAbDevice,
|
||||
onDismiss = { showSlotSelectionDialog = false },
|
||||
onSlotSelected = { slot ->
|
||||
showSlotSelectionDialog = false
|
||||
val horizonMethod = InstallMethod.HorizonKernel(
|
||||
uri = tempKernelUri,
|
||||
slot = slot,
|
||||
summary = summary
|
||||
)
|
||||
installMethod = horizonMethod
|
||||
}
|
||||
)
|
||||
|
||||
val currentKmi by produceState(initialValue = "") {
|
||||
value = getCurrentKmi()
|
||||
}
|
||||
|
||||
val selectKmiDialog = rememberSelectKmiDialog { kmi ->
|
||||
kmi?.let {
|
||||
lkmSelection = LkmSelection.KmiString(it)
|
||||
onInstall()
|
||||
}
|
||||
}
|
||||
|
||||
val onClickNext = {
|
||||
if (isGKI && lkmSelection == LkmSelection.KmiNone && currentKmi.isBlank()) {
|
||||
selectKmiDialog.show()
|
||||
} else {
|
||||
onInstall()
|
||||
}
|
||||
}
|
||||
|
||||
val selectLkmLauncher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.StartActivityForResult()
|
||||
) {
|
||||
if (it.resultCode == Activity.RESULT_OK) {
|
||||
it.data?.data?.let { uri ->
|
||||
lkmSelection = LkmSelection.LkmUri(uri)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val onLkmUpload = {
|
||||
selectLkmLauncher.launch(Intent(Intent.ACTION_GET_CONTENT).apply {
|
||||
type = "application/octet-stream"
|
||||
})
|
||||
}
|
||||
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopBar(
|
||||
onBack = { navigator.popBackStack() },
|
||||
onLkmUpload = onLkmUpload,
|
||||
scrollBehavior = scrollBehavior
|
||||
)
|
||||
},
|
||||
contentWindowInsets = WindowInsets.safeDrawing.only(
|
||||
WindowInsetsSides.Top + WindowInsetsSides.Horizontal
|
||||
)
|
||||
) { innerPadding ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(innerPadding)
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection)
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(top = 12.dp)
|
||||
) {
|
||||
SelectInstallMethod(
|
||||
isGKI = isGKI,
|
||||
isAbDevice = isAbDevice,
|
||||
onSelected = { method ->
|
||||
if (method is InstallMethod.HorizonKernel && method.uri != null) {
|
||||
if (isAbDevice) {
|
||||
tempKernelUri = method.uri
|
||||
showSlotSelectionDialog = true
|
||||
} else {
|
||||
installMethod = method
|
||||
}
|
||||
} else {
|
||||
installMethod = method
|
||||
}
|
||||
horizonKernelState.reset()
|
||||
}
|
||||
)
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = flashState.isFlashing && installMethod is InstallMethod.HorizonKernel,
|
||||
enter = fadeIn() + expandVertically(),
|
||||
exit = fadeOut() + shrinkVertically()
|
||||
) {
|
||||
HorizonKernelFlashProgress(flashState)
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp)
|
||||
) {
|
||||
(lkmSelection as? LkmSelection.LkmUri)?.let {
|
||||
ElevatedCard(
|
||||
colors = getCardColors(MaterialTheme.colorScheme.surfaceVariant),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 12.dp)
|
||||
.clip(MaterialTheme.shapes.medium)
|
||||
.shadow(
|
||||
elevation = cardElevation,
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
spotColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(
|
||||
id = R.string.selected_lkm,
|
||||
it.uri.lastPathSegment ?: "(file)"
|
||||
),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
modifier = Modifier.padding(16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
(installMethod as? InstallMethod.HorizonKernel)?.let { method ->
|
||||
if (method.slot != null) {
|
||||
ElevatedCard(
|
||||
colors = getCardColors(MaterialTheme.colorScheme.surfaceVariant),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 12.dp)
|
||||
.clip(MaterialTheme.shapes.medium)
|
||||
.shadow(
|
||||
elevation = cardElevation,
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
spotColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(
|
||||
id = R.string.selected_slot,
|
||||
if (method.slot == "a") stringResource(id = R.string.slot_a)
|
||||
else stringResource(id = R.string.slot_b)
|
||||
),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
modifier = Modifier.padding(16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Button(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
enabled = installMethod != null && !flashState.isFlashing,
|
||||
onClick = onClickNext,
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
contentColor = MaterialTheme.colorScheme.onPrimary,
|
||||
disabledContainerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.6f),
|
||||
disabledContentColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f)
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
stringResource(id = R.string.install_next),
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RebootDialog(
|
||||
show: Boolean,
|
||||
onDismiss: () -> Unit,
|
||||
onConfirm: () -> Unit
|
||||
) {
|
||||
if (show) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text(stringResource(id = R.string.reboot_complete_title)) },
|
||||
text = { Text(stringResource(id = R.string.reboot_complete_msg)) },
|
||||
confirmButton = {
|
||||
TextButton(onClick = onConfirm) {
|
||||
Text(stringResource(id = R.string.yes))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismiss) {
|
||||
Text(stringResource(id = R.string.no))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
sealed class InstallMethod {
|
||||
data class SelectFile(
|
||||
val uri: Uri? = null,
|
||||
@StringRes override val label: Int = R.string.select_file,
|
||||
override val summary: String?
|
||||
) : InstallMethod()
|
||||
|
||||
data object DirectInstall : InstallMethod() {
|
||||
override val label: Int
|
||||
get() = R.string.direct_install
|
||||
}
|
||||
|
||||
data object DirectInstallToInactiveSlot : InstallMethod() {
|
||||
override val label: Int
|
||||
get() = R.string.install_inactive_slot
|
||||
}
|
||||
|
||||
data class HorizonKernel(
|
||||
val uri: Uri? = null,
|
||||
val slot: String? = null,
|
||||
@StringRes override val label: Int = R.string.horizon_kernel,
|
||||
override val summary: String? = null
|
||||
) : InstallMethod()
|
||||
|
||||
abstract val label: Int
|
||||
open val summary: String? = null
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SelectInstallMethod(
|
||||
isGKI: Boolean = false,
|
||||
isAbDevice: Boolean = false,
|
||||
onSelected: (InstallMethod) -> Unit = {}
|
||||
) {
|
||||
val rootAvailable = rootAvailable()
|
||||
val isAbDevice = isAbDevice()
|
||||
val horizonKernelSummary = stringResource(R.string.horizon_kernel_summary)
|
||||
val selectFileTip = stringResource(
|
||||
id = R.string.select_file_tip,
|
||||
if (isInitBoot()) "init_boot" else "boot"
|
||||
)
|
||||
|
||||
val radioOptions = mutableListOf<InstallMethod>(
|
||||
InstallMethod.SelectFile(summary = selectFileTip)
|
||||
)
|
||||
|
||||
if (rootAvailable) {
|
||||
radioOptions.add(InstallMethod.DirectInstall)
|
||||
if (isAbDevice) {
|
||||
radioOptions.add(InstallMethod.DirectInstallToInactiveSlot)
|
||||
}
|
||||
radioOptions.add(InstallMethod.HorizonKernel(summary = horizonKernelSummary))
|
||||
}
|
||||
|
||||
var selectedOption by remember { mutableStateOf<InstallMethod?>(null) }
|
||||
var currentSelectingMethod by remember { mutableStateOf<InstallMethod?>(null) }
|
||||
|
||||
val selectImageLauncher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.StartActivityForResult()
|
||||
) {
|
||||
if (it.resultCode == Activity.RESULT_OK) {
|
||||
it.data?.data?.let { uri ->
|
||||
val option = when (currentSelectingMethod) {
|
||||
is InstallMethod.SelectFile -> InstallMethod.SelectFile(
|
||||
uri,
|
||||
summary = selectFileTip
|
||||
)
|
||||
|
||||
is InstallMethod.HorizonKernel -> InstallMethod.HorizonKernel(
|
||||
uri,
|
||||
summary = horizonKernelSummary
|
||||
)
|
||||
|
||||
else -> null
|
||||
}
|
||||
option?.let {
|
||||
selectedOption = it
|
||||
onSelected(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val confirmDialog = rememberConfirmDialog(
|
||||
onConfirm = {
|
||||
selectedOption = InstallMethod.DirectInstallToInactiveSlot
|
||||
onSelected(InstallMethod.DirectInstallToInactiveSlot)
|
||||
},
|
||||
onDismiss = null
|
||||
)
|
||||
|
||||
val dialogTitle = stringResource(id = android.R.string.dialog_alert_title)
|
||||
val dialogContent = stringResource(id = R.string.install_inactive_slot_warning)
|
||||
|
||||
val onClick = { option: InstallMethod ->
|
||||
currentSelectingMethod = option
|
||||
when (option) {
|
||||
is InstallMethod.SelectFile, is InstallMethod.HorizonKernel -> {
|
||||
selectImageLauncher.launch(Intent(Intent.ACTION_GET_CONTENT).apply {
|
||||
type = "application/*"
|
||||
putExtra(
|
||||
Intent.EXTRA_MIME_TYPES,
|
||||
arrayOf("application/octet-stream", "application/zip")
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
is InstallMethod.DirectInstall -> {
|
||||
selectedOption = option
|
||||
onSelected(option)
|
||||
}
|
||||
|
||||
is InstallMethod.DirectInstallToInactiveSlot -> {
|
||||
confirmDialog.showConfirm(dialogTitle, dialogContent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var LKMExpanded by remember { mutableStateOf(false) }
|
||||
var GKIExpanded by remember { mutableStateOf(false) }
|
||||
|
||||
Column(
|
||||
modifier = Modifier.padding(horizontal = 16.dp)
|
||||
) {
|
||||
// LKM 安装/修补
|
||||
if (isGKI) {
|
||||
ElevatedCard(
|
||||
colors = getCardColors(MaterialTheme.colorScheme.surfaceVariant),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 12.dp)
|
||||
.clip(MaterialTheme.shapes.large)
|
||||
.shadow(
|
||||
elevation = cardElevation,
|
||||
shape = MaterialTheme.shapes.large,
|
||||
spotColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
|
||||
)
|
||||
) {
|
||||
ListItem(
|
||||
leadingContent = {
|
||||
Icon(
|
||||
Icons.Filled.AutoFixHigh,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
},
|
||||
headlineContent = {
|
||||
Text(
|
||||
stringResource(R.string.Lkm_install_methods),
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
},
|
||||
modifier = Modifier.clickable {
|
||||
LKMExpanded = !LKMExpanded
|
||||
}
|
||||
)
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = LKMExpanded,
|
||||
enter = fadeIn() + expandVertically(),
|
||||
exit = shrinkVertically() + fadeOut()
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(
|
||||
start = 16.dp,
|
||||
end = 16.dp,
|
||||
bottom = 16.dp
|
||||
)
|
||||
) {
|
||||
radioOptions.take(3).forEach { option ->
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
Surface(
|
||||
color = if (option.javaClass == selectedOption?.javaClass)
|
||||
MaterialTheme.colorScheme.secondaryContainer.copy(alpha = cardAlpha)
|
||||
else
|
||||
MaterialTheme.colorScheme.surfaceContainerHighest.copy(alpha = cardAlpha),
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 4.dp)
|
||||
.clip(MaterialTheme.shapes.medium)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.toggleable(
|
||||
value = option.javaClass == selectedOption?.javaClass,
|
||||
onValueChange = { onClick(option) },
|
||||
role = Role.RadioButton,
|
||||
indication = LocalIndication.current,
|
||||
interactionSource = interactionSource
|
||||
)
|
||||
.padding(vertical = 8.dp, horizontal = 12.dp)
|
||||
) {
|
||||
RadioButton(
|
||||
selected = option.javaClass == selectedOption?.javaClass,
|
||||
onClick = null,
|
||||
interactionSource = interactionSource,
|
||||
colors = RadioButtonDefaults.colors(
|
||||
selectedColor = MaterialTheme.colorScheme.primary,
|
||||
unselectedColor = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(start = 10.dp)
|
||||
.weight(1f)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = option.label),
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
)
|
||||
option.summary?.let {
|
||||
Text(
|
||||
text = it,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// anykernel3 刷写
|
||||
if (rootAvailable) {
|
||||
ElevatedCard(
|
||||
colors = getCardColors(MaterialTheme.colorScheme.surfaceVariant),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 12.dp)
|
||||
.clip(MaterialTheme.shapes.large)
|
||||
.shadow(
|
||||
elevation = cardElevation,
|
||||
shape = MaterialTheme.shapes.large,
|
||||
spotColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
|
||||
)
|
||||
) {
|
||||
ListItem(
|
||||
leadingContent = {
|
||||
Icon(
|
||||
Icons.Filled.FileUpload,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
},
|
||||
headlineContent = {
|
||||
Text(
|
||||
stringResource(R.string.GKI_install_methods),
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
},
|
||||
modifier = Modifier.clickable {
|
||||
GKIExpanded = !GKIExpanded
|
||||
}
|
||||
)
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = GKIExpanded,
|
||||
enter = fadeIn() + expandVertically(),
|
||||
exit = shrinkVertically() + fadeOut()
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(
|
||||
start = 16.dp,
|
||||
end = 16.dp,
|
||||
bottom = 16.dp
|
||||
)
|
||||
) {
|
||||
radioOptions.filterIsInstance<InstallMethod.HorizonKernel>().forEach { option ->
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
Surface(
|
||||
color = if (option.javaClass == selectedOption?.javaClass)
|
||||
MaterialTheme.colorScheme.secondaryContainer.copy(alpha = cardAlpha)
|
||||
else
|
||||
MaterialTheme.colorScheme.surfaceContainerHighest.copy(alpha = cardAlpha),
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 4.dp)
|
||||
.clip(MaterialTheme.shapes.medium)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.toggleable(
|
||||
value = option.javaClass == selectedOption?.javaClass,
|
||||
onValueChange = { onClick(option) },
|
||||
role = Role.RadioButton,
|
||||
indication = LocalIndication.current,
|
||||
interactionSource = interactionSource
|
||||
)
|
||||
.padding(vertical = 8.dp, horizontal = 12.dp)
|
||||
) {
|
||||
RadioButton(
|
||||
selected = option.javaClass == selectedOption?.javaClass,
|
||||
onClick = null,
|
||||
interactionSource = interactionSource,
|
||||
colors = RadioButtonDefaults.colors(
|
||||
selectedColor = MaterialTheme.colorScheme.primary,
|
||||
unselectedColor = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(start = 10.dp)
|
||||
.weight(1f)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = option.label),
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
)
|
||||
option.summary?.let {
|
||||
Text(
|
||||
text = it,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun rememberSelectKmiDialog(onSelected: (String?) -> Unit): DialogHandle {
|
||||
return rememberCustomDialog { dismiss ->
|
||||
val supportedKmi by produceState(initialValue = emptyList<String>()) {
|
||||
value = getSupportedKmis()
|
||||
}
|
||||
val options = supportedKmi.map { value ->
|
||||
ListOption(
|
||||
titleText = value
|
||||
)
|
||||
}
|
||||
|
||||
var selection by remember { mutableStateOf<String?>(null) }
|
||||
val backgroundColor = MaterialTheme.colorScheme.surfaceContainerHighest
|
||||
|
||||
MaterialTheme(
|
||||
colorScheme = MaterialTheme.colorScheme.copy(
|
||||
surface = backgroundColor
|
||||
)
|
||||
) {
|
||||
ListDialog(state = rememberUseCaseState(visible = true, onFinishedRequest = {
|
||||
onSelected(selection)
|
||||
}, onCloseRequest = {
|
||||
dismiss()
|
||||
}), header = Header.Default(
|
||||
title = stringResource(R.string.select_kmi),
|
||||
), selection = ListSelection.Single(
|
||||
showRadioButtons = true,
|
||||
options = options,
|
||||
) { _, option ->
|
||||
selection = option.titleText
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun TopBar(
|
||||
onBack: () -> Unit = {},
|
||||
onLkmUpload: () -> Unit = {},
|
||||
scrollBehavior: TopAppBarScrollBehavior? = null
|
||||
) {
|
||||
val cardColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
val cardAlpha = cardAlpha
|
||||
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
stringResource(R.string.install),
|
||||
style = MaterialTheme.typography.titleLarge
|
||||
)
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = cardColor.copy(alpha = cardAlpha),
|
||||
scrolledContainerColor = cardColor.copy(alpha = cardAlpha)
|
||||
),
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(
|
||||
Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = stringResource(R.string.back)
|
||||
)
|
||||
}
|
||||
},
|
||||
windowInsets = WindowInsets.safeDrawing.only(
|
||||
WindowInsetsSides.Top + WindowInsetsSides.Horizontal
|
||||
),
|
||||
scrollBehavior = scrollBehavior
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun SelectInstallPreview() {
|
||||
InstallScreen(EmptyDestinationsNavigator)
|
||||
}
|
||||
783
manager/app/src/main/java/com/sukisu/ultra/ui/screen/Kpm.kt
Normal file
783
manager/app/src/main/java/com/sukisu/ultra/ui/screen/Kpm.kt
Normal file
@@ -0,0 +1,783 @@
|
||||
package com.sukisu.ultra.ui.screen
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.util.Log
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import com.sukisu.ultra.ui.component.*
|
||||
import com.sukisu.ultra.ui.theme.*
|
||||
import com.sukisu.ultra.ui.viewmodel.KpmViewModel
|
||||
import com.sukisu.ultra.ui.util.*
|
||||
import java.io.File
|
||||
import androidx.core.content.edit
|
||||
import com.sukisu.ultra.R
|
||||
import java.io.BufferedReader
|
||||
import java.io.FileInputStream
|
||||
import java.io.InputStreamReader
|
||||
import java.net.*
|
||||
import android.app.Activity
|
||||
import com.sukisu.ultra.ui.theme.CardConfig.cardElevation
|
||||
|
||||
/**
|
||||
* KPM 管理界面
|
||||
* 以下内核模块功能由KernelPatch开发,经过修改后加入SukiSU Ultra的内核模块功能
|
||||
* 开发者:ShirkNeko, Liaokong
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Destination<RootGraph>
|
||||
@Composable
|
||||
fun KpmScreen(
|
||||
navigator: DestinationsNavigator,
|
||||
viewModel: KpmViewModel = viewModel()
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
val snackBarHost = remember { SnackbarHostState() }
|
||||
val confirmDialog = rememberConfirmDialog()
|
||||
|
||||
val moduleConfirmContentMap = viewModel.moduleList.associate { module ->
|
||||
val moduleFileName = module.id
|
||||
module.id to stringResource(R.string.confirm_uninstall_content, moduleFileName)
|
||||
}
|
||||
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||
|
||||
val kpmInstallSuccess = stringResource(R.string.kpm_install_success)
|
||||
val kpmInstallFailed = stringResource(R.string.kpm_install_failed)
|
||||
val cancel = stringResource(R.string.cancel)
|
||||
val uninstall = stringResource(R.string.uninstall)
|
||||
val failedToCheckModuleFile = stringResource(R.string.snackbar_failed_to_check_module_file)
|
||||
val kpmUninstallSuccess = stringResource(R.string.kpm_uninstall_success)
|
||||
val kpmUninstallFailed = stringResource(R.string.kpm_uninstall_failed)
|
||||
val kpmInstallMode = stringResource(R.string.kpm_install_mode)
|
||||
val kpmInstallModeLoad = stringResource(R.string.kpm_install_mode_load)
|
||||
val kpmInstallModeEmbed = stringResource(R.string.kpm_install_mode_embed)
|
||||
val invalidFileTypeMessage = stringResource(R.string.invalid_file_type)
|
||||
val confirmTitle = stringResource(R.string.confirm_uninstall_title_with_filename)
|
||||
|
||||
var tempFileForInstall by remember { mutableStateOf<File?>(null) }
|
||||
val installModeDialog = rememberCustomDialog { dismiss ->
|
||||
var moduleName by remember { mutableStateOf<String?>(null) }
|
||||
|
||||
LaunchedEffect(tempFileForInstall) {
|
||||
tempFileForInstall?.let { tempFile ->
|
||||
try {
|
||||
val command = arrayOf("su", "-c", "strings ${tempFile.absolutePath} | grep 'name='")
|
||||
val process = Runtime.getRuntime().exec(command)
|
||||
val inputStream = process.inputStream
|
||||
val reader = BufferedReader(InputStreamReader(inputStream))
|
||||
var line: String?
|
||||
while (reader.readLine().also { line = it } != null) {
|
||||
if (line!!.startsWith("name=")) {
|
||||
moduleName = line.substringAfter("name=").trim()
|
||||
break
|
||||
}
|
||||
}
|
||||
process.waitFor()
|
||||
} catch (e: Exception) {
|
||||
Log.e("KsuCli", "Failed to get module name: ${e.message}", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = {
|
||||
dismiss()
|
||||
tempFileForInstall?.delete()
|
||||
tempFileForInstall = null
|
||||
},
|
||||
title = {
|
||||
Text(
|
||||
text = kpmInstallMode,
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
},
|
||||
text = {
|
||||
Column {
|
||||
moduleName?.let {
|
||||
Text(
|
||||
text = stringResource(R.string.kpm_install_mode_description, it),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Button(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
dismiss()
|
||||
tempFileForInstall?.let { tempFile ->
|
||||
handleModuleInstall(
|
||||
tempFile = tempFile,
|
||||
isEmbed = false,
|
||||
viewModel = viewModel,
|
||||
snackBarHost = snackBarHost,
|
||||
kpmInstallSuccess = kpmInstallSuccess,
|
||||
kpmInstallFailed = kpmInstallFailed
|
||||
)
|
||||
}
|
||||
tempFileForInstall = null
|
||||
}
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Download,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(18.dp).padding(end = 4.dp)
|
||||
)
|
||||
Text(kpmInstallModeLoad)
|
||||
}
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
dismiss()
|
||||
tempFileForInstall?.let { tempFile ->
|
||||
handleModuleInstall(
|
||||
tempFile = tempFile,
|
||||
isEmbed = true,
|
||||
viewModel = viewModel,
|
||||
snackBarHost = snackBarHost,
|
||||
kpmInstallSuccess = kpmInstallSuccess,
|
||||
kpmInstallFailed = kpmInstallFailed
|
||||
)
|
||||
}
|
||||
tempFileForInstall = null
|
||||
}
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.secondary
|
||||
)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Inventory,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(18.dp).padding(end = 4.dp)
|
||||
)
|
||||
Text(kpmInstallModeEmbed)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
},
|
||||
dismissButton = {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
TextButton(
|
||||
onClick = {
|
||||
dismiss()
|
||||
tempFileForInstall?.delete()
|
||||
tempFileForInstall = null
|
||||
}
|
||||
) {
|
||||
Text(cancel)
|
||||
}
|
||||
}
|
||||
},
|
||||
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
|
||||
shape = MaterialTheme.shapes.extraLarge
|
||||
)
|
||||
}
|
||||
|
||||
val selectPatchLauncher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.StartActivityForResult()
|
||||
) { result ->
|
||||
if (result.resultCode != Activity.RESULT_OK) return@rememberLauncherForActivityResult
|
||||
|
||||
val uri = result.data?.data ?: return@rememberLauncherForActivityResult
|
||||
|
||||
scope.launch {
|
||||
val fileName = uri.lastPathSegment ?: "unknown.kpm"
|
||||
val encodedFileName = URLEncoder.encode(fileName, "UTF-8")
|
||||
val tempFile = File(context.cacheDir, encodedFileName)
|
||||
|
||||
context.contentResolver.openInputStream(uri)?.use { input ->
|
||||
tempFile.outputStream().use { output ->
|
||||
input.copyTo(output)
|
||||
}
|
||||
}
|
||||
|
||||
val mimeType = context.contentResolver.getType(uri)
|
||||
val isCorrectMimeType = mimeType == null || mimeType.contains("application/octet-stream")
|
||||
|
||||
if (!isCorrectMimeType) {
|
||||
var shouldShowSnackbar = true
|
||||
try {
|
||||
val matchCount = checkStringsCommand(tempFile)
|
||||
val isElf = isElfFile(tempFile)
|
||||
|
||||
if (matchCount >= 1 || isElf) {
|
||||
shouldShowSnackbar = false
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("KsuCli", "Failed to execute checks: ${e.message}", e)
|
||||
}
|
||||
if (shouldShowSnackbar) {
|
||||
snackBarHost.showSnackbar(
|
||||
message = invalidFileTypeMessage,
|
||||
duration = SnackbarDuration.Short
|
||||
)
|
||||
}
|
||||
tempFile.delete()
|
||||
return@launch
|
||||
}
|
||||
tempFileForInstall = tempFile
|
||||
installModeDialog.show()
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
while(true) {
|
||||
viewModel.fetchModuleList()
|
||||
delay(5000)
|
||||
}
|
||||
}
|
||||
|
||||
val sharedPreferences = context.getSharedPreferences("app_preferences", Context.MODE_PRIVATE)
|
||||
var isNoticeClosed by remember { mutableStateOf(sharedPreferences.getBoolean("is_notice_closed", false)) }
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
SearchAppBar(
|
||||
title = { Text(stringResource(R.string.kpm_title)) },
|
||||
searchText = viewModel.search,
|
||||
onSearchTextChange = { viewModel.search = it },
|
||||
onClearClick = { viewModel.search = "" },
|
||||
scrollBehavior = scrollBehavior,
|
||||
dropdownContent = {
|
||||
IconButton(
|
||||
onClick = { viewModel.fetchModuleList() }
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Refresh,
|
||||
contentDescription = stringResource(R.string.refresh),
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
floatingActionButton = {
|
||||
ExtendedFloatingActionButton(
|
||||
onClick = {
|
||||
selectPatchLauncher.launch(
|
||||
Intent(Intent.ACTION_GET_CONTENT).apply {
|
||||
type = "application/octet-stream"
|
||||
}
|
||||
)
|
||||
},
|
||||
icon = {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Add,
|
||||
contentDescription = stringResource(R.string.kpm_install),
|
||||
)
|
||||
},
|
||||
text = {
|
||||
Text(
|
||||
text = stringResource(R.string.kpm_install),
|
||||
color = MaterialTheme.colorScheme.onPrimaryContainer
|
||||
)
|
||||
},
|
||||
contentColor = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||
expanded = true,
|
||||
)
|
||||
},
|
||||
snackbarHost = { SnackbarHost(snackBarHost) }
|
||||
) { padding ->
|
||||
Column(modifier = Modifier.padding(padding)) {
|
||||
if (!isNoticeClosed) {
|
||||
Card(
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.secondaryContainer
|
||||
),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp)
|
||||
.clip(MaterialTheme.shapes.medium)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Info,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.padding(end = 16.dp)
|
||||
.size(24.dp)
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.kernel_module_notice),
|
||||
modifier = Modifier.weight(1f),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSecondaryContainer
|
||||
)
|
||||
|
||||
IconButton(
|
||||
onClick = {
|
||||
isNoticeClosed = true
|
||||
sharedPreferences.edit { putBoolean("is_notice_closed", true) }
|
||||
},
|
||||
modifier = Modifier.size(24.dp),
|
||||
colors = IconButtonDefaults.iconButtonColors(
|
||||
contentColor = MaterialTheme.colorScheme.onSecondaryContainer
|
||||
)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Close,
|
||||
contentDescription = stringResource(R.string.close_notice)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (viewModel.moduleList.isEmpty()) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Code,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.6f),
|
||||
modifier = Modifier
|
||||
.size(96.dp)
|
||||
.padding(bottom = 16.dp)
|
||||
)
|
||||
Text(
|
||||
stringResource(R.string.kpm_empty),
|
||||
textAlign = TextAlign.Center,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
items(viewModel.moduleList) { module ->
|
||||
KpmModuleItem(
|
||||
module = module,
|
||||
onUninstall = {
|
||||
scope.launch {
|
||||
val confirmContent = moduleConfirmContentMap[module.id] ?: ""
|
||||
handleModuleUninstall(
|
||||
module = module,
|
||||
viewModel = viewModel,
|
||||
snackBarHost = snackBarHost,
|
||||
kpmUninstallSuccess = kpmUninstallSuccess,
|
||||
kpmUninstallFailed = kpmUninstallFailed,
|
||||
failedToCheckModuleFile = failedToCheckModuleFile,
|
||||
uninstall = uninstall,
|
||||
cancel = cancel,
|
||||
confirmDialog = confirmDialog,
|
||||
confirmTitle = confirmTitle,
|
||||
confirmContent = confirmContent
|
||||
)
|
||||
}
|
||||
},
|
||||
onControl = {
|
||||
viewModel.loadModuleDetail(module.id)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun handleModuleInstall(
|
||||
tempFile: File,
|
||||
isEmbed: Boolean,
|
||||
viewModel: KpmViewModel,
|
||||
snackBarHost: SnackbarHostState,
|
||||
kpmInstallSuccess: String,
|
||||
kpmInstallFailed: String
|
||||
) {
|
||||
var moduleId: String? = null
|
||||
try {
|
||||
val command = arrayOf("su", "-c", "strings ${tempFile.absolutePath} | grep 'name='")
|
||||
val process = Runtime.getRuntime().exec(command)
|
||||
val inputStream = process.inputStream
|
||||
val reader = BufferedReader(InputStreamReader(inputStream))
|
||||
var line: String?
|
||||
while (reader.readLine().also { line = it } != null) {
|
||||
if (line!!.startsWith("name=")) {
|
||||
moduleId = line.substringAfter("name=").trim()
|
||||
break
|
||||
}
|
||||
}
|
||||
process.waitFor()
|
||||
} catch (e: Exception) {
|
||||
Log.e("KsuCli", "Failed to get module ID from strings command: ${e.message}", e)
|
||||
}
|
||||
|
||||
if (moduleId == null || moduleId.isEmpty()) {
|
||||
Log.e("KsuCli", "Failed to extract module ID from file: ${tempFile.name}")
|
||||
snackBarHost.showSnackbar(
|
||||
message = kpmInstallFailed,
|
||||
duration = SnackbarDuration.Short
|
||||
)
|
||||
tempFile.delete()
|
||||
return
|
||||
}
|
||||
|
||||
val targetPath = "/data/adb/kpm/$moduleId.kpm"
|
||||
|
||||
try {
|
||||
if (isEmbed) {
|
||||
Runtime.getRuntime().exec(arrayOf("su", "-c", "mkdir -p /data/adb/kpm")).waitFor()
|
||||
Runtime.getRuntime().exec(arrayOf("su", "-c", "cp ${tempFile.absolutePath} $targetPath")).waitFor()
|
||||
}
|
||||
|
||||
val loadResult = loadKpmModule(tempFile.absolutePath)
|
||||
if (loadResult.startsWith("Error")) {
|
||||
Log.e("KsuCli", "Failed to load KPM module: $loadResult")
|
||||
snackBarHost.showSnackbar(
|
||||
message = kpmInstallFailed,
|
||||
duration = SnackbarDuration.Short
|
||||
)
|
||||
} else {
|
||||
viewModel.fetchModuleList()
|
||||
snackBarHost.showSnackbar(
|
||||
message = kpmInstallSuccess,
|
||||
duration = SnackbarDuration.Short
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("KsuCli", "Failed to load KPM module: ${e.message}", e)
|
||||
snackBarHost.showSnackbar(
|
||||
message = kpmInstallFailed,
|
||||
duration = SnackbarDuration.Short
|
||||
)
|
||||
}
|
||||
tempFile.delete()
|
||||
}
|
||||
|
||||
private suspend fun handleModuleUninstall(
|
||||
module: KpmViewModel.ModuleInfo,
|
||||
viewModel: KpmViewModel,
|
||||
snackBarHost: SnackbarHostState,
|
||||
kpmUninstallSuccess: String,
|
||||
kpmUninstallFailed: String,
|
||||
failedToCheckModuleFile: String,
|
||||
uninstall: String,
|
||||
cancel: String,
|
||||
confirmTitle : String,
|
||||
confirmContent : String,
|
||||
confirmDialog: ConfirmDialogHandle
|
||||
) {
|
||||
val moduleFileName = "${module.id}.kpm"
|
||||
val moduleFilePath = "/data/adb/kpm/$moduleFileName"
|
||||
|
||||
val fileExists = try {
|
||||
val result = Runtime.getRuntime().exec(arrayOf("su", "-c", "ls /data/adb/kpm/$moduleFileName")).waitFor() == 0
|
||||
result
|
||||
} catch (e: Exception) {
|
||||
Log.e("KsuCli", "Failed to check module file existence: ${e.message}", e)
|
||||
snackBarHost.showSnackbar(
|
||||
message = failedToCheckModuleFile,
|
||||
duration = SnackbarDuration.Short
|
||||
)
|
||||
false
|
||||
}
|
||||
val confirmResult = confirmDialog.awaitConfirm(
|
||||
title = confirmTitle,
|
||||
content = confirmContent,
|
||||
confirm = uninstall,
|
||||
dismiss = cancel
|
||||
)
|
||||
|
||||
if (confirmResult == ConfirmResult.Confirmed) {
|
||||
try {
|
||||
val unloadResult = unloadKpmModule(module.id)
|
||||
if (unloadResult.startsWith("Error")) {
|
||||
Log.e("KsuCli", "Failed to unload KPM module: $unloadResult")
|
||||
snackBarHost.showSnackbar(
|
||||
message = kpmUninstallFailed,
|
||||
duration = SnackbarDuration.Short
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (fileExists) {
|
||||
Runtime.getRuntime().exec(arrayOf("su", "-c", "rm $moduleFilePath")).waitFor()
|
||||
}
|
||||
|
||||
viewModel.fetchModuleList()
|
||||
snackBarHost.showSnackbar(
|
||||
message = kpmUninstallSuccess,
|
||||
duration = SnackbarDuration.Short
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.e("KsuCli", "Failed to unload KPM module: ${e.message}", e)
|
||||
snackBarHost.showSnackbar(
|
||||
message = kpmUninstallFailed,
|
||||
duration = SnackbarDuration.Short
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun KpmModuleItem(
|
||||
module: KpmViewModel.ModuleInfo,
|
||||
onUninstall: () -> Unit,
|
||||
onControl: () -> Unit
|
||||
) {
|
||||
val viewModel: KpmViewModel = viewModel()
|
||||
val scope = rememberCoroutineScope()
|
||||
val snackBarHost = remember { SnackbarHostState() }
|
||||
val successMessage = stringResource(R.string.kpm_control_success)
|
||||
val failureMessage = stringResource(R.string.kpm_control_failed)
|
||||
|
||||
if (viewModel.showInputDialog && viewModel.selectedModuleId == module.id) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { viewModel.hideInputDialog() },
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(R.string.kpm_control),
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
},
|
||||
text = {
|
||||
OutlinedTextField(
|
||||
value = viewModel.inputArgs,
|
||||
onValueChange = { viewModel.updateInputArgs(it) },
|
||||
label = {
|
||||
Text(
|
||||
text = stringResource(R.string.kpm_args),
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
},
|
||||
placeholder = {
|
||||
Text(
|
||||
text = module.args,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f)
|
||||
)
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
focusedBorderColor = MaterialTheme.colorScheme.primary,
|
||||
unfocusedBorderColor = MaterialTheme.colorScheme.outline
|
||||
)
|
||||
)
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
val result = viewModel.executeControl()
|
||||
val message = when (result) {
|
||||
0 -> successMessage
|
||||
else -> failureMessage
|
||||
}
|
||||
snackBarHost.showSnackbar(message)
|
||||
onControl()
|
||||
}
|
||||
}
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.confirm),
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { viewModel.hideInputDialog() }) {
|
||||
Text(
|
||||
text = stringResource(R.string.cancel),
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
},
|
||||
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
|
||||
shape = MaterialTheme.shapes.extraLarge
|
||||
)
|
||||
}
|
||||
|
||||
Card(
|
||||
colors = getCardColors(MaterialTheme.colorScheme.surfaceContainerHigh),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(MaterialTheme.shapes.large)
|
||||
.shadow(
|
||||
elevation = cardElevation,
|
||||
shape = MaterialTheme.shapes.large,
|
||||
spotColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
|
||||
)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(20.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = module.name,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
|
||||
Text(
|
||||
text = "${stringResource(R.string.kpm_version)}: ${module.version}",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
|
||||
Text(
|
||||
text = "${stringResource(R.string.kpm_author)}: ${module.author}",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
|
||||
Text(
|
||||
text = "${stringResource(R.string.kpm_args)}: ${module.args}",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
Text(
|
||||
text = module.description,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(20.dp))
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Button(
|
||||
onClick = { viewModel.showInputDialog(module.id) },
|
||||
enabled = module.hasAction,
|
||||
modifier = Modifier.weight(1f),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
disabledContainerColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Settings,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(stringResource(R.string.kpm_control))
|
||||
}
|
||||
|
||||
Button(
|
||||
onClick = onUninstall,
|
||||
modifier = Modifier.weight(1f),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.error
|
||||
)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Delete,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(stringResource(R.string.kpm_uninstall))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkStringsCommand(tempFile: File): Int {
|
||||
val command = arrayOf("su", "-c", "strings ${tempFile.absolutePath} | grep -E 'name=|version=|license=|author='")
|
||||
val process = Runtime.getRuntime().exec(command)
|
||||
val inputStream = process.inputStream
|
||||
val reader = BufferedReader(InputStreamReader(inputStream))
|
||||
var line: String?
|
||||
var matchCount = 0
|
||||
val keywords = listOf("name=", "version=", "license=", "author=")
|
||||
var nameExists = false
|
||||
|
||||
while (reader.readLine().also { line = it } != null) {
|
||||
if (!nameExists && line!!.startsWith("name=")) {
|
||||
nameExists = true
|
||||
matchCount++
|
||||
} else if (nameExists) {
|
||||
for (keyword in keywords) {
|
||||
if (line!!.startsWith(keyword)) {
|
||||
matchCount++
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
process.waitFor()
|
||||
|
||||
return if (nameExists) matchCount else 0
|
||||
}
|
||||
|
||||
private fun isElfFile(tempFile: File): Boolean {
|
||||
val elfMagic = byteArrayOf(0x7F, 'E'.code.toByte(), 'L'.code.toByte(), 'F'.code.toByte())
|
||||
val fileBytes = ByteArray(4)
|
||||
FileInputStream(tempFile).use { input ->
|
||||
input.read(fileBytes)
|
||||
}
|
||||
return fileBytes.contentEquals(elfMagic)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package zako.zako.zako.ui.screen
|
||||
package com.sukisu.ultra.ui.screen
|
||||
|
||||
import android.app.Activity.*
|
||||
import android.content.Context
|
||||
@@ -9,22 +9,7 @@ import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.only
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.selection.toggleable
|
||||
@@ -32,41 +17,14 @@ import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.outlined.*
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material.icons.outlined.*
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ElevatedCard
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExtendedFloatingActionButton
|
||||
import androidx.compose.material3.FilledTonalButton
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SnackbarDuration
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.SnackbarResult
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||
import androidx.compose.material3.rememberTopAppBarState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.rotate
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.*
|
||||
import androidx.compose.ui.res.stringResource
|
||||
@@ -88,32 +46,31 @@ import com.ramcosta.composedestinations.navigation.EmptyDestinationsNavigator
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import zako.zako.zako.Natives
|
||||
import zako.zako.zako.ui.component.ConfirmResult
|
||||
import zako.zako.zako.ui.component.SearchAppBar
|
||||
import zako.zako.zako.ui.component.rememberConfirmDialog
|
||||
import zako.zako.zako.ui.component.rememberLoadingDialog
|
||||
import zako.zako.zako.ui.util.DownloadListener
|
||||
import zako.zako.zako.ui.util.*
|
||||
import zako.zako.zako.ui.util.download
|
||||
import zako.zako.zako.ui.util.hasMagisk
|
||||
import zako.zako.zako.ui.util.reboot
|
||||
import zako.zako.zako.ui.util.restoreModule
|
||||
import zako.zako.zako.ui.util.toggleModule
|
||||
import zako.zako.zako.ui.util.uninstallModule
|
||||
import zako.zako.zako.ui.webui.WebUIActivity
|
||||
import com.sukisu.ultra.Natives
|
||||
import com.sukisu.ultra.ui.component.ConfirmResult
|
||||
import com.sukisu.ultra.ui.component.SearchAppBar
|
||||
import com.sukisu.ultra.ui.component.rememberConfirmDialog
|
||||
import com.sukisu.ultra.ui.component.rememberLoadingDialog
|
||||
import com.sukisu.ultra.ui.util.DownloadListener
|
||||
import com.sukisu.ultra.ui.util.*
|
||||
import com.sukisu.ultra.ui.util.download
|
||||
import com.sukisu.ultra.ui.util.hasMagisk
|
||||
import com.sukisu.ultra.ui.util.reboot
|
||||
import com.sukisu.ultra.ui.util.restoreModule
|
||||
import com.sukisu.ultra.ui.util.toggleModule
|
||||
import com.sukisu.ultra.ui.util.uninstallModule
|
||||
import com.sukisu.ultra.ui.webui.WebUIActivity
|
||||
import okhttp3.OkHttpClient
|
||||
import zako.zako.zako.ui.util.ModuleModify
|
||||
import zako.zako.zako.ui.theme.getCardColors
|
||||
import zako.zako.zako.ui.theme.getCardElevation
|
||||
import zako.zako.zako.ui.viewmodel.ModuleViewModel
|
||||
import com.sukisu.ultra.ui.util.ModuleModify
|
||||
import com.sukisu.ultra.ui.theme.getCardColors
|
||||
import com.sukisu.ultra.ui.viewmodel.ModuleViewModel
|
||||
import java.io.BufferedReader
|
||||
import java.io.InputStreamReader
|
||||
import java.util.zip.ZipInputStream
|
||||
import androidx.core.content.edit
|
||||
import androidx.core.net.toUri
|
||||
import zako.zako.zako.ui.theme.ThemeConfig
|
||||
import zako.zako.zako.R
|
||||
import com.sukisu.ultra.R
|
||||
import com.sukisu.ultra.ui.theme.CardConfig.cardElevation
|
||||
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@@ -239,7 +196,7 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
|
||||
val backupLauncher = ModuleModify.rememberModuleBackupLauncher(context, snackBarHost)
|
||||
val restoreLauncher = ModuleModify.rememberModuleRestoreLauncher(context, snackBarHost)
|
||||
|
||||
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||
val prefs = context.getSharedPreferences("settings", MODE_PRIVATE)
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
if (viewModel.moduleList.isEmpty() || viewModel.isNeedRefresh) {
|
||||
@@ -275,7 +232,7 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.MoreVert,
|
||||
contentDescription = stringResource(id = R.string.settings)
|
||||
contentDescription = stringResource(id = R.string.settings),
|
||||
)
|
||||
|
||||
DropdownMenu(
|
||||
@@ -284,7 +241,16 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.module_sort_action_first)) },
|
||||
trailingIcon = { Checkbox(viewModel.sortActionFirst, null) },
|
||||
trailingIcon = {
|
||||
Checkbox(
|
||||
checked = viewModel.sortActionFirst,
|
||||
onCheckedChange = null,
|
||||
colors = CheckboxDefaults.colors(
|
||||
checkedColor = MaterialTheme.colorScheme.primary,
|
||||
uncheckedColor = MaterialTheme.colorScheme.outline
|
||||
)
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
viewModel.sortActionFirst = !viewModel.sortActionFirst
|
||||
prefs.edit {
|
||||
@@ -300,7 +266,16 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.module_sort_enabled_first)) },
|
||||
trailingIcon = { Checkbox(viewModel.sortEnabledFirst, null) },
|
||||
trailingIcon = {
|
||||
Checkbox(
|
||||
checked = viewModel.sortEnabledFirst,
|
||||
onCheckedChange = null,
|
||||
colors = CheckboxDefaults.colors(
|
||||
checkedColor = MaterialTheme.colorScheme.primary,
|
||||
uncheckedColor = MaterialTheme.colorScheme.outline
|
||||
)
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
viewModel.sortEnabledFirst = !viewModel.sortEnabledFirst
|
||||
prefs.edit {
|
||||
@@ -311,12 +286,13 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
|
||||
}
|
||||
}
|
||||
)
|
||||
HorizontalDivider(thickness = Dp.Hairline, modifier = Modifier.padding(vertical = 4.dp))
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.backup_modules)) },
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Download,
|
||||
contentDescription = stringResource(R.string.backup)
|
||||
contentDescription = stringResource(R.string.backup),
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
@@ -329,7 +305,7 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Refresh,
|
||||
contentDescription = stringResource(R.string.restore)
|
||||
contentDescription = stringResource(R.string.restore),
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
@@ -346,11 +322,6 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
|
||||
floatingActionButton = {
|
||||
if (!hideInstallButton) {
|
||||
val moduleInstall = stringResource(id = R.string.module_install)
|
||||
val cardColor = if (!ThemeConfig.useDynamicColor) {
|
||||
ThemeConfig.currentTheme.ButtonContrast
|
||||
} else {
|
||||
MaterialTheme.colorScheme.secondaryContainer
|
||||
}
|
||||
ExtendedFloatingActionButton(
|
||||
onClick = {
|
||||
selectZipLauncher.launch(
|
||||
@@ -363,16 +334,17 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
|
||||
icon = {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Add,
|
||||
contentDescription = moduleInstall
|
||||
contentDescription = moduleInstall,
|
||||
)
|
||||
},
|
||||
text = {
|
||||
Text(
|
||||
text = moduleInstall
|
||||
text = moduleInstall,
|
||||
color = MaterialTheme.colorScheme.onPrimaryContainer
|
||||
)
|
||||
},
|
||||
containerColor = cardColor.copy(alpha = 1f),
|
||||
contentColor = MaterialTheme.colorScheme.onSecondaryContainer
|
||||
contentColor = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||
expanded = true,
|
||||
)
|
||||
}
|
||||
},
|
||||
@@ -389,12 +361,27 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
|
||||
.padding(24.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Warning,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.error,
|
||||
modifier = Modifier
|
||||
.size(64.dp)
|
||||
.padding(bottom = 16.dp)
|
||||
)
|
||||
Text(
|
||||
stringResource(R.string.module_magisk_conflict),
|
||||
textAlign = TextAlign.Center,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
ModuleList(
|
||||
navigator = navigator,
|
||||
@@ -591,10 +578,25 @@ private fun ModuleList(
|
||||
modifier = Modifier.fillParentMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
stringResource(R.string.module_empty),
|
||||
textAlign = TextAlign.Center
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Extension,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.6f),
|
||||
modifier = Modifier
|
||||
.size(96.dp)
|
||||
.padding(bottom = 16.dp)
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.module_empty),
|
||||
textAlign = TextAlign.Center,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -677,8 +679,16 @@ fun ModuleItem(
|
||||
onClick: (ModuleViewModel.ModuleInfo) -> Unit
|
||||
) {
|
||||
ElevatedCard(
|
||||
colors = getCardColors(MaterialTheme.colorScheme.secondaryContainer),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = getCardElevation())
|
||||
colors = getCardColors(MaterialTheme.colorScheme.surfaceContainerHigh),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(MaterialTheme.shapes.large)
|
||||
.shadow(
|
||||
elevation = cardElevation,
|
||||
shape = MaterialTheme.shapes.large,
|
||||
spotColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
|
||||
)
|
||||
) {
|
||||
val textDecoration = if (!module.remove) null else TextDecoration.LineThrough
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
@@ -706,6 +716,7 @@ fun ModuleItem(
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
val moduleVersion = stringResource(id = R.string.module_version)
|
||||
val moduleAuthor = stringResource(id = R.string.module_author)
|
||||
@@ -720,6 +731,7 @@ fun ModuleItem(
|
||||
lineHeight = MaterialTheme.typography.bodySmall.lineHeight,
|
||||
fontFamily = MaterialTheme.typography.titleMedium.fontFamily,
|
||||
textDecoration = textDecoration,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
|
||||
Text(
|
||||
@@ -727,7 +739,8 @@ fun ModuleItem(
|
||||
fontSize = MaterialTheme.typography.bodySmall.fontSize,
|
||||
lineHeight = MaterialTheme.typography.bodySmall.lineHeight,
|
||||
fontFamily = MaterialTheme.typography.bodySmall.fontFamily,
|
||||
textDecoration = textDecoration
|
||||
textDecoration = textDecoration,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
|
||||
Text(
|
||||
@@ -735,7 +748,8 @@ fun ModuleItem(
|
||||
fontSize = MaterialTheme.typography.bodySmall.fontSize,
|
||||
lineHeight = MaterialTheme.typography.bodySmall.lineHeight,
|
||||
fontFamily = MaterialTheme.typography.bodySmall.fontFamily,
|
||||
textDecoration = textDecoration
|
||||
textDecoration = textDecoration,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
|
||||
@@ -749,7 +763,15 @@ fun ModuleItem(
|
||||
enabled = !module.update,
|
||||
checked = module.enabled,
|
||||
onCheckedChange = onCheckChanged,
|
||||
interactionSource = if (!module.hasWebUi) interactionSource else null
|
||||
interactionSource = if (!module.hasWebUi) interactionSource else null,
|
||||
colors = SwitchDefaults.colors(
|
||||
checkedThumbColor = MaterialTheme.colorScheme.onPrimary,
|
||||
checkedTrackColor = MaterialTheme.colorScheme.primary,
|
||||
checkedIconColor = MaterialTheme.colorScheme.primary,
|
||||
uncheckedThumbColor = MaterialTheme.colorScheme.outline,
|
||||
uncheckedTrackColor = MaterialTheme.colorScheme.surfaceVariant,
|
||||
uncheckedIconColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -764,83 +786,70 @@ fun ModuleItem(
|
||||
fontWeight = MaterialTheme.typography.bodySmall.fontWeight,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 4,
|
||||
textDecoration = textDecoration
|
||||
textDecoration = textDecoration,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
HorizontalDivider(thickness = Dp.Hairline)
|
||||
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
|
||||
if (module.hasActionScript) {
|
||||
FilledTonalButton(
|
||||
modifier = Modifier.defaultMinSize(52.dp, 32.dp),
|
||||
modifier = Modifier.defaultMinSize(minWidth = 52.dp, minHeight = 32.dp),
|
||||
enabled = !module.remove && module.enabled,
|
||||
onClick = {
|
||||
navigator.navigate(ExecuteModuleActionScreenDestination(module.dirId))
|
||||
viewModel.markNeedRefresh()
|
||||
},
|
||||
contentPadding = ButtonDefaults.TextButtonContentPadding,
|
||||
colors = if (!ThemeConfig.useDynamicColor) {
|
||||
ButtonDefaults.filledTonalButtonColors(
|
||||
containerColor = ThemeConfig.currentTheme.ButtonContrast
|
||||
)
|
||||
} else {
|
||||
ButtonDefaults.filledTonalButtonColors()
|
||||
}
|
||||
colors = ButtonDefaults.filledTonalButtonColors()
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.size(20.dp),
|
||||
imageVector = Icons.Outlined.PlayArrow,
|
||||
contentDescription = null
|
||||
)
|
||||
if (!module.hasWebUi && updateUrl.isEmpty()) {
|
||||
Text(
|
||||
modifier = Modifier.padding(start = 7.dp),
|
||||
text = stringResource(R.string.action),
|
||||
fontFamily = MaterialTheme.typography.labelMedium.fontFamily,
|
||||
fontSize = MaterialTheme.typography.labelMedium.fontSize
|
||||
)
|
||||
//if (!module.hasWebUi && updateUrl.isEmpty()) {
|
||||
//Text(
|
||||
// modifier = Modifier.padding(start = 7.dp),
|
||||
// text = stringResource(R.string.action),
|
||||
// fontFamily = MaterialTheme.typography.labelMedium.fontFamily,
|
||||
// fontSize = MaterialTheme.typography.labelMedium.fontSize
|
||||
//)
|
||||
//}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.weight(0.1f, true))
|
||||
}
|
||||
|
||||
if (module.hasWebUi) {
|
||||
FilledTonalButton(
|
||||
modifier = Modifier.defaultMinSize(52.dp, 32.dp),
|
||||
modifier = Modifier.defaultMinSize(minWidth = 52.dp, minHeight = 32.dp),
|
||||
enabled = !module.remove && module.enabled,
|
||||
onClick = { onClick(module) },
|
||||
interactionSource = interactionSource,
|
||||
contentPadding = ButtonDefaults.TextButtonContentPadding,
|
||||
colors = if (!ThemeConfig.useDynamicColor) {
|
||||
ButtonDefaults.filledTonalButtonColors(
|
||||
containerColor = ThemeConfig.currentTheme.ButtonContrast
|
||||
)
|
||||
} else {
|
||||
ButtonDefaults.filledTonalButtonColors()
|
||||
}
|
||||
colors = ButtonDefaults.filledTonalButtonColors()
|
||||
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.size(20.dp),
|
||||
imageVector = Icons.AutoMirrored.Outlined.Wysiwyg,
|
||||
contentDescription = null
|
||||
)
|
||||
if (!module.hasActionScript && updateUrl.isEmpty()) {
|
||||
Text(
|
||||
modifier = Modifier.padding(start = 7.dp),
|
||||
fontFamily = MaterialTheme.typography.labelMedium.fontFamily,
|
||||
fontSize = MaterialTheme.typography.labelMedium.fontSize,
|
||||
text = stringResource(R.string.open)
|
||||
)
|
||||
}
|
||||
//if (!module.hasActionScript && updateUrl.isEmpty()) {
|
||||
//Text(
|
||||
// modifier = Modifier.padding(start = 7.dp),
|
||||
// fontFamily = MaterialTheme.typography.labelMedium.fontFamily,
|
||||
// fontSize = MaterialTheme.typography.labelMedium.fontSize,
|
||||
// text = stringResource(R.string.open)
|
||||
//)
|
||||
//}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -848,7 +857,7 @@ fun ModuleItem(
|
||||
|
||||
if (updateUrl.isNotEmpty()) {
|
||||
Button(
|
||||
modifier = Modifier.defaultMinSize(52.dp, 32.dp),
|
||||
modifier = Modifier.defaultMinSize(minWidth = 52.dp, minHeight = 32.dp),
|
||||
enabled = !module.remove,
|
||||
onClick = { onUpdate(module) },
|
||||
shape = ButtonDefaults.textShape,
|
||||
@@ -859,30 +868,23 @@ fun ModuleItem(
|
||||
imageVector = Icons.Outlined.Download,
|
||||
contentDescription = null
|
||||
)
|
||||
if (!module.hasActionScript || !module.hasWebUi) {
|
||||
Text(
|
||||
modifier = Modifier.padding(start = 7.dp),
|
||||
fontFamily = MaterialTheme.typography.labelMedium.fontFamily,
|
||||
fontSize = MaterialTheme.typography.labelMedium.fontSize,
|
||||
text = stringResource(R.string.module_update)
|
||||
)
|
||||
//if (!module.hasActionScript || !module.hasWebUi) {
|
||||
//Text(
|
||||
// modifier = Modifier.padding(start = 7.dp),
|
||||
// fontFamily = MaterialTheme.typography.labelMedium.fontFamily,
|
||||
// fontSize = MaterialTheme.typography.labelMedium.fontSize,
|
||||
// text = stringResource(R.string.module_update)
|
||||
//)
|
||||
//}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.weight(0.1f, true))
|
||||
}
|
||||
|
||||
FilledTonalButton(
|
||||
modifier = Modifier.defaultMinSize(52.dp, 32.dp),
|
||||
modifier = Modifier.defaultMinSize(minWidth = 52.dp, minHeight = 32.dp),
|
||||
onClick = { onUninstallClicked(module) },
|
||||
contentPadding = ButtonDefaults.TextButtonContentPadding,
|
||||
colors = if (!ThemeConfig.useDynamicColor) {
|
||||
ButtonDefaults.filledTonalButtonColors(
|
||||
containerColor = ThemeConfig.currentTheme.ButtonContrast
|
||||
)
|
||||
} else {
|
||||
ButtonDefaults.filledTonalButtonColors()
|
||||
}
|
||||
colors = ButtonDefaults.filledTonalButtonColors(
|
||||
containerColor = if (!module.remove) MaterialTheme.colorScheme.secondaryContainer else MaterialTheme.colorScheme.errorContainer)
|
||||
) {
|
||||
if (!module.remove) {
|
||||
Icon(
|
||||
@@ -894,18 +896,18 @@ fun ModuleItem(
|
||||
Icon(
|
||||
modifier = Modifier.size(20.dp).rotate(180f),
|
||||
imageVector = Icons.Outlined.Refresh,
|
||||
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)
|
||||
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
|
||||
//)
|
||||
//}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -932,4 +934,3 @@ fun ModuleItemPreview() {
|
||||
)
|
||||
ModuleItem(EmptyDestinationsNavigator, module, "", {}, {}, {}, {})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,925 @@
|
||||
package com.sukisu.ultra.ui.screen
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.widget.Toast
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.expandVertically
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.shrinkVertically
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.automirrored.filled.NavigateNext
|
||||
import androidx.compose.material.icons.filled.Brush
|
||||
import androidx.compose.material.icons.filled.ColorLens
|
||||
import androidx.compose.material.icons.filled.DarkMode
|
||||
import androidx.compose.material.icons.filled.KeyboardArrowDown
|
||||
import androidx.compose.material.icons.filled.KeyboardArrowUp
|
||||
import androidx.compose.material.icons.filled.Opacity
|
||||
import androidx.compose.material.icons.filled.Palette
|
||||
import androidx.compose.material.icons.filled.Security
|
||||
import androidx.compose.material.icons.filled.VisibilityOff
|
||||
import androidx.compose.material.icons.filled.Wallpaper
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.ListItemDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.RadioButton
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Slider
|
||||
import androidx.compose.material3.SliderDefaults
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.rememberTopAppBarState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.content.edit
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
import com.sukisu.ultra.Natives
|
||||
import com.sukisu.ultra.R
|
||||
import com.sukisu.ultra.ksuApp
|
||||
import com.sukisu.ultra.ui.component.ImageEditorDialog
|
||||
import com.sukisu.ultra.ui.component.KsuIsValid
|
||||
import com.sukisu.ultra.ui.component.SwitchItem
|
||||
import com.sukisu.ultra.ui.theme.CardConfig
|
||||
import com.sukisu.ultra.ui.theme.ThemeColors
|
||||
import com.sukisu.ultra.ui.theme.ThemeConfig
|
||||
import com.sukisu.ultra.ui.theme.saveAndApplyCustomBackground
|
||||
import com.sukisu.ultra.ui.theme.saveCustomBackground
|
||||
import com.sukisu.ultra.ui.theme.saveDynamicColorState
|
||||
import com.sukisu.ultra.ui.theme.saveThemeColors
|
||||
import com.sukisu.ultra.ui.theme.saveThemeMode
|
||||
import com.sukisu.ultra.ui.util.getSuSFS
|
||||
import com.sukisu.ultra.ui.util.getSuSFSFeatures
|
||||
import com.sukisu.ultra.ui.util.susfsSUS_SU_0
|
||||
import com.sukisu.ultra.ui.util.susfsSUS_SU_2
|
||||
import com.sukisu.ultra.ui.util.susfsSUS_SU_Mode
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
fun saveCardConfig(context: Context) {
|
||||
CardConfig.save(context)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Destination<RootGraph>
|
||||
@Composable
|
||||
fun MoreSettingsScreen(navigator: DestinationsNavigator) {
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||
val context = LocalContext.current
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val prefs = remember { context.getSharedPreferences("settings", Context.MODE_PRIVATE) }
|
||||
val systemIsDark = isSystemInDarkTheme()
|
||||
|
||||
// 主题模式选择
|
||||
var themeMode by remember {
|
||||
mutableIntStateOf(
|
||||
when(ThemeConfig.forceDarkMode) {
|
||||
true -> 2 // 深色
|
||||
false -> 1 // 浅色
|
||||
null -> 0 // 跟随系统
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// 动态颜色开关状态
|
||||
var useDynamicColor by remember {
|
||||
mutableStateOf(ThemeConfig.useDynamicColor)
|
||||
}
|
||||
|
||||
var showThemeModeDialog by remember { mutableStateOf(false) }
|
||||
// 主题模式选项
|
||||
val themeOptions = listOf(
|
||||
stringResource(R.string.theme_follow_system),
|
||||
stringResource(R.string.theme_light),
|
||||
stringResource(R.string.theme_dark)
|
||||
)
|
||||
|
||||
// 简洁模式开关状态
|
||||
var isSimpleMode by remember {
|
||||
mutableStateOf(prefs.getBoolean("is_simple_mode", false))
|
||||
}
|
||||
|
||||
// 更新简洁模式开关状态
|
||||
val onSimpleModeChange = { newValue: Boolean ->
|
||||
prefs.edit { putBoolean("is_simple_mode", newValue) }
|
||||
isSimpleMode = newValue
|
||||
}
|
||||
|
||||
// 隐藏内核版本号开关状态
|
||||
var isHideVersion by remember {
|
||||
mutableStateOf(prefs.getBoolean("is_hide_version", false))
|
||||
}
|
||||
|
||||
// 隐藏内核版本号开关状态
|
||||
val onHideVersionChange = { newValue: Boolean ->
|
||||
prefs.edit { putBoolean("is_hide_version", newValue) }
|
||||
isHideVersion = newValue
|
||||
}
|
||||
|
||||
// 隐藏模块数量等信息开关状态
|
||||
var isHideOtherInfo by remember {
|
||||
mutableStateOf(prefs.getBoolean("is_hide_other_info", false))
|
||||
}
|
||||
|
||||
// 隐藏模块数量等信息开关状态
|
||||
val onHideOtherInfoChange = { newValue: Boolean ->
|
||||
prefs.edit { putBoolean("is_hide_other_info", newValue) }
|
||||
isHideOtherInfo = newValue
|
||||
}
|
||||
|
||||
// 隐藏SuSFS状态开关状态
|
||||
var isHideSusfsStatus by remember {
|
||||
mutableStateOf(prefs.getBoolean("is_hide_susfs_status", false))
|
||||
}
|
||||
|
||||
// 隐藏SuSFS状态开关状态
|
||||
val onHideSusfsStatusChange = { newValue: Boolean ->
|
||||
prefs.edit { putBoolean("is_hide_susfs_status", newValue) }
|
||||
isHideSusfsStatus = newValue
|
||||
}
|
||||
|
||||
// 隐藏链接状态开关状态
|
||||
var isHideLinkCard by remember {
|
||||
mutableStateOf(prefs.getBoolean("is_hide_link_card", false))
|
||||
}
|
||||
|
||||
val onHideLinkCardChange = { newValue: Boolean ->
|
||||
prefs.edit { putBoolean("is_hide_link_card", newValue) }
|
||||
isHideLinkCard = newValue
|
||||
}
|
||||
|
||||
// SELinux状态
|
||||
var selinuxEnabled by remember {
|
||||
mutableStateOf(Shell.cmd("getenforce").exec().out.firstOrNull() == "Enforcing")
|
||||
}
|
||||
|
||||
// 卡片配置状态
|
||||
var cardAlpha by rememberSaveable { mutableFloatStateOf(CardConfig.cardAlpha) }
|
||||
var isCustomBackgroundEnabled by rememberSaveable {
|
||||
mutableStateOf(ThemeConfig.customBackgroundUri != null)
|
||||
}
|
||||
|
||||
// 图片编辑状态
|
||||
var showImageEditor by remember { mutableStateOf(false) }
|
||||
var selectedImageUri by remember { mutableStateOf<Uri?>(null) }
|
||||
|
||||
// 展开/折叠状态
|
||||
var isCustomizeExpanded by remember { mutableStateOf(false) }
|
||||
var isAppearanceExpanded by remember { mutableStateOf(true) }
|
||||
var isAdvancedExpanded by remember { mutableStateOf(false) }
|
||||
|
||||
// 初始化卡片配置
|
||||
LaunchedEffect(Unit) {
|
||||
// 加载设置
|
||||
CardConfig.load(context)
|
||||
cardAlpha = CardConfig.cardAlpha
|
||||
isCustomBackgroundEnabled = ThemeConfig.customBackgroundUri != null
|
||||
|
||||
// 设置主题模式
|
||||
themeMode = when (ThemeConfig.forceDarkMode) {
|
||||
true -> 2
|
||||
false -> 1
|
||||
null -> 0
|
||||
}
|
||||
|
||||
// 确保卡片样式跟随主题模式
|
||||
when (themeMode) {
|
||||
2 -> { // 深色
|
||||
CardConfig.isUserDarkModeEnabled = true
|
||||
CardConfig.isUserLightModeEnabled = false
|
||||
}
|
||||
1 -> { // 浅色
|
||||
CardConfig.isUserDarkModeEnabled = false
|
||||
CardConfig.isUserLightModeEnabled = true
|
||||
}
|
||||
0 -> { // 跟随系统
|
||||
CardConfig.isUserDarkModeEnabled = false
|
||||
CardConfig.isUserLightModeEnabled = false
|
||||
}
|
||||
}
|
||||
|
||||
// 如果启用了系统跟随且系统是深色模式,应用深色模式默认值
|
||||
if (themeMode == 0 && systemIsDark) {
|
||||
CardConfig.setDarkModeDefaults()
|
||||
}
|
||||
|
||||
// 保存设置
|
||||
CardConfig.save(context)
|
||||
}
|
||||
|
||||
// 主题色选项
|
||||
val themeColorOptions = listOf(
|
||||
stringResource(R.string.color_default) to ThemeColors.Default,
|
||||
stringResource(R.string.color_green) to ThemeColors.Green,
|
||||
stringResource(R.string.color_purple) to ThemeColors.Purple,
|
||||
stringResource(R.string.color_orange) to ThemeColors.Orange,
|
||||
stringResource(R.string.color_pink) to ThemeColors.Pink,
|
||||
stringResource(R.string.color_gray) to ThemeColors.Gray,
|
||||
stringResource(R.string.color_yellow) to ThemeColors.Yellow
|
||||
)
|
||||
|
||||
var showThemeColorDialog by remember { mutableStateOf(false) }
|
||||
|
||||
// 图片选择器
|
||||
val pickImageLauncher = rememberLauncherForActivityResult(
|
||||
ActivityResultContracts.GetContent()
|
||||
) { uri: Uri? ->
|
||||
uri?.let {
|
||||
selectedImageUri = it
|
||||
showImageEditor = true
|
||||
}
|
||||
}
|
||||
|
||||
// 显示图片编辑对话框
|
||||
if (showImageEditor && selectedImageUri != null) {
|
||||
ImageEditorDialog(
|
||||
imageUri = selectedImageUri!!,
|
||||
onDismiss = {
|
||||
showImageEditor = false
|
||||
selectedImageUri = null
|
||||
},
|
||||
onConfirm = { transformedUri ->
|
||||
context.saveAndApplyCustomBackground(transformedUri)
|
||||
isCustomBackgroundEnabled = true
|
||||
CardConfig.cardElevation = 0.dp
|
||||
CardConfig.isCustomBackgroundEnabled = true
|
||||
saveCardConfig(context)
|
||||
showImageEditor = false
|
||||
selectedImageUri = null
|
||||
|
||||
// 显示成功提示
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.background_set_success),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
val cardColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
val cardAlphaUse = CardConfig.cardAlpha
|
||||
|
||||
Scaffold(
|
||||
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text(stringResource(R.string.more_settings)) },
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = cardColor.copy(alpha = cardAlphaUse),
|
||||
scrolledContainerColor = cardColor.copy(alpha = cardAlphaUse)),
|
||||
navigationIcon = {
|
||||
IconButton(onClick = { navigator.popBackStack() }) {
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = stringResource(R.string.back))
|
||||
}
|
||||
},
|
||||
scrollBehavior = scrollBehavior,
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||
) {
|
||||
// 外观设置部分
|
||||
SectionHeader(
|
||||
title = stringResource(R.string.appearance_settings),
|
||||
expanded = isAppearanceExpanded,
|
||||
onToggle = { isAppearanceExpanded = !isAppearanceExpanded }
|
||||
)
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = isAppearanceExpanded,
|
||||
enter = fadeIn() + expandVertically(),
|
||||
exit = fadeOut() + shrinkVertically()
|
||||
) {
|
||||
Surface(
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
tonalElevation = 1.dp,
|
||||
modifier = Modifier.padding(bottom = 16.dp)
|
||||
) {
|
||||
Column {
|
||||
// 主题模式
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(R.string.theme_mode)) },
|
||||
supportingContent = { Text(themeOptions[themeMode]) },
|
||||
leadingContent = {
|
||||
Icon(
|
||||
Icons.Default.DarkMode,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
},
|
||||
trailingContent = {
|
||||
Icon(
|
||||
Icons.AutoMirrored.Filled.NavigateNext,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
},
|
||||
colors = ListItemDefaults.colors(
|
||||
containerColor = Color.Transparent
|
||||
),
|
||||
modifier = Modifier.clickable { showThemeModeDialog = true }
|
||||
)
|
||||
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
color = MaterialTheme.colorScheme.outlineVariant
|
||||
)
|
||||
|
||||
// 动态颜色开关
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
SwitchItem(
|
||||
icon = Icons.Filled.ColorLens,
|
||||
title = stringResource(R.string.dynamic_color_title),
|
||||
summary = stringResource(R.string.dynamic_color_summary),
|
||||
checked = useDynamicColor
|
||||
) { enabled ->
|
||||
useDynamicColor = enabled
|
||||
context.saveDynamicColorState(enabled)
|
||||
}
|
||||
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
color = MaterialTheme.colorScheme.outlineVariant
|
||||
)
|
||||
}
|
||||
|
||||
// 只在未启用动态颜色时显示主题色选择
|
||||
AnimatedVisibility(
|
||||
visible = Build.VERSION.SDK_INT < Build.VERSION_CODES.S || !useDynamicColor,
|
||||
enter = fadeIn() + expandVertically(),
|
||||
exit = fadeOut() + shrinkVertically()
|
||||
) {
|
||||
Column {
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(R.string.theme_color)) },
|
||||
supportingContent = {
|
||||
val currentThemeName = when (ThemeConfig.currentTheme) {
|
||||
is ThemeColors.Default -> stringResource(R.string.color_default)
|
||||
is ThemeColors.Green -> stringResource(R.string.color_green)
|
||||
is ThemeColors.Purple -> stringResource(R.string.color_purple)
|
||||
is ThemeColors.Orange -> stringResource(R.string.color_orange)
|
||||
is ThemeColors.Pink -> stringResource(R.string.color_pink)
|
||||
is ThemeColors.Gray -> stringResource(R.string.color_gray)
|
||||
is ThemeColors.Yellow -> stringResource(R.string.color_yellow)
|
||||
else -> stringResource(R.string.color_default)
|
||||
}
|
||||
Text(currentThemeName)
|
||||
},
|
||||
leadingContent = {
|
||||
Icon(
|
||||
Icons.Default.Palette,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
},
|
||||
trailingContent = {
|
||||
Icon(
|
||||
Icons.AutoMirrored.Filled.NavigateNext,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
},
|
||||
colors = ListItemDefaults.colors(
|
||||
containerColor = Color.Transparent
|
||||
),
|
||||
modifier = Modifier.clickable { showThemeColorDialog = true }
|
||||
)
|
||||
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
color = MaterialTheme.colorScheme.outlineVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 自定义背景开关
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(id = R.string.settings_custom_background)) },
|
||||
supportingContent = { Text(stringResource(id = R.string.settings_custom_background_summary)) },
|
||||
leadingContent = {
|
||||
Icon(
|
||||
Icons.Filled.Wallpaper,
|
||||
contentDescription = null,
|
||||
tint = if (isCustomBackgroundEnabled)
|
||||
MaterialTheme.colorScheme.primary
|
||||
else
|
||||
MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
},
|
||||
trailingContent = {
|
||||
Switch(
|
||||
checked = isCustomBackgroundEnabled,
|
||||
onCheckedChange = { isChecked ->
|
||||
if (isChecked) {
|
||||
pickImageLauncher.launch("image/*")
|
||||
} else {
|
||||
context.saveCustomBackground(null)
|
||||
isCustomBackgroundEnabled = false
|
||||
CardConfig.cardElevation
|
||||
CardConfig.cardAlpha = 1f
|
||||
CardConfig.isCustomAlphaSet = false
|
||||
CardConfig.isCustomBackgroundEnabled = false
|
||||
saveCardConfig(context)
|
||||
cardAlpha = 1f
|
||||
|
||||
// 重置其他相关设置
|
||||
ThemeConfig.needsResetOnThemeChange = true
|
||||
ThemeConfig.preventBackgroundRefresh = false
|
||||
|
||||
context.getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
||||
.edit {
|
||||
putBoolean(
|
||||
"prevent_background_refresh",
|
||||
false
|
||||
)
|
||||
}
|
||||
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.background_removed),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
colors = ListItemDefaults.colors(
|
||||
containerColor = Color.Transparent
|
||||
)
|
||||
)
|
||||
|
||||
// 透明度 Slider
|
||||
AnimatedVisibility(
|
||||
visible = ThemeConfig.customBackgroundUri != null,
|
||||
enter = fadeIn() + expandVertically(),
|
||||
exit = fadeOut() + shrinkVertically(),
|
||||
modifier = Modifier.padding(horizontal = 32.dp)
|
||||
) {
|
||||
Column(modifier = Modifier.padding(vertical = 8.dp)) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(bottom = 4.dp)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Filled.Opacity,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(20.dp),
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.settings_card_alpha),
|
||||
style = MaterialTheme.typography.titleSmall
|
||||
)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Text(
|
||||
text = "${(cardAlpha * 100).roundToInt()}%",
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
|
||||
Slider(
|
||||
value = cardAlpha,
|
||||
onValueChange = { newValue ->
|
||||
cardAlpha = newValue
|
||||
CardConfig.cardAlpha = newValue
|
||||
CardConfig.isCustomAlphaSet = true
|
||||
prefs.edit {
|
||||
putBoolean("is_custom_alpha_set", true)
|
||||
putFloat("card_alpha", newValue)
|
||||
}
|
||||
},
|
||||
onValueChangeFinished = {
|
||||
coroutineScope.launch(Dispatchers.IO) {
|
||||
saveCardConfig(context)
|
||||
}
|
||||
},
|
||||
valueRange = 0f..1f,
|
||||
steps = 20,
|
||||
colors = SliderDefaults.colors(
|
||||
thumbColor = MaterialTheme.colorScheme.primary,
|
||||
activeTrackColor = MaterialTheme.colorScheme.primary,
|
||||
inactiveTrackColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 自定义设置部分
|
||||
SectionHeader(
|
||||
title = stringResource(R.string.custom_settings),
|
||||
expanded = isCustomizeExpanded,
|
||||
onToggle = { isCustomizeExpanded = !isCustomizeExpanded }
|
||||
)
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = isCustomizeExpanded,
|
||||
enter = fadeIn() + expandVertically(),
|
||||
exit = fadeOut() + shrinkVertically()
|
||||
) {
|
||||
Surface(
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
tonalElevation = 1.dp,
|
||||
modifier = Modifier.padding(bottom = 16.dp)
|
||||
) {
|
||||
Column {
|
||||
// 添加简洁模式开关
|
||||
SwitchItem(
|
||||
icon = Icons.Filled.Brush,
|
||||
title = stringResource(R.string.simple_mode),
|
||||
summary = stringResource(R.string.simple_mode_summary),
|
||||
checked = isSimpleMode
|
||||
) {
|
||||
onSimpleModeChange(it)
|
||||
}
|
||||
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
color = MaterialTheme.colorScheme.outlineVariant
|
||||
)
|
||||
|
||||
// 隐藏内核部分版本号
|
||||
SwitchItem(
|
||||
icon = Icons.Filled.VisibilityOff,
|
||||
title = stringResource(R.string.hide_kernel_kernelsu_version),
|
||||
summary = stringResource(R.string.hide_kernel_kernelsu_version_summary),
|
||||
checked = isHideVersion
|
||||
) {
|
||||
onHideVersionChange(it)
|
||||
}
|
||||
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
color = MaterialTheme.colorScheme.outlineVariant
|
||||
)
|
||||
|
||||
// 模块数量等信息
|
||||
SwitchItem(
|
||||
icon = Icons.Filled.VisibilityOff,
|
||||
title = stringResource(R.string.hide_other_info),
|
||||
summary = stringResource(R.string.hide_other_info_summary),
|
||||
checked = isHideOtherInfo
|
||||
) {
|
||||
onHideOtherInfoChange(it)
|
||||
}
|
||||
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
color = MaterialTheme.colorScheme.outlineVariant
|
||||
)
|
||||
|
||||
// SuSFS 状态信息
|
||||
SwitchItem(
|
||||
icon = Icons.Filled.VisibilityOff,
|
||||
title = stringResource(R.string.hide_susfs_status),
|
||||
summary = stringResource(R.string.hide_susfs_status_summary),
|
||||
checked = isHideSusfsStatus
|
||||
) {
|
||||
onHideSusfsStatusChange(it)
|
||||
}
|
||||
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
color = MaterialTheme.colorScheme.outlineVariant
|
||||
)
|
||||
|
||||
// 隐藏链接信息
|
||||
SwitchItem(
|
||||
icon = Icons.Filled.VisibilityOff,
|
||||
title = stringResource(R.string.hide_link_card),
|
||||
summary = stringResource(R.string.hide_link_card_summary),
|
||||
checked = isHideLinkCard
|
||||
) {
|
||||
onHideLinkCardChange(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 高级设置部分
|
||||
SectionHeader(
|
||||
title = stringResource(R.string.advanced_settings),
|
||||
expanded = isAdvancedExpanded,
|
||||
onToggle = { isAdvancedExpanded = !isAdvancedExpanded }
|
||||
)
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = isAdvancedExpanded,
|
||||
enter = fadeIn() + expandVertically(),
|
||||
exit = fadeOut() + shrinkVertically()
|
||||
) {
|
||||
Surface(
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
tonalElevation = 1.dp,
|
||||
modifier = Modifier.padding(bottom = 16.dp)
|
||||
) {
|
||||
Column {
|
||||
// SELinux 开关
|
||||
KsuIsValid {
|
||||
SwitchItem(
|
||||
icon = Icons.Filled.Security,
|
||||
title = stringResource(R.string.selinux),
|
||||
summary = if (selinuxEnabled)
|
||||
stringResource(R.string.selinux_enabled) else
|
||||
stringResource(R.string.selinux_disabled),
|
||||
checked = selinuxEnabled
|
||||
) { enabled ->
|
||||
val command = if (enabled) "setenforce 1" else "setenforce 0"
|
||||
Shell.getShell().newJob().add(command).exec().let { result ->
|
||||
if (result.isSuccess) {
|
||||
selinuxEnabled = enabled
|
||||
// 显示成功提示
|
||||
val message = if (enabled)
|
||||
context.getString(R.string.selinux_enabled_toast)
|
||||
else
|
||||
context.getString(R.string.selinux_disabled_toast)
|
||||
|
||||
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
|
||||
} else {
|
||||
// 显示失败提示
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.selinux_change_failed),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
color = MaterialTheme.colorScheme.outlineVariant
|
||||
)
|
||||
}
|
||||
|
||||
// SuSFS 配置(仅在支持时显示)
|
||||
val suSFS = getSuSFS()
|
||||
val isSUS_SU = getSuSFSFeatures()
|
||||
if (suSFS == "Supported" && isSUS_SU == "CONFIG_KSU_SUSFS_SUS_SU") {
|
||||
// 初始化时,默认启用
|
||||
var isEnabled by rememberSaveable {
|
||||
mutableStateOf(true) // 默认启用
|
||||
}
|
||||
|
||||
// 在启动时检查状态
|
||||
LaunchedEffect(Unit) {
|
||||
// 如果当前模式不是2就强制启用
|
||||
val currentMode = susfsSUS_SU_Mode()
|
||||
val wasManuallyDisabled = prefs.getBoolean("enable_sus_su", true)
|
||||
if (currentMode != "2" && wasManuallyDisabled) {
|
||||
susfsSUS_SU_2() // 强制切换到模式2
|
||||
prefs.edit { putBoolean("enable_sus_su", true) }
|
||||
}
|
||||
isEnabled = currentMode == "2"
|
||||
}
|
||||
|
||||
SwitchItem(
|
||||
icon = Icons.Filled.Security,
|
||||
title = stringResource(id = R.string.settings_susfs_toggle),
|
||||
summary = stringResource(id = R.string.settings_susfs_toggle_summary),
|
||||
checked = isEnabled
|
||||
) {
|
||||
if (it) {
|
||||
// 手动启用
|
||||
susfsSUS_SU_2()
|
||||
prefs.edit { putBoolean("enable_sus_su", true) }
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.susfs_enabled),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
} else {
|
||||
// 手动关闭
|
||||
susfsSUS_SU_0()
|
||||
prefs.edit { putBoolean("enable_sus_su", false) }
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.susfs_disabled),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
isEnabled = it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 主题模式选择对话框
|
||||
if (showThemeModeDialog) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { showThemeModeDialog = false },
|
||||
title = { Text(stringResource(R.string.theme_mode)) },
|
||||
text = {
|
||||
Column {
|
||||
themeOptions.forEachIndexed { index, option ->
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
themeMode = index
|
||||
val newThemeMode = when(index) {
|
||||
0 -> null // 跟随系统
|
||||
1 -> false // 浅色
|
||||
2 -> true // 深色
|
||||
else -> null
|
||||
}
|
||||
context.saveThemeMode(newThemeMode)
|
||||
when (index) {
|
||||
2 -> { // 深色
|
||||
ThemeConfig.forceDarkMode = true
|
||||
CardConfig.isUserDarkModeEnabled = true
|
||||
CardConfig.isUserLightModeEnabled = false
|
||||
if (!CardConfig.isCustomAlphaSet) {
|
||||
CardConfig.cardAlpha = 1f
|
||||
}
|
||||
CardConfig.save(context)
|
||||
}
|
||||
1 -> { // 浅色
|
||||
ThemeConfig.forceDarkMode = false
|
||||
CardConfig.isUserLightModeEnabled = true
|
||||
CardConfig.isUserDarkModeEnabled = false
|
||||
if (!CardConfig.isCustomAlphaSet) {
|
||||
CardConfig.cardAlpha = 1f
|
||||
}
|
||||
CardConfig.save(context)
|
||||
}
|
||||
0 -> { // 跟随系统
|
||||
ThemeConfig.forceDarkMode = null
|
||||
CardConfig.isUserLightModeEnabled = false
|
||||
CardConfig.isUserDarkModeEnabled = false
|
||||
CardConfig.save(context)
|
||||
}
|
||||
}
|
||||
showThemeModeDialog = false
|
||||
}
|
||||
.padding(vertical = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
RadioButton(
|
||||
selected = themeMode == index,
|
||||
onClick = null
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(option)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = { showThemeModeDialog = false }
|
||||
) {
|
||||
Text(stringResource(R.string.cancel))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// 主题色选择对话框
|
||||
if (showThemeColorDialog) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { showThemeColorDialog = false },
|
||||
title = { Text(stringResource(R.string.choose_theme_color)) },
|
||||
text = {
|
||||
Column {
|
||||
themeColorOptions.forEach { (name, theme) ->
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
context.saveThemeColors(when (theme) {
|
||||
ThemeColors.Default -> "default"
|
||||
ThemeColors.Green -> "green"
|
||||
ThemeColors.Purple -> "purple"
|
||||
ThemeColors.Orange -> "orange"
|
||||
ThemeColors.Pink -> "pink"
|
||||
ThemeColors.Gray -> "gray"
|
||||
ThemeColors.Yellow -> "yellow"
|
||||
else -> "default"
|
||||
})
|
||||
showThemeColorDialog = false
|
||||
}
|
||||
.padding(vertical = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
RadioButton(
|
||||
selected = ThemeConfig.currentTheme::class == theme::class,
|
||||
onClick = null
|
||||
)
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(24.dp)
|
||||
.clip(CircleShape)
|
||||
.background(theme.Primary)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
Text(name)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = { showThemeColorDialog = false }
|
||||
) {
|
||||
Text(stringResource(R.string.cancel))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SectionHeader(
|
||||
title: String,
|
||||
expanded: Boolean,
|
||||
onToggle: () -> Unit
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { onToggle() }
|
||||
.padding(vertical = 12.dp, horizontal = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.titleMedium.copy(
|
||||
fontWeight = FontWeight.Bold
|
||||
),
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Icon(
|
||||
imageVector = if (expanded) Icons.Default.KeyboardArrowUp else Icons.Default.KeyboardArrowDown,
|
||||
contentDescription = if (expanded)
|
||||
stringResource(R.string.collapse)
|
||||
else
|
||||
stringResource(R.string.expand),
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
722
manager/app/src/main/java/com/sukisu/ultra/ui/screen/Settings.kt
Normal file
722
manager/app/src/main/java/com/sukisu/ultra/ui/screen/Settings.kt
Normal file
@@ -0,0 +1,722 @@
|
||||
package com.sukisu.ultra.ui.screen
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.widget.Toast
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.Undo
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.core.content.edit
|
||||
import com.maxkeppeker.sheets.core.models.base.IconSource
|
||||
import com.maxkeppeler.sheets.list.models.ListOption
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||
import com.ramcosta.composedestinations.generated.destinations.AppProfileTemplateScreenDestination
|
||||
import com.ramcosta.composedestinations.generated.destinations.FlashScreenDestination
|
||||
import com.ramcosta.composedestinations.generated.destinations.MoreSettingsScreenDestination
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import com.sukisu.ultra.BuildConfig
|
||||
import com.sukisu.ultra.Natives
|
||||
import com.sukisu.ultra.R
|
||||
import com.sukisu.ultra.*
|
||||
import com.sukisu.ultra.ui.component.*
|
||||
import com.sukisu.ultra.ui.theme.*
|
||||
import com.sukisu.ultra.ui.theme.CardConfig.cardAlpha
|
||||
import com.sukisu.ultra.ui.theme.CardConfig.cardElevation
|
||||
import com.sukisu.ultra.ui.util.LocalSnackbarHost
|
||||
import com.sukisu.ultra.ui.util.getBugreportFile
|
||||
import java.time.LocalDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
import com.sukisu.ultra.ui.component.KsuIsValid
|
||||
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Destination<RootGraph>
|
||||
@Composable
|
||||
fun SettingScreen(navigator: DestinationsNavigator) {
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||
val snackBarHost = LocalSnackbarHost.current
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopBar(
|
||||
scrollBehavior = scrollBehavior
|
||||
)
|
||||
},
|
||||
snackbarHost = { SnackbarHost(snackBarHost) },
|
||||
contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal)
|
||||
) { paddingValues ->
|
||||
val aboutDialog = rememberCustomDialog {
|
||||
AboutDialog(it)
|
||||
}
|
||||
val loadingDialog = rememberLoadingDialog()
|
||||
// endregion
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
// region 上下文与协程
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
// endregion
|
||||
|
||||
// region 日志导出功能
|
||||
val exportBugreportLauncher = rememberLauncherForActivityResult(
|
||||
ActivityResultContracts.CreateDocument("application/gzip")
|
||||
) { uri: Uri? ->
|
||||
if (uri == null) return@rememberLauncherForActivityResult
|
||||
scope.launch(Dispatchers.IO) {
|
||||
loadingDialog.show()
|
||||
context.contentResolver.openOutputStream(uri)?.use { output ->
|
||||
getBugreportFile(context).inputStream().use {
|
||||
it.copyTo(output)
|
||||
}
|
||||
}
|
||||
loadingDialog.hide()
|
||||
snackBarHost.showSnackbar(context.getString(R.string.log_saved))
|
||||
}
|
||||
}
|
||||
|
||||
// 配置
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceContainerLow.copy(alpha = cardAlpha)
|
||||
),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation)
|
||||
) {
|
||||
Column(modifier = Modifier.padding(vertical = 8.dp)) {
|
||||
Text(
|
||||
text = stringResource(R.string.configuration),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||
)
|
||||
|
||||
// 配置文件模板入口
|
||||
val profileTemplate = stringResource(id = R.string.settings_profile_template)
|
||||
KsuIsValid {
|
||||
SettingItem(
|
||||
icon = Icons.Filled.Fence,
|
||||
title = profileTemplate,
|
||||
summary = stringResource(id = R.string.settings_profile_template_summary),
|
||||
onClick = {
|
||||
navigator.navigate(AppProfileTemplateScreenDestination)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// 卸载模块开关
|
||||
var umountChecked by rememberSaveable {
|
||||
mutableStateOf(Natives.isDefaultUmountModules())
|
||||
}
|
||||
|
||||
KsuIsValid {
|
||||
SwitchSettingItem(
|
||||
icon = Icons.Filled.FolderDelete,
|
||||
title = stringResource(id = R.string.settings_umount_modules_default),
|
||||
summary = stringResource(id = R.string.settings_umount_modules_default_summary),
|
||||
checked = umountChecked,
|
||||
onCheckedChange = {
|
||||
if (Natives.setDefaultUmountModules(it)) {
|
||||
umountChecked = it
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// SU 禁用开关(仅在兼容版本显示)
|
||||
KsuIsValid {
|
||||
if (Natives.version >= Natives.MINIMAL_SUPPORTED_SU_COMPAT) {
|
||||
var isSuDisabled by rememberSaveable {
|
||||
mutableStateOf(!Natives.isSuEnabled())
|
||||
}
|
||||
SwitchSettingItem(
|
||||
icon = Icons.Filled.RemoveModerator,
|
||||
title = stringResource(id = R.string.settings_disable_su),
|
||||
summary = stringResource(id = R.string.settings_disable_su_summary),
|
||||
checked = isSuDisabled,
|
||||
onCheckedChange = { checked ->
|
||||
val shouldEnable = !checked
|
||||
if (Natives.setSuEnabled(shouldEnable)) {
|
||||
isSuDisabled = !shouldEnable
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 设置分组卡片 - 应用设置
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceContainerLow.copy(alpha = cardAlpha)
|
||||
),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation)
|
||||
) {
|
||||
Column(modifier = Modifier.padding(vertical = 8.dp)) {
|
||||
Text(
|
||||
text = stringResource(R.string.app_settings),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||
)
|
||||
|
||||
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||
|
||||
// 更新检查开关
|
||||
var checkUpdate by rememberSaveable {
|
||||
mutableStateOf(
|
||||
prefs.getBoolean("check_update", true)
|
||||
)
|
||||
}
|
||||
SwitchSettingItem(
|
||||
icon = Icons.Filled.Update,
|
||||
title = stringResource(id = R.string.settings_check_update),
|
||||
summary = stringResource(id = R.string.settings_check_update_summary),
|
||||
checked = checkUpdate,
|
||||
onCheckedChange = {
|
||||
prefs.edit {putBoolean("check_update", it) }
|
||||
checkUpdate = it
|
||||
}
|
||||
)
|
||||
|
||||
// Web调试开关
|
||||
var enableWebDebugging by rememberSaveable {
|
||||
mutableStateOf(
|
||||
prefs.getBoolean("enable_web_debugging", false)
|
||||
)
|
||||
}
|
||||
KsuIsValid {
|
||||
SwitchSettingItem(
|
||||
icon = Icons.Filled.DeveloperMode,
|
||||
title = stringResource(id = R.string.enable_web_debugging),
|
||||
summary = stringResource(id = R.string.enable_web_debugging_summary),
|
||||
checked = enableWebDebugging,
|
||||
onCheckedChange = {
|
||||
prefs.edit { putBoolean("enable_web_debugging", it) }
|
||||
enableWebDebugging = it
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// 更多设置
|
||||
SettingItem(
|
||||
icon = Icons.Filled.Settings,
|
||||
title = stringResource(id = R.string.more_settings),
|
||||
summary = stringResource(id = R.string.more_settings),
|
||||
onClick = {
|
||||
navigator.navigate(MoreSettingsScreenDestination)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 设置分组卡片 - 工具
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceContainerLow.copy(alpha = cardAlpha)
|
||||
),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation)
|
||||
) {
|
||||
Column(modifier = Modifier.padding(vertical = 8.dp)) {
|
||||
Text(
|
||||
text = stringResource(R.string.tools),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||
)
|
||||
|
||||
var showBottomsheet by remember { mutableStateOf(false) }
|
||||
|
||||
SettingItem(
|
||||
icon = Icons.Filled.BugReport,
|
||||
title = stringResource(id = R.string.send_log),
|
||||
onClick = {
|
||||
showBottomsheet = true
|
||||
}
|
||||
)
|
||||
|
||||
if (showBottomsheet) {
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = { showBottomsheet = false },
|
||||
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly
|
||||
) {
|
||||
LogActionButton(
|
||||
icon = Icons.Filled.Save,
|
||||
text = stringResource(R.string.save_log),
|
||||
onClick = {
|
||||
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH_mm")
|
||||
val current = LocalDateTime.now().format(formatter)
|
||||
exportBugreportLauncher.launch("KernelSU_bugreport_${current}.tar.gz")
|
||||
showBottomsheet = false
|
||||
}
|
||||
)
|
||||
|
||||
LogActionButton(
|
||||
icon = Icons.Filled.Share,
|
||||
text = stringResource(R.string.send_log),
|
||||
onClick = {
|
||||
scope.launch {
|
||||
val bugreport = loadingDialog.withLoading {
|
||||
withContext(Dispatchers.IO) {
|
||||
getBugreportFile(context)
|
||||
}
|
||||
}
|
||||
|
||||
val uri: Uri =
|
||||
FileProvider.getUriForFile(
|
||||
context,
|
||||
"${BuildConfig.APPLICATION_ID}.fileprovider",
|
||||
bugreport
|
||||
)
|
||||
|
||||
val shareIntent = Intent(Intent.ACTION_SEND).apply {
|
||||
putExtra(Intent.EXTRA_STREAM, uri)
|
||||
setDataAndType(uri, "application/gzip")
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
}
|
||||
|
||||
context.startActivity(
|
||||
Intent.createChooser(
|
||||
shareIntent,
|
||||
context.getString(R.string.send_log)
|
||||
)
|
||||
)
|
||||
|
||||
showBottomsheet = false
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
}
|
||||
}
|
||||
|
||||
val lkmMode = Natives.version >= Natives.MINIMAL_SUPPORTED_KERNEL_LKM && Natives.isLkmMode
|
||||
if (lkmMode) {
|
||||
UninstallItem(navigator) {
|
||||
loadingDialog.withLoading(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 设置分组卡片 - 关于
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceContainerLow.copy(alpha = cardAlpha)
|
||||
),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation)
|
||||
) {
|
||||
Column(modifier = Modifier.padding(vertical = 8.dp)) {
|
||||
Text(
|
||||
text = stringResource(R.string.about),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||
)
|
||||
|
||||
SettingItem(
|
||||
icon = Icons.Filled.Info,
|
||||
title = stringResource(R.string.about),
|
||||
onClick = {
|
||||
aboutDialog.show()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LogActionButton(
|
||||
icon: ImageVector,
|
||||
text: String,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier
|
||||
.clickable(onClick = onClick)
|
||||
.padding(8.dp)
|
||||
) {
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = Modifier
|
||||
.size(56.dp)
|
||||
.clip(CircleShape)
|
||||
.background(MaterialTheme.colorScheme.primaryContainer)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = text,
|
||||
tint = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = text,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SettingItem(
|
||||
icon: ImageVector,
|
||||
title: String,
|
||||
summary: String? = null,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(onClick = onClick)
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier
|
||||
.padding(end = 16.dp)
|
||||
.size(24.dp)
|
||||
)
|
||||
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
if (summary != null) {
|
||||
Text(
|
||||
text = summary,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Icon(
|
||||
imageVector = Icons.Filled.ChevronRight,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SwitchSettingItem(
|
||||
icon: ImageVector,
|
||||
title: String,
|
||||
summary: String? = null,
|
||||
checked: Boolean,
|
||||
onCheckedChange: (Boolean) -> Unit
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { onCheckedChange(!checked) }
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier
|
||||
.padding(end = 16.dp)
|
||||
.size(24.dp)
|
||||
)
|
||||
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
if (summary != null) {
|
||||
Text(
|
||||
text = summary,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Switch(
|
||||
checked = checked,
|
||||
onCheckedChange = onCheckedChange,
|
||||
colors = SwitchDefaults.colors(
|
||||
checkedThumbColor = MaterialTheme.colorScheme.onPrimary,
|
||||
checkedTrackColor = MaterialTheme.colorScheme.primary,
|
||||
checkedIconColor = MaterialTheme.colorScheme.primary,
|
||||
uncheckedThumbColor = MaterialTheme.colorScheme.outline,
|
||||
uncheckedTrackColor = MaterialTheme.colorScheme.surfaceVariant,
|
||||
uncheckedIconColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UninstallItem(
|
||||
navigator: DestinationsNavigator,
|
||||
withLoading: suspend (suspend () -> Unit) -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
val uninstallConfirmDialog = rememberConfirmDialog()
|
||||
val showTodo = {
|
||||
Toast.makeText(context, "TODO", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
val uninstallDialog = rememberUninstallDialog { uninstallType ->
|
||||
scope.launch {
|
||||
val result = uninstallConfirmDialog.awaitConfirm(
|
||||
title = context.getString(uninstallType.title),
|
||||
content = context.getString(uninstallType.message)
|
||||
)
|
||||
if (result == ConfirmResult.Confirmed) {
|
||||
withLoading {
|
||||
when (uninstallType) {
|
||||
UninstallType.TEMPORARY -> showTodo()
|
||||
UninstallType.PERMANENT -> navigator.navigate(
|
||||
FlashScreenDestination(FlashIt.FlashUninstall)
|
||||
)
|
||||
UninstallType.RESTORE_STOCK_IMAGE -> navigator.navigate(
|
||||
FlashScreenDestination(FlashIt.FlashRestore)
|
||||
)
|
||||
UninstallType.NONE -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SettingItem(
|
||||
icon = Icons.Filled.Delete,
|
||||
title = stringResource(id = R.string.settings_uninstall),
|
||||
onClick = {
|
||||
uninstallDialog.show()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
enum class UninstallType(val title: Int, val message: Int, val icon: ImageVector) {
|
||||
TEMPORARY(
|
||||
R.string.settings_uninstall_temporary,
|
||||
R.string.settings_uninstall_temporary_message,
|
||||
Icons.Filled.Delete
|
||||
),
|
||||
PERMANENT(
|
||||
R.string.settings_uninstall_permanent,
|
||||
R.string.settings_uninstall_permanent_message,
|
||||
Icons.Filled.DeleteForever
|
||||
),
|
||||
RESTORE_STOCK_IMAGE(
|
||||
R.string.settings_restore_stock_image,
|
||||
R.string.settings_restore_stock_image_message,
|
||||
Icons.AutoMirrored.Filled.Undo
|
||||
),
|
||||
NONE(0, 0, Icons.Filled.Delete)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun rememberUninstallDialog(onSelected: (UninstallType) -> Unit): DialogHandle {
|
||||
return rememberCustomDialog { dismiss ->
|
||||
val options = listOf(
|
||||
// UninstallType.TEMPORARY,
|
||||
UninstallType.PERMANENT,
|
||||
UninstallType.RESTORE_STOCK_IMAGE
|
||||
)
|
||||
val listOptions = options.map {
|
||||
ListOption(
|
||||
titleText = stringResource(it.title),
|
||||
subtitleText = if (it.message != 0) stringResource(it.message) else null,
|
||||
icon = IconSource(it.icon)
|
||||
)
|
||||
}
|
||||
|
||||
var selection = UninstallType.NONE
|
||||
val cardColor = if (!ThemeConfig.useDynamicColor) {
|
||||
ThemeConfig.currentTheme.ButtonContrast
|
||||
} else {
|
||||
MaterialTheme.colorScheme.surfaceContainerHigh
|
||||
}
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = {
|
||||
dismiss()
|
||||
},
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(R.string.settings_uninstall),
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
},
|
||||
text = {
|
||||
Column(
|
||||
modifier = Modifier.padding(vertical = 8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
listOptions.forEachIndexed { index, option ->
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(MaterialTheme.shapes.medium)
|
||||
.clickable {
|
||||
selection = options[index]
|
||||
}
|
||||
.padding(vertical = 12.dp, horizontal = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = options[index].icon,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier
|
||||
.padding(end = 16.dp)
|
||||
.size(24.dp)
|
||||
)
|
||||
Column {
|
||||
Text(
|
||||
text = option.titleText,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
option.subtitleText?.let {
|
||||
Text(
|
||||
text = it,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
if (selection != UninstallType.NONE) {
|
||||
onSelected(selection)
|
||||
}
|
||||
dismiss()
|
||||
}
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(android.R.string.ok),
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
dismiss()
|
||||
}
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(android.R.string.cancel),
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
},
|
||||
containerColor = cardColor,
|
||||
shape = MaterialTheme.shapes.extraLarge,
|
||||
tonalElevation = 4.dp
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun TopBar(
|
||||
scrollBehavior: TopAppBarScrollBehavior? = null
|
||||
) {
|
||||
val systemIsDark = isSystemInDarkTheme()
|
||||
val cardColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
val cardAlpha = if (ThemeConfig.customBackgroundUri != null) {
|
||||
cardAlpha
|
||||
} else {
|
||||
if (systemIsDark) 0.8f else 1f
|
||||
}
|
||||
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(R.string.settings),
|
||||
style = MaterialTheme.typography.titleLarge
|
||||
)
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = cardColor.copy(alpha = cardAlpha),
|
||||
scrolledContainerColor = cardColor.copy(alpha = cardAlpha)
|
||||
),
|
||||
windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),
|
||||
scrollBehavior = scrollBehavior
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,589 @@
|
||||
package com.sukisu.ultra.ui.screen
|
||||
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.sukisu.ultra.R
|
||||
import coil.compose.AsyncImage
|
||||
import coil.request.ImageRequest
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||
import com.ramcosta.composedestinations.generated.destinations.AppProfileScreenDestination
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
import kotlinx.coroutines.launch
|
||||
import com.sukisu.ultra.Natives
|
||||
import com.sukisu.ultra.ui.component.SearchAppBar
|
||||
import com.sukisu.ultra.ui.theme.CardConfig.cardElevation
|
||||
import com.sukisu.ultra.ui.util.ModuleModify
|
||||
import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class, ExperimentalMaterial3Api::class)
|
||||
@Destination<RootGraph>
|
||||
@Composable
|
||||
fun SuperUserScreen(navigator: DestinationsNavigator) {
|
||||
val viewModel = viewModel<SuperUserViewModel>()
|
||||
val scope = rememberCoroutineScope()
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||
val listState = rememberLazyListState()
|
||||
val context = LocalContext.current
|
||||
val snackBarHostState = remember { SnackbarHostState() }
|
||||
|
||||
// 添加备份和还原启动器
|
||||
val backupLauncher = ModuleModify.rememberAllowlistBackupLauncher(context, snackBarHostState)
|
||||
val restoreLauncher = ModuleModify.rememberAllowlistRestoreLauncher(context, snackBarHostState)
|
||||
|
||||
LaunchedEffect(key1 = navigator) {
|
||||
viewModel.search = ""
|
||||
if (viewModel.appList.isEmpty()) {
|
||||
viewModel.fetchAppList()
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(viewModel.search) {
|
||||
if (viewModel.search.isEmpty()) {
|
||||
listState.scrollToItem(0)
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
SearchAppBar(
|
||||
title = { Text(stringResource(R.string.superuser)) },
|
||||
searchText = viewModel.search,
|
||||
onSearchTextChange = { viewModel.search = it },
|
||||
onClearClick = { viewModel.search = "" },
|
||||
dropdownContent = {
|
||||
var showDropdown by remember { mutableStateOf(false) }
|
||||
|
||||
IconButton(
|
||||
onClick = { showDropdown = true },
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.MoreVert,
|
||||
contentDescription = stringResource(id = R.string.settings),
|
||||
)
|
||||
|
||||
DropdownMenu(expanded = showDropdown, onDismissRequest = {
|
||||
showDropdown = false
|
||||
}) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.refresh)) },
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Refresh,
|
||||
contentDescription = null,
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
scope.launch {
|
||||
viewModel.fetchAppList()
|
||||
}
|
||||
showDropdown = false
|
||||
}
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(
|
||||
if (viewModel.showSystemApps) {
|
||||
stringResource(R.string.hide_system_apps)
|
||||
} else {
|
||||
stringResource(R.string.show_system_apps)
|
||||
}
|
||||
)
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = if (viewModel.showSystemApps)
|
||||
Icons.Filled.VisibilityOff else Icons.Filled.Visibility,
|
||||
contentDescription = null,
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
viewModel.showSystemApps = !viewModel.showSystemApps
|
||||
showDropdown = false
|
||||
}
|
||||
)
|
||||
HorizontalDivider(thickness = 0.5.dp, modifier = Modifier.padding(vertical = 4.dp))
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.backup_allowlist)) },
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Save,
|
||||
contentDescription = null,
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
backupLauncher.launch(ModuleModify.createAllowlistBackupIntent())
|
||||
showDropdown = false
|
||||
}
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.restore_allowlist)) },
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.RestoreFromTrash,
|
||||
contentDescription = null,
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
restoreLauncher.launch(ModuleModify.createAllowlistRestoreIntent())
|
||||
showDropdown = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
scrollBehavior = scrollBehavior
|
||||
)
|
||||
},
|
||||
snackbarHost = { SnackbarHost(snackBarHostState) },
|
||||
contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),
|
||||
bottomBar = {
|
||||
AnimatedVisibility(
|
||||
visible = viewModel.showBatchActions && viewModel.selectedApps.isNotEmpty(),
|
||||
enter = slideInVertically(initialOffsetY = { it }),
|
||||
exit = slideOutVertically(targetOffsetY = { it })
|
||||
) {
|
||||
Surface(
|
||||
color = MaterialTheme.colorScheme.surfaceContainerHighest,
|
||||
tonalElevation = cardElevation,
|
||||
shadowElevation = cardElevation
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly
|
||||
) {
|
||||
OutlinedButton(
|
||||
onClick = {
|
||||
viewModel.selectedApps = emptySet()
|
||||
viewModel.showBatchActions = false
|
||||
},
|
||||
colors = ButtonDefaults.outlinedButtonColors(
|
||||
contentColor = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Close,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(6.dp))
|
||||
Text(stringResource(android.R.string.cancel))
|
||||
}
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
viewModel.updateBatchPermissions(true)
|
||||
}
|
||||
},
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Check,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(6.dp))
|
||||
Text(stringResource(R.string.batch_authorization))
|
||||
}
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
viewModel.updateBatchPermissions(false)
|
||||
}
|
||||
},
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.error
|
||||
)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Block,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(6.dp))
|
||||
Text(stringResource(R.string.batch_cancel_authorization))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
) { innerPadding ->
|
||||
PullToRefreshBox(
|
||||
modifier = Modifier.padding(innerPadding),
|
||||
onRefresh = {
|
||||
scope.launch { viewModel.fetchAppList() }
|
||||
},
|
||||
isRefreshing = viewModel.isRefreshing
|
||||
) {
|
||||
LazyColumn(
|
||||
state = listState,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
contentPadding = PaddingValues(
|
||||
top = 8.dp,
|
||||
bottom = if (viewModel.showBatchActions && viewModel.selectedApps.isNotEmpty()) 88.dp else 16.dp
|
||||
)
|
||||
) {
|
||||
// 获取分组后的应用列表
|
||||
val rootApps = viewModel.appList.filter { it.allowSu }
|
||||
val customApps = viewModel.appList.filter { !it.allowSu && it.hasCustomProfile }
|
||||
val otherApps = viewModel.appList.filter { !it.allowSu && !it.hasCustomProfile }
|
||||
|
||||
// 显示ROOT权限应用组
|
||||
if (rootApps.isNotEmpty()) {
|
||||
item {
|
||||
GroupHeader(title = stringResource(R.string.apps_with_root))
|
||||
}
|
||||
|
||||
items(rootApps, key = { "root_" + it.packageName + it.uid }) { app ->
|
||||
AppItem(
|
||||
app = app,
|
||||
isSelected = viewModel.selectedApps.contains(app.packageName),
|
||||
onToggleSelection = { viewModel.toggleAppSelection(app.packageName) },
|
||||
onSwitchChange = { allowSu ->
|
||||
scope.launch {
|
||||
val profile = Natives.getAppProfile(app.packageName, app.uid)
|
||||
val updatedProfile = profile.copy(allowSu = allowSu)
|
||||
if (Natives.setAppProfile(updatedProfile)) {
|
||||
viewModel.fetchAppList()
|
||||
}
|
||||
}
|
||||
},
|
||||
onClick = {
|
||||
if (viewModel.showBatchActions) {
|
||||
viewModel.toggleAppSelection(app.packageName)
|
||||
} else {
|
||||
navigator.navigate(AppProfileScreenDestination(app))
|
||||
}
|
||||
},
|
||||
onLongClick = {
|
||||
// 长按进入多选模式
|
||||
if (!viewModel.showBatchActions) {
|
||||
viewModel.toggleBatchMode()
|
||||
viewModel.toggleAppSelection(app.packageName)
|
||||
}
|
||||
},
|
||||
viewModel = viewModel
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 显示自定义配置应用组
|
||||
if (customApps.isNotEmpty()) {
|
||||
item {
|
||||
GroupHeader(title = stringResource(R.string.apps_with_custom_profile))
|
||||
}
|
||||
|
||||
items(customApps, key = { "custom_" + it.packageName + it.uid }) { app ->
|
||||
AppItem(
|
||||
app = app,
|
||||
isSelected = viewModel.selectedApps.contains(app.packageName),
|
||||
onToggleSelection = { viewModel.toggleAppSelection(app.packageName) },
|
||||
onSwitchChange = { allowSu ->
|
||||
scope.launch {
|
||||
val profile = Natives.getAppProfile(app.packageName, app.uid)
|
||||
val updatedProfile = profile.copy(allowSu = allowSu)
|
||||
if (Natives.setAppProfile(updatedProfile)) {
|
||||
viewModel.fetchAppList()
|
||||
}
|
||||
}
|
||||
},
|
||||
onClick = {
|
||||
if (viewModel.showBatchActions) {
|
||||
viewModel.toggleAppSelection(app.packageName)
|
||||
} else {
|
||||
navigator.navigate(AppProfileScreenDestination(app))
|
||||
}
|
||||
},
|
||||
onLongClick = {
|
||||
// 长按进入多选模式
|
||||
if (!viewModel.showBatchActions) {
|
||||
viewModel.toggleBatchMode()
|
||||
viewModel.toggleAppSelection(app.packageName)
|
||||
}
|
||||
},
|
||||
viewModel = viewModel
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 显示其他应用组
|
||||
if (otherApps.isNotEmpty()) {
|
||||
item {
|
||||
GroupHeader(title = stringResource(R.string.other_apps))
|
||||
}
|
||||
|
||||
items(otherApps, key = { "other_" + it.packageName + it.uid }) { app ->
|
||||
AppItem(
|
||||
app = app,
|
||||
isSelected = viewModel.selectedApps.contains(app.packageName),
|
||||
onToggleSelection = { viewModel.toggleAppSelection(app.packageName) },
|
||||
onSwitchChange = { allowSu ->
|
||||
scope.launch {
|
||||
val profile = Natives.getAppProfile(app.packageName, app.uid)
|
||||
val updatedProfile = profile.copy(allowSu = allowSu)
|
||||
if (Natives.setAppProfile(updatedProfile)) {
|
||||
viewModel.fetchAppList()
|
||||
}
|
||||
}
|
||||
},
|
||||
onClick = {
|
||||
if (viewModel.showBatchActions) {
|
||||
viewModel.toggleAppSelection(app.packageName)
|
||||
} else {
|
||||
navigator.navigate(AppProfileScreenDestination(app))
|
||||
}
|
||||
},
|
||||
onLongClick = {
|
||||
// 长按进入多选模式
|
||||
if (!viewModel.showBatchActions) {
|
||||
viewModel.toggleBatchMode()
|
||||
viewModel.toggleAppSelection(app.packageName)
|
||||
}
|
||||
},
|
||||
viewModel = viewModel
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 当没有应用显示时显示空状态
|
||||
if (viewModel.appList.isEmpty()) {
|
||||
item {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(400.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Apps,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.6f),
|
||||
modifier = Modifier
|
||||
.size(96.dp)
|
||||
.padding(bottom = 16.dp)
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.no_apps_found),
|
||||
textAlign = TextAlign.Center,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun GroupHeader(title: String) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(MaterialTheme.colorScheme.surfaceContainerHighest.copy(alpha = 0.7f))
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
style = TextStyle(
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
private fun AppItem(
|
||||
app: SuperUserViewModel.AppInfo,
|
||||
isSelected: Boolean,
|
||||
onToggleSelection: () -> Unit,
|
||||
onSwitchChange: (Boolean) -> Unit,
|
||||
onClick: () -> Unit,
|
||||
onLongClick: () -> Unit,
|
||||
viewModel: SuperUserViewModel
|
||||
) {
|
||||
val cardColor = if (app.allowSu)
|
||||
MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f)
|
||||
else if (app.hasCustomProfile)
|
||||
MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.3f)
|
||||
else
|
||||
MaterialTheme.colorScheme.surfaceContainerLow
|
||||
|
||||
Card(
|
||||
colors = CardDefaults.cardColors(containerColor = cardColor),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 0.dp),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 8.dp, vertical = 4.dp)
|
||||
.clip(MaterialTheme.shapes.medium)
|
||||
.shadow(
|
||||
elevation = 0.dp,
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
spotColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
|
||||
)
|
||||
.then(
|
||||
if (isSelected)
|
||||
Modifier.border(
|
||||
width = 2.dp,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
shape = MaterialTheme.shapes.medium
|
||||
)
|
||||
else
|
||||
Modifier
|
||||
)
|
||||
.pointerInput(Unit) {
|
||||
detectTapGestures(
|
||||
onLongPress = { onLongClick() },
|
||||
onTap = { onClick() }
|
||||
)
|
||||
}
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
AsyncImage(
|
||||
model = ImageRequest.Builder(LocalContext.current)
|
||||
.data(app.packageInfo)
|
||||
.crossfade(true)
|
||||
.build(),
|
||||
contentDescription = app.label,
|
||||
modifier = Modifier
|
||||
.padding(end = 16.dp)
|
||||
.size(48.dp)
|
||||
.clip(MaterialTheme.shapes.small)
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(end = 8.dp)
|
||||
) {
|
||||
Text(
|
||||
text = app.label,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
maxLines = 1,
|
||||
overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis
|
||||
)
|
||||
|
||||
Text(
|
||||
text = app.packageName,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
maxLines = 1,
|
||||
overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis
|
||||
)
|
||||
|
||||
FlowRow(
|
||||
modifier = Modifier.padding(top = 4.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
if (app.allowSu) {
|
||||
LabelText(label = "ROOT", backgroundColor = MaterialTheme.colorScheme.primary)
|
||||
}
|
||||
if (Natives.uidShouldUmount(app.uid)) {
|
||||
LabelText(label = "UMOUNT", backgroundColor = MaterialTheme.colorScheme.tertiary)
|
||||
}
|
||||
if (app.hasCustomProfile) {
|
||||
LabelText(label = "CUSTOM", backgroundColor = MaterialTheme.colorScheme.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!viewModel.showBatchActions) {
|
||||
Switch(
|
||||
checked = app.allowSu,
|
||||
onCheckedChange = onSwitchChange,
|
||||
colors = SwitchDefaults.colors(
|
||||
checkedThumbColor = MaterialTheme.colorScheme.onPrimary,
|
||||
checkedTrackColor = MaterialTheme.colorScheme.primary,
|
||||
checkedIconColor = MaterialTheme.colorScheme.primary,
|
||||
uncheckedThumbColor = MaterialTheme.colorScheme.outline,
|
||||
uncheckedTrackColor = MaterialTheme.colorScheme.surfaceVariant,
|
||||
uncheckedIconColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
)
|
||||
)
|
||||
} else {
|
||||
Checkbox(
|
||||
checked = isSelected,
|
||||
onCheckedChange = { onToggleSelection() },
|
||||
colors = CheckboxDefaults.colors(
|
||||
checkedColor = MaterialTheme.colorScheme.primary,
|
||||
uncheckedColor = MaterialTheme.colorScheme.outline
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LabelText(label: String, backgroundColor: Color) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(top = 2.dp, end = 2.dp)
|
||||
.background(
|
||||
backgroundColor,
|
||||
shape = RoundedCornerShape(4.dp)
|
||||
)
|
||||
.clip(RoundedCornerShape(4.dp))
|
||||
) {
|
||||
Text(
|
||||
text = label,
|
||||
modifier = Modifier.padding(vertical = 2.dp, horizontal = 6.dp),
|
||||
style = TextStyle(
|
||||
fontSize = 10.sp,
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
package zako.zako.zako.ui.screen
|
||||
package com.sukisu.ultra.ui.screen
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@@ -31,6 +33,7 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarColors
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||
@@ -44,11 +47,10 @@ import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.lifecycle.compose.dropUnlessResumed
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
@@ -59,9 +61,9 @@ import com.ramcosta.composedestinations.result.ResultRecipient
|
||||
import com.ramcosta.composedestinations.result.getOr
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import zako.zako.zako.R
|
||||
import zako.zako.zako.ui.theme.ThemeConfig
|
||||
import zako.zako.zako.ui.viewmodel.TemplateViewModel
|
||||
import com.sukisu.ultra.R
|
||||
import com.sukisu.ultra.ui.theme.CardConfig
|
||||
import com.sukisu.ultra.ui.viewmodel.TemplateViewModel
|
||||
|
||||
/**
|
||||
* @author weishu
|
||||
@@ -78,11 +80,6 @@ fun AppProfileTemplateScreen(
|
||||
val viewModel = viewModel<TemplateViewModel>()
|
||||
val scope = rememberCoroutineScope()
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||
val cardColor = if (!ThemeConfig.useDynamicColor) {
|
||||
ThemeConfig.currentTheme.ButtonContrast
|
||||
} else {
|
||||
MaterialTheme.colorScheme.secondaryContainer
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
if (viewModel.templateList.isEmpty()) {
|
||||
@@ -97,10 +94,13 @@ fun AppProfileTemplateScreen(
|
||||
}
|
||||
}
|
||||
|
||||
val cardColorUse = MaterialTheme.colorScheme.surfaceVariant
|
||||
val cardAlpha = CardConfig.cardAlpha
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
val clipboardManager = LocalClipboardManager.current
|
||||
val context = LocalContext.current
|
||||
val clipboardManager = context.getSystemService<ClipboardManager>()
|
||||
val showToast = fun(msg: String) {
|
||||
scope.launch(Dispatchers.Main) {
|
||||
Toast.makeText(context, msg, Toast.LENGTH_SHORT).show()
|
||||
@@ -108,25 +108,29 @@ fun AppProfileTemplateScreen(
|
||||
}
|
||||
TopBar(
|
||||
onBack = dropUnlessResumed { navigator.popBackStack() },
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = cardColorUse.copy(alpha = cardAlpha),
|
||||
scrolledContainerColor = cardColorUse.copy(alpha = cardAlpha)
|
||||
),
|
||||
onSync = {
|
||||
scope.launch { viewModel.fetchTemplates(true) }
|
||||
},
|
||||
onImport = {
|
||||
clipboardManager.getText()?.text?.let {
|
||||
if (it.isEmpty()) {
|
||||
showToast(context.getString(R.string.app_profile_template_import_empty))
|
||||
return@let
|
||||
}
|
||||
scope.launch {
|
||||
val clipboardText = clipboardManager?.primaryClip?.getItemAt(0)?.text?.toString()
|
||||
if (clipboardText.isNullOrEmpty()) {
|
||||
showToast(context.getString(R.string.app_profile_template_import_empty))
|
||||
return@launch
|
||||
}
|
||||
viewModel.importTemplates(
|
||||
it, {
|
||||
clipboardText,
|
||||
{
|
||||
showToast(context.getString(R.string.app_profile_template_import_success))
|
||||
viewModel.fetchTemplates(false)
|
||||
},
|
||||
showToast
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
onExport = {
|
||||
scope.launch {
|
||||
@@ -134,8 +138,8 @@ fun AppProfileTemplateScreen(
|
||||
{
|
||||
showToast(context.getString(R.string.app_profile_template_export_empty))
|
||||
}
|
||||
) {
|
||||
clipboardManager.setText(AnnotatedString(it))
|
||||
) { text ->
|
||||
clipboardManager?.setPrimaryClip(ClipData.newPlainText("", text))
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -154,7 +158,6 @@ fun AppProfileTemplateScreen(
|
||||
},
|
||||
icon = { Icon(Icons.Filled.Add, null) },
|
||||
text = { Text(stringResource(id = R.string.app_profile_template_create)) },
|
||||
containerColor = cardColor.copy(alpha = 1f),
|
||||
contentColor = MaterialTheme.colorScheme.onSecondaryContainer
|
||||
)
|
||||
},
|
||||
@@ -204,17 +207,17 @@ private fun TemplateItem(
|
||||
)
|
||||
Text(template.description)
|
||||
FlowRow {
|
||||
LabelText(label = "UID: ${template.uid}")
|
||||
LabelText(label = "GID: ${template.gid}")
|
||||
LabelText(label = template.context)
|
||||
LabelText(label = "UID: ${template.uid}", backgroundColor = MaterialTheme.colorScheme.surface)
|
||||
LabelText(label = "GID: ${template.gid}", backgroundColor = MaterialTheme.colorScheme.surface)
|
||||
LabelText(label = template.context, backgroundColor = MaterialTheme.colorScheme.surface)
|
||||
if (template.local) {
|
||||
LabelText(label = "local")
|
||||
LabelText(label = "local", backgroundColor = MaterialTheme.colorScheme.surface)
|
||||
} else {
|
||||
LabelText(label = "remote")
|
||||
LabelText(label = "remote", backgroundColor = MaterialTheme.colorScheme.surface)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -225,12 +228,20 @@ private fun TopBar(
|
||||
onSync: () -> Unit = {},
|
||||
onImport: () -> Unit = {},
|
||||
onExport: () -> Unit = {},
|
||||
colors: TopAppBarColors,
|
||||
scrollBehavior: TopAppBarScrollBehavior? = null
|
||||
) {
|
||||
val cardColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
val cardAlpha = CardConfig.cardAlpha
|
||||
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(stringResource(R.string.settings_profile_template))
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = cardColor.copy(alpha = cardAlpha),
|
||||
scrolledContainerColor = cardColor.copy(alpha = cardAlpha)
|
||||
),
|
||||
navigationIcon = {
|
||||
IconButton(
|
||||
onClick = onBack
|
||||
@@ -1,4 +1,4 @@
|
||||
package zako.zako.zako.ui.screen
|
||||
package com.sukisu.ultra.ui.screen
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.activity.compose.BackHandler
|
||||
@@ -47,14 +47,14 @@ import androidx.compose.ui.text.input.KeyboardType
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||
import com.ramcosta.composedestinations.result.ResultBackNavigator
|
||||
import zako.zako.zako.Natives
|
||||
import zako.zako.zako.R
|
||||
import zako.zako.zako.ui.component.profile.RootProfileConfig
|
||||
import zako.zako.zako.ui.util.deleteAppProfileTemplate
|
||||
import zako.zako.zako.ui.util.getAppProfileTemplate
|
||||
import zako.zako.zako.ui.util.setAppProfileTemplate
|
||||
import zako.zako.zako.ui.viewmodel.TemplateViewModel
|
||||
import zako.zako.zako.ui.viewmodel.toJSON
|
||||
import com.sukisu.ultra.Natives
|
||||
import com.sukisu.ultra.R
|
||||
import com.sukisu.ultra.ui.component.profile.RootProfileConfig
|
||||
import com.sukisu.ultra.ui.util.deleteAppProfileTemplate
|
||||
import com.sukisu.ultra.ui.util.getAppProfileTemplate
|
||||
import com.sukisu.ultra.ui.util.setAppProfileTemplate
|
||||
import com.sukisu.ultra.ui.viewmodel.TemplateViewModel
|
||||
import com.sukisu.ultra.ui.viewmodel.toJSON
|
||||
import androidx.lifecycle.compose.dropUnlessResumed
|
||||
|
||||
/**
|
||||
@@ -0,0 +1,107 @@
|
||||
package com.sukisu.ultra.ui.theme
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.luminance
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
object CardConfig {
|
||||
val settingElevation: Dp = 4.dp
|
||||
val customBackgroundElevation: Dp = 0.dp
|
||||
|
||||
var cardAlpha by mutableStateOf(1f)
|
||||
var cardElevation by mutableStateOf(settingElevation)
|
||||
var isShadowEnabled by mutableStateOf(true)
|
||||
var isCustomAlphaSet by mutableStateOf(false)
|
||||
var isUserDarkModeEnabled by mutableStateOf(false)
|
||||
var isUserLightModeEnabled by mutableStateOf(false)
|
||||
var isCustomBackgroundEnabled by mutableStateOf(false)
|
||||
|
||||
/**
|
||||
* 保存卡片配置到SharedPreferences
|
||||
*/
|
||||
fun save(context: Context) {
|
||||
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||
prefs.edit().apply {
|
||||
putFloat("card_alpha", cardAlpha)
|
||||
putBoolean("custom_background_enabled", isCustomBackgroundEnabled)
|
||||
putBoolean("is_shadow_enabled", isShadowEnabled)
|
||||
putBoolean("is_custom_alpha_set", isCustomAlphaSet)
|
||||
putBoolean("is_user_dark_mode_enabled", isUserDarkModeEnabled)
|
||||
putBoolean("is_user_light_mode_enabled", isUserLightModeEnabled)
|
||||
apply()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从SharedPreferences加载卡片配置
|
||||
*/
|
||||
fun load(context: Context) {
|
||||
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||
cardAlpha = prefs.getFloat("card_alpha", 1f)
|
||||
isCustomBackgroundEnabled = prefs.getBoolean("custom_background_enabled", false)
|
||||
isShadowEnabled = prefs.getBoolean("is_shadow_enabled", true)
|
||||
isCustomAlphaSet = prefs.getBoolean("is_custom_alpha_set", false)
|
||||
isUserDarkModeEnabled = prefs.getBoolean("is_user_dark_mode_enabled", false)
|
||||
isUserLightModeEnabled = prefs.getBoolean("is_user_light_mode_enabled", false)
|
||||
updateShadowEnabled(isShadowEnabled)
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新阴影启用状态
|
||||
*/
|
||||
fun updateShadowEnabled(enabled: Boolean) {
|
||||
isShadowEnabled = enabled
|
||||
cardElevation = if (isCustomBackgroundEnabled && cardAlpha != 1f) {
|
||||
customBackgroundElevation
|
||||
} else if (enabled) {
|
||||
settingElevation
|
||||
} else {
|
||||
customBackgroundElevation
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置深色模式默认值
|
||||
*/
|
||||
fun setDarkModeDefaults() {
|
||||
if (!isCustomAlphaSet) {
|
||||
cardAlpha = 1f
|
||||
}
|
||||
updateShadowEnabled(isShadowEnabled)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取卡片颜色配置
|
||||
*/
|
||||
@Composable
|
||||
fun getCardColors(originalColor: Color) = CardDefaults.cardColors(
|
||||
containerColor = originalColor.copy(alpha = CardConfig.cardAlpha),
|
||||
contentColor = determineContentColor(originalColor)
|
||||
)
|
||||
|
||||
/**
|
||||
* 根据背景颜色、主题模式和用户设置确定内容颜色
|
||||
*/
|
||||
@Composable
|
||||
private fun determineContentColor(originalColor: Color): Color {
|
||||
val isDarkTheme = isSystemInDarkTheme()
|
||||
if (ThemeConfig.isThemeChanging) {
|
||||
return if (isDarkTheme) Color.White else Color.Black
|
||||
}
|
||||
|
||||
return when {
|
||||
CardConfig.isUserLightModeEnabled -> Color.Black
|
||||
!isDarkTheme && originalColor.luminance() > 0.5f -> Color.Black
|
||||
isDarkTheme -> Color.White
|
||||
else -> if (originalColor.luminance() > 0.5f) Color.Black else Color.White
|
||||
}
|
||||
}
|
||||
273
manager/app/src/main/java/com/sukisu/ultra/ui/theme/Color.kt
Normal file
273
manager/app/src/main/java/com/sukisu/ultra/ui/theme/Color.kt
Normal file
@@ -0,0 +1,273 @@
|
||||
package com.sukisu.ultra.ui.theme
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
sealed class ThemeColors {
|
||||
abstract val Primary: Color
|
||||
abstract val Secondary: Color
|
||||
abstract val Tertiary: Color
|
||||
abstract val OnPrimary: Color
|
||||
abstract val OnSecondary: Color
|
||||
abstract val OnTertiary: Color
|
||||
abstract val PrimaryContainer: Color
|
||||
abstract val SecondaryContainer: Color
|
||||
abstract val TertiaryContainer: Color
|
||||
abstract val OnPrimaryContainer: Color
|
||||
abstract val OnSecondaryContainer: Color
|
||||
abstract val OnTertiaryContainer: Color
|
||||
abstract val ButtonContrast: Color
|
||||
|
||||
// 表面颜色
|
||||
abstract val Surface: Color
|
||||
abstract val SurfaceVariant: Color
|
||||
abstract val OnSurface: Color
|
||||
abstract val OnSurfaceVariant: Color
|
||||
|
||||
// 错误状态颜色
|
||||
abstract val Error: Color
|
||||
abstract val OnError: Color
|
||||
abstract val ErrorContainer: Color
|
||||
abstract val OnErrorContainer: Color
|
||||
|
||||
// 边框和背景色
|
||||
abstract val Outline: Color
|
||||
abstract val OutlineVariant: Color
|
||||
abstract val Background: Color
|
||||
abstract val OnBackground: Color
|
||||
|
||||
// 默认主题 (蓝色)
|
||||
object Default : ThemeColors() {
|
||||
override val Primary = Color(0xFF2196F3)
|
||||
override val Secondary = Color(0xFF64B5F6)
|
||||
override val Tertiary = Color(0xFF0D47A1)
|
||||
override val OnPrimary = Color(0xFFFFFFFF)
|
||||
override val OnSecondary = Color(0xFFFFFFFF)
|
||||
override val OnTertiary = Color(0xFFFFFFFF)
|
||||
override val PrimaryContainer = Color(0xFFD6EAFF)
|
||||
override val SecondaryContainer = Color(0xFFE3F2FD)
|
||||
override val TertiaryContainer = Color(0xFFCFD8DC)
|
||||
override val OnPrimaryContainer = Color(0xFF0A3049)
|
||||
override val OnSecondaryContainer = Color(0xFF0D3C61)
|
||||
override val OnTertiaryContainer = Color(0xFF071D41)
|
||||
override val ButtonContrast = Color(0xFF2196F3)
|
||||
|
||||
override val Surface = Color(0xFFF5F9FF)
|
||||
override val SurfaceVariant = Color(0xFFEDF5FE)
|
||||
override val OnSurface = Color(0xFF1A1C1E)
|
||||
override val OnSurfaceVariant = Color(0xFF42474E)
|
||||
|
||||
override val Error = Color(0xFFB00020)
|
||||
override val OnError = Color(0xFFFFFFFF)
|
||||
override val ErrorContainer = Color(0xFFFDE7E9)
|
||||
override val OnErrorContainer = Color(0xFF410008)
|
||||
|
||||
override val Outline = Color(0xFFBAC3CF)
|
||||
override val OutlineVariant = Color(0xFFDFE3EB)
|
||||
override val Background = Color(0xFFFAFCFF)
|
||||
override val OnBackground = Color(0xFF1A1C1E)
|
||||
}
|
||||
|
||||
// 绿色主题
|
||||
object Green : ThemeColors() {
|
||||
override val Primary = Color(0xFF43A047)
|
||||
override val Secondary = Color(0xFF66BB6A)
|
||||
override val Tertiary = Color(0xFF1B5E20)
|
||||
override val OnPrimary = Color(0xFFFFFFFF)
|
||||
override val OnSecondary = Color(0xFFFFFFFF)
|
||||
override val OnTertiary = Color(0xFFFFFFFF)
|
||||
override val PrimaryContainer = Color(0xFFD8EFDB)
|
||||
override val SecondaryContainer = Color(0xFFE8F5E9)
|
||||
override val TertiaryContainer = Color(0xFFB9F6CA)
|
||||
override val OnPrimaryContainer = Color(0xFF0A280D)
|
||||
override val OnSecondaryContainer = Color(0xFF0E2912)
|
||||
override val OnTertiaryContainer = Color(0xFF051B07)
|
||||
override val ButtonContrast = Color(0xFF43A047)
|
||||
|
||||
override val Surface = Color(0xFFF6FBF6)
|
||||
override val SurfaceVariant = Color(0xFFEDF7EE)
|
||||
override val OnSurface = Color(0xFF191C19)
|
||||
override val OnSurfaceVariant = Color(0xFF414941)
|
||||
|
||||
override val Error = Color(0xFFC62828)
|
||||
override val OnError = Color(0xFFFFFFFF)
|
||||
override val ErrorContainer = Color(0xFFF8D7DA)
|
||||
override val OnErrorContainer = Color(0xFF4A0808)
|
||||
|
||||
override val Outline = Color(0xFFBDC9BF)
|
||||
override val OutlineVariant = Color(0xFFDDE6DE)
|
||||
override val Background = Color(0xFFFBFDFB)
|
||||
override val OnBackground = Color(0xFF191C19)
|
||||
}
|
||||
|
||||
// 紫色主题
|
||||
object Purple : ThemeColors() {
|
||||
override val Primary = Color(0xFF9C27B0)
|
||||
override val Secondary = Color(0xFFBA68C8)
|
||||
override val Tertiary = Color(0xFF6A1B9A)
|
||||
override val OnPrimary = Color(0xFFFFFFFF)
|
||||
override val OnSecondary = Color(0xFFFFFFFF)
|
||||
override val OnTertiary = Color(0xFFFFFFFF)
|
||||
override val PrimaryContainer = Color(0xFFF3D8F8)
|
||||
override val SecondaryContainer = Color(0xFFF5E9F7)
|
||||
override val TertiaryContainer = Color(0xFFE1BEE7)
|
||||
override val OnPrimaryContainer = Color(0xFF2A0934)
|
||||
override val OnSecondaryContainer = Color(0xFF3C0F50)
|
||||
override val OnTertiaryContainer = Color(0xFF1D0830)
|
||||
override val ButtonContrast = Color(0xFF9C27B0)
|
||||
|
||||
override val Surface = Color(0xFFFCF6FF)
|
||||
override val SurfaceVariant = Color(0xFFF5EEFA)
|
||||
override val OnSurface = Color(0xFF1D1B1E)
|
||||
override val OnSurfaceVariant = Color(0xFF49454E)
|
||||
|
||||
override val Error = Color(0xFFD50000)
|
||||
override val OnError = Color(0xFFFFFFFF)
|
||||
override val ErrorContainer = Color(0xFFFFDCD5)
|
||||
override val OnErrorContainer = Color(0xFF480000)
|
||||
|
||||
override val Outline = Color(0xFFC9B9D0)
|
||||
override val OutlineVariant = Color(0xFFE8DAED)
|
||||
override val Background = Color(0xFFFFFBFF)
|
||||
override val OnBackground = Color(0xFF1D1B1E)
|
||||
}
|
||||
|
||||
// 橙色主题
|
||||
object Orange : ThemeColors() {
|
||||
override val Primary = Color(0xFFFF9800)
|
||||
override val Secondary = Color(0xFFFFB74D)
|
||||
override val Tertiary = Color(0xFFE65100)
|
||||
override val OnPrimary = Color(0xFFFFFFFF)
|
||||
override val OnSecondary = Color(0xFF000000)
|
||||
override val OnTertiary = Color(0xFFFFFFFF)
|
||||
override val PrimaryContainer = Color(0xFFFFECCC)
|
||||
override val SecondaryContainer = Color(0xFFFFF0D9)
|
||||
override val TertiaryContainer = Color(0xFFFFD180)
|
||||
override val OnPrimaryContainer = Color(0xFF351F00)
|
||||
override val OnSecondaryContainer = Color(0xFF3D2800)
|
||||
override val OnTertiaryContainer = Color(0xFF2E1500)
|
||||
override val ButtonContrast = Color(0xFFFF9800)
|
||||
|
||||
override val Surface = Color(0xFFFFF8F3)
|
||||
override val SurfaceVariant = Color(0xFFFFF0E6)
|
||||
override val OnSurface = Color(0xFF1F1B16)
|
||||
override val OnSurfaceVariant = Color(0xFF4E4639)
|
||||
|
||||
override val Error = Color(0xFFD32F2F)
|
||||
override val OnError = Color(0xFFFFFFFF)
|
||||
override val ErrorContainer = Color(0xFFFFDBC8)
|
||||
override val OnErrorContainer = Color(0xFF490700)
|
||||
|
||||
override val Outline = Color(0xFFD6C3AD)
|
||||
override val OutlineVariant = Color(0xFFEFDFCC)
|
||||
override val Background = Color(0xFFFFFBFF)
|
||||
override val OnBackground = Color(0xFF1F1B16)
|
||||
}
|
||||
|
||||
// 粉色主题
|
||||
object Pink : ThemeColors() {
|
||||
override val Primary = Color(0xFFE91E63)
|
||||
override val Secondary = Color(0xFFF06292)
|
||||
override val Tertiary = Color(0xFF880E4F)
|
||||
override val OnPrimary = Color(0xFFFFFFFF)
|
||||
override val OnSecondary = Color(0xFFFFFFFF)
|
||||
override val OnTertiary = Color(0xFFFFFFFF)
|
||||
override val PrimaryContainer = Color(0xFFFCE4EC)
|
||||
override val SecondaryContainer = Color(0xFFFCE4EC)
|
||||
override val TertiaryContainer = Color(0xFFF8BBD0)
|
||||
override val OnPrimaryContainer = Color(0xFF3B0819)
|
||||
override val OnSecondaryContainer = Color(0xFF3B0819)
|
||||
override val OnTertiaryContainer = Color(0xFF2B0516)
|
||||
override val ButtonContrast = Color(0xFFE91E63)
|
||||
|
||||
override val Surface = Color(0xFFFFF7F9)
|
||||
override val SurfaceVariant = Color(0xFFFCEEF2)
|
||||
override val OnSurface = Color(0xFF201A1C)
|
||||
override val OnSurfaceVariant = Color(0xFF534347)
|
||||
|
||||
override val Error = Color(0xFFB71C1C)
|
||||
override val OnError = Color(0xFFFFFFFF)
|
||||
override val ErrorContainer = Color(0xFFFFDAD6)
|
||||
override val OnErrorContainer = Color(0xFF410002)
|
||||
|
||||
override val Outline = Color(0xFFD6BABF)
|
||||
override val OutlineVariant = Color(0xFFEFDDE0)
|
||||
override val Background = Color(0xFFFFFBFF)
|
||||
override val OnBackground = Color(0xFF201A1C)
|
||||
}
|
||||
|
||||
// 灰色主题
|
||||
object Gray : ThemeColors() {
|
||||
override val Primary = Color(0xFF607D8B)
|
||||
override val Secondary = Color(0xFF90A4AE)
|
||||
override val Tertiary = Color(0xFF455A64)
|
||||
override val OnPrimary = Color(0xFFFFFFFF)
|
||||
override val OnSecondary = Color(0xFFFFFFFF)
|
||||
override val OnTertiary = Color(0xFFFFFFFF)
|
||||
override val PrimaryContainer = Color(0xFFECEFF1)
|
||||
override val SecondaryContainer = Color(0xFFECEFF1)
|
||||
override val TertiaryContainer = Color(0xFFCFD8DC)
|
||||
override val OnPrimaryContainer = Color(0xFF1A2327)
|
||||
override val OnSecondaryContainer = Color(0xFF1A2327)
|
||||
override val OnTertiaryContainer = Color(0xFF121A1D)
|
||||
override val ButtonContrast = Color(0xFF607D8B)
|
||||
|
||||
override val Surface = Color(0xFFF6F9FB)
|
||||
override val SurfaceVariant = Color(0xFFEEF2F4)
|
||||
override val OnSurface = Color(0xFF191C1E)
|
||||
override val OnSurfaceVariant = Color(0xFF41484D)
|
||||
|
||||
override val Error = Color(0xFFC62828)
|
||||
override val OnError = Color(0xFFFFFFFF)
|
||||
override val ErrorContainer = Color(0xFFFFDAD6)
|
||||
override val OnErrorContainer = Color(0xFF410002)
|
||||
|
||||
override val Outline = Color(0xFFBDC1C4)
|
||||
override val OutlineVariant = Color(0xFFDDE1E3)
|
||||
override val Background = Color(0xFFFBFCFE)
|
||||
override val OnBackground = Color(0xFF191C1E)
|
||||
}
|
||||
|
||||
// 黄色主题
|
||||
object Yellow : ThemeColors() {
|
||||
override val Primary = Color(0xFFFFC107)
|
||||
override val Secondary = Color(0xFFFFD54F)
|
||||
override val Tertiary = Color(0xFFFF8F00)
|
||||
override val OnPrimary = Color(0xFF000000)
|
||||
override val OnSecondary = Color(0xFF000000)
|
||||
override val OnTertiary = Color(0xFFFFFFFF)
|
||||
override val PrimaryContainer = Color(0xFFFFF8E1)
|
||||
override val SecondaryContainer = Color(0xFFFFF8E1)
|
||||
override val TertiaryContainer = Color(0xFFFFECB3)
|
||||
override val OnPrimaryContainer = Color(0xFF332A00)
|
||||
override val OnSecondaryContainer = Color(0xFF332A00)
|
||||
override val OnTertiaryContainer = Color(0xFF221200)
|
||||
override val ButtonContrast = Color(0xFFFFC107)
|
||||
|
||||
override val Surface = Color(0xFFFFFAF3)
|
||||
override val SurfaceVariant = Color(0xFFFFF7E6)
|
||||
override val OnSurface = Color(0xFF1F1C17)
|
||||
override val OnSurfaceVariant = Color(0xFF4E4A3C)
|
||||
|
||||
override val Error = Color(0xFFB71C1C)
|
||||
override val OnError = Color(0xFFFFFFFF)
|
||||
override val ErrorContainer = Color(0xFFFFDAD6)
|
||||
override val OnErrorContainer = Color(0xFF410002)
|
||||
|
||||
override val Outline = Color(0xFFD1C8AF)
|
||||
override val OutlineVariant = Color(0xFFEEE8D7)
|
||||
override val Background = Color(0xFFFFFCF8)
|
||||
override val OnBackground = Color(0xFF1F1C17)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun fromName(name: String): ThemeColors = when (name.lowercase()) {
|
||||
"green" -> Green
|
||||
"purple" -> Purple
|
||||
"orange" -> Orange
|
||||
"pink" -> Pink
|
||||
"gray" -> Gray
|
||||
"yellow" -> Yellow
|
||||
else -> Default
|
||||
}
|
||||
}
|
||||
}
|
||||
538
manager/app/src/main/java/com/sukisu/ultra/ui/theme/Theme.kt
Normal file
538
manager/app/src/main/java/com/sukisu/ultra/ui/theme/Theme.kt
Normal file
@@ -0,0 +1,538 @@
|
||||
package com.sukisu.ultra.ui.theme
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.compose.animation.core.animateFloat
|
||||
import androidx.compose.animation.core.spring
|
||||
import androidx.compose.animation.core.updateTransition
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.dynamicDarkColorScheme
|
||||
import androidx.compose.material3.dynamicLightColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.draw.paint
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.zIndex
|
||||
import coil.compose.AsyncImagePainter
|
||||
import coil.compose.rememberAsyncImagePainter
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.unit.dp
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.InputStream
|
||||
import androidx.core.content.edit
|
||||
import androidx.core.net.toUri
|
||||
import com.sukisu.ultra.ui.util.BackgroundTransformation
|
||||
import com.sukisu.ultra.ui.util.saveTransformedBackground
|
||||
|
||||
/**
|
||||
* 主题配置对象,管理应用的主题相关状态
|
||||
*/
|
||||
object ThemeConfig {
|
||||
var customBackgroundUri by mutableStateOf<Uri?>(null)
|
||||
var forceDarkMode by mutableStateOf<Boolean?>(null)
|
||||
var currentTheme by mutableStateOf<ThemeColors>(ThemeColors.Default)
|
||||
var useDynamicColor by mutableStateOf(false)
|
||||
var backgroundImageLoaded by mutableStateOf(false)
|
||||
var needsResetOnThemeChange by mutableStateOf(false)
|
||||
var isThemeChanging by mutableStateOf(false)
|
||||
var preventBackgroundRefresh by mutableStateOf(false)
|
||||
|
||||
private var lastDarkModeState: Boolean? = null
|
||||
fun detectThemeChange(currentDarkMode: Boolean): Boolean {
|
||||
val isChanged = lastDarkModeState != null && lastDarkModeState != currentDarkMode
|
||||
lastDarkModeState = currentDarkMode
|
||||
return isChanged
|
||||
}
|
||||
|
||||
fun resetBackgroundState() {
|
||||
if (!preventBackgroundRefresh) {
|
||||
backgroundImageLoaded = false
|
||||
}
|
||||
isThemeChanging = true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用主题
|
||||
*/
|
||||
@Composable
|
||||
fun KernelSUTheme(
|
||||
darkTheme: Boolean = when(ThemeConfig.forceDarkMode) {
|
||||
true -> true
|
||||
false -> false
|
||||
null -> isSystemInDarkTheme()
|
||||
},
|
||||
dynamicColor: Boolean = ThemeConfig.useDynamicColor,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val systemIsDark = isSystemInDarkTheme()
|
||||
|
||||
// 检测系统主题变化并保存状态
|
||||
val themeChanged = ThemeConfig.detectThemeChange(systemIsDark)
|
||||
LaunchedEffect(systemIsDark, themeChanged) {
|
||||
if (ThemeConfig.forceDarkMode == null && themeChanged) {
|
||||
Log.d("ThemeSystem", "系统主题变化检测: 从 ${!systemIsDark} 变为 $systemIsDark")
|
||||
ThemeConfig.resetBackgroundState()
|
||||
|
||||
if (!ThemeConfig.preventBackgroundRefresh) {
|
||||
context.loadCustomBackground()
|
||||
}
|
||||
|
||||
CardConfig.apply {
|
||||
load(context)
|
||||
if (!isCustomAlphaSet) {
|
||||
cardAlpha = if (systemIsDark) 0.50f else 1f
|
||||
}
|
||||
save(context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 初始加载配置
|
||||
LaunchedEffect(Unit) {
|
||||
context.loadThemeMode()
|
||||
context.loadThemeColors()
|
||||
context.loadDynamicColorState()
|
||||
CardConfig.load(context)
|
||||
|
||||
if (!ThemeConfig.backgroundImageLoaded && !ThemeConfig.preventBackgroundRefresh) {
|
||||
context.loadCustomBackground()
|
||||
ThemeConfig.backgroundImageLoaded = false
|
||||
}
|
||||
|
||||
ThemeConfig.preventBackgroundRefresh = context.getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
||||
.getBoolean("prevent_background_refresh", true)
|
||||
}
|
||||
|
||||
// 创建颜色方案
|
||||
val colorScheme = when {
|
||||
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
||||
if (darkTheme) createDynamicDarkColorScheme(context) else createDynamicLightColorScheme(context)
|
||||
}
|
||||
darkTheme -> createDarkColorScheme()
|
||||
else -> createLightColorScheme()
|
||||
}
|
||||
|
||||
// 根据暗色模式和自定义背景调整卡片配置
|
||||
val isDarkModeWithCustomBackground = darkTheme && ThemeConfig.customBackgroundUri != null
|
||||
if (darkTheme && !dynamicColor) {
|
||||
CardConfig.setDarkModeDefaults()
|
||||
}
|
||||
CardConfig.updateShadowEnabled(!isDarkModeWithCustomBackground)
|
||||
|
||||
val backgroundUri = rememberSaveable { mutableStateOf(ThemeConfig.customBackgroundUri) }
|
||||
|
||||
LaunchedEffect(ThemeConfig.customBackgroundUri) {
|
||||
backgroundUri.value = ThemeConfig.customBackgroundUri
|
||||
}
|
||||
|
||||
val bgImagePainter = backgroundUri.value?.let {
|
||||
rememberAsyncImagePainter(
|
||||
model = it,
|
||||
onError = {
|
||||
Log.e("ThemeSystem", "背景图加载失败: ${it.result.throwable.message}")
|
||||
ThemeConfig.customBackgroundUri = null
|
||||
context.saveCustomBackground(null)
|
||||
},
|
||||
onSuccess = {
|
||||
Log.d("ThemeSystem", "背景图加载成功")
|
||||
ThemeConfig.backgroundImageLoaded = true
|
||||
ThemeConfig.isThemeChanging = false
|
||||
|
||||
ThemeConfig.preventBackgroundRefresh = true
|
||||
context.getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
||||
.edit { putBoolean("prevent_background_refresh", true) }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
val transition = updateTransition(
|
||||
targetState = ThemeConfig.backgroundImageLoaded,
|
||||
label = "bgTransition"
|
||||
)
|
||||
val bgAlpha by transition.animateFloat(
|
||||
label = "bgAlpha",
|
||||
transitionSpec = {
|
||||
spring(
|
||||
dampingRatio = 0.8f,
|
||||
stiffness = 300f
|
||||
)
|
||||
}
|
||||
) { loaded -> if (loaded) 1f else 0f }
|
||||
|
||||
DisposableEffect(systemIsDark) {
|
||||
onDispose {
|
||||
if (ThemeConfig.isThemeChanging) {
|
||||
ThemeConfig.isThemeChanging = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MaterialTheme(
|
||||
colorScheme = colorScheme,
|
||||
typography = Typography
|
||||
) {
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.zIndex(-2f)
|
||||
.background(if (darkTheme) Color.Black else Color.White)
|
||||
)
|
||||
|
||||
// 自定义背景层
|
||||
backgroundUri.value?.let { uri ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.zIndex(-1f)
|
||||
.alpha(bgAlpha)
|
||||
) {
|
||||
// 背景图片
|
||||
bgImagePainter?.let { painter ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.paint(
|
||||
painter = painter,
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
.graphicsLayer {
|
||||
alpha = (painter.state as? AsyncImagePainter.State.Success)?.let { 1f } ?: 0f
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// 亮度调节层
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(
|
||||
if (darkTheme) Color.Black.copy(alpha = 0.6f)
|
||||
else Color.White.copy(alpha = 0.1f)
|
||||
)
|
||||
)
|
||||
|
||||
// 边缘渐变遮罩
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(
|
||||
Brush.radialGradient(
|
||||
colors = listOf(
|
||||
Color.Transparent,
|
||||
if (darkTheme) Color.Black.copy(alpha = 0.5f)
|
||||
else Color.Black.copy(alpha = 0.2f)
|
||||
),
|
||||
radius = 1200f
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 内容层
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.zIndex(1f)
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建动态深色颜色方案
|
||||
*/
|
||||
@RequiresApi(Build.VERSION_CODES.S)
|
||||
@Composable
|
||||
private fun createDynamicDarkColorScheme(context: Context) =
|
||||
dynamicDarkColorScheme(context).copy(
|
||||
background = Color.Transparent,
|
||||
surface = Color.Transparent,
|
||||
onBackground = Color.White,
|
||||
onSurface = Color.White
|
||||
)
|
||||
|
||||
/**
|
||||
* 创建动态浅色颜色方案
|
||||
*/
|
||||
@RequiresApi(Build.VERSION_CODES.S)
|
||||
@Composable
|
||||
private fun createDynamicLightColorScheme(context: Context) =
|
||||
dynamicLightColorScheme(context).copy(
|
||||
background = Color.Transparent,
|
||||
surface = Color.Transparent
|
||||
)
|
||||
|
||||
/**
|
||||
* 创建深色颜色方案
|
||||
*/
|
||||
@Composable
|
||||
private fun createDarkColorScheme() = darkColorScheme(
|
||||
primary = ThemeConfig.currentTheme.Primary.copy(alpha = 0.8f),
|
||||
onPrimary = Color.White,
|
||||
primaryContainer = ThemeConfig.currentTheme.PrimaryContainer.copy(alpha = 0.15f),
|
||||
onPrimaryContainer = Color.White,
|
||||
secondary = ThemeConfig.currentTheme.Secondary.copy(alpha = 0.8f),
|
||||
onSecondary = Color.White,
|
||||
secondaryContainer = ThemeConfig.currentTheme.SecondaryContainer.copy(alpha = 0.15f),
|
||||
onSecondaryContainer = Color.White,
|
||||
tertiary = ThemeConfig.currentTheme.Tertiary.copy(alpha = 0.8f),
|
||||
onTertiary = Color.White,
|
||||
tertiaryContainer = ThemeConfig.currentTheme.TertiaryContainer.copy(alpha = 0.15f),
|
||||
onTertiaryContainer = Color.White,
|
||||
background = Color.Transparent,
|
||||
surface = Color.Transparent,
|
||||
onBackground = Color.White,
|
||||
onSurface = Color.White,
|
||||
surfaceVariant = Color(0xFF2F2F2F),
|
||||
onSurfaceVariant = Color.White.copy(alpha = 0.7f),
|
||||
outline = Color.White.copy(alpha = 0.12f),
|
||||
outlineVariant = Color.White.copy(alpha = 0.12f),
|
||||
error = ThemeConfig.currentTheme.Error,
|
||||
onError = ThemeConfig.currentTheme.OnError,
|
||||
errorContainer = ThemeConfig.currentTheme.ErrorContainer.copy(alpha = 0.15f),
|
||||
onErrorContainer = Color.White
|
||||
)
|
||||
|
||||
/**
|
||||
* 创建浅色颜色方案
|
||||
*/
|
||||
@Composable
|
||||
private fun createLightColorScheme() = lightColorScheme(
|
||||
primary = ThemeConfig.currentTheme.Primary,
|
||||
onPrimary = ThemeConfig.currentTheme.OnPrimary,
|
||||
primaryContainer = ThemeConfig.currentTheme.PrimaryContainer,
|
||||
onPrimaryContainer = ThemeConfig.currentTheme.OnPrimaryContainer,
|
||||
secondary = ThemeConfig.currentTheme.Secondary,
|
||||
onSecondary = ThemeConfig.currentTheme.OnSecondary,
|
||||
secondaryContainer = ThemeConfig.currentTheme.SecondaryContainer,
|
||||
onSecondaryContainer = ThemeConfig.currentTheme.OnSecondaryContainer,
|
||||
tertiary = ThemeConfig.currentTheme.Tertiary,
|
||||
onTertiary = ThemeConfig.currentTheme.OnTertiary,
|
||||
tertiaryContainer = ThemeConfig.currentTheme.TertiaryContainer,
|
||||
onTertiaryContainer = ThemeConfig.currentTheme.OnTertiaryContainer,
|
||||
background = Color.Transparent,
|
||||
surface = Color.Transparent,
|
||||
onBackground = Color.Black.copy(alpha = 0.87f),
|
||||
onSurface = Color.Black.copy(alpha = 0.87f),
|
||||
surfaceVariant = Color(0xFFF5F5F5),
|
||||
onSurfaceVariant = Color.Black.copy(alpha = 0.78f),
|
||||
outline = Color.Black.copy(alpha = 0.12f),
|
||||
outlineVariant = Color.Black.copy(alpha = 0.12f),
|
||||
error = ThemeConfig.currentTheme.Error,
|
||||
onError = ThemeConfig.currentTheme.OnError,
|
||||
errorContainer = ThemeConfig.currentTheme.ErrorContainer,
|
||||
onErrorContainer = ThemeConfig.currentTheme.OnErrorContainer
|
||||
)
|
||||
|
||||
/**
|
||||
* 复制图片到应用内部存储并提升持久性
|
||||
*/
|
||||
private fun Context.copyImageToInternalStorage(uri: Uri): Uri? {
|
||||
return try {
|
||||
val contentResolver: ContentResolver = contentResolver
|
||||
val inputStream: InputStream = contentResolver.openInputStream(uri) ?: return null
|
||||
|
||||
val fileName = "custom_background.jpg"
|
||||
val file = File(filesDir, fileName)
|
||||
|
||||
val backupFile = File(filesDir, "${fileName}.backup")
|
||||
val outputStream = FileOutputStream(backupFile)
|
||||
val buffer = ByteArray(4 * 1024)
|
||||
var read: Int
|
||||
|
||||
while (inputStream.read(buffer).also { read = it } != -1) {
|
||||
outputStream.write(buffer, 0, read)
|
||||
}
|
||||
|
||||
outputStream.flush()
|
||||
outputStream.close()
|
||||
inputStream.close()
|
||||
|
||||
if (file.exists()) {
|
||||
file.delete()
|
||||
}
|
||||
backupFile.renameTo(file)
|
||||
|
||||
Uri.fromFile(file)
|
||||
} catch (e: Exception) {
|
||||
Log.e("ImageCopy", "复制图片失败: ${e.message}")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存并应用自定义背景
|
||||
*/
|
||||
fun Context.saveAndApplyCustomBackground(uri: Uri, transformation: BackgroundTransformation? = null) {
|
||||
val finalUri = if (transformation != null) {
|
||||
saveTransformedBackground(uri, transformation)
|
||||
} else {
|
||||
copyImageToInternalStorage(uri)
|
||||
}
|
||||
|
||||
// 保存到配置文件
|
||||
getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
||||
.edit {
|
||||
putString("custom_background", finalUri?.toString())
|
||||
// 设置阻止刷新标志为false,允许新设置的背景加载一次
|
||||
putBoolean("prevent_background_refresh", false)
|
||||
}
|
||||
|
||||
ThemeConfig.customBackgroundUri = finalUri
|
||||
ThemeConfig.backgroundImageLoaded = false
|
||||
ThemeConfig.preventBackgroundRefresh = false
|
||||
CardConfig.cardElevation = 0.dp
|
||||
CardConfig.isCustomBackgroundEnabled = true
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存自定义背景
|
||||
*/
|
||||
fun Context.saveCustomBackground(uri: Uri?) {
|
||||
val newUri = uri?.let { copyImageToInternalStorage(it) }
|
||||
|
||||
// 保存到配置文件
|
||||
getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
||||
.edit {
|
||||
putString("custom_background", newUri?.toString())
|
||||
if (uri == null) {
|
||||
// 如果清除背景,也重置阻止刷新标志
|
||||
putBoolean("prevent_background_refresh", false)
|
||||
} else {
|
||||
// 设置阻止刷新标志为false,允许新设置的背景加载一次
|
||||
putBoolean("prevent_background_refresh", false)
|
||||
}
|
||||
}
|
||||
|
||||
ThemeConfig.customBackgroundUri = newUri
|
||||
ThemeConfig.backgroundImageLoaded = false
|
||||
ThemeConfig.preventBackgroundRefresh = false
|
||||
|
||||
if (uri != null) {
|
||||
CardConfig.cardElevation = 0.dp
|
||||
CardConfig.isCustomBackgroundEnabled = true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载自定义背景
|
||||
*/
|
||||
fun Context.loadCustomBackground() {
|
||||
val uriString = getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
||||
.getString("custom_background", null)
|
||||
|
||||
val newUri = uriString?.toUri()
|
||||
val preventRefresh = getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
||||
.getBoolean("prevent_background_refresh", false)
|
||||
|
||||
ThemeConfig.preventBackgroundRefresh = preventRefresh
|
||||
|
||||
if (!preventRefresh || ThemeConfig.customBackgroundUri?.toString() != newUri?.toString()) {
|
||||
Log.d("ThemeSystem", "加载自定义背景: $uriString, 阻止刷新: $preventRefresh")
|
||||
ThemeConfig.customBackgroundUri = newUri
|
||||
ThemeConfig.backgroundImageLoaded = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存主题模式
|
||||
*/
|
||||
fun Context.saveThemeMode(forceDark: Boolean?) {
|
||||
getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
||||
.edit {
|
||||
putString(
|
||||
"theme_mode", when (forceDark) {
|
||||
true -> "dark"
|
||||
false -> "light"
|
||||
null -> "system"
|
||||
}
|
||||
)
|
||||
}
|
||||
ThemeConfig.forceDarkMode = forceDark
|
||||
ThemeConfig.needsResetOnThemeChange = forceDark == null
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载主题模式
|
||||
*/
|
||||
fun Context.loadThemeMode() {
|
||||
val mode = getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
||||
.getString("theme_mode", "system")
|
||||
|
||||
ThemeConfig.forceDarkMode = when(mode) {
|
||||
"dark" -> true
|
||||
"light" -> false
|
||||
else -> null
|
||||
}
|
||||
ThemeConfig.needsResetOnThemeChange = ThemeConfig.forceDarkMode == null
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存主题颜色
|
||||
*/
|
||||
fun Context.saveThemeColors(themeName: String) {
|
||||
getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
||||
.edit {
|
||||
putString("theme_colors", themeName)
|
||||
}
|
||||
|
||||
ThemeConfig.currentTheme = ThemeColors.fromName(themeName)
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载主题颜色
|
||||
*/
|
||||
fun Context.loadThemeColors() {
|
||||
val themeName = getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
||||
.getString("theme_colors", "default")
|
||||
|
||||
ThemeConfig.currentTheme = ThemeColors.fromName(themeName ?: "default")
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存动态颜色状态
|
||||
*/
|
||||
fun Context.saveDynamicColorState(enabled: Boolean) {
|
||||
getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
||||
.edit {
|
||||
putBoolean("use_dynamic_color", enabled)
|
||||
}
|
||||
ThemeConfig.useDynamicColor = enabled
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载动态颜色状态
|
||||
*/
|
||||
fun Context.loadDynamicColorState() {
|
||||
val enabled = getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
||||
.getBoolean("use_dynamic_color", true)
|
||||
|
||||
ThemeConfig.useDynamicColor = enabled
|
||||
}
|
||||
108
manager/app/src/main/java/com/sukisu/ultra/ui/theme/Type.kt
Normal file
108
manager/app/src/main/java/com/sukisu/ultra/ui/theme/Type.kt
Normal file
@@ -0,0 +1,108 @@
|
||||
package com.sukisu.ultra.ui.theme
|
||||
|
||||
import androidx.compose.material3.Typography
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
val Typography = Typography(
|
||||
// 大标题
|
||||
displayLarge = TextStyle(
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 57.sp,
|
||||
lineHeight = 64.sp,
|
||||
letterSpacing = (-0.25).sp
|
||||
),
|
||||
displayMedium = TextStyle(
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 45.sp,
|
||||
lineHeight = 52.sp,
|
||||
letterSpacing = 0.sp
|
||||
),
|
||||
displaySmall = TextStyle(
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 36.sp,
|
||||
lineHeight = 44.sp,
|
||||
letterSpacing = 0.sp
|
||||
),
|
||||
|
||||
// 标题
|
||||
headlineLarge = TextStyle(
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
fontSize = 32.sp,
|
||||
lineHeight = 40.sp,
|
||||
letterSpacing = 0.sp
|
||||
),
|
||||
headlineMedium = TextStyle(
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
fontSize = 28.sp,
|
||||
lineHeight = 36.sp,
|
||||
letterSpacing = 0.sp
|
||||
),
|
||||
headlineSmall = TextStyle(
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
fontSize = 24.sp,
|
||||
lineHeight = 32.sp,
|
||||
letterSpacing = 0.sp
|
||||
),
|
||||
|
||||
// 标题栏
|
||||
titleLarge = TextStyle(
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
fontSize = 22.sp,
|
||||
lineHeight = 28.sp,
|
||||
letterSpacing = 0.sp
|
||||
),
|
||||
titleMedium = TextStyle(
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
fontSize = 16.sp,
|
||||
lineHeight = 24.sp,
|
||||
letterSpacing = 0.15.sp
|
||||
),
|
||||
titleSmall = TextStyle(
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 14.sp,
|
||||
lineHeight = 20.sp,
|
||||
letterSpacing = 0.1.sp
|
||||
),
|
||||
|
||||
// 主体文字
|
||||
bodyLarge = TextStyle(
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 16.sp,
|
||||
lineHeight = 24.sp,
|
||||
letterSpacing = 0.5.sp
|
||||
),
|
||||
bodyMedium = TextStyle(
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 14.sp,
|
||||
lineHeight = 20.sp,
|
||||
letterSpacing = 0.25.sp
|
||||
),
|
||||
bodySmall = TextStyle(
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 12.sp,
|
||||
lineHeight = 16.sp,
|
||||
letterSpacing = 0.4.sp
|
||||
),
|
||||
|
||||
// 标签
|
||||
labelLarge = TextStyle(
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 14.sp,
|
||||
lineHeight = 20.sp,
|
||||
letterSpacing = 0.1.sp
|
||||
),
|
||||
labelMedium = TextStyle(
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 12.sp,
|
||||
lineHeight = 16.sp,
|
||||
letterSpacing = 0.5.sp
|
||||
),
|
||||
labelSmall = TextStyle(
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 11.sp,
|
||||
lineHeight = 16.sp,
|
||||
letterSpacing = 0.5.sp
|
||||
)
|
||||
)
|
||||
@@ -0,0 +1,110 @@
|
||||
package com.sukisu.ultra.ui.util
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Matrix
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.InputStream
|
||||
import androidx.core.graphics.createBitmap
|
||||
|
||||
data class BackgroundTransformation(
|
||||
val scale: Float = 1f,
|
||||
val offsetX: Float = 0f,
|
||||
val offsetY: Float = 0f
|
||||
)
|
||||
|
||||
fun Context.getImageBitmap(uri: Uri): Bitmap? {
|
||||
return try {
|
||||
val contentResolver: ContentResolver = contentResolver
|
||||
val inputStream: InputStream = contentResolver.openInputStream(uri) ?: return null
|
||||
val bitmap = BitmapFactory.decodeStream(inputStream)
|
||||
inputStream.close()
|
||||
bitmap
|
||||
} catch (e: Exception) {
|
||||
Log.e("BackgroundUtils", "Failed to get image bitmap: ${e.message}")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun Context.applyTransformationToBitmap(bitmap: Bitmap, transformation: BackgroundTransformation): Bitmap {
|
||||
val width = bitmap.width
|
||||
val height = bitmap.height
|
||||
|
||||
// 创建与屏幕比例相同的目标位图
|
||||
val displayMetrics = resources.displayMetrics
|
||||
val screenWidth = displayMetrics.widthPixels
|
||||
val screenHeight = displayMetrics.heightPixels
|
||||
val screenRatio = screenHeight.toFloat() / screenWidth.toFloat()
|
||||
|
||||
// 计算目标宽高
|
||||
val targetWidth: Int
|
||||
val targetHeight: Int
|
||||
if (width.toFloat() / height.toFloat() > screenRatio) {
|
||||
targetHeight = height
|
||||
targetWidth = (height / screenRatio).toInt()
|
||||
} else {
|
||||
targetWidth = width
|
||||
targetHeight = (width * screenRatio).toInt()
|
||||
}
|
||||
|
||||
// 创建与目标相同大小的位图
|
||||
val scaledBitmap = createBitmap(targetWidth, targetHeight)
|
||||
val canvas = Canvas(scaledBitmap)
|
||||
|
||||
val matrix = Matrix()
|
||||
|
||||
// 确保缩放值有效
|
||||
val safeScale = maxOf(0.1f, transformation.scale)
|
||||
matrix.postScale(safeScale, safeScale)
|
||||
|
||||
// 计算偏移量,确保不会出现负最大值的问题
|
||||
val widthDiff = (bitmap.width * safeScale - targetWidth)
|
||||
val heightDiff = (bitmap.height * safeScale - targetHeight)
|
||||
|
||||
// 安全计算偏移量边界
|
||||
val maxOffsetX = maxOf(0f, widthDiff / 2)
|
||||
val maxOffsetY = maxOf(0f, heightDiff / 2)
|
||||
|
||||
// 限制偏移范围
|
||||
val safeOffsetX = if (maxOffsetX > 0)
|
||||
transformation.offsetX.coerceIn(-maxOffsetX, maxOffsetX) else 0f
|
||||
val safeOffsetY = if (maxOffsetY > 0)
|
||||
transformation.offsetY.coerceIn(-maxOffsetY, maxOffsetY) else 0f
|
||||
|
||||
// 应用偏移量到矩阵
|
||||
val translationX = -widthDiff / 2 + safeOffsetX
|
||||
val translationY = -heightDiff / 2 + safeOffsetY
|
||||
|
||||
matrix.postTranslate(translationX, translationY)
|
||||
|
||||
// 将原始位图绘制到新位图上
|
||||
canvas.drawBitmap(bitmap, matrix, null)
|
||||
|
||||
return scaledBitmap
|
||||
}
|
||||
|
||||
fun Context.saveTransformedBackground(uri: Uri, transformation: BackgroundTransformation): Uri? {
|
||||
try {
|
||||
val bitmap = getImageBitmap(uri) ?: return null
|
||||
val transformedBitmap = applyTransformationToBitmap(bitmap, transformation)
|
||||
|
||||
val fileName = "custom_background_transformed.jpg"
|
||||
val file = File(filesDir, fileName)
|
||||
val outputStream = FileOutputStream(file)
|
||||
|
||||
transformedBitmap.compress(Bitmap.CompressFormat.JPEG, 90, outputStream)
|
||||
outputStream.flush()
|
||||
outputStream.close()
|
||||
|
||||
return Uri.fromFile(file)
|
||||
} catch (e: Exception) {
|
||||
Log.e("BackgroundUtils", "Failed to save transformed image: ${e.message}", e)
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package zako.zako.zako.ui.util
|
||||
package com.sukisu.ultra.ui.util
|
||||
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.runtime.compositionLocalOf
|
||||
@@ -1,4 +1,4 @@
|
||||
package zako.zako.zako.ui.util
|
||||
package com.sukisu.ultra.ui.util
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.DownloadManager
|
||||
@@ -12,7 +12,8 @@ import android.util.Log
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.core.content.ContextCompat
|
||||
import zako.zako.zako.ui.util.module.LatestVersionInfo
|
||||
import com.sukisu.ultra.ui.util.module.LatestVersionInfo
|
||||
import androidx.core.net.toUri
|
||||
|
||||
/**
|
||||
* @author weishu
|
||||
@@ -42,14 +43,14 @@ fun download(
|
||||
onDownloading()
|
||||
return
|
||||
} else if (status == DownloadManager.STATUS_SUCCESSFUL) {
|
||||
onDownloaded(Uri.parse(localUri))
|
||||
onDownloaded(localUri.toUri())
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val request = DownloadManager.Request(Uri.parse(url))
|
||||
val request = DownloadManager.Request(url.toUri())
|
||||
.setDestinationInExternalPublicDir(
|
||||
Environment.DIRECTORY_DOWNLOADS,
|
||||
fileName
|
||||
@@ -140,7 +141,7 @@ fun DownloadListener(context: Context, onDownloaded: (Uri) -> Unit) {
|
||||
val uri = cursor.getString(
|
||||
cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI)
|
||||
)
|
||||
onDownloaded(Uri.parse(uri))
|
||||
onDownloaded(uri.toUri())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package zako.zako.zako.ui.util;
|
||||
package com.sukisu.ultra.ui.util;
|
||||
/*
|
||||
* Copyright (C) 2009 The Android Open Source Project
|
||||
*
|
||||
@@ -1,4 +1,4 @@
|
||||
package zako.zako.zako.ui.util
|
||||
package com.sukisu.ultra.ui.util
|
||||
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
@@ -1,4 +1,4 @@
|
||||
package zako.zako.zako.ui.util
|
||||
package com.sukisu.ultra.ui.util
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
@@ -16,9 +16,9 @@ import com.topjohnwu.superuser.ShellUtils
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import zako.zako.zako.BuildConfig
|
||||
import zako.zako.zako.Natives
|
||||
import zako.zako.zako.ksuApp
|
||||
import com.sukisu.ultra.BuildConfig
|
||||
import com.sukisu.ultra.Natives
|
||||
import com.sukisu.ultra.ksuApp
|
||||
import org.json.JSONArray
|
||||
import java.io.File
|
||||
|
||||
@@ -436,7 +436,7 @@ fun restartApp(packageName: String) {
|
||||
launchApp(packageName)
|
||||
}
|
||||
|
||||
private fun getSuSFSDaemonPath(): String {
|
||||
fun getSuSFSDaemonPath(): String {
|
||||
return ksuApp.applicationInfo.nativeLibraryDir + File.separator + "libzakozakozako.so"
|
||||
}
|
||||
|
||||
@@ -537,7 +537,7 @@ fun getKpmModuleInfo(name: String): String {
|
||||
|
||||
fun controlKpmModule(name: String, args: String? = null): Int {
|
||||
val shell = getRootShell()
|
||||
val cmd = "${getKpmmgrPath()} control $name ${args ?: ""}"
|
||||
val cmd = """${getKpmmgrPath()} control $name "${args ?: ""}""""
|
||||
val result = runCmd(shell, cmd)
|
||||
return result.trim().toIntOrNull() ?: -1
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
package zako.zako.zako.ui.util
|
||||
package com.sukisu.ultra.ui.util
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.system.Os
|
||||
import com.topjohnwu.superuser.ShellUtils
|
||||
import zako.zako.zako.Natives
|
||||
import zako.zako.zako.ui.screen.getManagerVersion
|
||||
import com.sukisu.ultra.Natives
|
||||
import com.sukisu.ultra.ui.screen.getManagerVersion
|
||||
import java.io.File
|
||||
import java.io.FileWriter
|
||||
import java.io.PrintWriter
|
||||
@@ -1,4 +1,4 @@
|
||||
package zako.zako.zako.ui.util
|
||||
package com.sukisu.ultra.ui.util
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.content.Context
|
||||
@@ -16,7 +16,7 @@ import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import zako.zako.zako.R
|
||||
import com.sukisu.ultra.R
|
||||
import java.io.BufferedReader
|
||||
import java.io.IOException
|
||||
import java.io.InputStreamReader
|
||||
@@ -1,9 +1,9 @@
|
||||
package zako.zako.zako.ui.util
|
||||
package com.sukisu.ultra.ui.util
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import zako.zako.zako.R
|
||||
import com.sukisu.ultra.R
|
||||
|
||||
@Composable
|
||||
fun getSELinuxStatus(): String {
|
||||
@@ -1,4 +1,4 @@
|
||||
package zako.zako.zako.ui.util.module
|
||||
package com.sukisu.ultra.ui.util.module
|
||||
|
||||
data class LatestVersionInfo(
|
||||
val versionCode : Int = 0,
|
||||
@@ -1,4 +1,4 @@
|
||||
package zako.zako.zako.ui.viewmodel
|
||||
package com.sukisu.ultra.ui.viewmodel
|
||||
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -9,7 +9,7 @@ import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import zako.zako.zako.ui.util.*
|
||||
import com.sukisu.ultra.ui.util.*
|
||||
|
||||
class KpmViewModel : ViewModel() {
|
||||
var moduleList by mutableStateOf(emptyList<ModuleInfo>())
|
||||
@@ -143,17 +143,6 @@ class KpmViewModel : ViewModel() {
|
||||
return result
|
||||
}
|
||||
|
||||
fun controlModule(moduleId: String, args: String? = null): Int {
|
||||
return try {
|
||||
val result = controlKpmModule(moduleId, args)
|
||||
Log.d("KsuCli", "Control module $moduleId result: $result")
|
||||
result
|
||||
} catch (e: Exception) {
|
||||
Log.e("KsuCli", "Failed to control module $moduleId", e)
|
||||
-1
|
||||
}
|
||||
}
|
||||
|
||||
data class ModuleInfo(
|
||||
val id: String,
|
||||
val name: String,
|
||||
@@ -1,4 +1,4 @@
|
||||
package zako.zako.zako.ui.viewmodel
|
||||
package com.sukisu.ultra.ui.viewmodel
|
||||
|
||||
import android.os.SystemClock
|
||||
import android.util.Log
|
||||
@@ -10,8 +10,8 @@ import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import zako.zako.zako.ui.util.HanziToPinyin
|
||||
import zako.zako.zako.ui.util.listModules
|
||||
import com.sukisu.ultra.ui.util.HanziToPinyin
|
||||
import com.sukisu.ultra.ui.util.listModules
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import java.text.Collator
|
||||
@@ -40,13 +40,6 @@ class ModuleViewModel : ViewModel() {
|
||||
val dirId: String, // real module id (dir name)
|
||||
)
|
||||
|
||||
data class ModuleUpdateInfo(
|
||||
val version: String,
|
||||
val versionCode: Int,
|
||||
val zipUrl: String,
|
||||
val changelog: String,
|
||||
)
|
||||
|
||||
var isRefreshing by mutableStateOf(false)
|
||||
private set
|
||||
var search by mutableStateOf("")
|
||||
@@ -1,4 +1,4 @@
|
||||
package zako.zako.zako.ui.viewmodel
|
||||
package com.sukisu.ultra.ui.viewmodel
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Intent
|
||||
@@ -18,12 +18,12 @@ import com.topjohnwu.superuser.Shell
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import zako.zako.zako.IKsuInterface
|
||||
import zako.zako.zako.Natives
|
||||
import zako.zako.zako.ksuApp
|
||||
import zako.zako.zako.ui.KsuService
|
||||
import zako.zako.zako.ui.util.HanziToPinyin
|
||||
import zako.zako.zako.ui.util.KsuCli
|
||||
import com.sukisu.zako.IKsuInterface
|
||||
import com.sukisu.ultra.Natives
|
||||
import com.sukisu.ultra.ksuApp
|
||||
import com.sukisu.ultra.ui.KsuService
|
||||
import com.sukisu.ultra.ui.util.HanziToPinyin
|
||||
import com.sukisu.ultra.ui.util.KsuCli
|
||||
import java.text.Collator
|
||||
import java.util.*
|
||||
import kotlin.coroutines.resume
|
||||
@@ -68,9 +68,9 @@ class SuperUserViewModel : ViewModel() {
|
||||
|
||||
// 批量操作相关状态
|
||||
var showBatchActions by mutableStateOf(false)
|
||||
private set
|
||||
internal set
|
||||
var selectedApps by mutableStateOf<Set<String>>(emptySet())
|
||||
private set
|
||||
internal set
|
||||
|
||||
private val sortedList by derivedStateOf {
|
||||
val comparator = compareBy<AppInfo> {
|
||||
@@ -1,4 +1,4 @@
|
||||
package zako.zako.zako.ui.viewmodel
|
||||
package com.sukisu.ultra.ui.viewmodel
|
||||
|
||||
import android.os.Parcelable
|
||||
import android.util.Log
|
||||
@@ -10,12 +10,12 @@ import androidx.lifecycle.ViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import zako.zako.zako.Natives
|
||||
import zako.zako.zako.profile.Capabilities
|
||||
import zako.zako.zako.profile.Groups
|
||||
import zako.zako.zako.ui.util.getAppProfileTemplate
|
||||
import zako.zako.zako.ui.util.listAppProfileTemplates
|
||||
import zako.zako.zako.ui.util.setAppProfileTemplate
|
||||
import com.sukisu.ultra.Natives
|
||||
import com.sukisu.ultra.profile.Capabilities
|
||||
import com.sukisu.ultra.profile.Groups
|
||||
import com.sukisu.ultra.ui.util.getAppProfileTemplate
|
||||
import com.sukisu.ultra.ui.util.listAppProfileTemplates
|
||||
import com.sukisu.ultra.ui.util.setAppProfileTemplate
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import org.json.JSONArray
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package zako.zako.zako.ui.webui;
|
||||
package com.sukisu.ultra.ui.webui;
|
||||
|
||||
import java.net.URLConnection;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package zako.zako.zako.ui.webui;
|
||||
package com.sukisu.ultra.ui.webui;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
@@ -1,4 +1,4 @@
|
||||
package zako.zako.zako.ui.webui
|
||||
package com.sukisu.ultra.ui.webui
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.ActivityManager
|
||||
@@ -16,7 +16,7 @@ import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.webkit.WebViewAssetLoader
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import zako.zako.zako.ui.util.createRootShell
|
||||
import com.sukisu.ultra.ui.util.createRootShell
|
||||
import java.io.File
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
@@ -1,4 +1,4 @@
|
||||
package zako.zako.zako.ui.webui
|
||||
package com.sukisu.ultra.ui.webui
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
@@ -14,13 +14,13 @@ import androidx.core.view.WindowInsetsControllerCompat
|
||||
import com.topjohnwu.superuser.CallbackList
|
||||
import com.topjohnwu.superuser.ShellUtils
|
||||
import com.topjohnwu.superuser.internal.UiThreadHandler
|
||||
import zako.zako.zako.ui.util.createRootShell
|
||||
import zako.zako.zako.ui.util.listModules
|
||||
import zako.zako.zako.ui.util.withNewRootShell
|
||||
import com.sukisu.ultra.ui.util.createRootShell
|
||||
import com.sukisu.ultra.ui.util.listModules
|
||||
import com.sukisu.ultra.ui.util.withNewRootShell
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import zako.zako.zako.ui.util.controlKpmModule
|
||||
import zako.zako.zako.ui.util.listKpmModules
|
||||
import com.sukisu.ultra.ui.util.controlKpmModule
|
||||
import com.sukisu.ultra.ui.util.listKpmModules
|
||||
import java.io.File
|
||||
import java.util.concurrent.CompletableFuture
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package zako.zako.zako.utils
|
||||
package com.sukisu.ultra.utils
|
||||
|
||||
import android.content.Context
|
||||
import java.io.File
|
||||
@@ -1,8 +1,8 @@
|
||||
package io.zako.zako;
|
||||
package io.sukisu.ultra;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
import zako.zako.zako.ui.util.KsuCli;
|
||||
import com.sukisu.ultra.ui.util.KsuCli;
|
||||
|
||||
public class UltraShellHelper {
|
||||
public static String runCmd(String cmds) {
|
||||
@@ -19,7 +19,7 @@ public class UltraShellHelper {
|
||||
}
|
||||
|
||||
public static boolean isPathExists(String path) {
|
||||
return !runCmd("file " + path).contains("No such file or directory");
|
||||
return runCmd("file " + path).contains("No such file or directory");
|
||||
}
|
||||
|
||||
public static void CopyFileTo(String path, String target) {
|
||||
@@ -0,0 +1,21 @@
|
||||
package io.sukisu.ultra;
|
||||
|
||||
import static com.sukisu.ultra.ui.util.KsuCliKt.getKpmmgrPath;
|
||||
import static com.sukisu.ultra.ui.util.KsuCliKt.getSuSFSDaemonPath;
|
||||
|
||||
public class UltraToolInstall {
|
||||
private static final String OUTSIDE_KPMMGR_PATH = "/data/adb/ksu/bin/kpmmgr";
|
||||
private static final String OUTSIDE_SUSFSD_PATH = "/data/adb/ksu/bin/susfsd";
|
||||
public static void tryToInstall() {
|
||||
String kpmmgrPath = getKpmmgrPath();
|
||||
if (UltraShellHelper.isPathExists(OUTSIDE_KPMMGR_PATH)) {
|
||||
UltraShellHelper.CopyFileTo(kpmmgrPath, OUTSIDE_KPMMGR_PATH);
|
||||
UltraShellHelper.runCmd("chmod a+rx " + OUTSIDE_KPMMGR_PATH);
|
||||
}
|
||||
String SuSFSDaemonPath = getSuSFSDaemonPath();
|
||||
if (UltraShellHelper.isPathExists(OUTSIDE_SUSFSD_PATH)) {
|
||||
UltraShellHelper.CopyFileTo(SuSFSDaemonPath, OUTSIDE_SUSFSD_PATH);
|
||||
UltraShellHelper.runCmd("chmod a+rx " + OUTSIDE_SUSFSD_PATH);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
package io.zako.zako;
|
||||
|
||||
import static zako.zako.zako.ui.util.KsuCliKt.getKpmmgrPath;
|
||||
|
||||
public class UltraToolInstall {
|
||||
private static final String OUTSIDE_KPMMGR_PATH = "/data/adb/ksu/bin/kpmmgr";
|
||||
public static void tryToInstall() {
|
||||
String kpmmgrPath = getKpmmgrPath();
|
||||
if (!UltraShellHelper.isPathExists(OUTSIDE_KPMMGR_PATH)) {
|
||||
UltraShellHelper.CopyFileTo(kpmmgrPath, OUTSIDE_KPMMGR_PATH);
|
||||
UltraShellHelper.runCmd("chmod a+rx " + OUTSIDE_KPMMGR_PATH);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,190 +0,0 @@
|
||||
package zako.zako.zako.ui
|
||||
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.compose.animation.AnimatedContentTransitionScope
|
||||
import androidx.compose.animation.EnterTransition
|
||||
import androidx.compose.animation.ExitTransition
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||
import androidx.compose.foundation.layout.displayCutout
|
||||
import androidx.compose.foundation.layout.only
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.systemBars
|
||||
import androidx.compose.foundation.layout.union
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.NavigationBar
|
||||
import androidx.compose.material3.NavigationBarItem
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.navigation.NavBackStackEntry
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import com.ramcosta.composedestinations.DestinationsNavHost
|
||||
import com.ramcosta.composedestinations.animations.NavHostAnimatedDestinationStyle
|
||||
import com.ramcosta.composedestinations.generated.NavGraphs
|
||||
import com.ramcosta.composedestinations.utils.isRouteOnBackStackAsState
|
||||
import com.ramcosta.composedestinations.utils.rememberDestinationsNavigator
|
||||
import io.zako.zako.UltraToolInstall
|
||||
import zako.zako.zako.Natives
|
||||
import zako.zako.zako.ksuApp
|
||||
import zako.zako.zako.ui.screen.BottomBarDestination
|
||||
import zako.zako.zako.ui.theme.CardConfig
|
||||
import zako.zako.zako.ui.theme.KernelSUTheme
|
||||
import zako.zako.zako.ui.theme.loadCustomBackground
|
||||
import zako.zako.zako.ui.theme.loadThemeMode
|
||||
import zako.zako.zako.ui.util.LocalSnackbarHost
|
||||
import zako.zako.zako.ui.util.getKpmVersion
|
||||
import zako.zako.zako.ui.util.rootAvailable
|
||||
import zako.zako.zako.ui.util.install
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
|
||||
// Enable edge to edge
|
||||
enableEdgeToEdge()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
window.isNavigationBarContrastEnforced = false
|
||||
}
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
// 加载保存的背景设置
|
||||
loadCustomBackground()
|
||||
loadThemeMode()
|
||||
CardConfig.load(applicationContext)
|
||||
|
||||
|
||||
val isManager = Natives.becomeManager(ksuApp.packageName)
|
||||
if (isManager) {
|
||||
install()
|
||||
UltraToolInstall.tryToInstall()
|
||||
}
|
||||
|
||||
setContent {
|
||||
KernelSUTheme {
|
||||
val navController = rememberNavController()
|
||||
val snackBarHostState = remember { SnackbarHostState() }
|
||||
Scaffold(
|
||||
bottomBar = { BottomBar(navController) },
|
||||
contentWindowInsets = WindowInsets(0, 0, 0, 0)
|
||||
) { innerPadding ->
|
||||
CompositionLocalProvider(
|
||||
LocalSnackbarHost provides snackBarHostState,
|
||||
) {
|
||||
DestinationsNavHost(
|
||||
modifier = Modifier.padding(innerPadding),
|
||||
navGraph = NavGraphs.root,
|
||||
navController = navController,
|
||||
defaultTransitions = object : NavHostAnimatedDestinationStyle() {
|
||||
override val enterTransition: AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition
|
||||
get() = { fadeIn(animationSpec = tween(340)) }
|
||||
override val exitTransition: AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition
|
||||
get() = { fadeOut(animationSpec = tween(340)) }
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BottomBar(navController: NavHostController) {
|
||||
val navigator = navController.rememberDestinationsNavigator()
|
||||
val isManager = Natives.becomeManager(ksuApp.packageName)
|
||||
val fullFeatured = isManager && !Natives.requireNewKernel() && rootAvailable()
|
||||
val kpmVersion = getKpmVersion()
|
||||
|
||||
// 获取卡片颜色和透明度
|
||||
val cardColor = MaterialTheme.colorScheme.secondaryContainer
|
||||
val cardAlpha = CardConfig.cardAlpha
|
||||
val cardElevation = CardConfig.cardElevation
|
||||
|
||||
NavigationBar(
|
||||
tonalElevation = cardElevation, // 动态设置阴影
|
||||
containerColor = cardColor.copy(alpha = cardAlpha),
|
||||
windowInsets = WindowInsets.systemBars.union(WindowInsets.displayCutout).only(
|
||||
WindowInsetsSides.Horizontal + WindowInsetsSides.Bottom
|
||||
)
|
||||
) {
|
||||
BottomBarDestination.entries.forEach { destination ->
|
||||
if (destination == BottomBarDestination.Kpm) {
|
||||
if (kpmVersion.isNotEmpty() && !kpmVersion.startsWith("Error")) {
|
||||
if (!fullFeatured && destination.rootRequired) return@forEach
|
||||
val isCurrentDestOnBackStack by navController.isRouteOnBackStackAsState(destination.direction)
|
||||
NavigationBarItem(
|
||||
selected = isCurrentDestOnBackStack,
|
||||
onClick = {
|
||||
if (isCurrentDestOnBackStack) {
|
||||
navigator.popBackStack(destination.direction, false)
|
||||
}
|
||||
navigator.navigate(destination.direction) {
|
||||
popUpTo(NavGraphs.root) {
|
||||
saveState = true
|
||||
}
|
||||
launchSingleTop = true
|
||||
restoreState = true
|
||||
}
|
||||
},
|
||||
icon = {
|
||||
if (isCurrentDestOnBackStack) {
|
||||
Icon(destination.iconSelected, stringResource(destination.label))
|
||||
} else {
|
||||
Icon(destination.iconNotSelected, stringResource(destination.label))
|
||||
}
|
||||
},
|
||||
label = { Text(stringResource(destination.label)) },
|
||||
alwaysShowLabel = false,
|
||||
colors = androidx.compose.material3.NavigationBarItemDefaults.colors(
|
||||
unselectedTextColor = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
if (!fullFeatured && destination.rootRequired) return@forEach
|
||||
val isCurrentDestOnBackStack by navController.isRouteOnBackStackAsState(destination.direction)
|
||||
NavigationBarItem(
|
||||
selected = isCurrentDestOnBackStack,
|
||||
onClick = {
|
||||
if (isCurrentDestOnBackStack) {
|
||||
navigator.popBackStack(destination.direction, false)
|
||||
}
|
||||
navigator.navigate(destination.direction) {
|
||||
popUpTo(NavGraphs.root) {
|
||||
saveState = true
|
||||
}
|
||||
launchSingleTop = true
|
||||
restoreState = true
|
||||
}
|
||||
},
|
||||
icon = {
|
||||
if (isCurrentDestOnBackStack) {
|
||||
Icon(destination.iconSelected, stringResource(destination.label))
|
||||
} else {
|
||||
Icon(destination.iconNotSelected, stringResource(destination.label))
|
||||
}
|
||||
},
|
||||
label = { Text(stringResource(destination.label)) },
|
||||
alwaysShowLabel = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
package zako.zako.zako.ui.component
|
||||
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
|
||||
@Composable
|
||||
fun SwitchItem(
|
||||
icon: ImageVector,
|
||||
title: String,
|
||||
summary: String,
|
||||
checked: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
onCheckedChange: (Boolean) -> Unit
|
||||
) {
|
||||
ListItem(
|
||||
modifier = modifier,
|
||||
leadingContent = { Icon(icon, contentDescription = null) },
|
||||
headlineContent = { Text(title) },
|
||||
supportingContent = { Text(summary) },
|
||||
trailingContent = {
|
||||
Switch(
|
||||
checked = checked,
|
||||
onCheckedChange = onCheckedChange
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -1,393 +0,0 @@
|
||||
package zako.zako.zako.ui.screen
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.only
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.safeDrawing
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.AccountCircle
|
||||
import androidx.compose.material.icons.filled.Android
|
||||
import androidx.compose.material.icons.filled.Security
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FilterChip
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.DpOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.dropUnlessResumed
|
||||
import coil.compose.AsyncImage
|
||||
import coil.request.ImageRequest
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||
import com.ramcosta.composedestinations.generated.destinations.AppProfileTemplateScreenDestination
|
||||
import com.ramcosta.composedestinations.generated.destinations.TemplateEditorScreenDestination
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
import kotlinx.coroutines.launch
|
||||
import zako.zako.zako.Natives
|
||||
import zako.zako.zako.R
|
||||
import zako.zako.zako.ui.component.SwitchItem
|
||||
import zako.zako.zako.ui.component.profile.AppProfileConfig
|
||||
import zako.zako.zako.ui.component.profile.RootProfileConfig
|
||||
import zako.zako.zako.ui.component.profile.TemplateConfig
|
||||
import zako.zako.zako.ui.util.LocalSnackbarHost
|
||||
import zako.zako.zako.ui.util.forceStopApp
|
||||
import zako.zako.zako.ui.util.getSepolicy
|
||||
import zako.zako.zako.ui.util.launchApp
|
||||
import zako.zako.zako.ui.util.restartApp
|
||||
import zako.zako.zako.ui.util.setSepolicy
|
||||
import zako.zako.zako.ui.viewmodel.SuperUserViewModel
|
||||
import zako.zako.zako.ui.viewmodel.getTemplateInfoById
|
||||
|
||||
/**
|
||||
* @author weishu
|
||||
* @date 2023/5/16.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Destination<RootGraph>
|
||||
@Composable
|
||||
fun AppProfileScreen(
|
||||
navigator: DestinationsNavigator,
|
||||
appInfo: SuperUserViewModel.AppInfo,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val snackBarHost = LocalSnackbarHost.current
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
|
||||
val scope = rememberCoroutineScope()
|
||||
val failToUpdateAppProfile = stringResource(R.string.failed_to_update_app_profile).format(appInfo.label)
|
||||
val failToUpdateSepolicy = stringResource(R.string.failed_to_update_sepolicy).format(appInfo.label)
|
||||
val suNotAllowed = stringResource(R.string.su_not_allowed).format(appInfo.label)
|
||||
|
||||
val packageName = appInfo.packageName
|
||||
val initialProfile = Natives.getAppProfile(packageName, appInfo.uid)
|
||||
if (initialProfile.allowSu) {
|
||||
initialProfile.rules = getSepolicy(packageName)
|
||||
}
|
||||
var profile by rememberSaveable {
|
||||
mutableStateOf(initialProfile)
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopBar(
|
||||
onBack = dropUnlessResumed { navigator.popBackStack() },
|
||||
scrollBehavior = scrollBehavior
|
||||
)
|
||||
},
|
||||
snackbarHost = { SnackbarHost(hostState = snackBarHost) },
|
||||
contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal)
|
||||
) { paddingValues ->
|
||||
AppProfileInner(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
packageName = appInfo.packageName,
|
||||
appLabel = appInfo.label,
|
||||
appIcon = {
|
||||
AsyncImage(
|
||||
model = ImageRequest.Builder(context).data(appInfo.packageInfo).crossfade(true).build(),
|
||||
contentDescription = appInfo.label,
|
||||
modifier = Modifier
|
||||
.padding(4.dp)
|
||||
.width(48.dp)
|
||||
.height(48.dp)
|
||||
)
|
||||
},
|
||||
profile = profile,
|
||||
onViewTemplate = {
|
||||
getTemplateInfoById(it)?.let { info ->
|
||||
navigator.navigate(TemplateEditorScreenDestination(info))
|
||||
}
|
||||
},
|
||||
onManageTemplate = {
|
||||
navigator.navigate(AppProfileTemplateScreenDestination())
|
||||
},
|
||||
onProfileChange = {
|
||||
scope.launch {
|
||||
if (it.allowSu) {
|
||||
// sync with allowlist.c - forbid_system_uid
|
||||
if (appInfo.uid < 2000 && appInfo.uid != 1000) {
|
||||
snackBarHost.showSnackbar(suNotAllowed)
|
||||
return@launch
|
||||
}
|
||||
if (!it.rootUseDefault && it.rules.isNotEmpty() && !setSepolicy(profile.name, it.rules)) {
|
||||
snackBarHost.showSnackbar(failToUpdateSepolicy)
|
||||
return@launch
|
||||
}
|
||||
}
|
||||
if (!Natives.setAppProfile(it)) {
|
||||
snackBarHost.showSnackbar(failToUpdateAppProfile.format(appInfo.uid))
|
||||
} else {
|
||||
profile = it
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AppProfileInner(
|
||||
modifier: Modifier = Modifier,
|
||||
packageName: String,
|
||||
appLabel: String,
|
||||
appIcon: @Composable () -> Unit,
|
||||
profile: Natives.Profile,
|
||||
onViewTemplate: (id: String) -> Unit = {},
|
||||
onManageTemplate: () -> Unit = {},
|
||||
onProfileChange: (Natives.Profile) -> Unit,
|
||||
) {
|
||||
val isRootGranted = profile.allowSu
|
||||
|
||||
Column(modifier = modifier) {
|
||||
AppMenuBox(packageName) {
|
||||
ListItem(
|
||||
headlineContent = { Text(appLabel) },
|
||||
supportingContent = { Text(packageName) },
|
||||
leadingContent = appIcon,
|
||||
)
|
||||
}
|
||||
|
||||
SwitchItem(
|
||||
icon = Icons.Filled.Security,
|
||||
title = stringResource(id = R.string.superuser),
|
||||
checked = isRootGranted,
|
||||
onCheckedChange = { onProfileChange(profile.copy(allowSu = it)) },
|
||||
)
|
||||
|
||||
Crossfade(targetState = isRootGranted, label = "") { current ->
|
||||
Column(
|
||||
modifier = Modifier.padding(bottom = 6.dp + 48.dp + 6.dp /* SnackBar height */)
|
||||
) {
|
||||
if (current) {
|
||||
val initialMode = if (profile.rootUseDefault) {
|
||||
Mode.Default
|
||||
} else if (profile.rootTemplate != null) {
|
||||
Mode.Template
|
||||
} else {
|
||||
Mode.Custom
|
||||
}
|
||||
var mode by rememberSaveable {
|
||||
mutableStateOf(initialMode)
|
||||
}
|
||||
ProfileBox(mode, true) {
|
||||
// template mode shouldn't change profile here!
|
||||
if (it == Mode.Default || it == Mode.Custom) {
|
||||
onProfileChange(profile.copy(rootUseDefault = it == Mode.Default))
|
||||
}
|
||||
mode = it
|
||||
}
|
||||
Crossfade(targetState = mode, label = "") { currentMode ->
|
||||
if (currentMode == Mode.Template) {
|
||||
TemplateConfig(
|
||||
profile = profile,
|
||||
onViewTemplate = onViewTemplate,
|
||||
onManageTemplate = onManageTemplate,
|
||||
onProfileChange = onProfileChange
|
||||
)
|
||||
} else if (mode == Mode.Custom) {
|
||||
RootProfileConfig(
|
||||
fixedName = true,
|
||||
profile = profile,
|
||||
onProfileChange = onProfileChange
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val mode = if (profile.nonRootUseDefault) Mode.Default else Mode.Custom
|
||||
ProfileBox(mode, false) {
|
||||
onProfileChange(profile.copy(nonRootUseDefault = (it == Mode.Default)))
|
||||
}
|
||||
Crossfade(targetState = mode, label = "") { currentMode ->
|
||||
val modifyEnabled = currentMode == Mode.Custom
|
||||
AppProfileConfig(
|
||||
fixedName = true,
|
||||
profile = profile,
|
||||
enabled = modifyEnabled,
|
||||
onProfileChange = onProfileChange
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private enum class Mode(@StringRes private val res: Int) {
|
||||
Default(R.string.profile_default), Template(R.string.profile_template), Custom(R.string.profile_custom);
|
||||
|
||||
val text: String
|
||||
@Composable get() = stringResource(res)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun TopBar(
|
||||
onBack: () -> Unit,
|
||||
scrollBehavior: TopAppBarScrollBehavior? = null
|
||||
) {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(stringResource(R.string.profile))
|
||||
},
|
||||
navigationIcon = {
|
||||
IconButton(
|
||||
onClick = onBack
|
||||
) { Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null) }
|
||||
},
|
||||
windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),
|
||||
scrollBehavior = scrollBehavior
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ProfileBox(
|
||||
mode: Mode,
|
||||
hasTemplate: Boolean,
|
||||
onModeChange: (Mode) -> Unit,
|
||||
) {
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(R.string.profile)) },
|
||||
supportingContent = { Text(mode.text) },
|
||||
leadingContent = { Icon(Icons.Filled.AccountCircle, null) },
|
||||
)
|
||||
HorizontalDivider(thickness = Dp.Hairline)
|
||||
ListItem(headlineContent = {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly
|
||||
) {
|
||||
FilterChip(
|
||||
selected = mode == Mode.Default,
|
||||
label = { Text(stringResource(R.string.profile_default)) },
|
||||
onClick = { onModeChange(Mode.Default) },
|
||||
)
|
||||
if (hasTemplate) {
|
||||
FilterChip(
|
||||
selected = mode == Mode.Template,
|
||||
label = { Text(stringResource(R.string.profile_template)) },
|
||||
onClick = { onModeChange(Mode.Template) },
|
||||
)
|
||||
}
|
||||
FilterChip(
|
||||
selected = mode == Mode.Custom,
|
||||
label = { Text(stringResource(R.string.profile_custom)) },
|
||||
onClick = { onModeChange(Mode.Custom) },
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AppMenuBox(packageName: String, content: @Composable () -> Unit) {
|
||||
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
var touchPoint: Offset by remember { mutableStateOf(Offset.Zero) }
|
||||
val density = LocalDensity.current
|
||||
|
||||
BoxWithConstraints(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.pointerInput(Unit) {
|
||||
detectTapGestures {
|
||||
touchPoint = it
|
||||
expanded = true
|
||||
}
|
||||
}
|
||||
) {
|
||||
|
||||
content()
|
||||
|
||||
val (offsetX, offsetY) = with(density) {
|
||||
(touchPoint.x.toDp()) to (touchPoint.y.toDp())
|
||||
}
|
||||
|
||||
DropdownMenu(
|
||||
expanded = expanded,
|
||||
offset = DpOffset(offsetX, -offsetY),
|
||||
onDismissRequest = {
|
||||
expanded = false
|
||||
},
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(id = R.string.launch_app)) },
|
||||
onClick = {
|
||||
expanded = false
|
||||
launchApp(packageName)
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(id = R.string.force_stop_app)) },
|
||||
onClick = {
|
||||
expanded = false
|
||||
forceStopApp(packageName)
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(id = R.string.restart_app)) },
|
||||
onClick = {
|
||||
expanded = false
|
||||
restartApp(packageName)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun AppProfilePreview() {
|
||||
var profile by remember { mutableStateOf(Natives.Profile("")) }
|
||||
AppProfileInner(
|
||||
packageName = "icu.nullptr.test",
|
||||
appLabel = "Test",
|
||||
appIcon = { Icon(Icons.Filled.Android, null) },
|
||||
profile = profile,
|
||||
onProfileChange = {
|
||||
profile = it
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,735 +0,0 @@
|
||||
package zako.zako.zako.ui.screen
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.os.PowerManager
|
||||
import android.system.Os
|
||||
import android.util.Log
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material.icons.outlined.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.*
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.content.pm.PackageInfoCompat
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||
import com.ramcosta.composedestinations.generated.destinations.InstallScreenDestination
|
||||
import com.ramcosta.composedestinations.generated.destinations.SettingScreenDestination
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import zako.zako.zako.*
|
||||
import zako.zako.zako.R
|
||||
import zako.zako.zako.ui.component.rememberConfirmDialog
|
||||
import zako.zako.zako.ui.util.*
|
||||
import zako.zako.zako.ui.util.module.LatestVersionInfo
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import zako.zako.zako.ui.theme.getCardColors
|
||||
import zako.zako.zako.ui.theme.getCardElevation
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.shrinkVertically
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import zako.zako.zako.ui.theme.CardConfig
|
||||
import androidx.core.content.edit
|
||||
import java.io.BufferedReader
|
||||
import java.io.InputStreamReader
|
||||
import java.util.zip.GZIPInputStream
|
||||
import kotlin.random.Random
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Destination<RootGraph>(start = true)
|
||||
@Composable
|
||||
fun HomeScreen(navigator: DestinationsNavigator) {
|
||||
val context = LocalContext.current
|
||||
var isSimpleMode by rememberSaveable { mutableStateOf(false) }
|
||||
var isHideVersion by rememberSaveable { mutableStateOf(false) }
|
||||
var isHideOtherInfo by rememberSaveable { mutableStateOf(false) }
|
||||
var isHideSusfsStatus by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
// 从 SharedPreferences 加载简洁模式状态
|
||||
LaunchedEffect(Unit) {
|
||||
isSimpleMode = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||
.getBoolean("is_simple_mode", false)
|
||||
}
|
||||
// 从 SharedPreferences 加载隐藏 KernelSU 版本号开关状态
|
||||
LaunchedEffect(Unit) {
|
||||
isHideVersion = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||
.getBoolean("is_hide_version", false)
|
||||
}
|
||||
// 从 SharedPreferences 加载隐藏模块数量等信息开关状态
|
||||
LaunchedEffect(Unit) {
|
||||
isHideOtherInfo = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||
.getBoolean("is_hide_other_info", false)
|
||||
}
|
||||
// 从 SharedPreferences 加载隐藏 SuSFS 状态开关状态
|
||||
LaunchedEffect(Unit) {
|
||||
isHideSusfsStatus = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||
.getBoolean("is_hide_susfs_status", false)
|
||||
}
|
||||
val kernelVersion = getKernelVersion()
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||
|
||||
val isManager = Natives.becomeManager(ksuApp.packageName)
|
||||
val deviceModel = getDeviceModel(context)
|
||||
val ksuVersion = if (isManager) Natives.version else null
|
||||
val managerVersion = getManagerVersion(context).second
|
||||
val Zako = "一.*加.*A.*c.*e.*5.*P.*r.*o".toRegex().matches(deviceModel)
|
||||
val isVersion = ksuVersion == 12777
|
||||
val isManagerVersionValid = managerVersion > (ksuVersion ?: 0) + 33
|
||||
val shouldTriggerRestart = Zako && kernelVersion.isGKI() && (isVersion || isManagerVersionValid)
|
||||
|
||||
LaunchedEffect(shouldTriggerRestart) {
|
||||
if (shouldTriggerRestart) {
|
||||
val random = Random.nextInt(0, 100)
|
||||
if (random <= 95) {
|
||||
reboot()
|
||||
} else {
|
||||
""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopBar(
|
||||
kernelVersion,
|
||||
onInstallClick = { navigator.navigate(InstallScreenDestination) },
|
||||
onSettingsClick = { navigator.navigate(SettingScreenDestination) },
|
||||
scrollBehavior = scrollBehavior
|
||||
)
|
||||
},
|
||||
contentWindowInsets = WindowInsets.safeDrawing.only(
|
||||
WindowInsetsSides.Top + WindowInsetsSides.Horizontal
|
||||
)
|
||||
) { innerPadding ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(innerPadding)
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection)
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(top = 12.dp)
|
||||
.padding(horizontal = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
if (shouldTriggerRestart) {
|
||||
WarningCard(message = "zakozako")
|
||||
return@Column
|
||||
}
|
||||
val isManager = Natives.becomeManager(ksuApp.packageName)
|
||||
val ksuVersion = if (isManager) Natives.version else null
|
||||
val lkmMode = ksuVersion?.let {
|
||||
if (it >= Natives.MINIMAL_SUPPORTED_KERNEL_LKM && kernelVersion.isGKI()) Natives.isLkmMode else null
|
||||
}
|
||||
|
||||
StatusCard(kernelVersion, ksuVersion, lkmMode) {
|
||||
navigator.navigate(InstallScreenDestination)
|
||||
}
|
||||
if (isManager && Natives.requireNewKernel()) {
|
||||
WarningCard(
|
||||
stringResource(id = R.string.require_kernel_version).format(
|
||||
ksuVersion, Natives.MINIMAL_SUPPORTED_KERNEL
|
||||
)
|
||||
)
|
||||
}
|
||||
if (ksuVersion != null && !rootAvailable()) {
|
||||
WarningCard(
|
||||
stringResource(id = R.string.grant_root_failed)
|
||||
)
|
||||
}
|
||||
val checkUpdate =
|
||||
LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||
.getBoolean("check_update", true)
|
||||
if (checkUpdate) {
|
||||
UpdateCard()
|
||||
}
|
||||
val prefs = remember { context.getSharedPreferences("app_prefs", Context.MODE_PRIVATE) }
|
||||
var clickCount by rememberSaveable { mutableIntStateOf(prefs.getInt("click_count", 0)) }
|
||||
|
||||
if (!isSimpleMode && clickCount < 3) {
|
||||
AnimatedVisibility(
|
||||
visible = clickCount < 3,
|
||||
exit = shrinkVertically() + fadeOut()
|
||||
) {
|
||||
ElevatedCard(
|
||||
colors = getCardColors(MaterialTheme.colorScheme.secondaryContainer),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = getCardElevation())
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
clickCount++
|
||||
prefs.edit { putInt("click_count", clickCount) }
|
||||
}
|
||||
.padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.using_mksu_manager),
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
InfoCard()
|
||||
if (!isSimpleMode) {
|
||||
ContributionCard()
|
||||
DonateCard()
|
||||
LearnMoreCard()
|
||||
}
|
||||
Spacer(Modifier)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UpdateCard() {
|
||||
val context = LocalContext.current
|
||||
val latestVersionInfo = LatestVersionInfo()
|
||||
val newVersion by produceState(initialValue = latestVersionInfo) {
|
||||
value = withContext(Dispatchers.IO) {
|
||||
checkNewVersion()
|
||||
}
|
||||
}
|
||||
|
||||
val currentVersionCode = getManagerVersion(context).second
|
||||
val newVersionCode = newVersion.versionCode
|
||||
val newVersionUrl = newVersion.downloadUrl
|
||||
val changelog = newVersion.changelog
|
||||
|
||||
Log.d("UpdateCard", "Current version code: $currentVersionCode")
|
||||
Log.d("UpdateCard", "New version code: $newVersionCode")
|
||||
|
||||
|
||||
|
||||
val uriHandler = LocalUriHandler.current
|
||||
val title = stringResource(id = R.string.module_changelog)
|
||||
val updateText = stringResource(id = R.string.module_update)
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = newVersionCode > currentVersionCode,
|
||||
enter = fadeIn() + expandVertically(),
|
||||
exit = shrinkVertically() + fadeOut()
|
||||
) {
|
||||
val updateDialog = rememberConfirmDialog(onConfirm = { uriHandler.openUri(newVersionUrl) })
|
||||
WarningCard(
|
||||
message = stringResource(id = R.string.new_version_available).format(newVersionCode),
|
||||
MaterialTheme.colorScheme.outlineVariant
|
||||
) {
|
||||
if (changelog.isEmpty()) {
|
||||
uriHandler.openUri(newVersionUrl)
|
||||
} else {
|
||||
updateDialog.showConfirm(
|
||||
title = title,
|
||||
content = changelog,
|
||||
markdown = true,
|
||||
confirm = updateText
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RebootDropdownItem(@StringRes id: Int, reason: String = "") {
|
||||
DropdownMenuItem(text = {
|
||||
Text(stringResource(id))
|
||||
}, onClick = {
|
||||
reboot(reason)
|
||||
})
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun TopBar(
|
||||
kernelVersion: KernelVersion,
|
||||
onInstallClick: () -> Unit,
|
||||
onSettingsClick: () -> Unit,
|
||||
scrollBehavior: TopAppBarScrollBehavior? = null
|
||||
) {
|
||||
val cardColor = MaterialTheme.colorScheme.secondaryContainer
|
||||
val cardAlpha = CardConfig.cardAlpha
|
||||
|
||||
TopAppBar(
|
||||
title = { Text(stringResource(R.string.app_name)) },
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = cardColor.copy(alpha = cardAlpha),
|
||||
scrolledContainerColor = cardColor.copy(alpha = cardAlpha)
|
||||
),
|
||||
actions = {
|
||||
if (kernelVersion.isGKI()) {
|
||||
IconButton(onClick = onInstallClick) {
|
||||
Icon(Icons.Filled.Archive, stringResource(R.string.install))
|
||||
}
|
||||
}
|
||||
|
||||
var showDropdown by remember { mutableStateOf(false) }
|
||||
IconButton(onClick = { showDropdown = true }) {
|
||||
Icon(Icons.Filled.Refresh, stringResource(R.string.reboot))
|
||||
DropdownMenu(expanded = showDropdown, onDismissRequest = { showDropdown = false }
|
||||
) {
|
||||
|
||||
RebootDropdownItem(id = R.string.reboot)
|
||||
|
||||
val pm = LocalContext.current.getSystemService(Context.POWER_SERVICE) as PowerManager?
|
||||
@Suppress("DEPRECATION")
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && pm?.isRebootingUserspaceSupported == true) {
|
||||
RebootDropdownItem(id = R.string.reboot_userspace, reason = "userspace")
|
||||
}
|
||||
RebootDropdownItem(id = R.string.reboot_recovery, reason = "recovery")
|
||||
RebootDropdownItem(id = R.string.reboot_bootloader, reason = "bootloader")
|
||||
RebootDropdownItem(id = R.string.reboot_download, reason = "download")
|
||||
RebootDropdownItem(id = R.string.reboot_edl, reason = "edl")
|
||||
}
|
||||
}
|
||||
},
|
||||
windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),
|
||||
scrollBehavior = scrollBehavior
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun StatusCard(
|
||||
kernelVersion: KernelVersion,
|
||||
ksuVersion: Int?,
|
||||
lkmMode: Boolean?,
|
||||
onClickInstall: () -> Unit = {}
|
||||
) {
|
||||
ElevatedCard(
|
||||
colors = getCardColors(MaterialTheme.colorScheme.secondaryContainer),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = getCardElevation())
|
||||
) {
|
||||
Row(modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
if (kernelVersion.isGKI()) {
|
||||
onClickInstall()
|
||||
}
|
||||
}
|
||||
.padding(24.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
when {
|
||||
ksuVersion != null -> {
|
||||
val safeMode = when {
|
||||
Natives.isSafeMode -> " [${stringResource(id = R.string.safe_mode)}]"
|
||||
else -> ""
|
||||
}
|
||||
|
||||
val workingMode = when (lkmMode) {
|
||||
null -> " <Non-GKI>"
|
||||
true -> " <LKM>"
|
||||
else -> " <GKI>"
|
||||
}
|
||||
|
||||
val workingText =
|
||||
"${stringResource(id = R.string.home_working)}$workingMode$safeMode"
|
||||
|
||||
val isHideVersion = LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||
.getBoolean("is_hide_version", false)
|
||||
|
||||
val isHideOtherInfo = LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||
.getBoolean("is_hide_other_info", false)
|
||||
|
||||
val isHideSusfsStatus = LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||
.getBoolean("is_hide_susfs_status", false)
|
||||
|
||||
Icon(Icons.Outlined.CheckCircle, stringResource(R.string.home_working))
|
||||
Column(Modifier.padding(start = 20.dp)) {
|
||||
Text(
|
||||
text = workingText,
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
if (!isHideVersion) {
|
||||
Spacer(Modifier.height(4.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.home_working_version, ksuVersion),
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
if (!isHideOtherInfo) {
|
||||
Spacer(Modifier.height(4.dp))
|
||||
Text(
|
||||
text = stringResource(
|
||||
R.string.home_superuser_count, getSuperuserCount()
|
||||
), style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
Spacer(Modifier.height(4.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.home_module_count, getModuleCount()),
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
val kpmVersion = getKpmVersion()
|
||||
if (kpmVersion.isNotEmpty() && !kpmVersion.startsWith("Error")) {
|
||||
Spacer(Modifier.height(4.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.home_kpm_module, getKpmModuleCount()),
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
if (!isHideSusfsStatus) {
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
|
||||
val suSFS = getSuSFS()
|
||||
if (lkmMode != true) {
|
||||
val translatedStatus = when (suSFS) {
|
||||
"Supported" -> stringResource(R.string.status_supported)
|
||||
"Not Supported" -> stringResource(R.string.status_not_supported)
|
||||
else -> stringResource(R.string.status_unknown)
|
||||
}
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.home_susfs, translatedStatus),
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
kernelVersion.isGKI() -> {
|
||||
Icon(Icons.Outlined.Warning, stringResource(R.string.home_not_installed))
|
||||
Column(Modifier.padding(start = 20.dp)) {
|
||||
Text(
|
||||
text = stringResource(R.string.home_not_installed),
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
Spacer(Modifier.height(4.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.home_click_to_install),
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
Icon(Icons.Outlined.Block, stringResource(R.string.home_unsupported))
|
||||
Column(Modifier.padding(start = 20.dp)) {
|
||||
Text(
|
||||
text = stringResource(R.string.home_unsupported),
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
Spacer(Modifier.height(4.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.home_unsupported_reason),
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun WarningCard(
|
||||
message: String, color: Color = MaterialTheme.colorScheme.error, onClick: (() -> Unit)? = null
|
||||
) {
|
||||
ElevatedCard(
|
||||
colors = getCardColors(MaterialTheme.colorScheme.secondaryContainer),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = getCardElevation())
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.then(onClick?.let { Modifier.clickable { it() } } ?: Modifier)
|
||||
.padding(24.dp)
|
||||
) {
|
||||
Text(
|
||||
text = message, style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@Composable
|
||||
fun ContributionCard() {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
val links = listOf("https://github.com/zako", "https://github.com/udochina")
|
||||
ElevatedCard(
|
||||
colors = getCardColors(MaterialTheme.colorScheme.secondaryContainer),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = getCardElevation())
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
val randomIndex = Random.nextInt(links.size)
|
||||
uriHandler.openUri(links[randomIndex])
|
||||
}
|
||||
.padding(24.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column {
|
||||
Text(
|
||||
text = stringResource(R.string.home_ContributionCard_kernelsu),
|
||||
style = MaterialTheme.typography.titleSmall
|
||||
)
|
||||
Spacer(Modifier.height(4.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.home_click_to_ContributionCard_kernelsu),
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LearnMoreCard() {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
val url = stringResource(R.string.home_learn_kernelsu_url)
|
||||
|
||||
ElevatedCard(
|
||||
colors = getCardColors(MaterialTheme.colorScheme.secondaryContainer),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = getCardElevation())
|
||||
) {
|
||||
|
||||
Row(modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
uriHandler.openUri(url)
|
||||
}
|
||||
.padding(24.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
Column {
|
||||
Text(
|
||||
text = stringResource(R.string.home_learn_kernelsu),
|
||||
style = MaterialTheme.typography.titleSmall
|
||||
)
|
||||
Spacer(Modifier.height(4.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.home_click_to_learn_kernelsu),
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DonateCard() {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
|
||||
ElevatedCard(
|
||||
colors = getCardColors(MaterialTheme.colorScheme.secondaryContainer),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = getCardElevation())
|
||||
) {
|
||||
|
||||
Row(modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
uriHandler.openUri("https://patreon.com/weishu")
|
||||
}
|
||||
.padding(24.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
Column {
|
||||
Text(
|
||||
text = stringResource(R.string.home_support_title),
|
||||
style = MaterialTheme.typography.titleSmall
|
||||
)
|
||||
Spacer(Modifier.height(4.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.home_support_content),
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun InfoCard() {
|
||||
val context = LocalContext.current
|
||||
val isSimpleMode = LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||
.getBoolean("is_simple_mode", false)
|
||||
|
||||
ElevatedCard(
|
||||
colors = getCardColors(MaterialTheme.colorScheme.secondaryContainer),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = getCardElevation())
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 24.dp, top = 24.dp, end = 24.dp, bottom = 16.dp)
|
||||
) withContext@{
|
||||
val contents = StringBuilder()
|
||||
val uname = Os.uname()
|
||||
|
||||
@Composable
|
||||
fun InfoCardItem(
|
||||
label: String,
|
||||
content: String,
|
||||
) {
|
||||
contents.appendLine(label).appendLine(content).appendLine()
|
||||
Text(text = label, style = MaterialTheme.typography.bodyLarge)
|
||||
Text(text = content, style = MaterialTheme.typography.bodyMedium)
|
||||
}
|
||||
|
||||
InfoCardItem(stringResource(R.string.home_kernel), uname.release)
|
||||
|
||||
if (!isSimpleMode) {
|
||||
Spacer(Modifier.height(16.dp))
|
||||
val androidVersion = Build.VERSION.RELEASE
|
||||
InfoCardItem(stringResource(R.string.home_android_version), androidVersion)
|
||||
}
|
||||
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
val deviceModel = getDeviceModel(context)
|
||||
InfoCardItem(stringResource(R.string.home_device_model), deviceModel)
|
||||
|
||||
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
val managerVersion = getManagerVersion(context)
|
||||
InfoCardItem(
|
||||
stringResource(R.string.home_manager_version),
|
||||
"${managerVersion.first} (${managerVersion.second})"
|
||||
)
|
||||
|
||||
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
InfoCardItem(stringResource(R.string.home_selinux_status), getSELinuxStatus())
|
||||
|
||||
|
||||
|
||||
if (!isSimpleMode) {
|
||||
val kpmVersion = getKpmVersion()
|
||||
var displayVersion: String
|
||||
val isKpmConfigured = checkKpmConfigured()
|
||||
|
||||
if (kpmVersion.isEmpty() || kpmVersion.startsWith("Error")) {
|
||||
val statusText = if (isKpmConfigured) {
|
||||
stringResource(R.string.kernel_patched)
|
||||
} else {
|
||||
stringResource(R.string.kernel_not_enabled)
|
||||
}
|
||||
displayVersion = "${stringResource(R.string.not_supported)} ($statusText)"
|
||||
} else {
|
||||
displayVersion = "${stringResource(R.string.supported)} ($kpmVersion)"
|
||||
}
|
||||
Spacer(Modifier.height(16.dp))
|
||||
InfoCardItem(stringResource(R.string.home_kpm_version), displayVersion)
|
||||
}
|
||||
|
||||
val isHideSusfsStatus = LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||
.getBoolean("is_hide_susfs_status", false)
|
||||
|
||||
if ((!isSimpleMode) && (!isHideSusfsStatus)) {
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
val suSFS = getSuSFS()
|
||||
if (suSFS == "Supported") {
|
||||
val suSFSVersion = getSuSFSVersion()
|
||||
if (suSFSVersion.isEmpty()) return@withContext
|
||||
val isSUS_SU = getSuSFSFeatures() == "CONFIG_KSU_SUSFS_SUS_SU"
|
||||
val infoText = buildString {
|
||||
append(suSFSVersion)
|
||||
append(if (isSUS_SU) " (${getSuSFSVariant()})" else " (${stringResource(R.string.manual_hook)})")
|
||||
if (isSUS_SU) {
|
||||
val susSUMode = try { susfsSUS_SU_Mode().toString() } catch (_: Exception) { "" }
|
||||
if (susSUMode.isNotEmpty()) {
|
||||
append(" ${stringResource(R.string.sus_su_mode)} $susSUMode")
|
||||
}
|
||||
}
|
||||
}
|
||||
InfoCardItem(
|
||||
stringResource(R.string.home_susfs_version),
|
||||
infoText
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getManagerVersion(context: Context): Pair<String, Long> {
|
||||
val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0)!!
|
||||
val versionCode = PackageInfoCompat.getLongVersionCode(packageInfo)
|
||||
return Pair(packageInfo.versionName!!, versionCode)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun StatusCardPreview() {
|
||||
Column {
|
||||
StatusCard(KernelVersion(5, 10, 101), 1, null)
|
||||
StatusCard(KernelVersion(5, 10, 101), 20000, true)
|
||||
StatusCard(KernelVersion(5, 10, 101), null, true)
|
||||
StatusCard(KernelVersion(4, 10, 101), null, false)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun WarningCardPreview() {
|
||||
Column {
|
||||
WarningCard(message = "Warning message")
|
||||
WarningCard(
|
||||
message = "Warning message ",
|
||||
MaterialTheme.colorScheme.outlineVariant,
|
||||
onClick = {})
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("PrivateApi")
|
||||
private fun getDeviceModel(context: Context): String {
|
||||
return try {
|
||||
val systemProperties = Class.forName("android.os.SystemProperties")
|
||||
val getMethod = systemProperties.getMethod("get", String::class.java, String::class.java)
|
||||
val marketNameKeys = listOf(
|
||||
"ro.product.marketname", // Xiaomi
|
||||
"ro.vendor.oplus.market.name", // Oppo, OnePlus, Realme
|
||||
"ro.vivo.market.name", // Vivo
|
||||
"ro.config.marketing_name" // Huawei
|
||||
)
|
||||
for (key in marketNameKeys) {
|
||||
val marketName = getMethod.invoke(null, key, "") as String
|
||||
if (marketName.isNotEmpty()) {
|
||||
return marketName
|
||||
}
|
||||
}
|
||||
Build.DEVICE
|
||||
} catch (e: Exception) {
|
||||
Build.DEVICE
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkKpmConfigured(): Boolean {
|
||||
try {
|
||||
val process = Runtime.getRuntime().exec("su -c cat /proc/config.gz")
|
||||
val inputStream = process.inputStream
|
||||
val gzipInputStream = GZIPInputStream(inputStream)
|
||||
val reader = BufferedReader(InputStreamReader(gzipInputStream))
|
||||
|
||||
var line: String?
|
||||
while (reader.readLine().also { line = it } != null) {
|
||||
if (line?.contains("CONFIG_KPM=y") == true) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
reader.close()
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -1,606 +0,0 @@
|
||||
package zako.zako.zako.ui.screen
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.widget.Toast
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.LocalIndication
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.selection.toggleable
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.FileUpload
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import com.maxkeppeler.sheets.list.models.ListOption
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||
import com.ramcosta.composedestinations.generated.destinations.FlashScreenDestination
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
import com.ramcosta.composedestinations.navigation.EmptyDestinationsNavigator
|
||||
import zako.zako.zako.ui.component.DialogHandle
|
||||
import zako.zako.zako.ui.component.rememberConfirmDialog
|
||||
import zako.zako.zako.ui.component.rememberCustomDialog
|
||||
import zako.zako.zako.ui.theme.ThemeConfig
|
||||
import zako.zako.zako.ui.theme.getCardColors
|
||||
import zako.zako.zako.ui.theme.getCardElevation
|
||||
import zako.zako.zako.ui.util.*
|
||||
import zako.zako.zako.R
|
||||
import zako.zako.zako.utils.AssetsUtil
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
|
||||
|
||||
/**
|
||||
* @author weishu
|
||||
* @date 2024/3/12.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Destination<RootGraph>
|
||||
@Composable
|
||||
fun InstallScreen(navigator: DestinationsNavigator) {
|
||||
var installMethod by remember { mutableStateOf<InstallMethod?>(null) }
|
||||
var lkmSelection by remember { mutableStateOf<LkmSelection>(LkmSelection.KmiNone) }
|
||||
val context = LocalContext.current
|
||||
|
||||
var showRebootDialog by remember { mutableStateOf(false) }
|
||||
|
||||
val onFlashComplete = {
|
||||
showRebootDialog = true
|
||||
}
|
||||
|
||||
if (showRebootDialog) {
|
||||
RebootDialog(
|
||||
show = true,
|
||||
onDismiss = { showRebootDialog = false },
|
||||
onConfirm = {
|
||||
showRebootDialog = false
|
||||
try {
|
||||
val process = Runtime.getRuntime().exec("su")
|
||||
process.outputStream.bufferedWriter().use { writer ->
|
||||
writer.write("svc power reboot\n")
|
||||
writer.write("exit\n")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Toast.makeText(context, R.string.failed_reboot, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
val onInstall = {
|
||||
installMethod?.let { method ->
|
||||
when (method) {
|
||||
is InstallMethod.HorizonKernel -> {
|
||||
method.uri?.let { uri ->
|
||||
val worker = HorizonKernelWorker(context)
|
||||
worker.uri = uri
|
||||
worker.setOnFlashCompleteListener(onFlashComplete)
|
||||
worker.start()
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
val flashIt = FlashIt.FlashBoot(
|
||||
boot = if (method is InstallMethod.SelectFile) method.uri else null,
|
||||
lkm = lkmSelection,
|
||||
ota = method is InstallMethod.DirectInstallToInactiveSlot
|
||||
)
|
||||
navigator.navigate(FlashScreenDestination(flashIt))
|
||||
}
|
||||
}
|
||||
}
|
||||
Unit
|
||||
}
|
||||
|
||||
val currentKmi by produceState(initialValue = "") {
|
||||
value = getCurrentKmi()
|
||||
}
|
||||
|
||||
val selectKmiDialog = rememberSelectKmiDialog { kmi ->
|
||||
kmi?.let {
|
||||
lkmSelection = LkmSelection.KmiString(it)
|
||||
onInstall()
|
||||
}
|
||||
}
|
||||
|
||||
val onClickNext = {
|
||||
if (lkmSelection == LkmSelection.KmiNone && currentKmi.isBlank()) {
|
||||
selectKmiDialog.show()
|
||||
} else {
|
||||
onInstall()
|
||||
}
|
||||
}
|
||||
|
||||
val selectLkmLauncher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.StartActivityForResult()
|
||||
) {
|
||||
if (it.resultCode == Activity.RESULT_OK) {
|
||||
it.data?.data?.let { uri ->
|
||||
lkmSelection = LkmSelection.LkmUri(uri)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val onLkmUpload = {
|
||||
selectLkmLauncher.launch(Intent(Intent.ACTION_GET_CONTENT).apply {
|
||||
type = "application/octet-stream"
|
||||
})
|
||||
}
|
||||
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopBar(
|
||||
onBack = { navigator.popBackStack() },
|
||||
onLkmUpload = onLkmUpload,
|
||||
scrollBehavior = scrollBehavior
|
||||
)
|
||||
},
|
||||
contentWindowInsets = WindowInsets.safeDrawing.only(
|
||||
WindowInsetsSides.Top + WindowInsetsSides.Horizontal
|
||||
)
|
||||
) { innerPadding ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(innerPadding)
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
SelectInstallMethod { method ->
|
||||
installMethod = method
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp)
|
||||
) {
|
||||
(lkmSelection as? LkmSelection.LkmUri)?.let {
|
||||
Text(
|
||||
stringResource(
|
||||
id = R.string.selected_lkm,
|
||||
it.uri.lastPathSegment ?: "(file)"
|
||||
)
|
||||
)
|
||||
}
|
||||
Button(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
enabled = installMethod != null,
|
||||
onClick = onClickNext
|
||||
) {
|
||||
Text(
|
||||
stringResource(id = R.string.install_next),
|
||||
fontSize = MaterialTheme.typography.bodyMedium.fontSize
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
private fun RebootDialog(
|
||||
show: Boolean,
|
||||
onDismiss: () -> Unit,
|
||||
onConfirm: () -> Unit
|
||||
) {
|
||||
if (show) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text(stringResource(id = R.string.reboot_complete_title)) },
|
||||
text = { Text(stringResource(id = R.string.reboot_complete_msg)) },
|
||||
confirmButton = {
|
||||
TextButton(onClick = onConfirm) {
|
||||
Text(stringResource(id = R.string.yes))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismiss) {
|
||||
Text(stringResource(id = R.string.no))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private class HorizonKernelWorker(private val context: Context) : Thread() {
|
||||
var uri: Uri? = null
|
||||
private lateinit var filePath: String
|
||||
private lateinit var binaryPath: String
|
||||
|
||||
|
||||
private var onFlashComplete: (() -> Unit)? = null
|
||||
|
||||
fun setOnFlashCompleteListener(listener: () -> Unit) {
|
||||
onFlashComplete = listener
|
||||
}
|
||||
|
||||
override fun run() {
|
||||
filePath = "${context.filesDir.absolutePath}/${DocumentFile.fromSingleUri(context, uri!!)?.name}"
|
||||
binaryPath = "${context.filesDir.absolutePath}/META-INF/com/google/android/update-binary"
|
||||
|
||||
try {
|
||||
cleanup()
|
||||
if (!rootAvailable()) {
|
||||
showError(context.getString(R.string.root_required))
|
||||
return
|
||||
}
|
||||
|
||||
copy()
|
||||
if (!File(filePath).exists()) {
|
||||
showError(context.getString(R.string.copy_failed))
|
||||
return
|
||||
}
|
||||
|
||||
getBinary()
|
||||
patch()
|
||||
flash()
|
||||
|
||||
(context as? Activity)?.runOnUiThread {
|
||||
onFlashComplete?.invoke()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
showError(e.message ?: context.getString(R.string.unknown_error))
|
||||
}
|
||||
}
|
||||
|
||||
private fun cleanup() {
|
||||
runCommand(false, "find ${context.filesDir.absolutePath} -type f ! -name '*.jpg' ! -name '*.png' -delete")
|
||||
}
|
||||
|
||||
private fun copy() {
|
||||
uri?.let { safeUri ->
|
||||
context.contentResolver.openInputStream(safeUri)?.use { input ->
|
||||
FileOutputStream(File(filePath)).use { output ->
|
||||
input.copyTo(output)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getBinary() {
|
||||
runCommand(false, "unzip \"$filePath\" \"*/update-binary\" -d ${context.filesDir.absolutePath}")
|
||||
if (!File(binaryPath).exists()) {
|
||||
throw IOException("Failed to extract update-binary")
|
||||
}
|
||||
}
|
||||
|
||||
private fun patch() {
|
||||
val mkbootfsPath = "${context.filesDir.absolutePath}/mkbootfs"
|
||||
AssetsUtil.exportFiles(context, "mkbootfs", mkbootfsPath)
|
||||
runCommand(false, "sed -i '/chmod -R 755 tools bin;/i cp -f $mkbootfsPath \$AKHOME/tools;' $binaryPath")
|
||||
}
|
||||
|
||||
private fun flash() {
|
||||
val process = ProcessBuilder("su")
|
||||
.redirectErrorStream(true)
|
||||
.start()
|
||||
|
||||
try {
|
||||
process.outputStream.bufferedWriter().use { writer ->
|
||||
writer.write("export POSTINSTALL=${context.filesDir.absolutePath}\n")
|
||||
writer.write("sh $binaryPath 3 1 \"$filePath\" && touch ${context.filesDir.absolutePath}/done\nexit\n")
|
||||
writer.flush()
|
||||
}
|
||||
|
||||
process.inputStream.bufferedReader().use { reader ->
|
||||
reader.lineSequence().forEach { line ->
|
||||
if (line.startsWith("ui_print")) {
|
||||
showLog(line.removePrefix("ui_print"))
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
process.destroy()
|
||||
}
|
||||
|
||||
if (!File("${context.filesDir.absolutePath}/done").exists()) {
|
||||
throw IOException("Flash failed")
|
||||
}
|
||||
}
|
||||
|
||||
private fun runCommand(su: Boolean, cmd: String): Int {
|
||||
val process = ProcessBuilder(if (su) "su" else "sh")
|
||||
.redirectErrorStream(true)
|
||||
.start()
|
||||
|
||||
return try {
|
||||
process.outputStream.bufferedWriter().use { writer ->
|
||||
writer.write("$cmd\n")
|
||||
writer.write("exit\n")
|
||||
writer.flush()
|
||||
}
|
||||
process.waitFor()
|
||||
} finally {
|
||||
process.destroy()
|
||||
}
|
||||
}
|
||||
|
||||
private fun showError(message: String) {
|
||||
(context as? Activity)?.runOnUiThread {
|
||||
Toast.makeText(context, message, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun showLog(message: String) {
|
||||
(context as? Activity)?.runOnUiThread {
|
||||
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed class InstallMethod {
|
||||
data class SelectFile(
|
||||
val uri: Uri? = null,
|
||||
@StringRes override val label: Int = R.string.select_file,
|
||||
override val summary: String?
|
||||
) : InstallMethod()
|
||||
|
||||
data object DirectInstall : InstallMethod() {
|
||||
override val label: Int
|
||||
get() = R.string.direct_install
|
||||
}
|
||||
|
||||
data object DirectInstallToInactiveSlot : InstallMethod() {
|
||||
override val label: Int
|
||||
get() = R.string.install_inactive_slot
|
||||
}
|
||||
|
||||
data class HorizonKernel(
|
||||
val uri: Uri? = null,
|
||||
@StringRes override val label: Int = R.string.horizon_kernel,
|
||||
override val summary: String? = null
|
||||
) : InstallMethod()
|
||||
|
||||
abstract val label: Int
|
||||
open val summary: String? = null
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SelectInstallMethod(onSelected: (InstallMethod) -> Unit = {}) {
|
||||
val rootAvailable = rootAvailable()
|
||||
val isAbDevice = isAbDevice()
|
||||
val selectFileTip = stringResource(
|
||||
id = R.string.select_file_tip,
|
||||
if (isInitBoot()) "init_boot" else "boot"
|
||||
)
|
||||
|
||||
val radioOptions = mutableListOf<InstallMethod>(
|
||||
InstallMethod.SelectFile(summary = selectFileTip)
|
||||
)
|
||||
|
||||
if (rootAvailable) {
|
||||
radioOptions.add(InstallMethod.DirectInstall)
|
||||
if (isAbDevice) {
|
||||
radioOptions.add(InstallMethod.DirectInstallToInactiveSlot)
|
||||
}
|
||||
radioOptions.add(InstallMethod.HorizonKernel(summary = "Flashing the Anykernel3 Kernel"))
|
||||
}
|
||||
|
||||
var selectedOption by remember { mutableStateOf<InstallMethod?>(null) }
|
||||
var currentSelectingMethod by remember { mutableStateOf<InstallMethod?>(null) }
|
||||
|
||||
val selectImageLauncher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.StartActivityForResult()
|
||||
) {
|
||||
if (it.resultCode == Activity.RESULT_OK) {
|
||||
it.data?.data?.let { uri ->
|
||||
val option = when (currentSelectingMethod) {
|
||||
is InstallMethod.SelectFile -> InstallMethod.SelectFile(uri, summary = selectFileTip)
|
||||
is InstallMethod.HorizonKernel -> InstallMethod.HorizonKernel(uri, summary = " Flashing the Anykernel3 Kernel")
|
||||
else -> null
|
||||
}
|
||||
option?.let {
|
||||
selectedOption = it
|
||||
onSelected(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val confirmDialog = rememberConfirmDialog(
|
||||
onConfirm = {
|
||||
selectedOption = InstallMethod.DirectInstallToInactiveSlot
|
||||
onSelected(InstallMethod.DirectInstallToInactiveSlot)
|
||||
},
|
||||
onDismiss = null
|
||||
)
|
||||
|
||||
val dialogTitle = stringResource(id = android.R.string.dialog_alert_title)
|
||||
val dialogContent = stringResource(id = R.string.install_inactive_slot_warning)
|
||||
|
||||
val onClick = { option: InstallMethod ->
|
||||
currentSelectingMethod = option
|
||||
when (option) {
|
||||
is InstallMethod.SelectFile, is InstallMethod.HorizonKernel -> {
|
||||
selectImageLauncher.launch(Intent(Intent.ACTION_GET_CONTENT).apply {
|
||||
type = "application/*"
|
||||
putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("application/octet-stream", "application/zip"))
|
||||
})
|
||||
}
|
||||
is InstallMethod.DirectInstall -> {
|
||||
selectedOption = option
|
||||
onSelected(option)
|
||||
}
|
||||
is InstallMethod.DirectInstallToInactiveSlot -> {
|
||||
confirmDialog.showConfirm(dialogTitle, dialogContent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
radioOptions.forEach { option ->
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.toggleable(
|
||||
value = option.javaClass == selectedOption?.javaClass,
|
||||
onValueChange = { onClick(option) },
|
||||
role = Role.RadioButton,
|
||||
indication = LocalIndication.current,
|
||||
interactionSource = interactionSource
|
||||
)
|
||||
) {
|
||||
RadioButton(
|
||||
selected = option.javaClass == selectedOption?.javaClass,
|
||||
onClick = { onClick(option) },
|
||||
interactionSource = interactionSource
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier.padding(vertical = 12.dp)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = option.label),
|
||||
fontSize = MaterialTheme.typography.titleMedium.fontSize,
|
||||
fontFamily = MaterialTheme.typography.titleMedium.fontFamily,
|
||||
fontStyle = MaterialTheme.typography.titleMedium.fontStyle
|
||||
)
|
||||
option.summary?.let {
|
||||
Text(
|
||||
text = it,
|
||||
fontSize = MaterialTheme.typography.bodySmall.fontSize,
|
||||
fontFamily = MaterialTheme.typography.bodySmall.fontFamily,
|
||||
fontStyle = MaterialTheme.typography.bodySmall.fontStyle
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun rememberSelectKmiDialog(onSelected: (String?) -> Unit): DialogHandle {
|
||||
return rememberCustomDialog { dismiss ->
|
||||
val supportedKmi by produceState(initialValue = emptyList<String>()) {
|
||||
value = getSupportedKmis()
|
||||
}
|
||||
val listOptions = supportedKmi.map { value ->
|
||||
ListOption(
|
||||
titleText = value,
|
||||
subtitleText = null,
|
||||
icon = null
|
||||
)
|
||||
}
|
||||
|
||||
var selection: String? = null
|
||||
val cardColor = if (!ThemeConfig.useDynamicColor) {
|
||||
ThemeConfig.currentTheme.ButtonContrast
|
||||
} else {
|
||||
MaterialTheme.colorScheme.secondaryContainer
|
||||
}
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = {
|
||||
dismiss()
|
||||
},
|
||||
title = {
|
||||
Text(text = stringResource(R.string.select_kmi))
|
||||
},
|
||||
text = {
|
||||
Column {
|
||||
listOptions.forEachIndexed { index, option ->
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.clickable {
|
||||
selection = supportedKmi[index]
|
||||
}
|
||||
.padding(vertical = 8.dp)
|
||||
) {
|
||||
Column {
|
||||
Text(text = option.titleText)
|
||||
option.subtitleText?.let {
|
||||
Text(
|
||||
text = it,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
if (selection != null) {
|
||||
onSelected(selection)
|
||||
}
|
||||
dismiss()
|
||||
}
|
||||
) {
|
||||
Text(text = stringResource(android.R.string.ok))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
dismiss()
|
||||
}
|
||||
) {
|
||||
Text(text = stringResource(android.R.string.cancel))
|
||||
}
|
||||
},
|
||||
containerColor = getCardColors(cardColor.copy(alpha = 0.9f)).containerColor.copy(alpha = 0.9f),
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
tonalElevation = getCardElevation()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun TopBar(
|
||||
onBack: () -> Unit = {},
|
||||
onLkmUpload: () -> Unit = {},
|
||||
scrollBehavior: TopAppBarScrollBehavior? = null
|
||||
) {
|
||||
TopAppBar(
|
||||
title = { Text(stringResource(R.string.install)) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null)
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
IconButton(onClick = onLkmUpload) {
|
||||
Icon(Icons.Filled.FileUpload, contentDescription = null)
|
||||
}
|
||||
},
|
||||
windowInsets = WindowInsets.safeDrawing.only(
|
||||
WindowInsetsSides.Top + WindowInsetsSides.Horizontal
|
||||
),
|
||||
scrollBehavior = scrollBehavior
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun SelectInstallPreview() {
|
||||
InstallScreen(EmptyDestinationsNavigator)
|
||||
}
|
||||
@@ -1,615 +0,0 @@
|
||||
package zako.zako.zako.ui.screen
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.DpSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.Slider
|
||||
import androidx.compose.material3.SliderDefaults
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import zako.zako.zako.ui.component.SwitchItem
|
||||
import zako.zako.zako.ui.theme.CardConfig
|
||||
import zako.zako.zako.ui.theme.ThemeColors
|
||||
import zako.zako.zako.ui.theme.ThemeConfig
|
||||
import zako.zako.zako.ui.theme.saveCustomBackground
|
||||
import zako.zako.zako.ui.theme.saveThemeColors
|
||||
import zako.zako.zako.ui.theme.saveThemeMode
|
||||
import zako.zako.zako.ui.theme.saveDynamicColorState
|
||||
import zako.zako.zako.ui.util.getSuSFS
|
||||
import zako.zako.zako.ui.util.getSuSFSFeatures
|
||||
import zako.zako.zako.ui.util.susfsSUS_SU_0
|
||||
import zako.zako.zako.ui.util.susfsSUS_SU_2
|
||||
import zako.zako.zako.ui.util.susfsSUS_SU_Mode
|
||||
import androidx.core.content.edit
|
||||
import zako.zako.zako.R
|
||||
|
||||
|
||||
fun saveCardConfig(context: Context) {
|
||||
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||
with(prefs.edit()) {
|
||||
putFloat("card_alpha", CardConfig.cardAlpha)
|
||||
putBoolean("custom_background_enabled", CardConfig.cardElevation == 0.dp)
|
||||
putBoolean("is_custom_alpha_set", CardConfig.isCustomAlphaSet)
|
||||
apply()
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Destination<RootGraph>
|
||||
@Composable
|
||||
fun MoreSettingsScreen(navigator: DestinationsNavigator) {
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||
val context = LocalContext.current
|
||||
val prefs = remember { context.getSharedPreferences("settings", Context.MODE_PRIVATE) }
|
||||
// 主题模式选择
|
||||
var themeMode by remember {
|
||||
mutableIntStateOf(
|
||||
when(ThemeConfig.forceDarkMode) {
|
||||
true -> 2 // 深色
|
||||
false -> 1 // 浅色
|
||||
null -> 0 // 跟随系统
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// 动态颜色开关状态
|
||||
var useDynamicColor by remember {
|
||||
mutableStateOf(ThemeConfig.useDynamicColor)
|
||||
}
|
||||
|
||||
var showThemeModeDialog by remember { mutableStateOf(false) }
|
||||
// 主题模式选项
|
||||
val themeOptions = listOf(
|
||||
stringResource(R.string.theme_follow_system),
|
||||
stringResource(R.string.theme_light),
|
||||
stringResource(R.string.theme_dark)
|
||||
)
|
||||
|
||||
// 简洁模块开关状态
|
||||
var isSimpleMode by remember {
|
||||
mutableStateOf(prefs.getBoolean("is_simple_mode", false))
|
||||
}
|
||||
|
||||
// 更新简洁模块开关状态
|
||||
val onSimpleModeChange = { newValue: Boolean ->
|
||||
prefs.edit { putBoolean("is_simple_mode", newValue) }
|
||||
isSimpleMode = newValue
|
||||
}
|
||||
|
||||
// 隐藏内核 KernelSU 版本号开关状态
|
||||
var isHideVersion by remember {
|
||||
mutableStateOf(prefs.getBoolean("is_hide_version", false))
|
||||
}
|
||||
|
||||
// 隐藏内核 KernelSU 版本号模块开关状态
|
||||
val onHideVersionChange = { newValue: Boolean ->
|
||||
prefs.edit { putBoolean("is_hide_version", newValue) }
|
||||
isHideVersion = newValue
|
||||
}
|
||||
|
||||
// 隐藏模块数量等信息开关状态
|
||||
var isHideOtherInfo by remember {
|
||||
mutableStateOf(prefs.getBoolean("is_hide_other_info", false))
|
||||
}
|
||||
|
||||
// 隐藏模块数量等信息开关状态
|
||||
val onHideOtherInfoChange = { newValue: Boolean ->
|
||||
prefs.edit { putBoolean("is_hide_other_info", newValue) }
|
||||
isHideOtherInfo = newValue
|
||||
}
|
||||
|
||||
// 隐藏 SuSFS 状态开关状态
|
||||
var isHideSusfsStatus by remember {
|
||||
mutableStateOf(prefs.getBoolean("is_hide_susfs_status", false))
|
||||
}
|
||||
|
||||
// 隐藏 SuSFS 状态开关状态
|
||||
val onHideSusfsStatusChange = { newValue: Boolean ->
|
||||
prefs.edit { putBoolean("is_hide_susfs_status", newValue) }
|
||||
isHideSusfsStatus = newValue
|
||||
}
|
||||
|
||||
// SELinux 状态
|
||||
var selinuxEnabled by remember {
|
||||
mutableStateOf(Shell.cmd("getenforce").exec().out.firstOrNull() == "Enforcing")
|
||||
}
|
||||
|
||||
// 卡片配置状态
|
||||
var cardAlpha by rememberSaveable { mutableFloatStateOf(CardConfig.cardAlpha) }
|
||||
var showCardSettings by remember { mutableStateOf(false) }
|
||||
var isCustomBackgroundEnabled by rememberSaveable {
|
||||
mutableStateOf(ThemeConfig.customBackgroundUri != null)
|
||||
}
|
||||
|
||||
// 初始化卡片配置
|
||||
val systemIsDark = isSystemInDarkTheme()
|
||||
LaunchedEffect(Unit) {
|
||||
CardConfig.apply {
|
||||
cardAlpha = prefs.getFloat("card_alpha", 0.45f)
|
||||
cardElevation = if (prefs.getBoolean("custom_background_enabled", false)) 0.dp else defaultElevation
|
||||
isCustomAlphaSet = prefs.getBoolean("is_custom_alpha_set", false)
|
||||
|
||||
// 如果没有手动设置透明度,且是深色模式,则使用默认值
|
||||
if (!isCustomAlphaSet) {
|
||||
val isDarkMode = ThemeConfig.forceDarkMode ?: systemIsDark
|
||||
if (isDarkMode) {
|
||||
cardAlpha = 0.35f
|
||||
}
|
||||
}
|
||||
}
|
||||
themeMode = when (ThemeConfig.forceDarkMode) {
|
||||
true -> 2
|
||||
false -> 1
|
||||
null -> 0
|
||||
}
|
||||
}
|
||||
|
||||
// 主题色选项
|
||||
val themeColorOptions = listOf(
|
||||
stringResource(R.string.color_default) to ThemeColors.Default,
|
||||
stringResource(R.string.color_blue) to ThemeColors.Blue,
|
||||
stringResource(R.string.color_green) to ThemeColors.Green,
|
||||
stringResource(R.string.color_purple) to ThemeColors.Purple,
|
||||
stringResource(R.string.color_orange) to ThemeColors.Orange,
|
||||
stringResource(R.string.color_pink) to ThemeColors.Pink,
|
||||
stringResource(R.string.color_gray) to ThemeColors.Gray,
|
||||
stringResource(R.string.color_yellow) to ThemeColors.Yellow
|
||||
)
|
||||
|
||||
var showThemeColorDialog by remember { mutableStateOf(false) }
|
||||
|
||||
// 图片选择器
|
||||
val pickImageLauncher = rememberLauncherForActivityResult(
|
||||
ActivityResultContracts.GetContent()
|
||||
) { uri: Uri? ->
|
||||
uri?.let {
|
||||
context.saveCustomBackground(it)
|
||||
isCustomBackgroundEnabled = true
|
||||
CardConfig.cardElevation = 0.dp
|
||||
saveCardConfig(context)
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text(stringResource(R.string.more_settings)) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = { navigator.popBackStack() }) {
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, null)
|
||||
}
|
||||
},
|
||||
scrollBehavior = scrollBehavior
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(top = 12.dp)
|
||||
) {
|
||||
// SELinux 开关
|
||||
SwitchItem(
|
||||
icon = Icons.Filled.Security,
|
||||
title = stringResource(R.string.selinux),
|
||||
summary = if (selinuxEnabled)
|
||||
stringResource(R.string.selinux_enabled) else
|
||||
stringResource(R.string.selinux_disabled),
|
||||
checked = selinuxEnabled
|
||||
) { enabled ->
|
||||
val command = if (enabled) "setenforce 1" else "setenforce 0"
|
||||
Shell.getShell().newJob().add(command).exec().let { result ->
|
||||
if (result.isSuccess) selinuxEnabled = enabled
|
||||
}
|
||||
}
|
||||
|
||||
var isExpanded by remember { mutableStateOf(false) }
|
||||
|
||||
ListItem(
|
||||
leadingContent = { Icon(Icons.Filled.AutoFixHigh, null) },
|
||||
headlineContent = { Text(stringResource(R.string.custom_settings)) },
|
||||
modifier = Modifier.clickable {
|
||||
isExpanded = !isExpanded
|
||||
}
|
||||
)
|
||||
AnimatedVisibility(
|
||||
visible = isExpanded,
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp)
|
||||
) {
|
||||
// 添加简洁模块开关
|
||||
SwitchItem(
|
||||
icon = Icons.Filled.Brush,
|
||||
title = stringResource(R.string.simple_mode),
|
||||
summary = stringResource(R.string.simple_mode_summary),
|
||||
checked = isSimpleMode
|
||||
) {
|
||||
onSimpleModeChange(it)
|
||||
}
|
||||
}
|
||||
AnimatedVisibility(
|
||||
visible = isExpanded,
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp)
|
||||
) {
|
||||
// 隐藏内核部分版本号
|
||||
SwitchItem(
|
||||
icon = Icons.Filled.VisibilityOff,
|
||||
title = stringResource(R.string.hide_kernel_kernelsu_version),
|
||||
summary = stringResource(R.string.hide_kernel_kernelsu_version_summary),
|
||||
checked = isHideVersion
|
||||
) {
|
||||
onHideVersionChange(it)
|
||||
}
|
||||
}
|
||||
AnimatedVisibility(
|
||||
visible = isExpanded,
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp)
|
||||
) {
|
||||
// 模块数量等信息
|
||||
SwitchItem(
|
||||
icon = Icons.Filled.VisibilityOff,
|
||||
title = stringResource(R.string.hide_other_info),
|
||||
summary = stringResource(R.string.hide_other_info_summary),
|
||||
checked = isHideOtherInfo
|
||||
) {
|
||||
onHideOtherInfoChange(it)
|
||||
}
|
||||
}
|
||||
AnimatedVisibility(
|
||||
visible = isExpanded,
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp)
|
||||
) {
|
||||
// SuSFS 状态信息
|
||||
SwitchItem(
|
||||
icon = Icons.Filled.VisibilityOff,
|
||||
title = stringResource(R.string.hide_susfs_status),
|
||||
summary = stringResource(R.string.hide_susfs_status_summary),
|
||||
checked = isHideSusfsStatus
|
||||
) {
|
||||
onHideSusfsStatusChange(it)
|
||||
}
|
||||
}
|
||||
|
||||
// region SUSFS 配置(仅在支持时显示)
|
||||
val suSFS = getSuSFS()
|
||||
val isSUS_SU = getSuSFSFeatures()
|
||||
if (suSFS == "Supported") {
|
||||
if (isSUS_SU == "CONFIG_KSU_SUSFS_SUS_SU") {
|
||||
// 初始化时,默认启用
|
||||
var isEnabled by rememberSaveable {
|
||||
mutableStateOf(true) // 默认启用
|
||||
}
|
||||
|
||||
// 在启动时检查状态
|
||||
LaunchedEffect(Unit) {
|
||||
// 如果当前模式不是2就强制启用
|
||||
val currentMode = susfsSUS_SU_Mode()
|
||||
val wasManuallyDisabled = prefs.getBoolean("enable_sus_su", true)
|
||||
if (currentMode != "2" && wasManuallyDisabled) {
|
||||
susfsSUS_SU_2() // 强制切换到模式2
|
||||
prefs.edit { putBoolean("enable_sus_su", true) }
|
||||
}
|
||||
isEnabled = currentMode == "2"
|
||||
}
|
||||
|
||||
SwitchItem(
|
||||
icon = Icons.Filled.VisibilityOff,
|
||||
title = stringResource(id = R.string.settings_susfs_toggle),
|
||||
summary = stringResource(id = R.string.settings_susfs_toggle_summary),
|
||||
checked = isEnabled
|
||||
) {
|
||||
if (it) {
|
||||
// 手动启用
|
||||
susfsSUS_SU_2()
|
||||
prefs.edit { putBoolean("enable_sus_su", true) }
|
||||
} else {
|
||||
// 手动关闭
|
||||
susfsSUS_SU_0()
|
||||
prefs.edit { putBoolean("enable_sus_su", false) }
|
||||
}
|
||||
isEnabled = it
|
||||
}
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
// 动态颜色开关
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
SwitchItem(
|
||||
icon = Icons.Filled.ColorLens,
|
||||
title = stringResource(R.string.dynamic_color_title),
|
||||
summary = stringResource(R.string.dynamic_color_summary),
|
||||
checked = useDynamicColor
|
||||
) { enabled ->
|
||||
useDynamicColor = enabled
|
||||
context.saveDynamicColorState(enabled)
|
||||
}
|
||||
}
|
||||
// 只在未启用动态颜色时显示主题色选择
|
||||
AnimatedVisibility(
|
||||
visible = !useDynamicColor
|
||||
) {
|
||||
ListItem(
|
||||
leadingContent = { Icon(Icons.Default.Palette, null) },
|
||||
headlineContent = { Text(stringResource(R.string.theme_color)) },
|
||||
supportingContent = {
|
||||
val currentThemeName = when (ThemeConfig.currentTheme) {
|
||||
is ThemeColors.Default -> stringResource(R.string.color_default)
|
||||
is ThemeColors.Blue -> stringResource(R.string.color_blue)
|
||||
is ThemeColors.Green -> stringResource(R.string.color_green)
|
||||
is ThemeColors.Purple -> stringResource(R.string.color_purple)
|
||||
is ThemeColors.Orange -> stringResource(R.string.color_orange)
|
||||
is ThemeColors.Pink -> stringResource(R.string.color_pink)
|
||||
is ThemeColors.Gray -> stringResource(R.string.color_gray)
|
||||
is ThemeColors.Yellow -> stringResource(R.string.color_yellow)
|
||||
else -> stringResource(R.string.color_default)
|
||||
}
|
||||
Text(currentThemeName)
|
||||
},
|
||||
modifier = Modifier.clickable { showThemeColorDialog = true }
|
||||
)
|
||||
|
||||
if (showThemeColorDialog) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { showThemeColorDialog = false },
|
||||
title = { Text(stringResource(R.string.choose_theme_color)) },
|
||||
text = {
|
||||
Column {
|
||||
themeColorOptions.forEach { (name, theme) ->
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
context.saveThemeColors(when (theme) {
|
||||
ThemeColors.Default -> "default"
|
||||
ThemeColors.Blue -> "blue"
|
||||
ThemeColors.Green -> "green"
|
||||
ThemeColors.Purple -> "purple"
|
||||
ThemeColors.Orange -> "orange"
|
||||
ThemeColors.Pink -> "pink"
|
||||
ThemeColors.Gray -> "gray"
|
||||
ThemeColors.Yellow -> "yellow"
|
||||
else -> "default"
|
||||
})
|
||||
showThemeColorDialog = false
|
||||
}
|
||||
.padding(vertical = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
RadioButton(
|
||||
selected = ThemeConfig.currentTheme::class == theme::class,
|
||||
onClick = null
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(24.dp)
|
||||
.background(theme.Primary, shape = CircleShape)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(name)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
// 自定义背景开关
|
||||
ListItem(
|
||||
leadingContent = { Icon(Icons.Filled.Wallpaper, null) },
|
||||
headlineContent = { Text(stringResource(id = R.string.settings_custom_background)) },
|
||||
supportingContent = { Text(stringResource(id = R.string.settings_custom_background_summary)) },
|
||||
modifier = Modifier.clickable {
|
||||
if (isCustomBackgroundEnabled) {
|
||||
showCardSettings = !showCardSettings
|
||||
}
|
||||
},
|
||||
trailingContent = {
|
||||
Switch(
|
||||
checked = isCustomBackgroundEnabled,
|
||||
onCheckedChange = { isChecked ->
|
||||
if (isChecked) {
|
||||
pickImageLauncher.launch("image/*")
|
||||
} else {
|
||||
context.saveCustomBackground(null)
|
||||
isCustomBackgroundEnabled = false
|
||||
CardConfig.cardElevation = CardConfig.defaultElevation
|
||||
CardConfig.cardAlpha = 0.45f
|
||||
CardConfig.isCustomAlphaSet = false
|
||||
saveCardConfig(context)
|
||||
cardAlpha = 0.35f
|
||||
themeMode = 0
|
||||
context.saveThemeMode(null)
|
||||
CardConfig.isUserDarkModeEnabled = false
|
||||
CardConfig.isUserLightModeEnabled = false
|
||||
CardConfig.save(context)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
// 透明度 Slider
|
||||
AnimatedVisibility(
|
||||
visible = ThemeConfig.customBackgroundUri != null && showCardSettings,
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp)
|
||||
) {
|
||||
ListItem(
|
||||
leadingContent = { Icon(Icons.Filled.Opacity, null) },
|
||||
headlineContent = { Text(stringResource(R.string.settings_card_alpha)) },
|
||||
supportingContent = {
|
||||
Slider(
|
||||
value = cardAlpha,
|
||||
onValueChange = { newValue ->
|
||||
cardAlpha = newValue
|
||||
CardConfig.cardAlpha = newValue
|
||||
CardConfig.isCustomAlphaSet = true
|
||||
prefs.edit { putBoolean("is_custom_alpha_set", true) }
|
||||
prefs.edit { putFloat("card_alpha", newValue) }
|
||||
},
|
||||
onValueChangeFinished = {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
saveCardConfig(context)
|
||||
}
|
||||
},
|
||||
valueRange = 0f..1f,
|
||||
colors = getSliderColors(cardAlpha, useCustomColors = true),
|
||||
thumb = {
|
||||
SliderDefaults.Thumb(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
thumbSize = DpSize(0.dp, 0.dp)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
AnimatedVisibility(
|
||||
visible = ThemeConfig.customBackgroundUri != null && showCardSettings,
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp)
|
||||
){
|
||||
ListItem(
|
||||
leadingContent = { Icon(Icons.Filled.DarkMode, null) },
|
||||
headlineContent = { Text(stringResource(R.string.theme_mode)) },
|
||||
supportingContent = { Text(themeOptions[themeMode]) },
|
||||
modifier = Modifier.clickable {
|
||||
showThemeModeDialog = true
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// 主题模式选择对话框
|
||||
if (showThemeModeDialog) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { showThemeModeDialog = false },
|
||||
title = { Text(stringResource(R.string.theme_mode)) },
|
||||
text = {
|
||||
Column {
|
||||
themeOptions.forEachIndexed { index, option ->
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
themeMode = index
|
||||
val newThemeMode = when(index) {
|
||||
0 -> null // 跟随系统
|
||||
1 -> false // 浅色
|
||||
2 -> true // 深色
|
||||
else -> null
|
||||
}
|
||||
context.saveThemeMode(newThemeMode)
|
||||
when (index) {
|
||||
2 -> {
|
||||
ThemeConfig.forceDarkMode = true
|
||||
CardConfig.isUserLightModeEnabled = false
|
||||
CardConfig.isUserDarkModeEnabled = true
|
||||
CardConfig.save(context)
|
||||
}
|
||||
1 -> {
|
||||
ThemeConfig.forceDarkMode = false
|
||||
CardConfig.isUserLightModeEnabled = true
|
||||
CardConfig.isUserDarkModeEnabled = false
|
||||
CardConfig.save(context)
|
||||
}
|
||||
0 -> {
|
||||
ThemeConfig.forceDarkMode = null
|
||||
CardConfig.isUserLightModeEnabled = false
|
||||
CardConfig.isUserDarkModeEnabled = false
|
||||
CardConfig.save(context)
|
||||
}
|
||||
}
|
||||
showThemeModeDialog = false
|
||||
}
|
||||
.padding(vertical = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
RadioButton(
|
||||
selected = themeMode == index,
|
||||
onClick = null
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(option)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
private fun getSliderColors(cardAlpha: Float, useCustomColors: Boolean = false): SliderColors {
|
||||
val theme = ThemeConfig.currentTheme
|
||||
val isDarkTheme = ThemeConfig.forceDarkMode ?: isSystemInDarkTheme()
|
||||
val useDynamicColor = ThemeConfig.useDynamicColor
|
||||
|
||||
return when {
|
||||
// 使用动态颜色时
|
||||
useDynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
||||
SliderDefaults.colors(
|
||||
activeTrackColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.8f),
|
||||
inactiveTrackColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.3f),
|
||||
thumbColor = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
// 使用自定义主题色时
|
||||
useCustomColors -> {
|
||||
SliderDefaults.colors(
|
||||
activeTrackColor = theme.getCustomSliderActiveColor(),
|
||||
inactiveTrackColor = theme.getCustomSliderInactiveColor(),
|
||||
thumbColor = theme.Primary
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
val activeColor = if (isDarkTheme) {
|
||||
theme.Primary.copy(alpha = cardAlpha)
|
||||
} else {
|
||||
theme.Primary.copy(alpha = cardAlpha)
|
||||
}
|
||||
val inactiveColor = if (isDarkTheme) {
|
||||
Color.DarkGray.copy(alpha = 0.3f)
|
||||
} else {
|
||||
Color.LightGray.copy(alpha = 0.3f)
|
||||
}
|
||||
SliderDefaults.colors(
|
||||
activeTrackColor = activeColor,
|
||||
inactiveTrackColor = inactiveColor,
|
||||
thumbColor = activeColor
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,547 +0,0 @@
|
||||
package zako.zako.zako.ui.screen
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.widget.Toast
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||
import androidx.compose.foundation.layout.only
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.safeDrawing
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.Undo
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||
import androidx.compose.material3.rememberTopAppBarState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.LineHeightStyle
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.core.content.edit
|
||||
import com.maxkeppeker.sheets.core.models.base.IconSource
|
||||
import com.maxkeppeler.sheets.list.models.ListOption
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||
import com.ramcosta.composedestinations.generated.destinations.AppProfileTemplateScreenDestination
|
||||
import com.ramcosta.composedestinations.generated.destinations.FlashScreenDestination
|
||||
import com.ramcosta.composedestinations.generated.destinations.MoreSettingsScreenDestination
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
import com.ramcosta.composedestinations.navigation.EmptyDestinationsNavigator
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import zako.zako.zako.BuildConfig
|
||||
import zako.zako.zako.Natives
|
||||
import zako.zako.zako.R
|
||||
import zako.zako.zako.ui.component.AboutDialog
|
||||
import zako.zako.zako.ui.component.ConfirmResult
|
||||
import zako.zako.zako.ui.component.DialogHandle
|
||||
import zako.zako.zako.ui.component.SwitchItem
|
||||
import zako.zako.zako.ui.component.rememberConfirmDialog
|
||||
import zako.zako.zako.ui.component.rememberCustomDialog
|
||||
import zako.zako.zako.ui.component.rememberLoadingDialog
|
||||
import zako.zako.zako.ui.theme.CardConfig
|
||||
import zako.zako.zako.ui.theme.ThemeConfig
|
||||
import zako.zako.zako.ui.theme.getCardColors
|
||||
import zako.zako.zako.ui.theme.getCardElevation
|
||||
import zako.zako.zako.ui.util.LocalSnackbarHost
|
||||
import zako.zako.zako.ui.util.getBugreportFile
|
||||
import java.time.LocalDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
|
||||
/**
|
||||
* @author weishu
|
||||
* @date 2023/1/1.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Destination<RootGraph>
|
||||
@Composable
|
||||
fun SettingScreen(navigator: DestinationsNavigator) {
|
||||
// region 界面基础设置
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||
val snackBarHost = LocalSnackbarHost.current
|
||||
// endregion
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopBar(
|
||||
scrollBehavior = scrollBehavior
|
||||
)
|
||||
},
|
||||
snackbarHost = { SnackbarHost(snackBarHost) },
|
||||
contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal)
|
||||
) { paddingValues ->
|
||||
val aboutDialog = rememberCustomDialog {
|
||||
AboutDialog(it)
|
||||
}
|
||||
val loadingDialog = rememberLoadingDialog()
|
||||
// endregion
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
// region 上下文与协程
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
// endregion
|
||||
|
||||
// region 日志导出功能
|
||||
val exportBugreportLauncher = rememberLauncherForActivityResult(
|
||||
ActivityResultContracts.CreateDocument("application/gzip")
|
||||
) { uri: Uri? ->
|
||||
if (uri == null) return@rememberLauncherForActivityResult
|
||||
scope.launch(Dispatchers.IO) {
|
||||
loadingDialog.show()
|
||||
context.contentResolver.openOutputStream(uri)?.use { output ->
|
||||
getBugreportFile(context).inputStream().use {
|
||||
it.copyTo(output)
|
||||
}
|
||||
}
|
||||
loadingDialog.hide()
|
||||
snackBarHost.showSnackbar(context.getString(R.string.log_saved))
|
||||
}
|
||||
}
|
||||
// region 配置项列表
|
||||
// 配置文件模板入口
|
||||
val profileTemplate = stringResource(id = R.string.settings_profile_template)
|
||||
ListItem(
|
||||
leadingContent = { Icon(Icons.Filled.Fence, profileTemplate) },
|
||||
headlineContent = { Text(profileTemplate) },
|
||||
supportingContent = { Text(stringResource(id = R.string.settings_profile_template_summary)) },
|
||||
modifier = Modifier.clickable {
|
||||
navigator.navigate(AppProfileTemplateScreenDestination)
|
||||
}
|
||||
)
|
||||
// 卸载模块开关
|
||||
var umountChecked by rememberSaveable {
|
||||
mutableStateOf(Natives.isDefaultUmountModules())
|
||||
}
|
||||
SwitchItem(
|
||||
icon = Icons.Filled.FolderDelete,
|
||||
title = stringResource(id = R.string.settings_umount_modules_default),
|
||||
summary = stringResource(id = R.string.settings_umount_modules_default_summary),
|
||||
checked = umountChecked
|
||||
) {
|
||||
if (Natives.setDefaultUmountModules(it)) {
|
||||
umountChecked = it
|
||||
}
|
||||
}
|
||||
// SU 禁用开关(仅在兼容版本显示)
|
||||
if (Natives.version >= Natives.MINIMAL_SUPPORTED_SU_COMPAT) {
|
||||
var isSuDisabled by rememberSaveable {
|
||||
mutableStateOf(!Natives.isSuEnabled())
|
||||
}
|
||||
SwitchItem(
|
||||
icon = Icons.Filled.RemoveModerator,
|
||||
title = stringResource(id = R.string.settings_disable_su),
|
||||
summary = stringResource(id = R.string.settings_disable_su_summary),
|
||||
checked = isSuDisabled,
|
||||
) { checked ->
|
||||
val shouldEnable = !checked
|
||||
if (Natives.setSuEnabled(shouldEnable)) {
|
||||
isSuDisabled = !shouldEnable
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||
|
||||
// 更新检查开关
|
||||
var checkUpdate by rememberSaveable {
|
||||
mutableStateOf(
|
||||
prefs.getBoolean("check_update", true)
|
||||
)
|
||||
}
|
||||
SwitchItem(
|
||||
icon = Icons.Filled.Update,
|
||||
title = stringResource(id = R.string.settings_check_update),
|
||||
summary = stringResource(id = R.string.settings_check_update_summary),
|
||||
checked = checkUpdate
|
||||
) {
|
||||
prefs.edit {putBoolean("check_update", it) }
|
||||
checkUpdate = it
|
||||
}
|
||||
|
||||
// Web调试开关
|
||||
var enableWebDebugging by rememberSaveable {
|
||||
mutableStateOf(
|
||||
prefs.getBoolean("enable_web_debugging", false)
|
||||
)
|
||||
}
|
||||
SwitchItem(
|
||||
icon = Icons.Filled.DeveloperMode,
|
||||
title = stringResource(id = R.string.enable_web_debugging),
|
||||
summary = stringResource(id = R.string.enable_web_debugging_summary),
|
||||
checked = enableWebDebugging
|
||||
) {
|
||||
prefs.edit { putBoolean("enable_web_debugging", it) }
|
||||
enableWebDebugging = it
|
||||
}
|
||||
// 更多设置
|
||||
val newButtonTitle = stringResource(id = R.string.more_settings)
|
||||
ListItem(
|
||||
leadingContent = {
|
||||
Icon(
|
||||
Icons.Filled.Settings,
|
||||
contentDescription = newButtonTitle
|
||||
)
|
||||
},
|
||||
headlineContent = { Text(newButtonTitle) },
|
||||
supportingContent = { Text(stringResource(id = R.string.more_settings)) },
|
||||
modifier = Modifier.clickable {
|
||||
navigator.navigate(MoreSettingsScreenDestination)
|
||||
}
|
||||
)
|
||||
|
||||
var showBottomsheet by remember { mutableStateOf(false) }
|
||||
|
||||
ListItem(
|
||||
leadingContent = {
|
||||
Icon(
|
||||
Icons.Filled.BugReport,
|
||||
stringResource(id = R.string.send_log)
|
||||
)
|
||||
},
|
||||
headlineContent = { Text(stringResource(id = R.string.send_log)) },
|
||||
modifier = Modifier.clickable {
|
||||
showBottomsheet = true
|
||||
}
|
||||
)
|
||||
if (showBottomsheet) {
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = { showBottomsheet = false },
|
||||
content = {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(10.dp)
|
||||
.align(Alignment.CenterHorizontally)
|
||||
|
||||
) {
|
||||
Box {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(16.dp)
|
||||
.clickable {
|
||||
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH_mm")
|
||||
val current = LocalDateTime.now().format(formatter)
|
||||
exportBugreportLauncher.launch("KernelSU_bugreport_${current}.tar.gz")
|
||||
showBottomsheet = false
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
Icons.Filled.Save,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally)
|
||||
)
|
||||
Text(
|
||||
text = stringResource(id = R.string.save_log),
|
||||
modifier = Modifier.padding(top = 16.dp),
|
||||
textAlign = TextAlign.Center.also {
|
||||
LineHeightStyle(
|
||||
alignment = LineHeightStyle.Alignment.Center,
|
||||
trim = LineHeightStyle.Trim.None
|
||||
)
|
||||
}
|
||||
|
||||
)
|
||||
}
|
||||
}
|
||||
Box {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(16.dp)
|
||||
.clickable {
|
||||
scope.launch {
|
||||
val bugreport = loadingDialog.withLoading {
|
||||
withContext(Dispatchers.IO) {
|
||||
getBugreportFile(context)
|
||||
}
|
||||
}
|
||||
|
||||
val uri: Uri =
|
||||
FileProvider.getUriForFile(
|
||||
context,
|
||||
"${BuildConfig.APPLICATION_ID}.fileprovider",
|
||||
bugreport
|
||||
)
|
||||
|
||||
val shareIntent = Intent(Intent.ACTION_SEND).apply {
|
||||
putExtra(Intent.EXTRA_STREAM, uri)
|
||||
setDataAndType(uri, "application/gzip")
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
}
|
||||
|
||||
context.startActivity(
|
||||
Intent.createChooser(
|
||||
shareIntent,
|
||||
context.getString(R.string.send_log)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
Icons.Filled.Share,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally)
|
||||
)
|
||||
Text(
|
||||
text = stringResource(id = R.string.send_log),
|
||||
modifier = Modifier.padding(top = 16.dp),
|
||||
textAlign = TextAlign.Center.also {
|
||||
LineHeightStyle(
|
||||
alignment = LineHeightStyle.Alignment.Center,
|
||||
trim = LineHeightStyle.Trim.None
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
val lkmMode = Natives.version >= Natives.MINIMAL_SUPPORTED_KERNEL_LKM && Natives.isLkmMode
|
||||
if (lkmMode) {
|
||||
UninstallItem(navigator) {
|
||||
loadingDialog.withLoading(it)
|
||||
}
|
||||
}
|
||||
|
||||
val about = stringResource(id = R.string.about)
|
||||
ListItem(
|
||||
leadingContent = {
|
||||
Icon(
|
||||
Icons.Filled.ContactPage,
|
||||
about
|
||||
)
|
||||
},
|
||||
headlineContent = { Text(about) },
|
||||
modifier = Modifier.clickable {
|
||||
aboutDialog.show()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UninstallItem(
|
||||
navigator: DestinationsNavigator,
|
||||
withLoading: suspend (suspend () -> Unit) -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
val uninstallConfirmDialog = rememberConfirmDialog()
|
||||
val showTodo = {
|
||||
Toast.makeText(context, "TODO", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
val uninstallDialog = rememberUninstallDialog { uninstallType ->
|
||||
scope.launch {
|
||||
val result = uninstallConfirmDialog.awaitConfirm(
|
||||
title = context.getString(uninstallType.title),
|
||||
content = context.getString(uninstallType.message)
|
||||
)
|
||||
if (result == ConfirmResult.Confirmed) {
|
||||
withLoading {
|
||||
when (uninstallType) {
|
||||
UninstallType.TEMPORARY -> showTodo()
|
||||
UninstallType.PERMANENT -> navigator.navigate(
|
||||
FlashScreenDestination(FlashIt.FlashUninstall)
|
||||
)
|
||||
UninstallType.RESTORE_STOCK_IMAGE -> navigator.navigate(
|
||||
FlashScreenDestination(FlashIt.FlashRestore)
|
||||
)
|
||||
UninstallType.NONE -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
val uninstall = stringResource(id = R.string.settings_uninstall)
|
||||
ListItem(
|
||||
leadingContent = {
|
||||
Icon(
|
||||
Icons.Filled.Delete,
|
||||
uninstall
|
||||
)
|
||||
},
|
||||
headlineContent = { Text(uninstall) },
|
||||
modifier = Modifier.clickable {
|
||||
uninstallDialog.show()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
enum class UninstallType(val title: Int, val message: Int, val icon: ImageVector) {
|
||||
TEMPORARY(
|
||||
R.string.settings_uninstall_temporary,
|
||||
R.string.settings_uninstall_temporary_message,
|
||||
Icons.Filled.Delete
|
||||
),
|
||||
PERMANENT(
|
||||
R.string.settings_uninstall_permanent,
|
||||
R.string.settings_uninstall_permanent_message,
|
||||
Icons.Filled.DeleteForever
|
||||
),
|
||||
RESTORE_STOCK_IMAGE(
|
||||
R.string.settings_restore_stock_image,
|
||||
R.string.settings_restore_stock_image_message,
|
||||
Icons.AutoMirrored.Filled.Undo
|
||||
),
|
||||
NONE(0, 0, Icons.Filled.Delete)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun rememberUninstallDialog(onSelected: (UninstallType) -> Unit): DialogHandle {
|
||||
return rememberCustomDialog { dismiss ->
|
||||
val options = listOf(
|
||||
// UninstallType.TEMPORARY,
|
||||
UninstallType.PERMANENT,
|
||||
UninstallType.RESTORE_STOCK_IMAGE
|
||||
)
|
||||
val listOptions = options.map {
|
||||
ListOption(
|
||||
titleText = stringResource(it.title),
|
||||
subtitleText = if (it.message != 0) stringResource(it.message) else null,
|
||||
icon = IconSource(it.icon)
|
||||
)
|
||||
}
|
||||
|
||||
var selection = UninstallType.NONE
|
||||
val cardColor = if (!ThemeConfig.useDynamicColor) {
|
||||
ThemeConfig.currentTheme.ButtonContrast
|
||||
} else {
|
||||
MaterialTheme.colorScheme.secondaryContainer
|
||||
}
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = {
|
||||
dismiss()
|
||||
},
|
||||
title = {
|
||||
Text(text = stringResource(R.string.settings_uninstall))
|
||||
},
|
||||
text = {
|
||||
Column {
|
||||
listOptions.forEachIndexed { index, option ->
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.clickable {
|
||||
selection = options[index]
|
||||
}
|
||||
.padding(vertical = 8.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = options[index].icon,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.padding(end = 8.dp)
|
||||
)
|
||||
Column {
|
||||
Text(text = option.titleText)
|
||||
option.subtitleText?.let {
|
||||
Text(
|
||||
text = it,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
androidx.compose.material3.TextButton(
|
||||
onClick = {
|
||||
if (selection != UninstallType.NONE) {
|
||||
onSelected(selection)
|
||||
}
|
||||
dismiss()
|
||||
}
|
||||
) {
|
||||
Text(text = stringResource(android.R.string.ok))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
androidx.compose.material3.TextButton(
|
||||
onClick = {
|
||||
dismiss()
|
||||
}
|
||||
) {
|
||||
Text(text = stringResource(android.R.string.cancel))
|
||||
}
|
||||
},
|
||||
containerColor = getCardColors(cardColor.copy(alpha = 0.9f)).containerColor.copy(alpha = 0.9f),
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
tonalElevation = getCardElevation()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun TopBar(
|
||||
scrollBehavior: TopAppBarScrollBehavior? = null
|
||||
) {
|
||||
val cardColor = MaterialTheme.colorScheme.secondaryContainer
|
||||
val cardAlpha = CardConfig.cardAlpha
|
||||
TopAppBar(
|
||||
title = { Text(stringResource(R.string.settings)) },
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = cardColor.copy(alpha = cardAlpha),
|
||||
scrolledContainerColor = cardColor.copy(alpha = cardAlpha)
|
||||
),
|
||||
|
||||
windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),
|
||||
scrollBehavior = scrollBehavior
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun SettingsPreview() {
|
||||
SettingScreen(EmptyDestinationsNavigator)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,393 +0,0 @@
|
||||
package zako.zako.zako.ui.screen
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import zako.zako.zako.R
|
||||
import coil.compose.AsyncImage
|
||||
import coil.request.ImageRequest
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||
import com.ramcosta.composedestinations.generated.destinations.AppProfileScreenDestination
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
import kotlinx.coroutines.launch
|
||||
import zako.zako.zako.Natives
|
||||
import zako.zako.zako.ui.component.SearchAppBar
|
||||
import zako.zako.zako.ui.util.ModuleModify
|
||||
import zako.zako.zako.ui.viewmodel.SuperUserViewModel
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class, ExperimentalMaterial3Api::class)
|
||||
@Destination<RootGraph>
|
||||
@Composable
|
||||
fun SuperUserScreen(navigator: DestinationsNavigator) {
|
||||
val viewModel = viewModel<SuperUserViewModel>()
|
||||
val scope = rememberCoroutineScope()
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||
val listState = rememberLazyListState()
|
||||
val context = LocalContext.current
|
||||
val snackBarHostState = remember { SnackbarHostState() }
|
||||
|
||||
// 添加备份和还原启动器
|
||||
val backupLauncher = ModuleModify.rememberAllowlistBackupLauncher(context, snackBarHostState)
|
||||
val restoreLauncher = ModuleModify.rememberAllowlistRestoreLauncher(context, snackBarHostState)
|
||||
|
||||
LaunchedEffect(key1 = navigator) {
|
||||
viewModel.search = ""
|
||||
if (viewModel.appList.isEmpty()) {
|
||||
viewModel.fetchAppList()
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(viewModel.search) {
|
||||
if (viewModel.search.isEmpty()) {
|
||||
listState.scrollToItem(0)
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
SearchAppBar(
|
||||
title = { Text(stringResource(R.string.superuser)) },
|
||||
searchText = viewModel.search,
|
||||
onSearchTextChange = { viewModel.search = it },
|
||||
onClearClick = { viewModel.search = "" },
|
||||
dropdownContent = {
|
||||
var showDropdown by remember { mutableStateOf(false) }
|
||||
|
||||
IconButton(
|
||||
onClick = { showDropdown = true },
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.MoreVert,
|
||||
contentDescription = stringResource(id = R.string.settings)
|
||||
)
|
||||
|
||||
DropdownMenu(expanded = showDropdown, onDismissRequest = {
|
||||
showDropdown = false
|
||||
}) {
|
||||
DropdownMenuItem(text = {
|
||||
Text(stringResource(R.string.refresh))
|
||||
}, onClick = {
|
||||
scope.launch {
|
||||
viewModel.fetchAppList()
|
||||
}
|
||||
showDropdown = false
|
||||
})
|
||||
DropdownMenuItem(text = {
|
||||
Text(
|
||||
if (viewModel.showSystemApps) {
|
||||
stringResource(R.string.hide_system_apps)
|
||||
} else {
|
||||
stringResource(R.string.show_system_apps)
|
||||
}
|
||||
)
|
||||
}, onClick = {
|
||||
viewModel.showSystemApps = !viewModel.showSystemApps
|
||||
showDropdown = false
|
||||
})
|
||||
DropdownMenuItem(text = {
|
||||
Text(stringResource(R.string.backup_allowlist))
|
||||
}, onClick = {
|
||||
backupLauncher.launch(ModuleModify.createAllowlistBackupIntent())
|
||||
showDropdown = false
|
||||
})
|
||||
DropdownMenuItem(text = {
|
||||
Text(stringResource(R.string.restore_allowlist))
|
||||
}, onClick = {
|
||||
restoreLauncher.launch(ModuleModify.createAllowlistRestoreIntent())
|
||||
showDropdown = false
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
scrollBehavior = scrollBehavior
|
||||
)
|
||||
},
|
||||
snackbarHost = { SnackbarHost(snackBarHostState) },
|
||||
contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),
|
||||
bottomBar = {
|
||||
// 批量操作按钮,直接放在底部栏
|
||||
if (viewModel.showBatchActions && viewModel.selectedApps.isNotEmpty()) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(MaterialTheme.colorScheme.surface)
|
||||
.padding(16.dp),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly
|
||||
) {
|
||||
Button(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
viewModel.updateBatchPermissions(true)
|
||||
}
|
||||
}
|
||||
) {
|
||||
Text(stringResource(R.string.batch_authorization))
|
||||
}
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
viewModel.updateBatchPermissions(false)
|
||||
}
|
||||
}
|
||||
) {
|
||||
Text(stringResource(R.string.batch_cancel_authorization))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
) { innerPadding ->
|
||||
PullToRefreshBox(
|
||||
modifier = Modifier.padding(innerPadding),
|
||||
onRefresh = {
|
||||
scope.launch { viewModel.fetchAppList() }
|
||||
},
|
||||
isRefreshing = viewModel.isRefreshing
|
||||
) {
|
||||
LazyColumn(
|
||||
state = listState,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection)
|
||||
) {
|
||||
// 获取分组后的应用列表 - 修改分组逻辑,避免应用重复出现在多个分组中
|
||||
val rootApps = viewModel.appList.filter { it.allowSu }
|
||||
val customApps = viewModel.appList.filter { !it.allowSu && it.hasCustomProfile }
|
||||
val otherApps = viewModel.appList.filter { !it.allowSu && !it.hasCustomProfile }
|
||||
|
||||
// 显示ROOT权限应用组
|
||||
if (rootApps.isNotEmpty()) {
|
||||
items(rootApps, key = { "root_" + it.packageName + it.uid }) { app ->
|
||||
AppItem(
|
||||
app = app,
|
||||
isSelected = viewModel.selectedApps.contains(app.packageName),
|
||||
onToggleSelection = { viewModel.toggleAppSelection(app.packageName) },
|
||||
onSwitchChange = { allowSu ->
|
||||
scope.launch {
|
||||
val profile = Natives.getAppProfile(app.packageName, app.uid)
|
||||
val updatedProfile = profile.copy(allowSu = allowSu)
|
||||
if (Natives.setAppProfile(updatedProfile)) {
|
||||
viewModel.fetchAppList()
|
||||
}
|
||||
}
|
||||
},
|
||||
onClick = {
|
||||
if (viewModel.showBatchActions) {
|
||||
viewModel.toggleAppSelection(app.packageName)
|
||||
} else {
|
||||
navigator.navigate(AppProfileScreenDestination(app))
|
||||
}
|
||||
},
|
||||
onLongClick = {
|
||||
// 长按进入多选模式
|
||||
if (!viewModel.showBatchActions) {
|
||||
viewModel.toggleBatchMode()
|
||||
viewModel.toggleAppSelection(app.packageName)
|
||||
}
|
||||
},
|
||||
viewModel = viewModel
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 显示自定义配置应用组
|
||||
if (customApps.isNotEmpty()) {
|
||||
items(customApps, key = { "custom_" + it.packageName + it.uid }) { app ->
|
||||
AppItem(
|
||||
app = app,
|
||||
isSelected = viewModel.selectedApps.contains(app.packageName),
|
||||
onToggleSelection = { viewModel.toggleAppSelection(app.packageName) },
|
||||
onSwitchChange = { allowSu ->
|
||||
scope.launch {
|
||||
val profile = Natives.getAppProfile(app.packageName, app.uid)
|
||||
val updatedProfile = profile.copy(allowSu = allowSu)
|
||||
if (Natives.setAppProfile(updatedProfile)) {
|
||||
viewModel.fetchAppList()
|
||||
}
|
||||
}
|
||||
},
|
||||
onClick = {
|
||||
if (viewModel.showBatchActions) {
|
||||
viewModel.toggleAppSelection(app.packageName)
|
||||
} else {
|
||||
navigator.navigate(AppProfileScreenDestination(app))
|
||||
}
|
||||
},
|
||||
onLongClick = {
|
||||
// 长按进入多选模式
|
||||
if (!viewModel.showBatchActions) {
|
||||
viewModel.toggleBatchMode()
|
||||
viewModel.toggleAppSelection(app.packageName)
|
||||
}
|
||||
},
|
||||
viewModel = viewModel
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 显示其他应用组
|
||||
if (otherApps.isNotEmpty()) {
|
||||
items(otherApps, key = { "other_" + it.packageName + it.uid }) { app ->
|
||||
AppItem(
|
||||
app = app,
|
||||
isSelected = viewModel.selectedApps.contains(app.packageName),
|
||||
onToggleSelection = { viewModel.toggleAppSelection(app.packageName) },
|
||||
onSwitchChange = { allowSu ->
|
||||
scope.launch {
|
||||
val profile = Natives.getAppProfile(app.packageName, app.uid)
|
||||
val updatedProfile = profile.copy(allowSu = allowSu)
|
||||
if (Natives.setAppProfile(updatedProfile)) {
|
||||
viewModel.fetchAppList()
|
||||
}
|
||||
}
|
||||
},
|
||||
onClick = {
|
||||
if (viewModel.showBatchActions) {
|
||||
viewModel.toggleAppSelection(app.packageName)
|
||||
} else {
|
||||
navigator.navigate(AppProfileScreenDestination(app))
|
||||
}
|
||||
},
|
||||
onLongClick = {
|
||||
// 长按进入多选模式
|
||||
if (!viewModel.showBatchActions) {
|
||||
viewModel.toggleBatchMode()
|
||||
viewModel.toggleAppSelection(app.packageName)
|
||||
}
|
||||
},
|
||||
viewModel = viewModel
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun GroupHeader(title: String) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(MaterialTheme.colorScheme.surfaceVariant)
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
style = TextStyle(
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
private fun AppItem(
|
||||
app: SuperUserViewModel.AppInfo,
|
||||
isSelected: Boolean,
|
||||
onToggleSelection: () -> Unit,
|
||||
onSwitchChange: (Boolean) -> Unit,
|
||||
onClick: () -> Unit,
|
||||
onLongClick: () -> Unit,
|
||||
viewModel: SuperUserViewModel
|
||||
) {
|
||||
ListItem(
|
||||
modifier = Modifier
|
||||
.pointerInput(Unit) {
|
||||
detectTapGestures(
|
||||
onLongPress = { onLongClick() },
|
||||
onTap = { onClick() }
|
||||
)
|
||||
},
|
||||
headlineContent = { Text(app.label) },
|
||||
supportingContent = {
|
||||
Column {
|
||||
Text(app.packageName)
|
||||
FlowRow {
|
||||
if (app.allowSu) {
|
||||
LabelText(label = "ROOT")
|
||||
} else {
|
||||
if (Natives.uidShouldUmount(app.uid)) {
|
||||
LabelText(label = "UMOUNT")
|
||||
}
|
||||
}
|
||||
if (app.hasCustomProfile) {
|
||||
LabelText(label = "CUSTOM")
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
leadingContent = {
|
||||
AsyncImage(
|
||||
model = ImageRequest.Builder(LocalContext.current)
|
||||
.data(app.packageInfo)
|
||||
.crossfade(true)
|
||||
.build(),
|
||||
contentDescription = app.label,
|
||||
modifier = Modifier
|
||||
.padding(4.dp)
|
||||
.width(48.dp)
|
||||
.height(48.dp)
|
||||
)
|
||||
},
|
||||
trailingContent = {
|
||||
if (!viewModel.showBatchActions) {
|
||||
Switch(
|
||||
checked = app.allowSu,
|
||||
onCheckedChange = onSwitchChange
|
||||
)
|
||||
} else {
|
||||
Checkbox(
|
||||
checked = isSelected,
|
||||
onCheckedChange = { onToggleSelection() }
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LabelText(label: String) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(top = 4.dp, end = 4.dp)
|
||||
.background(
|
||||
Color.Black,
|
||||
shape = RoundedCornerShape(4.dp)
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
text = label,
|
||||
modifier = Modifier.padding(vertical = 2.dp, horizontal = 5.dp),
|
||||
style = TextStyle(
|
||||
fontSize = 8.sp,
|
||||
color = Color.White,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,556 +0,0 @@
|
||||
package zako.zako.zako.ui.screen
|
||||
|
||||
import android.app.Activity.RESULT_OK
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.util.Log
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import zako.zako.zako.ui.component.ConfirmResult
|
||||
import zako.zako.zako.ui.component.SearchAppBar
|
||||
import zako.zako.zako.ui.component.rememberConfirmDialog
|
||||
import zako.zako.zako.ui.theme.getCardColors
|
||||
import zako.zako.zako.ui.theme.getCardElevation
|
||||
import zako.zako.zako.ui.viewmodel.KpmViewModel
|
||||
import zako.zako.zako.ui.util.loadKpmModule
|
||||
import zako.zako.zako.ui.util.unloadKpmModule
|
||||
import java.io.File
|
||||
import androidx.core.content.edit
|
||||
import zako.zako.zako.ui.theme.ThemeConfig
|
||||
import zako.zako.zako.ui.component.rememberCustomDialog
|
||||
import zako.zako.zako.ui.component.ConfirmDialogHandle
|
||||
import zako.zako.zako.R
|
||||
import java.net.URLDecoder
|
||||
import java.net.URLEncoder
|
||||
|
||||
/**
|
||||
* KPM 管理界面
|
||||
* 以下内核模块功能由KernelPatch开发,经过修改后加入SukiSU Ultra的内核模块功能
|
||||
* 开发者:zako, Liaokong
|
||||
*/
|
||||
var globalModuleFileName: String = ""
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Destination<RootGraph>
|
||||
@Composable
|
||||
fun KpmScreen(
|
||||
navigator: DestinationsNavigator,
|
||||
viewModel: KpmViewModel = viewModel()
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
val snackBarHost = remember { SnackbarHostState() }
|
||||
val confirmDialog = rememberConfirmDialog()
|
||||
val cardColor = if (!ThemeConfig.useDynamicColor) {
|
||||
ThemeConfig.currentTheme.ButtonContrast
|
||||
} else {
|
||||
MaterialTheme.colorScheme.secondaryContainer
|
||||
}
|
||||
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||
|
||||
val kpmInstallSuccess = stringResource(R.string.kpm_install_success)
|
||||
val kpmInstallFailed = stringResource(R.string.kpm_install_failed)
|
||||
val cancel = stringResource(R.string.cancel)
|
||||
val uninstall = stringResource(R.string.uninstall)
|
||||
val failedToCheckModuleFile = stringResource(R.string.snackbar_failed_to_check_module_file)
|
||||
val kpmUninstallSuccess = stringResource(R.string.kpm_uninstall_success)
|
||||
val kpmUninstallFailed = stringResource(R.string.kpm_uninstall_failed)
|
||||
val kpmInstallMode = stringResource(R.string.kpm_install_mode)
|
||||
val kpmInstallModeLoad = stringResource(R.string.kpm_install_mode_load)
|
||||
val kpmInstallModeEmbed = stringResource(R.string.kpm_install_mode_embed)
|
||||
val kpmInstallModeDescription = stringResource(R.string.kpm_install_mode_description)
|
||||
val invalidFileTypeMessage = stringResource(R.string.invalid_file_type)
|
||||
val confirmTitle = stringResource(R.string.confirm_uninstall_title_with_filename)
|
||||
val confirmContent = stringResource(R.string.confirm_uninstall_content, globalModuleFileName)
|
||||
|
||||
var tempFileForInstall by remember { mutableStateOf<File?>(null) }
|
||||
val installModeDialog = rememberCustomDialog { dismiss ->
|
||||
AlertDialog(
|
||||
onDismissRequest = {
|
||||
dismiss()
|
||||
tempFileForInstall?.delete()
|
||||
tempFileForInstall = null
|
||||
},
|
||||
title = { Text(kpmInstallMode) },
|
||||
text = { Text(kpmInstallModeDescription) },
|
||||
confirmButton = {
|
||||
Column {
|
||||
Button(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
dismiss()
|
||||
tempFileForInstall?.let { tempFile ->
|
||||
handleModuleInstall(
|
||||
tempFile = tempFile,
|
||||
isEmbed = false,
|
||||
viewModel = viewModel,
|
||||
snackBarHost = snackBarHost,
|
||||
kpmInstallSuccess = kpmInstallSuccess,
|
||||
kpmInstallFailed = kpmInstallFailed
|
||||
)
|
||||
}
|
||||
tempFileForInstall = null
|
||||
}
|
||||
}
|
||||
) {
|
||||
Text(kpmInstallModeLoad)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Button(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
dismiss()
|
||||
tempFileForInstall?.let { tempFile ->
|
||||
handleModuleInstall(
|
||||
tempFile = tempFile,
|
||||
isEmbed = true,
|
||||
viewModel = viewModel,
|
||||
snackBarHost = snackBarHost,
|
||||
kpmInstallSuccess = kpmInstallSuccess,
|
||||
kpmInstallFailed = kpmInstallFailed
|
||||
)
|
||||
}
|
||||
tempFileForInstall = null
|
||||
}
|
||||
}
|
||||
) {
|
||||
Text(kpmInstallModeEmbed)
|
||||
}
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
dismiss()
|
||||
tempFileForInstall?.delete()
|
||||
tempFileForInstall = null
|
||||
}
|
||||
) {
|
||||
Text(cancel)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
val selectPatchLauncher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.StartActivityForResult()
|
||||
) { result ->
|
||||
if (result.resultCode != RESULT_OK) return@rememberLauncherForActivityResult
|
||||
|
||||
val uri = result.data?.data ?: return@rememberLauncherForActivityResult
|
||||
|
||||
scope.launch {
|
||||
val fileName = uri.lastPathSegment ?: "unknown.kpm"
|
||||
val encodedFileName = URLEncoder.encode(fileName, "UTF-8")
|
||||
val tempFile = File(context.cacheDir, encodedFileName)
|
||||
|
||||
context.contentResolver.openInputStream(uri)?.use { input ->
|
||||
tempFile.outputStream().use { output ->
|
||||
input.copyTo(output)
|
||||
}
|
||||
}
|
||||
|
||||
if (!tempFile.name.endsWith(".kpm")) {
|
||||
snackBarHost.showSnackbar(
|
||||
message = invalidFileTypeMessage,
|
||||
duration = SnackbarDuration.Short
|
||||
)
|
||||
tempFile.delete()
|
||||
return@launch
|
||||
}
|
||||
|
||||
tempFileForInstall = tempFile
|
||||
installModeDialog.show()
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
while(true) {
|
||||
viewModel.fetchModuleList()
|
||||
delay(5000)
|
||||
}
|
||||
}
|
||||
|
||||
val sharedPreferences = context.getSharedPreferences("app_preferences", Context.MODE_PRIVATE)
|
||||
var isNoticeClosed by remember { mutableStateOf(sharedPreferences.getBoolean("is_notice_closed", false)) }
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
SearchAppBar(
|
||||
title = { Text(stringResource(R.string.kpm_title)) },
|
||||
searchText = viewModel.search,
|
||||
onSearchTextChange = { viewModel.search = it },
|
||||
onClearClick = { viewModel.search = "" },
|
||||
scrollBehavior = scrollBehavior,
|
||||
dropdownContent = {
|
||||
IconButton(onClick = { viewModel.fetchModuleList() }) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Refresh,
|
||||
contentDescription = stringResource(R.string.refresh)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
floatingActionButton = {
|
||||
ExtendedFloatingActionButton(
|
||||
onClick = {
|
||||
selectPatchLauncher.launch(
|
||||
Intent(Intent.ACTION_GET_CONTENT).apply {
|
||||
type = "*/*"
|
||||
}
|
||||
)
|
||||
},
|
||||
icon = {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Add,
|
||||
contentDescription = stringResource(R.string.kpm_install)
|
||||
)
|
||||
},
|
||||
text = { Text(stringResource(R.string.kpm_install)) },
|
||||
containerColor = cardColor.copy(alpha = 1f),
|
||||
contentColor = MaterialTheme.colorScheme.onSecondaryContainer
|
||||
)
|
||||
},
|
||||
snackbarHost = { SnackbarHost(snackBarHost) }
|
||||
) { padding ->
|
||||
Column(modifier = Modifier.padding(padding)) {
|
||||
if (!isNoticeClosed) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.kernel_module_notice),
|
||||
modifier = Modifier.weight(1f),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
IconButton(onClick = {
|
||||
isNoticeClosed = true
|
||||
sharedPreferences.edit { putBoolean("is_notice_closed", true) }
|
||||
}) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Close,
|
||||
contentDescription = stringResource(R.string.close_notice)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (viewModel.moduleList.isEmpty()) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
stringResource(R.string.kpm_empty),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
} else {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = PaddingValues(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
items(viewModel.moduleList) { module ->
|
||||
KpmModuleItem(
|
||||
module = module,
|
||||
onUninstall = {
|
||||
scope.launch {
|
||||
handleModuleUninstall(
|
||||
module = module,
|
||||
viewModel = viewModel,
|
||||
snackBarHost = snackBarHost,
|
||||
kpmUninstallSuccess = kpmUninstallSuccess,
|
||||
kpmUninstallFailed = kpmUninstallFailed,
|
||||
failedToCheckModuleFile = failedToCheckModuleFile,
|
||||
uninstall = uninstall,
|
||||
cancel = cancel,
|
||||
confirmDialog = confirmDialog,
|
||||
confirmTitle = confirmTitle,
|
||||
confirmContent = confirmContent
|
||||
)
|
||||
}
|
||||
},
|
||||
onControl = {
|
||||
viewModel.loadModuleDetail(module.id)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun handleModuleInstall(
|
||||
tempFile: File,
|
||||
isEmbed: Boolean,
|
||||
viewModel: KpmViewModel,
|
||||
snackBarHost: SnackbarHostState,
|
||||
kpmInstallSuccess: String,
|
||||
kpmInstallFailed: String
|
||||
) {
|
||||
val moduleId = extractModuleId(tempFile.name)
|
||||
if (moduleId == null) {
|
||||
Log.e("KsuCli", "Failed to extract module ID from file: ${tempFile.name}")
|
||||
snackBarHost.showSnackbar(
|
||||
message = kpmInstallFailed,
|
||||
duration = SnackbarDuration.Short
|
||||
)
|
||||
tempFile.delete()
|
||||
return
|
||||
}
|
||||
|
||||
val targetPath = "/data/adb/kpm/$moduleId.kpm"
|
||||
|
||||
try {
|
||||
if (isEmbed) {
|
||||
Runtime.getRuntime().exec(arrayOf("su", "-c", "mkdir -p /data/adb/kpm")).waitFor()
|
||||
Runtime.getRuntime().exec(arrayOf("su", "-c", "cp ${tempFile.absolutePath} $targetPath")).waitFor()
|
||||
}
|
||||
|
||||
val loadResult = loadKpmModule(tempFile.absolutePath)
|
||||
if (loadResult.startsWith("Error")) {
|
||||
Log.e("KsuCli", "Failed to load KPM module: $loadResult")
|
||||
snackBarHost.showSnackbar(
|
||||
message = kpmInstallFailed,
|
||||
duration = SnackbarDuration.Short
|
||||
)
|
||||
} else {
|
||||
viewModel.fetchModuleList()
|
||||
snackBarHost.showSnackbar(
|
||||
message = kpmInstallSuccess,
|
||||
duration = SnackbarDuration.Short
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("KsuCli", "Failed to load KPM module: ${e.message}", e)
|
||||
snackBarHost.showSnackbar(
|
||||
message = kpmInstallFailed,
|
||||
duration = SnackbarDuration.Short
|
||||
)
|
||||
}
|
||||
tempFile.delete()
|
||||
}
|
||||
|
||||
private fun extractModuleId(fileName: String): String? {
|
||||
return try {
|
||||
val decodedFileName = URLDecoder.decode(fileName, "UTF-8")
|
||||
val pattern = "([^/]*?)\\.kpm$".toRegex()
|
||||
val matchResult = pattern.find(decodedFileName)
|
||||
matchResult?.groupValues?.get(1)
|
||||
} catch (e: Exception) {
|
||||
Log.e("KsuCli", "Failed to extract module ID: ${e.message}", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun handleModuleUninstall(
|
||||
module: KpmViewModel.ModuleInfo,
|
||||
viewModel: KpmViewModel,
|
||||
snackBarHost: SnackbarHostState,
|
||||
kpmUninstallSuccess: String,
|
||||
kpmUninstallFailed: String,
|
||||
failedToCheckModuleFile: String,
|
||||
uninstall: String,
|
||||
cancel: String,
|
||||
confirmTitle : String,
|
||||
confirmContent : String,
|
||||
confirmDialog: ConfirmDialogHandle
|
||||
) {
|
||||
val moduleFileName = "${module.id}.kpm"
|
||||
globalModuleFileName = moduleFileName
|
||||
val moduleFilePath = "/data/adb/kpm/$moduleFileName"
|
||||
|
||||
val fileExists = try {
|
||||
val result = Runtime.getRuntime().exec(arrayOf("su", "-c", "ls /data/adb/kpm/$moduleFileName")).waitFor() == 0
|
||||
result
|
||||
} catch (e: Exception) {
|
||||
Log.e("KsuCli", "Failed to check module file existence: ${e.message}", e)
|
||||
snackBarHost.showSnackbar(
|
||||
message = failedToCheckModuleFile,
|
||||
duration = SnackbarDuration.Short
|
||||
)
|
||||
false
|
||||
}
|
||||
val confirmResult = confirmDialog.awaitConfirm(
|
||||
title = confirmTitle,
|
||||
content = confirmContent,
|
||||
confirm = uninstall,
|
||||
dismiss = cancel
|
||||
)
|
||||
|
||||
if (confirmResult == ConfirmResult.Confirmed) {
|
||||
try {
|
||||
val unloadResult = unloadKpmModule(module.id)
|
||||
if (unloadResult.startsWith("Error")) {
|
||||
Log.e("KsuCli", "Failed to unload KPM module: $unloadResult")
|
||||
snackBarHost.showSnackbar(
|
||||
message = kpmUninstallFailed,
|
||||
duration = SnackbarDuration.Short
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (fileExists) {
|
||||
Runtime.getRuntime().exec(arrayOf("su", "-c", "rm $moduleFilePath")).waitFor()
|
||||
}
|
||||
|
||||
viewModel.fetchModuleList()
|
||||
snackBarHost.showSnackbar(
|
||||
message = kpmUninstallSuccess,
|
||||
duration = SnackbarDuration.Short
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.e("KsuCli", "Failed to unload KPM module: ${e.message}", e)
|
||||
snackBarHost.showSnackbar(
|
||||
message = kpmUninstallFailed,
|
||||
duration = SnackbarDuration.Short
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun KpmModuleItem(
|
||||
module: KpmViewModel.ModuleInfo,
|
||||
onUninstall: () -> Unit,
|
||||
onControl: () -> Unit
|
||||
) {
|
||||
val viewModel: KpmViewModel = viewModel()
|
||||
val scope = rememberCoroutineScope()
|
||||
val snackBarHost = remember { SnackbarHostState() }
|
||||
val successMessage = stringResource(R.string.kpm_control_success)
|
||||
val failureMessage = stringResource(R.string.kpm_control_failed)
|
||||
|
||||
if (viewModel.showInputDialog && viewModel.selectedModuleId == module.id) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { viewModel.hideInputDialog() },
|
||||
title = { Text(stringResource(R.string.kpm_control)) },
|
||||
text = {
|
||||
OutlinedTextField(
|
||||
value = viewModel.inputArgs,
|
||||
onValueChange = { viewModel.updateInputArgs(it) },
|
||||
label = { Text(stringResource(R.string.kpm_args)) },
|
||||
placeholder = { Text(module.args) }
|
||||
)
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
val result = viewModel.executeControl()
|
||||
val message = when (result) {
|
||||
0 -> successMessage
|
||||
else -> failureMessage
|
||||
}
|
||||
snackBarHost.showSnackbar(message)
|
||||
onControl()
|
||||
}
|
||||
}
|
||||
) {
|
||||
Text(stringResource(R.string.confirm))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { viewModel.hideInputDialog() }) {
|
||||
Text(stringResource(R.string.cancel))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
ElevatedCard(
|
||||
colors = getCardColors(MaterialTheme.colorScheme.secondaryContainer),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = getCardElevation())
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = module.name,
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
Text(
|
||||
text = "${stringResource(R.string.kpm_version)}: ${module.version}",
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
Text(
|
||||
text = "${stringResource(R.string.kpm_author)}: ${module.author}",
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
Text(
|
||||
text = "${stringResource(R.string.kpm_args)}: ${module.args}",
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Text(
|
||||
text = module.description,
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
FilledTonalButton(
|
||||
onClick = { viewModel.showInputDialog(module.id) },
|
||||
enabled = module.hasAction
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Settings,
|
||||
contentDescription = null
|
||||
)
|
||||
Text(stringResource(R.string.kpm_control))
|
||||
}
|
||||
|
||||
FilledTonalButton(
|
||||
onClick = onUninstall
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Delete,
|
||||
contentDescription = null
|
||||
)
|
||||
Text(stringResource(R.string.kpm_uninstall))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
package zako.zako.zako.ui.theme
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.luminance
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
object CardConfig {
|
||||
val defaultElevation: Dp = 0.dp
|
||||
|
||||
var cardAlpha by mutableStateOf(0.45f)
|
||||
var cardElevation by mutableStateOf(defaultElevation)
|
||||
var isShadowEnabled by mutableStateOf(true)
|
||||
var isCustomAlphaSet by mutableStateOf(false)
|
||||
var isUserDarkModeEnabled by mutableStateOf(false)
|
||||
var isUserLightModeEnabled by mutableStateOf(false)
|
||||
var isCustomBackgroundEnabled by mutableStateOf(false)
|
||||
|
||||
fun save(context: Context) {
|
||||
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||
prefs.edit().apply {
|
||||
putFloat("card_alpha", cardAlpha)
|
||||
putBoolean("custom_background_enabled", cardElevation == 0.dp)
|
||||
putBoolean("is_custom_alpha_set", isCustomAlphaSet)
|
||||
putBoolean("is_user_dark_mode_enabled", isUserDarkModeEnabled)
|
||||
putBoolean("is_user_light_mode_enabled", isUserLightModeEnabled)
|
||||
putBoolean("is_custom_background_enabled", isCustomBackgroundEnabled)
|
||||
apply()
|
||||
}
|
||||
}
|
||||
|
||||
fun load(context: Context) {
|
||||
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||
cardAlpha = prefs.getFloat("card_alpha", 0.45f)
|
||||
cardElevation = if (prefs.getBoolean("custom_background_enabled", false)) 0.dp else defaultElevation
|
||||
isCustomAlphaSet = prefs.getBoolean("is_custom_alpha_set", false)
|
||||
isUserDarkModeEnabled = prefs.getBoolean("is_user_dark_mode_enabled", false)
|
||||
isUserLightModeEnabled = prefs.getBoolean("is_user_light_mode_enabled", false)
|
||||
isCustomBackgroundEnabled = prefs.getBoolean("is_custom_background_enabled", false)
|
||||
}
|
||||
|
||||
fun updateShadowEnabled(enabled: Boolean) {
|
||||
isShadowEnabled = enabled
|
||||
cardElevation = if (enabled) defaultElevation else 0.dp
|
||||
}
|
||||
|
||||
fun setDarkModeDefaults() {
|
||||
if (!isCustomAlphaSet) {
|
||||
cardAlpha = 0.35f
|
||||
cardElevation = 0.dp
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
fun getCardColors(originalColor: Color) = CardDefaults.elevatedCardColors(
|
||||
containerColor = originalColor.copy(alpha = CardConfig.cardAlpha),
|
||||
contentColor = when {
|
||||
CardConfig.isUserLightModeEnabled -> {
|
||||
Color.Black
|
||||
}
|
||||
CardConfig.isUserDarkModeEnabled -> {
|
||||
Color.White
|
||||
}
|
||||
!isSystemInDarkTheme() && !CardConfig.isUserDarkModeEnabled -> {
|
||||
Color.Black
|
||||
}
|
||||
!isSystemInDarkTheme() && !CardConfig.isCustomBackgroundEnabled && !CardConfig.isUserDarkModeEnabled && originalColor.luminance() > 0.3 -> {
|
||||
Color.Black
|
||||
}
|
||||
isSystemInDarkTheme() && !CardConfig.isUserDarkModeEnabled && !CardConfig.isUserLightModeEnabled-> {
|
||||
Color.White
|
||||
}
|
||||
else -> {
|
||||
Color.White
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
fun getCardElevation() = CardConfig.cardElevation
|
||||
@@ -1,170 +0,0 @@
|
||||
package zako.zako.zako.ui.theme
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
sealed class ThemeColors {
|
||||
abstract val Primary: Color
|
||||
abstract val Secondary: Color
|
||||
abstract val Tertiary: Color
|
||||
abstract val OnPrimary: Color
|
||||
abstract val OnSecondary: Color
|
||||
abstract val OnTertiary: Color
|
||||
abstract val PrimaryContainer: Color
|
||||
abstract val SecondaryContainer: Color
|
||||
abstract val TertiaryContainer: Color
|
||||
abstract val OnPrimaryContainer: Color
|
||||
abstract val OnSecondaryContainer: Color
|
||||
abstract val OnTertiaryContainer: Color
|
||||
abstract val ButtonContrast: Color
|
||||
|
||||
open fun getCustomSliderActiveColor(): Color = Primary
|
||||
open fun getCustomSliderInactiveColor(): Color = PrimaryContainer
|
||||
|
||||
// Default Theme (white)
|
||||
object Default : ThemeColors() {
|
||||
override val Primary = Color(0xFFFFFFFF)
|
||||
override val Secondary = Color(0xFFF5F5F5)
|
||||
override val Tertiary = Color(0xFFE0E0E0)
|
||||
override val OnPrimary = Color(0xFF616161)
|
||||
override val OnSecondary = Color(0xFF616161)
|
||||
override val OnTertiary = Color(0xFF616161)
|
||||
override val PrimaryContainer = Color(0xFFF5F5F5)
|
||||
override val SecondaryContainer = Color(0xFFEEEEEE)
|
||||
override val TertiaryContainer = Color(0xFFE0E0E0)
|
||||
override val OnPrimaryContainer = Color(0xFF000000)
|
||||
override val OnSecondaryContainer = Color(0xFF000000)
|
||||
override val OnTertiaryContainer = Color(0xFF000000)
|
||||
override val ButtonContrast = Color(0xFF00BFFF)
|
||||
}
|
||||
|
||||
// Blue Theme
|
||||
object Blue : ThemeColors() {
|
||||
override val Primary = Color(0xFF2196F3)
|
||||
override val Secondary = Color(0xFF1E88E5)
|
||||
override val Tertiary = Color(0xFF0D47A1)
|
||||
override val OnPrimary = Color(0xFFFFFFFF)
|
||||
override val OnSecondary = Color(0xFFFFFFFF)
|
||||
override val OnTertiary = Color(0xFFFFFFFF)
|
||||
override val PrimaryContainer = Color(0xFFCBE6FC)
|
||||
override val SecondaryContainer = Color(0xFFBBDEFB)
|
||||
override val TertiaryContainer = Color(0xFF90CAF9)
|
||||
override val OnPrimaryContainer = Color(0xFF0A1A2E)
|
||||
override val OnSecondaryContainer = Color(0xFF0A192D)
|
||||
override val OnTertiaryContainer = Color(0xFF071B3D)
|
||||
override val ButtonContrast = Color(0xFF00BFFF)
|
||||
}
|
||||
|
||||
// Green Theme
|
||||
object Green : ThemeColors() {
|
||||
override val Primary = Color(0xFF4CAF50)
|
||||
override val Secondary = Color(0xFF43A047)
|
||||
override val Tertiary = Color(0xFF1B5E20)
|
||||
override val OnPrimary = Color(0xFFFFFFFF)
|
||||
override val OnSecondary = Color(0xFFFFFFFF)
|
||||
override val OnTertiary = Color(0xFFFFFFFF)
|
||||
override val PrimaryContainer = Color(0xFFC8E6C9)
|
||||
override val SecondaryContainer = Color(0xFFA5D6A7)
|
||||
override val TertiaryContainer = Color(0xFF81C784)
|
||||
override val OnPrimaryContainer = Color(0xFF0A1F0B)
|
||||
override val OnSecondaryContainer = Color(0xFF0A1D0B)
|
||||
override val OnTertiaryContainer = Color(0xFF071F09)
|
||||
override val ButtonContrast = Color(0xFF32CD32)
|
||||
}
|
||||
|
||||
// Purple Theme
|
||||
object Purple : ThemeColors() {
|
||||
override val Primary = Color(0xFF9C27B0)
|
||||
override val Secondary = Color(0xFF8E24AA)
|
||||
override val Tertiary = Color(0xFF4A148C)
|
||||
override val OnPrimary = Color(0xFFFFFFFF)
|
||||
override val OnSecondary = Color(0xFFFFFFFF)
|
||||
override val OnTertiary = Color(0xFFFFFFFF)
|
||||
override val PrimaryContainer = Color(0xFFE1BEE7)
|
||||
override val SecondaryContainer = Color(0xFFCE93D8)
|
||||
override val TertiaryContainer = Color(0xFFB39DDB)
|
||||
override val OnPrimaryContainer = Color(0xFF1F0A23)
|
||||
override val OnSecondaryContainer = Color(0xFF1C0A21)
|
||||
override val OnTertiaryContainer = Color(0xFF12071C)
|
||||
override val ButtonContrast = Color(0xFFDA70D6)
|
||||
}
|
||||
|
||||
// Orange Theme
|
||||
object Orange : ThemeColors() {
|
||||
override val Primary = Color(0xFFFF9800)
|
||||
override val Secondary = Color(0xFFFB8C00)
|
||||
override val Tertiary = Color(0xFFE65100)
|
||||
override val OnPrimary = Color(0xFFFFFFFF)
|
||||
override val OnSecondary = Color(0xFFFFFFFF)
|
||||
override val OnTertiary = Color(0xFFFFFFFF)
|
||||
override val PrimaryContainer = Color(0xFFFFE0B2)
|
||||
override val SecondaryContainer = Color(0xFFFFCC80)
|
||||
override val TertiaryContainer = Color(0xFFFFB74D)
|
||||
override val OnPrimaryContainer = Color(0xFF1A1100)
|
||||
override val OnSecondaryContainer = Color(0xFF1A1000)
|
||||
override val OnTertiaryContainer = Color(0xFF1A0B00)
|
||||
override val ButtonContrast = Color(0xFFFF6347)
|
||||
}
|
||||
|
||||
// Pink Theme
|
||||
object Pink : ThemeColors() {
|
||||
override val Primary = Color(0xFFE91E63)
|
||||
override val Secondary = Color(0xFFD81B60)
|
||||
override val Tertiary = Color(0xFF880E4F)
|
||||
override val OnPrimary = Color(0xFFFFFFFF)
|
||||
override val OnSecondary = Color(0xFFFFFFFF)
|
||||
override val OnTertiary = Color(0xFFFFFFFF)
|
||||
override val PrimaryContainer = Color(0xFFF8BBD0)
|
||||
override val SecondaryContainer = Color(0xFFF48FB1)
|
||||
override val TertiaryContainer = Color(0xFFE91E63)
|
||||
override val OnPrimaryContainer = Color(0xFF2E0A14)
|
||||
override val OnSecondaryContainer = Color(0xFF2B0A13)
|
||||
override val OnTertiaryContainer = Color(0xFF1C0311)
|
||||
override val ButtonContrast = Color(0xFFFF1493)
|
||||
}
|
||||
|
||||
// Gray Theme
|
||||
object Gray : ThemeColors() {
|
||||
override val Primary = Color(0xFF9E9E9E)
|
||||
override val Secondary = Color(0xFF757575)
|
||||
override val Tertiary = Color(0xFF616161)
|
||||
override val OnPrimary = Color(0xFFFFFFFF)
|
||||
override val OnSecondary = Color(0xFFFFFFFF)
|
||||
override val OnTertiary = Color(0xFFFFFFFF)
|
||||
override val PrimaryContainer = Color(0xFFEEEEEE)
|
||||
override val SecondaryContainer = Color(0xFFE0E0E0)
|
||||
override val TertiaryContainer = Color(0xFFBDBDBD)
|
||||
override val OnPrimaryContainer = Color(0xFF1A1A1A)
|
||||
override val OnSecondaryContainer = Color(0xFF171717)
|
||||
override val OnTertiaryContainer = Color(0xFF141414)
|
||||
override val ButtonContrast = Color(0xFF696969)
|
||||
}
|
||||
|
||||
object Yellow : ThemeColors() {
|
||||
override val Primary = Color(0xFFFFD700)
|
||||
override val Secondary = Color(0xFFFFBC52)
|
||||
override val Tertiary = Color(0xFF795548)
|
||||
override val OnPrimary = Color(0xFFFFFFFF)
|
||||
override val OnSecondary = Color(0xFFFFFFFF)
|
||||
override val OnTertiary = Color(0xFFFFFFFF)
|
||||
override val PrimaryContainer = Color(0xFFFFF7D6)
|
||||
override val SecondaryContainer = Color(0xFFFFE6B3)
|
||||
override val TertiaryContainer = Color(0xFFD7CCC8)
|
||||
override val OnPrimaryContainer = Color(0xFF1A1600)
|
||||
override val OnSecondaryContainer = Color(0xFF1A1100)
|
||||
override val OnTertiaryContainer = Color(0xFF1A1717)
|
||||
override val ButtonContrast = Color(0xFFFFD700)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun fromName(name: String): ThemeColors = when (name.lowercase()) {
|
||||
"blue" -> Blue
|
||||
"green" -> Green
|
||||
"purple" -> Purple
|
||||
"orange" -> Orange
|
||||
"pink" -> Pink
|
||||
"gray" -> Gray
|
||||
"white" -> Yellow
|
||||
else -> Default
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,350 +0,0 @@
|
||||
package zako.zako.zako.ui.theme
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.dynamicDarkColorScheme
|
||||
import androidx.compose.material3.dynamicLightColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.paint
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.zIndex
|
||||
import coil.compose.rememberAsyncImagePainter
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.ui.graphics.luminance
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.InputStream
|
||||
import androidx.core.content.edit
|
||||
import androidx.core.net.toUri
|
||||
|
||||
object ThemeConfig {
|
||||
var customBackgroundUri by mutableStateOf<Uri?>(null)
|
||||
var forceDarkMode by mutableStateOf<Boolean?>(null)
|
||||
var currentTheme by mutableStateOf<ThemeColors>(ThemeColors.Default)
|
||||
var useDynamicColor by mutableStateOf(false)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getDarkColorScheme() = darkColorScheme(
|
||||
primary = ThemeConfig.currentTheme.Primary.copy(alpha = 0.8f),
|
||||
onPrimary = mixColors(ThemeConfig.currentTheme.Primary, Color.White, 0.2f),
|
||||
primaryContainer = ThemeConfig.currentTheme.PrimaryContainer.copy(alpha = 0.15f),
|
||||
onPrimaryContainer = mixColors(ThemeConfig.currentTheme.Primary, Color.White, 0.2f),
|
||||
secondary = ThemeConfig.currentTheme.Secondary.copy(alpha = 0.8f),
|
||||
onSecondary = mixColors(ThemeConfig.currentTheme.Secondary, Color.White, 0.2f),
|
||||
secondaryContainer = ThemeConfig.currentTheme.SecondaryContainer.copy(alpha = 0.15f),
|
||||
onSecondaryContainer = mixColors(ThemeConfig.currentTheme.Secondary, Color.White, 0.2f),
|
||||
tertiary = ThemeConfig.currentTheme.Tertiary.copy(alpha = 0.8f),
|
||||
onTertiary = mixColors(ThemeConfig.currentTheme.Tertiary, Color.White, 0.2f),
|
||||
tertiaryContainer = ThemeConfig.currentTheme.TertiaryContainer.copy(alpha = 0.15f),
|
||||
onTertiaryContainer = mixColors(ThemeConfig.currentTheme.Tertiary, Color.White, 0.2f),
|
||||
background = Color.Transparent,
|
||||
surface = Color.Transparent,
|
||||
onBackground = mixColors(ThemeConfig.currentTheme.Primary, Color.White, 0.1f),
|
||||
onSurface = mixColors(ThemeConfig.currentTheme.Primary, Color.White, 0.1f),
|
||||
surfaceVariant = Color(0xFF2F2F2F),
|
||||
onSurfaceVariant = mixColors(ThemeConfig.currentTheme.Primary, Color.White, 0.2f),
|
||||
outline = Color.White.copy(alpha = 0.12f),
|
||||
outlineVariant = Color.White.copy(alpha = 0.12f)
|
||||
)
|
||||
|
||||
@Composable
|
||||
private fun getLightColorScheme() = lightColorScheme(
|
||||
primary = ThemeConfig.currentTheme.Primary,
|
||||
onPrimary = ThemeConfig.currentTheme.OnPrimary,
|
||||
primaryContainer = ThemeConfig.currentTheme.PrimaryContainer,
|
||||
onPrimaryContainer = ThemeConfig.currentTheme.OnPrimaryContainer,
|
||||
secondary = ThemeConfig.currentTheme.Secondary,
|
||||
onSecondary = ThemeConfig.currentTheme.OnSecondary,
|
||||
secondaryContainer = ThemeConfig.currentTheme.SecondaryContainer,
|
||||
onSecondaryContainer = ThemeConfig.currentTheme.OnSecondaryContainer,
|
||||
tertiary = ThemeConfig.currentTheme.Tertiary,
|
||||
onTertiary = ThemeConfig.currentTheme.OnTertiary,
|
||||
tertiaryContainer = ThemeConfig.currentTheme.TertiaryContainer,
|
||||
onTertiaryContainer = ThemeConfig.currentTheme.OnTertiaryContainer,
|
||||
background = Color.Transparent,
|
||||
surface = Color.Transparent,
|
||||
onBackground = Color.Black.copy(alpha = 0.87f),
|
||||
onSurface = Color.Black.copy(alpha = 0.87f),
|
||||
surfaceVariant = Color(0xFFF5F5F5),
|
||||
onSurfaceVariant = Color.Black.copy(alpha = 0.78f),
|
||||
outline = Color.Black.copy(alpha = 0.12f),
|
||||
outlineVariant = Color.Black.copy(alpha = 0.12f)
|
||||
)
|
||||
|
||||
// 复制图片到应用内部存储
|
||||
fun Context.copyImageToInternalStorage(uri: Uri): Uri? {
|
||||
try {
|
||||
val contentResolver: ContentResolver = contentResolver
|
||||
val inputStream: InputStream = contentResolver.openInputStream(uri)!!
|
||||
val fileName = "custom_background.jpg"
|
||||
val file = File(filesDir, fileName)
|
||||
val outputStream = FileOutputStream(file)
|
||||
val buffer = ByteArray(4 * 1024)
|
||||
var read: Int
|
||||
while (inputStream.read(buffer).also { read = it } != -1) {
|
||||
outputStream.write(buffer, 0, read)
|
||||
}
|
||||
outputStream.flush()
|
||||
outputStream.close()
|
||||
inputStream.close()
|
||||
return Uri.fromFile(file)
|
||||
} catch (e: Exception) {
|
||||
Log.e("ImageCopy", "Failed to copy image: ${e.message}")
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun KernelSUTheme(
|
||||
darkTheme: Boolean = when(ThemeConfig.forceDarkMode) {
|
||||
true -> true
|
||||
false -> false
|
||||
null -> isSystemInDarkTheme()
|
||||
},
|
||||
dynamicColor: Boolean = ThemeConfig.useDynamicColor,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
context.loadCustomBackground()
|
||||
context.loadThemeColors()
|
||||
context.loadDynamicColorState()
|
||||
|
||||
val colorScheme = when {
|
||||
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
||||
if (darkTheme) {
|
||||
val originalScheme = dynamicDarkColorScheme(context)
|
||||
originalScheme.copy(
|
||||
// 调整按钮相关颜色
|
||||
primary = adjustColor(originalScheme.primary),
|
||||
onPrimary = adjustColor(originalScheme.onPrimary),
|
||||
primaryContainer = adjustColor(originalScheme.primaryContainer),
|
||||
onPrimaryContainer = adjustColor(originalScheme.onPrimaryContainer),
|
||||
background = Color.Transparent,
|
||||
surface = Color.Transparent,
|
||||
onBackground = Color.White,
|
||||
onSurface = Color.White,
|
||||
onSecondary = Color.White,
|
||||
onTertiary = Color.White,
|
||||
onSecondaryContainer = Color.White,
|
||||
onTertiaryContainer = Color.White
|
||||
)
|
||||
} else {
|
||||
val originalScheme = dynamicLightColorScheme(context)
|
||||
originalScheme.copy(
|
||||
primary = adjustColor(originalScheme.primary),
|
||||
onPrimary = adjustColor(originalScheme.onPrimary),
|
||||
primaryContainer = adjustColor(originalScheme.primaryContainer),
|
||||
onPrimaryContainer = adjustColor(originalScheme.onPrimaryContainer),
|
||||
background = Color.Transparent,
|
||||
surface = Color.Transparent
|
||||
)
|
||||
}
|
||||
}
|
||||
darkTheme -> getDarkColorScheme()
|
||||
else -> getLightColorScheme()
|
||||
}
|
||||
|
||||
val isDarkModeWithCustomBackground = darkTheme && ThemeConfig.customBackgroundUri != null
|
||||
|
||||
if (darkTheme && !dynamicColor) {
|
||||
CardConfig.setDarkModeDefaults()
|
||||
}
|
||||
|
||||
CardConfig.updateShadowEnabled(!isDarkModeWithCustomBackground)
|
||||
|
||||
MaterialTheme(
|
||||
colorScheme = colorScheme,
|
||||
typography = Typography
|
||||
) {
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
// 背景图层
|
||||
ThemeConfig.customBackgroundUri?.let { uri ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.zIndex(-1f)
|
||||
) {
|
||||
// 背景图片
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.paint(
|
||||
painter = rememberAsyncImagePainter(
|
||||
model = uri,
|
||||
onError = {
|
||||
ThemeConfig.customBackgroundUri = null
|
||||
context.saveCustomBackground(null)
|
||||
}
|
||||
),
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
)
|
||||
|
||||
// 亮度调节层
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(
|
||||
if (darkTheme) {
|
||||
Color.Black.copy(alpha = 0.4f)
|
||||
} else {
|
||||
Color.White.copy(alpha = 0.1f)
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
// 边缘渐变遮罩层
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(
|
||||
Brush.radialGradient(
|
||||
colors = listOf(
|
||||
Color.Transparent,
|
||||
if (darkTheme) {
|
||||
Color.Black.copy(alpha = 0.5f)
|
||||
} else {
|
||||
Color.Black.copy(alpha = 0.2f)
|
||||
}
|
||||
),
|
||||
radius = 1200f
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
// 内容图层
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.zIndex(1f)
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Context.saveCustomBackground(uri: Uri?) {
|
||||
val newUri = uri?.let { copyImageToInternalStorage(it) }
|
||||
getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
||||
.edit {
|
||||
putString("custom_background", newUri?.toString())
|
||||
}
|
||||
ThemeConfig.customBackgroundUri = newUri
|
||||
}
|
||||
|
||||
fun Context.loadCustomBackground() {
|
||||
val uriString = getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
||||
.getString("custom_background", null)
|
||||
ThemeConfig.customBackgroundUri = uriString?.toUri()
|
||||
}
|
||||
|
||||
fun Context.saveThemeMode(forceDark: Boolean?) {
|
||||
getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
||||
.edit {
|
||||
putString(
|
||||
"theme_mode", when (forceDark) {
|
||||
true -> "dark"
|
||||
false -> "light"
|
||||
null -> "system"
|
||||
}
|
||||
)
|
||||
}
|
||||
ThemeConfig.forceDarkMode = forceDark
|
||||
}
|
||||
|
||||
fun Context.loadThemeMode() {
|
||||
val mode = getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
||||
.getString("theme_mode", "system")
|
||||
ThemeConfig.forceDarkMode = when(mode) {
|
||||
"dark" -> true
|
||||
"light" -> false
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
fun Context.saveThemeColors(themeName: String) {
|
||||
getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
||||
.edit {
|
||||
putString("theme_colors", themeName)
|
||||
}
|
||||
|
||||
ThemeConfig.currentTheme = when(themeName) {
|
||||
"blue" -> ThemeColors.Blue
|
||||
"green" -> ThemeColors.Green
|
||||
"purple" -> ThemeColors.Purple
|
||||
"orange" -> ThemeColors.Orange
|
||||
"pink" -> ThemeColors.Pink
|
||||
"gray" -> ThemeColors.Gray
|
||||
"yellow" -> ThemeColors.Yellow
|
||||
else -> ThemeColors.Default
|
||||
}
|
||||
}
|
||||
|
||||
fun Context.loadThemeColors() {
|
||||
val themeName = getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
||||
.getString("theme_colors", "default")
|
||||
|
||||
ThemeConfig.currentTheme = when(themeName) {
|
||||
"blue" -> ThemeColors.Blue
|
||||
"green" -> ThemeColors.Green
|
||||
"purple" -> ThemeColors.Purple
|
||||
"orange" -> ThemeColors.Orange
|
||||
"pink" -> ThemeColors.Pink
|
||||
"gray" -> ThemeColors.Gray
|
||||
"yellow" -> ThemeColors.Yellow
|
||||
else -> ThemeColors.Default
|
||||
}
|
||||
}
|
||||
|
||||
fun Context.saveDynamicColorState(enabled: Boolean) {
|
||||
getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
||||
.edit {
|
||||
putBoolean("use_dynamic_color", enabled)
|
||||
}
|
||||
ThemeConfig.useDynamicColor = enabled
|
||||
}
|
||||
|
||||
fun Context.loadDynamicColorState() {
|
||||
val enabled = getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
||||
.getBoolean("use_dynamic_color", true)
|
||||
ThemeConfig.useDynamicColor = enabled
|
||||
}
|
||||
|
||||
private fun adjustColor(color: Color): Color {
|
||||
val minLuminance = 0.75f
|
||||
val maxLuminance = 1f
|
||||
var luminance = color.luminance()
|
||||
if (luminance < minLuminance) {
|
||||
luminance = minLuminance
|
||||
} else if (luminance > maxLuminance) {
|
||||
luminance = maxLuminance
|
||||
}
|
||||
return color.copy(luminance)
|
||||
}
|
||||
|
||||
private fun mixColors(color1: Color, color2: Color, ratio: Float): Color {
|
||||
val r = (color1.red * ratio + color2.red * (1 - ratio))
|
||||
val g = (color1.green * ratio + color2.green * (1 - ratio))
|
||||
val b = (color1.blue * ratio + color2.blue * (1 - ratio))
|
||||
val a = (color1.alpha * ratio + color2.alpha * (1 - ratio))
|
||||
return Color(r, g, b, a)
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
package zako.zako.zako.ui.theme
|
||||
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
// Set of Material typography styles to start with
|
||||
val Typography = androidx.compose.material3.Typography(
|
||||
bodyLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 16.sp,
|
||||
lineHeight = 24.sp,
|
||||
letterSpacing = 0.5.sp
|
||||
)
|
||||
/* Other default text styles to override
|
||||
titleLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 22.sp,
|
||||
lineHeight = 28.sp,
|
||||
letterSpacing = 0.sp
|
||||
),
|
||||
labelSmall = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 11.sp,
|
||||
lineHeight = 16.sp,
|
||||
letterSpacing = 0.5.sp
|
||||
)
|
||||
*/
|
||||
)
|
||||
2
manager/app/src/main/jniLibs/.gitignore
vendored
2
manager/app/src/main/jniLibs/.gitignore
vendored
@@ -1,3 +1,5 @@
|
||||
libzakozako.so
|
||||
libzakozakozako.so
|
||||
libkpmmgr.so
|
||||
libzako.so
|
||||
libandroidx.graphics.path.so
|
||||
@@ -50,7 +50,6 @@
|
||||
<string name="home_click_to_learn_kernelsu">یاد بگیرید چگونه از کرنل اس یو و ماژول ها استفاده کنید</string>
|
||||
<string name="home_support_title">از ما حمایت کنید</string>
|
||||
<string name="home_support_content">KernelSU رایگان است و همیشه خواهد بود و منبع باز است. با این حال، می توانید با اهدای کمک مالی به ما نشان دهید که برایتان مهم است.</string>
|
||||
<string name="profile">پروفایل برنامه</string>
|
||||
<string name="profile_default">پیشفرض</string>
|
||||
<string name="profile_template">قالب</string>
|
||||
<string name="profile_custom">شخصی سازی شده</string>
|
||||
|
||||
@@ -50,7 +50,6 @@
|
||||
<string name="home_click_to_learn_kernelsu">Pelajari cara instal KernelSU dan menggunakan modul</string>
|
||||
<string name="home_support_title">Dukung Kami</string>
|
||||
<string name="home_support_content">KernelSU akan selalu menjadi aplikasi gratis dan terbuka. Anda dapat memberikan donasi sebagai bentuk dukungan.</string>
|
||||
<string name="profile">Profil Apl</string>
|
||||
<string name="profile_default">Bawaan</string>
|
||||
<string name="profile_template">Templat</string>
|
||||
<string name="profile_custom">Khusus</string>
|
||||
|
||||
@@ -34,10 +34,10 @@
|
||||
<string name="reboot">再起動</string>
|
||||
<string name="settings">設定</string>
|
||||
<string name="reboot_userspace">ソフトリブート</string>
|
||||
<string name="reboot_recovery">リカバリーへ再起動</string>
|
||||
<string name="reboot_bootloader">ブートローダーへ再起動</string>
|
||||
<string name="reboot_download">ダウンロードモードへ再起動</string>
|
||||
<string name="reboot_edl">EDL へ再起動</string>
|
||||
<string name="reboot_recovery">リカバリーで再起動</string>
|
||||
<string name="reboot_bootloader">ブートローダーで再起動</string>
|
||||
<string name="reboot_download">ダウンロードモードで再起動</string>
|
||||
<string name="reboot_edl">EDL で再起動</string>
|
||||
<string name="about">アプリについて</string>
|
||||
<string name="module_uninstall_confirm">モジュール %s をアンインストールしますか?</string>
|
||||
<string name="module_uninstall_success">%s はアンインストールされました</string>
|
||||
@@ -72,9 +72,8 @@
|
||||
<string name="failed_to_update_app_profile">%s のアプリのプロファイルの更新をできませでした</string>
|
||||
<string name="require_kernel_version" formatted="false">現在の KernelSU のバージョン %d は低すぎるため、マネージャーは正常に動作しません。バージョン %d 以上に更新してください!</string>
|
||||
<string name="settings_umount_modules_default">デフォルトでモジュールのマウントを解除する</string>
|
||||
<string name="settings_umount_modules_default_summary">アプリプロファイルの「モジュールのアンマウント」の共通のデフォルト値です。 有効にすると、プロファイルセットを持たないアプリのシステムに対するすべてのモジュールの変更が削除されます。</string>
|
||||
<string name="settings_susfs_toggle">Kprobe フックを非表示にする</string>
|
||||
<string name="settings_susfs_toggle_summary">KSU によって生成された Kprobe フックを無効化して、代替となる組み込みの非 Kprobe を有効化します。Kprobe をサポートしない 非 GKI カーネルに適用される同等の機能を実装します。</string>
|
||||
<string name="settings_umount_modules_default_summary">アプリプロファイルの「モジュールのアンマウント」の共通となるデフォルト値です。 有効にすると、プロファイルセットを持たないアプリのシステムに対するすべてのモジュールの変更が削除されます。</string>
|
||||
<string name="settings_susfs_toggle">kprobe フックを無効化</string>
|
||||
<string name="profile_umount_modules_summary">このオプションを有効にすると、KernelSU はこのアプリのモジュールによって変更されたファイルを復元できるようになります。</string>
|
||||
<string name="profile_selinux_domain">ドメイン</string>
|
||||
<string name="profile_selinux_rules">ルール</string>
|
||||
@@ -88,7 +87,7 @@
|
||||
<string name="failed_to_update_sepolicy">SELinux ルールの更新に失敗しました %s</string>
|
||||
<string name="module_changelog">変更履歴</string>
|
||||
<string name="settings_profile_template">アプリプロファイルのテンプレート</string>
|
||||
<string name="settings_profile_template_summary">アプリプロファイルのローカルおよびオンラインテンプレートを管理します。</string>
|
||||
<string name="settings_profile_template_summary">アプリプロファイルのローカルおよびオンラインテンプレートを管理します</string>
|
||||
<string name="app_profile_template_create">テンプレートの作成</string>
|
||||
<string name="app_profile_template_edit">テンプレートの編集</string>
|
||||
<string name="app_profile_template_id">ID</string>
|
||||
@@ -110,14 +109,14 @@
|
||||
<string name="app_profile_template_import_empty">クリップボードが空です!</string>
|
||||
<string name="module_changelog_failed">変更ログの取得に失敗しました: %s</string>
|
||||
<string name="settings_check_update">更新を確認する</string>
|
||||
<string name="settings_check_update_summary">アプリを開いたときに更新を自動的に確認します。</string>
|
||||
<string name="settings_check_update_summary">アプリを開いたときに更新を自動的に確認します</string>
|
||||
<string name="grant_root_failed">root の付与に失敗しました!</string>
|
||||
<string name="action">アクション</string>
|
||||
<string name="open">開く</string>
|
||||
<string name="enable_web_debugging">WebView デバッグを有効化する</string>
|
||||
<string name="enable_web_debugging_summary">WebUI のデバッグに使用できます。必要な場合にのみ有効にしてください。</string>
|
||||
<string name="enable_web_debugging_summary">WebUI のデバッグに使用できます。必要な場合でのみ有効化してください</string>
|
||||
<string name="direct_install">直接インストール (推奨)</string>
|
||||
<string name="select_file">ファイルを選択</string>
|
||||
<string name="select_file">パッチを適用する必要があるミラーを選択</string>
|
||||
<string name="install_inactive_slot">非アクティブなスロットにインストール (OTA 後)</string>
|
||||
<string name="install_inactive_slot_warning">再起動後、デバイスは**強制的に**、現在非アクティブなスロットから起動します。
|
||||
\nこのオプションは、OTA が完了した後にのみ使用してください。
|
||||
@@ -171,10 +170,9 @@
|
||||
<string name="allowlist_restore_failed">許可リストの復元に失敗: %1$s</string>
|
||||
<string name="backup_allowlist">許可リストをバックアップ</string>
|
||||
<string name="restore_allowlist">許可リストを復元</string>
|
||||
<string name="settings_custom_background">カスタム背景を設定</string>
|
||||
<string name="settings_custom_background_summary">カスタム背景を設定します。</string>
|
||||
<string name="settings_card_manage">カードの管理</string>
|
||||
<string name="settings_card_alpha">カードのアルファ</string>
|
||||
<string name="settings_custom_background">カスタムアプリ背景</string>
|
||||
<string name="settings_custom_background_summary">背景にする画像を選択してください</string>
|
||||
<string name="settings_card_alpha">ナビゲーションバーの透過</string>
|
||||
<string name="settings_restore_default">デフォルトに復元</string>
|
||||
<string name="home_android_version">Android のバージョン</string>
|
||||
<string name="home_device_model">デバイスモデル</string>
|
||||
@@ -189,23 +187,24 @@
|
||||
<string name="selinux_enabled">有効</string>
|
||||
<string name="selinux_disabled">無効</string>
|
||||
<string name="simple_mode">シンプルモード</string>
|
||||
<string name="simple_mode_summary">ON にすると不要なカードを非表示にします。</string>
|
||||
<string name="simple_mode_summary">ON にすると不要なカードを非表示にします</string>
|
||||
<string name="hide_kernel_kernelsu_version">カーネルのバージョンを非表示にする</string>
|
||||
<string name="hide_kernel_kernelsu_version_summary">カーネルのバージョンを非表示にします。</string>
|
||||
<string name="hide_kernel_kernelsu_version_summary">カーネルのバージョンを非表示にします</string>
|
||||
<string name="hide_other_info">その他の情報を非表示にする</string>
|
||||
<string name="hide_other_info_summary">ホームページ上のスーパーユーザー、モジュール、KPM モジュールの数に関する情報を非表示にします。</string>
|
||||
<string name="hide_other_info_summary">ホームページ上のスーパーユーザー、モジュール、KPM モジュールの数に関する情報を非表示にします</string>
|
||||
<string name="hide_susfs_status">SuSFS ステータスを非表示にする</string>
|
||||
<string name="hide_susfs_status_summary">ホームページ上の SuSFS ステータス情報を非表示にします。</string>
|
||||
<string name="theme_mode">テーマモード</string>
|
||||
<string name="hide_susfs_status_summary">ホームページ上の SuSFS ステータス情報を非表示にします</string>
|
||||
<string name="hide_link_card">リンクカードのステータスを隠す</string>
|
||||
<string name="hide_link_card_summary">ホームページのリンクカード情報を隠す</string>
|
||||
<string name="theme_mode">テーマ</string>
|
||||
<string name="theme_follow_system">システムに従う</string>
|
||||
<string name="theme_light">ライトカラー</string>
|
||||
<string name="theme_dark">ダークカラー</string>
|
||||
<string name="theme_light">ライト</string>
|
||||
<string name="theme_dark">ダーク</string>
|
||||
<string name="manual_hook">手動でフック</string>
|
||||
<string name="dynamic_color_title">ダイナミックカラー</string>
|
||||
<string name="dynamic_color_summary">システムテーマのダイナミックカラーを使用します</string>
|
||||
<string name="choose_theme_color">テーマカラーを選択</string>
|
||||
<string name="color_default">ホワイト</string>
|
||||
<string name="color_blue">ブルー</string>
|
||||
<string name="color_default">ブルー</string>
|
||||
<string name="color_green">グリーン</string>
|
||||
<string name="color_purple">パープル</string>
|
||||
<string name="color_orange">オレンジ</string>
|
||||
@@ -214,7 +213,8 @@
|
||||
<string name="color_ivory">アイボリー</string>
|
||||
<string name="flash_option">ブラシの設定</string>
|
||||
<string name="flash_option_tip">フラッシュするファイルを選択</string>
|
||||
<string name="horizon_kernel">AnyKernel3 をフラッシュ</string>
|
||||
<string name="horizon_kernel">AnyKernel3 をインストール</string>
|
||||
<string name="horizon_kernel_summary">AnyKernel3 カーネルファイルをフラッシュします</string>
|
||||
<string name="root_required">root 権限が必要です</string>
|
||||
<string name="copy_failed">ファイルのコピーに失敗しました</string>
|
||||
<string name="reboot_complete_title">スクラブが完了しました</string>
|
||||
@@ -222,48 +222,107 @@
|
||||
<string name="yes">はい</string>
|
||||
<string name="no">いいえ</string>
|
||||
<string name="failed_reboot">再起動に失敗しました</string>
|
||||
<string name="batch_authorization">bulk ライセンス</string>
|
||||
<string name="batch_authorization">Bulk ライセンス</string>
|
||||
<string name="batch_cancel_authorization">認証を一括でキャンセル</string>
|
||||
<string name="backup">バックアップ</string>
|
||||
<string name="color_yellow">イエロー</string>
|
||||
<string name="kpm">KPM モジュール</string>
|
||||
<string name="kpm">カーネルモジュール</string>
|
||||
<string name="kpm_title">カーネルモジュール</string>
|
||||
<string name="kpm_empty">カーネルモジュールは現在インストールされていません</string>
|
||||
<string name="kpm_version">リリース</string>
|
||||
<string name="kpm_version">バージョン</string>
|
||||
<string name="kpm_author">作者</string>
|
||||
<string name="kpm_uninstall">アンインストール</string>
|
||||
<string name="kpm_uninstall_success">アンインストールに失敗しました</string>
|
||||
<string name="kpm_uninstall_failed">アンインストールに失敗しました</string>
|
||||
<string name="kpm_install">インストールを選択</string>
|
||||
<string name="kpm_install">インストール</string>
|
||||
<string name="kpm_install_success">KPM モジュールの読み込みに成功しました</string>
|
||||
<string name="kpm_install_failed">KPM モジュールの読み込みに失敗しました</string>
|
||||
<string name="kpm_args">KPM パラメーター</string>
|
||||
<string name="kpm_control">完全</string>
|
||||
<string name="kpm_args">パラメータ</string>
|
||||
<string name="kpm_control">実行</string>
|
||||
<string name="home_kpm_version">KPM のバージョン</string>
|
||||
<string name="close_notice">閉じる</string>
|
||||
<string name="kernel_module_notice">以下のカーネルモジュール関数は KernelPatch によって開発され、SukiSU Ultra のカーネルモジュール関数を含むように変更されました</string>
|
||||
<string name="home_ContributionCard_kernelsu">SukiSU Ultra の今後にご期待ください</string>
|
||||
<string name="kpm_control_success">成功</string>
|
||||
<string name="kpm_control_failed">失敗</string>
|
||||
<string name="home_click_to_ContributionCard_kernelsu">SukiSU Ultra は将来的に KSU から比較的に独立したブランチになりますが、公式の KernelSU や MKSU などの貢献に繋がるでしょう!</string>
|
||||
<string name="home_click_to_ContributionCard_kernelsu">SukiSU Ultra は将来的に KSU から比較的に独立したブランチになりますが、公式の KernelSU や MKSU などの貢献は引き続き感謝しています!</string>
|
||||
<string name="not_supported">非対応</string>
|
||||
<string name="supported">対応</string>
|
||||
<string name="home_kpm_module">KPM モジュール数: %d </string>
|
||||
<string name="home_kpm_module">"KPM モジュールの数: %d "</string>
|
||||
<string name="kpm_invalid_file">無効な KPM ファイル</string>
|
||||
<string name="kernel_patched">カーネルはパッチされていません</string>
|
||||
<string name="kernel_not_enabled">カーネルは設定されていません</string>
|
||||
<string name="kernel_not_enabled">カーネルは未設定です</string>
|
||||
<string name="custom_settings">カスタム設定</string>
|
||||
<string name="kpm_install_mode">インストール</string>
|
||||
<string name="kpm_install_mode">KPM をインストール</string>
|
||||
<string name="kpm_install_mode_load">読み込む</string>
|
||||
<string name="kpm_install_mode_embed">埋め込む</string>
|
||||
<string name="kpm_install_mode_description">モジュールのインストールモードを選択してください:\n\n読み込む: モジュールを一時的に読み込みます\n埋め込む: システムに恒久的にインストールします</string>
|
||||
<string name="kpm_install_mode_description">選択してください: %1\$s モジュールのインストールモード \n\n読み込む: モジュールを一時的に読み込みます\n埋め込む: システムで恒久的にインストールします</string>
|
||||
<string name="log_failed_to_check_module_file">モジュールファイルの存在を確認できませんでした</string>
|
||||
<string name="snackbar_failed_to_check_module_file">モジュールファイルが存在するか確認できません</string>
|
||||
<string name="confirm_uninstall_title">アンインストールを確認</string>
|
||||
<string name="confirm_uninstall_confirm">アンインストール中</string>
|
||||
<string name="confirm_uninstall_dismiss">中止</string>
|
||||
<string name="confirm_uninstall_confirm">アンインストール</string>
|
||||
<string name="confirm_uninstall_dismiss">キャンセル</string>
|
||||
<string name="theme_color">テーマカラー</string>
|
||||
<string name="invalid_file_type">ファイルの種類が正しくありません。.kpm ファイルを選択してください。</string>
|
||||
<string name="invalid_file_type">ファイルの種類が間違っています!.kpm ファイルを選択してください。</string>
|
||||
<string name="confirm_uninstall_title_with_filename">アンインストール</string>
|
||||
<string name="confirm_uninstall_content">次の KPM モジュールがアンインストールされます:\n%s</string>
|
||||
<string name="confirm_uninstall_content">次の KPM がアンインストールされます: %s</string>
|
||||
<string name="settings_susfs_toggle_summary">KernelSU によって作成された kprobe フックを無効化して、代替となるインラインフックを使用します。これは、非 GKI カーネルのフック方式に似た物になります。</string>
|
||||
<string name="image_editor_title">背景画像を調整</string>
|
||||
<string name="image_editor_hint">2 本の指で画像を拡大、1 本の指でドラッグで位置を調整します</string>
|
||||
<string name="background_image_error">イメージを読み込めません</string>
|
||||
<string name="reprovision">再プロビジョニング</string>
|
||||
<!-- Kernel Flash Progress Related -->
|
||||
<string name="horizon_flash_title">カーネルをフラッシュ</string>
|
||||
<string name="horizon_logs_label">ログ:</string>
|
||||
<string name="horizon_flash_complete">フラッシュが完了しました</string>
|
||||
<!-- Flash Status Related -->
|
||||
<string name="horizon_preparing">準備中…</string>
|
||||
<string name="horizon_cleaning_files">ファイルを削除中…</string>
|
||||
<string name="horizon_copying_files">ファイルをコピー中…</string>
|
||||
<string name="horizon_extracting_tool">フラッシュツールを展開中…</string>
|
||||
<string name="horizon_patching_script">フラッシュスクリプトをパッチ中…</string>
|
||||
<string name="horizon_flashing">カーネルをフラッシュ中…</string>
|
||||
<string name="horizon_flash_complete_status">フラッシュが完了しました</string>
|
||||
<!-- Slot selection related strings -->
|
||||
<string name="select_slot_title">フラッシュ先のスロットを選択</string>
|
||||
<string name="select_slot_description">フラッシュする boot のターゲットスロットを選択</string>
|
||||
<string name="slot_a">スロット A</string>
|
||||
<string name="slot_b">スロット B</string>
|
||||
<string name="selected_slot">選択したスロット: %1$s</string>
|
||||
<string name="horizon_getting_original_slot">オリジナルのスロットを取得</string>
|
||||
<string name="horizon_setting_target_slot">指定するスロットを設定</string>
|
||||
<string name="horizon_restoring_original_slot">デフォルトのスロットに復元</string>
|
||||
<string name="current_slot">現在のスロット: %1$s </string>
|
||||
<!-- Error Messages -->
|
||||
<string name="horizon_copy_failed">コピーに失敗しました</string>
|
||||
<string name="horizon_unknown_error">不明なエラー</string>
|
||||
<string name="flash_failed_message">フラッシュに失敗しました</string>
|
||||
<!-- lkm/gki install -->
|
||||
<string name="Lkm_install_methods">LKM の修復またはインストール</string>
|
||||
<string name="GKI_install_methods">GKI/non-GKI のインストール</string>
|
||||
<string name="kernel_version_log">カーネルのバージョン: %1$s</string>
|
||||
<string name="tool_version_log">パッチ適用ツールの使用: %1$s</string>
|
||||
<string name="configuration">設定</string>
|
||||
<string name="app_settings">アプリの設定</string>
|
||||
<string name="tools">ツール</string>
|
||||
<string name="currently_selected">現在</string>
|
||||
<!-- String resources used in SuperUser -->
|
||||
<string name="clear">削除</string>
|
||||
<string name="apps_with_root">root アプリの権限</string>
|
||||
<string name="apps_with_custom_profile">カスタマイズされたアプリ構成</string>
|
||||
<string name="other_apps">その他のアプリ</string>
|
||||
<string name="no_apps_found">アプリがありません</string>
|
||||
<string name="selinux_enabled_toast">SELinux 有効</string>
|
||||
<string name="selinux_disabled_toast">SELinux 無効</string>
|
||||
<string name="selinux_change_failed">SELinux ステータスの変更に失敗しました</string>
|
||||
<string name="advanced_settings">高度な設定</string>
|
||||
<string name="appearance_settings">ツールバーをカスタマイズ</string>
|
||||
<string name="back">戻る</string>
|
||||
<string name="expand">最高の状態</string>
|
||||
<string name="collapse">設置</string>
|
||||
<string name="susfs_enabled">SuSFS 有効</string>
|
||||
<string name="susfs_disabled">SuSFS 無効</string>
|
||||
<string name="background_set_success">背景の設定が成功しました</string>
|
||||
<string name="background_removed">カスタム背景を削除しました</string>
|
||||
<string name="root_require_for_install">root 権限が必要</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name" translatable="false">KernelSU</string>
|
||||
<string name="home">Strona główna</string>
|
||||
<string name="home_not_installed">Nie zainstalowano</string>
|
||||
<string name="home_click_to_install">Kliknij, aby zainstalować</string>
|
||||
@@ -51,7 +50,6 @@
|
||||
<string name="home_click_to_learn_kernelsu">Dowiedz się jak zainstalować KernelSU i jak korzystać z modułów</string>
|
||||
<string name="home_support_title">Wesprzyj nas</string>
|
||||
<string name="home_support_content">KernelSU jest i zawsze będzie darmowy oraz otwarty. Niemniej jednak możesz nam pokazać, że Ci zależy, wysyłając darowiznę.</string>
|
||||
<string name="profile" translatable="false">Profil aplikacji</string>
|
||||
<string name="profile_default">Domyślny</string>
|
||||
<string name="profile_template">Szablon</string>
|
||||
<string name="profile_custom">Własny</string>
|
||||
|
||||
@@ -50,7 +50,6 @@
|
||||
<string name="home_click_to_learn_kernelsu">Saiba como instalar o KernelSU e usar os módulos</string>
|
||||
<string name="home_support_title">Apoie-nos</string>
|
||||
<string name="home_support_content">KernelSU sempre foi e sempre será, gratuito e de código aberto. No entanto, você pode nos ajudar enviando uma pequena doação.</string>
|
||||
<string name="profile" translatable="false">Perfil do Aplicativo</string>
|
||||
<string name="profile_default">Padrão</string>
|
||||
<string name="profile_template">Modelo</string>
|
||||
<string name="profile_custom">Personalizado</string>
|
||||
|
||||
@@ -52,7 +52,6 @@
|
||||
<string name="home_click_to_learn_kernelsu">Узнайте, как установить KernelSU и использовать модули</string>
|
||||
<string name="home_support_title">Поддержите нас</string>
|
||||
<string name="home_support_content">KernelSU был и всегда будет бесплатным и открытым проектом. Однако Вы всегда можете поддержать нас, отправив небольшое пожертвование.</string>
|
||||
<string name="profile" translatable="false">App Profile</string>
|
||||
<!--Don't translate this string!-->
|
||||
<string name="profile_default">По умолчанию</string>
|
||||
<string name="profile_template">Шаблон</string>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name" translatable="false">KernelSU</string>
|
||||
<string name="home">Ana Sayfa</string>
|
||||
<string name="home_not_installed">Kurulmadı</string>
|
||||
<string name="home_click_to_install">Kurmak için tıklayın</string>
|
||||
@@ -51,7 +50,6 @@
|
||||
<string name="home_click_to_learn_kernelsu">KernelSU\'nun nasıl kurulacağını ve modüllerin nasıl kullanılacağını öğrenin</string>
|
||||
<string name="home_support_title">Bizi destekleyin</string>
|
||||
<string name="home_support_content">KernelSU ücretsiz ve açık kaynaklı bir yazılımdır ve her zaman öyle kalacaktır. Ancak bağış yaparak bize destek olduğunuzu gösterebilirsiniz.</string>
|
||||
<string name="profile" translatable="false">Uygulama profili</string>
|
||||
<string name="profile_default">Varsayılan</string>
|
||||
<string name="profile_template">Şablon</string>
|
||||
<string name="profile_custom">Özel</string>
|
||||
|
||||
@@ -50,7 +50,6 @@
|
||||
<string name="home_click_to_learn_kernelsu">Дізнайтеся, як інсталювати KernelSU і використовувати модулі</string>
|
||||
<string name="home_support_title">Підтримати нас</string>
|
||||
<string name="home_support_content">KernelSU є, і завжди буде безкоштовним та з відкритим кодом. Однак, якщо вам не байдуже, можете зробити невеличке пожертвування.</string>
|
||||
<string name="profile">Профіль додатка</string>
|
||||
<string name="profile_default">Типовий</string>
|
||||
<string name="profile_template">Шаблон</string>
|
||||
<string name="profile_custom">Власний</string>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name" translatable="false">SukiSU Ultra</string>
|
||||
<string name="home">Home</string>
|
||||
<string name="home_not_installed">Chưa cài đặt</string>
|
||||
<string name="home_click_to_install">Nhấn vào đây để cài đặt</string>
|
||||
@@ -58,7 +57,6 @@
|
||||
<string name="home_support_title">Hỗ trợ chúng tôi</string>
|
||||
<string name="home_support_content">KernelSU sẽ luôn là miễn phí và mã nguồn mở. Tuy nhiên, bạn có thể cho chúng tôi thấy rằng bạn quan tâm bằng cách quyên góp.</string>
|
||||
<string name="about_source_code"><![CDATA[View source code at %1$s<br/>Tham gia kênh %2$s của chúng tôi]]></string>
|
||||
<string name="profile" translatable="false">Hồ sơ ứng dụng</string>
|
||||
<string name="profile_default">Mặc định</string>
|
||||
<string name="profile_template">Bản mẫu</string>
|
||||
<string name="profile_custom">Tùy chỉnh</string>
|
||||
@@ -173,7 +171,6 @@
|
||||
<string name="restore_allowlist">Khôi phục danh sách cho phép</string>
|
||||
<string name="settings_custom_background">Cài đặt nền tùy chỉnh</string>
|
||||
<string name="settings_custom_background_summary">Cài đặt tùy chỉnh nền</string>
|
||||
<string name="settings_card_manage">Quản lý thẻ</string>
|
||||
<string name="settings_card_alpha">Thẻ alpha</string>
|
||||
<string name="settings_restore_default">Khôi phục mặc định</string>
|
||||
<string name="home_android_version">Phiên bản Android</string>
|
||||
@@ -204,8 +201,7 @@
|
||||
<string name="dynamic_color_title">Màu sắc động</string>
|
||||
<string name="dynamic_color_summary">Màu sắc động sử dụng chủ đề hệ thống</string>
|
||||
<string name="choose_theme_color">Chọn màu chủ đề</string>
|
||||
<string name="color_default">Trắng</string>
|
||||
<string name="color_blue">Xanh dương</string>
|
||||
<string name="color_default">Xanh dương</string>
|
||||
<string name="color_green">Xanh lá</string>
|
||||
<string name="color_purple">Tím</string>
|
||||
<string name="color_orange">Cam</string>
|
||||
@@ -256,7 +252,7 @@
|
||||
<string name="kpm_install_mode">Cài đặt</string>
|
||||
<string name="kpm_install_mode_load">Tải</string>
|
||||
<string name="kpm_install_mode_embed">Nhúng</string>
|
||||
<string name="kpm_install_mode_description">Vui lòng chọn chế độ cài đặt mô-đun: \n\nTải: Tải tạm thời mô-đun \nNhúng: Cài đặt vĩnh viễn vào hệ thống</string>
|
||||
<string name="kpm_install_mode_description">Vui lòng:%1\$s chọn chế độ cài đặt mô-đun \n\nTải: Tải tạm thời mô-đun \nNhúng: Cài đặt vĩnh viễn vào hệ thống</string>
|
||||
<string name="log_failed_to_check_module_file">Không kiểm tra được sự tồn tại của tệp mô-đun</string>
|
||||
<string name="snackbar_failed_to_check_module_file">Không thể kiểm tra xem tệp mô-đun có tồn tại không</string>
|
||||
<string name="confirm_uninstall_title">Xác nhận gỡ cài đặt</string>
|
||||
|
||||
@@ -117,7 +117,7 @@
|
||||
<string name="enable_web_debugging">启用 WebView 调试</string>
|
||||
<string name="enable_web_debugging_summary">可用于调试 WebUI 。请仅在需要时启用。</string>
|
||||
<string name="direct_install">直接安装(推荐)</string>
|
||||
<string name="select_file">选择一个文件</string>
|
||||
<string name="select_file">选择一个需要修补的镜像</string>
|
||||
<string name="install_inactive_slot">安装到未使用的槽位(OTA 后)</string>
|
||||
<string name="install_inactive_slot_warning">将在重启后强制切换到另一个槽位!\n注意只能在 OTA 更新完成后的重启之前使用。\n确认?</string>
|
||||
<string name="install_next">下一步</string>
|
||||
@@ -172,8 +172,7 @@
|
||||
<string name="restore_allowlist">还原应用列表</string>
|
||||
<string name="settings_custom_background">自定义背景</string>
|
||||
<string name="settings_custom_background_summary">选择一张图片作为应用背景</string>
|
||||
<string name="settings_card_manage">卡片管理</string>
|
||||
<string name="settings_card_alpha">卡片透明度</string>
|
||||
<string name="settings_card_alpha">卡片不透明度</string>
|
||||
<string name="settings_restore_default">恢复默认</string>
|
||||
<string name="home_android_version">Android 版本</string>
|
||||
<string name="home_device_model">设备</string>
|
||||
@@ -195,6 +194,8 @@
|
||||
<string name="hide_other_info_summary">隐藏主页上的超级用户数、模块数和 KPM 模块数信息</string>
|
||||
<string name="hide_susfs_status">隐藏 SuSFS 状态信息</string>
|
||||
<string name="hide_susfs_status_summary">隐藏主页上的 SuSFS 状态信息</string>
|
||||
<string name="hide_link_card">隐藏链接卡片</string>
|
||||
<string name="hide_link_card_summary">隐藏主页上的链接卡片信息</string>
|
||||
<string name="theme_mode">主题模式</string>
|
||||
<string name="theme_follow_system">跟随系统</string>
|
||||
<string name="theme_light">浅色</string>
|
||||
@@ -203,8 +204,7 @@
|
||||
<string name="dynamic_color_title">动态颜色</string>
|
||||
<string name="dynamic_color_summary">使用系统主题的动态颜色</string>
|
||||
<string name="choose_theme_color">选择主题色</string>
|
||||
<string name="color_default">白色</string>
|
||||
<string name="color_blue">蓝色</string>
|
||||
<string name="color_default">蓝色</string>
|
||||
<string name="color_green">绿色</string>
|
||||
<string name="color_purple">紫色</string>
|
||||
<string name="color_orange">橙色</string>
|
||||
@@ -214,6 +214,7 @@
|
||||
<string name="flash_option">刷入选项</string>
|
||||
<string name="flash_option_tip">选择要刷入的文件</string>
|
||||
<string name="horizon_kernel">刷写 AnyKernel3 压缩包</string>
|
||||
<string name="horizon_kernel_summary">刷入 Anykernel3 内核</string>
|
||||
<string name="root_required">需要 root 权限</string>
|
||||
<string name="copy_failed">文件复制失败</string>
|
||||
<string name="reboot_complete_title">刷写完成</string>
|
||||
@@ -221,10 +222,9 @@
|
||||
<string name="yes">是</string>
|
||||
<string name="no">否</string>
|
||||
<string name="failed_reboot">重启失败</string>
|
||||
<string name="batch_authorization">批量授权</string>
|
||||
<string name="batch_cancel_authorization">批量取消授权</string>
|
||||
<string name="batch_authorization">授权</string>
|
||||
<string name="batch_cancel_authorization">撤销</string>
|
||||
<string name="color_yellow">黄色</string>
|
||||
<string name="kpm">内核模块</string>
|
||||
<string name="kpm_title">内核模块</string>
|
||||
<string name="kpm_empty">暂无已安装的内核模块</string>
|
||||
<string name="kpm_version">版本</string>
|
||||
@@ -236,7 +236,7 @@
|
||||
<string name="kpm_install_success">加载 kpm 模块成功</string>
|
||||
<string name="kpm_install_failed">加载 kpm 模块失败</string>
|
||||
<string name="home_kpm_version">KPM 版本</string>
|
||||
<string name="kpm_control">执行</string>
|
||||
<string name="kpm_control">调参</string>
|
||||
<string name="close_notice">关闭</string>
|
||||
<string name="kpm_control_success">成功</string>
|
||||
<string name="kpm_control_failed">错误</string>
|
||||
@@ -253,7 +253,7 @@
|
||||
<string name="kpm_install_mode">安装</string>
|
||||
<string name="kpm_install_mode_load">加载</string>
|
||||
<string name="kpm_install_mode_embed">嵌入</string>
|
||||
<string name="kpm_install_mode_description">请选择模块安装模式:\n\n加载:临时加载模块\n嵌入:永久安装到系统</string>
|
||||
<string name="kpm_install_mode_description">请选择: %1\$s 模块的安装模式 \n\n加载:临时加载模块\n嵌入:永久安装到系统</string>
|
||||
<string name="snackbar_failed_to_check_module_file">无法检查模块文件是否存在</string>
|
||||
<string name="confirm_uninstall_title">确认卸载</string>
|
||||
<string name="confirm_uninstall_confirm">删除</string>
|
||||
@@ -262,4 +262,62 @@
|
||||
<string name="invalid_file_type">文件类型不正确,请选择 .kpm 文件</string>
|
||||
<string name="confirm_uninstall_title_with_filename">卸载</string>
|
||||
<string name="confirm_uninstall_content">将卸载以下 kpm 模块:\n%s</string>
|
||||
<string name="image_editor_title">调整背景图片</string>
|
||||
<string name="image_editor_hint">使用双指缩放图片,单指拖动调整位置</string>
|
||||
<string name="background_image_error">无法加载图片</string>
|
||||
<string name="reprovision">重置</string>
|
||||
<!-- Anykernel3 Kernel刷写进度相关 -->
|
||||
<string name="horizon_flash_title">刷写Kernel</string>
|
||||
<string name="horizon_logs_label">日志:</string>
|
||||
<string name="horizon_flash_complete">刷写完成</string>
|
||||
<!-- 刷写状态相关 -->
|
||||
<string name="horizon_preparing">准备中…</string>
|
||||
<string name="horizon_cleaning_files">清理文件…</string>
|
||||
<string name="horizon_copying_files">复制文件…</string>
|
||||
<string name="horizon_extracting_tool">提取刷写工具…</string>
|
||||
<string name="horizon_patching_script">修补刷写脚本…</string>
|
||||
<string name="horizon_flashing">刷写内核中…</string>
|
||||
<string name="horizon_flash_complete_status">刷写完成</string>
|
||||
<!-- 槽位选择相关字符串 -->
|
||||
<string name="select_slot_title">选择刷写槽位</string>
|
||||
<string name="select_slot_description">请选择要刷写boot的目标槽位</string>
|
||||
<string name="slot_a">A槽位</string>
|
||||
<string name="slot_b">B槽位</string>
|
||||
<string name="selected_slot">已选择槽位: %1$s</string>
|
||||
<string name="horizon_getting_original_slot">获取原有槽位</string>
|
||||
<string name="horizon_setting_target_slot">设置指定槽位</string>
|
||||
<string name="horizon_restoring_original_slot">恢复默认槽位</string>
|
||||
<string name="current_slot">当前槽位:%1$s </string>
|
||||
<!-- 错误信息 -->
|
||||
<string name="horizon_copy_failed">复制失败</string>
|
||||
<string name="horizon_unknown_error">未知错误</string>
|
||||
<string name="flash_failed_message">刷写失败</string>
|
||||
<!-- lkm/gki install -->
|
||||
<string name="Lkm_install_methods">LKM修补/安装</string>
|
||||
<string name="GKI_install_methods">GKI/non-GKI安装</string>
|
||||
<string name="kernel_version_log">内核版本:%1$s</string>
|
||||
<string name="tool_version_log">使用修补工具:%1$s</string>
|
||||
<string name="configuration">配置</string>
|
||||
<string name="app_settings">应用设置</string>
|
||||
<string name="tools">工具</string>
|
||||
<string name="currently_selected">当前</string>
|
||||
<!-- SuperUser 里用到的字符串资源 -->
|
||||
<string name="clear">清除</string>
|
||||
<string name="apps_with_root">Root 权限应用</string>
|
||||
<string name="apps_with_custom_profile">自定义配置应用</string>
|
||||
<string name="other_apps">其他应用</string>
|
||||
<string name="no_apps_found">未找到应用</string>
|
||||
<string name="selinux_enabled_toast">SELinux 已设置为启用状态</string>
|
||||
<string name="selinux_disabled_toast">SELinux 已设置为禁用状态</string>
|
||||
<string name="selinux_change_failed">SELinux 状态更改失败</string>
|
||||
<string name="advanced_settings">高级设置</string>
|
||||
<string name="appearance_settings">外观设置</string>
|
||||
<string name="back">返回</string>
|
||||
<string name="expand">展开</string>
|
||||
<string name="collapse">收起</string>
|
||||
<string name="susfs_enabled">SuSFS 已启用</string>
|
||||
<string name="susfs_disabled">SuSFS 已禁用</string>
|
||||
<string name="background_set_success">背景设置成功</string>
|
||||
<string name="background_removed">已移除自定义背景</string>
|
||||
<string name="root_require_for_install">需要 root 权限</string>
|
||||
</resources>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<string name="home_unsupported_reason">No KernelSU driver detected on your kernel, wrong kernel?.</string>
|
||||
<string name="home_kernel">Kernel version</string>
|
||||
<string name="home_susfs">SuSFS: %s</string>
|
||||
<string name="home_susfs_version">Version de SuSFS</string>
|
||||
<string name="home_susfs_version">SuSFS Version</string>
|
||||
<string name="home_susfs_sus_su">SuS SU</string>
|
||||
<string name="home_manager_version">Manager version</string>
|
||||
<string name="home_fingerprint">Fingerprint</string>
|
||||
@@ -75,8 +75,7 @@
|
||||
<string name="require_kernel_version" formatted="false">The current KernelSU version %d is too low for the manager to work properly. Please upgrade to version %d or higher!</string>
|
||||
<string name="settings_umount_modules_default">Umount modules by default</string>
|
||||
<string name="settings_umount_modules_default_summary">The global default value for \"Umount modules\" in App Profile. If enabled, it will remove all module modifications to the system for apps that don\'t have a profile set.</string>
|
||||
<string name="settings_susfs_toggle">Cacher les hooks kprobe</string>
|
||||
<string name="settings_susfs_toggle_summary">Désactive les hooks kprobe créés par KSU et, à la place, active les hooks non-kprobe intégrés, implémentant les mêmes fonctionnalités qui seraient appliquées à un kernel non-GKI, qui ne supportent krpobe.</string>
|
||||
<string name="settings_susfs_toggle">Disable kprobe hooks</string>
|
||||
<string name="profile_umount_modules_summary">Enabling this option will allow KernelSU to restore any modified files by the modules for this app.</string>
|
||||
<string name="profile_selinux_domain">Domain</string>
|
||||
<string name="profile_selinux_rules">Rules</string>
|
||||
@@ -119,7 +118,7 @@
|
||||
<string name="enable_web_debugging">Enable WebView debugging</string>
|
||||
<string name="enable_web_debugging_summary">Can be used to debug WebUI. Please enable only when needed.</string>
|
||||
<string name="direct_install">Direct install (Recommended)</string>
|
||||
<string name="select_file">Select a file</string>
|
||||
<string name="select_file">Select a image that needs to be patched</string>
|
||||
<string name="install_inactive_slot">Install to inactive slot (After OTA)</string>
|
||||
<string name="install_inactive_slot_warning">Your device will be **FORCED** to boot to the current inactive slot after a reboot!\nOnly use this option after OTA is done.\nContinue?</string>
|
||||
<string name="install_next">Next</string>
|
||||
@@ -171,24 +170,23 @@
|
||||
<string name="allowlist_restore_failed">Allowlist restore failed: %1$s</string>
|
||||
<string name="backup_allowlist">Backup Allowlist</string>
|
||||
<string name="restore_allowlist">Restore Allowlist</string>
|
||||
<string name="settings_custom_background">settings custom background</string>
|
||||
<string name="settings_custom_background_summary">settings custom background summary</string>
|
||||
<string name="settings_card_manage">card manage</string>
|
||||
<string name="settings_card_alpha">card alpha</string>
|
||||
<string name="settings_restore_default">restore default</string>
|
||||
<string name="settings_custom_background">Custom App Background</string>
|
||||
<string name="settings_custom_background_summary">Select an image as background</string>
|
||||
<string name="settings_card_alpha">Navigation bar transparency</string>
|
||||
<string name="settings_restore_default">Restore default</string>
|
||||
<string name="home_android_version">Android version</string>
|
||||
<string name="home_device_model">device model</string>
|
||||
<string name="home_device_model">Device model</string>
|
||||
<string name="su_not_allowed">Granting superuser to %s is not allowed</string>
|
||||
<string name="settings_disable_su">Disable su compatibility</string>
|
||||
<string name="settings_disable_su_summary">Temporarily disable any applications from obtaining root privileges via the su command (existing root processes will not be affected).</string>
|
||||
<string name="using_mksu_manager">You are using the SukiSU Beta manager</string>
|
||||
<string name="module_install_multiple_confirm">Are you sure you want to install the selected %d modules?</string>
|
||||
<string name="module_install_multiple_confirm_with_names">Sure you want to install the following %1$d modules? \n\n%2$s</string>
|
||||
<string name="more_settings">more settings</string>
|
||||
<string name="more_settings">More settings</string>
|
||||
<string name="selinux">SELinux</string>
|
||||
<string name="selinux_enabled">Enabled</string>
|
||||
<string name="selinux_disabled">Disabled</string>
|
||||
<string name="simple_mode">simplicity mode</string>
|
||||
<string name="simple_mode">Simplicity mode</string>
|
||||
<string name="simple_mode_summary">Hides unnecessary cards when turned on</string>
|
||||
<string name="hide_kernel_kernelsu_version">Hide kernel version</string>
|
||||
<string name="hide_kernel_kernelsu_version_summary">Hide kernel version</string>
|
||||
@@ -196,74 +194,134 @@
|
||||
<string name="hide_other_info_summary">Hides information about the number of super users, modules and KPM modules on the home page</string>
|
||||
<string name="hide_susfs_status">Hide SuSFS status</string>
|
||||
<string name="hide_susfs_status_summary">Hide SuSFS status information on the home page</string>
|
||||
<string name="theme_mode">Theme Mode</string>
|
||||
<string name="theme_follow_system">follow-up system</string>
|
||||
<string name="theme_light">light color</string>
|
||||
<string name="theme_dark">dark colored</string>
|
||||
<string name="hide_link_card">Hide Link Card Status</string>
|
||||
<string name="hide_link_card_summary">Hide link card information on the home page</string>
|
||||
<string name="theme_mode">Theme</string>
|
||||
<string name="theme_follow_system">Follow system</string>
|
||||
<string name="theme_light">Light</string>
|
||||
<string name="theme_dark">Dark</string>
|
||||
<string name="manual_hook">Manual Hook</string>
|
||||
<string name="dynamic_color_title">Dynamic colours</string>
|
||||
<string name="dynamic_color_summary">Dynamic colours using system themes</string>
|
||||
<string name="choose_theme_color">Choose a theme colour</string>
|
||||
<string name="color_default">White</string>
|
||||
<string name="color_blue">blue</string>
|
||||
<string name="color_green">green</string>
|
||||
<string name="color_purple">purple</string>
|
||||
<string name="color_orange">orange</string>
|
||||
<string name="color_pink">pink</string>
|
||||
<string name="color_gray">gray</string>
|
||||
<string name="color_ivory">ivory</string>
|
||||
<string name="color_default">Blue</string>
|
||||
<string name="color_green">Green</string>
|
||||
<string name="color_purple">Purple</string>
|
||||
<string name="color_orange">Orange</string>
|
||||
<string name="color_pink">Pink</string>
|
||||
<string name="color_gray">Gray</string>
|
||||
<string name="color_ivory">Ivory</string>
|
||||
<string name="flash_option">Brush Options</string>
|
||||
<string name="flash_option_tip">Select the file to be flashed</string>
|
||||
<string name="horizon_kernel">Anykernel3 Flash</string>
|
||||
<string name="horizon_kernel">Install Anykernel3</string>
|
||||
<string name="horizon_kernel_summary">Flash AnyKernel3 kernel file</string>
|
||||
<string name="root_required">Requires root privileges</string>
|
||||
<string name="copy_failed">File Copy Failure</string>
|
||||
<string name="reboot_complete_title">Scrubbing complete</string>
|
||||
<string name="reboot_complete_msg">Whether to reboot immediately?</string>
|
||||
<string name="yes">yes</string>
|
||||
<string name="no">no</string>
|
||||
<string name="yes">Yes</string>
|
||||
<string name="no">No</string>
|
||||
<string name="failed_reboot">Reboot Failed</string>
|
||||
<string name="batch_authorization">bulk license</string>
|
||||
<string name="batch_cancel_authorization">Batch cancel authorization</string>
|
||||
<string name="batch_authorization">empower</string>
|
||||
<string name="batch_cancel_authorization">withdraw</string>
|
||||
<string name="backup">Backup</string>
|
||||
<string name="color_yellow">Yellow</string>
|
||||
<string name="kpm">kpm module</string>
|
||||
<string name="kpm_title">kernel module</string>
|
||||
<string name="kpm_title">KPM</string>
|
||||
<string name="kpm_empty">No installed kernel modules at this time</string>
|
||||
<string name="kpm_version">releases</string>
|
||||
<string name="kpm_author">author</string>
|
||||
<string name="kpm_uninstall">uninstallation</string>
|
||||
<string name="kpm_version">Version</string>
|
||||
<string name="kpm_author">Author</string>
|
||||
<string name="kpm_uninstall">Uninstall</string>
|
||||
<string name="kpm_uninstall_success">Uninstalled successfully</string>
|
||||
<string name="kpm_uninstall_failed">Failed to uninstall</string>
|
||||
<string name="kpm_install">Select Installation</string>
|
||||
<string name="kpm_install">Install</string>
|
||||
<string name="kpm_install_success">Load of kpm module successful</string>
|
||||
<string name="kpm_install_failed">Load of kpm module failed</string>
|
||||
<string name="kpm_args">kpm parameters</string>
|
||||
<string name="kpm_control">fulfillment</string>
|
||||
<string name="kpm_args">Parameters</string>
|
||||
<string name="kpm_control">Execute</string>
|
||||
<string name="home_kpm_version">KPM Version</string>
|
||||
<string name="close_notice">close</string>
|
||||
<string name="close_notice">Close</string>
|
||||
<string name="kernel_module_notice">The following kernel module functions were developed by KernelPatch and modified to include the kernel module functions of SukiSU Ultra</string>
|
||||
<string name="home_ContributionCard_kernelsu">SukiSU Ultra Look forward to</string>
|
||||
<string name="kpm_control_success">success</string>
|
||||
<string name="kpm_control_failed">failed</string>
|
||||
<string name="home_click_to_ContributionCard_kernelsu">SukiSU Ultra will be a relatively independent branch of KSU in the future, but thanks to the official KernelSU and MKSU etc. for their contributions!</string>
|
||||
<string name="not_supported">unsupported</string>
|
||||
<string name="supported">supported</string>
|
||||
<string name="home_kpm_module">Number of KPM modules:%d </string>
|
||||
<string name="kpm_control_success">Success</string>
|
||||
<string name="kpm_control_failed">Failed</string>
|
||||
<string name="home_click_to_ContributionCard_kernelsu">SukiSU Ultra will be a relatively independent branch of KSU in the future, but we still appreciate the official KernelSU and MKSU etc. for their contributions!</string>
|
||||
<string name="not_supported">Unsupported</string>
|
||||
<string name="supported">Supported</string>
|
||||
<string name="home_kpm_module">"Number of KPM modules: %d "</string>
|
||||
<string name="kpm_invalid_file">Invalid KPM file</string>
|
||||
<string name="kernel_patched">Kernel not patched</string>
|
||||
<string name="kernel_not_enabled">Kernel not configured</string>
|
||||
<string name="custom_settings">Custom settings</string>
|
||||
<string name="kpm_install_mode">install</string>
|
||||
<string name="kpm_install_mode">KPM Install</string>
|
||||
<string name="kpm_install_mode_load">Load</string>
|
||||
<string name="kpm_install_mode_embed">embed</string>
|
||||
<string name="kpm_install_mode_description">Please select the module installation mode: \n\nLoad: Temporarily load the module \nEmbedded: Permanently install into the system</string>
|
||||
<string name="kpm_install_mode_embed">Embed</string>
|
||||
<string name="kpm_install_mode_description">Please select: %1\$s Module Installation Mode \n\nLoad: Temporarily load the module \nEmbedded: Permanently install into the system</string>
|
||||
<string name="log_failed_to_check_module_file">Failed to check module file existence</string>
|
||||
<string name="snackbar_failed_to_check_module_file">Unable to check if module file exists</string>
|
||||
<string name="confirm_uninstall_title">Confirm uninstallation</string>
|
||||
<string name="confirm_uninstall_confirm">removing</string>
|
||||
<string name="confirm_uninstall_dismiss">abolish</string>
|
||||
<string name="confirm_uninstall_confirm">Uninstall</string>
|
||||
<string name="confirm_uninstall_dismiss">Cancel</string>
|
||||
<string name="theme_color">Theme Color</string>
|
||||
<string name="invalid_file_type">Incorrect file type, select .kpm file</string>
|
||||
<string name="confirm_uninstall_title_with_filename">uninstallation</string>
|
||||
<string name="confirm_uninstall_content">The following kpm modules will be uninstalled:\n%s</string>
|
||||
<string name="invalid_file_type">Incorrect file type! Please select .kpm file.</string>
|
||||
<string name="confirm_uninstall_title_with_filename">Uninstall</string>
|
||||
<string name="confirm_uninstall_content">The following KPM will be uninstalled: %s</string>
|
||||
<string name="settings_susfs_toggle_summary">Disable kprobe hooks created by KernelSU, using inline hooks instead, which is similar to non-GKI kernel hooking method.</string>
|
||||
<string name="image_editor_title">Adjust background image</string>
|
||||
<string name="image_editor_hint">Use two fingers to zoom the image, and one finger to drag it to adjust the position</string>
|
||||
<string name="background_image_error">Could not load image</string>
|
||||
<string name="reprovision">Reprovision</string>
|
||||
<!-- Kernel Flash Progress Related -->
|
||||
<string name="horizon_flash_title">Kernel Flashing</string>
|
||||
<string name="horizon_logs_label">Logs:</string>
|
||||
<string name="horizon_flash_complete">Flash Complete</string>
|
||||
<!-- Flash Status Related -->
|
||||
<string name="horizon_preparing">Preparing…</string>
|
||||
<string name="horizon_cleaning_files">Cleaning files…</string>
|
||||
<string name="horizon_copying_files">Copying files…</string>
|
||||
<string name="horizon_extracting_tool">Extracting flash tool…</string>
|
||||
<string name="horizon_patching_script">Patching flash script…</string>
|
||||
<string name="horizon_flashing">Flashing kernel…</string>
|
||||
<string name="horizon_flash_complete_status">Flash completed</string>
|
||||
<!-- Slot selection related strings -->
|
||||
<string name="select_slot_title">Select Flash Slot</string>
|
||||
<string name="select_slot_description">Please select the target slot for flashing boot</string>
|
||||
<string name="slot_a">Slot A</string>
|
||||
<string name="slot_b">Slot B</string>
|
||||
<string name="selected_slot">Selected slot: %1$s</string>
|
||||
<string name="horizon_getting_original_slot">Getting the original slot</string>
|
||||
<string name="horizon_setting_target_slot">Setting the specified slot</string>
|
||||
<string name="horizon_restoring_original_slot">Restore Default Slot</string>
|
||||
<string name="current_slot">Current Slot:%1$s </string>
|
||||
<!-- Error Messages -->
|
||||
<string name="horizon_copy_failed">Copy failed</string>
|
||||
<string name="horizon_unknown_error">Unknown error</string>
|
||||
<string name="flash_failed_message">Flash failed</string>
|
||||
<!-- lkm/gki install -->
|
||||
<string name="Lkm_install_methods">LKM repair/installation</string>
|
||||
<string name="GKI_install_methods">GKI/non-GKI installation</string>
|
||||
<string name="kernel_version_log">Kernel version:%1$s</string>
|
||||
<string name="tool_version_log">Using the patching tool:%1$s</string>
|
||||
<string name="configuration">Configure</string>
|
||||
<string name="app_settings">Application Settings</string>
|
||||
<string name="tools">Tools</string>
|
||||
<string name="currently_selected">Currently</string>
|
||||
<!-- String resources used in SuperUser -->
|
||||
<string name="clear">Removals</string>
|
||||
<string name="apps_with_root">Root Application of permissions</string>
|
||||
<string name="apps_with_custom_profile">Customized Configuration Application</string>
|
||||
<string name="other_apps">Other Applications</string>
|
||||
<string name="no_apps_found">Application not found</string>
|
||||
<string name="selinux_enabled_toast">SELinux Enabled</string>
|
||||
<string name="selinux_disabled_toast">SELinux Disabled</string>
|
||||
<string name="selinux_change_failed">SELinux Status change failed</string>
|
||||
<string name="advanced_settings">Advanced Settings</string>
|
||||
<string name="appearance_settings">Customize the toolbar</string>
|
||||
<string name="back">Comeback</string>
|
||||
<string name="expand">Be in full swing</string>
|
||||
<string name="collapse">put away</string>
|
||||
<string name="susfs_enabled">SuSFS enabled</string>
|
||||
<string name="susfs_disabled">SuSFS disabled</string>
|
||||
<string name="background_set_success">Background set successfully</string>
|
||||
<string name="background_removed">Removed custom backgrounds</string>
|
||||
<string name="root_require_for_install">Requires root privileges</string>
|
||||
</resources>
|
||||
|
||||
@@ -28,8 +28,8 @@ cmaker {
|
||||
}
|
||||
|
||||
val androidMinSdkVersion = 26
|
||||
val androidTargetSdkVersion = 35
|
||||
val androidCompileSdkVersion = 35
|
||||
val androidTargetSdkVersion = 36
|
||||
val androidCompileSdkVersion = 36
|
||||
val androidCompileNdkVersion = "28.0.13004108"
|
||||
val androidSourceCompatibility = JavaVersion.VERSION_21
|
||||
val androidTargetCompatibility = JavaVersion.VERSION_21
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user